Repository: Texera/texera Branch: main Commit: 2b763e442d3b Files: 1986 Total size: 25.6 MB Directory structure: gitextract_mvod0m3k/ ├── .asf.yaml ├── .dockerignore ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-template.yaml │ │ ├── feature-template.yaml │ │ └── task-template.yaml │ ├── PULL_REQUEST_TEMPLATE │ ├── labeler.yml │ ├── release/ │ │ └── vote-email-template.md │ ├── scripts/ │ │ ├── compose-backport-message.py │ │ └── prepare-backport-checkout.sh │ └── workflows/ │ ├── auto-queue.yml │ ├── automatic-email-notif-on-ddl-change.yml │ ├── build-and-push-images.yml │ ├── build.yml │ ├── check-header.yml │ ├── comment-commands.yml │ ├── create-release-candidate.yml │ ├── direct-backport-push.yml │ ├── license-binary-checker.yml │ ├── lint-pr.yml │ ├── pr-assignment.yml │ ├── pr-labeler.yml │ └── required-checks.yml ├── .gitignore ├── .jvmopts ├── .licenserc.yaml ├── .run/ │ ├── AccessControlService.run.xml │ ├── ComputingUnitManagingService.run.xml │ ├── ComputingUnitMaster.run.xml │ ├── ComputingUnitWorker.run.xml │ ├── ConfigService.run.xml │ ├── FileService.run.xml │ ├── TexeraWebApplication.run.xml │ ├── WorkflowCompilingService.run.xml │ ├── frontend.run.xml │ ├── texera micro services.run.xml │ └── texera-lakefs.run.xml ├── .scalafix.conf ├── .scalafmt.conf ├── AGENTS.md ├── CLAUDE.md ├── CONTRIBUTING.md ├── DISCLAIMER ├── LICENSE ├── NOTICE ├── README.md ├── SECURITY.md ├── access-control-service/ │ ├── LICENSE-binary │ ├── NOTICE-binary │ ├── build.sbt │ ├── project/ │ │ └── build.properties │ └── src/ │ ├── main/ │ │ ├── resources/ │ │ │ ├── access-control-service-web-config.yaml │ │ │ └── logback.xml │ │ └── scala/ │ │ └── org/ │ │ └── apache/ │ │ └── texera/ │ │ └── service/ │ │ ├── AccessControlService.scala │ │ ├── AccessControlServiceConfiguration.scala │ │ ├── activity/ │ │ │ └── UserActivityEventListener.scala │ │ └── resource/ │ │ ├── AccessControlResource.scala │ │ └── HealthCheckResource.scala │ └── test/ │ └── scala/ │ └── org/ │ └── apache/ │ └── texera/ │ ├── AccessControlResourceSpec.scala │ └── service/ │ ├── AccessControlServiceRunSpec.scala │ └── activity/ │ └── UserActivityEventListenerSpec.scala ├── agent-service/ │ ├── .dockerignore │ ├── .prettierrc │ ├── LICENSE-binary │ ├── bin/ │ │ └── collect-licenses.ts │ ├── package.json │ ├── src/ │ │ ├── agent/ │ │ │ ├── index.ts │ │ │ ├── prompts.ts │ │ │ ├── texera-agent.ts │ │ │ ├── tools/ │ │ │ │ ├── index.ts │ │ │ │ ├── result-formatting.test.ts │ │ │ │ ├── result-formatting.ts │ │ │ │ ├── tools-utility.test.ts │ │ │ │ ├── tools-utility.ts │ │ │ │ ├── workflow-crud-tools.ts │ │ │ │ └── workflow-execution-tools.ts │ │ │ ├── util/ │ │ │ │ ├── auto-layout.test.ts │ │ │ │ ├── auto-layout.ts │ │ │ │ ├── context-utils.ts │ │ │ │ ├── workflow-system-metadata.ts │ │ │ │ └── workflow-utils.ts │ │ │ ├── workflow-result-state.test.ts │ │ │ ├── workflow-result-state.ts │ │ │ ├── workflow-state.test.ts │ │ │ └── workflow-state.ts │ │ ├── api/ │ │ │ ├── auth-api.ts │ │ │ ├── backend-api.ts │ │ │ ├── compile-api.ts │ │ │ ├── execution-api.ts │ │ │ ├── index.ts │ │ │ └── workflow-api.ts │ │ ├── config/ │ │ │ └── env.ts │ │ ├── index.ts │ │ ├── logger.ts │ │ ├── server.test.ts │ │ ├── server.ts │ │ └── types/ │ │ ├── agent.ts │ │ ├── execution.ts │ │ ├── index.ts │ │ └── workflow.ts │ └── tsconfig.json ├── amber/ │ ├── .scalafix.conf │ ├── .scalafmt.conf │ ├── DESCRIPTION │ ├── LICENSE-binary-java │ ├── LICENSE-binary-python │ ├── NOTICE-binary │ ├── README.md │ ├── build.sbt │ ├── dev-requirements.txt │ ├── operator-requirements.txt │ ├── project/ │ │ ├── build.properties │ │ └── plugins.sbt │ ├── pyproject.toml │ ├── requirements.txt │ └── src/ │ ├── main/ │ │ ├── protobuf/ │ │ │ └── org/ │ │ │ └── apache/ │ │ │ └── texera/ │ │ │ └── amber/ │ │ │ └── engine/ │ │ │ ├── architecture/ │ │ │ │ ├── rpc/ │ │ │ │ │ ├── controlcommands.proto │ │ │ │ │ ├── controllerservice.proto │ │ │ │ │ ├── controlreturns.proto │ │ │ │ │ ├── testerservice.proto │ │ │ │ │ └── workerservice.proto │ │ │ │ ├── sendsemantics/ │ │ │ │ │ └── partitionings.proto │ │ │ │ └── worker/ │ │ │ │ └── statistics.proto │ │ │ └── common/ │ │ │ ├── actormessage.proto │ │ │ ├── ambermessage.proto │ │ │ └── executionruntimestate.proto │ │ ├── python/ │ │ │ ├── core/ │ │ │ │ ├── __init__.py │ │ │ │ ├── architecture/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── handlers/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── actorcommand/ │ │ │ │ │ │ │ ├── actor_handler_base.py │ │ │ │ │ │ │ ├── backpressure_handler.py │ │ │ │ │ │ │ └── credit_update_handler.py │ │ │ │ │ │ └── control/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── add_input_channel_handler.py │ │ │ │ │ │ ├── add_partitioning_handler.py │ │ │ │ │ │ ├── assign_port_handler.py │ │ │ │ │ │ ├── control_handler_base.py │ │ │ │ │ │ ├── debug_command_handler.py │ │ │ │ │ │ ├── end_channel_handler.py │ │ │ │ │ │ ├── end_worker_handler.py │ │ │ │ │ │ ├── evaluate_expression_handler.py │ │ │ │ │ │ ├── initialize_executor_handler.py │ │ │ │ │ │ ├── no_operation_handler.py │ │ │ │ │ │ ├── open_executor_handler.py │ │ │ │ │ │ ├── pause_worker_handler.py │ │ │ │ │ │ ├── query_statistics_handler.py │ │ │ │ │ │ ├── replay_current_tuple_handler.py │ │ │ │ │ │ ├── resume_worker_handler.py │ │ │ │ │ │ ├── start_channel_handler.py │ │ │ │ │ │ ├── start_worker_handler.py │ │ │ │ │ │ └── update_executor_handler.py │ │ │ │ │ ├── managers/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── console_message_manager.py │ │ │ │ │ │ ├── context.py │ │ │ │ │ │ ├── debug_manager.py │ │ │ │ │ │ ├── embedded_control_message_manager.py │ │ │ │ │ │ ├── exception_manager.py │ │ │ │ │ │ ├── executor_manager.py │ │ │ │ │ │ ├── pause_manager.py │ │ │ │ │ │ ├── state_manager.py │ │ │ │ │ │ ├── state_processing_manager.py │ │ │ │ │ │ ├── statistics_manager.py │ │ │ │ │ │ └── tuple_processing_manager.py │ │ │ │ │ ├── packaging/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── input_manager.py │ │ │ │ │ │ └── output_manager.py │ │ │ │ │ ├── rpc/ │ │ │ │ │ │ ├── async_rpc_client.py │ │ │ │ │ │ ├── async_rpc_handler_initializer.py │ │ │ │ │ │ └── async_rpc_server.py │ │ │ │ │ └── sendsemantics/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── broad_cast_partitioner.py │ │ │ │ │ ├── hash_based_shuffle_partitioner.py │ │ │ │ │ ├── one_to_one_partitioner.py │ │ │ │ │ ├── partitioner.py │ │ │ │ │ ├── range_based_shuffle_partitioner.py │ │ │ │ │ └── round_robin_partitioner.py │ │ │ │ ├── models/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── batch.py │ │ │ │ │ ├── internal_marker.py │ │ │ │ │ ├── internal_queue.py │ │ │ │ │ ├── operator.py │ │ │ │ │ ├── payload.py │ │ │ │ │ ├── schema/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── arrow_schema_utils.py │ │ │ │ │ │ ├── attribute_type.py │ │ │ │ │ │ ├── attribute_type_utils.py │ │ │ │ │ │ ├── field.py │ │ │ │ │ │ └── schema.py │ │ │ │ │ ├── single_blocking_io.py │ │ │ │ │ ├── state.py │ │ │ │ │ ├── table.py │ │ │ │ │ ├── tuple.py │ │ │ │ │ └── type/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── large_binary.py │ │ │ │ ├── proxy/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── proxy_client.py │ │ │ │ │ └── proxy_server.py │ │ │ │ ├── python_worker.py │ │ │ │ ├── runnables/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── data_processor.py │ │ │ │ │ ├── heartbeat.py │ │ │ │ │ ├── main_loop.py │ │ │ │ │ ├── network_receiver.py │ │ │ │ │ └── network_sender.py │ │ │ │ ├── storage/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── document_factory.py │ │ │ │ │ ├── iceberg/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── iceberg_catalog_instance.py │ │ │ │ │ │ ├── iceberg_document.py │ │ │ │ │ │ ├── iceberg_table_writer.py │ │ │ │ │ │ └── iceberg_utils.py │ │ │ │ │ ├── model/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── buffered_item_writer.py │ │ │ │ │ │ ├── readonly_virtual_document.py │ │ │ │ │ │ └── virtual_document.py │ │ │ │ │ ├── runnables/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── input_port_materialization_reader_runnable.py │ │ │ │ │ │ └── port_storage_writer.py │ │ │ │ │ ├── storage_config.py │ │ │ │ │ └── vfs_uri_factory.py │ │ │ │ └── util/ │ │ │ │ ├── __init__.py │ │ │ │ ├── atomic.py │ │ │ │ ├── base_protocols.py │ │ │ │ ├── buffer/ │ │ │ │ │ ├── buffer_base.py │ │ │ │ │ └── timed_buffer.py │ │ │ │ ├── console_message/ │ │ │ │ │ ├── replace_print.py │ │ │ │ │ ├── timed_buffer.py │ │ │ │ │ └── timestamp.py │ │ │ │ ├── customized_queue/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── double_blocking_queue.py │ │ │ │ │ ├── inner.py │ │ │ │ │ ├── linked_blocking_multi_queue.py │ │ │ │ │ └── queue_base.py │ │ │ │ ├── expression_evaluator.py │ │ │ │ ├── proto/ │ │ │ │ │ └── __init__.py │ │ │ │ ├── runnable.py │ │ │ │ ├── stoppable/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── stoppable.py │ │ │ │ │ └── stoppable_queue_blocking_thread.py │ │ │ │ └── virtual_identity.py │ │ │ ├── proto/ │ │ │ │ ├── __init__.py │ │ │ │ ├── org/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── apache/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── texera/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── amber/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── core/ │ │ │ │ │ │ │ └── __init__.py │ │ │ │ │ │ └── engine/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── architecture/ │ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ │ ├── rpc/ │ │ │ │ │ │ │ │ └── __init__.py │ │ │ │ │ │ │ ├── sendsemantics/ │ │ │ │ │ │ │ │ └── __init__.py │ │ │ │ │ │ │ └── worker/ │ │ │ │ │ │ │ └── __init__.py │ │ │ │ │ │ └── common/ │ │ │ │ │ │ └── __init__.py │ │ │ │ │ └── web/ │ │ │ │ │ └── __init__.py │ │ │ │ └── scalapb/ │ │ │ │ └── __init__.py │ │ │ ├── pyamber/ │ │ │ │ └── __init__.py │ │ │ ├── pytexera/ │ │ │ │ ├── __init__.py │ │ │ │ ├── storage/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── dataset_file_document.py │ │ │ │ │ ├── large_binary_input_stream.py │ │ │ │ │ ├── large_binary_manager.py │ │ │ │ │ └── large_binary_output_stream.py │ │ │ │ └── udf/ │ │ │ │ ├── __init__.py │ │ │ │ ├── examples/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── count_batch_operator.py │ │ │ │ │ ├── echo_operator.py │ │ │ │ │ ├── echo_table_operator.py │ │ │ │ │ ├── generator_operator_binary.py │ │ │ │ │ ├── generator_operator_integer.py │ │ │ │ │ ├── join_operator.py │ │ │ │ │ └── rudf/ │ │ │ │ │ ├── r_table_operator.py │ │ │ │ │ └── r_tuple_operator.py │ │ │ │ └── udf_operator.py │ │ │ └── texera_run_python_worker.py │ │ ├── resources/ │ │ │ ├── cache.ccf │ │ │ ├── computing-unit-master-config.yml │ │ │ ├── logback.xml │ │ │ ├── texera-compiling-service-web-config.yml │ │ │ └── web-config.yml │ │ └── scala/ │ │ └── org/ │ │ └── apache/ │ │ └── texera/ │ │ ├── amber/ │ │ │ ├── clustering/ │ │ │ │ ├── ClusterListener.scala │ │ │ │ └── SingleNodeListener.scala │ │ │ ├── engine/ │ │ │ │ ├── architecture/ │ │ │ │ │ ├── common/ │ │ │ │ │ │ ├── AmberProcessor.scala │ │ │ │ │ │ ├── ExecutorDeployment.scala │ │ │ │ │ │ ├── PekkoActorRefMappingService.scala │ │ │ │ │ │ ├── PekkoActorService.scala │ │ │ │ │ │ ├── PekkoMessageTransferService.scala │ │ │ │ │ │ ├── ProcessingStepCursor.scala │ │ │ │ │ │ └── WorkflowActor.scala │ │ │ │ │ ├── controller/ │ │ │ │ │ │ ├── ClientEvent.scala │ │ │ │ │ │ ├── Controller.scala │ │ │ │ │ │ ├── ControllerAsyncRPCHandlerInitializer.scala │ │ │ │ │ │ ├── ControllerProcessor.scala │ │ │ │ │ │ ├── ControllerTimerService.scala │ │ │ │ │ │ ├── GlobalReplayManager.scala │ │ │ │ │ │ ├── Workflow.scala │ │ │ │ │ │ ├── WorkflowScheduler.scala │ │ │ │ │ │ ├── execution/ │ │ │ │ │ │ │ ├── ChannelExecution.scala │ │ │ │ │ │ │ ├── ExecutionUtils.scala │ │ │ │ │ │ │ ├── LinkExecution.scala │ │ │ │ │ │ │ ├── OperatorExecution.scala │ │ │ │ │ │ │ ├── RegionExecution.scala │ │ │ │ │ │ │ ├── WorkerPortExecution.scala │ │ │ │ │ │ │ └── WorkflowExecution.scala │ │ │ │ │ │ └── promisehandlers/ │ │ │ │ │ │ ├── ConsoleMessageHandler.scala │ │ │ │ │ │ ├── DebugCommandHandler.scala │ │ │ │ │ │ ├── EmbeddedControlMessageHandler.scala │ │ │ │ │ │ ├── EvaluatePythonExpressionHandler.scala │ │ │ │ │ │ ├── JumpToOperatorRegionHandler.scala │ │ │ │ │ │ ├── LinkWorkersHandler.scala │ │ │ │ │ │ ├── PauseHandler.scala │ │ │ │ │ │ ├── PortCompletedHandler.scala │ │ │ │ │ │ ├── QueryWorkerStatisticsHandler.scala │ │ │ │ │ │ ├── ReconfigurationHandler.scala │ │ │ │ │ │ ├── ResumeHandler.scala │ │ │ │ │ │ ├── RetrieveWorkflowStateHandler.scala │ │ │ │ │ │ ├── RetryWorkflowHandler.scala │ │ │ │ │ │ ├── StartWorkflowHandler.scala │ │ │ │ │ │ ├── TakeGlobalCheckpointHandler.scala │ │ │ │ │ │ ├── WorkerExecutionCompletedHandler.scala │ │ │ │ │ │ └── WorkerStateUpdatedHandler.scala │ │ │ │ │ ├── deploysemantics/ │ │ │ │ │ │ ├── AddressInfo.scala │ │ │ │ │ │ ├── deploystrategy/ │ │ │ │ │ │ │ ├── DeployStrategy.scala │ │ │ │ │ │ │ ├── OneOnEach.scala │ │ │ │ │ │ │ ├── RandomDeployment.scala │ │ │ │ │ │ │ └── RoundRobinDeployment.scala │ │ │ │ │ │ └── layer/ │ │ │ │ │ │ └── WorkerExecution.scala │ │ │ │ │ ├── logreplay/ │ │ │ │ │ │ ├── AsyncReplayLogWriter.scala │ │ │ │ │ │ ├── EmptyReplayLogger.scala │ │ │ │ │ │ ├── OrderEnforcer.scala │ │ │ │ │ │ ├── ReplayLogGenerator.scala │ │ │ │ │ │ ├── ReplayLogManager.scala │ │ │ │ │ │ ├── ReplayLogger.scala │ │ │ │ │ │ ├── ReplayLoggerImpl.scala │ │ │ │ │ │ └── ReplayOrderEnforcer.scala │ │ │ │ │ ├── messaginglayer/ │ │ │ │ │ │ ├── AmberFIFOChannel.scala │ │ │ │ │ │ ├── CongestionControl.scala │ │ │ │ │ │ ├── DeadLetterMonitorActor.scala │ │ │ │ │ │ ├── FlowControl.scala │ │ │ │ │ │ ├── InputGateway.scala │ │ │ │ │ │ ├── InputManager.scala │ │ │ │ │ │ ├── NetworkInputGateway.scala │ │ │ │ │ │ ├── NetworkOutputGateway.scala │ │ │ │ │ │ ├── OrderingEnforcer.scala │ │ │ │ │ │ ├── OutputManager.scala │ │ │ │ │ │ ├── WorkerPort.scala │ │ │ │ │ │ └── WorkerTimerService.scala │ │ │ │ │ ├── pythonworker/ │ │ │ │ │ │ ├── PythonProxyClient.scala │ │ │ │ │ │ ├── PythonProxyServer.scala │ │ │ │ │ │ ├── PythonWorkflowWorker.scala │ │ │ │ │ │ └── WorkerBatchInternalQueue.scala │ │ │ │ │ ├── scheduling/ │ │ │ │ │ │ ├── CostBasedScheduleGenerator.scala │ │ │ │ │ │ ├── CostEstimator.scala │ │ │ │ │ │ ├── ExpansionGreedyScheduleGenerator.scala │ │ │ │ │ │ ├── Region.scala │ │ │ │ │ │ ├── RegionExecutionCoordinator.scala │ │ │ │ │ │ ├── RegionPlan.scala │ │ │ │ │ │ ├── Schedule.scala │ │ │ │ │ │ ├── ScheduleGenerator.scala │ │ │ │ │ │ ├── SchedulingUtils.scala │ │ │ │ │ │ ├── WorkflowExecutionCoordinator.scala │ │ │ │ │ │ ├── config/ │ │ │ │ │ │ │ ├── ChannelConfig.scala │ │ │ │ │ │ │ ├── LinkConfig.scala │ │ │ │ │ │ │ ├── OperatorConfig.scala │ │ │ │ │ │ │ ├── PortConfig.scala │ │ │ │ │ │ │ ├── ResourceConfig.scala │ │ │ │ │ │ │ └── WorkerConfig.scala │ │ │ │ │ │ └── resourcePolicies/ │ │ │ │ │ │ ├── ExecutionClusterInfo.scala │ │ │ │ │ │ └── ResourceAllocator.scala │ │ │ │ │ ├── sendsemantics/ │ │ │ │ │ │ └── partitioners/ │ │ │ │ │ │ ├── BroadcastPartitioner.scala │ │ │ │ │ │ ├── HashBasedShufflePartitioner.scala │ │ │ │ │ │ ├── OneToOnePartitioner.scala │ │ │ │ │ │ ├── Partitioner.scala │ │ │ │ │ │ ├── RangeBasedShufflePartitioner.scala │ │ │ │ │ │ └── RoundRobinPartitioner.scala │ │ │ │ │ └── worker/ │ │ │ │ │ ├── DPThread.scala │ │ │ │ │ ├── DataProcessor.scala │ │ │ │ │ ├── DataProcessorRPCHandlerInitializer.scala │ │ │ │ │ ├── EmbeddedControlMessageManager.scala │ │ │ │ │ ├── PauseManager.scala │ │ │ │ │ ├── PauseType.scala │ │ │ │ │ ├── WorkflowWorker.scala │ │ │ │ │ ├── managers/ │ │ │ │ │ │ ├── InputPortMaterializationReaderThread.scala │ │ │ │ │ │ ├── OutputPortResultWriterThread.scala │ │ │ │ │ │ ├── SerializationManager.scala │ │ │ │ │ │ └── StatisticsManager.scala │ │ │ │ │ └── promisehandlers/ │ │ │ │ │ ├── AddInputChannelHandler.scala │ │ │ │ │ ├── AddPartitioningHandler.scala │ │ │ │ │ ├── AssignPortHandler.scala │ │ │ │ │ ├── EndChannelHandler.scala │ │ │ │ │ ├── EndHandler.scala │ │ │ │ │ ├── FinalizeCheckpointHandler.scala │ │ │ │ │ ├── FlushNetworkBufferHandler.scala │ │ │ │ │ ├── InitializeExecutorHandler.scala │ │ │ │ │ ├── OpenExecutorHandler.scala │ │ │ │ │ ├── PauseHandler.scala │ │ │ │ │ ├── PrepareCheckpointHandler.scala │ │ │ │ │ ├── QueryStatisticsHandler.scala │ │ │ │ │ ├── ResumeHandler.scala │ │ │ │ │ ├── RetrieveStateHandler.scala │ │ │ │ │ ├── StartChannelHandler.scala │ │ │ │ │ ├── StartHandler.scala │ │ │ │ │ └── UpdateExecutorHandler.scala │ │ │ │ └── common/ │ │ │ │ ├── AmberConfig.scala │ │ │ │ ├── AmberKryoInitializer.scala │ │ │ │ ├── AmberLogging.scala │ │ │ │ ├── AmberRuntime.scala │ │ │ │ ├── CheckpointState.scala │ │ │ │ ├── CheckpointSupport.scala │ │ │ │ ├── ElidableStatement.scala │ │ │ │ ├── FriesReconfigurationAlgorithm.scala │ │ │ │ ├── FutureBijection.scala │ │ │ │ ├── SerializedState.scala │ │ │ │ ├── Utils.scala │ │ │ │ ├── ambermessage/ │ │ │ │ │ ├── DataPayload.scala │ │ │ │ │ ├── DirectControlMessagePayload.scala │ │ │ │ │ ├── RecoveryPayload.scala │ │ │ │ │ ├── WorkflowFIFOMessagePayload.scala │ │ │ │ │ └── WorkflowMessage.scala │ │ │ │ ├── client/ │ │ │ │ │ ├── AmberClient.scala │ │ │ │ │ └── ClientActor.scala │ │ │ │ ├── rpc/ │ │ │ │ │ ├── AsyncRPCClient.scala │ │ │ │ │ ├── AsyncRPCHandlerInitializer.scala │ │ │ │ │ └── AsyncRPCServer.scala │ │ │ │ ├── statetransition/ │ │ │ │ │ ├── StateManager.scala │ │ │ │ │ └── WorkerStateManager.scala │ │ │ │ ├── storage/ │ │ │ │ │ ├── EmptyRecordStorage.scala │ │ │ │ │ ├── HDFSRecordStorage.scala │ │ │ │ │ ├── SequentialRecordStorage.scala │ │ │ │ │ └── VFSRecordStorage.scala │ │ │ │ └── virtualidentity/ │ │ │ │ └── util.scala │ │ │ └── error/ │ │ │ └── ErrorUtils.scala │ │ ├── web/ │ │ │ ├── ComputingUnitMaster.scala │ │ │ ├── ComputingUnitWorker.scala │ │ │ ├── ServletAwareConfigurator.scala │ │ │ ├── SessionState.scala │ │ │ ├── SubscriptionManager.scala │ │ │ ├── TexeraWebApplication.scala │ │ │ ├── TexeraWebConfiguration.java │ │ │ ├── WebsocketInput.scala │ │ │ ├── WorkflowLifecycleManager.scala │ │ │ ├── auth/ │ │ │ │ ├── GuestAuthFilter.scala │ │ │ │ ├── JwtAuth.scala │ │ │ │ ├── UserAuthenticator.scala │ │ │ │ └── UserRoleAuthorizer.scala │ │ │ ├── model/ │ │ │ │ ├── collab/ │ │ │ │ │ ├── event/ │ │ │ │ │ │ ├── CollabWebSocketEvent.scala │ │ │ │ │ │ ├── CommandEvent.scala │ │ │ │ │ │ ├── LockGrantedEvent.scala │ │ │ │ │ │ ├── LockRejectedEvent.scala │ │ │ │ │ │ ├── ReleaseLockEvent.scala │ │ │ │ │ │ ├── RestoreVersionEvent.scala │ │ │ │ │ │ └── WorkflowAccessEvent.scala │ │ │ │ │ ├── request/ │ │ │ │ │ │ ├── AcquireLockRequest.scala │ │ │ │ │ │ ├── CollabWebSocketRequest.scala │ │ │ │ │ │ ├── CommandRequest.scala │ │ │ │ │ │ ├── HeartBeatRequest.scala │ │ │ │ │ │ ├── RestoreVersionRequest.scala │ │ │ │ │ │ ├── TryLockRequest.scala │ │ │ │ │ │ └── WIdRequest.scala │ │ │ │ │ └── response/ │ │ │ │ │ └── HeartBeatResponse.scala │ │ │ │ ├── common/ │ │ │ │ │ └── AccessEntry.scala │ │ │ │ ├── http/ │ │ │ │ │ ├── request/ │ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ │ ├── UserLoginRequest.scala │ │ │ │ │ │ │ └── UserRegistrationRequest.scala │ │ │ │ │ │ └── result/ │ │ │ │ │ │ └── ResultExportRequest.scala │ │ │ │ │ └── response/ │ │ │ │ │ ├── SchemaPropagationResponse.scala │ │ │ │ │ ├── TokenIssueResponse.scala │ │ │ │ │ └── result/ │ │ │ │ │ └── ResultExportResponse.scala │ │ │ │ └── websocket/ │ │ │ │ ├── event/ │ │ │ │ │ ├── CacheStatusUpdateEvent.scala │ │ │ │ │ ├── ExecutionDurationUpdateEvent.scala │ │ │ │ │ ├── ExecutionStatusEnum.scala │ │ │ │ │ ├── OperatorStatisticsUpdateEvent.scala │ │ │ │ │ ├── PaginatedResultEvent.scala │ │ │ │ │ ├── RegionStateEvent.scala │ │ │ │ │ ├── TexeraWebSocketEvent.scala │ │ │ │ │ ├── WebResultUpdateEvent.scala │ │ │ │ │ ├── WorkerAssignmentUpdateEvent.scala │ │ │ │ │ ├── WorkflowAvailableResultEvent.scala │ │ │ │ │ ├── WorkflowErrorEvent.scala │ │ │ │ │ ├── WorkflowStateEvent.scala │ │ │ │ │ └── python/ │ │ │ │ │ └── ConsoleUpdateEvent.scala │ │ │ │ ├── request/ │ │ │ │ │ ├── EditingTimeCompilationRequest.scala │ │ │ │ │ ├── HeartBeatRequest.scala │ │ │ │ │ ├── ModifyLogicRequest.scala │ │ │ │ │ ├── ResultPaginationRequest.scala │ │ │ │ │ ├── RetryRequest.scala │ │ │ │ │ ├── SkipTupleRequest.scala │ │ │ │ │ ├── TexeraWebSocketRequest.scala │ │ │ │ │ ├── WorkflowCheckpointRequest.scala │ │ │ │ │ ├── WorkflowExecuteRequest.scala │ │ │ │ │ ├── WorkflowKillRequest.scala │ │ │ │ │ ├── WorkflowPauseRequest.scala │ │ │ │ │ ├── WorkflowResumeRequest.scala │ │ │ │ │ └── python/ │ │ │ │ │ ├── DebugCommandRequest.scala │ │ │ │ │ └── PythonExpressionEvaluateRequest.scala │ │ │ │ └── response/ │ │ │ │ ├── ClusterStatusUpdateEvent.scala │ │ │ │ ├── HeartBeatResponse.scala │ │ │ │ ├── ModifyLogicResponse.scala │ │ │ │ ├── RegionUpdateEvent.scala │ │ │ │ └── python/ │ │ │ │ └── PythonExpressionEvaluateResponse.scala │ │ │ ├── resource/ │ │ │ │ ├── CollaborationResource.scala │ │ │ │ ├── EmailTemplate.scala │ │ │ │ ├── GmailResource.scala │ │ │ │ ├── HealthCheckResource.scala │ │ │ │ ├── MockKillWorkerResource.scala │ │ │ │ ├── SuccessExecutionResult.scala │ │ │ │ ├── SyncExecutionResource.scala │ │ │ │ ├── SystemMetadataResource.scala │ │ │ │ ├── UserConfigResource.scala │ │ │ │ ├── WebsocketPayloadSizeTuner.scala │ │ │ │ ├── WorkflowWebsocketResource.scala │ │ │ │ ├── aiassistant/ │ │ │ │ │ ├── AiAssistantManager.scala │ │ │ │ │ ├── AiAssistantResource.scala │ │ │ │ │ ├── test_type_annotation_visitor.py │ │ │ │ │ └── type_annotation_visitor.py │ │ │ │ ├── auth/ │ │ │ │ │ ├── AuthResource.scala │ │ │ │ │ └── GoogleAuthResource.scala │ │ │ │ ├── dashboard/ │ │ │ │ │ ├── DashboardResource.scala │ │ │ │ │ ├── DatasetSearchQueryBuilder.scala │ │ │ │ │ ├── FulltextSearchQueryUtils.scala │ │ │ │ │ ├── ProjectSearchQueryBuilder.scala │ │ │ │ │ ├── SearchQueryBuilder.scala │ │ │ │ │ ├── UnifiedResourceSchema.scala │ │ │ │ │ ├── WorkflowSearchQueryBuilder.scala │ │ │ │ │ ├── admin/ │ │ │ │ │ │ ├── execution/ │ │ │ │ │ │ │ └── AdminExecutionResource.scala │ │ │ │ │ │ ├── settings/ │ │ │ │ │ │ │ └── AdminSettingsResource.scala │ │ │ │ │ │ └── user/ │ │ │ │ │ │ └── AdminUserResource.scala │ │ │ │ │ ├── hub/ │ │ │ │ │ │ ├── ActionType.scala │ │ │ │ │ │ ├── EntityTables.scala │ │ │ │ │ │ ├── EntityType.scala │ │ │ │ │ │ └── HubResource.scala │ │ │ │ │ └── user/ │ │ │ │ │ ├── UserResource.scala │ │ │ │ │ ├── dataset/ │ │ │ │ │ │ ├── DatasetResource.scala │ │ │ │ │ │ └── utils/ │ │ │ │ │ │ └── DatasetStatisticsUtils.scala │ │ │ │ │ ├── project/ │ │ │ │ │ │ ├── ProjectAccessResource.scala │ │ │ │ │ │ ├── ProjectResource.scala │ │ │ │ │ │ └── PublicProjectResource.scala │ │ │ │ │ ├── quota/ │ │ │ │ │ │ └── UserQuotaResource.scala │ │ │ │ │ └── workflow/ │ │ │ │ │ ├── WorkflowAccessResource.scala │ │ │ │ │ ├── WorkflowExecutionsResource.scala │ │ │ │ │ ├── WorkflowResource.scala │ │ │ │ │ └── WorkflowVersionResource.scala │ │ │ │ └── pythonvirtualenvironment/ │ │ │ │ ├── PveManager.scala │ │ │ │ ├── PveResource.scala │ │ │ │ └── PveWebsocketResource.scala │ │ │ ├── service/ │ │ │ │ ├── EmailNotificationService.scala │ │ │ │ ├── ExecutionConsoleService.scala │ │ │ │ ├── ExecutionReconfigurationService.scala │ │ │ │ ├── ExecutionResultService.scala │ │ │ │ ├── ExecutionRuntimeService.scala │ │ │ │ ├── ExecutionStatsService.scala │ │ │ │ ├── ExecutionsMetadataPersistService.scala │ │ │ │ ├── ResultExportService.scala │ │ │ │ ├── WorkflowEmailNotifier.scala │ │ │ │ ├── WorkflowExecutionService.scala │ │ │ │ └── WorkflowService.scala │ │ │ └── storage/ │ │ │ ├── ExecutionReconfigurationStore.scala │ │ │ ├── ExecutionStateStore.scala │ │ │ ├── StateStore.scala │ │ │ └── WorkflowStateStore.scala │ │ └── workflow/ │ │ ├── LogicalLink.scala │ │ ├── LogicalPlan.scala │ │ └── WorkflowCompiler.scala │ └── test/ │ ├── integration/ │ │ └── org/ │ │ └── apache/ │ │ └── texera/ │ │ └── amber/ │ │ ├── engine/ │ │ │ └── e2e/ │ │ │ └── ReconfigurationIntegrationSpec.scala │ │ ├── storage/ │ │ │ └── iceberg/ │ │ │ └── IcebergRestCatalogIntegrationSpec.scala │ │ └── tags/ │ │ └── IntegrationTest.java │ ├── java/ │ │ └── org/ │ │ └── apache/ │ │ └── texera/ │ │ └── web/ │ │ └── resource/ │ │ └── dashboard/ │ │ └── user/ │ │ └── dataset/ │ │ └── GitVersionControlLocalFileStorageSpec.java │ ├── python/ │ │ ├── core/ │ │ │ ├── architecture/ │ │ │ │ ├── handlers/ │ │ │ │ │ └── control/ │ │ │ │ │ ├── test_debug_command_handler.py │ │ │ │ │ ├── test_evaluate_expression_handler.py │ │ │ │ │ ├── test_replay_current_tuple_handler.py │ │ │ │ │ └── test_update_executor_handler.py │ │ │ │ ├── managers/ │ │ │ │ │ ├── test_console_message_manager.py │ │ │ │ │ ├── test_debug_manager.py │ │ │ │ │ ├── test_embedded_control_message_manager.py │ │ │ │ │ ├── test_exception_manager.py │ │ │ │ │ ├── test_executor_manager.py │ │ │ │ │ ├── test_pause_manager.py │ │ │ │ │ ├── test_state_manager.py │ │ │ │ │ ├── test_state_processing_manager.py │ │ │ │ │ ├── test_statistics_manager.py │ │ │ │ │ └── test_tuple_processing_manager.py │ │ │ │ ├── rpc/ │ │ │ │ │ └── test_async_rpc_client.py │ │ │ │ └── sendsemantics/ │ │ │ │ └── test_partitioners.py │ │ │ ├── models/ │ │ │ │ ├── schema/ │ │ │ │ │ └── test_schema.py │ │ │ │ ├── test_operator.py │ │ │ │ ├── test_state.py │ │ │ │ ├── test_table.py │ │ │ │ ├── test_tuple.py │ │ │ │ └── type/ │ │ │ │ └── test_large_binary.py │ │ │ ├── proxy/ │ │ │ │ ├── test_proxy_client.py │ │ │ │ └── test_proxy_server.py │ │ │ ├── runnables/ │ │ │ │ ├── test_console_message.py │ │ │ │ ├── test_data_processor.py │ │ │ │ ├── test_heartbeat.py │ │ │ │ ├── test_main_loop.py │ │ │ │ ├── test_network_receiver.py │ │ │ │ └── test_network_sender.py │ │ │ ├── storage/ │ │ │ │ └── iceberg/ │ │ │ │ ├── test_iceberg_document.py │ │ │ │ ├── test_iceberg_rest_catalog_integration.py │ │ │ │ ├── test_iceberg_utils_catalog.py │ │ │ │ └── test_iceberg_utils_large_binary.py │ │ │ ├── test_python_worker.py │ │ │ └── util/ │ │ │ ├── console_message/ │ │ │ │ └── test_replace_print.py │ │ │ ├── customized_queue/ │ │ │ │ ├── test_inner.py │ │ │ │ └── test_linked_blocking_multi_queue.py │ │ │ ├── test_atomic.py │ │ │ ├── test_expression_evaluator.py │ │ │ └── test_virtual_identity.py │ │ └── pytexera/ │ │ ├── storage/ │ │ │ ├── test_dataset_file_document.py │ │ │ ├── test_large_binary_input_stream.py │ │ │ ├── test_large_binary_manager.py │ │ │ └── test_large_binary_output_stream.py │ │ └── udf/ │ │ └── examples/ │ │ ├── test_count_batch_operator.py │ │ ├── test_echo_operator.py │ │ ├── test_echo_table_operator.py │ │ ├── test_generator_operator_binary.py │ │ └── test_generator_operator_integer.py │ └── scala/ │ └── org/ │ └── apache/ │ └── texera/ │ ├── amber/ │ │ ├── engine/ │ │ │ ├── architecture/ │ │ │ │ ├── common/ │ │ │ │ │ └── ProcessingStepCursorSpec.scala │ │ │ │ ├── control/ │ │ │ │ │ ├── TrivialControlSpec.scala │ │ │ │ │ └── utils/ │ │ │ │ │ ├── ChainHandler.scala │ │ │ │ │ ├── CollectHandler.scala │ │ │ │ │ ├── ErrorHandler.scala │ │ │ │ │ ├── MultiCallHandler.scala │ │ │ │ │ ├── NestedHandler.scala │ │ │ │ │ ├── PingPongHandler.scala │ │ │ │ │ ├── RecursionHandler.scala │ │ │ │ │ ├── TesterAsyncRPCHandlerInitializer.scala │ │ │ │ │ └── TrivialControlTester.scala │ │ │ │ ├── controller/ │ │ │ │ │ ├── ControllerSpec.scala │ │ │ │ │ ├── GlobalReplayManagerSpec.scala │ │ │ │ │ ├── WorkflowSchedulerSpec.scala │ │ │ │ │ └── execution/ │ │ │ │ │ ├── ExecutionUtilsSpec.scala │ │ │ │ │ ├── LinkExecutionSpec.scala │ │ │ │ │ ├── WorkerPortExecutionSpec.scala │ │ │ │ │ └── WorkflowExecutionSpec.scala │ │ │ │ ├── deploysemantics/ │ │ │ │ │ ├── AddressInfoSpec.scala │ │ │ │ │ ├── deploystrategy/ │ │ │ │ │ │ └── DeployStrategiesSpec.scala │ │ │ │ │ └── layer/ │ │ │ │ │ └── WorkerExecutionSpec.scala │ │ │ │ ├── logreplay/ │ │ │ │ │ ├── EmptyReplayLogManagerImplSpec.scala │ │ │ │ │ └── LogreplayPrimitivesSpec.scala │ │ │ │ ├── messaginglayer/ │ │ │ │ │ ├── AmberFIFOChannelSpec.scala │ │ │ │ │ ├── CongestionControlSpec.scala │ │ │ │ │ ├── FlowControlSpec.scala │ │ │ │ │ ├── NetworkInputGatewaySpec.scala │ │ │ │ │ ├── OrderingEnforcerSpec.scala │ │ │ │ │ ├── OutputManagerSpec.scala │ │ │ │ │ ├── RangeBasedShuffleSpec.scala │ │ │ │ │ └── WorkerPortSpec.scala │ │ │ │ ├── pythonworker/ │ │ │ │ │ └── PythonWorkflowWorkerSpec.scala │ │ │ │ ├── scheduling/ │ │ │ │ │ ├── CostBasedScheduleGeneratorSpec.scala │ │ │ │ │ ├── DefaultCostEstimatorSpec.scala │ │ │ │ │ ├── ExpansionGreedyScheduleGeneratorSpec.scala │ │ │ │ │ ├── RegionCoordinatorTestSupport.scala │ │ │ │ │ ├── RegionExecutionCoordinatorSpec.scala │ │ │ │ │ ├── RegionPlanSpec.scala │ │ │ │ │ ├── RegionSpec.scala │ │ │ │ │ ├── ScheduleSpec.scala │ │ │ │ │ ├── SchedulingUtilsSpec.scala │ │ │ │ │ ├── WorkflowExecutionCoordinatorSpec.scala │ │ │ │ │ ├── config/ │ │ │ │ │ │ ├── ChannelConfigSpec.scala │ │ │ │ │ │ ├── LinkConfigSpec.scala │ │ │ │ │ │ └── SchedulingConfigsSpec.scala │ │ │ │ │ └── resourcePolicies/ │ │ │ │ │ └── ResourcePoliciesSpec.scala │ │ │ │ ├── sendsemantics/ │ │ │ │ │ └── partitioners/ │ │ │ │ │ ├── NetworkOutputBufferSpec.scala │ │ │ │ │ └── PartitionersSpec.scala │ │ │ │ └── worker/ │ │ │ │ ├── DPThreadSpec.scala │ │ │ │ ├── DataProcessorSpec.scala │ │ │ │ ├── PauseTypeSpec.scala │ │ │ │ ├── WorkerSpec.scala │ │ │ │ ├── managers/ │ │ │ │ │ ├── OutputPortResultWriterThreadSpec.scala │ │ │ │ │ └── WorkerManagersSpec.scala │ │ │ │ └── promisehandlers/ │ │ │ │ └── EndHandlerSpec.scala │ │ │ ├── common/ │ │ │ │ ├── CheckpointSubsystemSpec.scala │ │ │ │ ├── UtilsSpec.scala │ │ │ │ ├── ambermessage/ │ │ │ │ │ ├── AmberMessageEnvelopesSpec.scala │ │ │ │ │ └── DataPayloadSpec.scala │ │ │ │ └── statetransition/ │ │ │ │ ├── StateManagerSpec.scala │ │ │ │ └── WorkerStateManagerSpec.scala │ │ │ ├── e2e/ │ │ │ │ ├── BatchSizePropagationSpec.scala │ │ │ │ ├── DataProcessingSpec.scala │ │ │ │ ├── PauseSpec.scala │ │ │ │ ├── ReconfigurationSpec.scala │ │ │ │ └── TestUtils.scala │ │ │ └── faulttolerance/ │ │ │ ├── CheckpointSpec.scala │ │ │ ├── LoggingSpec.scala │ │ │ └── ReplaySpec.scala │ │ └── error/ │ │ └── ErrorUtilsSpec.scala │ ├── web/ │ │ ├── auth/ │ │ │ └── UserAuthenticatorSpec.scala │ │ ├── resource/ │ │ │ ├── dashboard/ │ │ │ │ ├── file/ │ │ │ │ │ └── WorkflowResourceSpec.scala │ │ │ │ └── user/ │ │ │ │ └── workflow/ │ │ │ │ ├── WorkflowAccessResourceSpec.scala │ │ │ │ ├── WorkflowExecutionsResourceSpec.scala │ │ │ │ └── WorkflowVersionResourceSpec.scala │ │ │ └── pythonvirtualenvironment/ │ │ │ └── PveResourceSpec.scala │ │ └── service/ │ │ ├── ExecutionConsoleServiceSpec.scala │ │ ├── ExecutionReconfigurationServiceSpec.scala │ │ └── ExecutionResultServiceSpec.scala │ └── workflow/ │ ├── WorkflowCompilerSpec.scala │ └── common/ │ └── storage/ │ └── ReadonlyLocalFileDocumentSpec.scala ├── bin/ │ ├── .htaccess │ ├── README.md │ ├── access-control-service.dockerfile │ ├── add-computing-unit-worker.sh │ ├── agent-service.dockerfile │ ├── bootstrap-lakekeeper.sh │ ├── build-images.sh │ ├── build-services.sh │ ├── build.sh │ ├── computing-unit-managing-service.sh │ ├── computing-unit-master.dockerfile │ ├── computing-unit-worker.dockerfile │ ├── config-service.dockerfile │ ├── config-service.sh │ ├── config.php │ ├── cron-restart-crashed-worker.sh │ ├── deploy-daemon.sh │ ├── deploy-docker.sh │ ├── file-service.dockerfile │ ├── file-service.sh │ ├── fix-format.sh │ ├── forum/ │ │ ├── flarum.sql │ │ ├── macos-install.sh │ │ ├── start-flarum.sh │ │ └── ubuntu-install.sh │ ├── frontend-dev.sh │ ├── frontend-proto-gen.sh │ ├── frontend.sh │ ├── install-nltk.sh │ ├── k8s/ │ │ ├── Chart.yaml │ │ ├── README.md │ │ ├── templates/ │ │ │ ├── access-control-service-deployment.yaml │ │ │ ├── access-control-service-service.yaml │ │ │ ├── config-service-deployment.yaml │ │ │ ├── config-service-service.yaml │ │ │ ├── example-data-loader-job.yaml │ │ │ ├── external-names.yaml │ │ │ ├── file-service-deployment.yaml │ │ │ ├── file-service-service.yaml │ │ │ ├── gateway-backend.yaml │ │ │ ├── gateway-routes.yaml │ │ │ ├── gateway-security-policy.yaml │ │ │ ├── gateway.yaml │ │ │ ├── lakefs-secret.yaml │ │ │ ├── lakefs-setup-job.yaml │ │ │ ├── minio-persistence.yaml │ │ │ ├── postgresql-init-script-config.yaml │ │ │ ├── postgresql-persistence.yaml │ │ │ ├── pylsp.yaml │ │ │ ├── shared-editing-server.yaml │ │ │ ├── webserver-deployment.yaml │ │ │ ├── webserver-service.yaml │ │ │ ├── workflow-compiling-service-deployment.yaml │ │ │ ├── workflow-compiling-service-service.yaml │ │ │ ├── workflow-computing-unit-manager-deployment.yaml │ │ │ ├── workflow-computing-unit-manager-service-account.yaml │ │ │ ├── workflow-computing-unit-manager-service.yaml │ │ │ ├── workflow-computing-unit-master-prepull-daemonset.yaml │ │ │ ├── workflow-computing-unit-resource-quota.yaml │ │ │ ├── workflow-computing-units-namespace.yaml │ │ │ └── workflow-computing-units-service.yaml │ │ ├── values-development.yaml │ │ └── values.yaml │ ├── licensing/ │ │ ├── audit_jar_licenses.py │ │ ├── check_binary_deps.py │ │ ├── concat_license_binary.py │ │ └── test_check_binary_deps.py │ ├── litellm-config.yaml │ ├── merge-image-tags.sh │ ├── pylsp/ │ │ ├── Dockerfile │ │ ├── python-language-server.yaml │ │ └── run_pylsp.sh │ ├── python-language-service.sh │ ├── python-proto-gen.sh │ ├── server.sh │ ├── shared-editing-server.sh │ ├── single-node/ │ │ ├── DISCLAIMER │ │ ├── LICENSE │ │ ├── NOTICE │ │ ├── README.md │ │ ├── docker-compose.yml │ │ ├── examples/ │ │ │ ├── datasets/ │ │ │ │ ├── iris-species/ │ │ │ │ │ ├── Iris.csv │ │ │ │ │ └── description.txt │ │ │ │ └── popular-movies-of-imdb/ │ │ │ │ ├── TMDb_updated.csv │ │ │ │ └── description.txt │ │ │ ├── load-examples.sh │ │ │ └── workflows/ │ │ │ ├── [Example] Data Exploration on Movies Dataset.json │ │ │ └── [Example] Machine Learning on Iris Dataset.json │ │ ├── litellm-config.yaml │ │ └── nginx.conf │ ├── terminate-daemon.sh │ ├── texera-web-application.dockerfile │ ├── utils/ │ │ ├── resolve-texera-home.sh │ │ └── texera-logging.sh │ ├── workflow-compiling-service.dockerfile │ ├── workflow-compiling-service.sh │ ├── workflow-computing-unit-managing-service.dockerfile │ ├── workflow-computing-unit.sh │ └── y-websocket-server/ │ ├── Dockerfile │ └── package.json ├── build.sbt ├── codecov.yml ├── common/ │ ├── auth/ │ │ ├── build.sbt │ │ └── src/ │ │ ├── main/ │ │ │ └── scala/ │ │ │ └── org/ │ │ │ └── apache/ │ │ │ └── texera/ │ │ │ └── auth/ │ │ │ ├── JwtAuth.scala │ │ │ ├── JwtAuthFilter.scala │ │ │ ├── JwtParser.scala │ │ │ ├── RequestLoggingFilter.scala │ │ │ ├── SessionUser.scala │ │ │ ├── UserActivityTracker.scala │ │ │ └── util/ │ │ │ ├── ComputingUnitAccess.scala │ │ │ └── HeaderField.scala │ │ └── test/ │ │ └── scala/ │ │ └── org/ │ │ └── apache/ │ │ └── texera/ │ │ └── auth/ │ │ ├── JwtParserSpec.scala │ │ └── UserActivityTrackerSpec.scala │ ├── config/ │ │ ├── build.sbt │ │ └── src/ │ │ └── main/ │ │ ├── resources/ │ │ │ ├── application.conf │ │ │ ├── auth.conf │ │ │ ├── cluster.conf │ │ │ ├── computing-unit.conf │ │ │ ├── default.conf │ │ │ ├── gui.conf │ │ │ ├── kubernetes.conf │ │ │ ├── llm.conf │ │ │ ├── storage.conf │ │ │ ├── udf.conf │ │ │ └── user-system.conf │ │ └── scala/ │ │ └── org/ │ │ └── apache/ │ │ └── texera/ │ │ ├── amber/ │ │ │ ├── config/ │ │ │ │ ├── ApplicationConfig.scala │ │ │ │ ├── EnvironmentalVariable.scala │ │ │ │ ├── PekkoConfig.scala │ │ │ │ ├── PythonUtils.scala │ │ │ │ ├── StorageConfig.scala │ │ │ │ └── UdfConfig.scala │ │ │ └── util/ │ │ │ └── ConfigParserUtil.scala │ │ └── config/ │ │ ├── AuthConfig.scala │ │ ├── ComputingUnitConfig.scala │ │ ├── DefaultsConfig.scala │ │ ├── GuiConfig.scala │ │ ├── KubernetesConfig.scala │ │ ├── LLMConfig.scala │ │ └── UserSystemConfig.scala │ ├── dao/ │ │ ├── .gitignore │ │ ├── build.sbt │ │ └── src/ │ │ ├── main/ │ │ │ ├── resources/ │ │ │ │ └── jooq-conf.xml │ │ │ └── scala/ │ │ │ └── org/ │ │ │ └── apache/ │ │ │ └── texera/ │ │ │ └── dao/ │ │ │ ├── SiteSettings.scala │ │ │ └── SqlServer.scala │ │ └── test/ │ │ └── scala/ │ │ └── org/ │ │ └── apache/ │ │ └── texera/ │ │ └── dao/ │ │ ├── MockTexeraDB.scala │ │ └── SiteSettingsSpec.scala │ ├── pybuilder/ │ │ ├── build.sbt │ │ └── src/ │ │ ├── main/ │ │ │ └── scala/ │ │ │ └── org/ │ │ │ └── apache/ │ │ │ └── texera/ │ │ │ └── amber/ │ │ │ └── pybuilder/ │ │ │ ├── BoundaryValidator.scala │ │ │ ├── EncodableInspector.scala │ │ │ ├── EncodableStringAnnotation.java │ │ │ ├── PythonLexerUtils.scala │ │ │ └── PythonTemplateBuilder.scala │ │ └── test/ │ │ └── scala/ │ │ └── org/ │ │ └── apache/ │ │ └── texera/ │ │ └── amber/ │ │ └── pybuilder/ │ │ ├── PythonLexerUtilsSpec.scala │ │ ├── PythonTemplateBuilderApiSpec.scala │ │ └── PythonTemplateBuilderSpec.scala │ ├── workflow-core/ │ │ ├── build.sbt │ │ └── src/ │ │ ├── main/ │ │ │ ├── protobuf/ │ │ │ │ ├── org/ │ │ │ │ │ └── apache/ │ │ │ │ │ └── texera/ │ │ │ │ │ └── amber/ │ │ │ │ │ └── core/ │ │ │ │ │ ├── executor.proto │ │ │ │ │ ├── virtualidentity.proto │ │ │ │ │ ├── workflow.proto │ │ │ │ │ └── workflowruntimestate.proto │ │ │ │ └── scalapb/ │ │ │ │ └── scalapb.proto │ │ │ └── scala/ │ │ │ └── org/ │ │ │ └── apache/ │ │ │ └── texera/ │ │ │ ├── amber/ │ │ │ │ ├── core/ │ │ │ │ │ ├── WorkflowRuntimeException.scala │ │ │ │ │ ├── executor/ │ │ │ │ │ │ ├── ExecFactory.scala │ │ │ │ │ │ ├── JavaRuntimeCompilation.scala │ │ │ │ │ │ ├── OperatorExecutor.scala │ │ │ │ │ │ └── SourceOperatorExecutor.scala │ │ │ │ │ ├── state/ │ │ │ │ │ │ └── State.scala │ │ │ │ │ ├── storage/ │ │ │ │ │ │ ├── DocumentFactory.scala │ │ │ │ │ │ ├── FileResolver.scala │ │ │ │ │ │ ├── IcebergCatalogInstance.scala │ │ │ │ │ │ ├── VFSURIFactory.scala │ │ │ │ │ │ ├── model/ │ │ │ │ │ │ │ ├── BufferedItemWriter.scala │ │ │ │ │ │ │ ├── DatasetFileDocument.scala │ │ │ │ │ │ │ ├── OnDataset.scala │ │ │ │ │ │ │ ├── ReadonlyLocalFileDocument.scala │ │ │ │ │ │ │ ├── ReadonlyVirtualDocument.scala │ │ │ │ │ │ │ ├── VirtualCollection.scala │ │ │ │ │ │ │ └── VirtualDocument.scala │ │ │ │ │ │ ├── result/ │ │ │ │ │ │ │ ├── ResultSchema.scala │ │ │ │ │ │ │ ├── WorkflowResultStore.scala │ │ │ │ │ │ │ └── iceberg/ │ │ │ │ │ │ │ ├── IcebergDocument.scala │ │ │ │ │ │ │ ├── IcebergTableWriter.scala │ │ │ │ │ │ │ └── OnIceberg.scala │ │ │ │ │ │ └── util/ │ │ │ │ │ │ ├── LakeFSStorageClient.scala │ │ │ │ │ │ ├── StorageUtil.scala │ │ │ │ │ │ └── dataset/ │ │ │ │ │ │ ├── GitVersionControlLocalFileStorage.java │ │ │ │ │ │ ├── JGitVersionControl.java │ │ │ │ │ │ └── PhysicalFileNode.java │ │ │ │ │ ├── tuple/ │ │ │ │ │ │ ├── Attribute.java │ │ │ │ │ │ ├── AttributeType.java │ │ │ │ │ │ ├── AttributeTypeUtils.scala │ │ │ │ │ │ ├── LargeBinary.java │ │ │ │ │ │ ├── Schema.scala │ │ │ │ │ │ ├── Tuple.scala │ │ │ │ │ │ ├── TupleLike.scala │ │ │ │ │ │ └── TupleUtils.scala │ │ │ │ │ └── workflow/ │ │ │ │ │ ├── ExecutionMode.java │ │ │ │ │ ├── LocationPreference.scala │ │ │ │ │ ├── PartitionInfo.scala │ │ │ │ │ ├── PhysicalOp.scala │ │ │ │ │ ├── PhysicalPlan.scala │ │ │ │ │ ├── WorkflowContext.scala │ │ │ │ │ └── WorkflowSettings.scala │ │ │ │ └── util/ │ │ │ │ ├── ArrowUtils.scala │ │ │ │ ├── IcebergUtil.scala │ │ │ │ ├── JSONUtils.scala │ │ │ │ ├── VirtualIdentityUtils.scala │ │ │ │ └── serde/ │ │ │ │ ├── GlobalPortIdentitySerde.scala │ │ │ │ ├── PortIdentityKeyDeserializer.scala │ │ │ │ └── PortIdentityKeySerializer.scala │ │ │ └── service/ │ │ │ └── util/ │ │ │ ├── LargeBinaryInputStream.scala │ │ │ ├── LargeBinaryManager.scala │ │ │ ├── LargeBinaryOutputStream.scala │ │ │ └── S3StorageClient.scala │ │ └── test/ │ │ ├── resources/ │ │ │ ├── country_sales_small.csv │ │ │ └── datasets/ │ │ │ └── 1/ │ │ │ ├── directory/ │ │ │ │ └── a.csv │ │ │ └── random_data.csv │ │ └── scala/ │ │ └── org/ │ │ └── apache/ │ │ └── texera/ │ │ ├── amber/ │ │ │ ├── core/ │ │ │ │ ├── WorkflowRuntimeExceptionSpec.scala │ │ │ │ ├── executor/ │ │ │ │ │ └── CoreExecutorReflectionSpec.scala │ │ │ │ ├── state/ │ │ │ │ │ └── StateSpec.scala │ │ │ │ ├── storage/ │ │ │ │ │ ├── VFSURIFactorySpec.scala │ │ │ │ │ ├── model/ │ │ │ │ │ │ └── VirtualDocumentSpec.scala │ │ │ │ │ └── util/ │ │ │ │ │ └── StorageUtilSpec.scala │ │ │ │ ├── tuple/ │ │ │ │ │ ├── AttributeTypeUtilsSpec.scala │ │ │ │ │ ├── InternalMarkerSpec.scala │ │ │ │ │ ├── SchemaSpec.scala │ │ │ │ │ ├── TupleSpec.scala │ │ │ │ │ └── TupleUtilsSpec.scala │ │ │ │ └── workflow/ │ │ │ │ ├── PartitionInfoSpec.scala │ │ │ │ ├── WorkflowContextSpec.scala │ │ │ │ └── WorkflowCoreTypesSpec.scala │ │ │ ├── storage/ │ │ │ │ ├── FileResolverSpec.scala │ │ │ │ └── result/ │ │ │ │ └── iceberg/ │ │ │ │ ├── IcebergDocumentConsoleMessagesSpec.scala │ │ │ │ ├── IcebergDocumentSpec.scala │ │ │ │ └── IcebergTableStatsSpec.scala │ │ │ └── util/ │ │ │ ├── ArrowUtilsSpec.scala │ │ │ ├── IcebergUtilSpec.scala │ │ │ ├── JSONUtilsSpec.scala │ │ │ └── VirtualIdentityUtilsSpec.scala │ │ └── service/ │ │ └── util/ │ │ ├── LargeBinaryInputStreamSpec.scala │ │ ├── LargeBinaryManagerSpec.scala │ │ ├── LargeBinaryOutputStreamSpec.scala │ │ ├── S3StorageClientSpec.scala │ │ └── S3StorageTestBase.scala │ └── workflow-operator/ │ ├── build.sbt │ ├── project/ │ │ └── build.properties │ └── src/ │ ├── main/ │ │ └── scala/ │ │ ├── com/ │ │ │ └── kjetland/ │ │ │ └── jackson/ │ │ │ └── jsonSchema/ │ │ │ ├── JsonSchemaDraft.java │ │ │ ├── JsonSchemaGenerator.scala │ │ │ └── annotations/ │ │ │ ├── JsonSchemaArrayWithUniqueItems.java │ │ │ ├── JsonSchemaBool.java │ │ │ ├── JsonSchemaDefault.java │ │ │ ├── JsonSchemaDescription.java │ │ │ ├── JsonSchemaExamples.java │ │ │ ├── JsonSchemaFormat.java │ │ │ ├── JsonSchemaInject.java │ │ │ ├── JsonSchemaInt.java │ │ │ ├── JsonSchemaOptions.java │ │ │ ├── JsonSchemaString.java │ │ │ └── JsonSchemaTitle.java │ │ └── org/ │ │ └── apache/ │ │ └── texera/ │ │ └── amber/ │ │ ├── operator/ │ │ │ ├── DummyProperties.scala │ │ │ ├── LogicalOp.scala │ │ │ ├── PortDescriptor.scala │ │ │ ├── PythonOperatorDescriptor.scala │ │ │ ├── SpecialPhysicalOpFactory.scala │ │ │ ├── TestOperators.scala │ │ │ ├── aggregate/ │ │ │ │ ├── AggregateOpDesc.scala │ │ │ │ ├── AggregateOpExec.scala │ │ │ │ ├── AggregationFunction.java │ │ │ │ ├── AggregationOperation.scala │ │ │ │ └── DistributedAggregation.scala │ │ │ ├── cartesianProduct/ │ │ │ │ ├── CartesianProductOpDesc.scala │ │ │ │ └── CartesianProductOpExec.scala │ │ │ ├── dictionary/ │ │ │ │ ├── DictionaryMatcherOpDesc.scala │ │ │ │ ├── DictionaryMatcherOpExec.scala │ │ │ │ └── MatchingType.java │ │ │ ├── difference/ │ │ │ │ ├── DifferenceOpDesc.scala │ │ │ │ └── DifferenceOpExec.scala │ │ │ ├── distinct/ │ │ │ │ ├── DistinctOpDesc.scala │ │ │ │ └── DistinctOpExec.scala │ │ │ ├── dummy/ │ │ │ │ └── DummyOpDesc.scala │ │ │ ├── filter/ │ │ │ │ ├── ComparisonType.java │ │ │ │ ├── FilterOpDesc.scala │ │ │ │ ├── FilterOpExec.scala │ │ │ │ ├── FilterPredicate.java │ │ │ │ ├── SpecializedFilterOpDesc.scala │ │ │ │ └── SpecializedFilterOpExec.scala │ │ │ ├── flatmap/ │ │ │ │ ├── FlatMapOpDesc.scala │ │ │ │ └── FlatMapOpExec.scala │ │ │ ├── hashJoin/ │ │ │ │ ├── HashJoinBuildOpExec.scala │ │ │ │ ├── HashJoinOpDesc.scala │ │ │ │ ├── HashJoinProbeOpExec.scala │ │ │ │ └── JoinType.java │ │ │ ├── huggingFace/ │ │ │ │ ├── HuggingFaceIrisLogisticRegressionOpDesc.scala │ │ │ │ ├── HuggingFaceSentimentAnalysisOpDesc.scala │ │ │ │ ├── HuggingFaceSpamSMSDetectionOpDesc.scala │ │ │ │ └── HuggingFaceTextSummarizationOpDesc.scala │ │ │ ├── ifStatement/ │ │ │ │ ├── IfOpDesc.scala │ │ │ │ └── IfOpExec.scala │ │ │ ├── intersect/ │ │ │ │ ├── IntersectOpDesc.scala │ │ │ │ └── IntersectOpExec.scala │ │ │ ├── intervalJoin/ │ │ │ │ ├── IntervalJoinOpDesc.scala │ │ │ │ ├── IntervalJoinOpExec.scala │ │ │ │ └── TimeIntervalType.java │ │ │ ├── keywordSearch/ │ │ │ │ ├── KeywordSearchOpDesc.scala │ │ │ │ └── KeywordSearchOpExec.scala │ │ │ ├── limit/ │ │ │ │ ├── LimitOpDesc.scala │ │ │ │ └── LimitOpExec.scala │ │ │ ├── machineLearning/ │ │ │ │ ├── Scorer/ │ │ │ │ │ ├── MachineLearningScorerOpDesc.scala │ │ │ │ │ ├── classificationMetricsFnc.java │ │ │ │ │ └── regressionMetricsFnc.java │ │ │ │ └── sklearnAdvanced/ │ │ │ │ ├── KNNTrainer/ │ │ │ │ │ ├── SklearnAdvancedKNNClassifierTrainerOpDesc.scala │ │ │ │ │ ├── SklearnAdvancedKNNParameters.java │ │ │ │ │ └── SklearnAdvancedKNNRegressorTrainerOpDesc.scala │ │ │ │ ├── SVCTrainer/ │ │ │ │ │ ├── SklearnAdvancedSVCParameters.java │ │ │ │ │ └── SklearnAdvancedSVCTrainerOpDesc.scala │ │ │ │ ├── SVRTrainer/ │ │ │ │ │ ├── SklearnAdvancedSVRParameters.java │ │ │ │ │ └── SklearnAdvancedSVRTrainerOpDesc.scala │ │ │ │ └── base/ │ │ │ │ ├── HyperParameters.scala │ │ │ │ └── SklearnAdvancedBaseDesc.scala │ │ │ ├── map/ │ │ │ │ ├── MapOpDesc.scala │ │ │ │ └── MapOpExec.scala │ │ │ ├── metadata/ │ │ │ │ ├── OPVersion.java │ │ │ │ ├── OperatorGroupConstants.scala │ │ │ │ ├── OperatorMetadataGenerator.scala │ │ │ │ ├── PropertyNameConstants.scala │ │ │ │ └── annotations/ │ │ │ │ ├── AutofillAttributeName.java │ │ │ │ ├── AutofillAttributeNameLambda.java │ │ │ │ ├── AutofillAttributeNameList.java │ │ │ │ ├── AutofillAttributeNameOnPort1.java │ │ │ │ ├── BatchByColumn.java │ │ │ │ ├── CommonOpDescAnnotation.java │ │ │ │ ├── EnablePresets.java │ │ │ │ ├── HideAnnotation.java │ │ │ │ └── UIWidget.java │ │ │ ├── projection/ │ │ │ │ ├── AttributeUnit.java │ │ │ │ ├── ProjectionOpDesc.scala │ │ │ │ └── ProjectionOpExec.scala │ │ │ ├── randomksampling/ │ │ │ │ ├── RandomKSamplingOpDesc.scala │ │ │ │ └── RandomKSamplingOpExec.scala │ │ │ ├── regex/ │ │ │ │ ├── RegexOpDesc.scala │ │ │ │ └── RegexOpExec.scala │ │ │ ├── reservoirsampling/ │ │ │ │ ├── ReservoirSamplingOpDesc.scala │ │ │ │ └── ReservoirSamplingOpExec.scala │ │ │ ├── sink/ │ │ │ │ └── ProgressiveUtils.scala │ │ │ ├── sklearn/ │ │ │ │ ├── SklearnAdaptiveBoostingOpDesc.scala │ │ │ │ ├── SklearnBaggingOpDesc.scala │ │ │ │ ├── SklearnBernoulliNaiveBayesOpDesc.scala │ │ │ │ ├── SklearnClassifierOpDesc.scala │ │ │ │ ├── SklearnComplementNaiveBayesOpDesc.scala │ │ │ │ ├── SklearnDecisionTreeOpDesc.scala │ │ │ │ ├── SklearnDummyClassifierOpDesc.scala │ │ │ │ ├── SklearnExtraTreeOpDesc.scala │ │ │ │ ├── SklearnExtraTreesOpDesc.scala │ │ │ │ ├── SklearnGaussianNaiveBayesOpDesc.scala │ │ │ │ ├── SklearnGradientBoostingOpDesc.scala │ │ │ │ ├── SklearnKNNOpDesc.scala │ │ │ │ ├── SklearnLinearRegressionOpDesc.scala │ │ │ │ ├── SklearnLinearSVMOpDesc.scala │ │ │ │ ├── SklearnLogisticRegressionCVOpDesc.scala │ │ │ │ ├── SklearnLogisticRegressionOpDesc.scala │ │ │ │ ├── SklearnMultiLayerPerceptronOpDesc.scala │ │ │ │ ├── SklearnMultinomialNaiveBayesOpDesc.scala │ │ │ │ ├── SklearnNearestCentroidOpDesc.scala │ │ │ │ ├── SklearnPassiveAggressiveOpDesc.scala │ │ │ │ ├── SklearnPerceptronOpDesc.scala │ │ │ │ ├── SklearnPredictionOpDesc.scala │ │ │ │ ├── SklearnProbabilityCalibrationOpDesc.scala │ │ │ │ ├── SklearnRandomForestOpDesc.scala │ │ │ │ ├── SklearnRidgeCVOpDesc.scala │ │ │ │ ├── SklearnRidgeOpDesc.scala │ │ │ │ ├── SklearnSDGOpDesc.scala │ │ │ │ ├── SklearnSVMOpDesc.scala │ │ │ │ ├── testing/ │ │ │ │ │ └── SklearnTestingOpDesc.scala │ │ │ │ └── training/ │ │ │ │ ├── SklearnTrainingAdaptiveBoostingOpDesc.scala │ │ │ │ ├── SklearnTrainingBaggingOpDesc.scala │ │ │ │ ├── SklearnTrainingBernoulliNaiveBayesOpDesc.scala │ │ │ │ ├── SklearnTrainingComplementNaiveBayesOpDesc.scala │ │ │ │ ├── SklearnTrainingDecisionTreeOpDesc.scala │ │ │ │ ├── SklearnTrainingDummyClassifierOpDesc.scala │ │ │ │ ├── SklearnTrainingExtraTreeOpDesc.scala │ │ │ │ ├── SklearnTrainingExtraTreesOpDesc.scala │ │ │ │ ├── SklearnTrainingGaussianNaiveBayesOpDesc.scala │ │ │ │ ├── SklearnTrainingGradientBoostingOpDesc.scala │ │ │ │ ├── SklearnTrainingKNNOpDesc.scala │ │ │ │ ├── SklearnTrainingLinearRegressionOpDesc.scala │ │ │ │ ├── SklearnTrainingLinearSVMOpDesc.scala │ │ │ │ ├── SklearnTrainingLogisticRegressionCVOpDesc.scala │ │ │ │ ├── SklearnTrainingLogisticRegressionOpDesc.scala │ │ │ │ ├── SklearnTrainingMultiLayerPerceptronOpDesc.scala │ │ │ │ ├── SklearnTrainingMultinomialNaiveBayesOpDesc.scala │ │ │ │ ├── SklearnTrainingNearestCentroidOpDesc.scala │ │ │ │ ├── SklearnTrainingOpDesc.scala │ │ │ │ ├── SklearnTrainingPassiveAggressiveOpDesc.scala │ │ │ │ ├── SklearnTrainingPerceptronOpDesc.scala │ │ │ │ ├── SklearnTrainingProbabilityCalibrationOpDesc.scala │ │ │ │ ├── SklearnTrainingRandomForestOpDesc.scala │ │ │ │ ├── SklearnTrainingRidgeCVOpDesc.scala │ │ │ │ ├── SklearnTrainingRidgeOpDesc.scala │ │ │ │ ├── SklearnTrainingSDGOpDesc.scala │ │ │ │ └── SklearnTrainingSVMOpDesc.scala │ │ │ ├── sleep/ │ │ │ │ ├── SleepOpDesc.scala │ │ │ │ └── SleepOpExec.scala │ │ │ ├── sort/ │ │ │ │ ├── SortCriteriaUnit.scala │ │ │ │ ├── SortOpDesc.scala │ │ │ │ ├── SortPreference.java │ │ │ │ ├── StableMergeSortOpDesc.scala │ │ │ │ └── StableMergeSortOpExec.scala │ │ │ ├── sortPartitions/ │ │ │ │ ├── SortPartitionsOpDesc.scala │ │ │ │ └── SortPartitionsOpExec.scala │ │ │ ├── source/ │ │ │ │ ├── BufferedBlockReader.java │ │ │ │ ├── PythonSourceOperatorDescriptor.scala │ │ │ │ ├── SourceOperatorDescriptor.scala │ │ │ │ ├── apis/ │ │ │ │ │ ├── reddit/ │ │ │ │ │ │ ├── RedditSearchSourceOpDesc.scala │ │ │ │ │ │ └── RedditSourceOperatorFunction.java │ │ │ │ │ └── twitter/ │ │ │ │ │ ├── TwitterSourceOpDesc.scala │ │ │ │ │ ├── TwitterSourceOpExec.scala │ │ │ │ │ └── v2/ │ │ │ │ │ ├── TwitterFullArchiveSearchSourceOpDesc.scala │ │ │ │ │ ├── TwitterFullArchiveSearchSourceOpExec.scala │ │ │ │ │ ├── TwitterSearchSourceOpDesc.scala │ │ │ │ │ └── TwitterSearchSourceOpExec.scala │ │ │ │ ├── cache/ │ │ │ │ │ └── CacheSourceOpExec.scala │ │ │ │ ├── dataset/ │ │ │ │ │ ├── FileListerSourceOpDesc.scala │ │ │ │ │ └── FileListerSourceOpExec.scala │ │ │ │ ├── fetcher/ │ │ │ │ │ ├── DecodingMethod.java │ │ │ │ │ ├── RandomUserAgent.java │ │ │ │ │ ├── URLFetchUtil.scala │ │ │ │ │ ├── URLFetcherOpDesc.scala │ │ │ │ │ └── URLFetcherOpExec.scala │ │ │ │ ├── scan/ │ │ │ │ │ ├── AutoClosingIterator.scala │ │ │ │ │ ├── FileAttributeType.java │ │ │ │ │ ├── FileDecodingMethod.java │ │ │ │ │ ├── ScanSourceOpDesc.scala │ │ │ │ │ ├── arrow/ │ │ │ │ │ │ ├── ArrowSourceOpDesc.scala │ │ │ │ │ │ └── ArrowSourceOpExec.scala │ │ │ │ │ ├── csv/ │ │ │ │ │ │ ├── CSVScanSourceOpDesc.scala │ │ │ │ │ │ ├── CSVScanSourceOpExec.scala │ │ │ │ │ │ ├── ParallelCSVScanSourceOpDesc.scala │ │ │ │ │ │ └── ParallelCSVScanSourceOpExec.scala │ │ │ │ │ ├── csvOld/ │ │ │ │ │ │ ├── CSVOldScanSourceOpDesc.scala │ │ │ │ │ │ └── CSVOldScanSourceOpExec.scala │ │ │ │ │ ├── file/ │ │ │ │ │ │ ├── FileScanOpDesc.scala │ │ │ │ │ │ ├── FileScanOpExec.scala │ │ │ │ │ │ ├── FileScanSourceOpDesc.scala │ │ │ │ │ │ ├── FileScanSourceOpExec.scala │ │ │ │ │ │ └── FileScanUtils.scala │ │ │ │ │ ├── json/ │ │ │ │ │ │ ├── JSONLScanSourceOpDesc.scala │ │ │ │ │ │ └── JSONLScanSourceOpExec.scala │ │ │ │ │ └── text/ │ │ │ │ │ ├── TextInputSourceOpDesc.scala │ │ │ │ │ ├── TextInputSourceOpExec.scala │ │ │ │ │ └── TextSourceOpDesc.scala │ │ │ │ └── sql/ │ │ │ │ ├── SQLSourceOpDesc.scala │ │ │ │ ├── SQLSourceOpExec.scala │ │ │ │ ├── asterixdb/ │ │ │ │ │ ├── AsterixDBConnUtil.scala │ │ │ │ │ ├── AsterixDBSourceOpDesc.scala │ │ │ │ │ └── AsterixDBSourceOpExec.scala │ │ │ │ ├── mysql/ │ │ │ │ │ ├── MySQLConnUtil.scala │ │ │ │ │ ├── MySQLSourceOpDesc.scala │ │ │ │ │ └── MySQLSourceOpExec.scala │ │ │ │ └── postgresql/ │ │ │ │ ├── PostgreSQLConnUtil.scala │ │ │ │ ├── PostgreSQLSourceOpDesc.scala │ │ │ │ └── PostgreSQLSourceOpExec.scala │ │ │ ├── split/ │ │ │ │ ├── SplitOpDesc.scala │ │ │ │ └── SplitOpExec.scala │ │ │ ├── substringSearch/ │ │ │ │ ├── SubstringSearchOpDesc.scala │ │ │ │ └── SubstringSearchOpExec.scala │ │ │ ├── symmetricDifference/ │ │ │ │ ├── SymmetricDifferenceOpDesc.scala │ │ │ │ └── SymmetricDifferenceOpExec.scala │ │ │ ├── typecasting/ │ │ │ │ ├── TypeCastingOpDesc.scala │ │ │ │ ├── TypeCastingOpExec.scala │ │ │ │ └── TypeCastingUnit.java │ │ │ ├── udf/ │ │ │ │ ├── java/ │ │ │ │ │ └── JavaUDFOpDesc.scala │ │ │ │ ├── python/ │ │ │ │ │ ├── DualInputPortsPythonUDFOpDescV2.scala │ │ │ │ │ ├── LambdaAttributeUnit.java │ │ │ │ │ ├── PythonLambdaFunctionOpDesc.scala │ │ │ │ │ ├── PythonTableReducerOpDesc.scala │ │ │ │ │ ├── PythonUDFOpDescV2.scala │ │ │ │ │ └── source/ │ │ │ │ │ └── PythonUDFSourceOpDescV2.scala │ │ │ │ └── r/ │ │ │ │ ├── RUDFOpDesc.scala │ │ │ │ └── RUDFSourceOpDesc.scala │ │ │ ├── union/ │ │ │ │ ├── UnionOpDesc.scala │ │ │ │ └── UnionOpExec.scala │ │ │ ├── unneststring/ │ │ │ │ ├── UnnestStringOpDesc.scala │ │ │ │ └── UnnestStringOpExec.scala │ │ │ ├── util/ │ │ │ │ └── OperatorDescriptorUtils.scala │ │ │ └── visualization/ │ │ │ ├── DotPlot/ │ │ │ │ └── DotPlotOpDesc.scala │ │ │ ├── IcicleChart/ │ │ │ │ └── IcicleChartOpDesc.scala │ │ │ ├── ImageUtility.scala │ │ │ ├── ImageViz/ │ │ │ │ └── ImageVisualizerOpDesc.scala │ │ │ ├── ScatterMatrixChart/ │ │ │ │ └── ScatterMatrixChartOpDesc.scala │ │ │ ├── barChart/ │ │ │ │ └── BarChartOpDesc.scala │ │ │ ├── boxViolinPlot/ │ │ │ │ ├── BoxViolinPlotOpDesc.scala │ │ │ │ └── BoxViolinPlotQuartileFunction.java │ │ │ ├── bubbleChart/ │ │ │ │ └── BubbleChartOpDesc.scala │ │ │ ├── bulletChart/ │ │ │ │ ├── BulletChartOpDesc.scala │ │ │ │ └── BulletChartStepDefinition.scala │ │ │ ├── candlestickChart/ │ │ │ │ └── CandlestickChartOpDesc.scala │ │ │ ├── carpetPlot/ │ │ │ │ └── CarpetPlotOpDesc.scala │ │ │ ├── choroplethMap/ │ │ │ │ └── ChoroplethMapOpDesc.scala │ │ │ ├── continuousErrorBands/ │ │ │ │ ├── BandConfig.scala │ │ │ │ └── ContinuousErrorBandsOpDesc.scala │ │ │ ├── contourPlot/ │ │ │ │ ├── ContourPlotColoringFunction.java │ │ │ │ └── ContourPlotOpDesc.scala │ │ │ ├── dendrogram/ │ │ │ │ └── DendrogramOpDesc.scala │ │ │ ├── dumbbellPlot/ │ │ │ │ ├── DumbbellDotConfig.scala │ │ │ │ └── DumbbellPlotOpDesc.scala │ │ │ ├── ecdfPlot/ │ │ │ │ └── ECDFPlotOpDesc.scala │ │ │ ├── figureFactoryTable/ │ │ │ │ ├── FigureFactoryTableConfig.scala │ │ │ │ └── FigureFactoryTableOpDesc.scala │ │ │ ├── filledAreaPlot/ │ │ │ │ └── FilledAreaPlotOpDesc.scala │ │ │ ├── funnelPlot/ │ │ │ │ └── FunnelPlotOpDesc.scala │ │ │ ├── ganttChart/ │ │ │ │ └── GanttChartOpDesc.scala │ │ │ ├── gaugeChart/ │ │ │ │ ├── GaugeChartOpDesc.scala │ │ │ │ └── GaugeChartSteps.scala │ │ │ ├── heatMap/ │ │ │ │ └── HeatMapOpDesc.scala │ │ │ ├── hierarchychart/ │ │ │ │ ├── HierarchyChartOpDesc.scala │ │ │ │ ├── HierarchyChartType.java │ │ │ │ └── HierarchySection.scala │ │ │ ├── histogram/ │ │ │ │ └── HistogramChartOpDesc.scala │ │ │ ├── histogram2d/ │ │ │ │ ├── Histogram2DOpDesc.scala │ │ │ │ └── NormalizationType.java │ │ │ ├── htmlviz/ │ │ │ │ ├── HtmlVizOpDesc.scala │ │ │ │ └── HtmlVizOpExec.scala │ │ │ ├── lineChart/ │ │ │ │ ├── LineChartOpDesc.scala │ │ │ │ ├── LineConfig.scala │ │ │ │ └── LineMode.java │ │ │ ├── nestedTable/ │ │ │ │ ├── NestedTableConfig.scala │ │ │ │ └── NestedTableOpDesc.scala │ │ │ ├── networkGraph/ │ │ │ │ └── NetworkGraphOpDesc.scala │ │ │ ├── parallelCoordinatesPlot/ │ │ │ │ └── ParallelCoordinatesPlotOpDesc.scala │ │ │ ├── pieChart/ │ │ │ │ └── PieChartOpDesc.scala │ │ │ ├── polarChart/ │ │ │ │ └── PolarChartOpDesc.scala │ │ │ ├── quiverPlot/ │ │ │ │ └── QuiverPlotOpDesc.scala │ │ │ ├── radarChart/ │ │ │ │ └── RadarChartOpDesc.scala │ │ │ ├── radarPlot/ │ │ │ │ ├── RadarPlotLinePattern.java │ │ │ │ └── RadarPlotOpDesc.scala │ │ │ ├── rangeSlider/ │ │ │ │ ├── RangeSliderHandleDuplicateFunction.java │ │ │ │ └── RangeSliderOpDesc.scala │ │ │ ├── sankeyDiagram/ │ │ │ │ └── SankeyDiagramOpDesc.scala │ │ │ ├── scatter3DChart/ │ │ │ │ └── Scatter3dChartOpDesc.scala │ │ │ ├── scatterplot/ │ │ │ │ └── ScatterplotOpDesc.scala │ │ │ ├── stripChart/ │ │ │ │ └── StripChartOpDesc.scala │ │ │ ├── tablesChart/ │ │ │ │ ├── TablesConfig.scala │ │ │ │ └── TablesPlotOpDesc.scala │ │ │ ├── ternaryContour/ │ │ │ │ └── TernaryContourOpDesc.scala │ │ │ ├── ternaryPlot/ │ │ │ │ └── TernaryPlotOpDesc.scala │ │ │ ├── timeSeriesplot/ │ │ │ │ └── TimeSeriesOpDesc.scala │ │ │ ├── treeplot/ │ │ │ │ └── TreePlotOpDesc.scala │ │ │ ├── urlviz/ │ │ │ │ ├── UrlVizOpDesc.scala │ │ │ │ └── UrlVizOpExec.scala │ │ │ ├── volcanoPlot/ │ │ │ │ └── VolcanoPlotOpDesc.scala │ │ │ ├── waterfallChart/ │ │ │ │ └── WaterfallChartOpDesc.scala │ │ │ ├── windRoseChart/ │ │ │ │ └── WindRoseChartOpDesc.scala │ │ │ └── wordCloud/ │ │ │ └── WordCloudOpDesc.scala │ │ └── util/ │ │ └── ObjectMapperUtils.scala │ └── test/ │ ├── resources/ │ │ ├── 100.jsonl │ │ ├── 1000.jsonl │ │ ├── country_sales_headerless_small.csv │ │ ├── country_sales_headerless_small_multi_line.csv │ │ ├── country_sales_headerless_small_multi_line_custom_delimiter.csv │ │ ├── country_sales_medium.csv │ │ ├── country_sales_small.csv │ │ ├── country_sales_small_multi_line.csv │ │ ├── line_numbers.txt │ │ ├── line_numbers_crlf.txt │ │ └── numbers.txt │ └── scala/ │ └── org/ │ └── apache/ │ └── texera/ │ └── amber/ │ ├── operator/ │ │ ├── aggregate/ │ │ │ ├── AggregateOpSpec.scala │ │ │ └── AggregationOperationSpec.scala │ │ ├── cartesianProduct/ │ │ │ └── CartesianProductOpExecSpec.scala │ │ ├── dictionary/ │ │ │ └── DictionaryMatcherOpExecSpec.scala │ │ ├── difference/ │ │ │ └── DifferenceOpExecSpec.scala │ │ ├── distinct/ │ │ │ └── DistinctOpExecSpec.scala │ │ ├── filter/ │ │ │ └── SpecializedFilterOpExecSpec.scala │ │ ├── flatmap/ │ │ │ └── FlatMapOpExecSpec.scala │ │ ├── hashJoin/ │ │ │ └── HashJoinOpSpec.scala │ │ ├── ifStatement/ │ │ │ └── IfOpExecSpec.scala │ │ ├── intersect/ │ │ │ ├── IntersectOpDescSpec.scala │ │ │ └── IntersectOpExecSpec.scala │ │ ├── intervalJoin/ │ │ │ └── IntervalOpExecSpec.scala │ │ ├── keywordSearch/ │ │ │ └── KeywordSearchOpExecSpec.scala │ │ ├── limit/ │ │ │ └── LimitOpExecSpec.scala │ │ ├── map/ │ │ │ └── MapOpExecSpec.scala │ │ ├── projection/ │ │ │ ├── ProjectionOpDescSpec.scala │ │ │ └── ProjectionOpExecSpec.scala │ │ ├── sink/ │ │ │ └── ProgressiveUtilsSpec.scala │ │ ├── sklearn/ │ │ │ └── SklearnOpDescRegistrySpec.scala │ │ ├── sleep/ │ │ │ └── SleepOpDescSpec.scala │ │ ├── sort/ │ │ │ └── StableMergeSortOpExecSpec.scala │ │ ├── sortPartitions/ │ │ │ └── SortPartitionsOpExecSpec.scala │ │ ├── source/ │ │ │ ├── dataset/ │ │ │ │ └── FileListerSourceOpDescSpec.scala │ │ │ ├── fetcher/ │ │ │ │ ├── URLFetcherOpDescSpec.scala │ │ │ │ └── URLFetcherOpExecSpec.scala │ │ │ └── scan/ │ │ │ ├── csv/ │ │ │ │ ├── CSVScanSourceOpDescSpec.scala │ │ │ │ └── CSVScanSourceOpExecSpec.scala │ │ │ ├── file/ │ │ │ │ ├── FileScanOpDescSpec.scala │ │ │ │ ├── FileScanSourceOpDescSpec.scala │ │ │ │ └── FileScanSourceOpExecSpec.scala │ │ │ └── text/ │ │ │ └── TextInputSourceOpDescSpec.scala │ │ ├── symmetricDifference/ │ │ │ └── SymmetricDifferenceOpExecSpec.scala │ │ ├── timeSeriesPlot/ │ │ │ └── TimeSeriesOpDescSpec.scala │ │ ├── typecasting/ │ │ │ └── TypeCastingOpExecSpec.scala │ │ ├── udf/ │ │ │ └── python/ │ │ │ └── PythonLambdaFunctionOpDescSpec.scala │ │ ├── unneststring/ │ │ │ └── UnnestStringOpExecSpec.scala │ │ └── visualization/ │ │ ├── DotPlot/ │ │ │ └── DotPlotOpDescSpec.scala │ │ ├── ImageViz/ │ │ │ └── ImageVisualizerOpDescSpec.scala │ │ ├── barChart/ │ │ │ └── BarChartOpDescSpec.scala │ │ ├── bubbleChart/ │ │ │ └── BubbleChartOpDescSpec.scala │ │ ├── bulletChart/ │ │ │ └── BulletChartOpDescSpec.scala │ │ ├── ecdfPlot/ │ │ │ └── ECDFPlotOpDescSpec.scala │ │ ├── filledAreaPlot/ │ │ │ └── FilledAreaPlotOpDescSpec.scala │ │ ├── funnelPlot/ │ │ │ └── FunnelPlotOpDescSpec.scala │ │ ├── ganttChart/ │ │ │ └── GanttChartOpDescSpec.scala │ │ ├── heatMap/ │ │ │ └── HeatMapOpDescSpec.scala │ │ ├── hierarchychart/ │ │ │ └── HierarchyChartOpDescSpec.scala │ │ ├── htmlviz/ │ │ │ └── HtmlVizOpExecSpec.scala │ │ ├── lineChart/ │ │ │ └── LineChartOpDescSpec.scala │ │ ├── pieChart/ │ │ │ └── PieChartOpDescSpec.scala │ │ ├── scatterplot/ │ │ │ └── ScatterPlotOpDescSpec.scala │ │ ├── volcanoPlot/ │ │ │ └── VolcanoPlotOpDescSpec.scala │ │ └── wordCloud/ │ │ └── WordCloudOpDescSpec.scala │ ├── pybuilder/ │ │ ├── DescriptorChecker.scala │ │ ├── PythonClassgraphScanner.scala │ │ ├── PythonConsoleCapture.scala │ │ ├── PythonRawTextReportRenderer.scala │ │ ├── PythonReflectionTextUtils.scala │ │ └── PythonReflectionUtils.scala │ └── util/ │ ├── ArrowUtilsSpec.scala │ └── PythonCodeRawInvalidTextSpec.scala ├── computing-unit-managing-service/ │ ├── LICENSE-binary │ ├── NOTICE-binary │ ├── build.sbt │ ├── project/ │ │ ├── build.properties │ │ └── plugins.sbt │ └── src/ │ └── main/ │ ├── resources/ │ │ └── computing-unit-managing-service-config.yaml │ └── scala/ │ └── org/ │ └── apache/ │ └── texera/ │ └── service/ │ ├── ComputingUnitManagingService.scala │ ├── ComputingUnitManagingServiceConfiguration.scala │ ├── resource/ │ │ ├── ComputingUnitAccessResource.scala │ │ ├── ComputingUnitManagingResource.scala │ │ ├── ComputingUnitState.scala │ │ └── HealthCheckResource.scala │ └── util/ │ ├── ComputingUnitHelpers.scala │ ├── ComputingUnitManagingServiceException.scala │ └── KubernetesClient.scala ├── config-service/ │ ├── LICENSE-binary │ ├── NOTICE-binary │ ├── build.sbt │ └── src/ │ └── main/ │ ├── resources/ │ │ └── config-service-web-config.yaml │ └── scala/ │ └── org/ │ └── apache/ │ └── texera/ │ └── service/ │ ├── ConfigService.scala │ ├── ConfigServiceConfiguration.scala │ └── resource/ │ ├── ConfigResource.scala │ └── HealthCheckResource.scala ├── file-service/ │ ├── LICENSE-binary │ ├── NOTICE-binary │ ├── build.sbt │ └── src/ │ ├── main/ │ │ ├── resources/ │ │ │ ├── docker-compose.yml │ │ │ ├── file-service-web-config.yaml │ │ │ └── minio-config.yml │ │ └── scala/ │ │ └── org/ │ │ └── apache/ │ │ └── texera/ │ │ └── service/ │ │ ├── FileService.scala │ │ ├── FileServiceConfiguration.scala │ │ ├── resource/ │ │ │ ├── DatasetAccessResource.scala │ │ │ ├── DatasetResource.scala │ │ │ └── HealthCheckResource.scala │ │ ├── type/ │ │ │ ├── dataset/ │ │ │ │ └── DatasetFileNode.scala │ │ │ └── serde/ │ │ │ └── DatasetFileNodeSerializer.java │ │ └── util/ │ │ └── LakeFSExceptionHandler.scala │ └── test/ │ └── scala/ │ └── org/ │ └── apache/ │ └── texera/ │ └── service/ │ ├── MockLakeFS.scala │ └── resource/ │ └── DatasetResourceSpec.scala ├── frontend/ │ ├── .editorconfig │ ├── .eslintrc.json │ ├── .gitignore │ ├── .nvmrc │ ├── .prettierignore │ ├── .prettierrc.json │ ├── .yarn/ │ │ └── releases/ │ │ └── yarn-4.14.1.cjs │ ├── .yarnrc.yml │ ├── LICENSE-binary │ ├── README.md │ ├── angular.json │ ├── custom-webpack.config.js │ ├── git-version.js │ ├── nx.json │ ├── package.json │ ├── proxy.config.json │ ├── src/ │ │ ├── app/ │ │ │ ├── app-routing.constant.ts │ │ │ ├── app-routing.module.ts │ │ │ ├── app.component.ts │ │ │ ├── app.module.ts │ │ │ ├── common/ │ │ │ │ ├── app-setting.ts │ │ │ │ ├── formly/ │ │ │ │ │ ├── array.type.ts │ │ │ │ │ ├── collab-wrapper/ │ │ │ │ │ │ └── collab-wrapper/ │ │ │ │ │ │ ├── collab-wrapper.component.css │ │ │ │ │ │ ├── collab-wrapper.component.html │ │ │ │ │ │ └── collab-wrapper.component.ts │ │ │ │ │ ├── formly-config.ts │ │ │ │ │ ├── formly-utils.ts │ │ │ │ │ ├── multischema.type.ts │ │ │ │ │ ├── null.type.ts │ │ │ │ │ ├── object.type.ts │ │ │ │ │ ├── preset-wrapper/ │ │ │ │ │ │ ├── preset-wrapper.component.html │ │ │ │ │ │ ├── preset-wrapper.component.scss │ │ │ │ │ │ ├── preset-wrapper.component.spec.ts │ │ │ │ │ │ └── preset-wrapper.component.ts │ │ │ │ │ └── repeat-dnd/ │ │ │ │ │ ├── repeat-dnd.component.css │ │ │ │ │ ├── repeat-dnd.component.html │ │ │ │ │ └── repeat-dnd.component.ts │ │ │ │ ├── service/ │ │ │ │ │ ├── blob-error-http-interceptor.service.ts │ │ │ │ │ ├── computing-unit/ │ │ │ │ │ │ ├── computing-unit-actions/ │ │ │ │ │ │ │ └── computing-unit-actions.service.ts │ │ │ │ │ │ ├── computing-unit-status/ │ │ │ │ │ │ │ ├── computing-unit-status.service.ts │ │ │ │ │ │ │ └── mock-computing-unit-status.service.ts │ │ │ │ │ │ └── workflow-computing-unit/ │ │ │ │ │ │ └── workflow-computing-unit-managing.service.ts │ │ │ │ │ ├── gui-config.service.mock.ts │ │ │ │ │ ├── gui-config.service.ts │ │ │ │ │ ├── notification/ │ │ │ │ │ │ ├── notification.service.spec.ts │ │ │ │ │ │ └── notification.service.ts │ │ │ │ │ ├── user/ │ │ │ │ │ │ ├── auth-guard.service.ts │ │ │ │ │ │ ├── auth.service.ts │ │ │ │ │ │ ├── config/ │ │ │ │ │ │ │ ├── user-config.service.spec.ts │ │ │ │ │ │ │ └── user-config.service.ts │ │ │ │ │ │ ├── google-auth.service.ts │ │ │ │ │ │ ├── registration-request-modal/ │ │ │ │ │ │ │ ├── registration-request-modal.component.html │ │ │ │ │ │ │ ├── registration-request-modal.component.scss │ │ │ │ │ │ │ └── registration-request-modal.component.ts │ │ │ │ │ │ ├── stub-auth.service.ts │ │ │ │ │ │ ├── stub-user.service.ts │ │ │ │ │ │ ├── user.service.spec.ts │ │ │ │ │ │ └── user.service.ts │ │ │ │ │ └── workflow-persist/ │ │ │ │ │ ├── stub-workflow-persist.service.ts │ │ │ │ │ ├── workflow-persist.service.spec.ts │ │ │ │ │ └── workflow-persist.service.ts │ │ │ │ ├── testing/ │ │ │ │ │ └── test-utils.ts │ │ │ │ ├── type/ │ │ │ │ │ ├── computing-unit-connection.interface.ts │ │ │ │ │ ├── dataset-file.ts │ │ │ │ │ ├── dataset-staged-object.ts │ │ │ │ │ ├── dataset.ts │ │ │ │ │ ├── datasetVersionFileTree.ts │ │ │ │ │ ├── execution.ts │ │ │ │ │ ├── generic-web-response.ts │ │ │ │ │ ├── gui-config.ts │ │ │ │ │ ├── physical-plan.ts │ │ │ │ │ ├── proto/ │ │ │ │ │ │ ├── google/ │ │ │ │ │ │ │ └── protobuf/ │ │ │ │ │ │ │ └── descriptor.ts │ │ │ │ │ │ ├── org/ │ │ │ │ │ │ │ └── apache/ │ │ │ │ │ │ │ └── texera/ │ │ │ │ │ │ │ └── amber/ │ │ │ │ │ │ │ └── core/ │ │ │ │ │ │ │ ├── virtualidentity.ts │ │ │ │ │ │ │ └── workflow.ts │ │ │ │ │ │ └── scalapb/ │ │ │ │ │ │ └── scalapb.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ ├── workflow-computing-unit.ts │ │ │ │ │ └── workflow.ts │ │ │ │ └── util/ │ │ │ │ ├── array-utils.ts │ │ │ │ ├── assert.ts │ │ │ │ ├── computing-unit.util.ts │ │ │ │ ├── context.ts │ │ │ │ ├── error.ts │ │ │ │ ├── format.util.ts │ │ │ │ ├── logical-operator-port-serde.ts │ │ │ │ ├── map.ts │ │ │ │ ├── panel-dock.ts │ │ │ │ ├── port-identity-serde.ts │ │ │ │ ├── predicate.ts │ │ │ │ ├── set.ts │ │ │ │ ├── size-formatter.util.spec.ts │ │ │ │ ├── size-formatter.util.ts │ │ │ │ ├── storage.ts │ │ │ │ ├── stub.ts │ │ │ │ ├── switch.ts │ │ │ │ ├── url.ts │ │ │ │ ├── workflow-check.ts │ │ │ │ └── workflow-compilation-utils.ts │ │ │ ├── dashboard/ │ │ │ │ ├── component/ │ │ │ │ │ ├── admin/ │ │ │ │ │ │ ├── execution/ │ │ │ │ │ │ │ ├── admin-execution.component.html │ │ │ │ │ │ │ ├── admin-execution.component.scss │ │ │ │ │ │ │ ├── admin-execution.component.spec.ts │ │ │ │ │ │ │ └── admin-execution.component.ts │ │ │ │ │ │ ├── settings/ │ │ │ │ │ │ │ ├── admin-settings.component.html │ │ │ │ │ │ │ ├── admin-settings.component.scss │ │ │ │ │ │ │ ├── admin-settings.component.spec.ts │ │ │ │ │ │ │ └── admin-settings.component.ts │ │ │ │ │ │ └── user/ │ │ │ │ │ │ ├── admin-user.component.html │ │ │ │ │ │ ├── admin-user.component.scss │ │ │ │ │ │ ├── admin-user.component.spec.ts │ │ │ │ │ │ └── admin-user.component.ts │ │ │ │ │ ├── button-style.scss │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ ├── dashboard.component.spec.ts │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ ├── section-style.scss │ │ │ │ │ ├── user/ │ │ │ │ │ │ ├── files-uploader/ │ │ │ │ │ │ │ ├── conflicting-file-modal-content/ │ │ │ │ │ │ │ │ ├── conflicting-file-modal-content.component.html │ │ │ │ │ │ │ │ ├── conflicting-file-modal-content.component.scss │ │ │ │ │ │ │ │ └── conflicting-file-modal-content.component.ts │ │ │ │ │ │ │ ├── files-uploader.component.html │ │ │ │ │ │ │ ├── files-uploader.component.scss │ │ │ │ │ │ │ └── files-uploader.component.ts │ │ │ │ │ │ ├── filters/ │ │ │ │ │ │ │ ├── filters.component.html │ │ │ │ │ │ │ ├── filters.component.scss │ │ │ │ │ │ │ ├── filters.component.spec.ts │ │ │ │ │ │ │ └── filters.component.ts │ │ │ │ │ │ ├── filters-instructions/ │ │ │ │ │ │ │ ├── filters-instructions.component.html │ │ │ │ │ │ │ ├── filters-instructions.component.spec.ts │ │ │ │ │ │ │ └── filters-instructions.component.ts │ │ │ │ │ │ ├── flarum/ │ │ │ │ │ │ │ ├── flarum.component.html │ │ │ │ │ │ │ └── flarum.component.ts │ │ │ │ │ │ ├── list-item/ │ │ │ │ │ │ │ ├── list-item.component.html │ │ │ │ │ │ │ ├── list-item.component.scss │ │ │ │ │ │ │ ├── list-item.component.spec.ts │ │ │ │ │ │ │ └── list-item.component.ts │ │ │ │ │ │ ├── markdown-description/ │ │ │ │ │ │ │ ├── markdown-description.component.html │ │ │ │ │ │ │ ├── markdown-description.component.scss │ │ │ │ │ │ │ └── markdown-description.component.ts │ │ │ │ │ │ ├── search/ │ │ │ │ │ │ │ ├── search.component.html │ │ │ │ │ │ │ ├── search.component.scss │ │ │ │ │ │ │ └── search.component.ts │ │ │ │ │ │ ├── search-bar/ │ │ │ │ │ │ │ ├── search-bar.component.html │ │ │ │ │ │ │ ├── search-bar.component.scss │ │ │ │ │ │ │ └── search-bar.component.ts │ │ │ │ │ │ ├── search-results/ │ │ │ │ │ │ │ ├── search-results.component.html │ │ │ │ │ │ │ ├── search-results.component.scss │ │ │ │ │ │ │ └── search-results.component.ts │ │ │ │ │ │ ├── share-access/ │ │ │ │ │ │ │ ├── share-access.component.html │ │ │ │ │ │ │ ├── share-access.component.scss │ │ │ │ │ │ │ └── share-access.component.ts │ │ │ │ │ │ ├── sort-button/ │ │ │ │ │ │ │ ├── sort-button.component.html │ │ │ │ │ │ │ ├── sort-button.component.scss │ │ │ │ │ │ │ └── sort-button.component.ts │ │ │ │ │ │ ├── user-avatar/ │ │ │ │ │ │ │ ├── user-avatar.component.html │ │ │ │ │ │ │ ├── user-avatar.component.scss │ │ │ │ │ │ │ ├── user-avatar.component.spec.ts │ │ │ │ │ │ │ └── user-avatar.component.ts │ │ │ │ │ │ ├── user-computing-unit/ │ │ │ │ │ │ │ ├── user-computing-unit-list-item/ │ │ │ │ │ │ │ │ ├── user-computing-unit-list-item.component.html │ │ │ │ │ │ │ │ ├── user-computing-unit-list-item.component.scss │ │ │ │ │ │ │ │ └── user-computing-unit-list-item.component.ts │ │ │ │ │ │ │ ├── user-computing-unit.component.html │ │ │ │ │ │ │ ├── user-computing-unit.component.scss │ │ │ │ │ │ │ ├── user-computing-unit.component.spec.ts │ │ │ │ │ │ │ └── user-computing-unit.component.ts │ │ │ │ │ │ ├── user-dataset/ │ │ │ │ │ │ │ ├── user-dataset-explorer/ │ │ │ │ │ │ │ │ ├── dataset-detail.component.html │ │ │ │ │ │ │ │ ├── dataset-detail.component.scss │ │ │ │ │ │ │ │ ├── dataset-detail.component.ts │ │ │ │ │ │ │ │ ├── user-dataset-file-renderer/ │ │ │ │ │ │ │ │ │ ├── user-dataset-file-renderer.component.html │ │ │ │ │ │ │ │ │ ├── user-dataset-file-renderer.component.scss │ │ │ │ │ │ │ │ │ ├── user-dataset-file-renderer.component.spec.ts │ │ │ │ │ │ │ │ │ └── user-dataset-file-renderer.component.ts │ │ │ │ │ │ │ │ ├── user-dataset-staged-objects-list/ │ │ │ │ │ │ │ │ │ ├── user-dataset-staged-objects-list.component.html │ │ │ │ │ │ │ │ │ ├── user-dataset-staged-objects-list.component.scss │ │ │ │ │ │ │ │ │ └── user-dataset-staged-objects-list.component.ts │ │ │ │ │ │ │ │ ├── user-dataset-version-creator/ │ │ │ │ │ │ │ │ │ ├── user-dataset-version-creator.component.html │ │ │ │ │ │ │ │ │ ├── user-dataset-version-creator.component.scss │ │ │ │ │ │ │ │ │ └── user-dataset-version-creator.component.ts │ │ │ │ │ │ │ │ └── user-dataset-version-filetree/ │ │ │ │ │ │ │ │ ├── user-dataset-version-filetree.component.html │ │ │ │ │ │ │ │ ├── user-dataset-version-filetree.component.scss │ │ │ │ │ │ │ │ └── user-dataset-version-filetree.component.ts │ │ │ │ │ │ │ ├── user-dataset-list-item/ │ │ │ │ │ │ │ │ ├── user-dataset-list-item.component.html │ │ │ │ │ │ │ │ ├── user-dataset-list-item.component.scss │ │ │ │ │ │ │ │ └── user-dataset-list-item.component.ts │ │ │ │ │ │ │ ├── user-dataset.component.html │ │ │ │ │ │ │ ├── user-dataset.component.scss │ │ │ │ │ │ │ └── user-dataset.component.ts │ │ │ │ │ │ ├── user-icon/ │ │ │ │ │ │ │ ├── user-icon.component.html │ │ │ │ │ │ │ ├── user-icon.component.scss │ │ │ │ │ │ │ ├── user-icon.component.spec.ts │ │ │ │ │ │ │ └── user-icon.component.ts │ │ │ │ │ │ ├── user-project/ │ │ │ │ │ │ │ ├── public-project/ │ │ │ │ │ │ │ │ ├── public-project.component.html │ │ │ │ │ │ │ │ └── public-project.component.ts │ │ │ │ │ │ │ ├── user-project-list-item/ │ │ │ │ │ │ │ │ ├── user-project-list-item.component.html │ │ │ │ │ │ │ │ ├── user-project-list-item.component.scss │ │ │ │ │ │ │ │ ├── user-project-list-item.component.spec.ts │ │ │ │ │ │ │ │ └── user-project-list-item.component.ts │ │ │ │ │ │ │ ├── user-project-section/ │ │ │ │ │ │ │ │ ├── ngbd-modal-add-project-workflow/ │ │ │ │ │ │ │ │ │ ├── ngbd-modal-add-project-workflow.component.html │ │ │ │ │ │ │ │ │ ├── ngbd-modal-add-project-workflow.component.scss │ │ │ │ │ │ │ │ │ └── ngbd-modal-add-project-workflow.component.ts │ │ │ │ │ │ │ │ ├── ngbd-modal-remove-project-workflow/ │ │ │ │ │ │ │ │ │ ├── ngbd-modal-remove-project-workflow.component.html │ │ │ │ │ │ │ │ │ ├── ngbd-modal-remove-project-workflow.component.scss │ │ │ │ │ │ │ │ │ └── ngbd-modal-remove-project-workflow.component.ts │ │ │ │ │ │ │ │ ├── user-project-section.component.html │ │ │ │ │ │ │ │ ├── user-project-section.component.scss │ │ │ │ │ │ │ │ └── user-project-section.component.ts │ │ │ │ │ │ │ ├── user-project.component.html │ │ │ │ │ │ │ ├── user-project.component.scss │ │ │ │ │ │ │ └── user-project.component.ts │ │ │ │ │ │ ├── user-quota/ │ │ │ │ │ │ │ ├── user-quota.component.html │ │ │ │ │ │ │ ├── user-quota.component.scss │ │ │ │ │ │ │ ├── user-quota.component.spec.ts │ │ │ │ │ │ │ └── user-quota.component.ts │ │ │ │ │ │ └── user-workflow/ │ │ │ │ │ │ ├── ngbd-modal-workflow-executions/ │ │ │ │ │ │ │ ├── workflow-execution-history.component.html │ │ │ │ │ │ │ ├── workflow-execution-history.component.scss │ │ │ │ │ │ │ ├── workflow-execution-history.component.ts │ │ │ │ │ │ │ └── workflow-runtime-statistics/ │ │ │ │ │ │ │ ├── workflow-runtime-statistics.component.html │ │ │ │ │ │ │ ├── workflow-runtime-statistics.component.scss │ │ │ │ │ │ │ └── workflow-runtime-statistics.component.ts │ │ │ │ │ │ ├── user-workflow-list-item/ │ │ │ │ │ │ │ ├── highlight-search-terms.pipe.ts │ │ │ │ │ │ │ ├── user-workflow-list-item.component.html │ │ │ │ │ │ │ ├── user-workflow-list-item.component.scss │ │ │ │ │ │ │ ├── user-workflow-list-item.component.spec.ts │ │ │ │ │ │ │ └── user-workflow-list-item.component.ts │ │ │ │ │ │ ├── user-workflow.component.html │ │ │ │ │ │ ├── user-workflow.component.scss │ │ │ │ │ │ ├── user-workflow.component.spec.ts │ │ │ │ │ │ └── user-workflow.component.ts │ │ │ │ │ └── user-dashboard-test-fixtures.ts │ │ │ │ ├── service/ │ │ │ │ │ ├── admin/ │ │ │ │ │ │ ├── execution/ │ │ │ │ │ │ │ └── admin-execution.service.ts │ │ │ │ │ │ ├── guard/ │ │ │ │ │ │ │ └── admin-guard.service.ts │ │ │ │ │ │ ├── settings/ │ │ │ │ │ │ │ └── admin-settings.service.ts │ │ │ │ │ │ └── user/ │ │ │ │ │ │ └── admin-user.service.ts │ │ │ │ │ └── user/ │ │ │ │ │ ├── dataset/ │ │ │ │ │ │ └── dataset.service.ts │ │ │ │ │ ├── download/ │ │ │ │ │ │ ├── download.service.spec.ts │ │ │ │ │ │ └── download.service.ts │ │ │ │ │ ├── file/ │ │ │ │ │ │ └── file-saver.service.ts │ │ │ │ │ ├── flarum/ │ │ │ │ │ │ └── flarum.service.ts │ │ │ │ │ ├── project/ │ │ │ │ │ │ ├── stub-user-project.service.ts │ │ │ │ │ │ └── user-project.service.ts │ │ │ │ │ ├── public-project/ │ │ │ │ │ │ └── public-project.service.ts │ │ │ │ │ ├── quota/ │ │ │ │ │ │ └── user-quota.service.ts │ │ │ │ │ ├── search.service.ts │ │ │ │ │ ├── share-access/ │ │ │ │ │ │ └── share-access.service.ts │ │ │ │ │ ├── stub-search.service.ts │ │ │ │ │ ├── workflow-executions/ │ │ │ │ │ │ ├── workflow-executions.service.spec.ts │ │ │ │ │ │ └── workflow-executions.service.ts │ │ │ │ │ ├── workflow-snapshot/ │ │ │ │ │ │ └── workflow-snapshot.service.ts │ │ │ │ │ └── workflow-version/ │ │ │ │ │ ├── workflow-version.service.spec.ts │ │ │ │ │ └── workflow-version.service.ts │ │ │ │ └── type/ │ │ │ │ ├── dashboard-dataset.interface.ts │ │ │ │ ├── dashboard-entry.ts │ │ │ │ ├── dashboard-file.interface.ts │ │ │ │ ├── dashboard-project.interface.ts │ │ │ │ ├── dashboard-workflow.interface.ts │ │ │ │ ├── google-api-response.ts │ │ │ │ ├── quota-statistic.interface.ts │ │ │ │ ├── search-filter-parameters.ts │ │ │ │ ├── search-result.ts │ │ │ │ ├── share-access.interface.ts │ │ │ │ ├── sort-method.ts │ │ │ │ ├── type-predicates.ts │ │ │ │ ├── workflow-executions-entry.ts │ │ │ │ ├── workflow-metadata.interface.ts │ │ │ │ ├── workflow-runtime-statistics.ts │ │ │ │ ├── workflow-snapshot-entry.ts │ │ │ │ └── workflow-version-entry.ts │ │ │ ├── hub/ │ │ │ │ ├── component/ │ │ │ │ │ ├── about/ │ │ │ │ │ │ ├── about.component.html │ │ │ │ │ │ ├── about.component.scss │ │ │ │ │ │ ├── about.component.spec.ts │ │ │ │ │ │ ├── about.component.ts │ │ │ │ │ │ └── local-login/ │ │ │ │ │ │ ├── local-login.component.html │ │ │ │ │ │ ├── local-login.component.scss │ │ │ │ │ │ └── local-login.component.ts │ │ │ │ │ ├── browse-section/ │ │ │ │ │ │ ├── browse-section.component.html │ │ │ │ │ │ ├── browse-section.component.scss │ │ │ │ │ │ ├── browse-section.component.spec.ts │ │ │ │ │ │ └── browse-section.component.ts │ │ │ │ │ ├── hub-search-result/ │ │ │ │ │ │ ├── hub-search-result.component.html │ │ │ │ │ │ ├── hub-search-result.component.scss │ │ │ │ │ │ └── hub-search-result.component.ts │ │ │ │ │ ├── hub.component.html │ │ │ │ │ ├── hub.component.scss │ │ │ │ │ ├── hub.component.ts │ │ │ │ │ ├── landing-page/ │ │ │ │ │ │ ├── landing-page.component.html │ │ │ │ │ │ ├── landing-page.component.scss │ │ │ │ │ │ └── landing-page.component.ts │ │ │ │ │ ├── type/ │ │ │ │ │ │ └── hub-workflow.interface.ts │ │ │ │ │ └── workflow/ │ │ │ │ │ └── detail/ │ │ │ │ │ ├── hub-workflow-detail.component.html │ │ │ │ │ ├── hub-workflow-detail.component.scss │ │ │ │ │ └── hub-workflow-detail.component.ts │ │ │ │ └── service/ │ │ │ │ └── hub.service.ts │ │ │ └── workspace/ │ │ │ ├── component/ │ │ │ │ ├── agent/ │ │ │ │ │ ├── agent-interaction/ │ │ │ │ │ │ ├── agent-interaction.component.html │ │ │ │ │ │ ├── agent-interaction.component.scss │ │ │ │ │ │ └── agent-interaction.component.ts │ │ │ │ │ └── agent-panel/ │ │ │ │ │ ├── agent-chat/ │ │ │ │ │ │ ├── agent-chat.component.html │ │ │ │ │ │ ├── agent-chat.component.scss │ │ │ │ │ │ └── agent-chat.component.ts │ │ │ │ │ ├── agent-panel.component.html │ │ │ │ │ ├── agent-panel.component.scss │ │ │ │ │ ├── agent-panel.component.ts │ │ │ │ │ ├── agent-registration/ │ │ │ │ │ │ ├── agent-registration.component.html │ │ │ │ │ │ ├── agent-registration.component.scss │ │ │ │ │ │ └── agent-registration.component.ts │ │ │ │ │ └── react-step-detail-modal/ │ │ │ │ │ ├── react-step-detail-modal.component.html │ │ │ │ │ ├── react-step-detail-modal.component.scss │ │ │ │ │ └── react-step-detail-modal.component.ts │ │ │ │ ├── code-editor-dialog/ │ │ │ │ │ ├── annotation-suggestion.component.html │ │ │ │ │ ├── annotation-suggestion.component.scss │ │ │ │ │ ├── annotation-suggestion.component.spec.ts │ │ │ │ │ ├── annotation-suggestion.component.ts │ │ │ │ │ ├── breakpoint-condition-input/ │ │ │ │ │ │ ├── breakpoint-condition-input.component.html │ │ │ │ │ │ ├── breakpoint-condition-input.component.scss │ │ │ │ │ │ ├── breakpoint-condition-input.component.spec.ts │ │ │ │ │ │ └── breakpoint-condition-input.component.ts │ │ │ │ │ ├── code-debugger.component.html │ │ │ │ │ ├── code-debugger.component.spec.ts │ │ │ │ │ ├── code-debugger.component.ts │ │ │ │ │ ├── code-editor.component.html │ │ │ │ │ ├── code-editor.component.scss │ │ │ │ │ ├── code-editor.component.spec.ts │ │ │ │ │ └── code-editor.component.ts │ │ │ │ ├── codearea-custom-template/ │ │ │ │ │ ├── codearea-custom-template.component.html │ │ │ │ │ ├── codearea-custom-template.component.scss │ │ │ │ │ ├── codearea-custom-template.component.spec.ts │ │ │ │ │ └── codearea-custom-template.component.ts │ │ │ │ ├── dataset-file-selector/ │ │ │ │ │ ├── dataset-file-selector.component.html │ │ │ │ │ └── dataset-file-selector.component.ts │ │ │ │ ├── dataset-selection-modal/ │ │ │ │ │ ├── dataset-selection-modal.component.html │ │ │ │ │ ├── dataset-selection-modal.component.scss │ │ │ │ │ └── dataset-selection-modal.component.ts │ │ │ │ ├── dataset-version-selector/ │ │ │ │ │ ├── dataset-version-selector.component.html │ │ │ │ │ └── dataset-version-selector.component.ts │ │ │ │ ├── left-panel/ │ │ │ │ │ ├── environment/ │ │ │ │ │ │ └── environment.component.ts │ │ │ │ │ ├── left-panel.component.html │ │ │ │ │ ├── left-panel.component.scss │ │ │ │ │ ├── left-panel.component.spec.ts │ │ │ │ │ ├── left-panel.component.ts │ │ │ │ │ ├── operator-menu/ │ │ │ │ │ │ ├── operator-label/ │ │ │ │ │ │ │ ├── operator-label.component.html │ │ │ │ │ │ │ ├── operator-label.component.scss │ │ │ │ │ │ │ ├── operator-label.component.spec.ts │ │ │ │ │ │ │ └── operator-label.component.ts │ │ │ │ │ │ ├── operator-menu.component.html │ │ │ │ │ │ ├── operator-menu.component.scss │ │ │ │ │ │ ├── operator-menu.component.spec.ts │ │ │ │ │ │ └── operator-menu.component.ts │ │ │ │ │ ├── settings/ │ │ │ │ │ │ ├── settings.component.html │ │ │ │ │ │ ├── settings.component.scss │ │ │ │ │ │ ├── settings.component.spec.ts │ │ │ │ │ │ └── settings.component.ts │ │ │ │ │ ├── time-travel/ │ │ │ │ │ │ ├── time-travel.component.html │ │ │ │ │ │ ├── time-travel.component.scss │ │ │ │ │ │ ├── time-travel.component.spec.ts │ │ │ │ │ │ └── time-travel.component.ts │ │ │ │ │ └── versions-list/ │ │ │ │ │ ├── versions-list.component.html │ │ │ │ │ ├── versions-list.component.scss │ │ │ │ │ ├── versions-list.component.spec.ts │ │ │ │ │ └── versions-list.component.ts │ │ │ │ ├── menu/ │ │ │ │ │ ├── coeditor-user-icon/ │ │ │ │ │ │ ├── coeditor-user-icon.component.css │ │ │ │ │ │ ├── coeditor-user-icon.component.html │ │ │ │ │ │ ├── coeditor-user-icon.component.spec.ts │ │ │ │ │ │ └── coeditor-user-icon.component.ts │ │ │ │ │ ├── menu.component.html │ │ │ │ │ ├── menu.component.scss │ │ │ │ │ ├── menu.component.spec.ts │ │ │ │ │ └── menu.component.ts │ │ │ │ ├── power-button/ │ │ │ │ │ ├── computing-unit-selection.component.html │ │ │ │ │ ├── computing-unit-selection.component.scss │ │ │ │ │ ├── computing-unit-selection.component.spec.ts │ │ │ │ │ └── computing-unit-selection.component.ts │ │ │ │ ├── property-editor/ │ │ │ │ │ ├── operator-property-edit-frame/ │ │ │ │ │ │ ├── operator-property-edit-frame.component.html │ │ │ │ │ │ ├── operator-property-edit-frame.component.scss │ │ │ │ │ │ ├── operator-property-edit-frame.component.spec.ts │ │ │ │ │ │ └── operator-property-edit-frame.component.ts │ │ │ │ │ ├── port-property-edit-frame/ │ │ │ │ │ │ ├── port-property-edit-frame.component.html │ │ │ │ │ │ ├── port-property-edit-frame.component.scss │ │ │ │ │ │ ├── port-property-edit-frame.component.spec.ts │ │ │ │ │ │ └── port-property-edit-frame.component.ts │ │ │ │ │ ├── property-editor.component.html │ │ │ │ │ ├── property-editor.component.scss │ │ │ │ │ ├── property-editor.component.spec.ts │ │ │ │ │ ├── property-editor.component.ts │ │ │ │ │ └── typecasting-display/ │ │ │ │ │ ├── type-casting-display.component.html │ │ │ │ │ ├── type-casting-display.component.spec.ts │ │ │ │ │ └── type-casting-display.component.ts │ │ │ │ ├── result-exportation/ │ │ │ │ │ ├── result-exportation.component.html │ │ │ │ │ ├── result-exportation.component.scss │ │ │ │ │ └── result-exportation.component.ts │ │ │ │ ├── result-panel/ │ │ │ │ │ ├── console-frame/ │ │ │ │ │ │ ├── console-frame.component.html │ │ │ │ │ │ ├── console-frame.component.scss │ │ │ │ │ │ ├── console-frame.component.spec.ts │ │ │ │ │ │ └── console-frame.component.ts │ │ │ │ │ ├── error-frame/ │ │ │ │ │ │ ├── error-frame.component.html │ │ │ │ │ │ ├── error-frame.component.scss │ │ │ │ │ │ ├── error-frame.component.spec.ts │ │ │ │ │ │ └── error-frame.component.ts │ │ │ │ │ ├── result-panel-modal.component.html │ │ │ │ │ ├── result-panel-modal.component.ts │ │ │ │ │ ├── result-panel-model.component.scss │ │ │ │ │ ├── result-panel.component.html │ │ │ │ │ ├── result-panel.component.scss │ │ │ │ │ ├── result-panel.component.spec.ts │ │ │ │ │ ├── result-panel.component.ts │ │ │ │ │ └── result-table-frame/ │ │ │ │ │ ├── result-table-frame.component.html │ │ │ │ │ ├── result-table-frame.component.scss │ │ │ │ │ ├── result-table-frame.component.spec.ts │ │ │ │ │ └── result-table-frame.component.ts │ │ │ │ ├── visualization-panel-content/ │ │ │ │ │ ├── visualization-frame-content.component.html │ │ │ │ │ ├── visualization-frame-content.component.scss │ │ │ │ │ └── visualization-frame-content.component.ts │ │ │ │ ├── workflow-editor/ │ │ │ │ │ ├── comment-box-modal/ │ │ │ │ │ │ ├── nz-modal-comment-box.component.html │ │ │ │ │ │ ├── nz-modal-comment-box.component.scss │ │ │ │ │ │ └── nz-modal-comment-box.component.ts │ │ │ │ │ ├── context-menu/ │ │ │ │ │ │ └── context-menu/ │ │ │ │ │ │ ├── context-menu.component.html │ │ │ │ │ │ ├── context-menu.component.scss │ │ │ │ │ │ ├── context-menu.component.spec.ts │ │ │ │ │ │ └── context-menu.component.ts │ │ │ │ │ ├── mini-map/ │ │ │ │ │ │ ├── mini-map.component.html │ │ │ │ │ │ ├── mini-map.component.scss │ │ │ │ │ │ ├── mini-map.component.spec.ts │ │ │ │ │ │ └── mini-map.component.ts │ │ │ │ │ ├── workflow-editor.component.html │ │ │ │ │ ├── workflow-editor.component.scss │ │ │ │ │ ├── workflow-editor.component.spec.ts │ │ │ │ │ └── workflow-editor.component.ts │ │ │ │ ├── workspace.component.html │ │ │ │ ├── workspace.component.scss │ │ │ │ ├── workspace.component.spec.ts │ │ │ │ └── workspace.component.ts │ │ │ ├── service/ │ │ │ │ ├── agent/ │ │ │ │ │ ├── agent-types.ts │ │ │ │ │ └── agent.service.ts │ │ │ │ ├── ai-analyst/ │ │ │ │ │ └── ai-analyst.service.ts │ │ │ │ ├── ai-assistant/ │ │ │ │ │ └── ai-assistant.service.ts │ │ │ │ ├── code-editor/ │ │ │ │ │ └── code-editor.service.ts │ │ │ │ ├── compile-workflow/ │ │ │ │ │ └── workflow-compiling.service.ts │ │ │ │ ├── drag-drop/ │ │ │ │ │ ├── drag-drop.service.spec.ts │ │ │ │ │ └── drag-drop.service.ts │ │ │ │ ├── dynamic-schema/ │ │ │ │ │ ├── dynamic-schema.service.spec.ts │ │ │ │ │ └── dynamic-schema.service.ts │ │ │ │ ├── execute-workflow/ │ │ │ │ │ ├── execute-workflow.service.spec.ts │ │ │ │ │ ├── execute-workflow.service.ts │ │ │ │ │ ├── mock-result-data.ts │ │ │ │ │ └── mock-workflow-plan.ts │ │ │ │ ├── joint-ui/ │ │ │ │ │ ├── joint-ui.service.spec.ts │ │ │ │ │ └── joint-ui.service.ts │ │ │ │ ├── operator-debug/ │ │ │ │ │ ├── udf-debug.service.spec.ts │ │ │ │ │ └── udf-debug.service.ts │ │ │ │ ├── operator-menu/ │ │ │ │ │ ├── operator-menu.service.spec.ts │ │ │ │ │ └── operator-menu.service.ts │ │ │ │ ├── operator-metadata/ │ │ │ │ │ ├── mock-operator-metadata.data.ts │ │ │ │ │ ├── operator-metadata.service.spec.ts │ │ │ │ │ ├── operator-metadata.service.ts │ │ │ │ │ └── stub-operator-metadata.service.ts │ │ │ │ ├── panel/ │ │ │ │ │ └── panel.service.ts │ │ │ │ ├── preset/ │ │ │ │ │ ├── preset.service.spec.ts │ │ │ │ │ └── preset.service.ts │ │ │ │ ├── report-generation/ │ │ │ │ │ └── report-generation.service.ts │ │ │ │ ├── undo-redo/ │ │ │ │ │ ├── undo-redo.service.spec.ts │ │ │ │ │ └── undo-redo.service.ts │ │ │ │ ├── validation/ │ │ │ │ │ ├── validation-workflow.service.spec.ts │ │ │ │ │ └── validation-workflow.service.ts │ │ │ │ ├── virtual-environment/ │ │ │ │ │ └── virtual-environment.service.ts │ │ │ │ ├── workflow-console/ │ │ │ │ │ ├── workflow-console.service.spec.ts │ │ │ │ │ └── workflow-console.service.ts │ │ │ │ ├── workflow-graph/ │ │ │ │ │ ├── model/ │ │ │ │ │ │ ├── coeditor-presence.service.spec.ts │ │ │ │ │ │ ├── coeditor-presence.service.ts │ │ │ │ │ │ ├── joint-graph-wrapper.spec.ts │ │ │ │ │ │ ├── joint-graph-wrapper.ts │ │ │ │ │ │ ├── mock-workflow-data.ts │ │ │ │ │ │ ├── shared-model-change-handler.ts │ │ │ │ │ │ ├── shared-model.ts │ │ │ │ │ │ ├── sync-texera-model.spec.ts │ │ │ │ │ │ ├── sync-texera-model.ts │ │ │ │ │ │ ├── workflow-action.service.spec.ts │ │ │ │ │ │ ├── workflow-action.service.ts │ │ │ │ │ │ ├── workflow-graph.spec.ts │ │ │ │ │ │ └── workflow-graph.ts │ │ │ │ │ └── util/ │ │ │ │ │ ├── workflow-util.service.spec.ts │ │ │ │ │ └── workflow-util.service.ts │ │ │ │ ├── workflow-result/ │ │ │ │ │ ├── panel-resize/ │ │ │ │ │ │ ├── panel-resize.service.spec.ts │ │ │ │ │ │ └── panel-resize.service.ts │ │ │ │ │ ├── workflow-result.service.spec.ts │ │ │ │ │ └── workflow-result.service.ts │ │ │ │ ├── workflow-result-export/ │ │ │ │ │ ├── workflow-result-export.service.spec.ts │ │ │ │ │ └── workflow-result-export.service.ts │ │ │ │ ├── workflow-status/ │ │ │ │ │ ├── operator-reuse-cache-status.service.spec.ts │ │ │ │ │ ├── operator-reuse-cache-status.service.ts │ │ │ │ │ └── workflow-status.service.ts │ │ │ │ └── workflow-websocket/ │ │ │ │ ├── workflow-websocket.service.spec.ts │ │ │ │ └── workflow-websocket.service.ts │ │ │ └── types/ │ │ │ ├── collab-websocket.interface.ts │ │ │ ├── custom-json-schema.interface.ts │ │ │ ├── execute-workflow.interface.ts │ │ │ ├── operator-schema.interface.ts │ │ │ ├── result-table.interface.ts │ │ │ ├── shared-editing.interface.ts │ │ │ ├── workflow-common.interface.ts │ │ │ ├── workflow-compiling.interface.ts │ │ │ └── workflow-websocket.interface.ts │ │ ├── assets/ │ │ │ ├── .gitkeep │ │ │ └── logos/ │ │ │ └── site.webmanifest │ │ ├── environments/ │ │ │ ├── environment.default.ts │ │ │ ├── environment.prod.ts │ │ │ ├── environment.test.ts │ │ │ └── environment.ts │ │ ├── index.html │ │ ├── jsdom-svg-polyfill.ts │ │ ├── main.test.ts │ │ ├── main.ts │ │ ├── styles.scss │ │ ├── test-zone-setup.ts │ │ ├── tsconfig.app.json │ │ ├── tsconfig.spec.json │ │ ├── tsconfig.test.json │ │ └── vitest-globals.d.ts │ ├── tools/ │ │ └── jschardet-stub/ │ │ ├── index.js │ │ └── package.json │ ├── tsconfig.json │ ├── vitest.browser.config.ts │ └── vitest.config.ts ├── licenses/ │ ├── LICENSE-0BSD.txt │ ├── LICENSE-BSD-2-Clause.txt │ ├── LICENSE-BSD-3-Clause.txt │ ├── LICENSE-CDDL-1.0.txt │ ├── LICENSE-CDDL-1.1.txt │ ├── LICENSE-EDL-1.0.txt │ ├── LICENSE-EPL-1.0.txt │ ├── LICENSE-EPL-2.0.txt │ ├── LICENSE-ISC.txt │ ├── LICENSE-MIT-CMU.txt │ ├── LICENSE-MIT.txt │ ├── LICENSE-MPL-2.0.txt │ ├── LICENSE-PSF-2.0.txt │ ├── LICENSE-Unlicense.txt │ ├── LICENSE-avro.txt │ ├── LICENSE-aws-sdk.txt │ ├── LICENSE-awssdk-third-party-jackson.txt │ ├── LICENSE-commons-math3.txt │ ├── LICENSE-glassfish-hk2.txt │ ├── LICENSE-guice.txt │ ├── LICENSE-hadoop-shaded.txt │ ├── LICENSE-hadoop.txt │ ├── LICENSE-iceberg-bundled-guava.txt │ ├── LICENSE-iceberg.txt │ ├── LICENSE-jackson-afterburner.txt │ ├── LICENSE-jackson-blackbird.txt │ ├── LICENSE-jackson-core.txt │ ├── LICENSE-jakarta-ee.txt │ ├── LICENSE-javax-activation.txt │ ├── LICENSE-javax-ee-cddl.txt │ ├── LICENSE-javax-mail.txt │ ├── LICENSE-jaxb-api.txt │ ├── LICENSE-jersey.txt │ ├── LICENSE-jetty-11.0.txt │ ├── LICENSE-jetty-9.4.txt │ ├── LICENSE-jetty-jakarta-servlet-api.txt │ ├── LICENSE-lombok.txt │ ├── LICENSE-lucene.txt │ ├── LICENSE-netty-tcnative-boringssl.txt │ ├── LICENSE-netty.txt │ ├── LICENSE-pekko-actor.txt │ ├── LICENSE-pekko-cluster.txt │ ├── LICENSE-pekko-protobuf-v3.txt │ ├── LICENSE-pekko-remote.txt │ ├── LICENSE-postgresql.txt │ ├── LICENSE-slf4j.txt │ └── LICENSE-threeten-extra.txt ├── licenses-3rd-party-code/ │ ├── angular.md │ ├── mbknor-jackson-jsonschema.txt │ └── monaco-languageclient.txt ├── project/ │ ├── AddMetaInfLicenseFiles.scala │ ├── JdkOptions.scala │ ├── build.properties │ └── plugins.sbt ├── pyright-language-service/ │ ├── README.md │ ├── package.json │ ├── src/ │ │ ├── config.json │ │ ├── language-server-runner.ts │ │ ├── main.ts │ │ ├── server-commons.ts │ │ └── types/ │ │ └── hocon-parser.d.ts │ └── tsconfig.json ├── sql/ │ ├── changelog.xml │ ├── docker-compose.yml │ ├── iceberg_postgres_catalog.sql │ ├── misc/ │ │ └── tweets.sql │ ├── texera_ddl.sql │ ├── texera_lakefs.sql │ ├── texera_lakekeeper.sql │ └── updates/ │ ├── 01.sql │ ├── 02.sql │ ├── 03.sql │ ├── 04.sql │ ├── 05.sql │ ├── 06.sql │ ├── 07.sql │ ├── 08.sql │ ├── 09.sql │ ├── 10.sql │ ├── 11.sql │ ├── 12.sql │ ├── 13.sql │ ├── 14.sql │ ├── 15.sql │ ├── 16.sql │ ├── 17.sql │ ├── 18.sql │ ├── 19.sql │ ├── 20.sql │ ├── 21.sql │ └── 22.sql └── workflow-compiling-service/ ├── LICENSE-binary ├── NOTICE-binary ├── build.sbt ├── project/ │ └── build.properties └── src/ ├── main/ │ ├── resources/ │ │ └── workflow-compiling-service-config.yaml │ └── scala/ │ └── org/ │ └── apache/ │ └── texera/ │ ├── amber/ │ │ └── compiler/ │ │ ├── WorkflowCompiler.scala │ │ └── model/ │ │ ├── LogicalLink.scala │ │ ├── LogicalPlan.scala │ │ └── LogicalPlanPojo.scala │ └── service/ │ ├── WorkflowCompilingService.scala │ ├── WorkflowCompilingServiceConfiguration.scala │ └── resource/ │ ├── HealthCheckResource.scala │ └── WorkflowCompilationResource.scala └── test/ ├── resources/ │ └── country_sales_small.csv └── scala/ └── org/ └── apache/ └── texera/ └── service/ └── resource/ └── WorkflowCompilationResourceSpec.scala ================================================ FILE CONTENTS ================================================ ================================================ FILE: .asf.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # https://cwiki.apache.org/confluence/display/INFRA/Git+-+.asf.yaml+features github: description: "Human-AI Collaborative Data Science Using Visual Workflows" homepage: https://texera.apache.org/ labels: - human-ai-collaboration - ai-agents - visual-workflows - data-science - runtime-debugging - artificial-intelligence - machine-learning - interactive-engine - cloud-native protected_tags: - "v*.*.*" dependabot_alerts: true dependabot_updates: false features: # Enable wiki for documentation wiki: true # Enable issue management issues: true # Enable projects for project management boards projects: true # Enable github discussions discussions: true pull_requests: # allow auto-merge allow_auto_merge: true # enable updating head branches of pull requests allow_update_branch: true # auto-delete head branches after being merged del_branch_on_merge: true enabled_merge_buttons: squash: true squash_commit_message: PR_TITLE_AND_DESC merge: false rebase: false protected_branches: main: required_status_checks: # strict means "Require branches to be up to date before merging". strict: true # contexts are the names of checks that must pass contexts: - Required Checks - Check License Headers - Validate PR title required_pull_request_reviews: dismiss_stale_reviews: false require_code_owner_reviews: false required_approving_review_count: 1 required_linear_history: true required_conversation_resolution: true notifications: commits: commits@texera.apache.org issues: notifications@texera.apache.org pullrequests: notifications@texera.apache.org discussions: dev@texera.apache.org jobs: commits@texera.apache.org ================================================ FILE: .dockerignore ================================================ # Ignore all directories named `user-resources` anywhere in the project **/user-resources/ # Ignoring binary/output **/target/ **/out/ # Ignoring packages *.jar *.war *.nar *.ear *.zip *.tar.gz *.rar # Ignoring VSCode related files .vscode/ # Ignoring IntelliJ related files *.iml .idea/ .idea_modules/ lib_managed/ src_managed/ # Ignoring Eclipse files .classpath .project .settings # Ignoring sublime files *.sublime-workspace # Ignoring index folder and data folder index/ catalog/ plan/ plan_files/ query-results/ # Ignoring Mac OSX specific files .DS_Store # Ignoring jenv related files .java-version # Ignoring scala related files hs_err_pid* # Ignoring Python related files venv/ __pycache__/ *.py[cod] *$py.class .ipynb_checkpoints .pytype/ # Ignoring Python-generated files *.model *.pkl # Ignoring user-generated resources user-resources/ # Ignoring Gmail tokens gmail/ # Ignoring Maven-related files pom.xml.tag pom.xml.releaseBackup pom.xml.versionsBackup pom.xml.next release.properties dependency-reduced-pom.xml buildNumber.properties .mvn/timing.properties .mvn/wrapper/maven-wrapper.jar # Ignoring sbt related files .bsp/ sbt.json # Ignoring rebel related files rebel.xml # Ignoring log files *.log *.log.gz # Ignoring the entire log folder log/ # Ignoring package-lock.json package-lock.json # Ignoring Protobuf related files scalapb/scalapb # Ignoring credentials client_secret_* StoredCredential* **/apache2/ **/Apache24/ **/php/ Composer-Setup.exe # Ignoring folders generated by VSCode IDE .metals/ .bloop/ .ammonite/ metals.sbt # === NEW: Ignore frontend-related files === # Ignore node_modules in all subdirectories **/node_modules/ **/.pnp/ **/.pnp.js # Ignore Angular build output **/dist/ **/.angular/cache/ **/.nx/cache/ # Ignore Yarn cache and lock files **/.yarn/cache/ **/.yarn/install-state.gz **/.pnp.cjs **/.pnp.loader.mjs # Ignore frontend dependency-related files **/yarn-error.log **/.turbo/ **/.next/ **/coverage/ ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf ================================================ FILE: .github/ISSUE_TEMPLATE/bug-template.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: Bug Report description: File a bug report. type: "Bug" body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! - type: textarea id: what-happened attributes: label: What happened? description: Also tell us, what did you expect to happen? placeholder: Tell us what you see! value: "A bug happened!" validations: required: true - type: textarea id: reproduce attributes: label: How to reproduce? description: Please include steps for a repro. validations: required: true - type: dropdown id: branch attributes: label: Branch description: Which branch did you hit this on? options: - main - 1.1.0-incubating default: 0 validations: required: true - type: input id: commit-hash attributes: label: Commit Hash (Optional) description: If you know the specific commit that has the issue, please provide the commit hash here. placeholder: e.g., a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0 validations: required: false - type: dropdown id: browsers attributes: label: What browsers are you seeing the problem on? multiple: true options: - Chrome - Safari - Firefox - Microsoft Edge - type: textarea id: logs attributes: label: Relevant log output description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. render: shell - type: markdown attributes: value: | By submitting this issue, you agree to follow the [Apache Code of Conduct](https://www.apache.org/foundation/policies/conduct). ================================================ FILE: .github/ISSUE_TEMPLATE/feature-template.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: Feature Request description: Suggest a new feature or improvement. type: "Feature" body: - type: markdown attributes: value: | Thanks for suggesting a feature! Please provide as much detail as possible to help us evaluate your idea. - type: textarea id: summary attributes: label: Feature Summary description: Clearly describe what the feature is and the problem it solves. placeholder: Describe your feature idea and what problem it addresses. validations: required: true - type: textarea id: proposal attributes: label: Proposed Solution or Design description: Explain how you imagine this feature working. Include examples, diagrams, or pseudo-code if relevant. placeholder: Describe your proposed solution or design approach. validations: required: true - type: dropdown id: affected-area attributes: label: Affected Area description: Which part of the system does this feature relate to? multiple: true options: - Workflow Engine (Amber) - Workflow UI - Hub - Storage / Metadata - Deployment / Infrastructure - Other - type: markdown attributes: value: | By submitting this issue, you agree to follow the [Apache Code of Conduct](https://www.apache.org/foundation/policies/conduct). ================================================ FILE: .github/ISSUE_TEMPLATE/task-template.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: Task description: Create a new development or maintenance task. type: "Task" body: - type: markdown attributes: value: | Thanks for creating a task! Please describe what needs to be done and why. - type: textarea id: task-summary attributes: label: Task Summary description: Briefly describe what needs to be done, try to do a single step in a task. placeholder: Example — Refactor workflow scheduler module for better modularity. validations: required: true - type: checkboxes id: checklist attributes: label: Task Type description: Select the type of work involved. options: - label: Refactor / Cleanup - label: DevOps / Deployment / CI - label: Testing / QA - label: Documentation - label: Performance - label: Other - type: markdown attributes: value: | By submitting this issue, you agree to follow the [Apache Code of Conduct](https://www.apache.org/foundation/policies/conduct). ================================================ FILE: .github/PULL_REQUEST_TEMPLATE ================================================ ### What changes were proposed in this PR? ### Any related issues, documentation, discussions? ### How was this PR tested? ### Was this PR authored or co-authored using generative AI tooling? ================================================ FILE: .github/labeler.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. frontend: - changed-files: - any-glob-to-any-file: - 'frontend/**' common: - changed-files: - any-glob-to-any-file: - 'common/**' # Root-level Scala build / lint config: a change to any of these # affects every Scala stack (amber + the platform services). - 'build.sbt' - 'project/**' - '.scalafix.conf' - '.scalafmt.conf' platform: - changed-files: - any-glob-to-any-file: - 'access-control-service/**' - 'computing-unit-managing-service/**' - 'config-service/**' - 'file-service/**' - 'workflow-compiling-service/**' agent-service: - changed-files: - any-glob-to-any-file: - 'agent-service/**' engine: # Non-Python, non-integration parts of amber/. Pure Python changes # under amber/src/{main,test}/python/** intentionally fall through to # the `python` label (which the labeler also matches via **/*.py), # so they only trigger the python + amber-integration stacks rather # than the full Scala-only `amber` stack. Integration specs live # under amber/src/test/integration/** (added to sbt's Test sources # via amber/build.sbt) and are caught by the `amber-integration` # label below. Top-level Scala formatter / linter configs are # included so a PR that only updates them still triggers the Scala # stacks instead of producing an empty stack union. - changed-files: - any-glob-to-any-file: - 'amber/build.sbt' - 'amber/.scalafmt.conf' - 'amber/.scalafix.conf' - 'amber/project/**' - 'amber/src/main/scala/**' - 'amber/src/main/java/**' - 'amber/src/main/protobuf/**' - 'amber/src/main/resources/**' - 'amber/src/test/scala/**' - 'amber/src/test/java/**' amber-integration: # Scala specs that exercise both Scala and Python end-to-end. They # live under amber/src/test/integration/** (sbt picks them up via # `Test / unmanagedSourceDirectories += .../test/integration` in # amber/build.sbt) and are tagged @org.apache.texera.amber.tags # .IntegrationTest. This label maps to the amber-integration stack # only, so a PR that touches just an integration spec does not pay # for the full Scala-only `amber` job. - changed-files: - any-glob-to-any-file: - 'amber/src/test/integration/**' python: # Includes pip requirement manifests so dependency-only PRs still # exercise the Python + amber-integration stacks. Without this a # bumped requirements.txt would only get `dependencies` (no stack # mapping) and silently skip CI for the very deps it's changing. - changed-files: - any-glob-to-any-file: - 'amber/src/main/python/**' - 'amber/src/test/python/**' - 'amber/pyproject.toml' - '**/*.py' - '**/requirements.txt' - '**/*-requirements.txt' docs: - changed-files: - any-glob-to-any-file: - 'docs/**' - '**/*.md' - 'NOTICE' - 'LICENSE' ci: - changed-files: - any-glob-to-any-file: - '.github/**' - '.asf.yaml' - 'codecov.yml' dev: - changed-files: - any-glob-to-any-file: - 'bin/**' dependencies: - changed-files: - any-glob-to-any-file: - '**/requirements.txt' - '**/package.json' - '**/build.sbt' - '**/project.sbt' ddl-change: - changed-files: - any-glob-to-any-file: - '**/*.sql' feature: - head-branch: - '^feat' - 'feature' fix: - head-branch: '^fix' refactor: - head-branch: '^refactor' ================================================ FILE: .github/release/vote-email-template.md ================================================ Subject: [VOTE] Release Apache Texera (incubating) ${VERSION} RC${RC_NUM} Hi Texera Community, This is a call for vote to release Apache Texera (incubating) ${VERSION}. == Release Candidate Artifacts == https://dist.apache.org/repos/dist/dev/incubator/texera/${VERSION}-RC${RC_NUM}/ The directory contains: - Source tarball (.tar.gz) with GPG signature (.asc) and SHA512 checksum (.sha512) - Docker Compose deployment bundle with GPG signature and SHA512 checksum == Container Images == Container images are available at: ${IMAGE_REGISTRY}/texera-dashboard-service:${VERSION} ${IMAGE_REGISTRY}/texera-workflow-execution-coordinator:${VERSION} ${IMAGE_REGISTRY}/texera-workflow-compiling-service:${VERSION} ${IMAGE_REGISTRY}/texera-file-service:${VERSION} ${IMAGE_REGISTRY}/texera-config-service:${VERSION} ${IMAGE_REGISTRY}/texera-access-control-service:${VERSION} ${IMAGE_REGISTRY}/texera-workflow-computing-unit-managing-service:${VERSION} These images are built from the source tarball included in this release. The Dockerfiles are included in the source for audit and verification. == Git Tag == https://github.com/apache/texera/releases/tag/${TAG_NAME} Commit: ${COMMIT_HASH} == Keys == The release was signed with GPG key [${GPG_KEY_ID}] (${GPG_EMAIL}) KEYS file: https://downloads.apache.org/incubator/texera/KEYS == Vote == The vote will be open for at least 72 hours. [ ] +1 Approve the release [ ] 0 No opinion [ ] -1 Disapprove the release (please provide the reason) == Checklist == [ ] Checksums and PGP signatures are valid [ ] LICENSE and NOTICE files are correct [ ] All files have ASF license headers where appropriate [ ] No unexpected binary files [ ] Source tarball matches the Git tag [ ] Can compile from source successfully [ ] Docker Compose bundle deploys successfully with the published images Thanks, [Your Name] Apache Texera (incubating) PPMC ================================================ FILE: .github/scripts/compose-backport-message.py ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Composes the backport commit message: insert "(backported from commit X)" # between the message body and the trailer block (the trailing run of # `Key: value` lines such as Co-Authored-By and Signed-off-by) so trailers # stay contiguous at the bottom — that's where git itself parses them. # # The trailer block, by git convention, is the run of `Key: value` lines # after the LAST blank line in the message, and only counts if EVERY line # after that blank line is in trailer format. This avoids mis-detecting a # Conventional Commits subject like "feat: foo" or a body line like # "References:" as a trailer. # # Usage: original-message-on-stdin | compose-backport-message.py import re import sys sha = sys.argv[1] msg = sys.stdin.read().rstrip("\n") lines = msg.split("\n") trailer_re = re.compile(r"^[A-Za-z][A-Za-z0-9-]*:\s") last_blank = -1 for idx in range(len(lines) - 1, -1, -1): if lines[idx] == "": last_blank = idx break trailer_start = len(lines) if last_blank != -1: candidate = lines[last_blank + 1 :] if candidate and all(trailer_re.match(line) for line in candidate): trailer_start = last_blank + 1 backport = f"(backported from commit {sha})" if trailer_start == len(lines): print(msg + "\n\n" + backport) else: body = "\n".join(lines[:trailer_start]).rstrip("\n") trailers = "\n".join(lines[trailer_start:]) print(body + "\n\n" + backport + "\n\n" + trailers) ================================================ FILE: .github/scripts/prepare-backport-checkout.sh ================================================ #!/usr/bin/env bash # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -euo pipefail target_branch="${1:?target branch is required}" commit_range="${2:?commit range is required}" workspace_branch="ci-backport-${target_branch//\//-}" git fetch --no-tags origin "${target_branch}" git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" if [[ "${commit_range}" != *..* ]]; then echo "Invalid commit range: ${commit_range}" >&2 exit 1 fi start_sha="${commit_range%..*}" end_sha="${commit_range##*..}" if [[ -z "$(git rev-list -n 1 "${commit_range}")" ]]; then echo "No commits found in range ${commit_range}" >&2 exit 1 fi # Build a single squash commit whose parent is the range start and whose tree # matches the range end. Cherry-picking this squash onto the release branch # applies the cumulative diff in one 3-way merge, which avoids spurious # conflicts when intermediate commits in the range happen to overlap with # changes already present (under different SHAs) on the release branch. end_tree="$(git rev-parse "${end_sha}^{tree}")" squash_sha="$(git commit-tree -p "${start_sha}" -m "ci: squashed backport of ${commit_range}" "${end_tree}")" git checkout -B "${workspace_branch}" "origin/${target_branch}" git cherry-pick -x "${squash_sha}" ================================================ FILE: .github/workflows/auto-queue.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Temporary stand-in for GitHub Merge Queue. # # Triggers: # * push to main: advance the queue right after a merge. # * pull_request {auto_merge_enabled, ready_for_review}: a PR just # became eligible — kick the queue without waiting for cron. # * pull_request_review {submitted}: an approval may have just made # a PR eligible (script filters non-approval review states). # * workflow_run {Required Checks, completed}: the head PR's CI # just finished. On success, auto-merge fires and the next push to # main triggers us; on failure, the head PR's CI moves from PENDING # to FAILURE so the in-flight guard releases — this trigger gives # us a same-second kick instead of waiting on cron. # * 5-minute cron: bounded safety net for any missed event delivery # and for PRs that became BEHIND without producing any of the above. # * workflow_dispatch: manual smoke test. # # Strategy: scan open PRs targeting main and pick the oldest eligible PR with # mergeStateStatus=BEHIND, then call updateBranch on it. A PR is eligible only # if it would actually merge once CI passes — auto-merge enabled, not a draft, # not conflicting, reviewDecision=APPROVED, and zero unresolved review threads. # This avoids burning CI on PRs blocked on review. # # Emergency priority: a PR carrying the `emergency` label is bumped before # any non-emergency PR regardless of CREATED_AT ordering, AND its presence # in BEHIND bypasses the in-flight guard so a non-emergency PR's running # CI does not delay the bump. Non-emergency PRs continue to wait for the # queue head as usual. # # In-flight guard: if any eligible PR is already past the BEHIND state and # its required CI is still running (mergeStateStatus != BEHIND and # statusCheckRollup state is PENDING/EXPECTED), the run exits without # bumping anyone else. That PR is the queue head; bumping a different PR # while it is in flight would just preempt CI capacity for a PR that # would still need re-bumping after the head merges. PRs that are # BEHIND with PENDING checks do NOT count as in-flight — that CI is on # pre-update code and would need to re-run after updateBranch anyway. # # mergeStateStatus is computed asynchronously and is UNKNOWN for a window # after a base-branch push. If at least one eligible PR is UNKNOWN, retry # with backoff up to ~2min to let it settle. If everything is settled and # nothing is BEHIND, exit without retrying — there's no work. # # Token: needs AUTO_MERGE_TOKEN with contents:write + pull_requests:write so # the resulting push retriggers required CI on the PR. Falls back to # GITHUB_TOKEN, in which case auto-merge will not actually fire (GITHUB_TOKEN # pushes don't trigger downstream workflows). name: Auto Queue on: push: branches: [main] pull_request: types: [auto_merge_enabled, ready_for_review] pull_request_review: types: [submitted] workflow_run: workflows: [Required Checks] types: [completed] schedule: - cron: '*/5 * * * *' workflow_dispatch: permissions: contents: write pull-requests: write concurrency: group: autoqueue-${{ github.repository }} cancel-in-progress: false jobs: update-next-auto-merge-pr: runs-on: ubuntu-latest steps: - uses: actions/github-script@v7 with: github-token: ${{ secrets.AUTO_MERGE_TOKEN || secrets.GITHUB_TOKEN }} script: | const { owner, repo } = context.repo; // pull_request_review fires for any submitted review (Comment / // Approve / Request changes). Only Approve can newly satisfy the // reviewDecision=APPROVED gate, so other states are pure no-ops // worth short-circuiting before the GraphQL call. if ( context.eventName === 'pull_request_review' && context.payload.review?.state !== 'approved' ) { core.info( `Skip: pull_request_review state=` + `${context.payload.review?.state} (only "approved" can ` + `change queue eligibility).` ); return; } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); // 0, 10, 20, 30, 30, 30 = 120s total wall-clock budget across // attempts. Short ramp catches the common case where // mergeStateStatus settles within ~30s of a base-branch push; // the tail keeps trying for the rare slow case. const BACKOFFS_MS = [0, 10000, 20000, 30000, 30000, 30000]; const query = ` query($owner:String!, $name:String!) { repository(owner:$owner, name:$name) { pullRequests( states: OPEN, baseRefName: "main", first: 100, orderBy: {field: CREATED_AT, direction: ASC} ) { nodes { number title isDraft mergeable mergeStateStatus reviewDecision autoMergeRequest { enabledAt } labels(first: 20) { nodes { name } } reviewThreads(first: 100) { nodes { isResolved } } commits(last: 1) { nodes { commit { statusCheckRollup { state } } } } } } } }`; // Carrying the `emergency` label lifts a PR above all other // eligible PRs: it is bumped first regardless of CREATED_AT, and // its presence in BEHIND bypasses the in-flight guard so a // non-emergency PR's running CI does not block the bump. const EMERGENCY_LABEL = 'emergency'; function isEmergency(p) { return (p.labels?.nodes ?? []).some( (l) => l.name === EMERGENCY_LABEL, ); } function classify(p) { if (!p.autoMergeRequest) return 'skip: auto-merge not enabled'; if (p.isDraft) return 'skip: draft'; if (p.mergeable === 'CONFLICTING') return 'skip: mergeable=CONFLICTING'; if (p.reviewDecision !== 'APPROVED') { return `skip: reviewDecision=${p.reviewDecision || 'NONE'}`; } const threads = p.reviewThreads?.nodes ?? []; const unresolved = threads.filter((t) => !t.isResolved).length; if (unresolved > 0) { return `skip: ${unresolved} unresolved review thread(s)`; } const tag = isEmergency(p) ? ' [emergency]' : ''; return `eligible${tag}: mergeable=${p.mergeable} state=${p.mergeStateStatus}`; } const start = Date.now(); for (let attempt = 0; attempt < BACKOFFS_MS.length; attempt++) { if (BACKOFFS_MS[attempt] > 0) { const elapsedS = Math.round((Date.now() - start) / 1000); core.info( `Waiting ${BACKOFFS_MS[attempt] / 1000}s before attempt ` + `${attempt + 1}/${BACKOFFS_MS.length} (elapsed ${elapsedS}s).` ); await sleep(BACKOFFS_MS[attempt]); } core.startGroup(`Attempt ${attempt + 1}/${BACKOFFS_MS.length}`); let data; try { data = await github.graphql(query, { owner, name: repo }); } catch (e) { // Transient GitHub API failures (5xx, "terminated", etc.) // shouldn't kill the whole run — the backoff loop is exactly // the right place to absorb them. Try again next attempt. core.warning( `GraphQL query failed (status ${e.status ?? '?'}): ` + `${e.message}. Retrying after backoff.` ); core.endGroup(); continue; } const all = data.repository.pullRequests.nodes; core.info(`Scanned ${all.length} open PR(s) targeting main.`); const behind = []; const unknown = []; const inFlight = []; for (const p of all) { const verdict = classify(p); core.info(` #${p.number} ${verdict} — ${p.title}`); if (!verdict.startsWith('eligible')) continue; if (p.mergeStateStatus === 'BEHIND') { behind.push(p); continue; } if (p.mergeStateStatus === 'UNKNOWN') { unknown.push(p); continue; } // Eligible AND not BEHIND/UNKNOWN: this PR is ahead of any // BEHIND PR in the queue. Treat it as in-flight only if its // current required CI is actually working toward a merge. // PENDING/EXPECTED (CI still running on the with-main code) // means "wait for it"; SUCCESS (about to auto-merge) means // "wait for it"; FAILURE/ERROR (CI failed) is NOT in-flight // — auto-merge will not fire, queue can advance past it. const ciState = p.commits?.nodes?.[0]?.commit?.statusCheckRollup?.state; if ( ciState === 'PENDING' || ciState === 'EXPECTED' || ciState === 'SUCCESS' ) { inFlight.push({ pr: p, ciState }); } } // Stable partition: emergency-labeled PRs go first; within // each priority class the GraphQL ASC-by-CREATED_AT order // is preserved. const emergencyBehind = behind.filter(isEmergency); const normalBehind = behind.filter((p) => !isEmergency(p)); const orderedBehind = [...emergencyBehind, ...normalBehind]; core.info( `Eligible: ${behind.length} BEHIND ` + `(${emergencyBehind.length} emergency), ` + `${unknown.length} UNKNOWN, ` + `${inFlight.length} in-flight (queue head still merging), ` + `rest blocked on failed CI or non-CI gates.` ); // Emergency BEHIND bypasses the in-flight guard: an emergency // is by definition something that should preempt CI capacity // on a non-emergency PR. Without an emergency, fall back to // the normal "wait for the queue head" behavior. if (inFlight.length > 0 && emergencyBehind.length === 0) { const head = inFlight[0]; core.info( `Skip: PR #${head.pr.number} is in flight ` + `(state=${head.pr.mergeStateStatus}, ci=${head.ciState}). ` + `Letting it finish to avoid preempting CI on a PR we may ` + `need to re-bump.` ); core.endGroup(); return; } if (inFlight.length > 0 && emergencyBehind.length > 0) { core.info( `${emergencyBehind.length} emergency PR(s) BEHIND — ` + `bypassing in-flight guard for #${inFlight[0].pr.number}.` ); } if (orderedBehind.length > 0) { let updated = null; for (const pr of orderedBehind) { const tag = isEmergency(pr) ? ' [emergency]' : ''; core.info(`→ updateBranch #${pr.number}${tag}`); try { const res = await github.rest.pulls.updateBranch({ owner, repo, pull_number: pr.number, }); core.info( `✓ #${pr.number} updateBranch dispatched (HTTP ${res.status}).` ); updated = pr.number; break; } catch (e) { core.warning( `✗ #${pr.number} updateBranch failed ` + `(status ${e.status ?? '?'}): ${e.message}` ); } } core.endGroup(); if (updated !== null) { core.info(`Done: #${updated} updated on attempt ${attempt + 1}.`); return; } core.info( 'All BEHIND PRs failed updateBranch this attempt; retrying after backoff.' ); continue; } if (unknown.length > 0) { core.info( `No BEHIND PRs yet; ${unknown.length} eligible PR(s) ` + 'still UNKNOWN — retrying after backoff to let GitHub settle.' ); core.endGroup(); continue; } core.info( 'No BEHIND or UNKNOWN eligible PRs — nothing to do this run.' ); core.endGroup(); return; } const totalS = Math.round((Date.now() - start) / 1000); core.info( `Exhausted ${BACKOFFS_MS.length} attempt(s) over ${totalS}s ` + `without finding a BEHIND PR to update.` ); ================================================ FILE: .github/workflows/automatic-email-notif-on-ddl-change.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: Automatic email notification on DDL change on: pull_request: types: - closed jobs: notify: if: >- github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'ddl-change') runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v5 with: fetch-depth: 0 sparse-checkout: sql/updates/ - name: Get added file in sql/updates/ id: get_sql_file run: | FILE=$(git diff --name-only --diff-filter=A \ ${{ github.event.pull_request.base.sha }} \ ${{ github.event.pull_request.merge_commit_sha }} \ -- 'sql/updates/') echo "sql_file=$FILE" >> $GITHUB_OUTPUT - name: Send email run: | curl --ssl-reqd \ --url "smtps://smtp.gmail.com:465" \ --user "${{ secrets.NOREPLY_EMAIL_USERNAME }}:${{ secrets.NOREPLY_EMAIL_PASSWORD }}" \ --mail-from "${{ secrets.NOREPLY_EMAIL_USERNAME }}" \ --mail-rcpt "dev@texera.apache.org" \ --upload-file - <Hi all,

We have merged PR #${{ github.event.pull_request.number }} (${{ github.event.pull_request.html_url }}): ${{ github.event.pull_request.title }}. To incorporate the change, please apply ${{ steps.get_sql_file.outputs.sql_file }} to your local Postgres instance and run sbt jooqGenerate to generate jooq classes.

Best,
${{ github.event.pull_request.user.login }}

EOF ================================================ FILE: .github/workflows/build-and-push-images.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. name: Build and push images on: workflow_dispatch: inputs: branch: description: 'Branch to checkout and build from' required: false default: 'main' type: string image_tag: description: 'Docker image tag (e.g., latest, v1.0.0). Leave empty to use the short commit hash of the branch.' required: false default: '' type: string docker_registry: description: 'Full image registry prefix (e.g., ghcr.io/apache, docker.io/apache)' required: false default: 'ghcr.io/apache' type: string services: description: 'Services to build (comma-separated, "*" for all)' required: false default: '*' type: string platforms: description: 'Target platforms to build' required: false default: 'both' type: choice options: - both - amd64 - arm64 schedule: # Run nightly at 2:00 AM UTC - cron: '0 2 * * *' permissions: contents: read packages: write # Required for pushing to ghcr.io concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs.image_tag || 'nightly' }} cancel-in-progress: false jobs: # Step 0: Set runtime parameters (handles both manual and scheduled runs) set-parameters: runs-on: ubuntu-latest if: github.event_name != 'schedule' || github.repository == 'apache/texera' outputs: branch: ${{ steps.set-params.outputs.branch }} image_tag: ${{ steps.set-params.outputs.image_tag }} docker_registry: ${{ steps.set-params.outputs.docker_registry }} services: ${{ steps.set-params.outputs.services }} platforms: ${{ steps.set-params.outputs.platforms }} steps: - name: Set build parameters id: set-params env: GH_TOKEN: ${{ github.token }} run: | # Detect if this is a scheduled run if [[ "${{ github.event_name }}" == "schedule" ]]; then echo "Nightly build detected - using nightly defaults" echo "branch=main" >> $GITHUB_OUTPUT echo "image_tag=latest" >> $GITHUB_OUTPUT echo "docker_registry=ghcr.io/apache" >> $GITHUB_OUTPUT echo "services=*" >> $GITHUB_OUTPUT echo "platforms=both" >> $GITHUB_OUTPUT else echo "Manual workflow_dispatch - using user inputs" BRANCH="${{ github.event.inputs.branch || 'main' }}" IMAGE_TAG="${{ github.event.inputs.image_tag }}" # If image_tag is empty, resolve to the short commit hash of the branch if [[ -z "$IMAGE_TAG" ]]; then COMMIT_SHORT=$(gh api "repos/${{ github.repository }}/commits/${BRANCH}" --jq '.sha[:9]') IMAGE_TAG="$COMMIT_SHORT" echo "No image tag specified - using short commit hash: $IMAGE_TAG" fi echo "branch=$BRANCH" >> $GITHUB_OUTPUT echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT echo "docker_registry=${{ github.event.inputs.docker_registry || 'ghcr.io/apache' }}" >> $GITHUB_OUTPUT echo "services=${{ github.event.inputs.services || '*' }}" >> $GITHUB_OUTPUT echo "platforms=${{ github.event.inputs.platforms || 'both' }}" >> $GITHUB_OUTPUT fi # Step 1: Generate JOOQ code once and share it generate-jooq: needs: [set-parameters] runs-on: ubuntu-latest env: JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 steps: - name: Checkout Texera uses: actions/checkout@v5 with: ref: ${{ needs.set-parameters.outputs.branch }} - name: Setup JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: 17 - name: Setup sbt launcher uses: sbt/setup-sbt@508b753e53cb6095967669e0911487d2b9bc9f41 # v1.1.22 - uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # v8.1.0 with: extraSbtFiles: '["*.sbt", "project/**.{scala,sbt}", "project/build.properties" ]' - name: Install PostgreSQL run: sudo apt-get update && sudo apt-get install -y postgresql - name: Start PostgreSQL Service run: sudo systemctl start postgresql - name: Configure PostgreSQL authentication run: | sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'postgres';" sudo sed -i 's/local all postgres peer/local all postgres md5/' /etc/postgresql/*/main/pg_hba.conf sudo sed -i 's/host all all 127.0.0.1\/32 scram-sha-256/host all all 127.0.0.1\/32 md5/' /etc/postgresql/*/main/pg_hba.conf sudo systemctl restart postgresql sleep 2 - name: Create Databases run: | PGPASSWORD=postgres psql -h localhost -U postgres -f sql/texera_ddl.sql PGPASSWORD=postgres psql -h localhost -U postgres -f sql/iceberg_postgres_catalog.sql PGPASSWORD=postgres psql -h localhost -U postgres -f sql/texera_lakefs.sql - name: Generate JOOQ code run: sbt DAO/jooqGenerate - name: Upload JOOQ generated code uses: actions/upload-artifact@v5 with: name: jooq-code path: | common/dao/src/main/scala/org/apache/texera/dao/jooq/generated/ retention-days: 1 # Step 2: Parse services and prepare build matrix prepare-matrix: needs: [set-parameters] runs-on: ubuntu-latest outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} build_amd64: ${{ steps.set-platforms.outputs.build_amd64 }} build_arm64: ${{ steps.set-platforms.outputs.build_arm64 }} need_manifest: ${{ steps.set-platforms.outputs.need_manifest }} steps: - name: Checkout Texera uses: actions/checkout@v5 with: ref: ${{ needs.set-parameters.outputs.branch }} - name: Set target platforms id: set-platforms run: | PLATFORM_INPUT="${{ needs.set-parameters.outputs.platforms }}" case "$PLATFORM_INPUT" in both) echo "build_amd64=true" >> $GITHUB_OUTPUT echo "build_arm64=true" >> $GITHUB_OUTPUT echo "need_manifest=true" >> $GITHUB_OUTPUT echo "Building for both platforms (parallel jobs)" ;; amd64) echo "build_amd64=true" >> $GITHUB_OUTPUT echo "build_arm64=false" >> $GITHUB_OUTPUT echo "need_manifest=false" >> $GITHUB_OUTPUT echo "Building for AMD64 only" ;; arm64) echo "build_amd64=false" >> $GITHUB_OUTPUT echo "build_arm64=true" >> $GITHUB_OUTPUT echo "need_manifest=false" >> $GITHUB_OUTPUT echo "Building for ARM64 only" ;; esac - name: Discover and parse services id: set-matrix run: | SERVICES="${{ needs.set-parameters.outputs.services }}" # Discover all Dockerfiles in bin/ directory echo "Discovering services from Dockerfiles..." cd bin # Standard services from *.dockerfile pattern (excluding postgres17-pgroonga) STANDARD_SERVICES=() for dockerfile in *.dockerfile; do if [[ -f "$dockerfile" ]]; then service_name=$(basename "$dockerfile" .dockerfile) # Skip postgres17-pgroonga if [[ "$service_name" != "postgres17-pgroonga" ]]; then STANDARD_SERVICES+=("$service_name") fi fi done # All services are standard services only ALL_SERVICES=("${STANDARD_SERVICES[@]}") echo "Found ${#ALL_SERVICES[@]} services: ${ALL_SERVICES[*]}" # Filter based on user input if [[ "$SERVICES" == "*" ]]; then SERVICES_LIST=("${ALL_SERVICES[@]}") else IFS=',' read -ra SERVICES_LIST <<< "$SERVICES" # Trim whitespace for i in "${!SERVICES_LIST[@]}"; do SERVICES_LIST[$i]=$(echo "${SERVICES_LIST[$i]}" | xargs) done fi # Create JSON matrix with dockerfile info JSON="[" FIRST=true for service in "${SERVICES_LIST[@]}"; do # Determine dockerfile path and context if [[ " ${STANDARD_SERVICES[@]} " =~ " ${service} " ]]; then dockerfile="bin/${service}.dockerfile" context="." # Map dockerfile service names to Docker image names case "$service" in "texera-web-application") image_name="texera-dashboard-service" ;; "computing-unit-master") image_name="texera-workflow-execution-coordinator" ;; "computing-unit-worker") image_name="texera-workflow-execution-runner" ;; "access-control-service") image_name="texera-access-control-service" ;; "config-service") image_name="texera-config-service" ;; "file-service") image_name="texera-file-service" ;; "workflow-compiling-service") image_name="texera-workflow-compiling-service" ;; "workflow-computing-unit-managing-service") image_name="texera-workflow-computing-unit-managing-service" ;; "agent-service") image_name="texera-agent-service" ;; *) # Default: use service name as-is image_name="$service" ;; esac else echo "WARNING: Unknown service: $service, skipping" continue fi if [[ "$FIRST" == "true" ]]; then FIRST=false else JSON+="," fi JSON+="{\"service\":\"$service\",\"image_name\":\"$image_name\",\"dockerfile\":\"$dockerfile\",\"context\":\"$context\"}" done JSON+="]" echo "Generated matrix: $JSON" echo "matrix={\"include\":$JSON}" >> $GITHUB_OUTPUT # Step 3a: Build AMD64 images (runs in parallel with ARM64) build-amd64: runs-on: ubuntu-latest needs: [set-parameters, generate-jooq, prepare-matrix] if: needs.prepare-matrix.outputs.build_amd64 == 'true' strategy: matrix: ${{ fromJson(needs.prepare-matrix.outputs.matrix) }} fail-fast: false max-parallel: 8 # Higher parallelism for native builds env: DOCKER_REGISTRY: ${{ needs.set-parameters.outputs.docker_registry }} JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 steps: - name: Checkout Texera uses: actions/checkout@v5 with: ref: ${{ needs.set-parameters.outputs.branch }} - name: Setup JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: 17 - name: Setup sbt launcher uses: sbt/setup-sbt@508b753e53cb6095967669e0911487d2b9bc9f41 # v1.1.22 - uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # v8.1.0 with: extraSbtFiles: '["*.sbt", "project/**.{scala,sbt}", "project/build.properties" ]' - name: Download JOOQ generated code uses: actions/download-artifact@v6 with: name: jooq-code path: common/dao/src/main/scala/org/apache/texera/dao/jooq/generated/ - name: Free up disk space run: | sudo apt-get clean sudo rm -rf /usr/share/dotnet sudo rm -rf /opt/ghc sudo rm -rf /usr/local/share/boost df -h - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Log in to GitHub Container Registry if: startsWith(needs.set-parameters.outputs.docker_registry, 'ghcr.io/') uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Log in to Docker Hub if: ${{ !startsWith(needs.set-parameters.outputs.docker_registry, 'ghcr.io/') }} uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Build and push AMD64 image uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: ${{ matrix.context }} file: ${{ matrix.dockerfile }} platforms: linux/amd64 push: true tags: ${{ env.DOCKER_REGISTRY }}/${{ matrix.image_name }}:${{ needs.set-parameters.outputs.image_tag }}-amd64 cache-from: type=gha,scope=${{ matrix.image_name }}-amd64 cache-to: type=gha,mode=max,scope=${{ matrix.image_name }}-amd64 labels: | org.opencontainers.image.title=${{ matrix.image_name }} org.opencontainers.image.description=Apache Texera ${{ matrix.image_name }} (AMD64) org.opencontainers.image.vendor=Apache Texera # Step 3b: Build ARM64 images (runs in parallel with AMD64) build-arm64: runs-on: ubuntu-latest needs: [set-parameters, generate-jooq, prepare-matrix] if: needs.prepare-matrix.outputs.build_arm64 == 'true' strategy: matrix: ${{ fromJson(needs.prepare-matrix.outputs.matrix) }} fail-fast: false max-parallel: 4 # Lower for QEMU builds env: DOCKER_REGISTRY: ${{ needs.set-parameters.outputs.docker_registry }} JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 steps: - name: Checkout Texera uses: actions/checkout@v5 with: ref: ${{ needs.set-parameters.outputs.branch }} - name: Setup JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: 17 - name: Setup sbt launcher uses: sbt/setup-sbt@508b753e53cb6095967669e0911487d2b9bc9f41 # v1.1.22 - uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # v8.1.0 with: extraSbtFiles: '["*.sbt", "project/**.{scala,sbt}", "project/build.properties" ]' - name: Download JOOQ generated code uses: actions/download-artifact@v6 with: name: jooq-code path: common/dao/src/main/scala/org/apache/texera/dao/jooq/generated/ - name: Free up disk space run: | sudo apt-get clean sudo rm -rf /usr/share/dotnet sudo rm -rf /opt/ghc sudo rm -rf /usr/local/share/boost df -h # Set up QEMU for ARM64 emulation - name: Set up QEMU uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 with: platforms: linux/arm64 - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Log in to GitHub Container Registry if: startsWith(needs.set-parameters.outputs.docker_registry, 'ghcr.io/') uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Log in to Docker Hub if: ${{ !startsWith(needs.set-parameters.outputs.docker_registry, 'ghcr.io/') }} uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Build and push ARM64 image uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: ${{ matrix.context }} file: ${{ matrix.dockerfile }} platforms: linux/arm64 push: true tags: ${{ env.DOCKER_REGISTRY }}/${{ matrix.image_name }}:${{ needs.set-parameters.outputs.image_tag }}-arm64 cache-from: type=gha,scope=${{ matrix.image_name }}-arm64 cache-to: type=gha,mode=max,scope=${{ matrix.image_name }}-arm64 labels: | org.opencontainers.image.title=${{ matrix.image_name }} org.opencontainers.image.description=Apache Texera ${{ matrix.image_name }} (ARM64) org.opencontainers.image.vendor=Apache Texera # Step 4: Create multi-arch manifests (only if building both platforms) create-manifests: runs-on: ubuntu-latest needs: [set-parameters, prepare-matrix, build-amd64, build-arm64] if: always() && needs.prepare-matrix.outputs.need_manifest == 'true' strategy: matrix: ${{ fromJson(needs.prepare-matrix.outputs.matrix) }} fail-fast: false env: DOCKER_REGISTRY: ${{ needs.set-parameters.outputs.docker_registry }} steps: - name: Log in to GitHub Container Registry if: startsWith(needs.set-parameters.outputs.docker_registry, 'ghcr.io/') uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Log in to Docker Hub if: ${{ !startsWith(needs.set-parameters.outputs.docker_registry, 'ghcr.io/') }} uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Create and push multi-arch manifest run: | # Create manifest list combining both architectures docker buildx imagetools create -t \ ${{ env.DOCKER_REGISTRY }}/${{ matrix.image_name }}:${{ needs.set-parameters.outputs.image_tag }} \ ${{ env.DOCKER_REGISTRY }}/${{ matrix.image_name }}:${{ needs.set-parameters.outputs.image_tag }}-amd64 \ ${{ env.DOCKER_REGISTRY }}/${{ matrix.image_name }}:${{ needs.set-parameters.outputs.image_tag }}-arm64 - name: Inspect multi-arch manifest run: | docker buildx imagetools inspect ${{ env.DOCKER_REGISTRY }}/${{ matrix.image_name }}:${{ needs.set-parameters.outputs.image_tag }} # Step 5: Summary report build-summary: runs-on: ubuntu-latest needs: [set-parameters, prepare-matrix, build-amd64, build-arm64, create-manifests] if: always() steps: - name: Generate build summary run: | echo "# Texera Multi-Arch Build Complete (Parallel)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "## Build Configuration" >> $GITHUB_STEP_SUMMARY echo "- **Trigger:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY echo "- **Branch:** \`${{ needs.set-parameters.outputs.branch }}\`" >> $GITHUB_STEP_SUMMARY echo "- **Registry:** \`${{ needs.set-parameters.outputs.docker_registry }}\`" >> $GITHUB_STEP_SUMMARY echo "- **Tag:** \`${{ needs.set-parameters.outputs.image_tag }}\`" >> $GITHUB_STEP_SUMMARY echo "- **Services:** ${{ needs.set-parameters.outputs.services }}" >> $GITHUB_STEP_SUMMARY echo "- **Platforms:** ${{ needs.set-parameters.outputs.platforms }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "## Build Method" >> $GITHUB_STEP_SUMMARY echo "**Parallel platform builds** (faster)" >> $GITHUB_STEP_SUMMARY echo "- AMD64: Native build on \`ubuntu-latest\`" >> $GITHUB_STEP_SUMMARY echo "- ARM64: QEMU emulation on \`ubuntu-latest\` (runs in parallel)" >> $GITHUB_STEP_SUMMARY echo "- Manifests: Combined into multi-arch images" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "> **Performance:** AMD64 and ARM64 now build simultaneously instead of sequentially!" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "## Images Published" >> $GITHUB_STEP_SUMMARY echo "All images are now available as multi-arch manifests at:" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "docker pull ${{ needs.set-parameters.outputs.docker_registry }}/:${{ needs.set-parameters.outputs.image_tag }}" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Usage" >> $GITHUB_STEP_SUMMARY echo "The images will automatically use the correct architecture:" >> $GITHUB_STEP_SUMMARY echo "- On x86_64/AMD64: pulls linux/amd64 variant" >> $GITHUB_STEP_SUMMARY echo "- On ARM64/M1/M2: pulls linux/arm64 variant" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Build Status" >> $GITHUB_STEP_SUMMARY echo "- AMD64 builds: ${{ needs.build-amd64.result }}" >> $GITHUB_STEP_SUMMARY echo "- ARM64 builds: ${{ needs.build-arm64.result }}" >> $GITHUB_STEP_SUMMARY echo "- Manifest creation: ${{ needs.create-manifests.result }}" >> $GITHUB_STEP_SUMMARY ================================================ FILE: .github/workflows/build.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. name: Build on: workflow_call: inputs: checkout_ref: required: false type: string default: "" backport_target_branch: required: false type: string default: "" backport_commit_range: required: false type: string default: "" job_name_suffix: required: false type: string default: "" run_frontend: required: false type: boolean default: true run_amber: required: false type: boolean default: true run_amber_integration: required: false type: boolean default: true run_platform: required: false type: boolean default: true run_python: required: false type: boolean default: true run_agent_service: required: false type: boolean default: true mode: # PR (default) | nightly | release. Only "PR" passes # --ignore-transitive-version to check_binary_deps.py. required: false type: string default: PR env: NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} jobs: frontend: if: ${{ inputs.run_frontend }} runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] include: - os: macos-latest arch: arm64 - os: ubuntu-latest arch: x64 - os: windows-latest arch: x64 node-version: - 24.10.0 steps: - name: Checkout Texera uses: actions/checkout@v5 with: ref: ${{ inputs.checkout_ref || github.sha }} fetch-depth: 0 - name: Prepare backport workspace if: ${{ inputs.backport_target_branch != '' }} working-directory: ${{ github.workspace }} run: bash ./.github/scripts/prepare-backport-checkout.sh "${{ inputs.backport_target_branch }}" "${{ inputs.backport_commit_range }}" - name: Setup node uses: actions/setup-node@v5 with: node-version: ${{ matrix.node-version }} architecture: ${{ matrix.arch }} - uses: actions/cache@v5 with: path: frontend/.yarn/cache key: ${{ runner.os }}-${{ matrix.arch }}-${{ matrix.node-version }}-yarn-cache-v4-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-${{ matrix.arch }}-${{ matrix.node-version }}-yarn-cache-v4- - name: Prepare Yarn 4.14.1 run: corepack enable && corepack prepare yarn@4.14.1 --activate - name: Setup Python uses: actions/setup-python@v6 with: python-version: "3.12" - name: Install dependency timeout-minutes: 20 run: yarn --cwd frontend install --immutable --inline-builds --network-timeout=100000 - name: Lint with Prettier & ESLint run: yarn --cwd frontend format:ci - name: Prod build run: yarn --cwd frontend run build:ci - name: Check bundled npm packages against per-module LICENSE-binary files if: matrix.os == 'ubuntu-latest' run: ./bin/licensing/check_binary_deps.py ${{ inputs.mode == 'PR' && '--ignore-transitive-version' || '' }} npm frontend/dist/3rdpartylicenses.json - name: Run frontend unit tests run: yarn --cwd frontend run test:ci - name: Upload frontend coverage to Codecov if: matrix.os == 'ubuntu-latest' && always() uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5.5.4 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./frontend/coverage/**/lcov.info flags: frontend fail_ci_if_error: false - name: Install Playwright Chromium run: yarn --cwd frontend playwright install ${{ matrix.os == 'ubuntu-latest' && '--with-deps' || '' }} chromium - name: Run frontend browser-mode tests run: yarn --cwd frontend ng run gui:test-browser amber: # The amber job runs the cross-cutting Scala lints (scalafmtCheckAll, # scalafixAll --check) once on behalf of every Scala module, then builds # and tests just the WorkflowExecutionService dist. Per-service builds # and tests for the platform services live in the `platform` matrix # below. License-binary checks are scoped to the amber dist. if: ${{ inputs.run_amber }} strategy: matrix: os: [ubuntu-22.04] java-version: [17] runs-on: ${{ matrix.os }} env: JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 services: postgres: image: postgres env: POSTGRES_PASSWORD: postgres ports: - 5432:5432 options: >- --health-cmd="pg_isready -U postgres" --health-interval=10s --health-timeout=5s --health-retries=5 steps: - name: Checkout uses: actions/checkout@v5 with: ref: ${{ inputs.checkout_ref || github.sha }} fetch-depth: 0 - name: Prepare backport workspace if: ${{ inputs.backport_target_branch != '' }} working-directory: ${{ github.workspace }} run: bash ./.github/scripts/prepare-backport-checkout.sh "${{ inputs.backport_target_branch }}" "${{ inputs.backport_commit_range }}" - name: Setup JDK uses: actions/setup-java@v5 with: distribution: "temurin" java-version: 17 - name: Create Databases # Must run before any sbt compile step: the build's JOOQ source # generators connect to texera_db while compiling. run: | psql -h localhost -U postgres -f sql/texera_ddl.sql psql -h localhost -U postgres -f sql/iceberg_postgres_catalog.sql psql -h localhost -U postgres -f sql/texera_lakefs.sql env: PGPASSWORD: postgres - name: Setup sbt launcher uses: sbt/setup-sbt@508b753e53cb6095967669e0911487d2b9bc9f41 # v1.1.22 - uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # v8.1.0 with: extraSbtFiles: '["*.sbt", "project/**.{scala,sbt}", "project/build.properties" ]' - name: Lint and build amber distributable bundle # Single sbt invocation: scalafmt -> scalafix -> amber dist. # scalafmtCheckAll and scalafixAll cover every Scala module, so the # platform matrix below skips them. scalafix triggers compile (and # JOOQ codegen), which the dist command then reuses incrementally. run: | sbt scalafmtCheckAll \ "scalafixAll --check" \ WorkflowExecutionService/dist - name: Unzip amber dist and check binary licenses # Per-module LICENSE-binary files live at the repo root after #4668; # the amber JVM dist is checked against amber/LICENSE-binary-java. # The audit always runs (mirroring the previous 'if: always()' on its # own step) and never fails the step; the binding check's exit code # drives it. run: | set -euo pipefail mkdir -p /tmp/dists unzip -q amber/target/universal/amber-*.zip -d /tmp/dists/ check_exit=0 ./bin/licensing/check_binary_deps.py ${{ inputs.mode == 'PR' && '--ignore-transitive-version' || '' }} jar \ --license-binary amber/LICENSE-binary-java \ /tmp/dists/amber-*/lib || check_exit=$? ./bin/licensing/audit_jar_licenses.py /tmp/dists/amber-*/lib || true exit "$check_exit" - name: Create texera_db_for_test_cases run: psql -h localhost -U postgres -v DB_NAME=texera_db_for_test_cases -f sql/texera_ddl.sql env: PGPASSWORD: postgres - name: Set docker-java API version run: | echo "api.version=1.52" >> ~/.docker-java.properties cat ~/.docker-java.properties - name: Run amber and common module tests with coverage # 'jacoco' runs tests under sbt-jacoco's JVM agent and emits per- # module jacoco.xml that the codecov upload step picks up. # `WorkflowExecutionService/jacoco` only runs that project's # Test config (sbt's `test` task does not transit dependsOn), # so common modules' tests are listed explicitly here. Modules # with no tests (Auth, Config) are skipped. # # AMBER_TEST_FILTER=skip-integration tells amber/build.sbt to # exclude @org.apache.texera.amber.tags.IntegrationTest specs; # those run in the amber-integration job below. env: AMBER_TEST_FILTER: skip-integration run: | sbt "DAO/jacoco" \ "PyBuilder/jacoco" \ "WorkflowCore/jacoco" \ "WorkflowOperator/jacoco" \ "WorkflowExecutionService/jacoco" - name: Upload amber and common coverage to Codecov if: always() uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5.5.4 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./**/target/scala-2.13/jacoco/report/jacoco.xml flags: amber fail_ci_if_error: false amber-integration: # Runs Scala tests tagged @org.apache.texera.amber.tags.IntegrationTest — # currently the e2e specs that spawn Python UDF workers. Provisions # Python deps that the lighter `amber` job no longer installs. Cross- # cutting lints (scalafmt / scalafix) and the amber dist + binary # license check stay in `amber`; this job is tests-only. if: ${{ inputs.run_amber_integration }} strategy: matrix: os: [ubuntu-22.04] java-version: [17] runs-on: ${{ matrix.os }} env: JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 services: postgres: image: postgres env: POSTGRES_PASSWORD: postgres ports: - 5432:5432 options: >- --health-cmd="pg_isready -U postgres" --health-interval=10s --health-timeout=5s --health-retries=5 steps: - name: Checkout uses: actions/checkout@v5 with: ref: ${{ inputs.checkout_ref || github.sha }} fetch-depth: 0 - name: Prepare backport workspace if: ${{ inputs.backport_target_branch != '' }} working-directory: ${{ github.workspace }} run: bash ./.github/scripts/prepare-backport-checkout.sh "${{ inputs.backport_target_branch }}" "${{ inputs.backport_commit_range }}" - name: Setup JDK uses: actions/setup-java@v5 with: distribution: "temurin" java-version: 17 - name: Setup Python for Scala-Python integration tests uses: actions/setup-python@v6 with: python-version: "3.11" - name: Show Python run: python --version || python3 --version - name: Install Python dependencies # The integration tests spawn Python UDF workers; install # everything they need on the host. uv for speed; no licensing # concerns because no dist is built here. # --index-strategy unsafe-best-match makes uv consider every # version on every index (pip's default) rather than stopping at # the first index that lists a package. operator-requirements.txt # adds the pytorch CPU index as an --extra-index-url, which # mirrors a subset of common deps (e.g. pillow); without this # flag a dependabot bump to a version not yet mirrored there # fails to resolve even though PyPI has it. run: | python -m pip install uv if [ -f amber/requirements.txt ]; then uv pip install --system --index-strategy unsafe-best-match -r amber/requirements.txt; fi if [ -f amber/operator-requirements.txt ]; then uv pip install --system --index-strategy unsafe-best-match -r amber/operator-requirements.txt; fi if [ -f amber/dev-requirements.txt ]; then uv pip install --system --index-strategy unsafe-best-match -r amber/dev-requirements.txt; fi - name: Create Databases run: | psql -h localhost -U postgres -f sql/texera_ddl.sql psql -h localhost -U postgres -f sql/iceberg_postgres_catalog.sql psql -h localhost -U postgres -f sql/texera_lakefs.sql psql -h localhost -U postgres -f sql/texera_lakekeeper.sql env: PGPASSWORD: postgres - name: Setup sbt launcher uses: sbt/setup-sbt@508b753e53cb6095967669e0911487d2b9bc9f41 # v1.1.22 - uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # v8.1.0 with: extraSbtFiles: '["*.sbt", "project/**.{scala,sbt}", "project/build.properties" ]' - name: Create texera_db_for_test_cases run: psql -h localhost -U postgres -v DB_NAME=texera_db_for_test_cases -f sql/texera_ddl.sql env: PGPASSWORD: postgres - name: Start MinIO run: | docker run -d --name minio --network host \ -e MINIO_ROOT_USER=texera_minio \ -e MINIO_ROOT_PASSWORD=password \ minio/minio:RELEASE.2025-02-28T09-55-16Z server /data for i in $(seq 1 3); do curl -sf http://localhost:9000/minio/health/live && break echo "Waiting for MinIO... (attempt $i)" sleep 1 done - name: Start Lakekeeper env: LAKEKEEPER__PG_DATABASE_URL_READ: postgres://postgres:postgres@localhost:5432/texera_lakekeeper LAKEKEEPER__PG_DATABASE_URL_WRITE: postgres://postgres:postgres@localhost:5432/texera_lakekeeper LAKEKEEPER__PG_ENCRYPTION_KEY: texera_key run: | docker run --rm --network host \ -e LAKEKEEPER__PG_DATABASE_URL_READ \ -e LAKEKEEPER__PG_DATABASE_URL_WRITE \ -e LAKEKEEPER__PG_ENCRYPTION_KEY \ vakamo/lakekeeper:v0.11.0 migrate docker run -d --name lakekeeper --network host \ -e LAKEKEEPER__PG_DATABASE_URL_READ \ -e LAKEKEEPER__PG_DATABASE_URL_WRITE \ -e LAKEKEEPER__PG_ENCRYPTION_KEY \ -e LAKEKEEPER__METRICS_PORT=9091 \ vakamo/lakekeeper:v0.11.0 serve for i in $(seq 1 3); do docker exec lakekeeper /home/nonroot/lakekeeper healthcheck && break echo "Waiting for Lakekeeper... (attempt $i)" sleep 1 done docker exec lakekeeper /home/nonroot/lakekeeper healthcheck || { echo "Lakekeeper failed to start. Container logs:" docker logs lakekeeper exit 1 } - name: Initialize Lakekeeper warehouse # Pull defaults out of storage.conf so this step doesn't duplicate # values that already live in the runtime config. Each scalar in # storage.conf is followed by a `${?VAR}` env-override line whose # name is globally unique, so anchoring grep on that override line # selects the value unambiguously across nested scopes. run: | CONF=common/config/src/main/resources/storage.conf extract() { grep -B1 -F "\${?$1}" "$CONF" | head -1 | sed -E 's/.*"([^"]+)".*/\1/' } WAREHOUSE_NAME=$(extract STORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME) S3_BUCKET=$(extract STORAGE_ICEBERG_CATALOG_REST_S3_BUCKET) S3_ENDPOINT=$(extract STORAGE_S3_ENDPOINT) S3_REGION=$(extract STORAGE_S3_REGION) S3_USERNAME=$(extract STORAGE_S3_AUTH_USERNAME) S3_PASSWORD=$(extract STORAGE_S3_AUTH_PASSWORD) # Lakekeeper's management API lives on the same host as the # catalog; strip the /catalog suffix off the catalog URI to get # the base URL. REST_URI=$(extract STORAGE_ICEBERG_CATALOG_REST_URI) LAKEKEEPER_BASE=${REST_URI%/catalog} LAKEKEEPER_BASE=${LAKEKEEPER_BASE%/} docker run --rm --network host --entrypoint sh minio/mc -c \ "mc alias set minio $S3_ENDPOINT $S3_USERNAME $S3_PASSWORD && \ mc mb --ignore-existing minio/$S3_BUCKET" curl -sf -X POST -H 'Content-Type: application/json' \ -d '{"project-id":"00000000-0000-0000-0000-000000000000","project-name":"default"}' \ "$LAKEKEEPER_BASE/management/v1/project" || true curl -sf -X POST -H 'Content-Type: application/json' -d @- \ "$LAKEKEEPER_BASE/management/v1/warehouse" <- --health-cmd="pg_isready -U postgres" --health-interval=10s --health-timeout=5s --health-retries=5 steps: - name: Checkout uses: actions/checkout@v5 with: ref: ${{ inputs.checkout_ref || github.sha }} fetch-depth: 0 - name: Prepare backport workspace if: ${{ inputs.backport_target_branch != '' }} working-directory: ${{ github.workspace }} run: bash ./.github/scripts/prepare-backport-checkout.sh "${{ inputs.backport_target_branch }}" "${{ inputs.backport_commit_range }}" - name: Setup JDK uses: actions/setup-java@v5 with: distribution: "temurin" java-version: 17 - name: Setup sbt launcher uses: sbt/setup-sbt@508b753e53cb6095967669e0911487d2b9bc9f41 # v1.1.22 - uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # v8.1.0 with: extraSbtFiles: '["*.sbt", "project/**.{scala,sbt}", "project/build.properties" ]' - name: Create Databases run: | psql -h localhost -U postgres -f sql/texera_ddl.sql psql -h localhost -U postgres -f sql/iceberg_postgres_catalog.sql psql -h localhost -U postgres -f sql/texera_lakefs.sql env: PGPASSWORD: postgres - name: Build dist and run ${{ matrix.service }} tests with coverage # Single sbt invocation so dist + test share compiled state. Use # `jacoco` so the codecov upload step has a report to pick up. run: sbt "${{ matrix.sbt_project }}/dist" "${{ matrix.sbt_project }}/jacoco" - name: Unzip ${{ matrix.service }} dist and check binary licenses # Each platform service has its own LICENSE-binary at the repo root # after #4668; check this service's dist against just its own file. run: | set -euo pipefail mkdir -p /tmp/dists unzip -q ${{ matrix.service }}/target/universal/${{ matrix.service }}-*.zip -d /tmp/dists/ check_exit=0 ./bin/licensing/check_binary_deps.py ${{ inputs.mode == 'PR' && '--ignore-transitive-version' || '' }} jar \ --license-binary ${{ matrix.service }}/LICENSE-binary \ /tmp/dists/${{ matrix.service }}-*/lib || check_exit=$? ./bin/licensing/audit_jar_licenses.py /tmp/dists/${{ matrix.service }}-*/lib || true exit "$check_exit" - name: Upload ${{ matrix.service }} coverage to Codecov # Per-service flag so each matrix entry has its own Codecov view # rather than being merged into one umbrella `platform` flag. if: always() uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5.5.4 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./${{ matrix.service }}/target/scala-2.13/jacoco/report/jacoco.xml flags: ${{ matrix.service }} fail_ci_if_error: false python: if: ${{ inputs.run_python }} strategy: matrix: os: [ubuntu-latest] python-version: ["3.10", "3.11", "3.12", "3.13"] runs-on: ${{ matrix.os }} services: postgres: image: postgres env: POSTGRES_PASSWORD: postgres ports: - 5432:5432 options: >- --health-cmd="pg_isready -U postgres" --health-interval=10s --health-timeout=5s --health-retries=5 steps: - name: Checkout Texera uses: actions/checkout@v5 with: ref: ${{ inputs.checkout_ref || github.sha }} fetch-depth: 0 - name: Prepare backport workspace if: ${{ inputs.backport_target_branch != '' }} run: bash ./.github/scripts/prepare-backport-checkout.sh "${{ inputs.backport_target_branch }}" "${{ inputs.backport_commit_range }}" - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Unit-test licensing scripts # Stdlib only, no install needed. Runs on every matrix row (3.10 → # 3.13) so the script's behavior is guarded across all supported # Python versions before the license check itself runs (3.12 only). run: python3 -m unittest discover -s bin/licensing -p "test_*.py" -v - name: Install dependencies # 3.12 is the only leg that drives the binary-license check via # pip-licenses. Keep stock pip there so the resolved versions # match amber/LICENSE-binary-python (also generated with pip, # tracking what the production image installs). Other legs use # uv purely for install-speed. run: | if [ "${{ matrix.python-version }}" = "3.12" ]; then python -m pip install --upgrade pip pip-licenses install="pip install" else python -m pip install uv # See amber-integration job for why --index-strategy is set. install="uv pip install --system --index-strategy unsafe-best-match" fi if [ -f amber/requirements.txt ]; then $install -r amber/requirements.txt; fi if [ -f amber/operator-requirements.txt ]; then $install -r amber/operator-requirements.txt; fi - name: Generate pip-licenses manifest if: matrix.python-version == '3.12' run: pip-licenses --format=csv --ignore-packages pip-licenses prettytable wcwidth > /tmp/pip-licenses.csv - name: Check installed Python packages against per-module LICENSE-binary files if: matrix.python-version == '3.12' run: ./bin/licensing/check_binary_deps.py ${{ inputs.mode == 'PR' && '--ignore-transitive-version' || '' }} python /tmp/pip-licenses.csv - name: Create iceberg catalog database run: psql -h localhost -U postgres -f sql/iceberg_postgres_catalog.sql env: PGPASSWORD: postgres - name: Lint with Ruff run: | cd amber && ruff check src/main/python src/test/python && ruff format --check src/main/python src/test/python - name: Install dev dependencies # Test-only deps live in amber/dev-requirements.txt and are # installed after the LICENSE-binary snapshot above so they never # appear in pip-licenses output. Packaging skips this file. uv # is safe here regardless of leg because it runs post-snapshot. run: | python -m pip install uv if [ -f amber/dev-requirements.txt ]; then uv pip install --system -r amber/dev-requirements.txt; fi - name: Test with pytest run: | cd amber && pytest -m "not integration" --cov=src/main/python --cov-report=xml -sv - name: Upload python coverage to Codecov if: matrix.python-version == '3.12' && always() uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5.5.4 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./amber/coverage.xml flags: python fail_ci_if_error: false agent-service: if: ${{ inputs.run_agent_service }} name: ${{ format('agent-service{0} ({1})', inputs.job_name_suffix, matrix.os) }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest] bun-version: ["1.3.3"] defaults: run: working-directory: agent-service steps: - name: Checkout Texera uses: actions/checkout@v5 with: ref: ${{ inputs.checkout_ref || github.sha }} fetch-depth: 0 - name: Prepare backport workspace if: ${{ inputs.backport_target_branch != '' }} working-directory: ${{ github.workspace }} run: bash ./.github/scripts/prepare-backport-checkout.sh "${{ inputs.backport_target_branch }}" "${{ inputs.backport_commit_range }}" - name: Setup Bun run: | curl -fsSL https://bun.sh/install | bash -s -- bun-v${{ matrix.bun-version }} echo "$HOME/.bun/bin" >> $GITHUB_PATH - name: Install production dependencies run: bun install --production --frozen-lockfile - name: Generate agent-service license manifest if: matrix.os == 'ubuntu-latest' run: | mkdir -p dist bun run bin/collect-licenses.ts > dist/3rdpartylicenses.json - name: Check bundled agent-service packages against per-module LICENSE-binary files if: matrix.os == 'ubuntu-latest' run: ../bin/licensing/check_binary_deps.py ${{ inputs.mode == 'PR' && '--ignore-transitive-version' || '' }} agent-npm dist/3rdpartylicenses.json - name: Install development dependencies run: bun install --frozen-lockfile - name: Lint with Prettier run: bun run format:check - name: Typecheck run: bun run typecheck - name: Run unit tests run: bun test --coverage --coverage-reporter=lcov - name: Upload agent-service coverage to Codecov if: matrix.os == 'ubuntu-latest' && always() uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5.5.4 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./agent-service/coverage/lcov.info flags: agent-service fail_ci_if_error: false ================================================ FILE: .github/workflows/check-header.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: Release Auditing on: push: branches: - 'ci-enable/**' - 'main' pull_request: workflow_dispatch: jobs: test: name: Check License Headers runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: apache/skywalking-eyes@5c5b974209f0de5d905f37deb69369068ebfc15c # v0.7.0 ================================================ FILE: .github/workflows/comment-commands.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /take, /untake, /request-review, and /unrequest-review comment commands. # # Triage state is no longer materialized as a label — it is the search # filter `is:issue is:open no:assignee`. Anyone can self-claim an issue # by commenting `/take` (and self-release with `/untake`); PR-driven # assignee sync is handled by `pr-assignment.yml`. # # On pull requests, the author can request or cancel reviewer requests # via `/request-review @user [@user ...]` and `/unrequest-review @user # [@user ...]`. We avoid the `/review` namespace so it stays free for # future use (e.g. self-review). name: Comment commands on: issue_comment: types: [created] permissions: issues: write pull-requests: write jobs: take: # The startsWith filter at the job level keeps unrelated comments # from allocating a runner; the regex inside the script enforces an # exact `/take` or `/untake` so suffixes like `/take this` do not # silently match. if: >- github.event_name == 'issue_comment' && github.event.action == 'created' && github.event.issue.pull_request == null && github.event.comment.user.type != 'Bot' && (startsWith(github.event.comment.body, '/take') || startsWith(github.event.comment.body, '/untake')) runs-on: ubuntu-latest steps: - uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const body = (context.payload.comment.body || '').trim(); const issue_number = context.payload.issue.number; const login = context.payload.comment.user.login; const { owner, repo } = context.repo; core.info( `take/untake candidate: ${login} on issue #${issue_number}; ` + `body=${JSON.stringify(body)}`, ); if (/^\/take\s*$/.test(body)) { try { await github.rest.issues.addAssignees({ owner, repo, issue_number, assignees: [login], }); core.info(`Assigned ${login} to issue #${issue_number}`); } catch (e) { core.warning( `addAssignees on #${issue_number} failed: ${e.message}`, ); } } else if (/^\/untake\s*$/.test(body)) { try { await github.rest.issues.removeAssignees({ owner, repo, issue_number, assignees: [login], }); core.info(`Unassigned ${login} from issue #${issue_number}`); } catch (e) { core.warning( `removeAssignees on #${issue_number} failed: ${e.message}`, ); } } else { core.info( `Comment does not match exact '/take' or '/untake'; skipping.`, ); } request-review: # Job-level startsWith gate avoids spinning up a runner for every # PR comment; the regex inside the script enforces the exact shape. if: >- github.event_name == 'issue_comment' && github.event.action == 'created' && github.event.issue.pull_request != null && github.event.comment.user.type != 'Bot' && (startsWith(github.event.comment.body, '/request-review') || startsWith(github.event.comment.body, '/unrequest-review')) runs-on: ubuntu-latest steps: - uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const body = (context.payload.comment.body || '').trim(); const pull_number = context.payload.issue.number; const commenter = context.payload.comment.user.login; const author = context.payload.issue.user.login; const { owner, repo } = context.repo; const match = body.match( /^\/(request-review|unrequest-review)\b(.*)$/s, ); if (!match) { core.info(`Comment does not match exact command; skipping.`); return; } const action = match[1]; if (commenter !== author) { core.info( `${commenter} is not the author of #${pull_number}; skipping.`, ); return; } // Parse @user and @org/team mentions; route teams to the // team_reviewers bucket. Strip self so the API doesn't // reject the whole atomic call over one bad name. Copilot // is a bot reviewer that the REST API expects as the exact // slug "Copilot", so normalize any casing of @copilot. const reviewers = []; const team_reviewers = []; for (const [, h] of match[2].matchAll( /@([\w-]+(?:\/[\w.-]+)?)/g, )) { if (h.includes('/')) team_reviewers.push(h.split('/')[1]); else if (h.toLowerCase() === 'copilot') reviewers.push('Copilot'); else if (h.toLowerCase() !== author.toLowerCase()) reviewers.push(h); } if (!reviewers.length && !team_reviewers.length) { core.warning(`No valid @mentions in '${action}'; skipping.`); return; } const params = { owner, repo, pull_number, reviewers, team_reviewers }; try { if (action === 'request-review') { await github.rest.pulls.requestReviewers(params); } else { await github.rest.pulls.removeRequestedReviewers(params); } core.info( `${action} on #${pull_number} by ${commenter}: ` + `users=[${reviewers.join(', ')}] ` + `teams=[${team_reviewers.join(', ')}]`, ); } catch (e) { core.warning( `${action} on #${pull_number} failed: ${e.message}`, ); } ================================================ FILE: .github/workflows/create-release-candidate.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. name: Create and upload release candidate artifacts on: workflow_dispatch: inputs: tag: description: 'Existing Git tag (e.g., v1.1.0-incubating-rc1)' required: true type: string rc_number: description: 'Release candidate number for artifacts (e.g., 1 for RC1, 2 for RC2)' required: true type: string default: '1' image_registry: description: 'Container image registry prefix (e.g., ghcr.io/apache, docker.io/apache)' required: false type: string default: 'ghcr.io/apache' use_tag_as_image_tag: description: 'If true, pin the bundled docker-compose IMAGE_TAG to the git tag (with leading "v" stripped, e.g. 1.1.0-incubating-rc7). If false, pin it to the 9-char commit hash.' required: false type: boolean default: false jobs: # Strict-mode build of the tagged commit. Gates RC artifact creation: if # the tag doesn't compile or has license-binary drift, no RC is produced. build: uses: ./.github/workflows/build.yml with: checkout_ref: ${{ github.event.inputs.tag }} mode: release secrets: inherit create-rc: needs: build runs-on: ubuntu-latest outputs: version: ${{ steps.vars.outputs.version }} rc_num: ${{ steps.vars.outputs.rc_num }} tag_name: ${{ steps.vars.outputs.tag_name }} rc_dir: ${{ steps.vars.outputs.rc_dir }} commit_hash: ${{ steps.vars.outputs.commit_hash }} src_tarball: ${{ steps.vars.outputs.src_tarball }} compose_tarball: ${{ steps.vars.outputs.compose_tarball }} steps: - name: Checkout code uses: actions/checkout@v5 with: fetch-depth: 0 # Full history for proper tagging - name: Validate tag exists run: | TAG_NAME="${{ github.event.inputs.tag }}" # Check if tag exists if ! git rev-parse "$TAG_NAME" >/dev/null 2>&1; then echo "Error: Tag '$TAG_NAME' does not exist" echo "Available tags:" git tag -l | tail -10 exit 1 fi echo "✓ Tag validation passed: $TAG_NAME" - name: Set up variables id: vars run: | TAG_NAME="${{ github.event.inputs.tag }}" RC_NUM="${{ github.event.inputs.rc_number }}" IMAGE_REGISTRY="${{ github.event.inputs.image_registry }}" # Parse version from tag (format: v1.1.0-incubating or v1.1.0-incubating-rcN) # Both formats are accepted, but we use the input rc_number for artifacts if [[ "$TAG_NAME" =~ ^v([0-9]+\.[0-9]+\.[0-9]+-incubating)(-rc[0-9]+)?$ ]]; then VERSION="${BASH_REMATCH[1]}" else echo "Error: Tag must be in format vX.Y.Z-incubating or vX.Y.Z-incubating-rcN (e.g., v1.1.0-incubating-rc1)" exit 1 fi COMMIT_HASH=$(git rev-parse "$TAG_NAME") COMMIT_SHORT=$(git rev-parse "$TAG_NAME" | cut -c1-9) RC_DIR="${VERSION}-RC${RC_NUM}" SRC_TARBALL="apache-texera-${VERSION}-src.tar.gz" COMPOSE_TARBALL="apache-texera-${VERSION}-docker-compose.tar.gz" # Choose what gets pinned as IMAGE_TAG in the bundled .env. Default # is the commit short hash; if use_tag_as_image_tag is true, use the # git tag with the leading "v" stripped. USE_TAG_AS_IMAGE_TAG="${{ github.event.inputs.use_tag_as_image_tag }}" if [[ "$USE_TAG_AS_IMAGE_TAG" == "true" ]]; then IMAGE_TAG="${TAG_NAME#v}" else IMAGE_TAG="$COMMIT_SHORT" fi echo "version=$VERSION" >> $GITHUB_OUTPUT echo "rc_num=$RC_NUM" >> $GITHUB_OUTPUT echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT echo "rc_dir=$RC_DIR" >> $GITHUB_OUTPUT echo "commit_hash=$COMMIT_HASH" >> $GITHUB_OUTPUT echo "commit_short=$COMMIT_SHORT" >> $GITHUB_OUTPUT echo "image_registry=$IMAGE_REGISTRY" >> $GITHUB_OUTPUT echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT echo "src_tarball=$SRC_TARBALL" >> $GITHUB_OUTPUT echo "compose_tarball=$COMPOSE_TARBALL" >> $GITHUB_OUTPUT echo "Release Candidate: $TAG_NAME" echo "Version: $VERSION" echo "RC Number: $RC_NUM" echo "Commit: $COMMIT_HASH ($COMMIT_SHORT)" echo "Image Registry: $IMAGE_REGISTRY" echo "Bundled IMAGE_TAG: $IMAGE_TAG" echo "Staging directory: dist/dev/incubator/texera/$RC_DIR" - name: Create source tarball run: | TAG_NAME="${{ steps.vars.outputs.tag_name }}" SRC_TARBALL="${{ steps.vars.outputs.src_tarball }}" VERSION="${{ steps.vars.outputs.version }}" TEMP_DIR=$(mktemp -d) # Export the git repository at the tag git archive --format=tar --prefix="apache-texera-${VERSION}-src/" "$TAG_NAME" | tar -x -C "$TEMP_DIR" # Create tarball cd "$TEMP_DIR" tar -czf "$GITHUB_WORKSPACE/$SRC_TARBALL" "apache-texera-${VERSION}-src" cd "$GITHUB_WORKSPACE" # Verify tarball was created if [[ ! -f "$SRC_TARBALL" ]]; then echo "Error: Source tarball was not created" exit 1 fi # Show tarball info ls -lh "$SRC_TARBALL" echo "✓ Created source tarball: $SRC_TARBALL" - name: Create Docker Compose deployment bundle run: | VERSION="${{ steps.vars.outputs.version }}" TAG_NAME="${{ steps.vars.outputs.tag_name }}" IMAGE_REGISTRY="${{ steps.vars.outputs.image_registry }}" IMAGE_TAG="${{ steps.vars.outputs.image_tag }}" COMPOSE_TARBALL="${{ steps.vars.outputs.compose_tarball }}" TEMP_DIR=$(mktemp -d) BUNDLE_DIR="$TEMP_DIR/apache-texera-${VERSION}-docker-compose" mkdir -p "$BUNDLE_DIR" # Export the single-node directory from the tagged source mkdir -p "$TEMP_DIR/_raw" git archive --format=tar "$TAG_NAME" -- bin/single-node/ sql/ | tar -x -C "$TEMP_DIR/_raw" # Copy deployment files cp "$TEMP_DIR/_raw/bin/single-node/docker-compose.yml" "$BUNDLE_DIR/" cp "$TEMP_DIR/_raw/bin/single-node/nginx.conf" "$BUNDLE_DIR/" cp "$TEMP_DIR/_raw/bin/single-node/litellm-config.yaml" "$BUNDLE_DIR/" cp "$TEMP_DIR/_raw/bin/single-node/LICENSE" "$BUNDLE_DIR/" cp "$TEMP_DIR/_raw/bin/single-node/NOTICE" "$BUNDLE_DIR/" cp "$TEMP_DIR/_raw/bin/single-node/DISCLAIMER" "$BUNDLE_DIR/" cp -r "$TEMP_DIR/_raw/sql" "$BUNDLE_DIR/" # Patch the SQL mount path for the self-contained bundle layout # In the repo it's ../../sql (relative to bin/single-node/), in the bundle it's ./sql sed -i 's|../../sql|./sql|g' "$BUNDLE_DIR/docker-compose.yml" # Generate a release-pinned .env file with the version tag # Start from the source .env and ensure IMAGE_REGISTRY and IMAGE_TAG are set cp "$TEMP_DIR/_raw/bin/single-node/.env" "$BUNDLE_DIR/.env" # Replace if line exists, otherwise append if grep -q '^IMAGE_REGISTRY=' "$BUNDLE_DIR/.env"; then sed -i "s|^IMAGE_REGISTRY=.*|IMAGE_REGISTRY=${IMAGE_REGISTRY}|" "$BUNDLE_DIR/.env" else echo "IMAGE_REGISTRY=${IMAGE_REGISTRY}" >> "$BUNDLE_DIR/.env" fi if grep -q '^IMAGE_TAG=' "$BUNDLE_DIR/.env"; then sed -i "s|^IMAGE_TAG=.*|IMAGE_TAG=${IMAGE_TAG}|" "$BUNDLE_DIR/.env" else echo "IMAGE_TAG=${IMAGE_TAG}" >> "$BUNDLE_DIR/.env" fi if grep -q '^TEXERA_SERVICE_LOG_LEVEL=' "$BUNDLE_DIR/.env"; then sed -i "s|^TEXERA_SERVICE_LOG_LEVEL=.*|TEXERA_SERVICE_LOG_LEVEL=ERROR|" "$BUNDLE_DIR/.env" else echo "TEXERA_SERVICE_LOG_LEVEL=ERROR" >> "$BUNDLE_DIR/.env" fi # Include the README from the repo cp "$TEMP_DIR/_raw/bin/single-node/README.md" "$BUNDLE_DIR/" # Include example datasets, workflows, and the loader script if [ -d "$TEMP_DIR/_raw/bin/single-node/examples" ]; then cp -r "$TEMP_DIR/_raw/bin/single-node/examples" "$BUNDLE_DIR/" echo "✓ Included examples directory (datasets, workflows, load-examples.sh)" fi # Create tarball cd "$TEMP_DIR" tar -czf "$GITHUB_WORKSPACE/$COMPOSE_TARBALL" "apache-texera-${VERSION}-docker-compose" cd "$GITHUB_WORKSPACE" ls -lh "$COMPOSE_TARBALL" echo "✓ Created Docker Compose bundle: $COMPOSE_TARBALL" - name: Import GPG key run: | echo "${{ secrets.GPG_PRIVATE_KEY }}" | gpg --batch --import # List imported keys gpg --list-secret-keys echo "✓ GPG key imported successfully" - name: Sign and checksum all artifacts run: | for artifact in \ "${{ steps.vars.outputs.src_tarball }}" \ "${{ steps.vars.outputs.compose_tarball }}"; do # GPG signature echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 \ --armor --detach-sign --output "${artifact}.asc" "$artifact" gpg --verify "${artifact}.asc" "$artifact" echo "✓ Signed: ${artifact}" # SHA512 checksum sha512sum "$artifact" > "${artifact}.sha512" echo "✓ Checksum: ${artifact}.sha512" done - name: Generate vote email template id: vote_email run: | VERSION="${{ steps.vars.outputs.version }}" RC_NUM="${{ steps.vars.outputs.rc_num }}" TAG_NAME="${{ steps.vars.outputs.tag_name }}" RC_DIR="${{ steps.vars.outputs.rc_dir }}" COMMIT_HASH="${{ steps.vars.outputs.commit_hash }}" IMAGE_REGISTRY="${{ steps.vars.outputs.image_registry }}" # Get GPG key ID from the imported key GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep 'sec' | head -n1 | awk '{print $2}' | cut -d'/' -f2) GPG_EMAIL=$(gpg --list-secret-keys | grep 'uid' | head -n1 | grep -oP '[\w\.-]+@[\w\.-]+') # Copy template from repository cp .github/release/vote-email-template.md vote-email.txt # Substitute variables in the template sed -i "s|\${VERSION}|${VERSION}|g" vote-email.txt sed -i "s|\${RC_NUM}|${RC_NUM}|g" vote-email.txt sed -i "s|\${RC_DIR}|${RC_DIR}|g" vote-email.txt sed -i "s|\${TAG_NAME}|${TAG_NAME}|g" vote-email.txt sed -i "s|\${COMMIT_HASH}|${COMMIT_HASH}|g" vote-email.txt sed -i "s|\${GPG_KEY_ID}|${GPG_KEY_ID}|g" vote-email.txt sed -i "s|\${GPG_EMAIL}|${GPG_EMAIL}|g" vote-email.txt sed -i "s|\${IMAGE_REGISTRY}|${IMAGE_REGISTRY}|g" vote-email.txt echo "✓ Vote email template generated!" - name: Upload RC artifacts uses: actions/upload-artifact@v5 with: name: rc-artifacts path: | ${{ steps.vars.outputs.src_tarball }} ${{ steps.vars.outputs.src_tarball }}.asc ${{ steps.vars.outputs.src_tarball }}.sha512 ${{ steps.vars.outputs.compose_tarball }} ${{ steps.vars.outputs.compose_tarball }}.asc ${{ steps.vars.outputs.compose_tarball }}.sha512 vote-email.txt retention-days: 7 upload-rc: runs-on: ubuntu-latest needs: create-rc steps: - name: Download RC artifacts uses: actions/download-artifact@v6 with: name: rc-artifacts - name: Verify downloaded artifacts run: | SRC_TARBALL="${{ needs.create-rc.outputs.src_tarball }}" COMPOSE_TARBALL="${{ needs.create-rc.outputs.compose_tarball }}" echo "Verifying downloaded artifacts..." ls -lh for artifact in "$SRC_TARBALL" "$COMPOSE_TARBALL"; do if [[ ! -f "$artifact" ]] || [[ ! -f "${artifact}.asc" ]] || [[ ! -f "${artifact}.sha512" ]]; then echo "Error: Missing artifact or signature/checksum for: $artifact" exit 1 fi done echo "✓ All artifacts downloaded successfully" - name: Install SVN run: | sudo apt-get update sudo apt-get install -y subversion svn --version - name: Checkout SVN dev directory run: | RC_DIR="${{ needs.create-rc.outputs.rc_dir }}" # Checkout the dev directory with depth=empty (lightweight) svn co --depth=empty https://dist.apache.org/repos/dist/dev/incubator/texera svn-texera \ --username "${{ secrets.SVN_USERNAME }}" \ --password "${{ secrets.SVN_PASSWORD }}" \ --no-auth-cache cd svn-texera # Check if RC directory already exists on the remote SVN_BASE="https://dist.apache.org/repos/dist/dev/incubator/texera" if svn info "$SVN_BASE/$RC_DIR" >/dev/null 2>&1; then # Directory exists remotely — update (checkout) it into the working copy svn update --depth=infinity "$RC_DIR" \ --username "${{ secrets.SVN_USERNAME }}" \ --password "${{ secrets.SVN_PASSWORD }}" \ --no-auth-cache || true # If update didn't bring it down (empty parent checkout), do a sparse checkout if [[ ! -d "$RC_DIR" ]]; then svn update --set-depth=infinity "$RC_DIR" \ --username "${{ secrets.SVN_USERNAME }}" \ --password "${{ secrets.SVN_PASSWORD }}" \ --no-auth-cache fi echo "✓ RC directory already exists remotely, checked out: $RC_DIR" else # Directory doesn't exist remotely — create and add it mkdir -p "$RC_DIR" svn add "$RC_DIR" echo "✓ Created new RC directory: $RC_DIR" fi - name: Stage artifacts to SVN run: | SRC_TARBALL="${{ needs.create-rc.outputs.src_tarball }}" COMPOSE_TARBALL="${{ needs.create-rc.outputs.compose_tarball }}" RC_DIR="${{ needs.create-rc.outputs.rc_dir }}" cd svn-texera/"$RC_DIR" # Copy all artifacts for artifact in "$SRC_TARBALL" "$COMPOSE_TARBALL"; do cp "$GITHUB_WORKSPACE/$artifact" . cp "$GITHUB_WORKSPACE/${artifact}.asc" . cp "$GITHUB_WORKSPACE/${artifact}.sha512" . done # Add files to SVN svn add * --force # Check status svn status echo "✓ Staged all artifacts to SVN" - name: Commit artifacts to dist/dev run: | VERSION="${{ needs.create-rc.outputs.version }}" RC_NUM="${{ needs.create-rc.outputs.rc_num }}" RC_DIR="${{ needs.create-rc.outputs.rc_dir }}" cd svn-texera # Commit with descriptive message svn commit -m "Add Apache Texera ${VERSION} RC${RC_NUM} artifacts (source + docker-compose)" \ --username "${{ secrets.SVN_USERNAME }}" \ --password "${{ secrets.SVN_PASSWORD }}" \ --no-auth-cache echo "✓ Committed artifacts to dist/dev/incubator/texera/$RC_DIR" - name: Generate release summary run: | VERSION="${{ needs.create-rc.outputs.version }}" RC_NUM="${{ needs.create-rc.outputs.rc_num }}" TAG_NAME="${{ needs.create-rc.outputs.tag_name }}" RC_DIR="${{ needs.create-rc.outputs.rc_dir }}" COMMIT_HASH="${{ needs.create-rc.outputs.commit_hash }}" SRC_TARBALL="${{ needs.create-rc.outputs.src_tarball }}" COMPOSE_TARBALL="${{ needs.create-rc.outputs.compose_tarball }}" echo "## Release Candidate Created Successfully!" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Release Information" >> $GITHUB_STEP_SUMMARY echo "- **Version:** ${VERSION}" >> $GITHUB_STEP_SUMMARY echo "- **RC Number:** RC${RC_NUM}" >> $GITHUB_STEP_SUMMARY echo "- **Git Tag:** \`${TAG_NAME}\`" >> $GITHUB_STEP_SUMMARY echo "- **Commit:** \`${COMMIT_HASH}\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Artifacts Location" >> $GITHUB_STEP_SUMMARY echo "**Staging Directory:** https://dist.apache.org/repos/dist/dev/incubator/texera/${RC_DIR}/" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Artifacts Created" >> $GITHUB_STEP_SUMMARY echo "| Artifact | Description |" >> $GITHUB_STEP_SUMMARY echo "|----------|-------------|" >> $GITHUB_STEP_SUMMARY echo "| \`${SRC_TARBALL}\` | Source code |" >> $GITHUB_STEP_SUMMARY echo "| \`${COMPOSE_TARBALL}\` | Docker Compose deployment bundle |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Each artifact has a corresponding \`.asc\` (GPG signature) and \`.sha512\` (checksum) file." >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Next Steps" >> $GITHUB_STEP_SUMMARY echo "1. Build and push container images using the \`Build and push images\` workflow with tag \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY echo "2. Verify the artifacts at the staging directory" >> $GITHUB_STEP_SUMMARY echo "3. Send [VOTE] email to dev@texera.apache.org" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Verification" >> $GITHUB_STEP_SUMMARY echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY echo "# Import KEYS and verify signatures" >> $GITHUB_STEP_SUMMARY echo "gpg --import KEYS" >> $GITHUB_STEP_SUMMARY echo "gpg --verify ${SRC_TARBALL}.asc ${SRC_TARBALL}" >> $GITHUB_STEP_SUMMARY echo "gpg --verify ${COMPOSE_TARBALL}.asc ${COMPOSE_TARBALL}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "# Verify SHA512 checksums" >> $GITHUB_STEP_SUMMARY echo "sha512sum -c ${SRC_TARBALL}.sha512" >> $GITHUB_STEP_SUMMARY echo "sha512sum -c ${COMPOSE_TARBALL}.sha512" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**KEYS file:** https://downloads.apache.org/incubator/texera/KEYS" >> $GITHUB_STEP_SUMMARY echo "✓ Release candidate workflow completed successfully!" - name: Display vote email template run: | echo "## Vote Email Template" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Copy the content below to send to dev@texera.apache.org:" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY cat "$GITHUB_WORKSPACE/vote-email.txt" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY ================================================ FILE: .github/workflows/direct-backport-push.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: Direct Backport Push on: push: branches: - main permissions: actions: read contents: write issues: write pull-requests: write statuses: write jobs: discover: name: Discover direct backport targets runs-on: ubuntu-latest outputs: pr_number: ${{ steps.discover.outputs.pr_number }} targets: ${{ steps.discover.outputs.targets }} has_targets: ${{ steps.discover.outputs.has_targets }} steps: - name: Resolve merged PR and green targets id: discover uses: actions/github-script@v8 with: script: | const sha = context.sha; const { owner, repo } = context.repo; // Strategy 1 (preferred): parse the squash-merge commit message. // ASF .asf.yaml forces squash merges with PR_TITLE_AND_DESC, so the // first line ends with "(#NNNN)". This is deterministic and avoids // the commit↔PR association index, which can lag for tens of seconds // after a merge. async function resolvePrFromMessage() { const message = context.payload?.head_commit?.message ?? ""; const firstLine = message.split("\n", 1)[0]; const match = firstLine.match(/\(#(\d+)\)\s*$/); if (!match) { core.info('Commit message does not end with "(#N)"; falling back to API.'); return null; } const prNumber = Number(match[1]); try { const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber, }); if (!pr.merged) { core.warning(`PR #${prNumber} extracted from commit message is not merged; falling back to API.`); return null; } core.info(`Resolved PR #${prNumber} from commit message.`); return pr; } catch (e) { core.warning(`Failed to fetch PR #${prNumber}: ${e.message}. Falling back to API.`); return null; } } // Strategy 2 (fallback): GET /commits/{sha}/pulls with exponential // backoff. 5 attempts at 0/2/4/8/16s — total worst case ~30s. async function resolvePrFromApi() { const backoffsMs = [0, 2000, 4000, 8000, 16000]; for (let i = 0; i < backoffsMs.length; i++) { if (backoffsMs[i] > 0) { core.info(`Retrying commit→PR lookup in ${backoffsMs[i] / 1000}s (attempt ${i + 1}/${backoffsMs.length}).`); await new Promise((resolve) => setTimeout(resolve, backoffsMs[i])); } const response = await github.request( "GET /repos/{owner}/{repo}/commits/{commit_sha}/pulls", { owner, repo, commit_sha: sha, } ); const pr = response.data.find((p) => p.merge_commit_sha === sha) ?? response.data[0]; if (pr) { core.info(`Resolved PR #${pr.number} from commits/${sha}/pulls on attempt ${i + 1}.`); return pr; } } return null; } const pullRequest = (await resolvePrFromMessage()) ?? (await resolvePrFromApi()); if (!pullRequest) { core.info(`No merged pull request is associated with ${sha}.`); core.setOutput("pr_number", ""); core.setOutput("targets", "[]"); core.setOutput("has_targets", "false"); return; } const requestedTargets = [...new Set( pullRequest.labels .map((label) => label.name) .filter((name) => /^release\/.+$/.test(name)) )].sort(); if (requestedTargets.length === 0) { core.info(`PR #${pullRequest.number} does not request any backports.`); core.setOutput("pr_number", String(pullRequest.number)); core.setOutput("targets", "[]"); core.setOutput("has_targets", "false"); return; } const buildRuns = await github.paginate( github.rest.actions.listWorkflowRuns, { owner, repo, workflow_id: "required-checks.yml", head_sha: pullRequest.head.sha, per_page: 100, } ); let greenTargets = []; if (buildRuns.length === 0) { core.warning(`No Build workflow runs found for ${pullRequest.head.sha}.`); } else { const allJobs = []; for (const run of buildRuns) { const jobs = await github.paginate( github.rest.actions.listJobsForWorkflowRun, { owner, repo, run_id: run.id, per_page: 100, } ); allJobs.push(...jobs); } greenTargets = requestedTargets.filter((target) => { const prefix = `backport (${target}) / `; const targetJobs = allJobs.filter((job) => job.name.startsWith(prefix)); // Treat skipped as green: precheck legitimately skips stacks // based on PR labels, matching the Required Checks aggregator. return ( targetJobs.length > 0 && targetJobs.every( (job) => job.conclusion === "success" || job.conclusion === "skipped" ) ); }); } const skippedTargets = requestedTargets.filter((target) => !greenTargets.includes(target)); if (skippedTargets.length > 0) { core.warning(`Skipping targets without a successful Backport run: ${skippedTargets.join(", ")}`); } core.setOutput("pr_number", String(pullRequest.number)); core.setOutput("targets", JSON.stringify(greenTargets)); core.setOutput("has_targets", greenTargets.length > 0 ? "true" : "false"); push-backports: needs: discover if: ${{ needs.discover.outputs.has_targets == 'true' }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: target: ${{ fromJson(needs.discover.outputs.targets) }} steps: - name: Checkout main uses: actions/checkout@v5 with: fetch-depth: 0 # Use AUTO_MERGE_TOKEN (fine-grained PAT) so the push to the release # branch retriggers workflows on that branch. GITHUB_TOKEN-authored # pushes are excluded from triggering downstream workflows, which # silences post-merge CI on backport commits. token: ${{ secrets.AUTO_MERGE_TOKEN || secrets.GITHUB_TOKEN }} - name: Cherry-pick merge commit onto target branch id: cherry_pick env: MERGE_SHA: ${{ github.sha }} TARGET_BRANCH: ${{ matrix.target }} run: | set -euo pipefail log() { printf '[backport %s] %s\n' "${TARGET_BRANCH}" "$*"; } group() { printf '::group::%s\n' "$*"; } endgroup() { printf '::endgroup::\n'; } group "Validate merge commit ${MERGE_SHA}" parent_count=$(git rev-list --parents -n 1 "${MERGE_SHA}" | awk '{print NF-1}') log "parent_count=${parent_count}" if [[ "${parent_count}" -ne 1 ]]; then echo "::error::Direct backport expects a squash-merged commit on main. ${MERGE_SHA} has ${parent_count} parents." exit 1 fi endgroup git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" # Reuse the squash commit's full message so the PR title, description, # and any Co-Authored-By trailers GitHub injected stay attached to # the backport commit. The author is the squash commit's author # (the original PR author for squash merges). original_author=$(git log -1 --format='%an <%ae>' "${MERGE_SHA}") merge_message=$(git log -1 --format=%B "${MERGE_SHA}") log "original_author=${original_author}" group "Cherry-pick onto ${TARGET_BRANCH}" git fetch --no-tags origin "${TARGET_BRANCH}" git checkout -B "${TARGET_BRANCH}" "origin/${TARGET_BRANCH}" base_sha=$(git rev-parse HEAD) log "base_sha=${base_sha}" if ! git cherry-pick --no-commit "${MERGE_SHA}"; then endgroup group "Conflict diagnosis" conflicts=$(git diff --name-only --diff-filter=U) log "Conflicted files:" printf ' %s\n' ${conflicts} # merge-base of the source commit and the target branch — the # most recent point where main and the release branch shared # history. Anything on main between the merge-base and the # source commit that touches a conflicting file is a candidate # "missing prerequisite" the backport probably needs first. merge_base=$(git merge-base "${MERGE_SHA}^" "origin/${TARGET_BRANCH}" || echo "") log "" log "merge_base(${MERGE_SHA:0:8}^, origin/${TARGET_BRANCH})=${merge_base:-}" for f in ${conflicts}; do log "" log "── ${f} ──" log "Conflict markers (line numbers in the working tree):" grep -nE '^(<<<<<<<|=======|>>>>>>>)' -- "${f}" | head -40 || true if [[ -n "${merge_base}" ]]; then log "" log "Commits on main that modified ${f} between ${merge_base:0:8}..${MERGE_SHA:0:8}^ (likely-missing prerequisites — consider backporting these first):" git log --oneline --no-merges "${merge_base}..${MERGE_SHA}^" -- "${f}" | head -20 || true log "" log "Commits on ${TARGET_BRANCH} that modified ${f} since ${merge_base:0:8} (changes already on the release branch that diverged from main):" git log --oneline --no-merges "${merge_base}..origin/${TARGET_BRANCH}" -- "${f}" | head -20 || true fi log "" log "Last 3 commits anywhere that touched ${f}:" git log --oneline --all -3 -- "${f}" || true done endgroup # Write a condensed markdown summary for the failure PR comment. # Caps: 5 files, 10 prerequisite commits total — keep PR # comments scannable; the full detail is in the job log above. diagnosis_file="${RUNNER_TEMP:-/tmp}/backport-diagnosis.md" { echo "**Conflicts in:**" num=0 for f in ${conflicts}; do if [[ ${num} -lt 5 ]]; then printf -- '- `%s`\n' "${f}" fi num=$((num + 1)) done if [[ ${num} -gt 5 ]]; then printf -- '- _(+%d more)_\n' "$((num - 5))" fi if [[ -n "${merge_base}" ]]; then echo echo "**Likely-missing prerequisites on main** (commits that touched these files between merge-base \`${merge_base:0:8}\` and \`${MERGE_SHA:0:8}^\` — consider backporting these first):" { for f in ${conflicts}; do git log --oneline --no-merges "${merge_base}..${MERGE_SHA}^" -- "${f}" || true done } | sort -u | head -10 | while IFS= read -r line; do [[ -n "${line}" ]] && printf -- '- `%s`\n' "${line}" done fi } > "${diagnosis_file}" log "Wrote diagnosis summary to ${diagnosis_file}" echo "::error::Cherry-pick of ${MERGE_SHA} onto ${TARGET_BRANCH} hit conflicts. See 'Conflict diagnosis' group above for likely-missing prerequisite commits and per-file conflict markers." exit 1 fi endgroup group "Compose backport commit message" new_message=$( printf '%s' "${merge_message}" \ | python3 .github/scripts/compose-backport-message.py "${MERGE_SHA}" ) printf '%s\n' "${new_message}" | git commit -F - --author="${original_author}" log "local_sha=$(git rev-parse HEAD)" endgroup # Push with retry. Transient failures (network, GitHub 5xx) are pure # backoff. Non-fast-forward (race with another push to the same # release branch) refreshes origin/ and rebases this single # cherry-pick on top before retrying. push_attempts=5 push_backoffs=(0 5 15 30 60) push_success=0 for i in $(seq 0 $((push_attempts - 1))); do if [[ "${push_backoffs[i]}" -gt 0 ]]; then log "Push attempt $((i + 1))/${push_attempts}: sleeping ${push_backoffs[i]}s" sleep "${push_backoffs[i]}" fi group "Push attempt $((i + 1))/${push_attempts}" if git push origin "HEAD:${TARGET_BRANCH}" 2>&1; then push_success=1 endgroup break fi push_rc=$? log "git push exit code=${push_rc}" endgroup # Refresh origin and rebase before retrying. If the remote did not # advance, this is a no-op rebase and the next push will likely # hit the same transient error — backoff handles that. log "Refreshing origin/${TARGET_BRANCH} before retry" git fetch --no-tags origin "${TARGET_BRANCH}" old_remote_head="${remote_head:-${base_sha}}" remote_head=$(git rev-parse "origin/${TARGET_BRANCH}") log "origin/${TARGET_BRANCH}=${remote_head}" if ! git rebase "origin/${TARGET_BRANCH}"; then conflicts=$(git diff --name-only --diff-filter=U) group "Rebase conflict diagnosis" log "Conflicted files during rebase:" printf ' %s\n' ${conflicts} log "" log "Commits on ${TARGET_BRANCH} that landed since this run started (${old_remote_head:0:8}..${remote_head:0:8}):" git log --oneline --no-merges "${old_remote_head}..${remote_head}" | head -20 || true for f in ${conflicts}; do log "" log "── ${f} ──" log "Commits in ${old_remote_head:0:8}..${remote_head:0:8} that touched ${f}:" git log --oneline --no-merges "${old_remote_head}..${remote_head}" -- "${f}" | head -20 || true done endgroup diagnosis_file="${RUNNER_TEMP:-/tmp}/backport-diagnosis.md" { printf -- '**Rebase conflict during push** — another commit landed on `%s` between the start of this run and the push attempt.\n\n' "${TARGET_BRANCH}" echo "**Conflicts in:**" num=0 for f in ${conflicts}; do if [[ ${num} -lt 5 ]]; then printf -- '- `%s`\n' "${f}" fi num=$((num + 1)) done if [[ ${num} -gt 5 ]]; then printf -- '- _(+%d more)_\n' "$((num - 5))" fi echo printf -- '**Racing commits on `%s`** (`%s..%s`):\n' \ "${TARGET_BRANCH}" "${old_remote_head:0:8}" "${remote_head:0:8}" git log --oneline --no-merges "${old_remote_head}..${remote_head}" | head -10 | while IFS= read -r line; do [[ -n "${line}" ]] && printf -- '- `%s`\n' "${line}" done } > "${diagnosis_file}" log "Wrote diagnosis summary to ${diagnosis_file}" git rebase --abort || true echo "::error::Rebase onto refreshed origin/${TARGET_BRANCH} hit a conflict; another commit changed the same lines. See 'Rebase conflict diagnosis' group for the racing commits." exit 1 fi done if [[ "${push_success}" -ne 1 ]]; then echo "::error::git push to ${TARGET_BRANCH} failed after ${push_attempts} attempts" exit 1 fi new_sha=$(git rev-parse HEAD) log "new_sha=${new_sha}" echo "new_sha=${new_sha}" >> "$GITHUB_OUTPUT" - name: Annotate original PR and commit on success if: success() uses: actions/github-script@v8 env: MERGE_SHA: ${{ github.sha }} TARGET_BRANCH: ${{ matrix.target }} NEW_SHA: ${{ steps.cherry_pick.outputs.new_sha }} PR_NUMBER: ${{ needs.discover.outputs.pr_number }} with: script: | const { MERGE_SHA, TARGET_BRANCH, NEW_SHA, PR_NUMBER } = process.env; const { owner, repo } = context.repo; const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`; const newCommitUrl = `${context.serverUrl}/${owner}/${repo}/commit/${NEW_SHA}`; const branchUrl = `${context.serverUrl}/${owner}/${repo}/tree/${TARGET_BRANCH}`; core.info( `Annotating: merge_sha=${MERGE_SHA} new_sha=${NEW_SHA} ` + `target=${TARGET_BRANCH} pr=${PR_NUMBER || ''}`, ); // Annotation API calls are best-effort: a transient 5xx shouldn't // demote the whole job to failure when the cherry-pick itself // succeeded. Each call gets a small bounded retry; if it still // fails we log a warning and move on so the other annotations // still get a chance to land. const backoffsMs = [0, 2000, 5000, 15000]; async function withRetry(name, fn) { for (let i = 0; i < backoffsMs.length; i++) { if (backoffsMs[i] > 0) { core.info( `Retrying ${name} in ${backoffsMs[i] / 1000}s ` + `(attempt ${i + 1}/${backoffsMs.length}).`, ); await new Promise((r) => setTimeout(r, backoffsMs[i])); } try { const out = await fn(); core.info(`${name} ok.`); return out; } catch (e) { const msg = `${name} failed (status ${e.status ?? '?'}): ${e.message}`; if (i === backoffsMs.length - 1) { core.warning(`${msg} — giving up.`); return null; } core.warning(`${msg} — will retry.`); } } } // GitHub auto-derives the branch badges shown next to a commit // title from "branches that contain this commit". A cherry-pick // produces a different SHA than the main commit, so the release // branch will never naturally appear on the main commit's page. // Two surfacing channels instead: // 1. A commit status — appears as a green check badge in the // same row as CI statuses on the commit and any PRs that // reference it. target_url drops the user on the new commit. // 2. A commit comment with the same info, for richer detail. await withRetry("createCommitStatus", () => github.rest.repos.createCommitStatus({ owner, repo, sha: MERGE_SHA, state: "success", context: `backport/${TARGET_BRANCH}`, description: `Backported as ${NEW_SHA.slice(0, 7)}`, target_url: newCommitUrl, }), ); await withRetry("createCommitComment", () => github.rest.repos.createCommitComment({ owner, repo, commit_sha: MERGE_SHA, body: `Backported to [\`${TARGET_BRANCH}\`](${branchUrl}) as ` + `[\`${NEW_SHA.slice(0, 7)}\`](${newCommitUrl}). ` + `[Run](${runUrl})`, }), ); if (PR_NUMBER) { await withRetry("createPRComment", () => github.rest.issues.createComment({ owner, repo, issue_number: Number(PR_NUMBER), body: `Backport to [\`${TARGET_BRANCH}\`](${branchUrl}) succeeded ` + `as [\`${NEW_SHA.slice(0, 7)}\`](${newCommitUrl}). ` + `[Run](${runUrl})`, }), ); } else { core.info("No PR number resolved — skipping PR comment."); } - name: Annotate original PR and commit on failure if: failure() uses: actions/github-script@v8 env: MERGE_SHA: ${{ github.sha }} TARGET_BRANCH: ${{ matrix.target }} PR_NUMBER: ${{ needs.discover.outputs.pr_number }} with: script: | const { MERGE_SHA, TARGET_BRANCH, PR_NUMBER } = process.env; const { owner, repo } = context.repo; const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`; core.info( `Annotating failure: merge_sha=${MERGE_SHA} ` + `target=${TARGET_BRANCH} pr=${PR_NUMBER || ''}`, ); const backoffsMs = [0, 2000, 5000, 15000]; async function withRetry(name, fn) { for (let i = 0; i < backoffsMs.length; i++) { if (backoffsMs[i] > 0) { core.info( `Retrying ${name} in ${backoffsMs[i] / 1000}s ` + `(attempt ${i + 1}/${backoffsMs.length}).`, ); await new Promise((r) => setTimeout(r, backoffsMs[i])); } try { const out = await fn(); core.info(`${name} ok.`); return out; } catch (e) { const msg = `${name} failed (status ${e.status ?? '?'}): ${e.message}`; if (i === backoffsMs.length - 1) { core.warning(`${msg} — giving up.`); return null; } core.warning(`${msg} — will retry.`); } } } // Find this matrix leg's job so the link drops the user directly // onto the failing log instead of the run summary. let jobUrl = runUrl; const jobs = await withRetry("listJobsForWorkflowRun", () => github.paginate(github.rest.actions.listJobsForWorkflowRun, { owner, repo, run_id: context.runId, per_page: 100, }), ); if (jobs) { const me = jobs.find((j) => j.name.includes(`(${TARGET_BRANCH})`)); if (me?.html_url) { jobUrl = me.html_url; core.info(`Resolved job URL for matrix leg: ${jobUrl}`); } else { core.info( `No job matched name including "(${TARGET_BRANCH})"; ` + "falling back to run URL.", ); } } await withRetry("createCommitStatus", () => github.rest.repos.createCommitStatus({ owner, repo, sha: MERGE_SHA, state: "failure", context: `backport/${TARGET_BRANCH}`, description: "Backport failed", target_url: jobUrl, }), ); // Pick up the markdown diagnosis the bash step wrote on // conflict (cherry-pick or rebase). Missing file just means // the failure happened elsewhere (e.g. push 5xx after retries, // permissions) — we still post the basic comment. const fs = require("fs"); const diagPath = `${process.env.RUNNER_TEMP || "/tmp"}/backport-diagnosis.md`; let diagnosis = ""; try { diagnosis = fs.readFileSync(diagPath, "utf8").trim(); if (diagnosis) { core.info(`Found diagnosis at ${diagPath} (${diagnosis.length} chars)`); } } catch (e) { core.info( `No diagnosis file at ${diagPath} (${e.code}) — failure likely not a conflict.`, ); } if (PR_NUMBER) { const head = `Backport to \`${TARGET_BRANCH}\` failed. ` + `See [job log](${jobUrl}).`; const body = diagnosis ? `${head}\n\n${diagnosis}` : head; await withRetry("createPRComment", () => github.rest.issues.createComment({ owner, repo, issue_number: Number(PR_NUMBER), body, }), ); } else { core.info("No PR number resolved — skipping PR comment."); } ================================================ FILE: .github/workflows/license-binary-checker.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. name: License Binary Checker # Scheduled drift checker for per-module LICENSE-binary files. Calls # build.yml with mode=nightly (strict) and files / updates / closes # one tracking issue (label `license-binary-drift`). Sub-task of #4688. on: schedule: # 11:00 UTC daily = 04:00 PDT / 03:00 PST. - cron: "0 11 * * *" workflow_dispatch: permissions: contents: read issues: write # create / update / close the tracking issue actions: read # listJobsForWorkflowRun in the report step concurrency: group: license-binary-checker cancel-in-progress: false jobs: build: uses: ./.github/workflows/build.yml with: mode: nightly secrets: inherit report: if: always() needs: [build] runs-on: ubuntu-latest steps: - name: File or update tracking issue uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const TRACKING_LABEL = 'license-binary-drift'; const TITLE = 'License-binary drift detected'; // Match license-check step names; ignore unrelated failures. const LICENSE_STEP_RE = /license[-\s]?binary|binary licenses/i; const { owner, repo } = context.repo; const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`; // Walk every job in the current run (which includes the called // build.yml jobs, prefixed with the calling job id `build`). const jobs = await github.paginate( github.rest.actions.listJobsForWorkflowRun, { owner, repo, run_id: context.runId, per_page: 100 } ); const licenseFailures = []; const licenseSuccesses = []; for (const job of jobs) { for (const step of job.steps || []) { if (!LICENSE_STEP_RE.test(step.name)) continue; if (step.conclusion === 'failure') { licenseFailures.push({ job: job.name, step: step.name, url: job.html_url, }); } else if (step.conclusion === 'success') { licenseSuccesses.push({ job: job.name, step: step.name }); } } } const anyDrift = licenseFailures.length > 0; core.info( `License-check steps: ${licenseSuccesses.length} ok, ` + `${licenseFailures.length} failed.` ); const { data: existing } = await github.rest.issues.listForRepo({ owner, repo, state: 'open', labels: TRACKING_LABEL, per_page: 5, }); // Only manage issues for runs against the default branch. const defaultBranch = context.payload.repository.default_branch; const isDefaultBranch = context.ref === `refs/heads/${defaultBranch}` || context.eventName === 'schedule'; if (!isDefaultBranch) { core.info( `Not on default branch (ref=${context.ref}); ` + `skipping issue management. Drift detected: ${anyDrift}.` ); return; } if (!anyDrift) { if (existing.length === 0) { core.info('No drift, no existing tracking issue. Nothing to do.'); return; } for (const issue of existing) { await github.rest.issues.update({ owner, repo, issue_number: issue.number, state: 'closed', state_reason: 'completed', }); await github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body: `Closed automatically: scheduled run ${runUrl} found ` + `no license-binary drift across any ecosystem.`, }); core.info(`Closed stale tracking issue #${issue.number}`); } return; } // Body shape mirrors ISSUE_TEMPLATE/task-template.yaml. const failureLines = licenseFailures.map( (f) => `- \`${f.job}\` — ${f.step} ([logs](${f.url}))` ); const body = `### Task Summary\n\n` + `License-binary drift on \`${context.sha.slice(0, 7)}\` ` + `([run](${runUrl})).\n\n` + `**Where:**\n\n` + failureLines.join('\n') + `\n\n**How to resolve:** refresh the per-module ` + `\`LICENSE-binary\` file(s) to match the bundled versions.\n\n` + `### Task Type\n\n` + `- [ ] Refactor / Cleanup\n` + `- [x] DevOps / Deployment / CI\n` + `- [ ] Testing / QA\n` + `- [ ] Documentation\n` + `- [ ] Performance\n` + `- [ ] Other\n\n` + `---\n\n` + `By submitting this issue, you agree to follow the ` + `[Apache Code of Conduct]` + `(https://www.apache.org/foundation/policies/conduct).`; if (existing.length > 0) { const issue = existing[0]; await github.rest.issues.update({ owner, repo, issue_number: issue.number, title: TITLE, body, }); core.info(`Updated tracking issue #${issue.number}`); for (const extra of existing.slice(1)) { await github.rest.issues.update({ owner, repo, issue_number: extra.number, state: 'closed', state_reason: 'not_planned', }); core.info(`Closed duplicate tracking issue #${extra.number}`); } } else { const { data: created } = await github.rest.issues.create({ owner, repo, title: TITLE, body, labels: [TRACKING_LABEL, 'ci', 'triage'], }); core.info(`Filed new tracking issue #${created.number}`); } ================================================ FILE: .github/workflows/lint-pr.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: Lint PR on: pull_request_target: types: - opened - edited - reopened - synchronize jobs: main: name: Validate PR title runs-on: ubuntu-latest permissions: pull-requests: read steps: - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/pr-assignment.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: PR assignment on: pull_request_target: types: [opened, edited, closed] permissions: issues: write pull-requests: write jobs: # All three behaviors live as steps under one job so the PR Checks # tab shows a single entry per event instead of two-or-three skipped # siblings. Step-level if-guards keep the actual work scoped. pr-assignment: runs-on: ubuntu-latest steps: - name: Self-assign PR author to the PR if: >- github.event.action == 'opened' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.assignees[0] == null uses: actions/github-script@v8 with: script: | const pr = context.payload.pull_request; core.info(`PR #${pr.number} opened by ${pr.user.login}; self-assigning.`); try { await github.rest.issues.addAssignees({ ...context.repo, issue_number: pr.number, assignees: [pr.user.login], }); core.info(`Assigned ${pr.user.login} to PR #${pr.number}`); } catch (e) { core.warning(`Self-assign on PR #${pr.number} failed: ${e.message}`); } - name: Sync PR opener as assignee on linked issues # Mirror the PR opener as an assignee on each same-repo issue listed # in closingIssuesReferences. On body edits, also drop the opener # from issues whose closing keyword was removed. Other manual # assignees are never touched here, so this never fights with the # merge-time credit step or human triage decisions. if: >- contains(fromJSON('["opened","edited"]'), github.event.action) && github.event.pull_request.state == 'open' && github.event.pull_request.user.type != 'Bot' uses: actions/github-script@v8 with: script: | const { owner, repo } = context.repo; const pr = context.payload.pull_request; const opener = pr.user.login; core.info(`Event ${context.payload.action} on PR #${pr.number} by ${opener}; syncing closing-issue assignees.`); const { repository: { pullRequest: prq } } = await github.graphql(` query($owner: String!, $repo: String!, $pr: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { closingIssuesReferences(first: 50) { nodes { number repository { nameWithOwner } } } } } }`, { owner, repo, pr: pr.number }); const sameRepo = `${owner}/${repo}`; const allRefs = prq.closingIssuesReferences.nodes; const linked = allRefs .filter((n) => n.repository.nameWithOwner === sameRepo) .map((n) => n.number); const crossRepo = allRefs.filter((n) => n.repository.nameWithOwner !== sameRepo); core.info(`Found ${linked.length} same-repo closing reference(s): ${linked.join(', ') || '(none)'}`); if (crossRepo.length) { core.info(`Skipping ${crossRepo.length} cross-repo reference(s): ${crossRepo.map((n) => `${n.repository.nameWithOwner}#${n.number}`).join(', ')}`); } for (const issue_number of linked) { try { await github.rest.issues.addAssignees({ owner, repo, issue_number, assignees: [opener], }); core.info(`Assigned ${opener} to issue #${issue_number}`); } catch (e) { core.warning(`addAssignees on #${issue_number} failed: ${e.message}`); } } // On body edit, find closing refs that disappeared from the body // and remove the opener from those issues. closingIssuesReferences // is a snapshot of the *new* state, so we need text-diff to detect // removals. Cross-repo refs are intentionally skipped. if ( context.payload.action === 'edited' && context.payload.changes && context.payload.changes.body && typeof context.payload.changes.body.from === 'string' ) { // GitHub also recognizes the colon form ("Closes: #123"), so // allow an optional ":" between the keyword and the issue // ref. Multiple refs on one line still need their own // keyword each ("Closes #1, closes #2"), matching the // `closingIssuesReferences` semantics — `Closes #1, #2` // links only #1, so the diff treats only #1 as removed. const re = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?):?\s+#(\d+)/gi; const oldBody = context.payload.changes.body.from || ''; const newBody = pr.body || ''; const oldRefs = new Set([...oldBody.matchAll(re)].map((m) => Number(m[1]))); const newRefs = new Set([...newBody.matchAll(re)].map((m) => Number(m[1]))); const removed = [...oldRefs].filter((n) => !newRefs.has(n)); core.info(`Body-diff: oldRefs=[${[...oldRefs].join(',')}] newRefs=[${[...newRefs].join(',')}] removed=[${removed.join(',')}]`); for (const issue_number of removed) { try { await github.rest.issues.removeAssignees({ owner, repo, issue_number, assignees: [opener], }); core.info(`Unassigned ${opener} from issue #${issue_number}`); } catch (e) { core.warning(`removeAssignees on #${issue_number} failed: ${e.message}`); } } } else if (context.payload.action === 'edited') { core.info(`Body unchanged on edit; skipping removal-detection.`); } - name: Unassign PR opener from linked issues on PR close without merge # When a PR is closed without merging, the opener was added to # linked issues by the "Sync PR opener" step on open/edit. Mirror # the close: remove them so abandoned PRs do not leave stale # assignees. With no other assignee the issue then drops back into # the `is:open no:assignee` triage filter automatically. Cross-repo # refs are skipped, consistent with the assign side. if: >- github.event.action == 'closed' && github.event.pull_request.merged == false && github.event.pull_request.user.type != 'Bot' uses: actions/github-script@v8 with: script: | const { owner, repo } = context.repo; const pr = context.payload.pull_request; const opener = pr.user.login; core.info(`PR #${pr.number} closed without merge; unassigning ${opener} from linked issues.`); const { repository: { pullRequest: prq } } = await github.graphql(` query($owner: String!, $repo: String!, $pr: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { closingIssuesReferences(first: 50) { nodes { number repository { nameWithOwner } } } } } }`, { owner, repo, pr: pr.number }); const sameRepo = `${owner}/${repo}`; const linked = prq.closingIssuesReferences.nodes .filter((n) => n.repository.nameWithOwner === sameRepo) .map((n) => n.number); core.info(`Found ${linked.length} same-repo closing reference(s): ${linked.join(', ') || '(none)'}`); for (const issue_number of linked) { try { await github.rest.issues.removeAssignees({ owner, repo, issue_number, assignees: [opener], }); core.info(`Unassigned ${opener} from issue #${issue_number}`); } catch (e) { core.warning(`removeAssignees on #${issue_number} failed: ${e.message}`); } } - name: Credit issue assignees on PR merge if: github.event.action == 'closed' && github.event.pull_request.merged uses: actions/github-script@v8 with: script: | const { owner, repo } = context.repo; const pr = context.payload.pull_request; const opener = pr.user; const isHuman = (l) => l && !l.endsWith('[bot]'); core.info(`PR #${pr.number} merged by opener=${opener.login}; computing credited authors.`); const { repository: { pullRequest: prq } } = await github.graphql(` query($owner: String!, $repo: String!, $pr: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { closingIssuesReferences(first: 50) { nodes { number repository { nameWithOwner } assignees(first: 20) { nodes { login } } } } commits(first: 250) { nodes { commit { parents { totalCount } authors(first: 10) { nodes { user { login } } } } } } } } }`, { owner, repo, pr: pr.number }); const authors = new Set(); if (opener.type !== 'Bot' && isHuman(opener.login)) authors.add(opener.login); for (const { commit } of prq.commits.nodes) { if (commit.parents.totalCount > 1) continue; for (const a of commit.authors.nodes) { if (isHuman(a.user?.login)) authors.add(a.user.login); } } const credited = [...authors].slice(0, 10); const creditedSet = new Set(credited); core.info(`Credited authors (max 10, [bot] filtered): [${credited.join(', ') || '(none)'}]`); if (!credited.length) { core.info(`No human authors to credit; skipping all linked issues.`); return; } const sameRepoIssues = prq.closingIssuesReferences.nodes.filter((n) => n.repository.nameWithOwner === `${owner}/${repo}`); core.info(`Linked same-repo issues to credit: [${sameRepoIssues.map((i) => `#${i.number}`).join(', ') || '(none)'}]`); for (const issue of sameRepoIssues) { const current = issue.assignees.nodes.map(n => n.login); const toRemove = current.filter(l => !creditedSet.has(l)); const toAdd = credited.filter(l => !current.includes(l)); const args = { owner, repo, issue_number: issue.number }; core.info(`Issue #${issue.number}: current=[${current.join(',')}] credited=[${credited.join(',')}] toRemove=[${toRemove.join(',')}] toAdd=[${toAdd.join(',')}]`); try { if (toRemove.length) { await github.rest.issues.removeAssignees({ ...args, assignees: toRemove }); core.info(`Removed [${toRemove.join(', ')}] from issue #${issue.number}`); } if (toAdd.length) { await github.rest.issues.addAssignees({ ...args, assignees: toAdd }); core.info(`Added [${toAdd.join(', ')}] to issue #${issue.number}`); } } catch (e) { core.warning(`Updating assignees on #${issue.number} failed: ${e.message}`); } } ================================================ FILE: .github/workflows/pr-labeler.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: "Pull Request Labeler" on: - pull_request_target jobs: labeler: permissions: contents: read pull-requests: write runs-on: ubuntu-latest steps: - uses: actions/labeler@v6 with: sync-labels: true ================================================ FILE: .github/workflows/required-checks.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. name: Required Checks on: push: branches: - 'ci-enable/**' - 'main' - 'release/**' pull_request: types: - opened - reopened - synchronize - labeled - unlabeled workflow_dispatch: permissions: checks: write contents: read pull-requests: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') }} jobs: # Precheck decides which downstream jobs run for this event: # - On PR events, wait for the Pull Request Labeler workflow to finish so # the labels it applies (frontend, docs, dev, ...) are available, then # gate run_* outputs on those labels. # - run_frontend / run_amber / run_platform / run_python / # run_agent_service: gate the main build stacks. Each labeler-applied # label maps to the stacks it requires (LABEL_STACKS below); the run # set is the union across all PR labels. Empty union (e.g. docs-only # / dev-only PRs) skips every stack. Push and workflow_dispatch # events run every stack. # - backport_targets: JSON array of release/* labels currently on the PR. # Drives the backport matrix; empty array means no backport runs. precheck: name: Precheck runs-on: ubuntu-latest outputs: run_frontend: ${{ steps.decide.outputs.run_frontend }} run_amber: ${{ steps.decide.outputs.run_amber }} run_amber_integration: ${{ steps.decide.outputs.run_amber_integration }} run_platform: ${{ steps.decide.outputs.run_platform }} run_python: ${{ steps.decide.outputs.run_python }} run_agent_service: ${{ steps.decide.outputs.run_agent_service }} backport_targets: ${{ steps.decide.outputs.backport_targets }} steps: - name: Wait for Pull Request Labeler if: github.event_name == 'pull_request' uses: actions/github-script@v8 with: script: | const ref = context.payload.pull_request.head.sha; const maxAttempts = 30; for (let i = 0; i < maxAttempts; i++) { const { data } = await github.rest.checks.listForRef({ owner: context.repo.owner, repo: context.repo.repo, ref, check_name: "labeler", }); const check = data.check_runs[0]; if (check && check.status === "completed") { core.info(`labeler ${check.conclusion}`); return; } core.info(`labeler not ready (attempt ${i + 1}/${maxAttempts})`); await new Promise((r) => setTimeout(r, 10000)); } core.warning("labeler did not complete within 5 minutes; proceeding with current labels."); - name: Decide which jobs to run id: decide uses: actions/github-script@v8 with: script: | const eventName = context.eventName; let labels = []; if (eventName === "pull_request") { // Re-fetch labels: the labeler may have just added some. const { data: pr } = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, pull_number: context.payload.pull_request.number, }); labels = pr.labels.map((l) => l.name); core.info(`PR labels: ${labels.join(", ") || "(none)"}`); } // Map each labeler-applied label to the stacks it requires. The // run set is the union across all PR labels. Labels not listed // here (release/*, feature, fix, refactor) contribute no stacks. // dependencies is intentionally a no-op: every dep manifest the // labeler matches lives under a component dir and is already // covered by that component's label. // // label | frontend | amber | amber-integ | platform | python | agent-service // ------------------|----------|-------|-------------|----------|--------|-------------- // frontend | x | | | | | // python | | | x | | x | // engine | | x | x | | | // amber-integration | | | x | | | // platform | | | | x | | // agent-service | | | | | | x // common | | x | x | x | | (root // scala // build/lint // config) // ddl-change | | x | x | x | | // ci | x | x | x | x | x | x // docs / dev / | | | | | | // deps / release/ | | | | | | // * / branch | | | | | | // // amber-integration runs the Scala tests tagged // @org.apache.texera.amber.tags.IntegrationTest (e2e specs that // spawn Python UDF workers). The labeler attaches `python` to // any *.py change (including amber/src/{main,test}/python/**), // so `engine` does not need to fire the python stack itself — // pure-Python amber changes pick up `python` directly. The // `amber-integration` label catches *IntegrationSpec.scala // edits so a test-only change does not trigger the full // Scala-only amber stack. const LABEL_STACKS = { frontend: ["frontend"], python: ["amber-integration", "python"], engine: ["amber", "amber-integration"], "amber-integration": ["amber-integration"], platform: ["platform"], "agent-service": ["agent-service"], common: ["amber", "amber-integration", "platform"], "ddl-change": ["amber", "amber-integration", "platform"], ci: ["frontend", "amber", "amber-integration", "platform", "python", "agent-service"], }; let runFrontend = true; let runAmber = true; let runAmberIntegration = true; let runPlatform = true; let runPython = true; let runAgentService = true; if (eventName === "pull_request") { const stacks = new Set(); for (const label of labels) { for (const stack of LABEL_STACKS[label] || []) { stacks.add(stack); } } runFrontend = stacks.has("frontend"); runAmber = stacks.has("amber"); runAmberIntegration = stacks.has("amber-integration"); runPlatform = stacks.has("platform"); runPython = stacks.has("python"); runAgentService = stacks.has("agent-service"); core.info( `Stacks selected by label union: ${[...stacks].sort().join(", ") || "(none)"}` ); } core.setOutput("run_frontend", runFrontend ? "true" : "false"); core.setOutput("run_amber", runAmber ? "true" : "false"); core.setOutput("run_amber_integration", runAmberIntegration ? "true" : "false"); core.setOutput("run_platform", runPlatform ? "true" : "false"); core.setOutput("run_python", runPython ? "true" : "false"); core.setOutput("run_agent_service", runAgentService ? "true" : "false"); // Backport targets: all current release/* labels on the PR. const targets = [...new Set(labels.filter((n) => /^release\/.+$/.test(n)))].sort(); if (targets.length === 0) { core.info("No backport targets on PR."); } else { core.info(`Backport targets: ${targets.join(", ")}`); } core.setOutput("backport_targets", JSON.stringify(targets)); cleanup-stale-backport: if: ${{ github.event_name == 'pull_request' && github.event.action == 'unlabeled' && startsWith(github.event.label.name, 'release/') }} runs-on: ubuntu-latest steps: - name: Cancel obsolete backport check_runs for the removed target uses: actions/github-script@v8 with: script: | const { owner, repo } = context.repo; const target = context.payload.label.name; const headSha = context.payload.pull_request.head.sha; const prefix = `backport (${target}) `; const checks = await github.paginate( github.rest.checks.listForRef, { owner, repo, ref: headSha, per_page: 100 } ); for (const check of checks) { if (!check.name.startsWith(prefix)) continue; if (check.status === "completed" && check.conclusion === "cancelled") continue; try { await github.rest.checks.update({ owner, repo, check_run_id: check.id, status: "completed", conclusion: "cancelled", }); core.info(`Cancelled check ${check.name}`); } catch (e) { core.warning(`Failed to update check ${check.id} (${check.name}): ${e.message}`); } } build: needs: precheck uses: ./.github/workflows/build.yml with: run_frontend: ${{ needs.precheck.outputs.run_frontend == 'true' }} run_amber: ${{ needs.precheck.outputs.run_amber == 'true' }} run_amber_integration: ${{ needs.precheck.outputs.run_amber_integration == 'true' }} run_platform: ${{ needs.precheck.outputs.run_platform == 'true' }} run_python: ${{ needs.precheck.outputs.run_python == 'true' }} run_agent_service: ${{ needs.precheck.outputs.run_agent_service == 'true' }} secrets: inherit backport: needs: precheck if: ${{ needs.precheck.outputs.backport_targets != '[]' }} strategy: fail-fast: false matrix: target: ${{ fromJson(needs.precheck.outputs.backport_targets) }} uses: ./.github/workflows/build.yml with: checkout_ref: refs/pull/${{ github.event.pull_request.number }}/head backport_target_branch: ${{ matrix.target }} backport_commit_range: ${{ format('{0}..{1}', github.event.pull_request.base.sha, github.event.pull_request.head.sha) }} job_name_suffix: "" run_frontend: ${{ needs.precheck.outputs.run_frontend == 'true' }} run_amber: ${{ needs.precheck.outputs.run_amber == 'true' }} run_amber_integration: ${{ needs.precheck.outputs.run_amber_integration == 'true' }} run_platform: ${{ needs.precheck.outputs.run_platform == 'true' }} run_python: ${{ needs.precheck.outputs.run_python == 'true' }} run_agent_service: ${{ needs.precheck.outputs.run_agent_service == 'true' }} secrets: inherit required-checks: # Do not rename this job — its display name is referenced in .asf.yaml. name: Required Checks needs: [precheck, build, backport] if: always() runs-on: ubuntu-latest steps: - name: Verify all required checks succeeded or were skipped run: | declare -A results=( [precheck]="${{ needs.precheck.result }}" [build]="${{ needs.build.result }}" [backport]="${{ needs.backport.result }}" ) failed=0 for job in "${!results[@]}"; do r="${results[$job]}" echo "${job}: ${r}" if [[ "$r" != "success" && "$r" != "skipped" ]]; then failed=1 fi done if (( failed )); then echo "::error::One or more required checks did not succeed." exit 1 fi echo "All required checks succeeded or were skipped." ================================================ FILE: .gitignore ================================================ # Ignoring binary/output target/ out/ # Ignoring packages *.jar *.war *.nar *.ear *.zip *.tar.gz *.rar # Ignoring VSCode related files .vscode/ # Ignoring IntelliJ related files *.iml .idea/ .idea_modules/ lib_managed/ src_managed/ # Ignoring Eclipse files .classpath .project .settings # Ignoring sublime files *.sublime-workspace # Ignoring index folder and data folder index/ catalog/ plan/ plan_files/ query-results/ # Ignoring Mac OSX specific files .DS_Store # Ignoring jenv related files .java-version # Ignoring scala related files hs_err_pid* # Ignoring python related files venv/ __pycache__/ *.py[cod] *$py.class .ipynb_checkpoints .pytype/ .coverage coverage.xml .pytest_cache/ # Ignoring python generated files *.model *.pkl # Ingoring user generated resources user-resources/ # Ingoring gmail tokens gmail/ # Ignoring maven related files pom.xml.tag pom.xml.releaseBackup pom.xml.versionsBackup pom.xml.next release.properties dependency-reduced-pom.xml buildNumber.properties .mvn/timing.properties # https://github.com/takari/maven-wrapper#usage-without-binary-jar .mvn/wrapper/maven-wrapper.jar # Ignoring sbt related files .bsp/ sbt.json # Ignoring rebel related files rebel.xml # Ignoring log files *.log *.log.gz # Ignoring the entire log folder logs/ # Ignoring package-lock.json package-lock.json # Ignoring protobuf related files scalapb/scalapb # Ignoring credentials client_secret_* StoredCredential* **/apache2/ **/Apache24/ **/php/ Composer-Setup.exe # Ignoring folders generated by vscode IDE .metals/ .bloop/ .ammonite/ metals.sbt # Ignoring Helm related files **/charts/*.tgz **/texera-helmchart/charts/ **/texera-helmchart/*/requirements.lock **/texera-helmchart/Chart.lock **/.helm/ # Additional Helm/Kubernetes related files .kubeconfig *.kubeconfig **/.kube/ values-*.yaml # Any environment-specific value overrides # Ignore nested node modules **/node_modules/ **/package-lock.json .env # agent-service is Bun-based; yarn/npm artifacts aren't needed agent-service/.yarn/ agent-service/.yarnrc.yml agent-service/yarn.lock agent-service/CLAUDE.md # Ignoring local Claude Code artifacts (settings, worktrees, scratch) .claude/ ================================================ FILE: .jvmopts ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # Required by Kryo: https://github.com/altoo-ag/pekko-kryo-serialization#using-kryo-on-jdk-17 --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED # Required by Apache Arrow: https://arrow.apache.org/java/main/install.html --add-opens=java.base/java.nio=ALL-UNNAMED # Required by Apache Pekko: https://pekko.apache.org/docs/pekko/snapshot/release-notes/releases-2.0.html --add-opens=java.base/jdk.internal.misc=ALL-UNNAMED ================================================ FILE: .licenserc.yaml ================================================ header: license: spdx-id: Apache-2.0 copyright-owner: Apache Software Foundation paths-ignore: - 'licenses' - '**/*.md' - '**/*.csv' - '**/*.txt' - '**/*.json' - '**/*.jsonl' - 'DESCRIPTION' - 'DISCLAIMER' - 'LICENSE' - 'NOTICE' # Per-module LICENSE-binary / NOTICE-binary describing the third-party # content bundled in each module's binary artifact (Universal dist zip # or Docker image). Plain-text manifests, not source files. - '**/LICENSE-binary' - '**/LICENSE-binary-*' - '**/NOTICE-binary' - '.dockerignore' - '.gitattributes' - '.github/PULL_REQUEST_TEMPLATE' - 'yarn.lock' - '.nvmrc' - '.htaccess' - '.gitkeep' - 'site.webmanifest' - '.gitignore' - '.licenserc.yaml' - 'frontend/.yarn/**' - 'amber/src/main/python/proto/**' - '**/.env.example' - '**/.prettierrc' - '**/bun.lock' # Third-party code with MIT license - see LICENSE file for attribution - 'common/workflow-operator/src/main/scala/com/kjetland/**' # TypeFox monaco-languageclient derived files (MIT License) - 'pyright-language-service/src/main.ts' - 'pyright-language-service/src/language-server-runner.ts' - 'pyright-language-service/src/server-commons.ts' - 'frontend/src/app/common/formly/array.type.ts' - 'frontend/src/app/common/formly/object.type.ts' - 'frontend/src/app/common/formly/multischema.type.ts' - 'frontend/src/app/common/formly/null.type.ts' # Third-party SVG assets - see LICENSE file for attribution - 'frontend/src/assets/svg/operator-view-result.svg' - 'frontend/src/assets/svg/operator-reuse-cache-invalid.svg' - 'frontend/src/assets/svg/operator-reuse-cache-valid.svg' - 'frontend/src/app/common/type/proto/org/apache/texera/amber/core/virtualidentity.ts' - 'frontend/src/app/common/type/proto/org/apache/texera/amber/core/workflow.ts' - 'frontend/src/app/common/type/proto/google/protobuf/descriptor.ts' - 'frontend/src/app/common/type/proto/scalapb/scalapb.ts' ================================================ FILE: .run/AccessControlService.run.xml ================================================ ================================================ FILE: .run/ComputingUnitManagingService.run.xml ================================================ ================================================ FILE: .run/ComputingUnitMaster.run.xml ================================================ ================================================ FILE: .run/ComputingUnitWorker.run.xml ================================================ ================================================ FILE: .run/ConfigService.run.xml ================================================ ================================================ FILE: .run/FileService.run.xml ================================================ ================================================ FILE: .run/TexeraWebApplication.run.xml ================================================ ================================================ FILE: .run/WorkflowCompilingService.run.xml ================================================ ================================================ FILE: .run/frontend.run.xml ================================================
""" // Test case 1: With a simple long string val tuple1 = Tuple .builder(schema) .add("longStringCol", AttributeType.STRING, longString) .build() // Test case 2: With HTML visualization content val tuple2 = Tuple .builder(schema) .add("longStringCol", AttributeType.STRING, htmlVisualizationString) .build() // When isVisualization is false (default) val resultsDefault = ExecutionResultService.convertTuplesToJson(List(tuple1, tuple2)) // Verify truncation happens resultsDefault(0).get("longStringCol").asText() should ( have length 103 and // 100 chars + "..." startWith("a" * 100) and endWith("...") ) resultsDefault(1).get("longStringCol").asText() should ( have length 103 and endWith("...") ) // When isVisualization is true val resultsVisualization = ExecutionResultService.convertTuplesToJson(List(tuple1, tuple2), true) // Verify no truncation happens resultsVisualization(0).get("longStringCol").asText() shouldBe longString resultsVisualization(0).get("longStringCol").asText() should have length 150 resultsVisualization(1).get("longStringCol").asText() shouldBe htmlVisualizationString resultsVisualization(1) .get("longStringCol") .asText() should have length htmlVisualizationString.length } it should "handle direct comparison between non-visualization and visualization mode" in { val attributes = List( new Attribute("col1", AttributeType.STRING), new Attribute("col2", AttributeType.STRING), new Attribute("col3", AttributeType.STRING) ) val schema = new Schema(attributes) // Create strings of various lengths val shortString = "short string" // under maxStringLength val exactLengthString = "x" * 100 // exactly maxStringLength val longString = "y" * 200 // over maxStringLength val tuple = Tuple .builder(schema) .add("col1", AttributeType.STRING, shortString) .add("col2", AttributeType.STRING, exactLengthString) .add("col3", AttributeType.STRING, longString) .build() // Convert with both modes val resultDefault = ExecutionResultService.convertTuplesToJson(List(tuple), false) val resultVisualization = ExecutionResultService.convertTuplesToJson(List(tuple), true) // Short strings should be the same in both modes resultDefault(0).get("col1").asText() shouldBe shortString resultVisualization(0).get("col1").asText() shouldBe shortString // Exact length strings should be the same in both modes resultDefault(0).get("col2").asText() shouldBe exactLengthString resultVisualization(0).get("col2").asText() shouldBe exactLengthString // Long strings should be truncated in default mode but not in visualization mode resultDefault(0).get("col3").asText() should ( have length 103 and // 100 chars + "..." startWith("y" * 100) and endWith("...") ) resultVisualization(0).get("col3").asText() shouldBe longString resultVisualization(0).get("col3").asText() should have length 200 } it should "apply visualization flag correctly to mixed collections" in { val attributes = List( new Attribute("value", AttributeType.STRING) ) val schema = new Schema(attributes) // Create a collection with both short and long strings val tuples = List( Tuple.builder(schema).add("value", AttributeType.STRING, "short").build(), Tuple.builder(schema).add("value", AttributeType.STRING, "a" * 150).build(), Tuple.builder(schema).add("value", AttributeType.STRING, "medium length").build(), Tuple.builder(schema).add("value", AttributeType.STRING, "b" * 200).build() ) // Test with visualization flag true val resultsVisualization = ExecutionResultService.convertTuplesToJson(tuples, true) // All strings should remain intact resultsVisualization(0).get("value").asText() shouldBe "short" resultsVisualization(1).get("value").asText() shouldBe "a" * 150 resultsVisualization(2).get("value").asText() shouldBe "medium length" resultsVisualization(3).get("value").asText() shouldBe "b" * 200 // Test with visualization flag false (default) val resultsDefault = ExecutionResultService.convertTuplesToJson(tuples) // Short strings unchanged, long strings truncated resultsDefault(0).get("value").asText() shouldBe "short" resultsDefault(1).get("value").asText() should endWith("...") resultsDefault(2).get("value").asText() shouldBe "medium length" resultsDefault(3).get("value").asText() should endWith("...") } } ================================================ FILE: amber/src/test/scala/org/apache/texera/workflow/WorkflowCompilerSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.workflow import org.apache.texera.amber.core.workflow.{PortIdentity, WorkflowContext} import org.apache.texera.amber.operator.TestOperators import org.apache.texera.amber.operator.source.scan.csv.CSVScanSourceOpDesc import org.apache.texera.web.model.websocket.request.LogicalPlanPojo import org.scalatest.flatspec.AnyFlatSpec /** * Direct unit coverage for [[WorkflowCompiler]]. Today the compiler is only * exercised transitively by e2e/scheduler specs through * [[org.apache.texera.amber.engine.e2e.TestUtils.buildWorkflow]]. The cases * below pin its contract — physical-plan shape, storage-port collection, and * strict-mode error behavior — so future refactors (notably the planned * merge with workflow-compiling-service's compiler) have a direct anchor. * * Not yet covered: the Python codegen `#EXCEPTION DURING CODE GENERATION:` * regex branch. Triggering it requires a `PythonOperatorDescriptor` subclass * whose `generatePythonCode()` throws; left for a follow-up so this initial * spec stays focused on plumbing the compiler boundary itself. */ class WorkflowCompilerSpec extends AnyFlatSpec { private def pojo( operators: List[org.apache.texera.amber.operator.LogicalOp], links: List[LogicalLink], opsToViewResult: List[String] = List.empty ): LogicalPlanPojo = LogicalPlanPojo(operators, links, opsToViewResult, List.empty) // -------------------- physical-plan shape -------------------- "WorkflowCompiler" should "produce a physical plan that contains at least one physical op per logical op" in { val csv = TestOperators.smallCsvScanOpDesc() val keyword = TestOperators.keywordSearchOpDesc("Region", "Asia") val ctx = new WorkflowContext() val workflow = new WorkflowCompiler(ctx).compile( pojo( List(csv, keyword), List( LogicalLink( csv.operatorIdentifier, PortIdentity(), keyword.operatorIdentifier, PortIdentity() ) ) ) ) assert(workflow.logicalPlan.operators.size == 2) assert(workflow.physicalPlan.getPhysicalOpsOfLogicalOp(csv.operatorIdentifier).nonEmpty) assert(workflow.physicalPlan.getPhysicalOpsOfLogicalOp(keyword.operatorIdentifier).nonEmpty) } it should "translate a logical link into a physical link between the two logical ops' physical ops" in { val csv = TestOperators.smallCsvScanOpDesc() val keyword = TestOperators.keywordSearchOpDesc("Region", "Asia") val ctx = new WorkflowContext() val workflow = new WorkflowCompiler(ctx).compile( pojo( List(csv, keyword), List( LogicalLink( csv.operatorIdentifier, PortIdentity(), keyword.operatorIdentifier, PortIdentity() ) ) ) ) val csvPhysIds = workflow.physicalPlan.getPhysicalOpsOfLogicalOp(csv.operatorIdentifier).map(_.id).toSet val keywordPhysIds = workflow.physicalPlan.getPhysicalOpsOfLogicalOp(keyword.operatorIdentifier).map(_.id).toSet val bridging = workflow.physicalPlan.links.filter(l => csvPhysIds.contains(l.fromOpId) && keywordPhysIds.contains(l.toOpId) ) assert(bridging.nonEmpty, "expected at least one physical link from csv to keyword") } // -------------------- storage-port collection -------------------- // The compiler walks `logicalPlan.getTerminalOperatorIds` (logical ops with // out-degree 0) plus `opsToViewResult`, and for every physical op of those // logical ops collects every non-internal output port into // `outputPortsNeedingStorage`, which it then writes back onto the // workflow context. These tests pin that the *mutation* lands on the // context (not just a side value), and that both the terminal-default and // the opsToViewResult-additive paths populate it. "WorkflowCompiler" should "mark the terminal op's output port as needing storage on the context" in { val csv = TestOperators.smallCsvScanOpDesc() val keyword = TestOperators.keywordSearchOpDesc("Region", "Asia") val ctx = new WorkflowContext() new WorkflowCompiler(ctx).compile( pojo( List(csv, keyword), List( LogicalLink( csv.operatorIdentifier, PortIdentity(), keyword.operatorIdentifier, PortIdentity() ) ) ) ) val storage = ctx.workflowSettings.outputPortsNeedingStorage assert( storage.exists(_.opId.logicalOpId == keyword.operatorIdentifier), s"expected keyword to be marked for storage, got ${storage.map(_.opId.logicalOpId)}" ) assert( !storage.exists(_.opId.logicalOpId == csv.operatorIdentifier), "csv is not terminal and was not requested via opsToViewResult; it should not be in storage" ) } it should "also mark a non-terminal op for storage when it is named in opsToViewResult" in { val csv = TestOperators.smallCsvScanOpDesc() val keyword = TestOperators.keywordSearchOpDesc("Region", "Asia") val ctx = new WorkflowContext() new WorkflowCompiler(ctx).compile( pojo( List(csv, keyword), List( LogicalLink( csv.operatorIdentifier, PortIdentity(), keyword.operatorIdentifier, PortIdentity() ) ), opsToViewResult = List(csv.operatorIdentifier.id) ) ) val storage = ctx.workflowSettings.outputPortsNeedingStorage val logicalOpsInStorage = storage.map(_.opId.logicalOpId) assert( logicalOpsInStorage.contains(csv.operatorIdentifier), s"opsToViewResult should add csv to storage, got $logicalOpsInStorage" ) assert( logicalOpsInStorage.contains(keyword.operatorIdentifier), s"terminal keyword should remain in storage, got $logicalOpsInStorage" ) } it should "treat a single source op as terminal and mark its output port for storage" in { val csv = TestOperators.smallCsvScanOpDesc() val ctx = new WorkflowContext() new WorkflowCompiler(ctx).compile(pojo(List(csv), List.empty)) val storage = ctx.workflowSettings.outputPortsNeedingStorage assert( storage.exists(_.opId.logicalOpId == csv.operatorIdentifier), "single op has out-degree 0, so its output port should land in storage" ) assert( storage.forall(!_.portId.internal), "compiler must filter out internal ports; storage should expose only user-visible outputs" ) } // -------------------- strict-mode error semantics -------------------- // Re-anchor the subject after the sub-section above. "WorkflowCompiler in strict mode (no errorList)" should "throw when a scan source has no fileName set" in { // CSVScanSourceOpDesc defaults fileName to None; `resolveScanSourceOpFileName(None)` // hits `scanOp.fileName.getOrElse(throw new RuntimeException("no input file name"))` // and surfaces that exception out of `compile` because the compiler passes // `None` for the errorList (i.e. fail-fast on the execution path). val orphanCsv = new CSVScanSourceOpDesc() val ctx = new WorkflowContext() val ex = intercept[RuntimeException] { new WorkflowCompiler(ctx).compile(pojo(List(orphanCsv), List.empty)) } assert(ex.getMessage == "no input file name") } } ================================================ FILE: amber/src/test/scala/org/apache/texera/workflow/common/storage/ReadonlyLocalFileDocumentSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.engine.common.storage import org.apache.texera.amber.core.storage.DocumentFactory import org.apache.texera.amber.core.storage.model.ReadonlyVirtualDocument import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import java.io.{ByteArrayOutputStream, File, InputStream} import java.nio.file.Files import scala.util.Using class ReadonlyLocalFileDocumentSpec extends AnyFlatSpec with Matchers with BeforeAndAfter { var tempFile: File = _ var fileDocument: ReadonlyVirtualDocument[_] = _ val initialContent = "Initial Content\nsome more content to make the text longer\nadf\t\ttestteset" before { // Create a temporary file with initial content tempFile = File.createTempFile("test", ".txt") Files.write(tempFile.toPath, initialContent.getBytes) fileDocument = DocumentFactory.openReadonlyDocument(tempFile.toURI) } after { // Delete the temporary file Files.deleteIfExists(tempFile.toPath) } private def readAllBytes(inputStream: InputStream): Array[Byte] = { val buffer = new ByteArrayOutputStream() val data = new Array[Byte](1024) var nRead = 0 while ({ nRead = inputStream.read(data, 0, data.length) nRead != -1 }) { buffer.write(data, 0, nRead) } buffer.flush() buffer.toByteArray } "ReadonlyLocalFileDocument" should "correctly report its URI" in { fileDocument.getURI should be(tempFile.toURI) } it should "allow reading from the file" in { val content = Using(fileDocument.asInputStream()) { inStream => new String(readAllBytes(inStream)) }.getOrElse(fail("Failed to read from the file")) content should equal(initialContent) } it should "return the file itself when asFile is called" in { fileDocument.asFile() should be(tempFile) } it should "throw NotImplementedError for unsupported getItem method" in { intercept[NotImplementedError] { fileDocument.getItem(0) }.getMessage should include("getItem is not supported") } it should "throw NotImplementedError for unsupported get method" in { intercept[NotImplementedError] { fileDocument.get() }.getMessage should include("get is not supported") } it should "throw NotImplementedError for unsupported getRange method" in { intercept[NotImplementedError] { fileDocument.getRange(0, 1) }.getMessage should include("getRange is not supported") } it should "throw NotImplementedError for unsupported getAfter method" in { intercept[NotImplementedError] { fileDocument.getAfter(0) }.getMessage should include("getAfter is not supported") } it should "throw NotImplementedError for unsupported getCount method" in { intercept[NotImplementedError] { fileDocument.getCount }.getMessage should include("getCount is not supported") } } ================================================ FILE: bin/.htaccess ================================================ RewriteEngine on # Ensure the Authorization HTTP header is available to PHP RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] # Uncomment the following lines if you are not using a `public` directory # to prevent sensitive resources from being exposed. # RewriteRule /\.git / [F,L] # RewriteRule ^auth\.json$ / [F,L] # RewriteRule ^composer\.(lock|json)$ / [F,L] # RewriteRule ^config.php$ / [F,L] # RewriteRule ^flarum$ / [F,L] # RewriteRule ^storage/(.*)?$ / [F,L] # RewriteRule ^vendor/(.*)?$ / [F,L] # Pass requests that don't refer directly to files in the filesystem to index.php RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^ index.php [QSA,L] # Disable directory listings Options -Indexes # MultiViews can mess up our rewriting scheme Options -MultiViews # The following directives are based on best practices from H5BP Apache Server Configs # https://github.com/h5bp/server-configs-apache # Expire rules for static content ExpiresActive on ExpiresDefault "access plus 1 month" ExpiresByType text/css "access plus 1 year" ExpiresByType application/atom+xml "access plus 1 hour" ExpiresByType application/rdf+xml "access plus 1 hour" ExpiresByType application/rss+xml "access plus 1 hour" ExpiresByType application/json "access plus 0 seconds" ExpiresByType application/ld+json "access plus 0 seconds" ExpiresByType application/schema+json "access plus 0 seconds" ExpiresByType application/vnd.geo+json "access plus 0 seconds" ExpiresByType application/vnd.api+json "access plus 0 seconds" ExpiresByType application/xml "access plus 0 seconds" ExpiresByType text/calendar "access plus 0 seconds" ExpiresByType text/xml "access plus 0 seconds" ExpiresByType image/vnd.microsoft.icon "access plus 1 week" ExpiresByType image/x-icon "access plus 1 week" ExpiresByType text/html "access plus 0 seconds" ExpiresByType application/javascript "access plus 1 year" ExpiresByType application/x-javascript "access plus 1 year" ExpiresByType text/javascript "access plus 1 year" ExpiresByType application/manifest+json "access plus 1 week" ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds" ExpiresByType text/cache-manifest "access plus 0 seconds" ExpiresByType text/markdown "access plus 0 seconds" ExpiresByType audio/ogg "access plus 1 month" ExpiresByType image/bmp "access plus 1 month" ExpiresByType image/gif "access plus 1 month" ExpiresByType image/jpeg "access plus 1 month" ExpiresByType image/png "access plus 1 month" ExpiresByType image/svg+xml "access plus 1 month" ExpiresByType image/webp "access plus 1 month" ExpiresByType video/mp4 "access plus 1 month" ExpiresByType video/ogg "access plus 1 month" ExpiresByType video/webm "access plus 1 month" ExpiresByType application/wasm "access plus 1 year" ExpiresByType font/collection "access plus 1 month" ExpiresByType application/vnd.ms-fontobject "access plus 1 month" ExpiresByType font/eot "access plus 1 month" ExpiresByType font/opentype "access plus 1 month" ExpiresByType font/otf "access plus 1 month" ExpiresByType application/x-font-ttf "access plus 1 month" ExpiresByType font/ttf "access plus 1 month" ExpiresByType application/font-woff "access plus 1 month" ExpiresByType application/x-font-woff "access plus 1 month" ExpiresByType font/woff "access plus 1 month" ExpiresByType application/font-woff2 "access plus 1 month" ExpiresByType font/woff2 "access plus 1 month" ExpiresByType text/x-cross-domain-policy "access plus 1 week" # Gzip compression AddOutputFilterByType DEFLATE "application/atom+xml" \ "application/javascript" \ "application/json" \ "application/ld+json" \ "application/manifest+json" \ "application/rdf+xml" \ "application/rss+xml" \ "application/schema+json" \ "application/vnd.geo+json" \ "application/vnd.ms-fontobject" \ "application/wasm" \ "application/x-font-ttf" \ "application/x-javascript" \ "application/x-web-app-manifest+json" \ "application/xhtml+xml" \ "application/xml" \ "font/collection" \ "font/eot" \ "font/opentype" \ "font/otf" \ "font/ttf" \ "image/bmp" \ "image/svg+xml" \ "image/vnd.microsoft.icon" \ "image/x-icon" \ "text/cache-manifest" \ "text/calendar" \ "text/css" \ "text/html" \ "text/javascript" \ "text/plain" \ "text/markdown" \ "text/vcard" \ "text/vnd.rim.location.xloc" \ "text/vtt" \ "text/x-component" \ "text/x-cross-domain-policy" \ "text/xml" # Fix for https://httpoxy.org vulnerability RequestHeader unset Proxy Header always set Access-Control-Allow-Origin "http://localhost:4200" Header always set Access-Control-Allow-Methods "POST, GET, OPTIONS, DELETE, PUT" Header always set Access-Control-Max-Age "1000" Header always set Access-Control-Allow-Headers "x-requested-with, Content-Type, origin, authorization, accept, client-security-token" Header always set Access-Control-Allow-Credentials "true" # Added a rewrite to respond with a 200 SUCCESS on every OPTIONS request. RewriteEngine On RewriteCond %{REQUEST_METHOD} OPTIONS RewriteRule ^(.*)$ $1 [R=200,L] ================================================ FILE: bin/README.md ================================================ # Texera Deployment This directory contains Dockerfiles and configuration files for building and deploying Texera's microservices. ## Dockerfiles This directory includes several Dockerfiles, such as `file-service.dockerfile` and `computing-unit-master.dockerfile`. Each Dockerfile builds a specific Texera microservice. All Dockerfiles must be built from the `texera` project root as the Docker build context. For example, to build the image using `texera-web-application.dockerfile`, run the following command **from the project root**: ```bash docker build -f bin/texera-web-application.dockerfile -t your-repo/texera-web-application:test . ``` Two shell scripts, `build-images.sh` and `build-services.sh` are included for building platform-dependent images conveniently. You can also find prebuilt images published by the Texera team on the [Texera DockerHub Repository](https://hub.docker.com/repositories/texera). ## Deployment using images Subdirectories `single-node` and `k8s` contain configuration files for deploying Texera using the above Docker images. ================================================ FILE: bin/access-control-service.dockerfile ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Apache Texera is an effort undergoing incubation at The Apache Software # Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is # required of all newly accepted projects until a further review indicates # that the infrastructure, communications, and decision-making process have # stabilized in a manner consistent with other successful ASF projects. # While incubation status is not necessarily a reflection of the # completeness or stability of the code, it does indicate that the project # has yet to be fully endorsed by the ASF. FROM sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.3_2.13.11 AS build # Set working directory WORKDIR /texera # Copy modules for building the service COPY common/ common/ COPY access-control-service/ access-control-service/ COPY project/ project/ COPY build.sbt build.sbt COPY .jvmopts .jvmopts # Update system and install dependencies RUN apt-get update && apt-get install -y \ netcat \ unzip \ libpq-dev \ && apt-get clean # Add .git for runtime calls to jgit from OPversion COPY .git .git COPY LICENSE NOTICE DISCLAIMER ./ COPY licenses/ licenses/ RUN sbt clean AccessControlService/dist # Unzip the texera binary RUN unzip access-control-service/target/universal/access-control-service-*.zip -d target/ FROM eclipse-temurin:17-jre-jammy AS runtime WORKDIR /texera COPY --from=build /texera/.git /texera/.git # Copy the built texera binary from the build phase COPY --from=build /texera/target/access-control-service* /texera/ # Copy resources directories from build phase COPY --from=build /texera/access-control-service/src/main/resources /texera/access-control-service/src/main/resources # Copy ASF licensing files. LICENSE-binary and NOTICE-binary describe the # bundled third-party contents of this image and ship as /texera/LICENSE # and /texera/NOTICE; licenses/ holds the per-license full texts referenced # by LICENSE-binary. COPY --from=build /texera/access-control-service/LICENSE-binary /texera/LICENSE COPY --from=build /texera/access-control-service/NOTICE-binary /texera/NOTICE COPY --from=build /texera/licenses /texera/licenses COPY --from=build /texera/DISCLAIMER /texera/ RUN groupadd --system --gid 1001 texera \ && useradd --system --uid 1001 --gid texera --home-dir /texera --no-create-home texera \ && chown -R texera:texera /texera USER texera CMD ["bin/access-control-service"] EXPOSE 9096 ================================================ FILE: bin/add-computing-unit-worker.sh ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. ( cd amber || { echo "Error: amber directory not found"; exit 1; } if [ -n "$1" ]; then echo "Starting worker with server address: $1" target/texera-0.1-SNAPSHOT/bin/computing-unit-worker --serverAddr "$1" else echo "Starting worker without explicit server address" target/texera-0.1-SNAPSHOT/bin/computing-unit-worker fi ) ================================================ FILE: bin/agent-service.dockerfile ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Apache Texera is an effort undergoing incubation at The Apache Software # Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is # required of all newly accepted projects until a further review indicates # that the infrastructure, communications, and decision-making process have # stabilized in a manner consistent with other successful ASF projects. # While incubation status is not necessarily a reflection of the # completeness or stability of the code, it does indicate that the project # has yet to be fully endorsed by the ASF. FROM oven/bun:1-alpine WORKDIR /app COPY agent-service/package.json agent-service/bun.lock ./ RUN bun install --frozen-lockfile --production COPY agent-service/src ./src COPY agent-service/tsconfig.json ./ COPY agent-service/LICENSE-binary ./LICENSE COPY NOTICE ./NOTICE COPY DISCLAIMER ./DISCLAIMER COPY licenses ./licenses RUN addgroup -S -g 1001 texera \ && adduser -S -u 1001 -G texera -h /app texera \ && chown -R texera:texera /app USER texera EXPOSE 3001 CMD ["bun", "run", "src/server.ts"] ================================================ FILE: bin/bootstrap-lakekeeper.sh ================================================ #!/usr/bin/env bash # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Prerequisites: # - Already know how to setup Texera # - macOS or Linux (Lakekeeper does not publish Windows binaries) # - A running PostgreSQL instance (the same one Texera uses is fine) # - An accessible S3 bucket # - The AWS CLI (awscli) installed set -e # ============================================================================== # User Configuration - Edit the values below before running this script # ============================================================================== # # IMPORTANT: If you change values in storage.conf, you MUST also update the # matching defaults here (or export the corresponding STORAGE_* environment # variables before running this script) # # ============================================================================== # Storage settings — must stay in sync with storage.conf # if needed, update the default values after `:-` to match storage.conf STORAGE_ICEBERG_CATALOG_REST_URI="${STORAGE_ICEBERG_CATALOG_REST_URI:-http://localhost:8181/catalog}" STORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME="${STORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME:-texera}" STORAGE_ICEBERG_CATALOG_REST_S3_BUCKET="${STORAGE_ICEBERG_CATALOG_REST_S3_BUCKET:-texera-iceberg}" STORAGE_S3_REGION="${STORAGE_S3_REGION:-us-west-2}" STORAGE_S3_ENDPOINT="${STORAGE_S3_ENDPOINT:-http://localhost:9000}" STORAGE_S3_AUTH_USERNAME="${STORAGE_S3_AUTH_USERNAME:-texera_minio}" STORAGE_S3_AUTH_PASSWORD="${STORAGE_S3_AUTH_PASSWORD:-password}" # Lakekeeper binary — defaults to `lakekeeper` on $PATH (e.g. after # `brew install lakekeeper`). Override by exporting LAKEKEEPER_BINARY_PATH # or by editing the default below. LAKEKEEPER_BINARY_PATH="${LAKEKEEPER_BINARY_PATH:-lakekeeper}" # Lakekeeper PostgreSQL connection URLs # LAKEKEEPER__PG_DATABASE_URL_READ="postgres://:@:5432/texera_lakekeeper" # LAKEKEEPER__PG_DATABASE_URL_WRITE="postgres://:@:5432/texera_lakekeeper" LAKEKEEPER__PG_DATABASE_URL_READ="" LAKEKEEPER__PG_DATABASE_URL_WRITE="" # Lakekeeper encryption key LAKEKEEPER__PG_ENCRYPTION_KEY="texera_key" # Lakekeeper metrics port LAKEKEEPER__METRICS_PORT="9091" # ============================================================================== # End of User Configuration # ============================================================================== # Derive bootstrap-internal values from the storage settings above. LAKEKEEPER_BASE_URI="${STORAGE_ICEBERG_CATALOG_REST_URI%/}" LAKEKEEPER_BASE_URI="${LAKEKEEPER_BASE_URI%/catalog}" WAREHOUSE_NAME="$STORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME" S3_REGION="$STORAGE_S3_REGION" S3_BUCKET="$STORAGE_ICEBERG_CATALOG_REST_S3_BUCKET" S3_ENDPOINT="$STORAGE_S3_ENDPOINT" S3_USERNAME="$STORAGE_S3_AUTH_USERNAME" S3_PASSWORD="$STORAGE_S3_AUTH_PASSWORD" STORAGE_PATH="s3://${S3_BUCKET}/iceberg/${WAREHOUSE_NAME}" echo "==========================================" echo "Lakekeeper Bootstrap and Warehouse Setup" echo "==========================================" echo "Lakekeeper Base URI: $LAKEKEEPER_BASE_URI" echo "Lakekeeper Binary: $LAKEKEEPER_BINARY_PATH" echo "Warehouse Name: $WAREHOUSE_NAME" echo "S3 Endpoint: $S3_ENDPOINT" echo "S3 Region: $S3_REGION" echo "S3 Bucket: $S3_BUCKET" echo "Storage Path: $STORAGE_PATH" echo "" # Function to check if Lakekeeper is running check_lakekeeper_running() { local health_url="${LAKEKEEPER_BASE_URI}/health" if curl -s -f "$health_url" > /dev/null 2>&1; then return 0 # Running else return 1 # Not running fi } # Function to bootstrap the Lakekeeper server (creates default project). # This is idempotent - safe to call even if already bootstrapped. # Returns: 0=success (or already bootstrapped), 1=failure bootstrap_lakekeeper_server() { local base_uri="$1" local bootstrap_url="${base_uri}/management/v1/bootstrap" echo "Bootstrapping Lakekeeper server (creating default project)..." echo " URL: $bootstrap_url" local temp_response temp_response=$(mktemp) || { echo "✗ Failed to create temporary file" return 1 } local http_code http_code=$(curl -s -o "$temp_response" -w "%{http_code}" \ -X POST \ -H "Content-Type: application/json" \ -d '{"accept-terms-of-use": true}' \ "$bootstrap_url" 2>/dev/null || echo "000") echo " HTTP status: $http_code" case "$http_code" in 000) echo "✗ Failed to connect to Lakekeeper at $bootstrap_url" rm -f "$temp_response" || true return 1 ;; 2*) echo "✓ Lakekeeper server bootstrapped successfully (HTTP $http_code)" rm -f "$temp_response" || true return 0 ;; *) if grep -q "CatalogAlreadyBootstrapped" "$temp_response" 2>/dev/null; then echo "✓ Lakekeeper server already bootstrapped (HTTP $http_code), continuing." rm -f "$temp_response" || true return 0 fi echo "✗ Failed to bootstrap Lakekeeper server (HTTP $http_code)" echo " Response body:" cat "$temp_response" | sed 's/^/ /' || true rm -f "$temp_response" || true return 1 ;; esac } # Function to check if S3 bucket exists (requires AWS CLI) check_S3_bucket() { local bucket_name="$1" local endpoint="$2" local username="$3" local password="$4" local region="$5" if ! command -v aws >/dev/null 2>&1; then echo "✗ Error: AWS CLI is required for S3 bucket operations." echo " Install it with: pip install awscli" return 1 fi if AWS_ACCESS_KEY_ID="$username" AWS_SECRET_ACCESS_KEY="$password" AWS_DEFAULT_REGION="$region" \ aws --endpoint-url="$endpoint" s3 ls "s3://${bucket_name}/" >/dev/null 2>&1; then return 0 # Bucket exists else return 1 # Bucket doesn't exist or error fi } # Function to create S3 bucket (requires AWS CLI) create_S3_bucket() { local bucket_name="$1" local endpoint="$2" local username="$3" local password="$4" local region="$5" if ! command -v aws >/dev/null 2>&1; then echo "✗ Error: AWS CLI is required for S3 bucket operations." echo " Install it with: pip install awscli" return 1 fi if AWS_ACCESS_KEY_ID="$username" AWS_SECRET_ACCESS_KEY="$password" AWS_DEFAULT_REGION="$region" \ aws --endpoint-url="$endpoint" s3 mb "s3://${bucket_name}" >/dev/null 2>&1; then return 0 # Success else return 1 # Failed fi } # Function to start Lakekeeper start_lakekeeper() { export LAKEKEEPER__METRICS_PORT export LAKEKEEPER__PG_ENCRYPTION_KEY echo "Starting Lakekeeper..." # Validate LAKEKEEPER_BINARY_PATH — `command -v` resolves both bare names # via $PATH lookup and absolute paths. if ! command -v "$LAKEKEEPER_BINARY_PATH" >/dev/null 2>&1; then echo "✗ Error: Lakekeeper binary '$LAKEKEEPER_BINARY_PATH' not found." echo " Install it via 'brew install lakekeeper' (macOS) or set" echo " LAKEKEEPER_BINARY_PATH to an absolute path / edit the default in this script." exit 1 fi local binary_path="$LAKEKEEPER_BINARY_PATH" # Validate required database URLs if [ -z "$LAKEKEEPER__PG_DATABASE_URL_READ" ] || [ -z "$LAKEKEEPER__PG_DATABASE_URL_WRITE" ]; then echo "✗ Error: Database URLs not configured." echo " Please set LAKEKEEPER__PG_DATABASE_URL_READ and LAKEKEEPER__PG_DATABASE_URL_WRITE" echo " by exporting them as env vars or editing the User Configuration section in this script." exit 1 fi export LAKEKEEPER__PG_DATABASE_URL_READ export LAKEKEEPER__PG_DATABASE_URL_WRITE # Run migration first echo "Running Lakekeeper migration..." if ! "$binary_path" migrate; then echo "✗ Failed to run Lakekeeper migration" return 1 fi # Start Lakekeeper in background echo "Starting Lakekeeper server..." nohup "$binary_path" serve > /tmp/lakekeeper.log 2>&1 & local lakekeeper_pid=$! echo "Lakekeeper started with PID: $lakekeeper_pid" # Wait for Lakekeeper to be ready echo "Waiting for Lakekeeper to be ready..." local max_attempts=30 local attempt=1 while [ $attempt -le $max_attempts ]; do if check_lakekeeper_running; then echo "✓ Lakekeeper is ready!" return 0 fi if [ $attempt -eq $max_attempts ]; then echo "✗ Lakekeeper did not become ready after $max_attempts attempts" echo " Check logs at /tmp/lakekeeper.log" return 1 fi echo " Waiting for Lakekeeper... ($attempt/$max_attempts)" sleep 2 attempt=$((attempt + 1)) done } # Function to check if warehouse exists # Returns: 0=exists, 1=not found, 2=connection error check_warehouse_exists() { local warehouse_name="$1" local base_uri="$2" local list_url="${base_uri}/management/v1/warehouse" echo "Checking if warehouse '$warehouse_name' exists..." local temp_response temp_response=$(mktemp) || { echo "✗ Failed to create temporary file" return 2 } local http_code http_code=$(curl -s -o "$temp_response" -w "%{http_code}" "$list_url" 2>/dev/null || echo "000") if [ "$http_code" = "000" ]; then rm -f "$temp_response" || true echo "✗ Failed to connect to Lakekeeper at $list_url" return 2 fi if [ "$http_code" != "200" ]; then echo "⚠ Warning: Unexpected HTTP status $http_code when listing warehouses" cat "$temp_response" 2>/dev/null | sed 's/^/ /' || true rm -f "$temp_response" || true return 1 fi # Check if warehouse name exists in the response # Response format: {"warehouses":[{"name":"...",...},...]} local found=1 if command -v jq >/dev/null 2>&1; then if jq -e ".warehouses[] | select(.name == \"$warehouse_name\")" "$temp_response" >/dev/null 2>&1; then found=0 fi else if grep -q "\"name\"[[:space:]]*:[[:space:]]*\"$warehouse_name\"" "$temp_response" 2>/dev/null; then found=0 fi fi rm -f "$temp_response" 2>/dev/null || true return $found } # Function to create warehouse # Args: warehouse_name base_uri s3_bucket s3_region s3_endpoint s3_username s3_password # Returns: 0=success, 1=failure create_warehouse() { local warehouse_name="$1" local base_uri="$2" local bucket="$3" local region="$4" local endpoint="$5" local username="$6" local password="$7" local create_url="${base_uri}/management/v1/warehouse" local create_payload=$(cat </dev/null | sed 's/^/ /' || true rm -f "$temp_response" || true return 1 ;; esac } # Step 1: Check if Lakekeeper is running, start if not echo "Step 1: Checking Lakekeeper status..." if check_lakekeeper_running; then echo "✓ Lakekeeper is already running" else echo "Lakekeeper is not running, attempting to start..." if start_lakekeeper; then echo "✓ Lakekeeper started successfully" else echo "✗ Failed to start Lakekeeper" exit 1 fi fi echo "" # Step 2: Bootstrap the Lakekeeper server (creates default project) echo "Step 2: Bootstrapping Lakekeeper server..." if bootstrap_lakekeeper_server "$LAKEKEEPER_BASE_URI"; then echo "✓ Lakekeeper server bootstrap completed" else echo "✗ Failed to bootstrap Lakekeeper server" echo " Please check that Lakekeeper is running and accessible at $LAKEKEEPER_BASE_URI" exit 1 fi echo "" # Step 3: Check and create S3 bucket echo "Step 3: Checking S3 bucket..." if check_S3_bucket "$S3_BUCKET" "$S3_ENDPOINT" "$S3_USERNAME" "$S3_PASSWORD" "$S3_REGION"; then echo "✓ S3 bucket '$S3_BUCKET' already exists" else echo "S3 bucket '$S3_BUCKET' does not exist, creating..." if create_S3_bucket "$S3_BUCKET" "$S3_ENDPOINT" "$S3_USERNAME" "$S3_PASSWORD" "$S3_REGION"; then echo "✓ S3 bucket '$S3_BUCKET' created successfully" else echo "✗ Failed to create S3 bucket '$S3_BUCKET'" echo " Please ensure S3 is running and accessible at $S3_ENDPOINT" exit 1 fi fi echo "" # Step 4: Check and create warehouse echo "Step 4: Checking and creating warehouse..." set +e # Temporarily disable exit on error to capture function return value check_warehouse_exists "$WAREHOUSE_NAME" "$LAKEKEEPER_BASE_URI" check_result=$? set -e # Re-enable exit on error case $check_result in 0) echo "✓ Warehouse '$WAREHOUSE_NAME' already exists, skipping creation." echo "" echo "==========================================" echo "✓ Bootstrap completed successfully!" echo "==========================================" exit 0 ;; 1) echo "Warehouse '$WAREHOUSE_NAME' does not exist, will create..." ;; 2) exit 1 ;; *) echo "✗ Unexpected error (code: $check_result)" exit 1 ;; esac # Create warehouse if create_warehouse "$WAREHOUSE_NAME" "$LAKEKEEPER_BASE_URI" "$S3_BUCKET" "$S3_REGION" "$S3_ENDPOINT" "$S3_USERNAME" "$S3_PASSWORD"; then echo "" echo "==========================================" echo "✓ Bootstrap completed successfully!" echo "==========================================" exit 0 else echo "" echo "==========================================" echo "✗ Bootstrap failed!" echo "==========================================" exit 1 fi ================================================ FILE: bin/build-images.sh ================================================ #!/bin/bash # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. set -e # Default values DEFAULT_TAG="latest" DEFAULT_SERVICES="*" # Parse command-line arguments while [[ $# -gt 0 ]]; do case $1 in --tag|-t) BASE_TAG="$2" shift 2 ;; --services|-s) SERVICES_INPUT="$2" shift 2 ;; --help|-h) echo "Usage: $0 [OPTIONS]" echo "" echo "Options:" echo " -t, --tag TAG Base tag for the images (default: latest)" echo " -s, --services SERVICES Services to build, comma-separated or '*' for all (default: *)" echo " -h, --help Show this help message" echo "" echo "Examples:" echo " $0 --tag v1.0.0 --services '*'" echo " $0 -t latest -s 'gui,computing-unit-master'" echo " $0 # Interactive mode" exit 0 ;; *) echo "❌ Unknown option: $1" echo "Run '$0 --help' for usage information." exit 1 ;; esac done # If BASE_TAG not provided via command-line, prompt interactively if [[ -z "$BASE_TAG" ]]; then read -p "Enter the base tag for the images [${DEFAULT_TAG}]: " BASE_TAG BASE_TAG=${BASE_TAG:-$DEFAULT_TAG} fi # If SERVICES_INPUT not provided via command-line, prompt interactively if [[ -z "$SERVICES_INPUT" ]]; then read -p "Enter services to build (comma-separated, '*' for all) [${DEFAULT_SERVICES}]: " SERVICES_INPUT SERVICES_INPUT=${SERVICES_INPUT:-$DEFAULT_SERVICES} fi # Convert the user input into an array for easy lookup IFS=',' read -ra SELECTED_SERVICES <<< "$SERVICES_INPUT" # Helper to determine whether a given service should be built should_build() { local svc="$1" # Build everything if the user specified '*' if [[ "$SERVICES_INPUT" == "*" ]]; then return 0 fi for sel in "${SELECTED_SERVICES[@]}"; do # Trim possible whitespace around each token sel="$(echo -e "${sel}" | tr -d '[:space:]')" if [[ "$svc" == "$sel" ]]; then return 0 fi done return 1 } # Detect platform ARCH=$(uname -m) if [[ "$ARCH" == "x86_64" ]]; then PLATFORM="linux/amd64" TAG_SUFFIX="amd64" elif [[ "$ARCH" == "arm64" || "$ARCH" == "aarch64" ]]; then PLATFORM="linux/arm64" TAG_SUFFIX="arm64" else echo "❌ Unsupported architecture: $ARCH" exit 1 fi FULL_TAG="${BASE_TAG}-${TAG_SUFFIX}" echo "🔍 Detected architecture: $ARCH -> Building for $PLATFORM with tag :$FULL_TAG" # Ensure Buildx is ready docker buildx create --name texera-builder --use --bootstrap > /dev/null 2>&1 || docker buildx use texera-builder cd "$(dirname "$0")" # Auto-detect Dockerfiles in current directory dockerfiles=( *.dockerfile ) if [[ ${#dockerfiles[@]} -eq 0 ]]; then echo "❌ No Dockerfiles found (*.dockerfile) in the current directory." exit 1 fi echo "🔨 Building and pushing Texera images for $PLATFORM..." for dockerfile in "${dockerfiles[@]}"; do service_name=$(basename "$dockerfile" .dockerfile) # Skip services the user did not ask for if ! should_build "$service_name"; then echo "⏭️ Skipping $service_name" continue fi image="texera/$service_name:$FULL_TAG" echo "👉 Building $image from $dockerfile" docker buildx build \ --platform "$PLATFORM" \ -f "$dockerfile" \ -t "$image" \ --push \ .. done # Build pylsp service (directory: pylsp) if should_build "pylsp"; then image="texera/pylsp:$FULL_TAG" echo "👉 Building $image from pylsp/Dockerfile" docker buildx build \ --platform "$PLATFORM" \ -f "pylsp/Dockerfile" \ -t "$image" \ --push \ ./pylsp fi # Build y-websocket-server service (directory: y-websocket-server, image: y-websocket-server) if should_build "y-websocket-server"; then image="texera/y-websocket-server:$FULL_TAG" echo "👉 Building $image from y-websocket-server/Dockerfile" docker buildx build \ --platform "$PLATFORM" \ -f "y-websocket-server/Dockerfile" \ -t "$image" \ --push \ ./y-websocket-server fi echo "✅ All images built and pushed with tag :$FULL_TAG" ================================================ FILE: bin/build-services.sh ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. sbt clean dist unzip workflow-compiling-service/target/universal/workflow-compiling-service-*.zip -d target/ rm workflow-compiling-service/target/universal/workflow-compiling-service-*.zip unzip file-service/target/universal/file-service-*.zip -d target/ rm file-service/target/universal/file-service-*.zip unzip config-service/target/universal/config-service-*.zip -d target/ rm config-service/target/universal/config-service-*.zip unzip computing-unit-managing-service/target/universal/computing-unit-managing-service-*.zip -d target/ rm computing-unit-managing-service/target/universal/computing-unit-managing-service-*.zip unzip amber/target/universal/texera-*.zip -d amber/target/ rm amber/target/universal/texera-*.zip ================================================ FILE: bin/build.sh ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. bin/build-services.sh bin/frontend.sh ================================================ FILE: bin/computing-unit-managing-service.sh ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. target/computing-unit-managing-service-*/bin/computing-unit-managing-service ================================================ FILE: bin/computing-unit-master.dockerfile ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Apache Texera is an effort undergoing incubation at The Apache Software # Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is # required of all newly accepted projects until a further review indicates # that the infrastructure, communications, and decision-making process have # stabilized in a manner consistent with other successful ASF projects. # While incubation status is not necessarily a reflection of the # completeness or stability of the code, it does indicate that the project # has yet to be fully endorsed by the ASF. FROM sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.3_2.13.11 AS build # Set working directory WORKDIR /texera # Copy modules for building the service COPY common/ common/ COPY amber/ amber/ COPY project/ project/ COPY build.sbt build.sbt COPY .jvmopts .jvmopts # Update system and install dependencies. python3-minimal is needed by # bin/licensing/concat_license_binary.py below. RUN apt-get update && apt-get install -y \ netcat \ unzip \ libpq-dev \ python3-minimal \ && apt-get clean # Add .git for runtime calls to jgit from OPversion COPY .git .git COPY LICENSE NOTICE DISCLAIMER ./ COPY licenses/ licenses/ COPY bin/licensing/ bin/licensing/ RUN sbt clean WorkflowExecutionService/dist # Unzip the texera binary RUN unzip amber/target/universal/amber-*.zip -d amber/target/ # Merge per-aspect LICENSE-binary files (java jars + python packages) into # a single LICENSE-binary-combined keyed by license group, for the runtime # image. Per-license-group merge keeps Scala/Java jars and Python packages # inside the same Apache-2.0 / MIT / BSD / ... section instead of stacking # the inputs end-to-end. RUN python3 bin/licensing/concat_license_binary.py amber/LICENSE-binary-combined \ amber/LICENSE-binary-java \ amber/LICENSE-binary-python FROM eclipse-temurin:17-jdk-jammy AS runtime WORKDIR /texera/amber COPY --from=build /texera/amber/requirements.txt /tmp/requirements.txt COPY --from=build /texera/amber/operator-requirements.txt /tmp/operator-requirements.txt # Install Python runtime dependencies RUN apt-get update && apt-get install -y \ python3-pip \ python3-dev \ libpq-dev \ && apt-get clean # Install Python packages RUN pip3 install --upgrade pip setuptools wheel && \ pip3 install -r /tmp/requirements.txt && \ pip3 install -r /tmp/operator-requirements.txt # Copy the built texera binary from the build phase COPY --from=build /texera/.git /texera/amber/.git COPY --from=build /texera/amber/target/amber-* /texera/amber/ # Copy resources directories from build phase COPY --from=build /texera/common/config/src/main/resources /texera/amber/common/config/src/main/resources COPY --from=build /texera/amber/src/main/resources /texera/amber/src/main/resources # Copy code for python UDF COPY --from=build /texera/amber/src/main/python /texera/amber/src/main/python # Copy ASF licensing files. LICENSE-binary and NOTICE-binary describe the # bundled third-party contents of this image and ship as /texera/LICENSE # and /texera/NOTICE; licenses/ holds the per-license full texts referenced # by LICENSE-binary. COPY --from=build /texera/amber/LICENSE-binary-combined /texera/LICENSE COPY --from=build /texera/amber/NOTICE-binary /texera/NOTICE COPY --from=build /texera/licenses /texera/licenses COPY --from=build /texera/DISCLAIMER /texera/ RUN groupadd --system --gid 1001 texera \ && useradd --system --uid 1001 --gid texera --home-dir /texera --no-create-home texera \ && chown -R texera:texera /texera USER texera CMD ["bin/computing-unit-master"] EXPOSE 8085 ================================================ FILE: bin/computing-unit-worker.dockerfile ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Apache Texera is an effort undergoing incubation at The Apache Software # Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is # required of all newly accepted projects until a further review indicates # that the infrastructure, communications, and decision-making process have # stabilized in a manner consistent with other successful ASF projects. # While incubation status is not necessarily a reflection of the # completeness or stability of the code, it does indicate that the project # has yet to be fully endorsed by the ASF. FROM sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.3_2.13.11 AS build # Set working directory WORKDIR /texera # Copy modules for building the service COPY common/ common/ COPY amber/ amber/ COPY project/ project/ COPY build.sbt build.sbt COPY .jvmopts .jvmopts # Update system and install dependencies. python3-minimal is needed by # bin/licensing/concat_license_binary.py below. RUN apt-get update && apt-get install -y \ netcat \ unzip \ libpq-dev \ python3-minimal \ && apt-get clean # Add .git for runtime calls to jgit from OPversion COPY .git .git COPY LICENSE NOTICE DISCLAIMER ./ COPY licenses/ licenses/ COPY bin/licensing/ bin/licensing/ RUN sbt clean WorkflowExecutionService/dist # Unzip the texera binary RUN unzip amber/target/universal/amber-*.zip -d amber/target/ # Merge per-aspect LICENSE-binary files (java jars + python packages) into # a single LICENSE-binary-combined keyed by license group, for the runtime # image. Per-license-group merge keeps Scala/Java jars and Python packages # inside the same Apache-2.0 / MIT / BSD / ... section instead of stacking # the inputs end-to-end. RUN python3 bin/licensing/concat_license_binary.py amber/LICENSE-binary-combined \ amber/LICENSE-binary-java \ amber/LICENSE-binary-python FROM eclipse-temurin:17-jre-jammy AS runtime WORKDIR /texera/amber COPY --from=build /texera/amber/requirements.txt /tmp/requirements.txt COPY --from=build /texera/amber/operator-requirements.txt /tmp/operator-requirements.txt # Install Python runtime dependencies RUN apt-get update && apt-get install -y \ python3-pip \ python3-dev \ libpq-dev \ && apt-get clean # Install Python packages RUN pip3 install --upgrade pip setuptools wheel && \ pip3 install -r /tmp/requirements.txt && \ (pip3 install --no-cache-dir --find-links https://pypi.org/simple/ -r /tmp/operator-requirements.txt || \ pip3 install --no-cache-dir wordcloud==1.9.2) # Copy the built texera binary from the build phase COPY --from=build /texera/amber/target/amber-* /texera/amber/ # Copy resources directories from build phase COPY --from=build /texera/amber/src/main/resources /texera/amber/src/main/resources COPY --from=build /texera/common/config/src/main/resources /texera/amber/common/config/src/main/resources # Copy ASF licensing files. LICENSE-binary and NOTICE-binary describe the # bundled third-party contents of this image and ship as /texera/LICENSE # and /texera/NOTICE; licenses/ holds the per-license full texts referenced # by LICENSE-binary. COPY --from=build /texera/amber/LICENSE-binary-combined /texera/LICENSE COPY --from=build /texera/amber/NOTICE-binary /texera/NOTICE COPY --from=build /texera/licenses /texera/licenses COPY --from=build /texera/DISCLAIMER /texera/ RUN groupadd --system --gid 1001 texera \ && useradd --system --uid 1001 --gid texera --home-dir /texera --no-create-home texera \ && chown -R texera:texera /texera USER texera CMD ["bin/computing-unit-worker"] EXPOSE 8085 ================================================ FILE: bin/config-service.dockerfile ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Apache Texera is an effort undergoing incubation at The Apache Software # Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is # required of all newly accepted projects until a further review indicates # that the infrastructure, communications, and decision-making process have # stabilized in a manner consistent with other successful ASF projects. # While incubation status is not necessarily a reflection of the # completeness or stability of the code, it does indicate that the project # has yet to be fully endorsed by the ASF. FROM sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.3_2.13.11 AS build # Set working directory WORKDIR /texera # Copy modules for building the service COPY common/ common/ COPY config-service/ config-service/ COPY project/ project/ COPY build.sbt build.sbt COPY .jvmopts .jvmopts # Update system and install dependencies RUN apt-get update && apt-get install -y \ netcat \ unzip \ libpq-dev \ && apt-get clean # Add .git for runtime calls to jgit from OPversion COPY .git .git COPY LICENSE NOTICE DISCLAIMER ./ COPY licenses/ licenses/ RUN sbt clean ConfigService/dist # Unzip the texera binary RUN unzip config-service/target/universal/config-service-*.zip -d target/ FROM eclipse-temurin:17-jre-jammy AS runtime WORKDIR /texera COPY --from=build /texera/.git /texera/.git # Copy the built texera binary from the build phase COPY --from=build /texera/target/config-service-* /texera/ # Copy resources directories from build phase COPY --from=build /texera/common/config/src/main/resources /texera/common/config/src/main/resources COPY --from=build /texera/config-service/src/main/resources /texera/config-service/src/main/resources # Copy ASF licensing files. LICENSE-binary and NOTICE-binary describe the # bundled third-party contents of this image and ship as /texera/LICENSE # and /texera/NOTICE; licenses/ holds the per-license full texts referenced # by LICENSE-binary. COPY --from=build /texera/config-service/LICENSE-binary /texera/LICENSE COPY --from=build /texera/config-service/NOTICE-binary /texera/NOTICE COPY --from=build /texera/licenses /texera/licenses COPY --from=build /texera/DISCLAIMER /texera/ RUN groupadd --system --gid 1001 texera \ && useradd --system --uid 1001 --gid texera --home-dir /texera --no-create-home texera \ && chown -R texera:texera /texera USER texera CMD ["bin/config-service"] EXPOSE 9094 ================================================ FILE: bin/config-service.sh ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. target/config-service-*/bin/config-service ================================================ FILE: bin/config.php ================================================ false, 'database' => array ( 'driver' => 'mysql', 'host' => 'localhost', 'port' => 3306, 'database' => 'flarum', 'username' => 'REPLACE_WITH_YOUR_USERNAME', 'password' => 'REPLACE_WITH_YOUR_PASSWORD', 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_unicode_ci', 'prefix' => '', 'strict' => false, 'engine' => 'InnoDB', 'prefix_indexes' => true, ), 'url' => 'http://localhost:8888', 'paths' => array ( 'api' => 'api', 'admin' => 'admin', ), 'headers' => array ( 'poweredByHeader' => true, 'referrerPolicy' => 'same-origin', ), ); ================================================ FILE: bin/cron-restart-crashed-worker.sh ================================================ #!/bin/bash # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. if jps -m | grep -q "TexeraWebApplication"; then echo "TexeraWebApplication is running." # Check if TexeraRunWorker is missing if ! jps -m | grep -q "TexeraRunWorker"; then echo "TexeraRunWorker is missing. Restarting..." # Restart TexeraRunWorker bin/worker.sh >/dev/null echo "TexeraRunWorker restarted." else echo "TexeraRunWorker is already running." fi else echo "TexeraWebApplication is not running." fi ================================================ FILE: bin/deploy-daemon.sh ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. red=$(tput setaf 1) green=$(tput setaf 2) reset=$(tput sgr0) skipCompilation=false while getopts s flag do case "${flag}" in s) skipCompilation=true;; *) exit 1 esac done if ! $skipCompilation then echo "${green}Compiling Services...${reset}" bash bin/build-services.sh echo "${green}Services compiled.${reset}" echo "${green}Compiling GUI...${reset}" (cd frontend && yarn install && ng build --configuration production --deploy-url=/ --base-href=/) echo "${green}GUI compiled.${reset}" echo fi echo "${green}Starting TexeraWebApplication in daemon...${reset}" setsid nohup bin/server.sh >/dev/null 2>&1 & echo "${green}Waiting TexeraWebApplication to launch on 8080...${reset}" while ! nc -z localhost 8080; do sleep 0.1 # wait 100ms before check again done echo "${green}TexeraWebApplication launched at $(pgrep -f TexeraWebApplication)${reset}" echo echo "${green}Starting WorkflowCompilingService in daemon...${reset}" setsid nohup bin/workflow-compiling-service.sh >/dev/null 2>&1 & echo "${green}Waiting TexeraWorkflowCompilingService to launch on 9090...${reset}" while ! nc -z localhost 9090; do sleep 0.1 # wait 100ms before check again done echo "${green}WorkflowCompilingService launched at $(pgrep -f TexeraWorkflowCompilingService)${reset}" echo echo "${green}Starting FileService in daemon...${reset}" setsid nohup bin/file-service.sh >/dev/null 2>&1 & echo "${green}Waiting FileService to launch on 9092...${reset}" while ! nc -z localhost 9092; do sleep 0.1 # wait 100ms before check again done echo "${green}FileService launched at $(pgrep -f FileService)${reset}" echo echo "${green}Starting ConfigService in daemon...${reset}" setsid nohup bin/config-service.sh >/dev/null 2>&1 & echo "${green}Waiting ConfigService to launch on 9094...${reset}" while ! nc -z localhost 9094; do sleep 0.1 # wait 100ms before check again done echo "${green}ConfigService launched at $(pgrep -f ConfigService)${reset}" echo echo "${green}Starting ComputingUnitManagingService in daemon...${reset}" setsid nohup bin/computing-unit-managing-service.sh >/dev/null 2>&1 & echo "${green}Waiting ComputingUnitManagingService to launch on 8888...${reset}" while ! nc -z localhost 8888; do sleep 0.1 # wait 100ms before check again done echo "${green}ComputingUnitManagingService launched at $(pgrep -f ComputingUnitManagingService)${reset}" echo echo "${green}Starting WorkflowComputingUnit in daemon...${reset}" setsid nohup bin/workflow-computing-unit.sh >/dev/null 2>&1 & echo "${green}Waiting WorkflowComputingUnit to launch on 8085...${reset}" while ! nc -z localhost 8085; do sleep 0.1 # wait 100ms before check again done echo "${green}WorkflowComputingUnit launched at $(pgrep -f WorkflowComputingUnit)${reset}" echo echo "${green}Starting shared editing server...${reset}" setsid nohup bin/shared-editing-server.sh >/dev/null 2>&1 & sleep 2 echo "${green}Shared Editing Server launched at $(pgrep -f y-websocket)${reset}" ================================================ FILE: bin/deploy-docker.sh ================================================ #!/usr/bin/env bash # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Start server.sh in the background bash bin/server.sh & # Wait for server.sh to start by sleeping for a brief period (adjust as needed) sleep 5 # Check if server.sh is still running; if not, exit with an error if ! ps -p $! > /dev/null; then >&2 echo 'server.sh failed to start.' exit 1 fi # Start workflow-compiling-service.sh in the background bash bin/workflow-compiling-service.sh & # Wait for workflow-compiling-service.sh to start by sleeping for a brief period (adjust as needed) sleep 5 # Check if workflow-compiling-service.sh is still running; if not, exit with an error if ! ps -p $! > /dev/null; then >&2 echo 'workflow-compiling-service.sh failed to start.' exit 1 fi # Start computing unit master node in the background bash bin/workflow-computing-unit.sh & # Wait for one of server.sh and computing unit master node to complete wait -n ================================================ FILE: bin/file-service.dockerfile ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Apache Texera is an effort undergoing incubation at The Apache Software # Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is # required of all newly accepted projects until a further review indicates # that the infrastructure, communications, and decision-making process have # stabilized in a manner consistent with other successful ASF projects. # While incubation status is not necessarily a reflection of the # completeness or stability of the code, it does indicate that the project # has yet to be fully endorsed by the ASF. FROM sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.3_2.13.11 AS build # Set working directory WORKDIR /texera # Copy modules for building the service COPY common/ common/ COPY file-service/ file-service/ COPY project/ project/ COPY build.sbt build.sbt COPY .jvmopts .jvmopts # Update system and install dependencies RUN apt-get update && apt-get install -y \ netcat \ unzip \ libpq-dev \ && apt-get clean # Add .git for runtime calls to jgit from OPversion COPY .git .git COPY LICENSE NOTICE DISCLAIMER ./ COPY licenses/ licenses/ RUN sbt clean FileService/dist # Unzip the texera binary RUN unzip file-service/target/universal/file-service-*.zip -d target/ FROM eclipse-temurin:17-jre-jammy AS runtime WORKDIR /texera # Copy the built texera binary from the build phase COPY --from=build /texera/target/file-service-* /texera/ # Copy resources directories from build phase COPY --from=build /texera/common/config/src/main/resources /texera/common/config/src/main/resources COPY --from=build /texera/file-service/src/main/resources /texera/file-service/src/main/resources # Copy ASF licensing files. LICENSE-binary and NOTICE-binary describe the # bundled third-party contents of this image and ship as /texera/LICENSE # and /texera/NOTICE; licenses/ holds the per-license full texts referenced # by LICENSE-binary. COPY --from=build /texera/file-service/LICENSE-binary /texera/LICENSE COPY --from=build /texera/file-service/NOTICE-binary /texera/NOTICE COPY --from=build /texera/licenses /texera/licenses COPY --from=build /texera/DISCLAIMER /texera/ RUN groupadd --system --gid 1001 texera \ && useradd --system --uid 1001 --gid texera --home-dir /texera --no-create-home texera \ && chown -R texera:texera /texera USER texera CMD ["bin/file-service"] EXPOSE 9092 ================================================ FILE: bin/file-service.sh ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. target/file-service-*/bin/file-service ================================================ FILE: bin/fix-format.sh ================================================ #!/usr/bin/env bash # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # # ------------------------------------------------------------- # fix-format.sh # ------------------------------------------------------------- # Runs code formatters for the Texera repository. # # Usage: # bin/fix-format.sh # Format all (Scala, Frontend, Python) # bin/fix-format.sh --scala # Format Scala only # bin/fix-format.sh --frontend # Format Frontend only # bin/fix-format.sh --python # Format Python only # ------------------------------------------------------------- set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/utils/texera-logging.sh" TEXERA_HOME="$(/usr/bin/env bash "$SCRIPT_DIR/utils/resolve-texera-home.sh")" if [[ -z "${TEXERA_HOME:-}" ]]; then exit 1 fi # --- Key directories --- FRONTEND_DIR="$TEXERA_HOME/frontend" AMBER_DIR="$TEXERA_HOME/amber" [[ -d "$FRONTEND_DIR" ]] || { tx_error "Frontend directory not found: $FRONTEND_DIR"; exit 1; } [[ -d "$AMBER_DIR/src/main/python" ]] || { tx_error "Amber Python directory not found: $AMBER_DIR/src/main/python"; exit 1; } [[ -d "$AMBER_DIR/src/test/python" ]] || { tx_error "Amber Python test directory not found: $AMBER_DIR/src/test/python"; exit 1; } # --- Argument parsing --- TARGET="${1:-all}" run_scala=false run_frontend=false run_python=false case "$TARGET" in --scala) run_scala=true ;; --frontend) run_frontend=true ;; --python) run_python=true ;; ""|--all|all) run_scala=true; run_frontend=true; run_python=true ;; *) tx_error "Unknown option: $TARGET" echo "Usage: bin/fix-format.sh [--scala | --frontend | --python | --all]" exit 1 ;; esac # --- 1) Scala formatting --- if $run_scala; then tx_info "Running sbt scalafmtAll and scalafixAll at repo root..." if ! command -v sbt >/dev/null 2>&1; then tx_error "sbt not found. Please install sbt." exit 1 fi ( cd "$TEXERA_HOME" sbt scalafmtAll scalafixAll ) tx_success "Scala formatting completed." fi # --- 2) Frontend formatting --- if $run_frontend; then tx_info "Running yarn format:fix in frontend..." if ! command -v yarn >/dev/null 2>&1; then tx_error "yarn not found. Please install Yarn." exit 1 fi ( cd "$FRONTEND_DIR" yarn format:fix ) tx_success "Frontend formatting completed." fi # --- 3) Python formatting --- if $run_python; then tx_info "Running ruff in amber/src/{main,test}/python..." if ! command -v ruff >/dev/null 2>&1; then tx_error "ruff not found. Install with: pip install ruff" exit 1 fi ( cd "$AMBER_DIR" ruff format src/main/python src/test/python ) tx_success "Python formatting completed." fi tx_success "✅ Formatting tasks completed successfully!" ================================================ FILE: bin/forum/flarum.sql ================================================ -- Licensed to the Apache Software Foundation (ASF) under one -- or more contributor license agreements. See the NOTICE file -- distributed with this work for additional information -- regarding copyright ownership. The ASF licenses this file -- to you under the Apache License, Version 2.0 (the -- "License"); you may not use this file except in compliance -- with the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, -- software distributed under the License is distributed on an -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -- KIND, either express or implied. See the License for the -- specific language governing permissions and limitations -- under the License. -- MySQL dump 10.13 Distrib 8.2.0, for macos13 (arm64) -- -- Host: 127.0.0.1 Database: flarum -- ------------------------------------------------------ -- Server version 8.0.35-0ubuntu0.20.04.1 CREATE SCHEMA IF NOT EXISTS `flarum`; USE `flarum`; /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!50503 SET NAMES utf8mb4 */; /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; /*!40103 SET TIME_ZONE='+00:00' */; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; -- -- Table structure for table `access_tokens` -- DROP TABLE IF EXISTS `access_tokens`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `access_tokens` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `token` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `user_id` int unsigned NOT NULL, `last_activity_at` datetime DEFAULT NULL, `created_at` datetime NOT NULL, `type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `title` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, `last_ip_address` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, `last_user_agent` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `access_tokens_token_unique` (`token`), KEY `access_tokens_user_id_foreign` (`user_id`), KEY `access_tokens_type_index` (`type`), CONSTRAINT `access_tokens_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB AUTO_INCREMENT=226 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `access_tokens` -- LOCK TABLES `access_tokens` WRITE; /*!40000 ALTER TABLE `access_tokens` DISABLE KEYS */; INSERT INTO `access_tokens` VALUES (1,'IgO67IH27IxOvvtgXvET4KHleDvf5TjEmGxSp14r',1,'2023-09-30 20:58:55','2023-09-30 20:58:55','session_remember',NULL,NULL,NULL),(3,'4r7sjWlKaN9uEH45Fh2AyDXtrMjficVfUUqJ8dhe',1,'2023-09-30 21:27:35','2023-09-30 21:27:35','session_remember',NULL,'::1','PostmanRuntime/7.32.3'),(4,'YCA69PvpLrUj8EKaRewSRPxzl16TjBPWOoO43EyG',1,'2023-09-30 21:27:57','2023-09-30 21:27:57','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(5,'j9MM3PDNjw7obly3NIbkSVLTfAPm2HNOm30vh2Z2',1,'2023-09-30 23:03:56','2023-09-30 21:28:45','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(6,'vwGHbn3F26dWqUWYLCuwFJLmZMkNqZwaPI17a3pc',1,'2023-09-30 23:29:56','2023-09-30 23:03:56','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(7,'9ShdhU1u5xH7ItBOq7BOJ4UtA7ghx0wITR2Hc3JE',1,'2023-10-01 00:44:13','2023-09-30 23:29:56','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(8,'gzmjICYTvaGsoZdOQmR7Mekv10sBfzaaKGxEXTSD',1,'2023-10-01 00:44:13','2023-10-01 00:44:13','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(9,'E3huoFZhmSPgCNGH7tPDGPn6rbkZzxqV7mNL1DSq',1,'2023-10-01 00:44:13','2023-10-01 00:44:13','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(10,'lkZvI52vOXxZN8rIuZ2uHywMb9oo9KwXG0LtQhCh',1,'2023-10-01 00:44:14','2023-10-01 00:44:14','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(11,'H8fqc4CY0ay3b8b38CqG78ZLS05yVfgJ066dP9nV',1,'2023-10-01 00:44:14','2023-10-01 00:44:14','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(12,'nYYsFOLrfdaU1I5oP2mX5FFtY1oWplWSNAqYLcDJ',1,'2023-10-01 00:44:15','2023-10-01 00:44:15','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(13,'YIJiG26abCF4toOrK4dALytUy946dlrK3SjuXnC1',1,'2023-10-01 00:44:15','2023-10-01 00:44:15','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(14,'1BaIy5fXJVkcNtLTe5NPIY9B5G6Y0YAfnzgKA03O',1,'2023-10-01 00:44:16','2023-10-01 00:44:16','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(15,'OgrLQ6e4b148yhC2zSU678XcEPyRuyhq4nQ31on1',1,'2023-10-01 00:44:16','2023-10-01 00:44:16','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(16,'oRfwAzYBA6IgJN3LaxDcrOLVdblwY3rWWEbvO7M2',1,'2023-10-01 00:44:17','2023-10-01 00:44:17','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(17,'BzBSUAcVNrUVHrSyaOcC5TuSkYel1GVXUbgYON8g',1,'2023-10-01 00:44:17','2023-10-01 00:44:17','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(18,'xLmdhFcJXwn1eLQuYgs6NanUqgd8hMt6IA8EfXhd',1,'2023-10-01 00:44:18','2023-10-01 00:44:18','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(19,'DB0HhQHL9vm5xQUgiyMIIutM8gWIFtW7PX232Ork',1,'2023-10-01 00:44:18','2023-10-01 00:44:18','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(20,'29hPtmZhMxUlt1zMUTw5wg1xneeIRTFTmTIoQiD3',1,'2023-10-01 00:44:19','2023-10-01 00:44:19','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(21,'1jWDLbLeKKRuZDK1r433fVMmagvzXn9j6VCvjXnP',1,'2023-10-01 00:44:20','2023-10-01 00:44:20','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(22,'OxiTBc4Ck6uh4IZJPH4GBIokrQWcuuLgejoWvuFE',1,'2023-10-01 00:44:20','2023-10-01 00:44:20','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(23,'LieEyFxc6PH30b1MoMuSpYNyIwVut0BLzYSzhygE',1,'2023-10-01 00:44:21','2023-10-01 00:44:21','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(24,'qus5Ym9cwSDncfp8cZBTWOjW4AREEAy3Wb5QKv5k',1,'2023-10-01 00:44:21','2023-10-01 00:44:21','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(25,'sHNjKeAEbDVgYo9lmsHeKEjwlRHttTKgicU6Lo1f',1,'2023-10-01 00:44:22','2023-10-01 00:44:22','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(26,'TJRfYZBuvncK801xKESLQvTincVP14m1JonyIlGv',1,'2023-10-01 00:44:22','2023-10-01 00:44:22','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(27,'rsyvyiAIfG1cZlgyq77zodYTI5AnEvwtX0PXwYwq',1,'2023-10-01 00:44:23','2023-10-01 00:44:23','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(28,'Hyh7OlutN77CgQU4GuvAjsA0UdQNVkOMKgRIxsBO',1,'2023-10-01 00:44:23','2023-10-01 00:44:23','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(29,'AFBhGU2LeT7NYEsQg643Z5TSrczGKhK7bf87rkYb',1,'2023-10-01 00:44:24','2023-10-01 00:44:24','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(30,'SySAlr8N7PRDzzSkukPsqtgaU8zStlLwd1zVZ1cq',1,'2023-10-01 00:44:25','2023-10-01 00:44:25','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(31,'8bjzic0VipL7OzAslWLP7AiNhLaWMKah4tcjhTyd',1,'2023-10-01 00:44:25','2023-10-01 00:44:25','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(32,'MsOzL6beALyKMuDMfktqjYt7eIPnWc07YAZ8lutL',1,'2023-10-01 00:44:26','2023-10-01 00:44:25','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(33,'dxFGxkQ93eRISqF6neomZBGbYEA4I7RZTwFmDGxj',1,'2023-10-01 00:44:26','2023-10-01 00:44:26','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(34,'32G9jSWyWDu2wZUJ7IVJGRqkRt2KPzePcIPdR1eC',1,'2023-10-01 00:44:27','2023-10-01 00:44:27','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(35,'jtKQAPIuJABYmQc8V4HyDwooJ45cTpKkbcRM2FeI',1,'2023-10-01 00:44:28','2023-10-01 00:44:28','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(36,'LinH514826Y6rRKpBpw9MxmNCprKc11dU6iWzlxL',1,'2023-10-01 00:44:28','2023-10-01 00:44:28','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(37,'EXpsMeOSFiHdKiAVKSBBNZfsgWujqAGEFL4SC9yE',1,'2023-10-01 00:44:28','2023-10-01 00:44:28','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(38,'foDJPQuCTDMUVJKA1CCxQ5DbBM7MdV4bMtYrY6fE',1,'2023-10-01 00:44:29','2023-10-01 00:44:29','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(39,'53QcAj9VyGVzefU5YrSjRfPZg66CyDyi658cCODs',1,'2023-10-01 00:44:29','2023-10-01 00:44:29','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(40,'wxMyaEJT4c5tZlCUjUINA7Fj8HZZDEuqc0zgmuhb',1,'2023-10-01 00:44:30','2023-10-01 00:44:30','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(41,'Dhp14tvqGfUZ8642K1AUZsd03OjobUukB7XA9D66',1,'2023-10-01 00:44:30','2023-10-01 00:44:30','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(42,'J0NRTc0WQwYulnNpZRNtkartTsZMsQ91TNEpjEMA',1,'2023-10-01 00:44:31','2023-10-01 00:44:31','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(43,'ZmKiMQ3S0tdqpsloTYHcmBVGD2cBWxX68XuuuBwc',1,'2023-10-01 00:44:31','2023-10-01 00:44:31','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(44,'IpysPXbtY4jiiZlffgAiizqNwaVAO2Q4g2rVaRuA',1,'2023-10-01 00:44:31','2023-10-01 00:44:31','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(45,'RFcyXd9JtPBoSM5ry7bYq6aexc3NSu8V6seawOdA',1,'2023-10-01 00:44:32','2023-10-01 00:44:32','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(46,'Uoksd1RI9T1prtxzGIMHQO0HUwkZDKGiggMPY0v7',1,'2023-10-01 00:44:32','2023-10-01 00:44:32','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(47,'016FV2hSL7sGDvZBmnJw69GyDHoPFKOrF7gDphpf',1,'2023-10-01 00:44:33','2023-10-01 00:44:33','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(48,'afmCT0NIvkYIOhr2npUf8TcbHyNAJ9RLuz3zIdUZ',1,'2023-10-01 00:44:33','2023-10-01 00:44:33','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(49,'fLByiumpiWJVqC3QWLQWnkRuqWaoSqvERcLSOcJ8',1,'2023-10-01 00:44:34','2023-10-01 00:44:34','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(50,'mO8VjPVY5xCOkmkcZdc1k48NSL0AzAnmyheJEtEO',1,'2023-10-01 00:44:34','2023-10-01 00:44:34','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(51,'ffBvflYKh73WMo49OQwAGYzxsgigITrOKmwEQKyF',1,'2023-10-01 00:44:35','2023-10-01 00:44:35','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(52,'pv3jAhJsSU5rjTHHCiwMhrtYT2falLFlfqsB3XGw',1,'2023-10-01 00:44:35','2023-10-01 00:44:35','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(53,'3Epu6gQJAx5ydcXzZVKCgQG2xknvIBQjDXqPFEAC',1,'2023-10-01 00:44:35','2023-10-01 00:44:35','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(54,'maUFZsZWXTaQiTrjUjH9Gp5P9Wi29VfcJzMRnobx',1,'2023-10-01 00:44:36','2023-10-01 00:44:36','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(55,'WJmokOWUDxOD4wQC2GV44bkEedAQCNksP4dzbexg',1,'2023-10-01 00:44:36','2023-10-01 00:44:36','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(56,'pg4hYQqFeWD5mI3JTdXgFZrjdghI4yEeW33vOCe4',1,'2023-10-01 00:44:37','2023-10-01 00:44:37','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(57,'Zft1yLHiWZGefevCpf6FLet4UGCRk3xuJjX6fG78',1,'2023-10-01 00:44:37','2023-10-01 00:44:37','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(58,'gUnqkAIZC5xmYeJI8KNciCvJAm0JKipL5RdbkyhC',1,'2023-10-01 01:02:13','2023-10-01 00:44:38','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(59,'Jc0dBfRrOwQgonzvt0tpFJWSqW6wtGwB4VFu8ITF',1,'2023-10-01 01:02:27','2023-10-01 01:02:27','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(60,'s9fhJkIzplT4hSnYAFXY5nzNMIQwxbrqnEC4zgGj',1,'2023-10-01 01:02:28','2023-10-01 01:02:28','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(61,'tE5aR2DQDtP1aciYJH9Nfdphc7kqXKNgtPB1vm0c',1,'2023-10-01 01:02:29','2023-10-01 01:02:29','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(62,'34xlKg3er8QndJWI81IpALCf9C440dOZPQXWg48p',1,'2023-10-01 01:02:30','2023-10-01 01:02:30','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(63,'3r7GJiU74VmSdWn8e6e2DGH0XV02UP3gYAaUX2Km',1,'2023-10-01 01:02:31','2023-10-01 01:02:31','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(64,'Z9JnkFmJYgRFHhzkVG9jeZCRg0ETBKJNquyeyjpu',1,'2023-10-01 01:02:31','2023-10-01 01:02:31','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(65,'DCnphvtwmk9keyZ2qexX2aTsGYbhZCt1moE4MqlP',1,'2023-10-01 01:02:32','2023-10-01 01:02:32','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(66,'tXW9CBIVpmFmULnPyb4LdHwDxZBGc7ecNyz84Ccd',1,'2023-10-01 01:02:34','2023-10-01 01:02:34','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(67,'V7yKABcDqJRHnoQvkkOb8UMmAZL1TkOUIKqtReaf',1,'2023-10-01 01:02:35','2023-10-01 01:02:35','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(68,'c9lcF6rwbHcF5P7vFsaJ73qiTnYBhmzC9Bk6MPqX',1,'2023-10-01 01:02:36','2023-10-01 01:02:36','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(69,'iMFvlimAQlBZj4NAMXpTXznH5IIMRL0Emojs0Zt6',1,'2023-10-01 01:02:37','2023-10-01 01:02:37','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(70,'jCu8XYEBsU1H4QxsJ86OhfGSS5i68ew3tsdIY9Pc',1,'2023-10-01 01:02:38','2023-10-01 01:02:38','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(71,'RyU3COqEw1NvIaOm8Enm4ntcwj8r9R5KfILnXw6b',1,'2023-10-01 01:02:39','2023-10-01 01:02:39','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(72,'QVQPvSDzFmugaAphlVQkD8gUMlqSn9iEZ49g3oRx',1,'2023-10-01 01:02:40','2023-10-01 01:02:40','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(73,'AEZl1kKRhj6CoYlcyes4XL8CPoyQPgnHhT7SMoX2',1,'2023-10-01 01:02:40','2023-10-01 01:02:40','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(74,'ePSXh06pIuzvF4Wfy3EiMXTMsQ3XqYLybgfrrBIu',1,'2023-10-01 01:02:41','2023-10-01 01:02:41','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(75,'hV3Es4ICkENHehYTcnJUsmErbcYl6aOwvLlGth5B',1,'2023-10-01 01:02:41','2023-10-01 01:02:41','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(76,'061ZDp9jqGxA8o1UezrQnCHmhCdf1lapD0musZxn',1,'2023-10-01 01:02:42','2023-10-01 01:02:42','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(77,'1X07TU38uA9dZGqaoYDlzMCtdgW0C6JCWLa8BkGH',1,'2023-10-01 01:02:42','2023-10-01 01:02:42','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(78,'AMh3T9wwMWSO02L0e5VUbxEx0Y9zQixPdysveI1N',1,'2023-10-01 01:21:55','2023-10-01 01:02:43','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(79,'wQyhzIY1Y6UHx8Kd6vNn9Sf3qJ0OPeCubRvV1w91',1,'2023-10-01 01:02:43','2023-10-01 01:02:43','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(80,'z4saYkKefOw8ZlCyJFt4332FKnh7VnJc9MZWqce8',1,'2023-10-01 01:27:06','2023-10-01 01:22:36','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(81,'xfze5VhgREg1nmJ88smJAHqDs5c6fo9MR7gF4nWf',1,'2023-10-01 01:27:07','2023-10-01 01:27:07','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(82,'1a55Kxeukh3ZW6ymY0PoIr8hIXeLRwVpfH9aegvs',1,'2023-10-01 01:28:27','2023-10-01 01:28:27','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(83,'uWZEXV9JxJBfA2f98LviIV4IjwREi9YGQMORKcxn',1,'2023-10-01 01:28:29','2023-10-01 01:28:29','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(84,'7k6qweq0ZDY94ob2rio01LV6wGb9XxhDDIRW6xbJ',1,'2023-10-01 03:08:13','2023-10-01 01:29:37','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(85,'BWl7NM9nWf9EYrMwJ8uRo0XazCGS9VDdm7oNVUDl',1,'2023-10-01 01:32:49','2023-10-01 01:29:40','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(86,'ipvdQxvIl0j6sGNAHlKpDz0yeWT1yX4JGH6PJWgl',1,'2023-10-01 01:32:49','2023-10-01 01:32:49','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(87,'rl9BZLzdJCSmafu54NZYVDFFJIm7o2hVRvfbhGzx',1,'2023-10-01 01:35:42','2023-10-01 01:33:13','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(88,'2yUVXwxuxYu6l2bWrPpRVFHDg3RnoFcxPTZ6BGzo',1,'2023-10-01 01:35:42','2023-10-01 01:35:42','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(89,'gNxms9UpnIhFsP9EpDIka1k7dcwzPoORYqIRTEcN',1,'2023-10-01 01:35:48','2023-10-01 01:35:48','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(90,'C9lYmW2u6xDXnRqpUtMaHYl9D7JOxCUHMna3neku',1,'2023-10-01 01:43:12','2023-10-01 01:35:55','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(91,'0Abg1coz0ufqhkTm1ym0LO91EpotOAVGuDiIxqcw',1,'2023-10-01 01:56:58','2023-10-01 01:43:13','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(92,'eDnCMzO6TcUbgkspxNb3Eys85PBn3DKuELbibmJQ',1,'2023-10-01 01:56:59','2023-10-01 01:56:59','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(93,'alWcvK2YRMJPAiJ822ByHBHhANhVCE27klaoPnTh',1,'2023-10-01 01:58:18','2023-10-01 01:58:18','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(94,'zYyuCEtEVvTz6p4rAyafPYCtKnfHoILLl5cdt2lV',1,'2023-10-01 01:58:35','2023-10-01 01:58:35','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(95,'EoJYFmb250rzQ6TO40oJfRgyNMWkrwJ8XjPh3N9C',1,'2023-10-01 02:19:08','2023-10-01 01:59:16','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(96,'ZifdxIvsFtbPgoRFjXrhEdsP946eRsr36leC2Idy',1,'2023-10-01 02:19:09','2023-10-01 02:19:09','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(97,'jBANb7STSBPTd9wYftf0X6qt7MsT4o9UlIXEghDW',1,'2023-10-01 02:19:55','2023-10-01 02:19:55','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(98,'4FdytdXEphzlpNLilF4VsEZZ0uJVcwSEPxupZ28Y',1,'2023-10-01 02:20:03','2023-10-01 02:20:03','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(99,'CJVPFkOdrTuJhsxXZKFe3bcWaHlsG8zTBsKBit2a',1,'2023-10-01 02:20:05','2023-10-01 02:20:05','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(100,'KUxj8FDdzprviUJMyBA6em91UghBMqMWRekdIxsW',1,'2023-10-01 02:21:15','2023-10-01 02:21:15','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(101,'zSCne2idbnqQ4VPxwDKeExc9lBE8PGNC7Bk2mDIc',1,'2023-10-01 02:22:41','2023-10-01 02:22:41','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(102,'6sZFf8jz3yCtrGDaGpGgrpZiUt1ZTA1eQLxgJZLt',1,'2023-10-01 02:26:48','2023-10-01 02:22:59','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(103,'w70Nujehy8GakleXKrsxpjveRaOcq9UKcCDVNYpi',1,'2023-10-01 02:26:49','2023-10-01 02:26:49','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(104,'hgjxj2DP7DFYT3pH34n4hnN3ukwc9jQA1Mtjk0sl',1,'2023-10-01 02:26:59','2023-10-01 02:26:59','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(105,'FjTjcPFrfs7GGSeuzLLjb8d9EvjpLa8DL5yMUiy9',1,'2023-10-01 02:36:34','2023-10-01 02:27:05','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(106,'dKsZqWDQXHpKmXfsRhwdsNy2RPgnRek9aYKAP0Mj',1,'2023-10-01 02:36:34','2023-10-01 02:36:34','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(107,'p5trEskoyIgyCjI9xvCfEcjSySYJoS7EGrMYvd92',1,'2023-10-01 02:37:06','2023-10-01 02:37:06','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(108,'KOo50zZdRTTsjjqZRmxVcXlFOYppASuyRTI78xw0',1,'2023-10-01 02:38:55','2023-10-01 02:37:09','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(109,'2l1rIOfa5SKBatw9Za3ZEDBRFrtH2BefxhpPOvUz',1,'2023-10-01 02:38:55','2023-10-01 02:38:55','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(110,'IaV5FtSWsRev5En4Gv8AZFZYHIracF6pBtB1oTq2',1,'2023-10-01 02:50:28','2023-10-01 02:39:23','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(111,'kmxutQ5BESKUnfaSQNlPP46CDaqj39ghXg7ETieF',1,'2023-10-01 02:54:05','2023-10-01 02:50:29','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(112,'GBDerq4EvXK17U5SYwGspa7MTmME2Du4MQsCZIKx',1,'2023-10-01 02:54:05','2023-10-01 02:54:05','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(113,'rcrfbigKCc7FDskUeVgFCsNPZp1O64NQ2tXmFHex',1,'2023-10-01 03:01:39','2023-10-01 02:54:56','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(114,'W7Nk3pBaPjYVz2t2ER8UwWPSrEd2jkIkZct7pDz0',1,'2023-10-01 03:03:40','2023-10-01 03:01:40','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(115,'i9x4UM2XVMOjqPn3HuviNejrZZs9AxOwUetQh2eU',1,'2023-10-01 03:03:40','2023-10-01 03:03:40','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(116,'F0iWVexCNeqeftsB0N1QoVj0qwmtk4XrrTeFa4Vf',1,'2023-10-01 03:05:09','2023-10-01 03:05:09','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(117,'FRBBbJp3Y4lkVqJ7VWMCBNkHMZbId6Jba9hZVsAT',1,'2023-10-01 03:05:24','2023-10-01 03:05:24','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(118,'lXi7wb78mQpY5NdfkfGg1X2TDr8lomhEYYh94IDT',1,'2023-10-01 03:05:38','2023-10-01 03:05:38','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(119,'KWS0txbq19K2UnsZUnsS09qQM1gkKpBnn1cfgZ8V',1,'2023-10-01 03:06:36','2023-10-01 03:06:36','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(121,'GRfW5oM2vXBgpwCGB9aJGlhqyETnP1PRVKeg0skW',1,'2023-10-01 03:08:00','2023-10-01 03:08:00','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(122,'oFSjhY8mQ3GiJEH51ZEff5SOKK6cB9XKcwx3XZGH',1,'2023-10-01 03:10:11','2023-10-01 03:08:13','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(123,'DyOAoy9PiYFzLGce2by6qH9aXdTQeiwknrAWJuWm',1,'2023-10-01 03:10:12','2023-10-01 03:10:12','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(124,'x6uSsx2U5mWSXyfyzU3giw0rZ6SuB9gnkp0eQYzm',1,'2023-10-01 03:10:30','2023-10-01 03:10:30','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(126,'kspNiZoQi64WuRoAigCRvqf47K96Lz7U4Sqxo2sW',1,'2023-10-01 03:11:54','2023-10-01 03:11:54','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(129,'IIDMwybbG9YfINNEzOtkh3Wc11hsd8mdAZ9HarhO',1,'2023-10-01 03:15:53','2023-10-01 03:15:53','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(130,'zAKaaCRa5zLh5NqZeZvBircfhQi7PI9gfFS6jFid',1,'2023-10-01 03:16:11','2023-10-01 03:16:11','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(131,'O9empiSfXPHZFpoDMTaHu5vm5XygEG0tcDzNEq7W',1,'2023-10-05 00:51:51','2023-10-01 03:17:08','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(134,'giR9QOpSNGKggOmaqP3XvD4J4KznsrsL2KGvBXXQ',1,'2023-10-01 04:16:20','2023-10-01 04:16:20','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(135,'B0HsZMHB507t6mB7Eirwq84pNwfZF47eO7Eek6VO',1,'2023-10-01 04:16:39','2023-10-01 04:16:39','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(138,'YKOsCsu82c6QxOuecoeeZDr2SRbS6PRaObaYPLYy',1,'2023-10-01 04:36:43','2023-10-01 04:27:05','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(139,'3fK4W78EVVmMhdmmxkB8sBKjhc8fBRdQhp8ktots',1,'2023-10-01 04:36:44','2023-10-01 04:36:44','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(140,'kVWtXK1ErfVxibup4JyMtabAEQhgoLdu4CXGt5OO',1,'2023-10-01 04:47:53','2023-10-01 04:37:18','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(141,'DWMSux7cZpG5F9w6rQ8mF73r3TdVJn07WFPJJBPa',1,'2023-10-01 04:47:54','2023-10-01 04:47:54','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(142,'5C8IYGXyFbmQ90SQx74bpPfGoetaH6Da8dvLTUMj',1,'2023-10-01 04:48:29','2023-10-01 04:48:29','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(143,'99HaCeAl6La0rDUyUtkLwGoPzqFn0zE7EpnU3bEq',1,'2023-10-01 04:51:11','2023-10-01 04:48:34','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(144,'YNZc1cWQ5CTwDEMW88JHEiqu7lKWtQ8jH94lbXgi',1,'2023-10-01 04:55:09','2023-10-01 04:51:11','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(145,'F5UAZKk88rybNg13pwEQrflUM9IaKgldnnuKG1Eu',1,'2023-10-01 04:55:10','2023-10-01 04:55:10','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(171,'o7wRQkQ01dR8MFpihSG2cwoU06qBp0KehjEVDM89',1,'2023-10-05 00:58:20','2023-10-05 00:51:51','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(174,'O1RIoQBn2pE0EmiflTqzGOPbsnU4RWfRFyty2XIu',1,'2023-10-05 00:58:21','2023-10-05 00:58:21','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(175,'Fg8vVJ2VLXQkhfOMO3LId0dkBEbcR3xwV13FDP01',1,'2023-10-05 01:00:08','2023-10-05 00:58:21','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(176,'NSUb0PmkncZK4JlRaWeGiUv6ob17qycrOK8mk4Kt',1,'2023-10-05 00:58:24','2023-10-05 00:58:24','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(177,'1v62jCinxMxKpTHduoTGxr47aPwG78z7gUpzPLsM',1,'2023-10-05 00:58:24','2023-10-05 00:58:24','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(178,'pD3hLbgzzdWpoUk755B7FDJUiFtGDpEC5otDY5xe',1,'2023-10-08 20:57:14','2023-10-05 01:00:09','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(189,'1DpliGXt67OrjnhQzqhM9E4JkjqGZQfDMbFTQ38z',1,'2023-10-08 19:33:34','2023-10-08 19:33:34','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(190,'hk3rRuZ5LaLDdPpgqTpVUoEqzcoNl4Q04R05Dvzl',1,'2023-10-08 19:34:07','2023-10-08 19:34:07','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(191,'WmtG9MobcXQBB9bmIArdeGYS604jAiEFmfpydgcw',1,'2023-10-08 19:40:10','2023-10-08 19:35:10','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(192,'tNpGp8c6xAha8zi51Ljh34iF8j9dwavGOYLEmdX8',1,'2023-10-08 19:41:32','2023-10-08 19:41:32','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(193,'hUkQ6MzdfceJJ8fgHgVAPcuWfBciHy5Yah0tckJL',1,'2023-10-08 19:46:04','2023-10-08 19:41:54','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(194,'y8UfDdFWS0tEq7qostN2uMhN4MIinaqtqrvCtN56',1,'2023-10-08 19:48:11','2023-10-08 19:46:33','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(195,'s3j8rrPJWql94xG2GgCzOZuJ7ZDhy6yXImPnLqw1',1,'2023-10-08 19:55:30','2023-10-08 19:49:35','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(196,'rjBibPgweU6qBzEAe3DoTkcKHM1Gdc3Ne8mMSW33',1,'2023-10-08 19:55:31','2023-10-08 19:55:31','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(197,'fmPQGhYKQ78grIOEP11wcEU3pldFe5443gyhwUdn',1,'2023-10-08 19:55:39','2023-10-08 19:55:39','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(198,'WqzOm8McASSB8qRk7alI8DAeEQEYvR8MXzg227qa',1,'2023-10-08 19:56:41','2023-10-08 19:56:41','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(199,'gJIK3xo0EGJ31at3MsYsIOs8cDarbn8lSGVZdepu',1,'2023-10-08 19:57:32','2023-10-08 19:57:32','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(200,'Bc0cv0iIJ9h51A6ZbfPJstXRTc0yFKwELdvVv8fN',1,'2023-10-08 19:57:44','2023-10-08 19:57:44','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(201,'6e3gHPWXcIcoqYgYrVWnxIV3bx0wyvO7wbq44t3Q',1,'2023-10-08 19:58:09','2023-10-08 19:58:09','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(202,'f1sMeZCtqN7qGaPyVbSKVCAGTfgHDzGvjaXPeDlu',1,'2023-10-08 19:58:12','2023-10-08 19:58:12','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(203,'Qz40PRVFlDMISsd31E6DKruVvFeeYPXLTosBQAEW',1,'2023-10-08 20:24:01','2023-10-08 19:58:14','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(204,'hugwJil29IvogajgdguBF2jrvzxNBwXLtsV45UOR',1,'2023-10-08 20:24:01','2023-10-08 20:24:01','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(205,'1yxUBHj1eo2rbZQwFX2rD994ejFHwc46j2uVnoqm',1,'2023-10-08 20:24:21','2023-10-08 20:24:21','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(206,'BFMI9kW2C5dz1gljiOUiRUqm9XR1lTT5rBxwIbpO',1,'2023-10-08 20:31:25','2023-10-08 20:24:33','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(207,'KdwFl2XcV0WQMgKqi1VQ2g2hPNFvbfOdUmY0BsPQ',1,'2023-10-29 22:27:49','2023-10-08 20:31:26','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36'),(208,'R8OfIxlPbQOC8HCIQv1RJVgwHRTOh2BlzIgpk6eU',1,'2023-10-08 21:02:04','2023-10-08 21:00:26','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(210,'3S5OweRZIajrDwJoLYZXIABFJkhxiQ0Z6eP13YFN',1,'2023-10-10 19:14:38','2023-10-10 19:14:38','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36'),(212,'PTGIBwqagqvVtgrjdcRpA8TzSlkDHsmnPoBE9W5Y',1,'2023-10-23 19:07:11','2023-10-23 17:59:20','session',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36'),(213,'TR8Eb8toWjfG3fsFkC26KedB3SiYb1wgEnpN97ue',1,'2023-10-23 19:07:46','2023-10-23 19:07:46','session',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36'),(214,'rvlzWHyswSkwzzsSXRaNzOVxiwaVHyesMtwOoO3Y',1,'2023-10-29 22:38:18','2023-10-29 22:27:50','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36'),(215,'BZvUHqOSG3O1eSyMldElwKFi32L1lgW6ocBEp604',1,'2023-10-29 22:27:51','2023-10-29 22:27:51','session_remember',NULL,'::1','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36'),(216,'r2xAI188sw9M653bwS87bz61rTggZQlrDr0HtxyU',4,'2024-01-09 00:51:17','2024-01-09 00:38:36','session_remember',NULL,'128.195.14.114','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'),(219,'tGkeficFWCsHiBCXCxewfNRSk145wUFxpzaKTqzb',6,'2024-01-09 00:45:27','2024-01-09 00:45:27','session_remember',NULL,'128.195.14.114','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'),(220,'FMgfrQkdpvBNPnNYmZ1raMuX8fMaxLuVlSFggorK',6,'2024-01-09 00:47:58','2024-01-09 00:47:58','session_remember',NULL,'128.195.14.114','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'),(222,'takqGwdohQk61e7SCWtnczJY0lYFVcNUnMxIFkhr',6,'2024-01-09 02:55:55','2024-01-09 02:55:55','session_remember',NULL,'128.195.14.114','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'),(225,'9NmDqdrDsw02GoB95y8zpuRXT54uUBNA8Wj1iq0u',1,'2024-01-09 03:24:05','2024-01-09 03:24:05','session_remember',NULL,'128.195.14.114','Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); /*!40000 ALTER TABLE `access_tokens` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `api_keys` -- DROP TABLE IF EXISTS `api_keys`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `api_keys` ( `key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `id` int unsigned NOT NULL AUTO_INCREMENT, `allowed_ips` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, `scopes` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, `user_id` int unsigned DEFAULT NULL, `created_at` datetime NOT NULL, `last_activity_at` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `api_keys_key_unique` (`key`), KEY `api_keys_user_id_foreign` (`user_id`), CONSTRAINT `api_keys_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `api_keys` -- LOCK TABLES `api_keys` WRITE; /*!40000 ALTER TABLE `api_keys` DISABLE KEYS */; INSERT INTO `api_keys` VALUES ('hdebsyxiigyklxgsqivyswwiisohzlnezzzzzzzz',1,NULL,NULL,NULL,'2023-09-30 21:19:35','2023-10-01 04:14:27'); /*!40000 ALTER TABLE `api_keys` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `discussion_tag` -- DROP TABLE IF EXISTS `discussion_tag`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `discussion_tag` ( `discussion_id` int unsigned NOT NULL, `tag_id` int unsigned NOT NULL, `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`discussion_id`,`tag_id`), KEY `discussion_tag_tag_id_foreign` (`tag_id`), CONSTRAINT `discussion_tag_discussion_id_foreign` FOREIGN KEY (`discussion_id`) REFERENCES `discussions` (`id`) ON DELETE CASCADE, CONSTRAINT `discussion_tag_tag_id_foreign` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `discussion_tag` -- LOCK TABLES `discussion_tag` WRITE; /*!40000 ALTER TABLE `discussion_tag` DISABLE KEYS */; INSERT INTO `discussion_tag` VALUES (2,1,'2023-10-09 19:31:56'),(3,1,'2023-10-09 19:46:22'),(4,2,'2023-10-23 18:24:29'),(5,1,'2024-01-09 00:41:34'); /*!40000 ALTER TABLE `discussion_tag` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `discussion_user` -- DROP TABLE IF EXISTS `discussion_user`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `discussion_user` ( `user_id` int unsigned NOT NULL, `discussion_id` int unsigned NOT NULL, `last_read_at` datetime DEFAULT NULL, `last_read_post_number` int unsigned DEFAULT NULL, `subscription` enum('follow','ignore') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`user_id`,`discussion_id`), KEY `discussion_user_discussion_id_foreign` (`discussion_id`), CONSTRAINT `discussion_user_discussion_id_foreign` FOREIGN KEY (`discussion_id`) REFERENCES `discussions` (`id`) ON DELETE CASCADE, CONSTRAINT `discussion_user_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `discussion_user` -- LOCK TABLES `discussion_user` WRITE; /*!40000 ALTER TABLE `discussion_user` DISABLE KEYS */; INSERT INTO `discussion_user` VALUES (1,2,'2023-10-23 19:27:14',21,NULL),(1,3,'2023-10-23 19:29:46',22,NULL),(1,4,'2023-10-23 18:37:28',6,NULL),(4,5,'2024-01-09 00:41:35',1,NULL); /*!40000 ALTER TABLE `discussion_user` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `discussion_views` -- DROP TABLE IF EXISTS `discussion_views`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `discussion_views` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `user_id` int unsigned DEFAULT NULL, `discussion_id` int unsigned NOT NULL, `ip` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL, `visited_at` datetime NOT NULL, PRIMARY KEY (`id`), KEY `discussion_views_discussion_id_foreign` (`discussion_id`), KEY `discussion_views_user_id_foreign` (`user_id`), CONSTRAINT `discussion_views_discussion_id_foreign` FOREIGN KEY (`discussion_id`) REFERENCES `discussions` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `discussion_views_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `discussion_views` -- LOCK TABLES `discussion_views` WRITE; /*!40000 ALTER TABLE `discussion_views` DISABLE KEYS */; INSERT INTO `discussion_views` VALUES (1,4,5,'128.195.14.114','2024-01-09 00:41:35'),(6,NULL,5,'128.195.14.114','2024-01-09 03:15:01'); /*!40000 ALTER TABLE `discussion_views` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `discussions` -- DROP TABLE IF EXISTS `discussions`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `discussions` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `comment_count` int NOT NULL DEFAULT '1', `participant_count` int unsigned NOT NULL DEFAULT '0', `post_number_index` int unsigned NOT NULL DEFAULT '0', `created_at` datetime NOT NULL, `user_id` int unsigned DEFAULT NULL, `first_post_id` int unsigned DEFAULT NULL, `last_posted_at` datetime DEFAULT NULL, `last_posted_user_id` int unsigned DEFAULT NULL, `last_post_id` int unsigned DEFAULT NULL, `last_post_number` int unsigned DEFAULT NULL, `hidden_at` datetime DEFAULT NULL, `hidden_user_id` int unsigned DEFAULT NULL, `slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `is_private` tinyint(1) NOT NULL DEFAULT '0', `is_approved` tinyint(1) NOT NULL DEFAULT '1', `is_sticky` tinyint(1) NOT NULL DEFAULT '0', `is_locked` tinyint(1) NOT NULL DEFAULT '0', `view_count` int NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `discussions_hidden_user_id_foreign` (`hidden_user_id`), KEY `discussions_first_post_id_foreign` (`first_post_id`), KEY `discussions_last_post_id_foreign` (`last_post_id`), KEY `discussions_last_posted_at_index` (`last_posted_at`), KEY `discussions_last_posted_user_id_index` (`last_posted_user_id`), KEY `discussions_created_at_index` (`created_at`), KEY `discussions_user_id_index` (`user_id`), KEY `discussions_comment_count_index` (`comment_count`), KEY `discussions_participant_count_index` (`participant_count`), KEY `discussions_hidden_at_index` (`hidden_at`), KEY `discussions_is_sticky_created_at_index` (`is_sticky`,`created_at`), KEY `discussions_is_sticky_last_posted_at_index` (`is_sticky`,`last_posted_at`), KEY `discussions_is_locked_index` (`is_locked`), FULLTEXT KEY `title` (`title`), CONSTRAINT `discussions_first_post_id_foreign` FOREIGN KEY (`first_post_id`) REFERENCES `posts` (`id`) ON DELETE SET NULL, CONSTRAINT `discussions_hidden_user_id_foreign` FOREIGN KEY (`hidden_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL, CONSTRAINT `discussions_last_post_id_foreign` FOREIGN KEY (`last_post_id`) REFERENCES `posts` (`id`) ON DELETE SET NULL, CONSTRAINT `discussions_last_posted_user_id_foreign` FOREIGN KEY (`last_posted_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL, CONSTRAINT `discussions_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `discussions` -- LOCK TABLES `discussions` WRITE; /*!40000 ALTER TABLE `discussions` DISABLE KEYS */; INSERT INTO `discussions` VALUES (2,'idk',21,1,0,'2023-10-09 19:31:56',1,2,'2023-10-23 19:27:14',1,47,21,'2024-01-09 00:40:54',4,'idk',0,1,0,0,0),(3,'wdejcwbejcebwjc',22,1,0,'2023-10-09 19:46:22',1,3,'2023-10-23 19:29:45',1,50,22,'2024-01-09 00:41:00',4,'wdejcwbejcebwjc',0,1,0,0,0),(4,'hi hih i',6,1,0,'2023-10-23 18:24:29',1,33,'2023-10-23 18:37:28',1,38,6,'2024-01-09 00:41:57',4,'hi-hih-i',0,1,0,0,1),(5,'Welcome to Texera!',1,1,0,'2024-01-09 00:41:34',4,51,'2024-01-09 00:41:34',4,51,1,NULL,NULL,'welcome-to-texera',0,1,0,0,5); /*!40000 ALTER TABLE `discussions` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `email_tokens` -- DROP TABLE IF EXISTS `email_tokens`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `email_tokens` ( `token` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `email` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `user_id` int unsigned NOT NULL, `created_at` datetime DEFAULT NULL, PRIMARY KEY (`token`), KEY `email_tokens_user_id_foreign` (`user_id`), CONSTRAINT `email_tokens_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `email_tokens` -- LOCK TABLES `email_tokens` WRITE; /*!40000 ALTER TABLE `email_tokens` DISABLE KEYS */; /*!40000 ALTER TABLE `email_tokens` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `flags` -- DROP TABLE IF EXISTS `flags`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `flags` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `post_id` int unsigned NOT NULL, `type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `user_id` int unsigned DEFAULT NULL, `reason` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, `reason_detail` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, `created_at` datetime NOT NULL, PRIMARY KEY (`id`), KEY `flags_post_id_foreign` (`post_id`), KEY `flags_user_id_foreign` (`user_id`), KEY `flags_created_at_index` (`created_at`), CONSTRAINT `flags_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE, CONSTRAINT `flags_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `flags` -- LOCK TABLES `flags` WRITE; /*!40000 ALTER TABLE `flags` DISABLE KEYS */; /*!40000 ALTER TABLE `flags` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `group_permission` -- DROP TABLE IF EXISTS `group_permission`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `group_permission` ( `group_id` int unsigned NOT NULL, `permission` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`group_id`,`permission`), CONSTRAINT `group_permission_group_id_foreign` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `group_permission` -- LOCK TABLES `group_permission` WRITE; /*!40000 ALTER TABLE `group_permission` DISABLE KEYS */; INSERT INTO `group_permission` VALUES (2,'viewForum',NULL),(3,'discussion.flagPosts','2023-09-30 20:58:55'),(3,'discussion.likePosts','2023-09-30 20:58:56'),(3,'discussion.reply',NULL),(3,'discussion.replyWithoutApproval','2023-09-30 20:58:56'),(3,'discussion.startWithoutApproval','2023-09-30 20:58:56'),(3,'searchUsers',NULL),(3,'startDiscussion',NULL),(3,'user.editOwnNickname','2024-01-09 00:40:10'),(4,'discussion.approvePosts','2023-09-30 20:58:56'),(4,'discussion.editPosts',NULL),(4,'discussion.hide',NULL),(4,'discussion.hidePosts',NULL),(4,'discussion.lock','2023-09-30 20:58:56'),(4,'discussion.rename',NULL),(4,'discussion.sticky','2023-09-30 20:58:56'),(4,'discussion.tag','2023-09-30 20:58:56'),(4,'discussion.viewFlags','2023-09-30 20:58:55'),(4,'discussion.viewIpsPosts',NULL),(4,'user.suspend','2023-09-30 20:58:56'),(4,'user.viewLastSeenAt',NULL); /*!40000 ALTER TABLE `group_permission` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `group_user` -- DROP TABLE IF EXISTS `group_user`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `group_user` ( `user_id` int unsigned NOT NULL, `group_id` int unsigned NOT NULL, `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`user_id`,`group_id`), KEY `group_user_group_id_foreign` (`group_id`), CONSTRAINT `group_user_group_id_foreign` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE, CONSTRAINT `group_user_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `group_user` -- LOCK TABLES `group_user` WRITE; /*!40000 ALTER TABLE `group_user` DISABLE KEYS */; INSERT INTO `group_user` VALUES (1,1,'2024-01-09 03:23:43'),(4,1,'2023-09-30 20:58:55'); /*!40000 ALTER TABLE `group_user` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `groups` -- DROP TABLE IF EXISTS `groups`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `groups` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `name_singular` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `name_plural` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `color` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, `icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, `is_hidden` tinyint(1) NOT NULL DEFAULT '0', `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `groups` -- LOCK TABLES `groups` WRITE; /*!40000 ALTER TABLE `groups` DISABLE KEYS */; INSERT INTO `groups` VALUES (1,'Admin','Admins','#B72A2A','fas fa-wrench',0,NULL,NULL),(2,'Guest','Guests',NULL,NULL,0,NULL,NULL),(3,'Member','Members',NULL,NULL,0,NULL,NULL),(4,'Mod','Mods','#80349E','fas fa-bolt',0,NULL,NULL); /*!40000 ALTER TABLE `groups` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `login_providers` -- DROP TABLE IF EXISTS `login_providers`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `login_providers` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `user_id` int unsigned NOT NULL, `provider` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `identifier` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `created_at` datetime DEFAULT NULL, `last_login_at` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `login_providers_provider_identifier_unique` (`provider`,`identifier`), KEY `login_providers_user_id_foreign` (`user_id`), CONSTRAINT `login_providers_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `login_providers` -- LOCK TABLES `login_providers` WRITE; /*!40000 ALTER TABLE `login_providers` DISABLE KEYS */; /*!40000 ALTER TABLE `login_providers` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `migrations` -- DROP TABLE IF EXISTS `migrations`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `migrations` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `migration` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `extension` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=158 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `migrations` -- LOCK TABLES `migrations` WRITE; /*!40000 ALTER TABLE `migrations` DISABLE KEYS */; INSERT INTO `migrations` VALUES (1,'2015_02_24_000000_create_access_tokens_table',NULL),(2,'2015_02_24_000000_create_api_keys_table',NULL),(3,'2015_02_24_000000_create_config_table',NULL),(4,'2015_02_24_000000_create_discussions_table',NULL),(5,'2015_02_24_000000_create_email_tokens_table',NULL),(6,'2015_02_24_000000_create_groups_table',NULL),(7,'2015_02_24_000000_create_notifications_table',NULL),(8,'2015_02_24_000000_create_password_tokens_table',NULL),(9,'2015_02_24_000000_create_permissions_table',NULL),(10,'2015_02_24_000000_create_posts_table',NULL),(11,'2015_02_24_000000_create_users_discussions_table',NULL),(12,'2015_02_24_000000_create_users_groups_table',NULL),(13,'2015_02_24_000000_create_users_table',NULL),(14,'2015_09_15_000000_create_auth_tokens_table',NULL),(15,'2015_09_20_224327_add_hide_to_discussions',NULL),(16,'2015_09_22_030432_rename_notification_read_time',NULL),(17,'2015_10_07_130531_rename_config_to_settings',NULL),(18,'2015_10_24_194000_add_ip_address_to_posts',NULL),(19,'2015_12_05_042721_change_access_tokens_columns',NULL),(20,'2015_12_17_194247_change_settings_value_column_to_text',NULL),(21,'2016_02_04_095452_add_slug_to_discussions',NULL),(22,'2017_04_07_114138_add_is_private_to_discussions',NULL),(23,'2017_04_07_114138_add_is_private_to_posts',NULL),(24,'2018_01_11_093900_change_access_tokens_columns',NULL),(25,'2018_01_11_094000_change_access_tokens_add_foreign_keys',NULL),(26,'2018_01_11_095000_change_api_keys_columns',NULL),(27,'2018_01_11_101800_rename_auth_tokens_to_registration_tokens',NULL),(28,'2018_01_11_102000_change_registration_tokens_rename_id_to_token',NULL),(29,'2018_01_11_102100_change_registration_tokens_created_at_to_datetime',NULL),(30,'2018_01_11_120604_change_posts_table_to_innodb',NULL),(31,'2018_01_11_155200_change_discussions_rename_columns',NULL),(32,'2018_01_11_155300_change_discussions_add_foreign_keys',NULL),(33,'2018_01_15_071700_rename_users_discussions_to_discussion_user',NULL),(34,'2018_01_15_071800_change_discussion_user_rename_columns',NULL),(35,'2018_01_15_071900_change_discussion_user_add_foreign_keys',NULL),(36,'2018_01_15_072600_change_email_tokens_rename_id_to_token',NULL),(37,'2018_01_15_072700_change_email_tokens_add_foreign_keys',NULL),(38,'2018_01_15_072800_change_email_tokens_created_at_to_datetime',NULL),(39,'2018_01_18_130400_rename_permissions_to_group_permission',NULL),(40,'2018_01_18_130500_change_group_permission_add_foreign_keys',NULL),(41,'2018_01_18_130600_rename_users_groups_to_group_user',NULL),(42,'2018_01_18_130700_change_group_user_add_foreign_keys',NULL),(43,'2018_01_18_133000_change_notifications_columns',NULL),(44,'2018_01_18_133100_change_notifications_add_foreign_keys',NULL),(45,'2018_01_18_134400_change_password_tokens_rename_id_to_token',NULL),(46,'2018_01_18_134500_change_password_tokens_add_foreign_keys',NULL),(47,'2018_01_18_134600_change_password_tokens_created_at_to_datetime',NULL),(48,'2018_01_18_135000_change_posts_rename_columns',NULL),(49,'2018_01_18_135100_change_posts_add_foreign_keys',NULL),(50,'2018_01_30_112238_add_fulltext_index_to_discussions_title',NULL),(51,'2018_01_30_220100_create_post_user_table',NULL),(52,'2018_01_30_222900_change_users_rename_columns',NULL),(55,'2018_09_15_041340_add_users_indicies',NULL),(56,'2018_09_15_041828_add_discussions_indicies',NULL),(57,'2018_09_15_043337_add_notifications_indices',NULL),(58,'2018_09_15_043621_add_posts_indices',NULL),(59,'2018_09_22_004100_change_registration_tokens_columns',NULL),(60,'2018_09_22_004200_create_login_providers_table',NULL),(61,'2018_10_08_144700_add_shim_prefix_to_group_icons',NULL),(62,'2019_10_12_195349_change_posts_add_discussion_foreign_key',NULL),(63,'2020_03_19_134512_change_discussions_default_comment_count',NULL),(64,'2020_04_21_130500_change_permission_groups_add_is_hidden',NULL),(65,'2021_03_02_040000_change_access_tokens_add_type',NULL),(66,'2021_03_02_040500_change_access_tokens_add_id',NULL),(67,'2021_03_02_041000_change_access_tokens_add_title_ip_agent',NULL),(68,'2021_04_18_040500_change_migrations_add_id_primary_key',NULL),(69,'2021_04_18_145100_change_posts_content_column_to_mediumtext',NULL),(70,'2018_07_21_000000_seed_default_groups',NULL),(71,'2018_07_21_000100_seed_default_group_permissions',NULL),(72,'2021_05_10_000000_rename_permissions',NULL),(73,'2022_05_20_000000_add_timestamps_to_groups_table',NULL),(74,'2022_05_20_000001_add_created_at_to_group_user_table',NULL),(75,'2022_05_20_000002_add_created_at_to_group_permission_table',NULL),(76,'2022_07_14_000000_add_type_index_to_posts',NULL),(77,'2022_07_14_000001_add_type_created_at_composite_index_to_posts',NULL),(78,'2022_08_06_000000_change_access_tokens_last_activity_at_to_nullable',NULL),(79,'2015_09_02_000000_add_flags_read_time_to_users_table','flarum-flags'),(80,'2015_09_02_000000_create_flags_table','flarum-flags'),(81,'2017_07_22_000000_add_default_permissions','flarum-flags'),(82,'2018_06_27_101500_change_flags_rename_time_to_created_at','flarum-flags'),(83,'2018_06_27_101600_change_flags_add_foreign_keys','flarum-flags'),(84,'2018_06_27_105100_change_users_rename_flags_read_time_to_read_flags_at','flarum-flags'),(85,'2018_09_15_043621_add_flags_indices','flarum-flags'),(86,'2019_10_22_000000_change_reason_text_col_type','flarum-flags'),(87,'2015_09_21_011527_add_is_approved_to_discussions','flarum-approval'),(88,'2015_09_21_011706_add_is_approved_to_posts','flarum-approval'),(89,'2017_07_22_000000_add_default_permissions','flarum-approval'),(90,'2015_02_24_000000_create_discussions_tags_table','flarum-tags'),(91,'2015_02_24_000000_create_tags_table','flarum-tags'),(92,'2015_02_24_000000_create_users_tags_table','flarum-tags'),(93,'2015_02_24_000000_set_default_settings','flarum-tags'),(94,'2015_10_19_061223_make_slug_unique','flarum-tags'),(95,'2017_07_22_000000_add_default_permissions','flarum-tags'),(96,'2018_06_27_085200_change_tags_columns','flarum-tags'),(97,'2018_06_27_085300_change_tags_add_foreign_keys','flarum-tags'),(98,'2018_06_27_090400_rename_users_tags_to_tag_user','flarum-tags'),(99,'2018_06_27_100100_change_tag_user_rename_read_time_to_marked_as_read_at','flarum-tags'),(100,'2018_06_27_100200_change_tag_user_add_foreign_keys','flarum-tags'),(101,'2018_06_27_103000_rename_discussions_tags_to_discussion_tag','flarum-tags'),(102,'2018_06_27_103100_add_discussion_tag_foreign_keys','flarum-tags'),(103,'2019_04_21_000000_add_icon_to_tags_table','flarum-tags'),(104,'2022_05_20_000003_add_timestamps_to_tags_table','flarum-tags'),(105,'2022_05_20_000004_add_created_at_to_discussion_tag_table','flarum-tags'),(106,'2023_03_01_000000_create_post_mentions_tag_table','flarum-tags'),(107,'2015_05_11_000000_add_suspended_until_to_users_table','flarum-suspend'),(108,'2015_09_14_000000_rename_suspended_until_column','flarum-suspend'),(109,'2017_07_22_000000_add_default_permissions','flarum-suspend'),(110,'2018_06_27_111400_change_users_rename_suspend_until_to_suspended_until','flarum-suspend'),(111,'2021_10_27_000000_add_suspend_reason_and_message','flarum-suspend'),(112,'2015_05_11_000000_add_subscription_to_users_discussions_table','flarum-subscriptions'),(113,'2015_02_24_000000_add_sticky_to_discussions','flarum-sticky'),(114,'2017_07_22_000000_add_default_permissions','flarum-sticky'),(115,'2018_09_15_043621_add_discussions_indices','flarum-sticky'),(116,'2021_01_13_000000_add_discussion_last_posted_at_indices','flarum-sticky'),(117,'2015_05_11_000000_create_mentions_posts_table','flarum-mentions'),(118,'2015_05_11_000000_create_mentions_users_table','flarum-mentions'),(119,'2018_06_27_102000_rename_mentions_posts_to_post_mentions_post','flarum-mentions'),(120,'2018_06_27_102100_rename_mentions_users_to_post_mentions_user','flarum-mentions'),(121,'2018_06_27_102200_change_post_mentions_post_rename_mentions_id_to_mentions_post_id','flarum-mentions'),(122,'2018_06_27_102300_change_post_mentions_post_add_foreign_keys','flarum-mentions'),(123,'2018_06_27_102400_change_post_mentions_user_rename_mentions_id_to_mentions_user_id','flarum-mentions'),(124,'2018_06_27_102500_change_post_mentions_user_add_foreign_keys','flarum-mentions'),(125,'2021_04_19_000000_set_default_settings','flarum-mentions'),(126,'2022_05_20_000005_add_created_at_to_post_mentions_post_table','flarum-mentions'),(127,'2022_05_20_000006_add_created_at_to_post_mentions_user_table','flarum-mentions'),(128,'2022_10_21_000000_create_post_mentions_group_table','flarum-mentions'),(129,'2021_03_25_000000_default_settings','flarum-markdown'),(130,'2015_02_24_000000_add_locked_to_discussions','flarum-lock'),(131,'2017_07_22_000000_add_default_permissions','flarum-lock'),(132,'2018_09_15_043621_add_discussions_indices','flarum-lock'),(133,'2015_05_11_000000_create_posts_likes_table','flarum-likes'),(134,'2015_09_04_000000_add_default_like_permissions','flarum-likes'),(135,'2018_06_27_100600_rename_posts_likes_to_post_likes','flarum-likes'),(136,'2018_06_27_100700_change_post_likes_add_foreign_keys','flarum-likes'),(137,'2021_05_10_094200_add_created_at_to_post_likes_table','flarum-likes'),(138,'2018_09_29_060444_replace_emoji_shorcuts_with_unicode','flarum-emoji'),(140,'2020_07_29_010101_add_post_field','kyrne-evergreen'),(141,'2019_06_07_000000_add_recipients_table','fof-byobu'),(142,'2019_06_07_000001_remove_flagrow_migrations','fof-byobu'),(143,'2019_07_08_000000_add_blocks_pd_to_users','fof-byobu'),(144,'2019_07_09_000000_blocks_pd_index','fof-byobu'),(145,'2020_02_14_214800_fix_user_id_not_nullable_for_group_pds','fof-byobu'),(146,'2020_02_19_110103_remove_retired_settings_key','fof-byobu'),(147,'2020_10_23_000000_users_unified_index_column','fof-byobu'),(148,'2021_01_13_000000_unified_index_index','fof-byobu'),(149,'2021_01_13_000001_byobu_indicies','fof-byobu'),(150,'2021_01_23_000000_drop_tags_from_old_private_discussions','fof-byobu'),(151,'2021_04_21_000000_drop_users_unified_index_column','fof-byobu'),(152,'2017_11_07_223624_discussions_add_views','michaelbelgium-discussion-views'),(153,'2018_11_30_141817_discussions_rename_views','michaelbelgium-discussion-views'),(154,'2020_01_11_220612_add_discussionviews_table','michaelbelgium-discussion-views'),(155,'2020_11_23_000000_add_nickname_column','flarum-nicknames'),(156,'2020_12_02_000001_set_default_permissions','flarum-nicknames'),(157,'2021_11_16_000000_nickname_column_nullable','flarum-nicknames'); /*!40000 ALTER TABLE `migrations` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `notifications` -- DROP TABLE IF EXISTS `notifications`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `notifications` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `user_id` int unsigned NOT NULL, `from_user_id` int unsigned DEFAULT NULL, `type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `subject_id` int unsigned DEFAULT NULL, `data` blob, `created_at` datetime NOT NULL, `is_deleted` tinyint(1) NOT NULL DEFAULT '0', `read_at` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `notifications_from_user_id_foreign` (`from_user_id`), KEY `notifications_user_id_index` (`user_id`), CONSTRAINT `notifications_from_user_id_foreign` FOREIGN KEY (`from_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL, CONSTRAINT `notifications_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `notifications` -- LOCK TABLES `notifications` WRITE; /*!40000 ALTER TABLE `notifications` DISABLE KEYS */; /*!40000 ALTER TABLE `notifications` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `password_tokens` -- DROP TABLE IF EXISTS `password_tokens`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `password_tokens` ( `token` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `user_id` int unsigned NOT NULL, `created_at` datetime DEFAULT NULL, PRIMARY KEY (`token`), KEY `password_tokens_user_id_foreign` (`user_id`), CONSTRAINT `password_tokens_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `password_tokens` -- LOCK TABLES `password_tokens` WRITE; /*!40000 ALTER TABLE `password_tokens` DISABLE KEYS */; /*!40000 ALTER TABLE `password_tokens` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `post_likes` -- DROP TABLE IF EXISTS `post_likes`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `post_likes` ( `post_id` int unsigned NOT NULL, `user_id` int unsigned NOT NULL, `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`post_id`,`user_id`), KEY `post_likes_user_id_foreign` (`user_id`), CONSTRAINT `post_likes_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE, CONSTRAINT `post_likes_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `post_likes` -- LOCK TABLES `post_likes` WRITE; /*!40000 ALTER TABLE `post_likes` DISABLE KEYS */; /*!40000 ALTER TABLE `post_likes` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `post_mentions_group` -- DROP TABLE IF EXISTS `post_mentions_group`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `post_mentions_group` ( `post_id` int unsigned NOT NULL, `mentions_group_id` int unsigned NOT NULL, `created_at` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`post_id`,`mentions_group_id`), KEY `post_mentions_group_mentions_group_id_foreign` (`mentions_group_id`), CONSTRAINT `post_mentions_group_mentions_group_id_foreign` FOREIGN KEY (`mentions_group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE, CONSTRAINT `post_mentions_group_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `post_mentions_group` -- LOCK TABLES `post_mentions_group` WRITE; /*!40000 ALTER TABLE `post_mentions_group` DISABLE KEYS */; /*!40000 ALTER TABLE `post_mentions_group` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `post_mentions_post` -- DROP TABLE IF EXISTS `post_mentions_post`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `post_mentions_post` ( `post_id` int unsigned NOT NULL, `mentions_post_id` int unsigned NOT NULL, `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`post_id`,`mentions_post_id`), KEY `post_mentions_post_mentions_post_id_foreign` (`mentions_post_id`), CONSTRAINT `post_mentions_post_mentions_post_id_foreign` FOREIGN KEY (`mentions_post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE, CONSTRAINT `post_mentions_post_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `post_mentions_post` -- LOCK TABLES `post_mentions_post` WRITE; /*!40000 ALTER TABLE `post_mentions_post` DISABLE KEYS */; /*!40000 ALTER TABLE `post_mentions_post` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `post_mentions_tag` -- DROP TABLE IF EXISTS `post_mentions_tag`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `post_mentions_tag` ( `post_id` int unsigned NOT NULL, `mentions_tag_id` int unsigned NOT NULL, `created_at` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`post_id`,`mentions_tag_id`), KEY `post_mentions_tag_mentions_tag_id_foreign` (`mentions_tag_id`), CONSTRAINT `post_mentions_tag_mentions_tag_id_foreign` FOREIGN KEY (`mentions_tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE, CONSTRAINT `post_mentions_tag_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `post_mentions_tag` -- LOCK TABLES `post_mentions_tag` WRITE; /*!40000 ALTER TABLE `post_mentions_tag` DISABLE KEYS */; /*!40000 ALTER TABLE `post_mentions_tag` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `post_mentions_user` -- DROP TABLE IF EXISTS `post_mentions_user`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `post_mentions_user` ( `post_id` int unsigned NOT NULL, `mentions_user_id` int unsigned NOT NULL, `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`post_id`,`mentions_user_id`), KEY `post_mentions_user_mentions_user_id_foreign` (`mentions_user_id`), CONSTRAINT `post_mentions_user_mentions_user_id_foreign` FOREIGN KEY (`mentions_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, CONSTRAINT `post_mentions_user_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `post_mentions_user` -- LOCK TABLES `post_mentions_user` WRITE; /*!40000 ALTER TABLE `post_mentions_user` DISABLE KEYS */; INSERT INTO `post_mentions_user` VALUES (5,1,'2023-10-09 19:47:13'); /*!40000 ALTER TABLE `post_mentions_user` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `post_user` -- DROP TABLE IF EXISTS `post_user`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `post_user` ( `post_id` int unsigned NOT NULL, `user_id` int unsigned NOT NULL, PRIMARY KEY (`post_id`,`user_id`), KEY `post_user_user_id_foreign` (`user_id`), CONSTRAINT `post_user_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE, CONSTRAINT `post_user_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `post_user` -- LOCK TABLES `post_user` WRITE; /*!40000 ALTER TABLE `post_user` DISABLE KEYS */; /*!40000 ALTER TABLE `post_user` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `posts` -- DROP TABLE IF EXISTS `posts`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `posts` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `discussion_id` int unsigned NOT NULL, `number` int unsigned DEFAULT NULL, `created_at` datetime NOT NULL, `user_id` int unsigned DEFAULT NULL, `type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, `content` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT ' ', `edited_at` datetime DEFAULT NULL, `edited_user_id` int unsigned DEFAULT NULL, `hidden_at` datetime DEFAULT NULL, `hidden_user_id` int unsigned DEFAULT NULL, `ip_address` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, `is_private` tinyint(1) NOT NULL DEFAULT '0', `is_approved` tinyint(1) NOT NULL DEFAULT '1', `reply_to` int NOT NULL, `reply_count` int NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `posts_discussion_id_number_unique` (`discussion_id`,`number`), KEY `posts_edited_user_id_foreign` (`edited_user_id`), KEY `posts_hidden_user_id_foreign` (`hidden_user_id`), KEY `posts_discussion_id_number_index` (`discussion_id`,`number`), KEY `posts_discussion_id_created_at_index` (`discussion_id`,`created_at`), KEY `posts_user_id_created_at_index` (`user_id`,`created_at`), KEY `posts_type_index` (`type`), KEY `posts_type_created_at_index` (`type`,`created_at`), FULLTEXT KEY `content` (`content`), CONSTRAINT `posts_discussion_id_foreign` FOREIGN KEY (`discussion_id`) REFERENCES `discussions` (`id`) ON DELETE CASCADE, CONSTRAINT `posts_edited_user_id_foreign` FOREIGN KEY (`edited_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL, CONSTRAINT `posts_hidden_user_id_foreign` FOREIGN KEY (`hidden_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL, CONSTRAINT `posts_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ) ENGINE=InnoDB AUTO_INCREMENT=52 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `posts` -- LOCK TABLES `posts` WRITE; /*!40000 ALTER TABLE `posts` DISABLE KEYS */; INSERT INTO `posts` VALUES (2,2,1,'2023-10-09 19:31:56',1,'comment','

idk

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(3,3,1,'2023-10-09 19:46:22',1,'comment','

**sxxbnsb nxsbxsxnsx**

',NULL,NULL,NULL,NULL,'::1',0,1,0,1),(4,3,2,'2023-10-09 19:46:54',1,'comment','

fjyfj

',NULL,NULL,NULL,NULL,'::1',0,1,0,1),(5,3,3,'2023-10-09 19:47:12',1,'comment','

@myAdmin

\n
',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(6,3,4,'2023-10-23 17:53:08',1,'comment','

sav

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(7,3,5,'2023-10-23 17:54:51',1,'comment','

@myAdmin#6 hfu

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(8,3,6,'2023-10-23 17:55:08',1,'comment','

@myAdmin#6 sahxhb

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(9,3,7,'2023-10-23 17:55:41',1,'comment','

@myAdmin#3 23r3r

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(10,3,8,'2023-10-23 17:56:30',1,'comment','

@myAdmin#6 wgtwqtaw

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(11,3,9,'2023-10-23 17:56:36',1,'comment','

@myAdmin#7 wqfawetet

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(12,3,10,'2023-10-23 17:57:10',1,'comment','

@myAdmin#4 hawegf

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(13,3,11,'2023-10-23 17:57:21',1,'comment','

@myAdmin#12 weakjfhaewf

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(14,3,12,'2023-10-23 17:59:27',1,'comment','

@myAdmin#9 hello

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(15,2,2,'2023-10-23 18:02:19',1,'comment','

hello

\n
',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(16,2,3,'2023-10-23 18:02:46',1,'comment','

ewafwef

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(17,2,4,'2023-10-23 18:02:52',1,'comment','

@myAdmin#16 weagfaewgaweg

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(18,2,5,'2023-10-23 18:03:41',1,'comment','

@myAdmin#17 wefgaew

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(19,2,6,'2023-10-23 18:05:21',1,'comment','

eee

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(20,2,7,'2023-10-23 18:05:27',1,'comment','

@myAdmin#17 ewfwew

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(21,3,13,'2023-10-23 18:09:38',1,'comment','

ewagewg

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(22,3,14,'2023-10-23 18:09:47',1,'comment','

abc

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(23,3,15,'2023-10-23 18:09:52',1,'comment','

@myAdmin#11 abc

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(24,3,16,'2023-10-23 18:12:52',1,'comment','

abcd

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(25,3,17,'2023-10-23 18:13:17',1,'comment','

abcdefg

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(26,3,18,'2023-10-23 18:13:44',1,'comment','

@myAdmin#25 abcdefghijk

',NULL,NULL,NULL,NULL,'::1',0,1,0,1),(27,3,19,'2023-10-23 18:18:20',1,'comment','

@myAdmin#14 123

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(28,2,8,'2023-10-23 18:20:01',1,'comment','

@myAdmin#19 eeee

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(29,2,9,'2023-10-23 18:20:12',1,'comment','

eeee

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(30,2,10,'2023-10-23 18:20:19',1,'comment','

@myAdmin#19 eeeee

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(31,2,11,'2023-10-23 18:20:37',1,'comment','

123

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(32,2,12,'2023-10-23 18:20:43',1,'comment','

@myAdmin#31 1234567

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(33,4,1,'2023-10-23 18:24:29',1,'comment','

welcome

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(34,4,2,'2023-10-23 18:24:38',1,'comment','

@myAdmin#33 good forum

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(35,4,3,'2023-10-23 18:25:24',1,'comment','

@myAdmin#33 yesi think so

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(36,4,4,'2023-10-23 18:35:56',1,'comment','

weafew

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(37,4,5,'2023-10-23 18:36:05',1,'comment','

@myAdmin#36 12345

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(38,4,6,'2023-10-23 18:37:28',1,'comment','

@myAdmin#36 1234567

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(39,2,13,'2023-10-23 18:43:42',1,'comment','

@myAdmin#16 123456789

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(40,2,14,'2023-10-23 19:01:13',1,'comment','

@myAdmin#32 1234

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(41,2,15,'2023-10-23 19:01:49',1,'comment','

@myAdmin#40 123

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(42,2,16,'2023-10-23 19:07:53',1,'comment','

@myAdmin#41 12355

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(43,2,17,'2023-10-23 19:14:37',1,'comment','

123

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(44,2,18,'2023-10-23 19:14:48',1,'comment','

@myAdmin#43 1234

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(45,2,19,'2023-10-23 19:15:28',1,'comment','

@myAdmin#43 gvuyg

',NULL,NULL,NULL,NULL,'::1',0,1,0,0),(46,2,20,'2023-10-23 19:15:43',1,'comment','

@myAdmin#43 kjbk

',NULL,NULL,NULL,NULL,'::1',0,1,0,1),(47,2,21,'2023-10-23 19:27:14',1,'comment','

@myAdmin#46 ewagfewgwe

',NULL,NULL,NULL,NULL,'::1',0,1,46,0),(48,3,20,'2023-10-23 19:29:05',1,'comment','

@myAdmin#3 ewafeg

',NULL,NULL,NULL,NULL,'::1',0,1,3,0),(49,3,21,'2023-10-23 19:29:16',1,'comment','

@myAdmin#26 evvevv

',NULL,NULL,NULL,NULL,'::1',0,1,26,0),(50,3,22,'2023-10-23 19:29:45',1,'comment','

@myAdmin#4 utfiufut

',NULL,NULL,NULL,NULL,'::1',0,1,4,0),(51,5,1,'2024-01-09 00:41:34',4,'comment','

Welcome to the discussion forum of Texera!

',NULL,NULL,NULL,NULL,'128.195.14.114',0,1,0,0); /*!40000 ALTER TABLE `posts` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `recipients` -- DROP TABLE IF EXISTS `recipients`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `recipients` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `discussion_id` int unsigned DEFAULT NULL, `user_id` int unsigned DEFAULT NULL, `group_id` int unsigned DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, `removed_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), KEY `recipients_user_id_foreign` (`user_id`), KEY `recipients_group_id_foreign` (`group_id`), KEY `recipients_removed_at_index` (`removed_at`), KEY `recipients_discussion_id_user_id_index` (`discussion_id`,`user_id`), KEY `recipients_discussion_id_group_id_index` (`discussion_id`,`group_id`), CONSTRAINT `recipients_discussion_id_foreign` FOREIGN KEY (`discussion_id`) REFERENCES `discussions` (`id`) ON DELETE CASCADE, CONSTRAINT `recipients_group_id_foreign` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE, CONSTRAINT `recipients_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `recipients` -- LOCK TABLES `recipients` WRITE; /*!40000 ALTER TABLE `recipients` DISABLE KEYS */; /*!40000 ALTER TABLE `recipients` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `registration_tokens` -- DROP TABLE IF EXISTS `registration_tokens`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `registration_tokens` ( `token` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `payload` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, `created_at` datetime DEFAULT NULL, `provider` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `identifier` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `user_attributes` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, PRIMARY KEY (`token`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `registration_tokens` -- LOCK TABLES `registration_tokens` WRITE; /*!40000 ALTER TABLE `registration_tokens` DISABLE KEYS */; /*!40000 ALTER TABLE `registration_tokens` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `settings` -- DROP TABLE IF EXISTS `settings`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `settings` ( `key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, PRIMARY KEY (`key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `settings` -- LOCK TABLES `settings` WRITE; /*!40000 ALTER TABLE `settings` DISABLE KEYS */; INSERT INTO `settings` VALUES ('allow_hide_own_posts','reply'),('allow_post_editing','reply'),('allow_renaming','10'),('allow_sign_up','1'),('custom_less','.item-account{\ndisplay: none;\n}\n\n.Header-title a{\nfont-size: 2rem;\ncolor: black;\n}\n\n.Search-input .FormControlz{\nwidth:150%;\n}\n\nbutton.Dropdown-toggle.Button.Button--user.Button--flat {\ndisplay: none;\n}\n\n.Header-title a{\ncolor: #000000D9;\nfont-family: -apple-system, BlinkMacSystemFont, sans-serif;\nmargin: 0 0 8px;\nmargin-bottom: 0.5rem;\nfont-weight: 500;\nline-height: 1.2;\nmargin-top: 0;\n}\n\n.Header-title a {\n font-size: 0; /* This will visually hide the text */\n position: relative; \n}\n\n.Header-title a::after {\n content: \"Home\"; /* This will insert the new text */\n display: inline-block; /* Makes it behave like inline text */\n}\n\n.Header-primary{\ndisplay: hidden;\n}\n\n.Header-secondary{\nalign: left;\n}\n\n.sideNav .Dropdown--select {\n display: none;\n}\n\n.sideNavContainer {\ndisplay: block !important;\n}\n\n.fa-reply::before {\n display: none !important;\n}'),('default_locale','en'),('default_route','/tags'),('display_name_driver','username'),('extensions_enabled','[\"flarum-flags\",\"flarum-approval\",\"michaelbelgium-discussion-views\",\"fof-byobu\",\"flarum-sticky\",\"flarum-statistics\",\"flarum-nicknames\",\"flarum-mentions\",\"flarum-markdown\",\"flarum-lock\",\"flarum-likes\",\"flarum-lang-english\",\"flarum-emoji\"]'),('flarum-markdown.mdarea','1'),('flarum-mentions.allow_username_format','1'),('flarum-tags.max_primary_tags','10'),('flarum-tags.max_secondary_tags','3'),('flarum-tags.min_primary_tags','1'),('flarum-tags.min_secondary_tags','0'),('forum_description',''),('forum_title','Discussions'),('mail_driver','mail'),('mail_from','noreply@localhost'),('slug_driver_Flarum\\Discussion\\Discussion',''),('slug_driver_Flarum\\User\\User','default'),('theme_colored_header','0'),('theme_dark_mode','0'),('theme_primary_color','#4D698E'),('theme_secondary_color','#4D698E'),('version','1.8.5'),('welcome_message','Discuss the Texera platform & machine learning topics – this includes sharing feedback, asking questions, and more.'),('welcome_title','Welcome to Texera '); /*!40000 ALTER TABLE `settings` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `tag_user` -- DROP TABLE IF EXISTS `tag_user`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `tag_user` ( `user_id` int unsigned NOT NULL, `tag_id` int unsigned NOT NULL, `marked_as_read_at` datetime DEFAULT NULL, `is_hidden` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`user_id`,`tag_id`), KEY `tag_user_tag_id_foreign` (`tag_id`), CONSTRAINT `tag_user_tag_id_foreign` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE, CONSTRAINT `tag_user_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `tag_user` -- LOCK TABLES `tag_user` WRITE; /*!40000 ALTER TABLE `tag_user` DISABLE KEYS */; /*!40000 ALTER TABLE `tag_user` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `tags` -- DROP TABLE IF EXISTS `tags`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `tags` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `slug` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, `color` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, `background_path` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, `background_mode` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, `position` int DEFAULT NULL, `parent_id` int unsigned DEFAULT NULL, `default_sort` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, `is_restricted` tinyint(1) NOT NULL DEFAULT '0', `is_hidden` tinyint(1) NOT NULL DEFAULT '0', `discussion_count` int unsigned NOT NULL DEFAULT '0', `last_posted_at` datetime DEFAULT NULL, `last_posted_discussion_id` int unsigned DEFAULT NULL, `last_posted_user_id` int unsigned DEFAULT NULL, `icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `tags_slug_unique` (`slug`), KEY `tags_parent_id_foreign` (`parent_id`), KEY `tags_last_posted_user_id_foreign` (`last_posted_user_id`), KEY `tags_last_posted_discussion_id_foreign` (`last_posted_discussion_id`), CONSTRAINT `tags_last_posted_discussion_id_foreign` FOREIGN KEY (`last_posted_discussion_id`) REFERENCES `discussions` (`id`) ON DELETE SET NULL, CONSTRAINT `tags_last_posted_user_id_foreign` FOREIGN KEY (`last_posted_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL, CONSTRAINT `tags_parent_id_foreign` FOREIGN KEY (`parent_id`) REFERENCES `tags` (`id`) ON DELETE SET NULL ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `tags` -- LOCK TABLES `tags` WRITE; /*!40000 ALTER TABLE `tags` DISABLE KEYS */; INSERT INTO `tags` VALUES (1,'General','general','Announcements, resources, and interesting discussions','#20BEFF',NULL,NULL,0,NULL,NULL,0,0,1,'2024-01-09 00:41:34',5,4,'fas fa-wrench',NULL,'2024-01-09 00:41:35'),(2,'Getting Started','getting-started','The first stop for new Kagglers','#FAE041',NULL,NULL,1,NULL,NULL,0,0,1,'2023-10-23 18:37:28',4,1,'fas fa-toolbox','2023-10-05 00:29:00','2023-10-23 18:37:28'),(3,'Product Feedback','product-feedback','Tell us what you love, hate, and wish for','#20BEFF',NULL,NULL,2,NULL,NULL,0,0,0,NULL,NULL,NULL,'fas fa-comment-dots','2023-10-05 00:29:16','2023-10-05 00:49:22'),(4,'Questions & Answers','questions-answers','Technical advice from other data scientists','#FAE041',NULL,NULL,3,NULL,NULL,0,0,0,NULL,NULL,NULL,'fas fa-plug','2023-10-05 00:29:46','2023-10-05 00:49:40'),(5,'Competition Hosting','competition-hosting','Advice and support on running your own competitions','#20BEFF',NULL,NULL,4,NULL,NULL,0,0,0,NULL,NULL,NULL,'fas fa-code','2023-10-05 00:30:34','2023-10-05 00:49:57'),(7,'Achievement ','achievement','Celebrate success, share achievement ','#FAE041',NULL,NULL,5,NULL,NULL,0,0,0,NULL,NULL,NULL,'fas fa-vote-yea','2023-10-05 00:35:51','2023-10-05 00:49:48'); /*!40000 ALTER TABLE `tags` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `users` -- DROP TABLE IF EXISTS `users`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `users` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `username` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, `email` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `is_email_confirmed` tinyint(1) NOT NULL DEFAULT '1', `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `avatar_url` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, `preferences` blob, `joined_at` datetime DEFAULT NULL, `last_seen_at` datetime DEFAULT NULL, `marked_all_as_read_at` datetime DEFAULT NULL, `read_notifications_at` datetime DEFAULT NULL, `discussion_count` int unsigned NOT NULL DEFAULT '0', `comment_count` int unsigned NOT NULL DEFAULT '0', `read_flags_at` datetime DEFAULT NULL, `suspended_until` datetime DEFAULT NULL, `suspend_reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, `suspend_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, `blocks_byobu_pd` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `users_username_unique` (`username`), UNIQUE KEY `users_email_unique` (`email`), KEY `users_joined_at_index` (`joined_at`), KEY `users_last_seen_at_index` (`last_seen_at`), KEY `users_discussion_count_index` (`discussion_count`), KEY `users_comment_count_index` (`comment_count`), KEY `users_blocks_byobu_pd_index` (`blocks_byobu_pd`), KEY `users_nickname_index` (`nickname`) ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `users` -- LOCK TABLES `users` WRITE; /*!40000 ALTER TABLE `users` DISABLE KEYS */; INSERT INTO `users` VALUES (1,'myAdmin',NULL,'xinyual3@uci.edu',1,'$2a$10$0raoWm.MtGloDa95/CDgne54rX9uoa4RlmeoCA.ce9V1axfIHECSK',NULL,NULL,'2023-09-30 20:58:55','2024-01-09 03:24:06',NULL,'2023-10-17 03:07:23',3,49,'2023-10-16 06:52:14',NULL,NULL,NULL,0),(4,'xiaozl3@uci.edu',NULL,'xiaozl3@uci.edu',1,'$2a$10$63ISJ9VtBtA7R33QqNWLxeqb9Qqg97nq6D8E4sSAfIiquLcSO9XHC',NULL,NULL,NULL,'2024-01-09 00:51:18',NULL,NULL,1,1,NULL,NULL,NULL,NULL,0),(6,'linxinyuan@gmail.com',NULL,'linxinyuan@gmail.com',1,'$2a$10$QLbj.1IBcVCfuJmoqLNm4.64sDw23rInF4NDEAw5.9D4V6bd6x0DK',NULL,NULL,NULL,'2024-01-09 02:54:23',NULL,'2024-01-09 02:55:22',0,0,NULL,NULL,NULL,NULL,0); /*!40000 ALTER TABLE `users` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; -- Dump completed on 2024-01-08 19:48:21 ================================================ FILE: bin/forum/macos-install.sh ================================================ #!/bin/bash # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. echo "Updating Homebrew..." brew update && brew upgrade # Install PHP, Apache, mysql-client and Composer brew install php httpd composer echo "Creating flarum directory..." rm -rf /opt/homebrew/var/www/flarum mkdir /opt/homebrew/var/www/flarum composer create-project flarum/flarum /opt/homebrew/var/www/flarum composer require --working-dir=/opt/homebrew/var/www/flarum michaelbelgium/flarum-discussion-views composer require --working-dir=/opt/homebrew/var/www/flarum fof/byobu:"*" cp config.php /opt/homebrew/var/www/flarum/config.php cp .htaccess /opt/homebrew/var/www/flarum/public/.htaccess # Database Configuration echo "Setting up mysql database for flarum..." mysql -u root -p < sql/flarum.sql # Apache Configuration HTTPD_CONF="/opt/homebrew/etc/httpd/httpd.conf" VHOST_CONF="/opt/homebrew/etc/httpd/extra/httpd-vhosts.conf" PHP_CONF="/opt/homebrew/etc/httpd/extra/httpd-php.conf" echo "Configuring Apache..." sed -i '' 's|#LoadModule rewrite_module|LoadModule rewrite_module|' $HTTPD_CONF sed -i '' 's|#Include /opt/homebrew/etc/httpd/extra/httpd-vhosts.conf|Include /opt/homebrew/etc/httpd/extra/httpd-vhosts.conf|' $HTTPD_CONF sed -i '' 's|Listen 8080|Listen 8888|' $HTTPD_CONF # Add PHP configuration echo "LoadModule php_module /opt/homebrew/opt/php/lib/httpd/modules/libphp.so" | tee -a $HTTPD_CONF echo "Include /opt/homebrew/etc/httpd/extra/httpd-php.conf" | tee -a $HTTPD_CONF # Check if httpd-php.conf exists, if not, create and configure it if [ ! -f $PHP_CONF ]; then echo "Creating and configuring httpd-php.conf..." echo " SetHandler application/x-httpd-php DirectoryIndex index.html index.php " | tee $PHP_CONF fi # Virtual Host Configuration echo " DocumentRoot \"/opt/homebrew/var/www/flarum/public\" Options Indexes FollowSymLinks AllowOverride All Require all granted " | tee -a $VHOST_CONF # Restart Apache echo "Restarting Apache..." sudo apachectl restart # Publish assets ( cd /opt/homebrew/var/www/flarum echo "Configuring flarum..." php flarum assets:publish sudo chown -R _www:_www /opt/homebrew/var/www/flarum echo "Flarum installation completed\nYou can now access your flarum forum in Texera" ) ================================================ FILE: bin/forum/start-flarum.sh ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. ./Apache24/bin/httpd.exe ================================================ FILE: bin/forum/ubuntu-install.sh ================================================ #!/bin/bash # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. sudo apt update && sudo apt upgrade sudo apt install apache2 php php-curl php-dom php-mysql sudo rm -rf /opt/flarum sudo mkdir /opt/flarum sudo chown $USER:$USER /opt/flarum php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" php composer-setup.php php -r "unlink('composer-setup.php');" composer create-project flarum/flarum /opt/flarum composer require --working-dir=/opt/flarum michaelbelgium/flarum-discussion-views composer require --working-dir=/opt/flarum fof/byobu:"*" sudo cp bin/config.php /opt/flarum/config.php sudo cp bin/.htaccess /opt/flarum/public/.htaccess sudo chown -R www-data:www-data /opt/flarum sudo mysql -u root -p < sql/flarum.sql VHOST_CONF="/etc/apache2/sites-available/flarum.conf" sudo touch VHOST_CONF sudo echo " DocumentRoot \"/opt/flarum/public\" Options Indexes FollowSymLinks AllowOverride All Require all granted #" | sudo tee -a $VHOST_CONF sudo a2ensite flarum.conf sudo service apache2 reload cd /opt/flarum read -p "Enter your database username: " dbusername # Ask for database password read -sp "Enter your database password: " dbpassword echo # Replace placeholders in the config.php file sudo chown $USER:$USER /opt/flarum sed -i "s/'username' => 'REPLACE_WITH_YOUR_USERNAME'/'username' => '$dbusername'/g" /opt/flarum/config.php sed -i "s/'password' => 'REPLACE_WITH_YOUR_PASSWORD'/'password' => '$dbpassword'/g" /opt/flarum/config.php sudo chown -R www-data:www-data /opt/flarum sudo php flarum assets:publish echo "Flarum installation completed\nYou can now access your flarum forum in Texera" ================================================ FILE: bin/frontend-dev.sh ================================================ # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # (cd frontend && ng serve) ================================================ FILE: bin/frontend-proto-gen.sh ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. TEXERA_HOME="$(git rev-parse --show-toplevel)" GUI_DIR="$TEXERA_HOME/frontend" PROTOBUF_DIR="$TEXERA_HOME/common/workflow-core/src/main/protobuf" GUI_PROTO_DIR="$GUI_DIR/src/app/common/type" WORKFLOW_PROTO=$(find "$PROTOBUF_DIR" -iname "workflow.proto") VIRTUALIDENTITY_PROTO=$(find "$PROTOBUF_DIR" -iname "virtualidentity.proto") protoc --plugin="$GUI_DIR/node_modules/.bin/protoc-gen-ts_proto" \ --ts_proto_out="$GUI_PROTO_DIR/proto" \ -I="$PROTOBUF_DIR" \ "$WORKFLOW_PROTO" \ "$VIRTUALIDENTITY_PROTO" \ --proto_path="$PROTOBUF_DIR" ================================================ FILE: bin/frontend.sh ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. (cd frontend && yarn install && yarn run build) ================================================ FILE: bin/install-nltk.sh ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. unameOut="$(uname -s)" case "${unameOut}" in Linux*) machine=Linux OS=$(lsb_release -si) VER=$(lsb_release -sr) if [ $OS = "Ubuntu" ]; then machine="UBUNTU" sudo apt-get install python3 pip3 install nltk fi ;; Darwin*) machine=Mac brew install python3 pip3 install nltk ;; esac ================================================ FILE: bin/k8s/Chart.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: v2 name: texera-helm description: A Helm chart for Kubernetes # A chart can be either an 'application' or a 'library' chart. # # Application charts are a collection of templates that can be packaged into versioned archives # to be deployed. # # Library charts provide useful utilities or functions for the chart developer. They're included as # a dependency of application charts to inject those utilities and functions into the rendering # pipeline. Library charts do not define any templates and therefore cannot be deployed. type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) version: 1.0.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. appVersion: "1.16.0" dependencies: - name: postgresql version: 16.5.6 repository: https://charts.bitnami.com/bitnami - name: minio version: 15.0.7 repository: https://charts.bitnami.com/bitnami - name: lakefs version: 1.8.1 repository: https://charts.lakefs.io - name: gateway-helm version: 1.6.3 repository: oci://docker.io/envoyproxy alias: envoy-gateway - name: metrics-server version: 3.12.2 repository: https://kubernetes-sigs.github.io/metrics-server/ condition: metrics-server.enabled ================================================ FILE: bin/k8s/README.md ================================================ # Kubernetes Deployment Refer to https://github.com/Texera/texera/wiki/Install-Texera for details. ================================================ FILE: bin/k8s/templates/access-control-service-deployment.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: apps/v1 kind: Deployment metadata: name: {{.Release.Name}}-{{ .Values.accessControlService.name }} namespace: {{ .Release.Namespace }} labels: app: {{ .Release.Name }}-{{ .Values.accessControlService.name }} spec: replicas: {{ .Values.accessControlService.numOfPods | default 1 }} selector: matchLabels: app: {{ .Release.Name }}-{{ .Values.accessControlService.name }} template: metadata: labels: app: {{ .Release.Name }}-{{ .Values.accessControlService.name }} spec: containers: - name: {{ .Values.accessControlService.name }} image: {{ .Values.texera.imageRegistry }}/{{ .Values.accessControlService.imageName }}:{{ .Values.texera.imageTag }} imagePullPolicy: {{ .Values.texeraImages.pullPolicy }} ports: - containerPort: {{ .Values.accessControlService.service.port }} env: - name: STORAGE_JDBC_URL value: jdbc:postgresql://{{ .Release.Name }}-postgresql:5432/texera_db?currentSchema=texera_db,public - name: STORAGE_JDBC_PASSWORD valueFrom: secretKeyRef: name: {{ .Release.Name }}-postgresql key: postgres-password - name: KUBERNETES_COMPUTE_UNIT_POOL_NAME value: {{ .Values.workflowComputingUnitPool.name }} - name: KUBERNETES_COMPUTE_UNIT_POOL_NAMESPACE value: {{ .Values.workflowComputingUnitPool.namespace }} {{- range .Values.texeraEnvVars }} - name: {{ .name }} value: "{{ .value }}" {{- end }} livenessProbe: httpGet: path: /api/healthcheck port: {{ .Values.accessControlService.service.port }} initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /api/healthcheck port: {{ .Values.accessControlService.service.port }} initialDelaySeconds: 5 periodSeconds: 5 ================================================ FILE: bin/k8s/templates/access-control-service-service.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: v1 kind: Service metadata: name: {{ .Release.Name }}-{{ .Values.accessControlService.name }}-svc namespace: {{ .Release.Namespace }} spec: type: {{ .Values.accessControlService.service.type }} selector: app: {{ .Release.Name }}-{{ .Values.accessControlService.name }} ports: - protocol: TCP port: {{ .Values.accessControlService.service.port }} targetPort: {{ .Values.accessControlService.service.port }} ================================================ FILE: bin/k8s/templates/config-service-deployment.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name }}-{{ .Values.configService.name }} namespace: {{ .Release.Namespace }} labels: app: {{ .Release.Name }}-{{ .Values.configService.name }} spec: replicas: {{ .Values.configService.numOfPods }} selector: matchLabels: app: {{ .Release.Name }}-{{ .Values.configService.name }} template: metadata: labels: app: {{ .Release.Name }}-{{ .Values.configService.name }} spec: containers: - name: {{ .Values.configService.name }} image: {{ .Values.texera.imageRegistry }}/{{ .Values.configService.imageName }}:{{ .Values.texera.imageTag }} imagePullPolicy: {{ .Values.texeraImages.pullPolicy }} ports: - containerPort: {{ .Values.configService.service.port }} env: # TexeraDB Access - name: STORAGE_JDBC_URL value: jdbc:postgresql://{{ .Release.Name }}-postgresql:5432/texera_db?currentSchema=texera_db,public - name: STORAGE_JDBC_PASSWORD valueFrom: secretKeyRef: name: {{ .Release.Name }}-postgresql key: postgres-password {{- range .Values.texeraEnvVars }} - name: {{ .name }} value: "{{ .value }}" {{- end }} livenessProbe: httpGet: path: /api/healthcheck port: {{ .Values.configService.service.port }} initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /api/healthcheck port: {{ .Values.configService.service.port }} initialDelaySeconds: 5 periodSeconds: 5 ================================================ FILE: bin/k8s/templates/config-service-service.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: v1 kind: Service metadata: name: {{ .Values.configService.name }}-svc namespace: {{ .Release.Namespace }} spec: type: {{ .Values.configService.service.type }} selector: app: {{ .Release.Name }}-{{ .Values.configService.name }} ports: - protocol: TCP port: {{ .Values.configService.service.port }} targetPort: {{ .Values.configService.service.port }} ================================================ FILE: bin/k8s/templates/example-data-loader-job.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. {{- if .Values.exampleDataLoader.enabled }} apiVersion: batch/v1 kind: Job metadata: name: {{ .Release.Name }}-example-data-loader namespace: {{ .Release.Namespace }} spec: backoffLimit: 3 template: metadata: name: {{ .Release.Name }}-example-data-loader spec: restartPolicy: Never containers: - name: example-data-loader image: {{ .Values.texera.imageRegistry }}/{{ .Values.exampleDataLoader.imageName }}:{{ .Values.texera.imageTag }} env: - name: TEXERA_EXAMPLE_USERNAME value: {{ .Values.exampleDataLoader.username }} - name: TEXERA_EXAMPLE_PASSWORD value: {{ .Values.exampleDataLoader.password }} - name: TEXERA_EXAMPLE_DATASET_DIR value: {{ .Values.exampleDataLoader.datasetDir }} - name: TEXERA_EXAMPLE_WORKFLOW_DIR value: {{ .Values.exampleDataLoader.workflowDir }} - name: TEXERA_WEB_APPLICATION_URL value: http://{{ .Values.webserver.name }}-svc:{{ .Values.webserver.service.port }}/api - name: TEXERA_FILE_SERVICE_URL value: http://{{ .Values.fileService.name }}-svc:{{ .Values.fileService.service.port }}/api {{- end }} ================================================ FILE: bin/k8s/templates/external-names.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. {{/* Define a helper template for creating ExternalName services. This template takes three parameters: - name: The name of the service to create - namespace: The namespace where the service should be created - externalName: The fully qualified domain name to redirect to */}} {{- define "external-name-service" -}} apiVersion: v1 kind: Service metadata: name: {{ .name }} namespace: {{ .namespace }} spec: type: ExternalName externalName: {{ .externalName }} {{- end }} {{/* Define namespace variables for better readability */}} {{- $namespace := .Release.Namespace }} {{- $workflowComputingUnitPoolNamespace := .Values.workflowComputingUnitPool.namespace }} {{/* Create ExternalName services in the workflow namespace to allow compute units to access services in the main namespace using the same service names. */}} {{/* File service ExternalName */}} {{- include "external-name-service" (dict "name" (printf "%s-svc" .Values.fileService.name) "namespace" $workflowComputingUnitPoolNamespace "externalName" (printf "%s-svc.%s.svc.cluster.local" .Values.fileService.name $namespace) ) | nindent 0 }} --- {{/* Config service ExternalName */}} {{- include "external-name-service" (dict "name" (printf "%s-svc" .Values.configService.name) "namespace" $workflowComputingUnitPoolNamespace "externalName" (printf "%s-svc.%s.svc.cluster.local" .Values.configService.name $namespace) ) | nindent 0 }} --- {{/* PostgreSQL ExternalName */}} {{- include "external-name-service" (dict "name" (printf "%s-postgresql" .Release.Name) "namespace" $workflowComputingUnitPoolNamespace "externalName" (printf "%s-postgresql.%s.svc.cluster.local" .Release.Name $namespace) ) | nindent 0 }} --- {{/* Webserver ExternalName */}} {{- include "external-name-service" (dict "name" (printf "%s-svc" .Values.webserver.name) "namespace" $workflowComputingUnitPoolNamespace "externalName" (printf "%s-svc.%s.svc.cluster.local" .Values.webserver.name $namespace) ) | nindent 0 }} --- {{/* MinIO ExternalName */}} {{- include "external-name-service" (dict "name" (printf "%s-minio" .Release.Name) "namespace" $workflowComputingUnitPoolNamespace "externalName" (printf "%s-minio.%s.svc.cluster.local" .Release.Name $namespace) ) | nindent 0 }} ================================================ FILE: bin/k8s/templates/file-service-deployment.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name }}-{{ .Values.fileService.name }} namespace: {{ .Release.Namespace }} labels: app: {{ .Release.Name }}-{{ .Values.fileService.name }} spec: replicas: {{ .Values.fileService.numOfPods }} selector: matchLabels: app: {{ .Release.Name }}-{{ .Values.fileService.name }} template: metadata: labels: app: {{ .Release.Name }}-{{ .Values.fileService.name }} spec: containers: - name: {{ .Values.fileService.name }} image: {{ .Values.texera.imageRegistry }}/{{ .Values.fileService.imageName }}:{{ .Values.texera.imageTag }} imagePullPolicy: {{ .Values.texeraImages.pullPolicy }} ports: - containerPort: {{ .Values.fileService.service.port }} env: # LakeFS & S3 Access - name: STORAGE_S3_ENDPOINT value: http://{{ .Release.Name }}-minio:9000 - name: STORAGE_S3_AUTH_USERNAME valueFrom: secretKeyRef: name: {{ .Release.Name }}-minio key: root-user - name: STORAGE_S3_AUTH_PASSWORD valueFrom: secretKeyRef: name: {{ .Release.Name }}-minio key: root-password - name: STORAGE_LAKEFS_ENDPOINT value: http://{{ .Release.Name }}-lakefs:8000/api/v1 - name: STORAGE_LAKEFS_AUTH_USERNAME valueFrom: secretKeyRef: name: {{ .Release.Name }}-lakefs-secret key: access_key - name: STORAGE_LAKEFS_AUTH_PASSWORD valueFrom: secretKeyRef: name: {{ .Release.Name }}-lakefs-secret key: secret_key # TexeraDB Access - name: STORAGE_JDBC_URL value: jdbc:postgresql://{{ .Release.Name }}-postgresql:5432/texera_db?currentSchema=texera_db,public - name: STORAGE_JDBC_PASSWORD valueFrom: secretKeyRef: name: {{ .Release.Name }}-postgresql key: postgres-password {{- range .Values.texeraEnvVars }} - name: {{ .name }} value: "{{ .value }}" {{- end }} ================================================ FILE: bin/k8s/templates/file-service-service.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: v1 kind: Service metadata: name: {{ .Values.fileService.name }}-svc namespace: {{ .Release.Namespace }} spec: type: {{ .Values.fileService.service.type }} selector: app: {{ .Release.Name }}-{{ .Values.fileService.name }} ports: - name: api-port protocol: TCP port: {{ .Values.fileService.service.port }} targetPort: {{ .Values.fileService.service.port }} # if service type is set to NodePort, include nodePort attribute {{- if eq .Values.fileService.service.type "NodePort" }} nodePort: {{ .Values.fileService.service.nodePort }} {{- end }} ================================================ FILE: bin/k8s/templates/gateway-backend.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: gateway.envoyproxy.io/v1alpha1 kind: Backend metadata: name: {{ .Release.Name }}-dynamic-backend namespace: {{ .Release.Namespace }} spec: type: DynamicResolver ================================================ FILE: bin/k8s/templates/gateway-routes.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: {{ .Release.Name }}-static-routes namespace: {{ .Release.Namespace }} spec: parentRefs: - name: {{ .Release.Name }}-gateway {{- if and .Values.gatewayConfig .Values.gatewayConfig.hostname }} hostnames: - {{ .Values.gatewayConfig.hostname }} {{- end }} rules: - matches: - path: type: PathPrefix value: /api/computing-unit backendRefs: - name: workflow-computing-unit-manager-svc port: 8888 - matches: - path: type: PathPrefix value: /api/compile backendRefs: - name: workflow-compiling-service-svc port: 9090 - matches: - path: type: PathPrefix value: /api/dataset - path: type: PathPrefix value: /api/access/dataset backendRefs: - name: file-service-svc port: 9092 - matches: - path: type: PathPrefix value: /api/access/computing-unit backendRefs: - name: workflow-computing-unit-manager-svc port: 8888 - matches: - path: type: PathPrefix value: /api/config backendRefs: - name: config-service-svc port: 9094 - matches: - path: type: PathPrefix value: /rtc backendRefs: - name: y-websocket-server-svc port: 1234 - matches: - path: type: PathPrefix value: /python-language-server backendRefs: - name: python-language-server-svc port: 3000 - matches: - path: type: PathPrefix value: /api - path: type: PathPrefix value: / backendRefs: - name: webserver-svc port: 8080 --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: {{ .Release.Name }}-dynamic-routes namespace: {{ .Release.Namespace }} spec: parentRefs: - name: {{ .Release.Name }}-gateway {{- if and .Values.gatewayConfig .Values.gatewayConfig.hostname }} hostnames: - {{ .Values.gatewayConfig.hostname }} {{- end }} rules: - matches: - path: type: PathPrefix value: /wsapi - path: type: RegularExpression value: "^/api/executions/\\d+/stats/\\d+$" - path: type: PathPrefix value: /api/executions/result/export backendRefs: - group: gateway.envoyproxy.io kind: Backend name: texera-dynamic-backend --- # MinIO Route {{- if .Values.minio.gateway.enabled }} apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: texera-minio-route namespace: {{ .Release.Namespace }} spec: parentRefs: - name: {{ .Release.Name }}-gateway hostnames: - {{ .Values.minio.gateway.hostname }} rules: - matches: - path: type: PathPrefix value: / backendRefs: - name: {{ .Release.Name }}-minio port: 9000 {{- end }} ================================================ FILE: bin/k8s/templates/gateway-security-policy.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: gateway.envoyproxy.io/v1alpha1 kind: SecurityPolicy metadata: name: {{ .Release.Name }}-ext-auth namespace: {{ .Release.Namespace }} spec: targetRefs: - group: gateway.networking.k8s.io kind: HTTPRoute name: {{ .Release.Name }}-dynamic-routes extAuth: http: backendRefs: - name: {{ .Release.Name }}-{{ .Values.accessControlService.name }}-svc port: {{ .Values.accessControlService.service.port }} path: /api/auth headersToBackend: - x-user-computing-unit-access - x-user-id - x-user-name - x-user-email - Host ================================================ FILE: bin/k8s/templates/gateway.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: gateway.networking.k8s.io/v1 kind: GatewayClass metadata: name: eg spec: controllerName: gateway.envoyproxy.io/gatewayclass-controller --- apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: {{ .Release.Name }}-gateway annotations: {{- if and .Values.gatewayConfig .Values.gatewayConfig.issuer }} {{- if eq (default "Issuer" .Values.gatewayConfig.issuerKind) "ClusterIssuer" }} cert-manager.io/cluster-issuer: {{ .Values.gatewayConfig.issuer }} {{- else }} cert-manager.io/issuer: {{ .Values.gatewayConfig.issuer }} {{- end }} {{- end }} spec: gatewayClassName: eg listeners: - name: http protocol: HTTP port: 80 {{- if and .Values.gatewayConfig .Values.gatewayConfig.hostname }} hostname: {{ .Values.gatewayConfig.hostname }} {{- end }} allowedRoutes: namespaces: from: Same - name: https protocol: HTTPS port: 443 {{- if and .Values.gatewayConfig .Values.gatewayConfig.hostname }} hostname: {{ .Values.gatewayConfig.hostname }} {{- end }} allowedRoutes: namespaces: from: Same tls: mode: Terminate certificateRefs: - name: {{ (index .Values "gatewayConfig" "tlsSecretName") | default (printf "%s-cert" .Release.Name) }} {{- if .Values.minio.gateway.enabled }} - name: minio-http protocol: HTTP port: 80 hostname: {{ .Values.minio.gateway.hostname }} allowedRoutes: namespaces: from: Same - name: minio-https protocol: HTTPS port: 443 hostname: {{ .Values.minio.gateway.hostname }} allowedRoutes: namespaces: from: Same tls: mode: Terminate certificateRefs: - name: {{ .Values.minio.gateway.tlsSecretName | default (printf "%s-minio-tls" .Release.Name) }} {{- end }} ================================================ FILE: bin/k8s/templates/lakefs-secret.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: v1 kind: Secret metadata: name: {{ .Release.Name }}-lakefs-secret namespace: {{ .Release.Namespace }} type: Opaque stringData: username: {{ .Values.lakefs.auth.username | quote }} access_key: {{ .Values.lakefs.auth.accessKey | quote }} secret_key: {{ .Values.lakefs.auth.secretKey | quote }} ================================================ FILE: bin/k8s/templates/lakefs-setup-job.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: batch/v1 kind: Job metadata: name: {{ .Release.Name }}-lakefs-setup-job namespace: {{ .Release.Namespace }} spec: backoffLimit: 3 template: metadata: name: {{ .Release.Name }}-lakefs-setup-job spec: restartPolicy: Never containers: - name: setup-lakefs image: curlimages/curl:latest env: - name: STORAGE_LAKEFS_ENDPOINT value: http://{{ .Release.Name }}-lakefs:8000/api/v1 - name: STORAGE_LAKEFS_AUTH_USERNAME valueFrom: secretKeyRef: name: {{ .Release.Name }}-lakefs-secret key: access_key - name: STORAGE_LAKEFS_AUTH_PASSWORD valueFrom: secretKeyRef: name: {{ .Release.Name }}-lakefs-secret key: secret_key command: - /bin/sh - -c - | set -e echo "Waiting for LakeFS healthcheck..." while true; do code=$(curl -s -o /dev/null -w "%{http_code}" "$STORAGE_LAKEFS_ENDPOINT/healthcheck") || true echo "Healthcheck status: $code" if [ "$code" = "200" ] || [ "$code" = "204" ]; then echo "LakeFS is healthy!" break fi sleep 5 done echo "Sending GET /setup_lakefs to check existing state..." curl -s -w "\nStatus: %{http_code}\n" "$STORAGE_LAKEFS_ENDPOINT/setup_lakefs" || echo "GET failed" echo "Sending POST /setup_lakefs..." curl -s -w "\nStatus: %{http_code}\n" -X POST \ -H "Content-Type: application/json" \ -d '{ "username": "texera-admin", "key": { "access_key_id": "'"${STORAGE_LAKEFS_AUTH_USERNAME}"'", "secret_access_key": "'"${STORAGE_LAKEFS_AUTH_PASSWORD}"'" } }' \ "${STORAGE_LAKEFS_ENDPOINT}/setup_lakefs" ================================================ FILE: bin/k8s/templates/minio-persistence.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. {{/* Define storage path configuration, please change it to your own path and make sure the path exists with the right permission*/}} {{/* This path only works for local-path storage class */}} {{- $hostBasePath := .Values.persistence.minioHostLocalPath }} {{- if .Values.minio.persistence.enabled }} {{- $name := "minio" }} {{- $persistence := .Values.minio.persistence }} {{- $volumeName := printf "%s-data-pv" $name }} {{- $claimName := printf "%s-data-pvc" $name }} {{- $storageClass := $persistence.storageClass | default "local-path" }} {{- $size := $persistence.size | default "20Gi" }} {{- $hostPath := printf "%s/%s/%s" $hostBasePath $.Release.Name $name }} {{/* Only create PV for local-path storage class */}} {{- if and (eq $storageClass "local-path") (ne $hostBasePath "") }} apiVersion: v1 kind: PersistentVolume metadata: name: {{ $volumeName }} {{- if not $.Values.persistence.removeAfterUninstall }} annotations: "helm.sh/resource-policy": keep {{- end }} labels: type: local app: {{ $.Release.Name }} component: {{ $name }} spec: storageClassName: {{ $storageClass }} capacity: storage: {{ $size }} accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain hostPath: path: {{ $hostPath }} --- {{- end }} apiVersion: v1 kind: PersistentVolumeClaim metadata: name: {{ $claimName }} namespace: {{ $.Release.Namespace }} {{- if not $.Values.persistence.removeAfterUninstall }} annotations: "helm.sh/resource-policy": keep {{- end }} labels: app: {{ $.Release.Name }} component: {{ $name }} spec: storageClassName: {{ $storageClass }} accessModes: - ReadWriteOnce resources: requests: storage: {{ $size }} {{- if and (eq $storageClass "local-path") (ne $hostBasePath "") }} volumeName: {{ $volumeName }} {{- end }} {{- end }} ================================================ FILE: bin/k8s/templates/postgresql-init-script-config.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: v1 kind: ConfigMap metadata: name: {{ .Values.postgresql.primary.initdb.scriptsConfigMap }} namespace: {{ .Release.Namespace }} labels: app: {{ .Release.Name }} data: init.sh: | #!/bin/bash echo "Running Texera schema initialization..." export PGPASSWORD=$POSTGRES_PASSWORD # Execute SQL files in order echo "Initializing LakeFS database..." cat <<'EOF' > /tmp/texera_lakefs.sql {{ .Files.Get "files/texera_lakefs.sql" | indent 6 }} EOF psql -U postgres -f /tmp/texera_lakefs.sql echo "Initializing Iceberg catalog database..." cat <<'EOF' > /tmp/iceberg_postgres_catalog.sql {{ .Files.Get "files/iceberg_postgres_catalog.sql" | indent 6 }} EOF psql -U postgres -f /tmp/iceberg_postgres_catalog.sql echo "Initializing Texera database..." cat <<'EOF' > /tmp/texera_ddl.sql {{ .Files.Get "files/texera_ddl.sql" | indent 6 }} EOF psql -U postgres -f /tmp/texera_ddl.sql echo "Schema initialization complete." ================================================ FILE: bin/k8s/templates/postgresql-persistence.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. {{/* Define storage path configuration, please change it to your own path and make sure the path exists with the right permission*/}} {{/* This path only works for local-path storage class */}} {{- $hostBasePath := .Values.persistence.postgresqlHostLocalPath }} {{- if .Values.postgresql.primary.persistence.enabled }} {{- $name := "postgresql" }} {{- $persistence := .Values.postgresql.primary.persistence }} {{- $volumeName := printf "%s-data-pv" $name }} {{- $claimName := printf "%s-data-pvc" $name }} {{- $storageClass := $persistence.storageClass | default "local-path" }} {{- $size := $persistence.size | default "10Gi" }} {{- $hostPath := printf "%s/%s/%s" $hostBasePath $.Release.Name $name }} {{/* Only create PV for local-path storage class */}} {{- if and (eq $storageClass "local-path") (ne $hostBasePath "") }} apiVersion: v1 kind: PersistentVolume metadata: name: {{ $volumeName }} {{- if not $.Values.persistence.removeAfterUninstall }} annotations: "helm.sh/resource-policy": keep {{- end }} labels: type: local app: {{ $.Release.Name }} component: {{ $name }} spec: storageClassName: {{ $storageClass }} capacity: storage: {{ $size }} accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain hostPath: path: {{ $hostPath }} --- {{- end }} apiVersion: v1 kind: PersistentVolumeClaim metadata: name: {{ $claimName }} namespace: {{ $.Release.Namespace }} {{- if not $.Values.persistence.removeAfterUninstall }} annotations: "helm.sh/resource-policy": keep {{- end }} labels: app: {{ $.Release.Name }} component: {{ $name }} spec: storageClassName: {{ $storageClass }} accessModes: - ReadWriteOnce resources: requests: storage: {{ $size }} {{- if and (eq $storageClass "local-path") (ne $hostBasePath "") }} volumeName: {{ $volumeName }} {{- end }} {{- end }} ================================================ FILE: bin/k8s/templates/pylsp.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name }}-{{ .Values.pythonLanguageServer.name }} namespace: {{ .Release.Namespace }} labels: app: {{ .Release.Name }}-{{ .Values.pythonLanguageServer.name }} spec: replicas: {{ .Values.pythonLanguageServer.replicaCount }} selector: matchLabels: app: {{ .Release.Name }}-{{ .Values.pythonLanguageServer.name }} template: metadata: labels: app: {{ .Release.Name }}-{{ .Values.pythonLanguageServer.name }} spec: containers: - name: {{ .Values.pythonLanguageServer.name }} image: {{ .Values.pythonLanguageServer.image | quote }} imagePullPolicy: Always ports: - containerPort: 3000 resources: limits: cpu: {{ .Values.pythonLanguageServer.resources.limits.cpu | quote }} memory: {{ .Values.pythonLanguageServer.resources.limits.memory | quote }} imagePullSecrets: - name: {{ .Values.pythonLanguageServer.imagePullSecret }} --- apiVersion: v1 kind: Service metadata: name: {{ .Values.pythonLanguageServer.name }}-svc namespace: {{ .Release.Namespace }} labels: app: {{ .Release.Name }}-{{ .Values.pythonLanguageServer.name }} spec: selector: app: {{ .Release.Name }}-{{ .Values.pythonLanguageServer.name }} ports: - protocol: TCP port: 3000 targetPort: 3000 ================================================ FILE: bin/k8s/templates/shared-editing-server.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name }}-{{ .Values.yWebsocketServer.name }} namespace: {{ .Release.Namespace }} labels: app: {{ .Release.Name }}-{{ .Values.yWebsocketServer.name }} spec: replicas: {{ .Values.yWebsocketServer.replicaCount }} selector: matchLabels: app: {{ .Release.Name }}-{{ .Values.yWebsocketServer.name }} template: metadata: labels: app: {{ .Release.Name }}-{{ .Values.yWebsocketServer.name }} spec: containers: - name: {{ .Values.yWebsocketServer.name }} image: {{ .Values.yWebsocketServer.image | quote }} imagePullPolicy: Always ports: - containerPort: 1234 --- apiVersion: v1 kind: Service metadata: name: {{ .Values.yWebsocketServer.name }}-svc namespace: {{ .Release.Namespace }} labels: app: {{ .Release.Name }}-{{ .Values.yWebsocketServer.name }} spec: selector: app: {{ .Release.Name }}-{{ .Values.yWebsocketServer.name }} ports: - protocol: TCP port: 1234 targetPort: 1234 ================================================ FILE: bin/k8s/templates/webserver-deployment.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name }}-{{ .Values.webserver.name }} namespace: {{ .Release.Namespace }} labels: app: {{ .Release.Name }}-{{ .Values.webserver.name }} spec: replicas: {{ .Values.webserver.numOfPods }} selector: matchLabels: app: {{ .Release.Name }}-{{ .Values.webserver.name }} template: metadata: labels: app: {{ .Release.Name }}-{{ .Values.webserver.name }} spec: containers: - name: {{ .Values.webserver.name }} image: {{ .Values.texera.imageRegistry }}/{{ .Values.webserver.imageName }}:{{ .Values.texera.imageTag }} imagePullPolicy: {{ .Values.texeraImages.pullPolicy }} ports: - containerPort: {{ .Values.webserver.service.port }} env: # TexeraDB Access - name: STORAGE_JDBC_URL value: jdbc:postgresql://{{ .Release.Name }}-postgresql:5432/texera_db?currentSchema=texera_db,public - name: STORAGE_JDBC_PASSWORD valueFrom: secretKeyRef: name: {{ .Release.Name }}-postgresql key: postgres-password # LakeFS Access (should be removed in production environment) - name: STORAGE_LAKEFS_ENDPOINT value: http://{{ .Release.Name }}-lakefs.{{ .Release.Namespace }}:8000/api/v1 - name: STORAGE_LAKEFS_AUTH_USERNAME valueFrom: secretKeyRef: name: {{ .Release.Name }}-lakefs-secret key: access_key - name: STORAGE_LAKEFS_AUTH_PASSWORD valueFrom: secretKeyRef: name: {{ .Release.Name }}-lakefs-secret key: secret_key {{- range .Values.texeraEnvVars }} - name: {{ .name }} value: "{{ .value }}" {{- end }} ================================================ FILE: bin/k8s/templates/webserver-service.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: v1 kind: Service metadata: name: {{ .Values.webserver.name }}-svc namespace: {{ .Release.Namespace }} spec: type: {{ .Values.webserver.service.type }} selector: app: {{ .Release.Name }}-{{ .Values.webserver.name }} ports: - name: api-port protocol: TCP port: {{ .Values.webserver.service.port }} targetPort: {{ .Values.webserver.service.port }} # if service type is set to NodePort, include nodePort attribute {{- if eq .Values.webserver.service.type "NodePort" }} nodePort: {{ .Values.webserver.service.nodePort }} {{- end }} ================================================ FILE: bin/k8s/templates/workflow-compiling-service-deployment.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name }}-{{ .Values.workflowCompilingService.name }} namespace: {{ .Release.Namespace }} labels: app: {{ .Release.Name }}-{{ .Values.workflowCompilingService.name }} spec: replicas: {{ .Values.workflowCompilingService.numOfPods }} selector: matchLabels: app: {{ .Release.Name }}-{{ .Values.workflowCompilingService.name }} template: metadata: labels: app: {{ .Release.Name }}-{{ .Values.workflowCompilingService.name }} spec: containers: - name: {{ .Values.workflowCompilingService.name }} image: {{ .Values.texera.imageRegistry }}/{{ .Values.workflowCompilingService.imageName }}:{{ .Values.texera.imageTag }} imagePullPolicy: {{ .Values.texeraImages.pullPolicy }} ports: - containerPort: {{ .Values.workflowCompilingService.service.port }} env: # FileService Access - name: FILE_SERVICE_GET_PRESIGNED_URL_ENDPOINT value: http://{{ .Values.fileService.name }}-svc:9092/api/dataset/presign-download # LakeFS Access - name: STORAGE_LAKEFS_ENDPOINT value: http://{{ .Release.Name }}-lakefs:8000/api/v1 - name: STORAGE_LAKEFS_AUTH_USERNAME valueFrom: secretKeyRef: name: {{ .Release.Name }}-lakefs-secret key: access_key - name: STORAGE_LAKEFS_AUTH_PASSWORD valueFrom: secretKeyRef: name: {{ .Release.Name }}-lakefs-secret key: secret_key # TexeraDB Access - name: STORAGE_JDBC_URL value: jdbc:postgresql://{{ .Release.Name }}-postgresql:5432/texera_db?currentSchema=texera_db,public - name: STORAGE_JDBC_PASSWORD valueFrom: secretKeyRef: name: {{ .Release.Name }}-postgresql key: postgres-password {{- range .Values.texeraEnvVars }} - name: {{ .name }} value: "{{ .value }}" {{- end }} ================================================ FILE: bin/k8s/templates/workflow-compiling-service-service.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: v1 kind: Service metadata: name: {{ .Values.workflowCompilingService.name }}-svc namespace: {{ .Release.Namespace }} spec: type: {{ .Values.workflowCompilingService.service.type }} selector: app: {{ .Release.Name }}-{{ .Values.workflowCompilingService.name }} ports: - name: api-port protocol: TCP port: {{ .Values.workflowCompilingService.service.port }} targetPort: {{ .Values.workflowCompilingService.service.port }} # if service type is set to NodePort, include nodePort attribute {{- if eq .Values.workflowCompilingService.service.type "NodePort" }} nodePort: {{ .Values.workflowCompilingService.service.nodePort }} {{- end }} ================================================ FILE: bin/k8s/templates/workflow-computing-unit-manager-deployment.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name }}-{{ .Values.workflowComputingUnitManager.name }} namespace: {{ .Release.Namespace }} labels: app: {{ .Release.Name }}-{{ .Values.workflowComputingUnitManager.name }} spec: replicas: {{ .Values.workflowComputingUnitManager.numOfPods }} selector: matchLabels: app: {{ .Release.Name }}-{{ .Values.workflowComputingUnitManager.name }} template: metadata: labels: app: {{ .Release.Name }}-{{ .Values.workflowComputingUnitManager.name }} spec: serviceAccountName: {{ .Values.workflowComputingUnitManager.serviceAccountName }} containers: - name: {{ .Values.workflowComputingUnitManager.name }} image: {{ .Values.texera.imageRegistry }}/{{ .Values.workflowComputingUnitManager.imageName }}:{{ .Values.texera.imageTag }} imagePullPolicy: {{ .Values.texeraImages.pullPolicy }} ports: - containerPort: {{ .Values.workflowComputingUnitManager.service.port }} env: # Kubernetes related variables - name: KUBERNETES_COMPUTE_UNIT_POOL_NAMESPACE value: {{ .Values.workflowComputingUnitPool.namespace }} - name: KUBERNETES_COMPUTE_UNIT_SERVICE_NAME value: {{ .Values.workflowComputingUnitPool.name }}-svc - name: KUBERNETES_IMAGE_NAME value: {{ .Values.texera.imageRegistry }}/{{ .Values.workflowComputingUnitPool.imageName }}:{{ .Values.texera.imageTag }} # TexeraDB Access - name: STORAGE_JDBC_URL value: jdbc:postgresql://{{ .Release.Name }}-postgresql:5432/texera_db?currentSchema=texera_db,public - name: STORAGE_JDBC_PASSWORD valueFrom: secretKeyRef: name: {{ .Release.Name }}-postgresql key: postgres-password # FileService Access - name: FILE_SERVICE_GET_PRESIGNED_URL_ENDPOINT value: http://{{ .Values.fileService.name }}-svc:9092/api/dataset/presign-download - name: FILE_SERVICE_UPLOAD_ONE_FILE_TO_DATASET_ENDPOINT value: http://{{ .Values.fileService.name }}-svc:9092/api/dataset/did/upload # S3 Access (for R UDF large binary support) - name: STORAGE_S3_ENDPOINT value: http://{{ .Release.Name }}-minio:9000 - name: STORAGE_S3_AUTH_USERNAME valueFrom: secretKeyRef: name: {{ .Release.Name }}-minio key: root-user - name: STORAGE_S3_AUTH_PASSWORD valueFrom: secretKeyRef: name: {{ .Release.Name }}-minio key: root-password # LakeFS Access (should be removed in production environment) - name: STORAGE_LAKEFS_ENDPOINT value: http://{{ .Release.Name }}-lakefs.{{ .Release.Namespace }}:8000/api/v1 - name: STORAGE_LAKEFS_AUTH_USERNAME valueFrom: secretKeyRef: name: {{ .Release.Name }}-lakefs-secret key: access_key - name: STORAGE_LAKEFS_AUTH_PASSWORD valueFrom: secretKeyRef: name: {{ .Release.Name }}-lakefs-secret key: secret_key # Workflow Result - name: STORAGE_ICEBERG_CATALOG_TYPE value: postgres - name: STORAGE_ICEBERG_CATALOG_POSTGRES_URI_WITHOUT_SCHEME value: {{ .Release.Name }}-postgresql:5432/texera_iceberg_catalog - name: STORAGE_ICEBERG_CATALOG_POSTGRES_USERNAME value: postgres - name: STORAGE_ICEBERG_CATALOG_POSTGRES_PASSWORD valueFrom: secretKeyRef: name: {{ .Release.Name }}-postgresql key: postgres-password {{- range .Values.texeraEnvVars }} - name: {{ .name }} value: "{{ .value }}" {{- end }} ================================================ FILE: bin/k8s/templates/workflow-computing-unit-manager-service-account.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: v1 kind: ServiceAccount metadata: name: {{ .Values.workflowComputingUnitManager.serviceAccountName }} namespace: {{ .Release.Namespace }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: {{ .Values.workflowComputingUnitManager.name }} namespace: {{ .Values.workflowComputingUnitPool.namespace }} rules: - apiGroups: [""] resources: ["pods"] verbs: ["get", "list", "watch", "create", "delete"] - apiGroups: ["metrics.k8s.io"] # Added metrics permissions resources: ["pods"] verbs: ["list", "get"] # Added metrics permissions --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: {{ .Values.workflowComputingUnitManager.name }}-binding namespace: {{ .Values.workflowComputingUnitPool.namespace }} subjects: - kind: ServiceAccount name: {{ .Values.workflowComputingUnitManager.serviceAccountName }} namespace: {{ .Release.Namespace }} roleRef: kind: Role name: {{ .Values.workflowComputingUnitManager.name }} apiGroup: rbac.authorization.k8s.io ================================================ FILE: bin/k8s/templates/workflow-computing-unit-manager-service.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: v1 kind: Service metadata: name: {{ .Values.workflowComputingUnitManager.name }}-svc namespace: {{ .Release.Namespace }} spec: type: {{ .Values.workflowComputingUnitManager.service.type }} selector: app: {{ .Release.Name }}-{{ .Values.workflowComputingUnitManager.name }} ports: - protocol: TCP port: {{ .Values.workflowComputingUnitManager.service.port }} targetPort: {{ .Values.workflowComputingUnitManager.service.port }} # if service type is set to NodePort, include nodePort attribute {{- if eq .Values.workflowComputingUnitManager.service.type "NodePort" }} nodePort: {{ .Values.workflowComputingUnitManager.service.nodePort }} {{- end }} ================================================ FILE: bin/k8s/templates/workflow-computing-unit-master-prepull-daemonset.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: apps/v1 kind: DaemonSet metadata: name: {{ .Release.Name }}-computing-unit-master-prepuller namespace: {{ .Release.Namespace }} labels: app: {{ .Release.Name }}-computing-unit-master-prepuller spec: selector: matchLabels: app: {{ .Release.Name }}-computing-unit-master-prepuller template: metadata: labels: app: {{ .Release.Name }}-computing-unit-master-prepuller spec: restartPolicy: Always tolerations: - operator: "Exists" initContainers: - name: prepuller image: {{ .Values.texera.imageRegistry }}/{{ .Values.workflowComputingUnitPool.imageName }}:{{ .Values.texera.imageTag }} imagePullPolicy: Always command: ["sh", "-c", "true"] containers: - name: pause image: gcr.io/google_containers/pause:3.2 resources: limits: cpu: 1m memory: 8Mi requests: cpu: 1m memory: 8Mi ================================================ FILE: bin/k8s/templates/workflow-computing-unit-resource-quota.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. {{- if and .Values.workflowComputingUnitPool.createNamespaces .Values.workflowComputingUnitPool.maxRequestedResources }} apiVersion: v1 kind: ResourceQuota metadata: name: {{ .Values.workflowComputingUnitPool.name }}-resource-quota namespace: {{ .Values.workflowComputingUnitPool.namespace }} spec: hard: requests.cpu: "{{ .Values.workflowComputingUnitPool.maxRequestedResources.cpu }}" requests.memory: {{ .Values.workflowComputingUnitPool.maxRequestedResources.memory }} {{- if .Values.workflowComputingUnitPool.maxRequestedResources.nvidiaGpu }} requests.nvidia.com/gpu: "{{ .Values.workflowComputingUnitPool.maxRequestedResources.nvidiaGpu }}" {{- end }} {{- end }} ================================================ FILE: bin/k8s/templates/workflow-computing-units-namespace.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. {{- if .Values.workflowComputingUnitPool.createNamespaces }} apiVersion: v1 kind: Namespace metadata: name: {{ .Values.workflowComputingUnitPool.namespace }} {{- end }} ================================================ FILE: bin/k8s/templates/workflow-computing-units-service.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: v1 kind: Service metadata: name: {{ .Values.workflowComputingUnitPool.name }}-svc namespace: {{ .Values.workflowComputingUnitPool.namespace }} spec: clusterIP: None selector: type: computing-unit # TODO: consider change this ports: - protocol: TCP port: {{ .Values.workflowComputingUnitPool.service.port }} targetPort: {{ .Values.workflowComputingUnitPool.service.targetPort }} ================================================ FILE: bin/k8s/values-development.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. texera: # Container image registry and tag for all Texera services # Override these to use a different registry or version imageRegistry: ghcr.io/apache imageTag: latest global: # Required by Bitnami sub-charts (postgresql, minio) to allow custom images security: allowInsecureImages: true # Persistence Configuration # This controls how Persistent Volumes (PVs) and Persistent Volume Claims (PVCs) are managed # # removeAfterUninstall: # - true: PVCs will be deleted when helm uninstalls the chart # - false: PVCs will remain after uninstall to preserve the data persistence: removeAfterUninstall: true minioHostLocalPath: "" postgresqlHostLocalPath: "" # Part 1: the configuration of Postgres, Minio and LakeFS postgresql: image: repository: groonga/pgroonga tag: latest debug: true auth: postgresPassword: root_password # for executing init script with superuser primary: containerSecurityContext: # Disabled because groonga/pgroonga needs to write a lock/socket file to /var/run/postgresql readOnlyRootFilesystem: false livenessProbe: initialDelaySeconds: 30 # increase this if the launching of postgresql is slow on the cluster readinessProbe: initialDelaySeconds: 30 # increase this if the launching of postgresql is slow on the cluster resources: requests: cpu: "0.25" memory: "256Mi" limits: cpu: "1" memory: "256Mi" persistence: enabled: true size: 10Gi storageClass: local-path existingClaim: "postgresql-data-pvc" initdb: scriptsConfigMap: "postgresql-init-script" minio: mode: standalone image: repository: bitnamilegacy/minio tag: 2025.3.12-debian-12-r0 resources: requests: memory: "256Mi" limits: memory: "256Mi" gateway: enabled: false hostname: "" # the url for the minio, e.g. "minio.example.com" tlsSecretName: "" # e.g. "minio-tls-secret" auth: rootUser: texera_minio rootPassword: password service: # In production, use ClusterIP to avoid exposing the minio to the internet # type: ClusterIP type: NodePort nodePorts: api: 31000 persistence: enabled: true size: 20Gi storageClass: local-path existingClaim: "minio-data-pvc" lakefs: secrets: authEncryptSecretKey: random_string_for_lakefs databaseConnectionString: postgres://postgres:root_password@texera-postgresql:5432/texera_lakefs?sslmode=disable auth: username: texera-admin accessKey: AKIAIOSFOLKFSSAMPLES secretKey: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY service: port: 8000 lakefsConfig: | database: type: postgres blockstore: type: s3 s3: endpoint: http://texera-minio:9000 pre_signed_expiry: 15m pre_signed_endpoint: http://localhost:31000 force_path_style: true credentials: access_key_id: texera_minio secret_access_key: password # Part2: configurations of Texera-related micro services texeraImages: pullPolicy: Always # Example data loader configuration exampleDataLoader: enabled: true imageName: texera-example-data-loader username: texera password: texera datasetDir: datasets workflowDir: workflows webserver: name: webserver numOfPods: 1 # Number of pods for the Texera deployment imageName: texera-dashboard-service service: type: ClusterIP port: 8080 resources: requests: cpu: 10m memory: 256Mi limits: cpu: 1000m memory: 1Gi workflowComputingUnitManager: name: workflow-computing-unit-manager numOfPods: 1 serviceAccountName: workflow-computing-unit-manager-service-account imageName: texera-workflow-computing-unit-managing-service service: type: ClusterIP port: 8888 resources: requests: cpu: 10m memory: 256Mi limits: cpu: 1000m memory: 256Mi workflowCompilingService: name: workflow-compiling-service numOfPods: 1 imageName: texera-workflow-compiling-service service: type: ClusterIP port: 9090 resources: requests: cpu: 10m memory: 256Mi limits: cpu: 1000m memory: 256Mi fileService: name: file-service numOfPods: 1 imageName: texera-file-service service: type: ClusterIP port: 9092 resources: requests: cpu: 10m memory: 256Mi limits: cpu: 1000m memory: 512Mi configService: name: config-service numOfPods: 1 imageName: texera-config-service service: type: ClusterIP port: 9094 resources: requests: cpu: 10m memory: 256Mi limits: cpu: 1000m memory: 256Mi accessControlService: name: access-control-service numOfPods: 1 imageName: texera-access-control-service service: type: ClusterIP port: 9096 resources: requests: cpu: 10m memory: 256Mi limits: cpu: 1000m memory: 256Mi # headless service for the access of computing units workflowComputingUnitPool: createNamespaces: true # The name of the workflow computing unit pool name: texera-workflow-computing-unit # Note: the namespace of the workflow computing unit pool might conflict when there are multiple texera deployments in the same cluster namespace: texera-workflow-computing-unit-pool # Max number of resources allocated for computing units maxRequestedResources: cpu: 100 memory: 100Gi nvidiaGpu: 5 imageName: texera-workflow-execution-coordinator service: port: 8085 targetPort: 8085 texeraEnvVars: - name: USER_SYS_ADMIN_USERNAME value: "texera" - name: USER_SYS_ADMIN_PASSWORD value: "texera" - name: STORAGE_JDBC_USERNAME value: postgres - name: USER_SYS_ENABLED value: "true" - name: SCHEDULE_GENERATOR_ENABLE_COST_BASED_SCHEDULE_GENERATOR value: "true" - name: MAX_WORKFLOW_WEBSOCKET_REQUEST_PAYLOAD_SIZE_KB value: "64" - name: MAX_NUM_OF_RUNNING_COMPUTING_UNITS_PER_USER value: "10" - name: KUBERNETES_COMPUTING_UNIT_CPU_LIMIT_OPTIONS value: "2" - name: KUBERNETES_COMPUTING_UNIT_MEMORY_LIMIT_OPTIONS value: "2Gi" - name: KUBERNETES_COMPUTING_UNIT_GPU_LIMIT_OPTIONS value: "0" - name: COMPUTING_UNIT_LOCAL_ENABLED value: "false" - name: KUBERNETES_COMPUTING_UNIT_ENABLED value: "true" - name: KUBERNETES_IMAGE_PULL_POLICY value: "IfNotPresent" - name: GUI_WORKFLOW_WORKSPACE_PYTHON_LANGUAGE_SERVER_PORT value: "" - name: GUI_WORKFLOW_WORKSPACE_PRODUCTION_SHARED_EDITING_SERVER value: "true" - name: GUI_LOGIN_LOCAL_LOGIN value: "true" - name: GUI_LOGIN_GOOGLE_LOGIN value: "true" - name: GUI_DATASET_SINGLE_FILE_UPLOAD_MAXIMUM_SIZE_MB value: "1024" - name: GUI_WORKFLOW_WORKSPACE_EXPORT_EXECUTION_RESULT_ENABLED value: "true" - name: GUI_WORKFLOW_WORKSPACE_WORKFLOW_EXECUTIONS_TRACKING_ENABLED value: "true" - name: GUI_WORKFLOW_WORKSPACE_ASYNC_RENDERING_ENABLED value: "true" - name: COMPUTING_UNIT_SHARING_ENABLED value: "true" - name: USER_SYS_INVITE_ONLY value: "true" - name: USER_SYS_GOOGLE_CLIENT_ID value: "" - name: USER_SYS_GOOGLE_SMTP_GMAIL value: "" - name: USER_SYS_GOOGLE_SMTP_PASSWORD value: "" - name: USER_SYS_DOMAIN value: "" yWebsocketServer: name: y-websocket-server replicaCount: 1 image: texera/y-websocket-server:latest pythonLanguageServer: name: python-language-server replicaCount: 1 image: texera/pylsp:latest imagePullSecret: regcred resources: limits: cpu: "100m" memory: "100Mi" # Metrics Server configuration metrics-server: enabled: true # set to false if metrics-server is already installed args: - --kubelet-insecure-tls - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname - --metric-resolution=15s resources: requests: cpu: 200m memory: 400Mi rbac: create: true serviceAccount: create: true priorityClassName: system-cluster-critical gatewayConfig: # Routes are available at bin/k8s/templates/gateway-routes.yaml # The hostname for the Gateway listener (HTTP/HTTPS). # e.g., "texera.example.com" hostname: "" # The name of the cert-manager Issuer or ClusterIssuer to use for obtaining certificates. # This requires cert-manager to be installed in the cluster. # You can find available ClusterIssuers with: `kubectl get clusterissuers` # You can find available Issuers with: `kubectl get issuers -A` # e.g., "letsencrypt-prod" issuer: "" # The Kind of the issuer specified above. Can be "Issuer" or "ClusterIssuer". # If you found it via `kubectl get clusterissuers`, use "ClusterIssuer". # If you found it via `kubectl get issuers`, use "Issuer". # defaults to "Issuer" if not specified. issuerKind: "Issuer" # The name of the Secret where the signed certificate should be stored. # If empty, it defaults to "{{ .Release.Name }}-cert". # e.g., "texera-tls" tlsSecretName: "" # Envoy Gateway Configuration envoy-gateway: config: envoyGateway: extensionApis: enableBackend: true enableEnvoyPatchPolicy: true ================================================ FILE: bin/k8s/values.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. texera: # Container image registry and tag for all Texera services # Override these to use a different registry or version imageRegistry: ghcr.io/apache imageTag: latest global: # Required by Bitnami sub-charts (postgresql, minio) to allow custom images security: allowInsecureImages: true # Persistence Configuration # This controls how Persistent Volumes (PVs) and Persistent Volume Claims (PVCs) are managed # # removeAfterUninstall: # - true: PVCs will be deleted when helm uninstalls the chart # - false: PVCs will remain after uninstall to preserve the data persistence: removeAfterUninstall: true minioHostLocalPath: "" postgresqlHostLocalPath: "" # Part 1: the configuration of Postgres, Minio and LakeFS postgresql: image: repository: groonga/pgroonga tag: latest debug: true auth: postgresPassword: root_password # for executing init script with superuser primary: containerSecurityContext: # Disabled because groonga/pgroonga needs to write a lock/socket file to /var/run/postgresql readOnlyRootFilesystem: false livenessProbe: initialDelaySeconds: 30 # increase this if the launching of postgresql is slow on the cluster readinessProbe: initialDelaySeconds: 30 # increase this if the launching of postgresql is slow on the cluster resources: requests: cpu: "1" memory: "1Gi" persistence: enabled: true size: 10Gi storageClass: local-path existingClaim: "postgresql-data-pvc" initdb: scriptsConfigMap: "postgresql-init-script" minio: mode: standalone image: repository: bitnamilegacy/minio tag: 2025.3.12-debian-12-r0 gateway: enabled: false hostname: "" # the url for the minio, e.g. "minio.example.com" tlsSecretName: "" # e.g. "minio-tls-secret" auth: rootUser: texera_minio rootPassword: password service: # In production, use ClusterIP to avoid exposing the minio to the internet # type: ClusterIP type: NodePort nodePorts: api: 31000 persistence: enabled: true size: 20Gi storageClass: local-path existingClaim: "minio-data-pvc" lakefs: secrets: authEncryptSecretKey: random_string_for_lakefs databaseConnectionString: postgres://postgres:root_password@texera-postgresql:5432/texera_lakefs?sslmode=disable auth: username: texera-admin accessKey: AKIAIOSFOLKFSSAMPLES secretKey: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY service: port: 8000 lakefsConfig: | database: type: postgres blockstore: type: s3 s3: endpoint: http://texera-minio:9000 pre_signed_expiry: 15m pre_signed_endpoint: http://localhost:31000 force_path_style: true credentials: access_key_id: texera_minio secret_access_key: password # Part2: configurations of Texera-related micro services texeraImages: pullPolicy: Always # Example data loader configuration exampleDataLoader: enabled: true imageName: texera-example-data-loader username: texera password: texera datasetDir: datasets workflowDir: workflows webserver: name: webserver numOfPods: 1 # Number of pods for the Texera deployment imageName: texera-dashboard-service service: type: ClusterIP port: 8080 workflowComputingUnitManager: name: workflow-computing-unit-manager numOfPods: 1 serviceAccountName: workflow-computing-unit-manager-service-account imageName: texera-workflow-computing-unit-managing-service service: type: ClusterIP port: 8888 workflowCompilingService: name: workflow-compiling-service numOfPods: 1 imageName: texera-workflow-compiling-service service: type: ClusterIP port: 9090 fileService: name: file-service numOfPods: 1 imageName: texera-file-service service: type: ClusterIP port: 9092 configService: name: config-service numOfPods: 1 imageName: texera-config-service service: type: ClusterIP port: 9094 accessControlService: name: access-control-service numOfPods: 1 imageName: texera-access-control-service service: type: ClusterIP port: 9096 # headless service for the access of computing units workflowComputingUnitPool: createNamespaces: true # The name of the workflow computing unit pool name: texera-workflow-computing-unit # Note: the namespace of the workflow computing unit pool might conflict when there are multiple texera deployments in the same cluster namespace: texera-workflow-computing-unit-pool # Max number of resources allocated for computing units maxRequestedResources: cpu: 100 memory: 100Gi nvidiaGpu: 5 imageName: texera-workflow-execution-coordinator service: port: 8085 targetPort: 8085 texeraEnvVars: - name: USER_SYS_ADMIN_USERNAME value: "texera" - name: USER_SYS_ADMIN_PASSWORD value: "texera" - name: STORAGE_JDBC_USERNAME value: postgres - name: USER_SYS_ENABLED value: "true" - name: SCHEDULE_GENERATOR_ENABLE_COST_BASED_SCHEDULE_GENERATOR value: "true" - name: MAX_WORKFLOW_WEBSOCKET_REQUEST_PAYLOAD_SIZE_KB value: "64" - name: MAX_NUM_OF_RUNNING_COMPUTING_UNITS_PER_USER value: "10" - name: KUBERNETES_COMPUTING_UNIT_CPU_LIMIT_OPTIONS value: "2" - name: KUBERNETES_COMPUTING_UNIT_MEMORY_LIMIT_OPTIONS value: "2Gi" - name: KUBERNETES_COMPUTING_UNIT_GPU_LIMIT_OPTIONS value: "0" - name: COMPUTING_UNIT_LOCAL_ENABLED value: "false" - name: KUBERNETES_COMPUTING_UNIT_ENABLED value: "true" - name: KUBERNETES_IMAGE_PULL_POLICY value: "IfNotPresent" - name: GUI_WORKFLOW_WORKSPACE_PYTHON_LANGUAGE_SERVER_PORT value: "" - name: GUI_WORKFLOW_WORKSPACE_PRODUCTION_SHARED_EDITING_SERVER value: "true" - name: GUI_LOGIN_LOCAL_LOGIN value: "true" - name: GUI_LOGIN_GOOGLE_LOGIN value: "true" - name: GUI_DATASET_SINGLE_FILE_UPLOAD_MAXIMUM_SIZE_MB value: "1024" - name: GUI_WORKFLOW_WORKSPACE_EXPORT_EXECUTION_RESULT_ENABLED value: "true" - name: GUI_WORKFLOW_WORKSPACE_WORKFLOW_EXECUTIONS_TRACKING_ENABLED value: "true" - name: GUI_WORKFLOW_WORKSPACE_ASYNC_RENDERING_ENABLED value: "true" - name: COMPUTING_UNIT_SHARING_ENABLED value: "true" - name: USER_SYS_INVITE_ONLY value: "true" - name: USER_SYS_GOOGLE_CLIENT_ID value: "" - name: USER_SYS_GOOGLE_SMTP_GMAIL value: "" - name: USER_SYS_GOOGLE_SMTP_PASSWORD value: "" - name: USER_SYS_DOMAIN value: "" - name: AUTH_JWT_SECRET # Development-only default (256-bit HS256 secret). Production environments MUST override this with a different, securely generated secret. value: "a7f3c8e9b14d2e6f5a0b9c3d8e1f4a6b2c5d7e9f0a3b6c8d1e4f7a9b2c5d8e1f" yWebsocketServer: name: y-websocket-server replicaCount: 1 image: texera/y-websocket-server:latest pythonLanguageServer: name: python-language-server replicaCount: 1 image: texera/pylsp:latest imagePullSecret: regcred resources: limits: cpu: "100m" memory: "100Mi" # Metrics Server configuration metrics-server: enabled: true # set to false if metrics-server is already installed args: - --kubelet-insecure-tls - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname - --metric-resolution=15s resources: requests: cpu: 200m memory: 400Mi rbac: create: true serviceAccount: create: true priorityClassName: system-cluster-critical gatewayConfig: # Routes are available at bin/k8s/templates/gateway-routes.yaml # The hostname for the Gateway listener (HTTP/HTTPS). # e.g., "texera.example.com" hostname: "" # The name of the cert-manager Issuer or ClusterIssuer to use for obtaining certificates. # This requires cert-manager to be installed in the cluster. # You can find available ClusterIssuers with: `kubectl get clusterissuers` # You can find available Issuers with: `kubectl get issuers -A` # e.g., "letsencrypt-prod" issuer: "" # The Kind of the issuer specified above. Can be "Issuer" or "ClusterIssuer". # If you found it via `kubectl get clusterissuers`, use "ClusterIssuer". # If you found it via `kubectl get issuers`, use "Issuer". # defaults to "Issuer" if not specified. issuerKind: "Issuer" # The name of the Secret where the signed certificate should be stored. # If empty, it defaults to "{{ .Release.Name }}-cert". # e.g., "texera-tls" tlsSecretName: "" # Envoy Gateway Configuration envoy-gateway: config: envoyGateway: extensionApis: enableBackend: true enableEnvoyPatchPolicy: true ================================================ FILE: bin/licensing/audit_jar_licenses.py ================================================ #!/usr/bin/env python3 # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. """Audit each bundled jar's META-INF/LICENSE* and META-INF/NOTICE* (plus top-level LICENSE/NOTICE files like Lombok's), grouping by content hash so intra-project duplicates collapse into a single row. Surfaces jars whose upstream LICENSE preserves third-party copyrights beyond the canonical SPDX template — those need a per-dep licenses/LICENSE-.txt under our convention from commit 6d2e17d4d. Usage: audit_jar_licenses.py [ ...] Advisory only: prints a report and exits 0. """ from __future__ import annotations import argparse import hashlib import re import sys import zipfile from collections import defaultdict from pathlib import Path TEXERA_OWN_JAR_PREFIX = "org.apache.texera." LICENSE_NAMES = {"license", "license.txt", "license.md", "licence", "licence.txt"} NOTICE_NAMES = {"notice", "notice.txt", "notice.md"} COPYRIGHT_RE = re.compile(r"^\s*Copyright\b", re.IGNORECASE | re.MULTILINE) # Canonical Apache-2.0 LICENSE shipped by Apache projects is ~11357 bytes. # Anything materially larger (> 11600) is augmented with project-specific # attributions or embedded third-party copyrights. AUGMENTED_LICENSE_THRESHOLD = 11600 def _classify(parts: list[str]) -> str | None: """Return 'license', 'notice', or None for a zip entry path. Root level: only exact LICENSE/NOTICE basenames (Lombok-style). Anywhere under META-INF/: match by basename — 'license' or 'licence' in the name → license, 'notice' → notice. Picks up: META-INF/LICENSE, META-INF/LICENSE.txt META-INF/FastDoubleParser-LICENSE, META-INF/thirdparty-LICENSE META-INF/license/LICENSE.bouncycastle.txt (netty-3.x) META-INF/licenses/com.ongres.scram/.../LICENSE (postgresql JDBC) """ if len(parts) == 1: base = parts[0].lower() if base in LICENSE_NAMES: return "license" if base in NOTICE_NAMES: return "notice" return None if parts[0].upper() != "META-INF": return None base = parts[-1].lower() if "license" in base or "licence" in base: return "license" if "notice" in base: return "notice" return None def extract_license_notice(jar_path: Path) -> tuple[str, str] | None: """Concatenate every LICENSE/NOTICE-style file in a jar (root level, META-INF/, or META-INF/license[s]/). Returns (license, notice) text or None on bad zip.""" licenses: list[str] = [] notices: list[str] = [] try: with zipfile.ZipFile(jar_path) as zf: for name in zf.namelist(): kind = _classify(name.split("/")) if kind is None: continue try: blob = zf.read(name).decode("utf-8", errors="replace") except Exception: continue (licenses if kind == "license" else notices).append(blob) except zipfile.BadZipFile: return None return "\n".join(licenses), "\n".join(notices) def short_hash(text: str) -> str: return hashlib.sha1(text.encode("utf-8", errors="replace")).hexdigest()[:10] def collect_groups(lib_dirs: list[Path]) -> dict[tuple[str, str], list[tuple[str, str, str]]]: """Return {(license_hash, notice_hash): [(jar_name, license_text, notice_text), ...]}.""" seen: dict[str, Path] = {} for d in lib_dirs: if not d.is_dir(): sys.stderr.write(f"error: {d} is not a directory\n") sys.exit(2) for jar in d.glob("*.jar"): if jar.name.startswith(TEXERA_OWN_JAR_PREFIX): continue seen.setdefault(jar.name, jar) groups: dict[tuple[str, str], list[tuple[str, str, str]]] = defaultdict(list) for name, path in sorted(seen.items()): result = extract_license_notice(path) if result is None: continue lic, noti = result groups[(short_hash(lic), short_hash(noti))].append((name, lic, noti)) return groups def needs_per_dep_file(license_text: str, copyright_count: int) -> bool: """Strict criterion: ship a per-dep LICENSE file when the upstream LICENSE is materially larger than the canonical SPDX template, or contains an explicit "Licenses for included components"-style block, or carries 4+ distinct Copyright attributions.""" if len(license_text) > AUGMENTED_LICENSE_THRESHOLD: return True if "licenses for included components" in license_text.lower(): return True if copyright_count >= 4: return True return False def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("lib_dirs", nargs="+", help="dist lib/ directories to audit") args = ap.parse_args() groups = collect_groups([Path(d) for d in args.lib_dirs]) rows = [] for (lic_h, not_h), members in groups.items(): sample_lic = members[0][1] sample_not = members[0][2] cprights = len(COPYRIGHT_RE.findall(sample_lic)) rows.append({ "lic_hash": lic_h, "not_hash": not_h, "lic_size": len(sample_lic), "not_size": len(sample_not), "cprights": cprights, "needs": needs_per_dep_file(sample_lic, cprights), "members": [m[0] for m in members], }) rows.sort(key=lambda r: (-int(r["needs"]), -r["cprights"], -r["lic_size"])) needs = [r for r in rows if r["needs"]] flagged_other = [r for r in rows if not r["needs"] and (r["cprights"] > 0 or r["not_size"] > 0)] total_jars = sum(len(r["members"]) for r in rows) print(f"Audited {total_jars} jars across {len(rows)} distinct (license, notice) groups.") print(f"Groups warranting a per-dep licenses/LICENSE-*.txt file: {len(needs)}") print(f"Other flagged groups (project-attribution only, covered by shared license): {len(flagged_other)}") print() print("=" * 110) print("GROUPS REQUIRING A PER-DEP LICENSE FILE") print("=" * 110) print(f"{'lic_hash':10} {'cp':>3} {'lic_sz':>7} {'not_sz':>7} {'jars':>4} members") print("-" * 110) for r in needs: head = ", ".join(r["members"][:3]) more = f" (+{len(r['members']) - 3} more)" if len(r["members"]) > 3 else "" print(f"{r['lic_hash']:10} {r['cprights']:>3} {r['lic_size']:>7} {r['not_size']:>7} {len(r['members']):>4} {head}{more}") return 0 if __name__ == "__main__": sys.exit(main()) ================================================ FILE: bin/licensing/check_binary_deps.py ================================================ #!/usr/bin/env python3 # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. """Diff actually-bundled deps against LICENSE-binary for one ecosystem (jar | npm | agent-npm | python). Exits non-zero on drift. Usage: check_binary_deps.py jar [ ...] check_binary_deps.py npm check_binary_deps.py agent-npm check_binary_deps.py python Strictness: Default (exact match): version drift on any package — direct or transitive — fails the run. --ignore-transitive-version: version drift on a *transitive* dep is reported as informational; presence of every claimed library and version agreement on every *direct* dep are still enforced. """ from __future__ import annotations import argparse import csv import json import re import sys import tempfile from collections import defaultdict from pathlib import Path # Per-module LICENSE-binary files that the combined LICENSE-binary unions. # Resolved relative to the repo root (parent of bin/licensing/). PER_MODULE_LICENSE_BINARIES: list[str] = [ "access-control-service/LICENSE-binary", "config-service/LICENSE-binary", "file-service/LICENSE-binary", "workflow-compiling-service/LICENSE-binary", "computing-unit-managing-service/LICENSE-binary", "amber/LICENSE-binary-java", "amber/LICENSE-binary-python", "frontend/LICENSE-binary", "agent-service/LICENSE-binary", ] # Primary requirement files used to mark deps as "direct" (vs. transitive). PRIMARY_REQUIREMENTS = { "python": ["amber/requirements.txt", "amber/operator-requirements.txt"], "npm": ["frontend/package.json"], "agent-npm": ["agent-service/package.json"], # All SBT files in the repo are scanned for libraryDependencies entries. } def _repo_root() -> Path: return Path(__file__).resolve().parent.parent.parent def build_default_license_binary() -> Path: """Concat all per-module LICENSE-binary files into a temp file using bin/licensing/concat_license_binary.py and return its path. Used as the default --license-binary when the caller doesn't pass one.""" here = Path(__file__).resolve().parent repo_root = _repo_root() inputs = [repo_root / p for p in PER_MODULE_LICENSE_BINARIES] missing = [p for p in inputs if not p.is_file()] if missing: sys.stderr.write( f"error: per-module LICENSE-binary file(s) not found: {missing}\n" ) sys.exit(2) sys.path.insert(0, str(here)) import concat_license_binary as concat parsed = [concat.parse(p) for p in inputs] apache_header, groups = concat.merge(parsed) text = concat.emit(apache_header, groups) out = Path(tempfile.mkstemp(prefix="combined-LICENSE-binary-", suffix=".txt")[1]) out.write_text(text) return out # Jars produced by Texera itself — not third-party deps, skip from drift checks. TEXERA_OWN_JAR_PREFIX = "org.apache.texera." ECO_HEADERS = { "jar": "Scala/Java jars:", "python": "Python packages:", "npm": "Angular / npm packages", "agent-npm": "Agent service npm packages", } JAR_BULLET = re.compile(r"^\s*-\s+(\S+\.jar)\b") # ` - @` — npm form, name may start with @scope/. NPM_BULLET = re.compile(r"^\s*-\s+(@?[\w@/.\-]+)@([^\s@]+)\s*$") # ` - ==` — pip form. PY_BULLET = re.compile(r"^\s*-\s+([\w][\w.\-]*)==(\S+)\s*$") # Splits a jar basename like `netty-all-4.1.96.Final.jar` into (artifact, version). # The version starts at the first dash followed by a digit. JAR_NAME_VERSION = re.compile(r"^(.+?)-(\d[^/]*)\.jar$") # SBT libraryDependencies syntax: "group" % "artifact" % "version" [...]. # %% / %%% mark Scala / Scala.js libs whose artifact gains a `_` suffix # at resolution time; we capture the bare artifact and reconstruct both forms. SBT_DEP = re.compile( r'"([\w.\-]+)"\s*(%%%?|%)\s*"([\w.\-]+)"\s*%\s*"([\w.\-]+)"' ) # --- direct-dep loaders ---------------------------------------------------- def load_direct_python() -> set[str]: """PEP 503 canonical names from amber/requirements.txt (and any other file listed in PRIMARY_REQUIREMENTS['python']).""" direct: set[str] = set() for rel in PRIMARY_REQUIREMENTS["python"]: p = _repo_root() / rel if not p.is_file(): continue for raw in p.read_text().splitlines(): line = raw.strip() if not line or line.startswith("#") or line.startswith("-"): # `-`-prefixed lines are pip flags (--extra-index-url, -r, ...). continue # Strip env markers, extras, and version specifiers; we only # need the package name. name = re.split(r"[<>=!~;\s\[]", line, maxsplit=1)[0] if name: direct.add(canonicalize_python_name(name)) return direct def load_direct_npm(rel_path: str) -> set[str]: """Top-level dep names from a package.json's dependencies / devDependencies / peerDependencies / optionalDependencies.""" direct: set[str] = set() p = _repo_root() / rel_path if not p.is_file(): return direct data = json.loads(p.read_text()) for key in ("dependencies", "devDependencies", "peerDependencies", "optionalDependencies"): section = data.get(key) or {} direct.update(section.keys()) return direct def load_direct_jar_artifacts() -> set[str]: """ArtifactIds declared in any *.sbt or project/Dependencies.scala file in the repo. For Scala libs (`%%`/`%%%`) the resolved jar gains a `_` suffix; we add the bare artifact here and let the matcher strip the suffix when comparing.""" repo_root = _repo_root() direct: set[str] = set() # Scan SBT build files. Walking the tree keeps this resilient to new # subprojects without having to keep an explicit list in sync. skip_dirs = {"node_modules", "target", ".git", ".idea", ".bsp", "dist"} for path in repo_root.rglob("*"): if any(part in skip_dirs for part in path.parts): continue if not path.is_file(): continue if path.suffix == ".sbt" or path.name == "Dependencies.scala": try: text = path.read_text(errors="replace") except OSError: continue for m in SBT_DEP.finditer(text): _group, _sep, artifact, _version = m.groups() direct.add(artifact) return direct # --- extracting claims from LICENSE-binary --------------------------------- def parse_prose(path: Path, ecosystem: str) -> set[str]: """Return the set of claimed entries: - jar: set of jar basenames (e.g. 'commons-cli-1.5.0.jar' qualified) - npm: set of '@' - python: set of '==' """ lines = path.read_text().splitlines() current_eco: str | None = None claims: set[str] = set() for raw in lines: stripped = raw.strip() matched_header = False for eco, needle in ECO_HEADERS.items(): if stripped.startswith(needle): current_eco = eco matched_header = True break if matched_header: continue if stripped.startswith("=====") or stripped.startswith("-----"): current_eco = None continue if current_eco != ecosystem: continue if ecosystem == "jar": m = JAR_BULLET.match(raw) if m: claims.add(m.group(1)) elif ecosystem in ("npm", "agent-npm"): m = NPM_BULLET.match(raw) if m: claims.add(f"{m.group(1)}@{m.group(2)}") else: # python m = PY_BULLET.match(raw) if m: name = canonicalize_python_name(m.group(1)) ver = canonicalize_python_version(m.group(2)) claims.add(f"{name}=={ver}") return claims # --- collecting reality ---------------------------------------------------- def collect_jars(lib_dirs) -> set[str]: result: set[str] = set() for d in lib_dirs: dp = Path(d) if not dp.is_dir(): sys.stderr.write(f"error: {dp} is not a directory\n") sys.exit(2) for jar in dp.glob("*.jar"): if jar.name.startswith(TEXERA_OWN_JAR_PREFIX): continue result.add(jar.name) return result def collect_npm(path: Path) -> set[str]: """3rdpartylicenses.json emitted by license-webpack-plugin (configured in frontend/custom-webpack.config.js): a JSON array of {name, version, license} entries scoped to the actual webpack bundle.""" data = json.loads(path.read_text()) return {f"{e['name']}@{e['version']}" for e in data if e.get('name') and e.get('version')} def canonicalize_python_name(name: str) -> str: """PEP 503 canonical form: lowercase, [-_.]+ collapsed to '-'.""" return re.sub(r"[-_.]+", "-", name.lower()) def canonicalize_python_version(version: str) -> str: """Drop PEP 440 local-version identifiers (everything after `+`). Wheels for the same release ship as e.g. `2.8.0` on macOS but `2.8.0+cpu` on Linux — same software, different platform tag.""" return version.split("+", 1)[0] def collect_python(path: Path) -> set[str]: """pip-licenses CSV: Name,Version,License (header row). Names are canonicalized per PEP 503 so the compare is indifferent to whether a distribution uses hyphens, underscores, or dots; versions are canonicalized to the public release form (no PEP 440 +local suffix).""" result: set[str] = set() with path.open(newline="") as f: reader = csv.reader(f) next(reader, None) # header for row in reader: if row and row[0] and row[1]: name = canonicalize_python_name(row[0]) ver = canonicalize_python_version(row[1]) result.add(f"{name}=={ver}") return result # --- diff ------------------------------------------------------------------ # Trailing Scala-version suffix that SBT appends to %% artifacts at resolve time. SCALA_SUFFIX = re.compile(r"_\d+(?:\.\d+)+$") def _index_npm(items: set[str]) -> dict[str, set[str]]: """{ 'react@18.2.0', 'react@17.0.0' } -> { 'react': {'18.2.0', '17.0.0'} }. Same name can legitimately appear at multiple versions when the bundle pulls in two majors of a transitive dep.""" out: dict[str, set[str]] = defaultdict(set) for entry in items: # Last '@' is the version separator; '@' inside scoped names is at index 0. idx = entry.rfind("@") if idx <= 0: continue out[entry[:idx]].add(entry[idx + 1:]) return out def _index_python(items: set[str]) -> dict[str, set[str]]: """{ 'numpy==2.1.0' } -> { 'numpy': {'2.1.0'} }.""" out: dict[str, set[str]] = defaultdict(set) for entry in items: if "==" not in entry: continue name, _, ver = entry.partition("==") out[name].add(ver) return out def _index_jar(items: set[str]) -> dict[str, set[str]]: """{ 'netty-all-4.1.96.Final.jar' } -> { 'netty-all': {'4.1.96.Final'} }. Same shape as _index_npm / _index_python: an artifact legitimately bundled at multiple versions (e.g. logback at 1.2.x in one service and 1.4.x in another) survives intact. Unparseable jar names are surfaced loudly rather than silently dropped — a parser bug here means real bundled deps would skip license validation.""" out: dict[str, set[str]] = defaultdict(set) for jar in items: m = JAR_NAME_VERSION.match(jar) if not m: sys.stderr.write(f"warning: cannot parse jar name: {jar}\n") continue out[m.group(1)].add(m.group(2)) return out def _jar_basename(artifact: str, version: str) -> str: return f"{artifact}-{version}.jar" def _is_direct_jar(artifact: str, direct_artifacts: set[str]) -> bool: """sbt-native-packager's default JavaAppPackaging names dist jars `.-.jar`, so the artifactId we extract from the basename is `.` (e.g. `io.netty.netty-buffer`). SBT's libraryDependencies record only the bare artifactId, so we match on the segment after the last `.`. Scala libs (`%%`) get a `_` suffix appended at resolve time which we strip first.""" bare = SCALA_SUFFIX.sub("", artifact) if bare in direct_artifacts: return True if "." in bare: tail = bare.rsplit(".", 1)[1] # Re-strip in case the Scala suffix was after the dot. tail = SCALA_SUFFIX.sub("", tail) if tail in direct_artifacts: return True return False # --- reporting ------------------------------------------------------------- def report( added: list[str], stale: list[str], drift_direct: list[tuple[str, str, str]], drift_transitive: list[tuple[str, str, str]], label: str, kind: str, ignore_transitive_version: bool, ) -> int: rc = 0 if added: print(f"NEW {label} not claimed by LICENSE-binary:") for a in sorted(added): print(f" + {a}") print() print("ACTION REQUIRED") print(f" 1. Verify each dep's license is ASF Category A or B.") print(f" 2. Add a bullet in LICENSE-binary under the matching license") print(f" section, either as '{kind}-compatible token' (see format below).") print(f" 3. If an upstream NOTICE must be bubbled up, add to NOTICE-binary.") print() rc = 1 if stale: print(f"STALE {label} claimed by LICENSE-binary but not actually bundled:") for s in sorted(stale): print(f" - {s}") print() print("ACTION REQUIRED") print(f" 1. Remove the matching bullet / token from LICENSE-binary.") print(f" 2. Remove any matching attribution from NOTICE-binary.") print() rc = 1 def _fmt_drift(entry: tuple[str, list[str], list[str]]) -> str: name, cvers, rvers = entry return f" ~ {name}: LICENSE-binary={', '.join(cvers)} bundled={', '.join(rvers)}" if drift_direct: print(f"DRIFT (direct) {label} — claimed versions differ from bundled:") for entry in sorted(drift_direct): print(_fmt_drift(entry)) print() print("ACTION REQUIRED") print(f" Update LICENSE-binary to match the bundled versions. Direct deps") print(f" always block CI — a version bump may carry license changes.") print() rc = 1 if drift_transitive: if ignore_transitive_version: print(f"DRIFT (transitive, informational) {label}:") for entry in sorted(drift_transitive): print(_fmt_drift(entry)) print(f" (--ignore-transitive-version is set; nightly exact-match") print(f" check on main is responsible for refreshing these.)") print() else: print(f"DRIFT (transitive) {label} — claimed versions differ from bundled:") for entry in sorted(drift_transitive): print(_fmt_drift(entry)) print() print("ACTION REQUIRED") print(f" Update LICENSE-binary to match the bundled versions, or rerun") print(f" with --ignore-transitive-version to treat transitive drift as") print(f" informational.") print() rc = 1 return rc # --- main ------------------------------------------------------------------ def diff_simple( claim_idx: dict[str, set[str]], real_idx: dict[str, set[str]], direct_names: set[str], joiner: str, ) -> tuple[list[str], list[str], list[tuple[str, list[str], list[str]]], list[tuple[str, list[str], list[str]]]]: """Diff name->{versions} multimaps for npm/python. `joiner` is the separator used when rendering added/stale entries (`@` for npm, `==` for python). Drifts are returned as (name, sorted_claimed_versions, sorted_real_versions).""" added: list[str] = [] stale: list[str] = [] drift_direct: list[tuple[str, list[str], list[str]]] = [] drift_transitive: list[tuple[str, list[str], list[str]]] = [] for name in sorted(real_idx.keys() - claim_idx.keys()): for v in sorted(real_idx[name]): added.append(f"{name}{joiner}{v}") for name in sorted(claim_idx.keys() - real_idx.keys()): for v in sorted(claim_idx[name]): stale.append(f"{name}{joiner}{v}") for name in sorted(claim_idx.keys() & real_idx.keys()): cvers, rvers = claim_idx[name], real_idx[name] if cvers == rvers: continue entry = (name, sorted(cvers), sorted(rvers)) (drift_direct if name in direct_names else drift_transitive).append(entry) return added, stale, drift_direct, drift_transitive def diff_jars( claim_idx: dict[str, set[str]], real_idx: dict[str, set[str]], direct_artifacts: set[str], ) -> tuple[list[str], list[str], list[tuple[str, list[str], list[str]]], list[tuple[str, list[str], list[str]]]]: """Diff artifact->{versions} multimaps. Added/stale are rendered as full jar basenames users will see in LICENSE-binary; drifts are (artifact, sorted_claimed, sorted_real).""" added: list[str] = [] stale: list[str] = [] drift_direct: list[tuple[str, list[str], list[str]]] = [] drift_transitive: list[tuple[str, list[str], list[str]]] = [] for artifact in sorted(real_idx.keys() - claim_idx.keys()): for v in sorted(real_idx[artifact]): added.append(_jar_basename(artifact, v)) for artifact in sorted(claim_idx.keys() - real_idx.keys()): for v in sorted(claim_idx[artifact]): stale.append(_jar_basename(artifact, v)) for artifact in sorted(claim_idx.keys() & real_idx.keys()): cvers, rvers = claim_idx[artifact], real_idx[artifact] if cvers == rvers: continue entry = (artifact, sorted(cvers), sorted(rvers)) if _is_direct_jar(artifact, direct_artifacts): drift_direct.append(entry) else: drift_transitive.append(entry) return added, stale, drift_direct, drift_transitive def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("kind", choices=["jar", "npm", "agent-npm", "python"]) ap.add_argument("inputs", nargs="+") ap.add_argument( "--license-binary", default=None, help=( "Path to LICENSE-binary to validate against. If omitted, the " "tool builds a combined LICENSE-binary on the fly from the " "per-module files (see PER_MODULE_LICENSE_BINARIES)." ), ) ap.add_argument( "--ignore-transitive-version", action="store_true", help=( "Treat version drift on transitive deps as informational instead " "of failing. Direct deps (declared in primary requirement files: " "build.sbt / package.json / requirements.txt) still block on any " "drift, and any added or removed package still blocks regardless " "of direct/transitive." ), ) args = ap.parse_args() if args.license_binary is None: lb = build_default_license_binary() else: lb = Path(args.license_binary) if not lb.exists(): sys.stderr.write(f"error: {lb} not found\n") return 2 if args.kind == "jar": claimed = parse_prose(lb, "jar") reality = collect_jars(args.inputs) direct_artifacts = load_direct_jar_artifacts() added, stale, dd, dt = diff_jars(_index_jar(claimed), _index_jar(reality), direct_artifacts) rc = report(added, stale, dd, dt, "JVM jars", "jar", args.ignore_transitive_version) if rc == 0: print(f"OK: {len(reality)} JVM jars match LICENSE-binary.") return rc if args.kind == "npm": claimed = parse_prose(lb, "npm") reality = collect_npm(Path(args.inputs[0])) direct = load_direct_npm("frontend/package.json") added, stale, dd, dt = diff_simple(_index_npm(claimed), _index_npm(reality), direct, joiner="@") rc = report(added, stale, dd, dt, "npm packages", "npm", args.ignore_transitive_version) if rc == 0: print(f"OK: {len(reality)} npm packages match LICENSE-binary.") return rc if args.kind == "agent-npm": claimed = parse_prose(lb, "agent-npm") reality = collect_npm(Path(args.inputs[0])) direct = load_direct_npm("agent-service/package.json") added, stale, dd, dt = diff_simple(_index_npm(claimed), _index_npm(reality), direct, joiner="@") rc = report(added, stale, dd, dt, "agent-service npm packages", "agent-npm", args.ignore_transitive_version) if rc == 0: print(f"OK: {len(reality)} agent-service npm packages match LICENSE-binary.") return rc if args.kind == "python": claimed = parse_prose(lb, "python") reality = collect_python(Path(args.inputs[0])) direct = load_direct_python() added, stale, dd, dt = diff_simple(_index_python(claimed), _index_python(reality), direct, joiner="==") rc = report(added, stale, dd, dt, "Python packages", "python", args.ignore_transitive_version) if rc == 0: print(f"OK: {len(reality)} Python packages match LICENSE-binary.") return rc return 2 if __name__ == "__main__": sys.exit(main()) ================================================ FILE: bin/licensing/concat_license_binary.py ================================================ #!/usr/bin/env python3 # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. """Merge multiple per-module LICENSE-binary files into a single combined LICENSE-binary, joining at the license-group level so that each ecosystem subsection (Scala/Java jars / Python packages / Angular npm / Agent npm / Source files derived) appears under its parent license group rather than the inputs being stacked end-to-end. Each per-module input file follows this structure: === THIRD-PARTY COMPONENTS === --- Dependencies under the X License --- [optional sub-license heading like "CDDL 1.0\\n~~~~"] Scala/Java jars: | Python packages: | ... : - --- Dependencies under the Y License --- ... Individual jars may contain their own META-INF/LICENSE... The merge: - Reuses the Apache-2.0 license text from the first input verbatim. - Builds one canonical THIRD-PARTY COMPONENTS preamble. - Walks license groups in first-seen order across inputs (later-only groups append after earlier ones). - Within each group, unions subsections by header, ordered by SUBSECTION_ORDER. - Within each subsection, unions entries (deduplicating by entry id — the trimmed text after the leading "- "). - Emits the trailer once at the end. Usage: concat_license_binary.py [ ...] """ from __future__ import annotations import argparse import sys from collections import OrderedDict from dataclasses import dataclass, field from pathlib import Path SEP = "-" * 80 EQ = "=" * 80 # Fixed display order of subsections inside any license group. SUBSECTION_ORDER = [ "Source files derived", # prefix; full headers vary by license name "Scala/Java jars:", "Python packages:", "Angular / npm packages:", "Agent service npm packages:", ] PREAMBLE = ( "Apache Texera's binary distribution bundles the following third-party\n" "components, grouped by license. Each section references licenses/ for\n" "the full text of the applicable license. Components under the Apache\n" "License, Version 2.0 are governed by the same license terms as Apache\n" "Texera itself and are listed for completeness." ) TRAILER = ( "Individual jars may contain their own META-INF/LICENSE and META-INF/NOTICE\n" "files that apply to their specific contents; those files continue to govern\n" "the use of those components." ) @dataclass class Subsection: header: str sub_license: str | None = None # e.g. "CDDL 1.0\n~~~~~~~~", or None entries: list[list[str]] = field(default_factory=list) @dataclass class Group: header_block: list[str] title: str # Keyed by (sub_license, header). Two subsections with the same header # (e.g. "Scala/Java jars:") under different sub-licenses (CDDL 1.0 vs # CDDL 1.1) are distinct entries — that's why we key on the tuple. subsections: "OrderedDict[tuple[str | None, str], Subsection]" = field(default_factory=OrderedDict) def has_entries(self) -> bool: return any(s.entries for s in self.subsections.values()) def is_subsection_header(line: str) -> bool: s = line.rstrip() if not s.endswith(":"): return False return any(s == h or s.startswith(p) for p, h in ((p, p) for p in SUBSECTION_ORDER)) def entry_id(entry: list[str]) -> str: """Identifier used for deduplication: the trimmed text after the leading ' - '. For multi-line entries (Source files derived), the first line uniquely identifies the entry.""" return entry[0][4:].strip() def parse(path: Path) -> tuple[str, list[Group]]: """Parse a per-module LICENSE-binary into (apache_header, groups).""" text = path.read_text() lines = text.splitlines() third_party_idx = next( i for i, line in enumerate(lines) if "THIRD-PARTY COMPONENTS" in line ) eq_idx = third_party_idx - 1 while eq_idx >= 0 and not lines[eq_idx].startswith("="): eq_idx -= 1 apache_header = "\n".join(lines[:eq_idx]).rstrip("\n") # Skip past THIRD-PARTY COMPONENTS preamble until first group SEP. i = eq_idx while i < len(lines) and lines[i] != SEP: i += 1 groups: list[Group] = [] while i < len(lines): if lines[i] != SEP: i += 1 continue # Group header: SEP / title (one or more lines) / SEP header_block = [lines[i]] i += 1 title_lines: list[str] = [] while i < len(lines) and lines[i] != SEP: title_lines.append(lines[i]) i += 1 title = " ".join(s.strip() for s in title_lines).strip() header_block.extend(title_lines) if i < len(lines): header_block.append(lines[i]) # closing SEP i += 1 grp = Group(header_block=header_block, title=title) current: Subsection | None = None current_sub_license: str | None = None while i < len(lines) and lines[i] != SEP: line = lines[i] # Sub-license heading (e.g. "CDDL 1.0\n~~~~~~~~"). A group can # carry multiple of these (CDDL 1.0 then CDDL 1.1); each acts # as a scope marker for the subsections that follow. if i + 1 < len(lines) and lines[i + 1].startswith("~~~"): if current is not None: grp.subsections[(current.sub_license, current.header)] = current current = None current_sub_license = line + "\n" + lines[i + 1] i += 3 continue # Subsection header (ends with ':'; matches our known set) stripped = line.rstrip() if stripped.endswith(":") and any( stripped == hdr or stripped.startswith(prefix) for prefix, hdr in [(p, p) for p in SUBSECTION_ORDER] ): if current is not None: grp.subsections[(current.sub_license, current.header)] = current current = Subsection(header=stripped, sub_license=current_sub_license) i += 1 continue # Entry: " - "; continuation lines start with 4 spaces and # do NOT start with " - " (those would be the next entry). if current is not None and line.startswith(" - "): entry = [line] i += 1 while i < len(lines): nxt = lines[i] if nxt.startswith(" ") and not nxt.startswith(" - "): entry.append(nxt) i += 1 else: break current.entries.append(entry) continue i += 1 if current is not None: grp.subsections[(current.sub_license, current.header)] = current groups.append(grp) return apache_header, groups def merge(parsed: list[tuple[str, list[Group]]]) -> tuple[str, list[Group]]: apache_header = parsed[0][0] merged: "OrderedDict[str, Group]" = OrderedDict() for _, groups in parsed: for g in groups: if g.title not in merged: merged[g.title] = Group( header_block=list(g.header_block), title=g.title, ) mg = merged[g.title] for key, sub in g.subsections.items(): if key not in mg.subsections: mg.subsections[key] = Subsection( header=sub.header, sub_license=sub.sub_license, ) target = mg.subsections[key] seen = {entry_id(e) for e in target.entries} for e in sub.entries: eid = entry_id(e) if eid not in seen: target.entries.append(e) seen.add(eid) # Reorder subsections within each group: group by sub_license bucket # (preserving first-seen sub-license order), and within each bucket # order by SUBSECTION_ORDER. for mg in merged.values(): sub_license_order: list[str | None] = [] by_sub_license: "OrderedDict[str | None, list[tuple[tuple[str | None, str], Subsection]]]" = OrderedDict() for key, sub in mg.subsections.items(): sl = sub.sub_license if sl not in by_sub_license: by_sub_license[sl] = [] sub_license_order.append(sl) by_sub_license[sl].append((key, sub)) ordered: "OrderedDict[tuple[str | None, str], Subsection]" = OrderedDict() for sl in sub_license_order: bucket = by_sub_license[sl] placed: set[tuple[str | None, str]] = set() for prefix in SUBSECTION_ORDER: for key, sub in bucket: if sub.header.startswith(prefix) and key not in placed: ordered[key] = sub placed.add(key) for key, sub in bucket: if key not in placed: ordered[key] = sub mg.subsections = ordered return apache_header, list(merged.values()) def emit(apache_header: str, groups: list[Group]) -> str: out: list[str] = [ apache_header, "", EQ, "THIRD-PARTY COMPONENTS", EQ, "", PREAMBLE, "", ] for g in groups: if not g.has_entries(): continue out.extend(g.header_block) out.append("") last_sub_license: str | None = None last_sub_license_emitted = False for sub in g.subsections.values(): if not sub.entries: continue # Emit sub-license heading once whenever the marker changes. if sub.sub_license != last_sub_license or not last_sub_license_emitted: if sub.sub_license: out.append(sub.sub_license) out.append("") last_sub_license = sub.sub_license last_sub_license_emitted = True out.append(sub.header) for entry in sub.entries: out.extend(entry) out.append("") out.append(TRAILER) out.append("") return "\n".join(out) def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("output", help="Path to write the combined LICENSE-binary") ap.add_argument("inputs", nargs="+", help="Per-module LICENSE-binary files to merge") args = ap.parse_args() parsed = [] for p in args.inputs: path = Path(p) if not path.is_file(): sys.stderr.write(f"error: {path} is not a file\n") return 2 parsed.append(parse(path)) apache_header, groups = merge(parsed) text = emit(apache_header, groups) Path(args.output).write_text(text) n_entries = sum(len(s.entries) for g in groups for s in g.subsections.values()) print(f"Wrote {args.output}: {len(groups)} groups, {n_entries} entries " f"from {len(args.inputs)} input file(s)") return 0 if __name__ == "__main__": sys.exit(main()) ================================================ FILE: bin/licensing/test_check_binary_deps.py ================================================ #!/usr/bin/env python3 # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. """Unit tests for check_binary_deps. Run via: python3 -m unittest bin.licensing.test_check_binary_deps or: python3 -m unittest discover -s bin/licensing -p "test_*.py" These tests use only the Python standard library — no pytest, no project deps — so they can run in any CI job that has Python set up. """ from __future__ import annotations import csv import io import sys import tempfile import textwrap import unittest from contextlib import redirect_stderr, redirect_stdout from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import check_binary_deps as cbd # noqa: E402 # --- pure-function tests --------------------------------------------------- class IndexersPreserveAllVersions(unittest.TestCase): """Regression test for the bug where dict-keyed-by-name indexers silently dropped a duplicate-name entry. The combined LICENSE-binary on `main` legitimately claims the same artifact at two versions in 97 cases (e.g. logback 1.2.x + 1.4.x); the indexer must preserve all of them.""" def test_index_npm_keeps_multiple_versions(self): idx = cbd._index_npm({"react@17.0.0", "react@18.2.0", "lodash@4.17.21"}) self.assertEqual(idx["react"], {"17.0.0", "18.2.0"}) self.assertEqual(idx["lodash"], {"4.17.21"}) def test_index_npm_handles_scoped_names(self): # `@scope/name@version` — the version separator is the LAST `@`. idx = cbd._index_npm({"@angular/core@18.0.0", "@angular/core@17.0.0"}) self.assertEqual(idx["@angular/core"], {"17.0.0", "18.0.0"}) def test_index_python_keeps_multiple_versions(self): idx = cbd._index_python({"numpy==2.1.0", "numpy==2.0.0", "pandas==2.2.3"}) self.assertEqual(idx["numpy"], {"2.0.0", "2.1.0"}) self.assertEqual(idx["pandas"], {"2.2.3"}) def test_index_jar_keeps_multiple_versions(self): # Two versions of the same artifact, plus an unrelated one. idx = cbd._index_jar({ "ch.qos.logback.logback-classic-1.2.3.jar", "ch.qos.logback.logback-classic-1.4.14.jar", "io.netty.netty-buffer-4.1.96.Final.jar", }) self.assertEqual( idx["ch.qos.logback.logback-classic"], {"1.2.3", "1.4.14"} ) self.assertEqual(idx["io.netty.netty-buffer"], {"4.1.96.Final"}) def test_index_jar_warns_on_unparseable_name(self): buf = io.StringIO() with redirect_stderr(buf): idx = cbd._index_jar({"weird-no-version.jar"}) self.assertEqual(idx, {}) self.assertIn("cannot parse jar name", buf.getvalue()) class JarBasenameRoundTrip(unittest.TestCase): """`_jar_basename` must reconstruct the exact basename that `JAR_NAME_VERSION` parsed, including for jars with classifier suffixes (the version regex captures the whole tail).""" def test_round_trip_simple(self): for jar in [ "io.netty.netty-buffer-4.1.96.Final.jar", "commons-cli-1.5.0.jar", "scala-library-2.13.10.jar", "io.netty.netty-tcnative-boringssl-static-2.0.61.Final-linux-x86_64.jar", "co.fs2.fs2-core_2.13-3.12.2.jar", ]: with self.subTest(jar=jar): m = cbd.JAR_NAME_VERSION.match(jar) self.assertIsNotNone(m, f"failed to parse {jar}") self.assertEqual(cbd._jar_basename(m.group(1), m.group(2)), jar) class IsDirectJar(unittest.TestCase): """`_is_direct_jar` reconciles SBT's bare artifactId with sbt-native-packager's `.-.jar` jar naming, plus Scala's `_` suffix on `%%` libs.""" direct = {"netty-buffer", "jersey-common", "fs2-core"} def test_group_prefixed(self): self.assertTrue(cbd._is_direct_jar("io.netty.netty-buffer", self.direct)) self.assertTrue(cbd._is_direct_jar("org.glassfish.jersey.core.jersey-common", self.direct)) def test_bare_artifact(self): self.assertTrue(cbd._is_direct_jar("netty-buffer", self.direct)) def test_scala_suffix_on_group_prefixed(self): self.assertTrue(cbd._is_direct_jar("co.fs2.fs2-core_2.13", self.direct)) def test_unknown_artifact(self): self.assertFalse(cbd._is_direct_jar("some.thing.unrelated", self.direct)) self.assertFalse(cbd._is_direct_jar("unrelated", self.direct)) class DiffSimple(unittest.TestCase): """`diff_simple` (npm/python): added/stale must include version, drift must be reported per-name with both version sets.""" def test_clean_no_diff(self): idx = {"a": {"1.0"}, "b": {"2.0"}} added, stale, dd, dt = cbd.diff_simple(idx, idx, set(), joiner="==") self.assertEqual((added, stale, dd, dt), ([], [], [], [])) def test_added_and_stale_include_version(self): claim = {"a": {"1.0"}} real = {"b": {"2.0"}} added, stale, dd, dt = cbd.diff_simple(claim, real, set(), joiner="==") self.assertEqual(added, ["b==2.0"]) self.assertEqual(stale, ["a==1.0"]) self.assertEqual(dd, []) self.assertEqual(dt, []) def test_added_and_stale_emit_one_entry_per_version(self): # Brand-new package bundled at two versions: surface both. claim = {} real = {"newpkg": {"1.0", "2.0"}} added, stale, dd, dt = cbd.diff_simple(claim, real, set(), joiner="==") self.assertEqual(added, ["newpkg==1.0", "newpkg==2.0"]) def test_single_version_drift_classified_direct_vs_transitive(self): claim = {"foo": {"1.0"}, "bar": {"1.0"}} real = {"foo": {"1.1"}, "bar": {"1.1"}} added, stale, dd, dt = cbd.diff_simple(claim, real, {"foo"}, joiner="==") self.assertEqual(added, []) self.assertEqual(stale, []) self.assertEqual(dd, [("foo", ["1.0"], ["1.1"])]) self.assertEqual(dt, [("bar", ["1.0"], ["1.1"])]) def test_multi_version_drift_reports_both_sides(self): # The bug this PR fixes: previously these collapsed. claim = {"jetty": {"9.4.20", "11.0.20"}} real = {"jetty": {"9.4.20", "11.0.21"}} _, _, _, dt = cbd.diff_simple(claim, real, set(), joiner="==") self.assertEqual(dt, [("jetty", ["11.0.20", "9.4.20"], ["11.0.21", "9.4.20"])]) def test_npm_joiner(self): claim, real = {}, {"react": {"18.2.0"}} added, _, _, _ = cbd.diff_simple(claim, real, set(), joiner="@") self.assertEqual(added, ["react@18.2.0"]) class DiffJars(unittest.TestCase): """`diff_jars`: same shape as diff_simple but added/stale are full jar basenames (reconstructed via `_jar_basename`), and direct/ transitive classification uses `_is_direct_jar`.""" def test_clean(self): idx = {"io.netty.netty-buffer": {"4.1.96.Final"}} added, stale, dd, dt = cbd.diff_jars(idx, idx, set()) self.assertEqual((added, stale, dd, dt), ([], [], [], [])) def test_added_stale_use_full_basename(self): claim = {"a.b": {"1.0"}} real = {"x.y": {"2.0"}} added, stale, _, _ = cbd.diff_jars(claim, real, set()) self.assertEqual(added, ["x.y-2.0.jar"]) self.assertEqual(stale, ["a.b-1.0.jar"]) def test_multi_version_added_stale_emits_one_basename_per_version(self): claim = {} real = {"io.netty.netty-buffer": {"4.1.96.Final", "4.1.100.Final"}} added, _, _, _ = cbd.diff_jars(claim, real, set()) self.assertEqual( added, [ "io.netty.netty-buffer-4.1.100.Final.jar", "io.netty.netty-buffer-4.1.96.Final.jar", ], ) def test_drift_direct_vs_transitive_via_group_prefixed_match(self): # `netty-buffer` is direct (declared in SBT bare); the bundled jar # is `io.netty.netty-buffer` (sbt-native-packager naming). claim = { "io.netty.netty-buffer": {"4.1.96.Final"}, "org.unknown.thing": {"1.0"}, } real = { "io.netty.netty-buffer": {"4.1.100.Final"}, "org.unknown.thing": {"1.1"}, } _, _, dd, dt = cbd.diff_jars(claim, real, {"netty-buffer"}) self.assertEqual(dd, [("io.netty.netty-buffer", ["4.1.96.Final"], ["4.1.100.Final"])]) self.assertEqual(dt, [("org.unknown.thing", ["1.0"], ["1.1"])]) # --- end-to-end tests ------------------------------------------------------ def _write_lb(text: str) -> Path: p = Path(tempfile.mkstemp(suffix=".txt")[1]) p.write_text(text) return p def _write_pip_csv(rows: list[tuple[str, str]]) -> Path: p = Path(tempfile.mkstemp(suffix=".csv")[1]) with p.open("w", newline="") as f: w = csv.writer(f) w.writerow(["Name", "Version", "License"]) for name, ver in rows: w.writerow([name, ver, "BSD"]) return p # Synthetic LICENSE-binary fixture mirroring the per-module file format # (Apache-2 / MIT divider lines + `Python packages:` header + bullets). # Two python packages claimed: one at one version, one at two versions. SYNTHETIC_LB = textwrap.dedent("""\ Apache header etc. -------------------------------------------------------------------------------- Dependencies under the Apache License, Version 2.0 -------------------------------------------------------------------------------- Python packages: - direct-pkg==1.0.0 - transitive-pkg==2.0.0 - transitive-pkg==2.5.0 """) class EndToEndPython(unittest.TestCase): """Run main() against a synthetic LICENSE-binary + pip-licenses CSV and assert the exit codes for each behavior class.""" def setUp(self): self.lb = _write_lb(SYNTHETIC_LB) def _run(self, csv_rows: list[tuple[str, str]], *flags: str) -> int: # main() reads sys.argv; route stdout/stderr through buffers so # failures don't pollute test output. csv_path = _write_pip_csv(csv_rows) argv_save = sys.argv sys.argv = [ "x", "--license-binary", str(self.lb), *flags, "python", str(csv_path), ] # Patch direct-deps loader to a known set rather than reading # the real repo's requirements.txt. loader_save = cbd.load_direct_python cbd.load_direct_python = lambda: {"direct-pkg"} try: with redirect_stdout(io.StringIO()), redirect_stderr(io.StringIO()): return cbd.main() finally: sys.argv = argv_save cbd.load_direct_python = loader_save def test_clean_passes(self): # Reality matches all 3 claimed (name, version) pairs. rc = self._run([ ("direct-pkg", "1.0.0"), ("transitive-pkg", "2.0.0"), ("transitive-pkg", "2.5.0"), ]) self.assertEqual(rc, 0) def test_transitive_drift_strict_fails(self): rc = self._run([ ("direct-pkg", "1.0.0"), ("transitive-pkg", "2.0.0"), ("transitive-pkg", "2.6.0"), # bumped from 2.5.0 ]) self.assertEqual(rc, 1) def test_transitive_drift_with_flag_passes(self): rc = self._run( [ ("direct-pkg", "1.0.0"), ("transitive-pkg", "2.0.0"), ("transitive-pkg", "2.6.0"), ], "--ignore-transitive-version", ) self.assertEqual(rc, 0) def test_direct_drift_with_flag_still_fails(self): rc = self._run( [ ("direct-pkg", "1.1.0"), # bumped ("transitive-pkg", "2.0.0"), ("transitive-pkg", "2.5.0"), ], "--ignore-transitive-version", ) self.assertEqual(rc, 1) def test_added_with_flag_still_fails(self): rc = self._run( [ ("direct-pkg", "1.0.0"), ("transitive-pkg", "2.0.0"), ("transitive-pkg", "2.5.0"), ("brand-new", "9.9.9"), # not claimed ], "--ignore-transitive-version", ) self.assertEqual(rc, 1) def test_stale_with_flag_still_fails(self): # Drop both versions of transitive-pkg from reality. rc = self._run( [("direct-pkg", "1.0.0")], "--ignore-transitive-version", ) self.assertEqual(rc, 1) def test_dropping_one_of_multi_versions_is_drift_not_stale(self): # transitive-pkg is still in reality (at one version); the missing # version is drift — passes with the flag. rc = self._run( [ ("direct-pkg", "1.0.0"), ("transitive-pkg", "2.5.0"), ], "--ignore-transitive-version", ) self.assertEqual(rc, 0) if __name__ == "__main__": unittest.main() ================================================ FILE: bin/litellm-config.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # The default configuration file for starting litellm (https://docs.litellm.ai/docs/proxy/quick_start) # To start the litellm service: # 1. Install litellm by: # pip install 'litellm[proxy]' # 2. Set your API keys as environment variable, e.g. # export ANTHROPIC_API_KEY= # 3. Start litellm by: # litellm --config bin/litellm-config.yaml # By default, litellm is running on http://0.0.0.0:4000 model_list: - model_name: claude-haiku-4.5 litellm_params: model: claude-haiku-4-5-20251001 api_key: "os.environ/ANTHROPIC_API_KEY" - model_name: gpt-5-mini litellm_params: model: gpt-5-mini-2025-08-07 api_key: "os.environ/OPENAI_API_KEY" ================================================ FILE: bin/merge-image-tags.sh ================================================ #!/bin/bash # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. set -e # Accept parameters from command line DEFAULT_TAG="latest" DEFAULT_SERVICES="*" BASE_TAG="${1:-$DEFAULT_TAG}" SERVICES_INPUT="${2:-$DEFAULT_SERVICES}" echo "Using base tag: $BASE_TAG" echo "Services to merge: $SERVICES_INPUT" # Convert input into array for lookup IFS=',' read -ra SELECTED_SERVICES <<< "$SERVICES_INPUT" # Helper to check if a service should be merged should_merge() { local svc="$1" if [[ "$SERVICES_INPUT" == "*" ]]; then return 0 fi for sel in "${SELECTED_SERVICES[@]}"; do sel="$(echo -e "${sel}" | tr -d '[:space:]')" if [[ "$svc" == "$sel" ]]; then return 0 fi done return 1 } cd "$(dirname "$0")" # Detect all Dockerfiles and extract service names dockerfiles=( *.dockerfile ) if [[ ${#dockerfiles[@]} -eq 0 ]]; then echo "❌ No Dockerfiles found in the current directory." exit 1 fi services=() for file in "${dockerfiles[@]}"; do svc=$(basename "$file" .dockerfile) services+=("$svc") done # Add additional services that don't have a *.dockerfile in the deployment root services+=("pylsp" "y-websocket-server") echo "🔗 Merging multi-arch manifests for tag :$BASE_TAG" for svc in "${services[@]}"; do # Skip if not selected by user if ! should_merge "$svc"; then echo "⏭️ Skipping $svc" continue fi echo "🔄 Creating manifest for texera/$svc:$BASE_TAG" docker buildx imagetools create \ -t texera/$svc:$BASE_TAG \ texera/$svc:${BASE_TAG}-amd64 \ texera/$svc:${BASE_TAG}-arm64 echo "✅ Created manifest: texera/$svc:$BASE_TAG" done echo "" echo "==========================================" echo "✅ All manifests merged successfully!" echo "==========================================" ================================================ FILE: bin/pylsp/Dockerfile ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Apache Texera is an effort undergoing incubation at The Apache Software # Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is # required of all newly accepted projects until a further review indicates # that the infrastructure, communications, and decision-making process have # stabilized in a manner consistent with other successful ASF projects. # While incubation status is not necessarily a reflection of the # completeness or stability of the code, it does indicate that the project # has yet to be fully endorsed by the ASF. # Use an official Python runtime as a parent image FROM python:3.10-slim # Set the working directory in the container WORKDIR /usr/src/app # Install pylsp RUN pip install python-lsp-server[all]==1.5.0 python-lsp-server[websockets] pylint==2.15.10 RUN apt update RUN apt install -y htop RUN apt update && apt install -y --no-install-recommends bash htop # Copy the bash script into the container COPY run_pylsp.sh /usr/src/app/run_pylsp.sh # Make the script executable RUN chmod +x /usr/src/app/run_pylsp.sh # Set the entrypoint to the script ENTRYPOINT ["/usr/bin/env", "bash", "/usr/src/app/run_pylsp.sh"] # Run pylsp on container startup # ENTRYPOINT ["pylsp", "--ws", "--port", "3000", "--verbose", "--check-parent-process"] ================================================ FILE: bin/pylsp/python-language-server.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. apiVersion: apps/v1 kind: Deployment metadata: name: python-language-server-deployment namespace: texera labels: app: python-language-server spec: replicas: 8 selector: matchLabels: app: python-language-server template: metadata: labels: app: python-language-server spec: containers: - name: python-language-server image: jxzliu/pylsp:latest imagePullPolicy: Always ports: - containerPort: 3000 resources: limits: cpu: 1 memory: "200Mi" imagePullSecrets: - name: regcred --- apiVersion: v1 kind: Service metadata: name: python-language-server-service namespace: texera labels: app: python-language-server spec: selector: app: python-language-server ports: - protocol: TCP port: 3000 targetPort: 3000 ================================================ FILE: bin/pylsp/run_pylsp.sh ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. #!/bin/bash # Function to forcibly restart pylsp restart_pylsp() { echo "Restarting pylsp..." # Kill the previous session of pylsp if it's still running [ ! -z "$PYLSP_PID" ] && kill -SIGTERM "$PYLSP_PID" # Start pylsp in a new session and redirect all output to stdout setsid pylsp --ws --port 3000 --verbose 2>&1 & PYLSP_PID=$! } # Setup traps for interruption and termination signals trap 'kill -SIGTERM "$PYLSP_PID"; exit 0' SIGINT SIGTERM # Start pylsp initially restart_pylsp # Main loop to restart pylsp every 5 minutes while true; do # Wait for 10 minutes sleep 300 # Restart pylsp restart_pylsp done ================================================ FILE: bin/python-language-service.sh ================================================ #!/bin/bash # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. set -e DEFAULT_PROVIDER="pylsp" DEFAULT_PORT=3000 PROVIDER="" PORT="" BASE_DIR=$(dirname "$0") PYRIGHT_DIR="$BASE_DIR/../pyright-language-service" while [ $# -gt 0 ]; do case "$1" in --server=*|--provider=*) PROVIDER="${1#*=}" ;; --port=*) PORT="${1#*=}" ;; *) echo "Unknown argument: $1" echo "Usage: $0 [--server=] [--port=]" exit 1 ;; esac shift done PROVIDER="${PROVIDER:-$DEFAULT_PROVIDER}" PORT="${PORT:-$DEFAULT_PORT}" # Validate port value if ! [[ "$PORT" =~ ^[0-9]+$ ]]; then echo "Invalid port: $PORT. Must be a number." exit 1 fi start_pyright() { echo "Starting Pyright Language Server on port $PORT..." cd "$PYRIGHT_DIR" yarn install --silent yarn start --port="$PORT" } start_pylsp() { echo "Starting Pylsp Language Server on port $PORT..." if ! command -v pylsp &>/dev/null; then echo "Error: pylsp is not installed. Install it with 'pip install python-lsp-server'." exit 1 fi pylsp --port "$PORT" --ws } case $PROVIDER in pyright) start_pyright ;; pylsp) start_pylsp ;; *) echo "Invalid provider: $PROVIDER. Valid options are: pyright, pylsp." exit 1 ;; esac ================================================ FILE: bin/python-proto-gen.sh ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # assuming inside the pytexera executing Python ENV # dirs TEXERA_HOME="$(git rev-parse --show-toplevel)" AMBER_DIR="$TEXERA_HOME/amber" PYAMBER_DIR="$AMBER_DIR/src/main/python" PROTOBUF_AMBER_DIR="$AMBER_DIR/src/main/protobuf" CORE_DIR="$TEXERA_HOME/common/workflow-core" PROTOBUF_CORE_DIR="$CORE_DIR/src/main/protobuf" # proto-gen protoc --python_betterproto_out="$PYAMBER_DIR/proto" \ -I="$PROTOBUF_AMBER_DIR" \ -I="$PROTOBUF_CORE_DIR" \ $(find "$PROTOBUF_AMBER_DIR" -iname "*.proto") \ $(find "$PROTOBUF_CORE_DIR" -iname "*.proto") ================================================ FILE: bin/server.sh ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. (cd amber && target/texera-*/bin/texera-web-application) ================================================ FILE: bin/shared-editing-server.sh ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. (cd frontend && npx y-websocket) ================================================ FILE: bin/single-node/DISCLAIMER ================================================ Apache Texera is an effort undergoing incubation at The Apache Software Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is required of all newly accepted projects until a further review indicates that the infrastructure, communications, and decision-making process have stabilized in a manner consistent with other successful ASF projects. While incubation status is not necessarily a reflection of the completeness or stability of the code, it does indicate that the project has yet to be fully endorsed by the ASF. ================================================ FILE: bin/single-node/LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: bin/single-node/NOTICE ================================================ Apache Texera (Incubating) Copyright 2025 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). ================================================ FILE: bin/single-node/README.md ================================================ This document describes how to set up and run Texera on a single machine using "Docker Compose". ## Prerequisites Before starting, make sure your computer meets the following requirements: | Resource Type | Minimum | Recommended | |-------------|---------|-------------| | CPU Cores | 2 | 8 | | Memory | 4GB | 16GB | | Disk Space | 20GB | 50GB | You also need to install and launch Docker Desktop on your computer. Choose the right installation link for your computer: | Operating System | Installation Link | |-----------------|-------------------| | macOS | [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/) | | Windows | [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/) | | Linux | [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/) | After installing and launching Docker Desktop, verify that Docker and Docker Compose are available by running the following commands from the command line: ```bash docker --version docker compose version ``` You should see output messages like the following (your versions may be different): ``` $ docker --version Docker version 27.5.1, build 9f9e405 $ docker compose version Docker Compose version v2.23.0-desktop.1 ``` By default, Texera services require ports **8080** and **9000** to be free. If either port is already in use, the services will fail to start. On macOS or Linux, run the following commands to check: ``` lsof -i :8080 lsof -i :9000 ``` If either command produces output, that port is occupied by another process. You will need to either stop that process or change Texera's port configuration. See [Advanced Settings > Run Texera on other ports](#run-texera-on-other-ports) for instructions. --- ## Launch Texera Enter the extracted directory and run the following command to start Texera: ```bash docker compose --profile examples up ``` This command will start docker containers that host the Texera services, and pre-create two example workflows and datasets. If you don't want to have these examples pre-created, run the following command instead: ```bash docker compose up ``` > If you see the error message like `unable to get image 'nginx:alpine': Cannot connect to the Docker daemon at unix:///Users/kunwoopark/.docker/run/docker.sock. Is the docker daemon running?`, please make sure Docker Desktop is installed and running > When you start Texera for the first time, it will take around 5 minutes to download needed images. The system should be ready around 1.5 minutes. After seeing the following startup message: ``` ... ========================================= Texera has started successfully! Access at: http://localhost:8080 ========================================= ... ``` you can open the browser and navigate to the URL shown in the message. Input the default account `texera` with password `texera`, and then click on the `Sign In` button to login: texera-login ## Stop, Restart, and Uninstall Texera ### Stop Press `Ctrl+C` in the terminal to stop Texera. If you already closed the terminal, you can go to the installation folder and run: ```bash docker compose --profile examples stop ``` to stop Texera. ### Restart Same as the way you [launch Texera](#launch-texera). ### Uninstall To remove Texera and all its data, go to the installation folder and run: ```bash docker compose --profile examples down -v ``` > ⚠️ Warning: This will permanently delete all the data used by Texera. ## Enable the Texera Agent The Texera agent is powered by a large language model (LLM). By default, Texera uses [Claude Haiku 4.5](https://www.anthropic.com/claude/haiku) as the LLM and queries it through [LiteLLM](https://docs.litellm.ai/). Without an API key, the Texera agent panel still appears but model calls will fail with a provider auth error. To enable it: 1. [Stop Texera](#stop) if it is already running. 2. Get an API key for the LLM. Since Claude Haiku 4.5 is enabled by default, you need an [Anthropic API key](https://console.anthropic.com/settings/keys). 3. Export the key and restart Texera: ```bash export ANTHROPIC_API_KEY=sk-ant-... docker compose --profile examples up ``` Once Texera is up, create a new workflow and open the Texera agent panel at the bottom right. Type a task like: > For /texera/popular-movies-of-imdb/v1/TMDb_updated.csv, visualize the top 10 most-voted movies. To switch providers or add more LLMs, see [Add more LLMs or providers](#add-more-llms-or-providers). ## Advanced Settings Before making any of the changes below, please [stop Texera](#stop) first. Once you finish the changes, [restart Texera](#restart) to apply them. All changes below are to the `.env` file in the installation folder, unless otherwise noted. ### Run Texera on other ports By default, Texera uses: - Port 8080 for its web service - Port 9000 for its MinIO storage service To change these ports, open the `.env` file and update the corresponding variables: - For the web service port (8080): change `TEXERA_PORT=8080` to your desired port, e.g., `TEXERA_PORT=8081`. - For the MinIO port (9000): change `MINIO_PORT=9000` to your desired port, e.g., `MINIO_PORT=9001`. ### Change the locations of Texera data By default, Docker manages Texera's data locations. To change them to your own locations: - Find the `persistent volumes` section. For each data volume you want to specify, add the following configuration: ```yaml volume_name: driver: local driver_opts: type: none o: bind device: /path/to/your/local/folder ``` For example, to change the folder of storing `workflow_result_data` to `/Users/johndoe/texera/data`, add the following: ```yaml workflow_result_data: driver: local driver_opts: type: none o: bind device: /Users/johndoe/texera/data ``` If you already launched texera and want to change the data locations, existing data volumes need to be recreated and override in the next boot-up, i.e. select `y` when running `docker compose up` again: ``` $ docker compose up ? Volume "texera-single-node-release-1-1-0_workflow_result_data" exists but doesn't match configuration in compose file. Recreate (data will be lost)? (y/N) y // answer y to this prompt ``` ### Add more LLMs or providers Only Claude Haiku 4.5 is enabled by default. To add more LLMs, open `litellm-config.yaml` in the installation folder and append entries under `model_list`. Each entry follows this shape: ```diff model_list: ... + - model_name: + litellm_params: + model: + api_key: "os.environ/" ``` For example, to add OpenAI's GPT-5.2 and Google's Gemini 2.5 Pro: ```diff model_list: ... + - model_name: gpt-5.2 + litellm_params: + model: gpt-5.2 + api_key: "os.environ/OPENAI_API_KEY" + + - model_name: gemini-2.5-pro + litellm_params: + model: gemini/gemini-2.5-pro + api_key: "os.environ/GEMINI_API_KEY" ``` Make sure to set the corresponding API key environment variable when you launch Texera (see [Enable the Texera Agent](#enable-the-texera-agent)). Get keys from each provider's console — for example, [OpenAI](https://platform.openai.com/api-keys) or [Google](https://aistudio.google.com/apikey). If your provider is not Anthropic, OpenAI, or Google, also pass its key into the LiteLLM container by editing `docker-compose.yml`: ```diff litellm: ... environment: ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} OPENAI_API_KEY: ${OPENAI_API_KEY:-} GEMINI_API_KEY: ${GEMINI_API_KEY:-} + : ${:-} ``` For the full list of supported providers and model IDs, see the [LiteLLM proxy config docs](https://docs.litellm.ai/docs/providers). ## Troubleshooting ### Port conflicts If Texera fails to start, a common cause is that ports 8080 or 9000 are already in use by another application. Check which ports are occupied: ``` lsof -i :8080 lsof -i :9000 ``` Stop the conflicting process, or change Texera's ports following the instructions in [Advanced Settings > Run Texera on other ports](#run-texera-on-other-ports). ### Volume conflicts PostgreSQL only runs the database initialization scripts on first startup (when its data volume is empty). If you previously started Texera and then ran `docker compose down` (without `-v`), the data volume still exists. On the next `docker compose up`, the initialization is skipped, which can cause services like lakeFS to fail because their required databases were never created. To resolve this, remove all existing volumes and start fresh: ``` docker compose --profile examples down -v docker compose --profile examples up ``` > ⚠️ Warning: `docker compose --profile examples down -v` permanently deletes all Texera data. ================================================ FILE: bin/single-node/docker-compose.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. name: texera-single-node services: # Part1: Specification of the storage services used by Texera # MinIO is an S3-compatible object storage used to store datasets and files. minio: image: minio/minio:RELEASE.2025-02-28T09-55-16Z container_name: texera-minio ports: - "${MINIO_PORT:-9000}:9000" env_file: - .env environment: - MINIO_ROOT_USER=${STORAGE_S3_AUTH_USERNAME} - MINIO_ROOT_PASSWORD=${STORAGE_S3_AUTH_PASSWORD} volumes: - minio_data:/data command: server --console-address ":9001" /data healthcheck: test: ["CMD", "curl", "-sf", "http://localhost:9000/minio/health/live"] interval: 5s timeout: 3s retries: 10 # This job creates the S3 bucket used by the Iceberg warehouse that the # Lakekeeper service manages. minio-init: image: minio/mc:RELEASE.2025-05-21T01-59-54Z container_name: texera-minio-init depends_on: minio: condition: service_healthy env_file: - .env restart: "no" entrypoint: ["/bin/sh", "-c"] command: - | set -e mc alias set local "$$STORAGE_S3_ENDPOINT" "$$STORAGE_S3_AUTH_USERNAME" "$$STORAGE_S3_AUTH_PASSWORD" mc mb --ignore-existing "local/$$STORAGE_ICEBERG_CATALOG_REST_S3_BUCKET" echo "MinIO bucket '$$STORAGE_ICEBERG_CATALOG_REST_S3_BUCKET' is ready." # PostgreSQL with PGroonga extension for full-text search. # Used by lakeFS and Texera's metadata storage. postgres: image: groonga/pgroonga:4.0.1-debian-15 container_name: texera-postgres restart: always env_file: - .env healthcheck: test: ["CMD", "pg_isready", "-U", "texera", "-d", "texera_db"] interval: 10s retries: 5 start_period: 5s volumes: - postgres_data:/var/lib/postgresql/data # mount the sql files for initializing the postgres - ../../sql:/docker-entrypoint-initdb.d # lakeFS is the underlying storage of Texera's dataset service lakefs: image: treeverse/lakefs:1.51 container_name: texera-lakefs restart: always depends_on: postgres: condition: service_healthy minio: condition: service_started env_file: - .env environment: # This port also need to be changed if the port of MinIO service is changed - LAKEFS_BLOCKSTORE_S3_PRE_SIGNED_ENDPOINT=${TEXERA_HOST}:${MINIO_PORT:-9000} - LAKEFS_BLOCKSTORE_S3_CREDENTIALS_ACCESS_KEY_ID=${STORAGE_S3_AUTH_USERNAME} - LAKEFS_BLOCKSTORE_S3_CREDENTIALS_SECRET_ACCESS_KEY=${STORAGE_S3_AUTH_PASSWORD} entrypoint: ["/bin/sh", "-c"] command: - | lakefs setup --user-name "$LAKEFS_INSTALLATION_USER_NAME" --access-key-id "$LAKEFS_INSTALLATION_ACCESS_KEY_ID" --secret-access-key "$LAKEFS_INSTALLATION_SECRET_ACCESS_KEY" || true lakefs run & wait healthcheck: test: ["CMD", "wget", "--spider", "-q", "http://localhost:8000/api/v1/healthcheck"] interval: 10s timeout: 5s retries: 10 # This job applies Lakekeeper's database schema to the Postgres instance — # creating or upgrading the tables that Lakekeeper uses to track its catalog metadata. lakekeeper-migrate: image: vakamo/lakekeeper:v0.11.0 container_name: texera-lakekeeper-migrate depends_on: postgres: condition: service_healthy env_file: - .env restart: "no" entrypoint: ["/home/nonroot/lakekeeper"] command: ["migrate"] # Lakekeeper is the Iceberg REST catalog service lakekeeper: image: vakamo/lakekeeper:v0.11.0 container_name: texera-lakekeeper restart: always depends_on: postgres: condition: service_healthy minio: condition: service_started lakekeeper-migrate: condition: service_completed_successfully env_file: - .env entrypoint: ["/home/nonroot/lakekeeper"] command: ["serve"] healthcheck: test: ["CMD", "/home/nonroot/lakekeeper", "healthcheck"] interval: 10s timeout: 5s retries: 10 start_period: 10s # One-shot init container that creates the Lakekeeper default project and # the Iceberg warehouse pointing at the MinIO bucket prepared by minio-init. lakekeeper-init: image: alpine:3.19 container_name: texera-lakekeeper-init depends_on: lakekeeper: condition: service_healthy minio-init: condition: service_completed_successfully env_file: - .env restart: "no" entrypoint: [ "/bin/sh", "-c" ] command: - | set -e echo "Installing curl (to call Lakekeeper's management API) and jq (to parse its list responses)..." apk add --no-cache curl jq # Lakekeeper organizes warehouses under "projects" (its top-level tenant # abstraction). Texera is single-tenant, so we use the NIL UUID (all # zeros) as the project ID — with LAKEKEEPER__ENABLE_DEFAULT_PROJECT=true # (Lakekeeper's default), this is the project that any client request # without a project-id is routed to. echo "Step 1: Ensuring Lakekeeper's default project exists (top-level tenant that will hold the Iceberg warehouse)..." echo "Checking whether Lakekeeper's default project already exists..." PROJECT_GET_CODE=$$(curl -s -o /tmp/project.txt -w "%{http_code}" \ "$$LAKEKEEPER_BASE_URI/management/v1/project" || echo "000") if [ "$$PROJECT_GET_CODE" = "200" ]; then echo "Lakekeeper's default project already exists. Skipping creation." elif [ "$$PROJECT_GET_CODE" = "404" ]; then echo "Default project not found. Creating..." PROJECT_PAYLOAD='{"project-id": "00000000-0000-0000-0000-000000000000", "project-name": "default"}' PROJECT_CODE=$$(curl -s -o /tmp/response.txt -w "%{http_code}" \ -X POST \ -H "Content-Type: application/json" \ -d "$$PROJECT_PAYLOAD" \ "$$LAKEKEEPER_BASE_URI/management/v1/project" || echo "000") if [ "$$PROJECT_CODE" -lt 200 ] || [ "$$PROJECT_CODE" -ge 300 ]; then echo "Failed to create Lakekeeper's default project. HTTP Code: $$PROJECT_CODE" echo "ERROR RESPONSE:" if [ -f /tmp/response.txt ]; then cat /tmp/response.txt; fi echo "" exit 1 fi echo "Created Lakekeeper's default project successfully (HTTP $$PROJECT_CODE)." else echo "Failed to check Lakekeeper's default project. HTTP Code: $$PROJECT_GET_CODE" echo "ERROR RESPONSE:" if [ -f /tmp/project.txt ]; then cat /tmp/project.txt; fi echo "" exit 1 fi echo "Step 2: Ensuring Warehouse '$$STORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME' exists..." # Detect existing warehouse first so we only POST when it's actually # missing — keeps this init idempotent across re-runs. echo "Checking whether Lakekeeper Warehouse '$$STORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME' already exists..." WAREHOUSE_LIST_CODE=$$(curl -s -o /tmp/warehouses.txt -w "%{http_code}" \ "$$LAKEKEEPER_BASE_URI/management/v1/warehouse" || echo "000") if [ "$$WAREHOUSE_LIST_CODE" -lt 200 ] || [ "$$WAREHOUSE_LIST_CODE" -ge 300 ]; then echo "Failed to list Lakekeeper warehouses. HTTP Code: $$WAREHOUSE_LIST_CODE" echo "ERROR RESPONSE:" if [ -f /tmp/warehouses.txt ]; then cat /tmp/warehouses.txt; fi echo "" exit 1 fi if jq -e --arg name "$$STORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME" '.warehouses[]? | select(.name == $$name)' /tmp/warehouses.txt >/dev/null; then echo "Lakekeeper Warehouse '$$STORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME' already exists. Skipping creation." else echo "Warehouse not found. Creating '$$STORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME'..." CREATE_PAYLOAD=$$(cat < sh -c ' echo ""; echo "========================================="; echo " Texera has started successfully!"; echo " Access at: http://localhost:$$TEXERA_PORT"; echo "========================================="; echo ""; ' # Part 4: Optional one-shot jobs # Loads example datasets and workflows into Texera on first startup. # Only runs when the "examples" profile is activated: # docker compose --profile examples up example-data-loader: image: alpine:latest depends_on: dashboard-service: condition: service_healthy file-service: condition: service_healthy volumes: - ./examples:/examples:ro environment: - TEXERA_DASHBOARD_SERVICE_URL=http://dashboard-service:8080/api - TEXERA_FILE_SERVICE_URL=http://file-service:9092/api - TEXERA_EXAMPLE_USERNAME=${USER_SYS_ADMIN_USERNAME} - TEXERA_EXAMPLE_PASSWORD=${USER_SYS_ADMIN_PASSWORD} restart: "no" profiles: - examples command: > sh -c 'apk add --no-cache curl jq bash > /dev/null 2>&1 && bash /examples/load-examples.sh' networks: default: name: texera-single-node # persistent volumes volumes: minio_data: postgres_data: workflow_result_data: ================================================ FILE: bin/single-node/examples/datasets/iris-species/Iris.csv ================================================ Id,SepalLengthCm,SepalWidthCm,PetalLengthCm,PetalWidthCm,Species 1,5.1,3.5,1.4,0.2,Iris-setosa 2,4.9,3.0,1.4,0.2,Iris-setosa 3,4.7,3.2,1.3,0.2,Iris-setosa 4,4.6,3.1,1.5,0.2,Iris-setosa 5,5.0,3.6,1.4,0.2,Iris-setosa 6,5.4,3.9,1.7,0.4,Iris-setosa 7,4.6,3.4,1.4,0.3,Iris-setosa 8,5.0,3.4,1.5,0.2,Iris-setosa 9,4.4,2.9,1.4,0.2,Iris-setosa 10,4.9,3.1,1.5,0.1,Iris-setosa 11,5.4,3.7,1.5,0.2,Iris-setosa 12,4.8,3.4,1.6,0.2,Iris-setosa 13,4.8,3.0,1.4,0.1,Iris-setosa 14,4.3,3.0,1.1,0.1,Iris-setosa 15,5.8,4.0,1.2,0.2,Iris-setosa 16,5.7,4.4,1.5,0.4,Iris-setosa 17,5.4,3.9,1.3,0.4,Iris-setosa 18,5.1,3.5,1.4,0.3,Iris-setosa 19,5.7,3.8,1.7,0.3,Iris-setosa 20,5.1,3.8,1.5,0.3,Iris-setosa 21,5.4,3.4,1.7,0.2,Iris-setosa 22,5.1,3.7,1.5,0.4,Iris-setosa 23,4.6,3.6,1.0,0.2,Iris-setosa 24,5.1,3.3,1.7,0.5,Iris-setosa 25,4.8,3.4,1.9,0.2,Iris-setosa 26,5.0,3.0,1.6,0.2,Iris-setosa 27,5.0,3.4,1.6,0.4,Iris-setosa 28,5.2,3.5,1.5,0.2,Iris-setosa 29,5.2,3.4,1.4,0.2,Iris-setosa 30,4.7,3.2,1.6,0.2,Iris-setosa 31,4.8,3.1,1.6,0.2,Iris-setosa 32,5.4,3.4,1.5,0.4,Iris-setosa 33,5.2,4.1,1.5,0.1,Iris-setosa 34,5.5,4.2,1.4,0.2,Iris-setosa 35,4.9,3.1,1.5,0.1,Iris-setosa 36,5.0,3.2,1.2,0.2,Iris-setosa 37,5.5,3.5,1.3,0.2,Iris-setosa 38,4.9,3.1,1.5,0.1,Iris-setosa 39,4.4,3.0,1.3,0.2,Iris-setosa 40,5.1,3.4,1.5,0.2,Iris-setosa 41,5.0,3.5,1.3,0.3,Iris-setosa 42,4.5,2.3,1.3,0.3,Iris-setosa 43,4.4,3.2,1.3,0.2,Iris-setosa 44,5.0,3.5,1.6,0.6,Iris-setosa 45,5.1,3.8,1.9,0.4,Iris-setosa 46,4.8,3.0,1.4,0.3,Iris-setosa 47,5.1,3.8,1.6,0.2,Iris-setosa 48,4.6,3.2,1.4,0.2,Iris-setosa 49,5.3,3.7,1.5,0.2,Iris-setosa 50,5.0,3.3,1.4,0.2,Iris-setosa 51,7.0,3.2,4.7,1.4,Iris-versicolor 52,6.4,3.2,4.5,1.5,Iris-versicolor 53,6.9,3.1,4.9,1.5,Iris-versicolor 54,5.5,2.3,4.0,1.3,Iris-versicolor 55,6.5,2.8,4.6,1.5,Iris-versicolor 56,5.7,2.8,4.5,1.3,Iris-versicolor 57,6.3,3.3,4.7,1.6,Iris-versicolor 58,4.9,2.4,3.3,1.0,Iris-versicolor 59,6.6,2.9,4.6,1.3,Iris-versicolor 60,5.2,2.7,3.9,1.4,Iris-versicolor 61,5.0,2.0,3.5,1.0,Iris-versicolor 62,5.9,3.0,4.2,1.5,Iris-versicolor 63,6.0,2.2,4.0,1.0,Iris-versicolor 64,6.1,2.9,4.7,1.4,Iris-versicolor 65,5.6,2.9,3.6,1.3,Iris-versicolor 66,6.7,3.1,4.4,1.4,Iris-versicolor 67,5.6,3.0,4.5,1.5,Iris-versicolor 68,5.8,2.7,4.1,1.0,Iris-versicolor 69,6.2,2.2,4.5,1.5,Iris-versicolor 70,5.6,2.5,3.9,1.1,Iris-versicolor 71,5.9,3.2,4.8,1.8,Iris-versicolor 72,6.1,2.8,4.0,1.3,Iris-versicolor 73,6.3,2.5,4.9,1.5,Iris-versicolor 74,6.1,2.8,4.7,1.2,Iris-versicolor 75,6.4,2.9,4.3,1.3,Iris-versicolor 76,6.6,3.0,4.4,1.4,Iris-versicolor 77,6.8,2.8,4.8,1.4,Iris-versicolor 78,6.7,3.0,5.0,1.7,Iris-versicolor 79,6.0,2.9,4.5,1.5,Iris-versicolor 80,5.7,2.6,3.5,1.0,Iris-versicolor 81,5.5,2.4,3.8,1.1,Iris-versicolor 82,5.5,2.4,3.7,1.0,Iris-versicolor 83,5.8,2.7,3.9,1.2,Iris-versicolor 84,6.0,2.7,5.1,1.6,Iris-versicolor 85,5.4,3.0,4.5,1.5,Iris-versicolor 86,6.0,3.4,4.5,1.6,Iris-versicolor 87,6.7,3.1,4.7,1.5,Iris-versicolor 88,6.3,2.3,4.4,1.3,Iris-versicolor 89,5.6,3.0,4.1,1.3,Iris-versicolor 90,5.5,2.5,4.0,1.3,Iris-versicolor 91,5.5,2.6,4.4,1.2,Iris-versicolor 92,6.1,3.0,4.6,1.4,Iris-versicolor 93,5.8,2.6,4.0,1.2,Iris-versicolor 94,5.0,2.3,3.3,1.0,Iris-versicolor 95,5.6,2.7,4.2,1.3,Iris-versicolor 96,5.7,3.0,4.2,1.2,Iris-versicolor 97,5.7,2.9,4.2,1.3,Iris-versicolor 98,6.2,2.9,4.3,1.3,Iris-versicolor 99,5.1,2.5,3.0,1.1,Iris-versicolor 100,5.7,2.8,4.1,1.3,Iris-versicolor 101,6.3,3.3,6.0,2.5,Iris-virginica 102,5.8,2.7,5.1,1.9,Iris-virginica 103,7.1,3.0,5.9,2.1,Iris-virginica 104,6.3,2.9,5.6,1.8,Iris-virginica 105,6.5,3.0,5.8,2.2,Iris-virginica 106,7.6,3.0,6.6,2.1,Iris-virginica 107,4.9,2.5,4.5,1.7,Iris-virginica 108,7.3,2.9,6.3,1.8,Iris-virginica 109,6.7,2.5,5.8,1.8,Iris-virginica 110,7.2,3.6,6.1,2.5,Iris-virginica 111,6.5,3.2,5.1,2.0,Iris-virginica 112,6.4,2.7,5.3,1.9,Iris-virginica 113,6.8,3.0,5.5,2.1,Iris-virginica 114,5.7,2.5,5.0,2.0,Iris-virginica 115,5.8,2.8,5.1,2.4,Iris-virginica 116,6.4,3.2,5.3,2.3,Iris-virginica 117,6.5,3.0,5.5,1.8,Iris-virginica 118,7.7,3.8,6.7,2.2,Iris-virginica 119,7.7,2.6,6.9,2.3,Iris-virginica 120,6.0,2.2,5.0,1.5,Iris-virginica 121,6.9,3.2,5.7,2.3,Iris-virginica 122,5.6,2.8,4.9,2.0,Iris-virginica 123,7.7,2.8,6.7,2.0,Iris-virginica 124,6.3,2.7,4.9,1.8,Iris-virginica 125,6.7,3.3,5.7,2.1,Iris-virginica 126,7.2,3.2,6.0,1.8,Iris-virginica 127,6.2,2.8,4.8,1.8,Iris-virginica 128,6.1,3.0,4.9,1.8,Iris-virginica 129,6.4,2.8,5.6,2.1,Iris-virginica 130,7.2,3.0,5.8,1.6,Iris-virginica 131,7.4,2.8,6.1,1.9,Iris-virginica 132,7.9,3.8,6.4,2.0,Iris-virginica 133,6.4,2.8,5.6,2.2,Iris-virginica 134,6.3,2.8,5.1,1.5,Iris-virginica 135,6.1,2.6,5.6,1.4,Iris-virginica 136,7.7,3.0,6.1,2.3,Iris-virginica 137,6.3,3.4,5.6,2.4,Iris-virginica 138,6.4,3.1,5.5,1.8,Iris-virginica 139,6.0,3.0,4.8,1.8,Iris-virginica 140,6.9,3.1,5.4,2.1,Iris-virginica 141,6.7,3.1,5.6,2.4,Iris-virginica 142,6.9,3.1,5.1,2.3,Iris-virginica 143,5.8,2.7,5.1,1.9,Iris-virginica 144,6.8,3.2,5.9,2.3,Iris-virginica 145,6.7,3.3,5.7,2.5,Iris-virginica 146,6.7,3.0,5.2,2.3,Iris-virginica 147,6.3,2.5,5.0,1.9,Iris-virginica 148,6.5,3.0,5.2,2.0,Iris-virginica 149,6.2,3.4,5.4,2.3,Iris-virginica 150,5.9,3.0,5.1,1.8,Iris-virginica ================================================ FILE: bin/single-node/examples/datasets/iris-species/description.txt ================================================ This dataset is from Kaggle at https://www.kaggle.com/datasets/uciml/iris ================================================ FILE: bin/single-node/examples/datasets/popular-movies-of-imdb/TMDb_updated.csv ================================================ id,title,overview,original_language,vote_count,vote_average 0,Ad Astra,"The near future, a time when both hope and hardships drive humanity to look to the stars and beyond. While a mysterious phenomenon menaces to destroy life on planet Earth, astronaut Roy McBride undertakes a mission across the immensity of space and its many perils to uncover the truth about a lost expedition that decades before boldly faced emptiness and silence in search of the unknown.",en,2853,5.9 1,Bloodshot,"After he and his wife are murdered, marine Ray Garrison is resurrected by a team of scientists. Enhanced with nanotechnology, he becomes a superhuman, biotech killing machine—'Bloodshot'. As Ray first trains with fellow super-soldiers, he cannot recall anything from his former life. But when his memories flood back and he remembers the man that killed both him and his wife, he breaks out of the facility to get revenge, only to discover that there's more to the conspiracy than he thought.",en,1349,7.2 2,Bad Boys for Life,"Marcus and Mike are forced to confront new threats, career changes, and midlife crises as they join the newly created elite team AMMO of the Miami police department to take down the ruthless Armando Armas, the vicious leader of a Miami drug cartel.",en,2530,7.1 3,Ant-Man,"Armed with the astonishing ability to shrink in scale but increase in strength, master thief Scott Lang must embrace his inner-hero and help his mentor, Doctor Hank Pym, protect the secret behind his spectacular Ant-Man suit from a new generation of towering threats. Against seemingly insurmountable obstacles, Pym and Lang must plan and pull off a heist that will save the world.",en,13611,7.1 4,Percy Jackson: Sea of Monsters,"In their quest to confront the ultimate evil, Percy and his friends battle swarms of mythical creatures to find the mythical Golden Fleece and to stop an ancient evil from rising.",en,3542,5.9 5,Birds of Prey (and the Fantabulous Emancipation of One Harley Quinn),"Harley Quinn joins forces with a singer, an assassin and a police detective to help a young girl who had a hit placed on her after she stole a rare diamond from a crime lord.",en,2639,7.1 6,Live Free or Die Hard,"John McClane is back and badder than ever, and this time he's working for Homeland Security. He calls on the services of a young hacker in his bid to stop a ring of Internet terrorists intent on taking control of America's computer infrastructure.",en,3714,6.5 7,Cold Blood,"A legendary but retired hit man lives in peace and isolation in the barren North American wilderness. When he rescues a woman from a snowmobiling accident, he soon discovers that she's harboring a secret that forces him to return to his lethal ways.",fr,119,5.1 8,Underwater,"After an earthquake destroys their underwater station, six researchers must navigate two miles along the dangerous, unknown depths of the ocean floor to make it to safety in a race against time.",en,584,6.5 9,The Platform,"A mysterious place, an indescribable prison, a deep hole. An unknown number of levels. Two inmates living on each level. A descending platform containing food for all of them. An inhuman fight for survival, but also an opportunity for solidarity…",es,1924,7.2 10,Jumanji: The Next Level,"As the gang return to Jumanji to rescue one of their own, they discover that nothing is as they expect. The players will have to brave parts unknown and unexplored in order to escape the world’s most dangerous game.",en,2974,6.8 11,The Twilight Saga: Eclipse,"Bella once again finds herself surrounded by danger as Seattle is ravaged by a string of mysterious killings and a malicious vampire continues her quest for revenge. In the midst of it all, she is forced to choose between her love for Edward and her friendship with Jacob, knowing that her decision has the potential to ignite the ageless struggle between vampire and werewolf. With her graduation quickly approaching, Bella is confronted with the most important decision of her life.",en,5687,6.1 12,Sonic the Hedgehog,"Based on the global blockbuster videogame franchise from Sega, Sonic the Hedgehog tells the story of the world’s speediest hedgehog as he embraces his new home on Earth. In this live-action adventure comedy, Sonic and his new best friend team up to defend the planet from the evil genius Dr. Robotnik and his plans for world domination.",en,2066,7.4 13,Star Wars: The Rise of Skywalker,"The surviving Resistance faces the First Order once again as the journey of Rey, Finn and Poe Dameron continues. With the power and knowledge of generations behind them, the final battle begins.",en,3800,6.5 14,Onward,"In a suburban fantasy world, two teenage elf brothers embark on an extraordinary quest to discover if there is still a little magic left out there.",en,956,8 15,Emma.,"In 1800s England, a well-meaning but selfish young woman meddles in the love lives of her friends.",en,148,7.1 16,Pocahontas II: Journey to a New World,"When news of John Smith's death reaches America, Pocahontas is devastated. She sets off to London with John Rolfe, to meet with the King of England on a diplomatic mission: to create peace and respect between the two great lands. However, Governor Ratcliffe is still around; he wants to return to Jamestown and take over. He will stop at nothing to discredit the young princess.",en,845,5.3 17,Lara Croft: Tomb Raider - The Cradle of Life,"Lara Croft ventures to an underwater temple in search of the mythological Pandora's Box but, after securing it, it is promptly stolen by the villainous leader of a Chinese crime syndicate. Lara must recover the box before the syndicate's evil mastermind uses it to construct a weapon of catastrophic capabilities.",en,2896,5.7 18,The Invisible Man,"When Cecilia's abusive ex takes his own life and leaves her his fortune, she suspects his death was a hoax. As a series of coincidences turn lethal, Cecilia works to prove that she is being hunted by someone nobody can see.",en,1249,7.2 19,Blood Father,An ex-con reunites with his estranged wayward 16-year old daughter to protect her from drug dealers who are trying to kill her.,en,946,6.1 20,A Rainy Day in New York,"Two young people arrive in New York to spend a weekend, but once they arrive they're met with bad weather and a series of adventures.",en,783,6.6 21,Joker,"During the 1980s, a failed stand-up comedian is driven insane and turns to a life of crime and chaos in Gotham City while becoming an infamous psychopathic crime figure.",en,10914,8.2 22,Miracle in Cell No. 7,"Separated from his daughter, a father with an intellectual disability must prove his innocence when he is jailed for the death of a commander's child.",tr,1639,8.4 23,The Hunt,"Twelve strangers wake up in a clearing. They don't know where they are—or how they got there. In the shadow of a dark internet conspiracy theory, ruthless elitists gather at a remote location to hunt humans for sport. But their master plan is about to be derailed when one of the hunted turns the tables on her pursuers.",en,376,6.9 24,Transformers: The Last Knight,"Autobots and Decepticons are at war, with humans on the sidelines. Optimus Prime is gone. The key to saving our future lies buried in the secrets of the past, in the hidden history of Transformers on Earth.",en,3761,6 25,Parasite,"All unemployed, Ki-taek's family takes peculiar interest in the wealthy and glamorous Parks for their livelihood until they get entangled in an unexpected incident.",ko,6046,8.5 26,F#*@BOIS,"Ace, 23, and Miko, 17, desperately want to become famous actors but it seems the Universe has a different plan for their lives.",tl,6,7.9 27,Pretty Little Stalker,A self help writer and her family become the target of a troubled girl.,en,2,4.5 28,Dolittle,"After losing his wife seven years earlier, the eccentric Dr. John Dolittle, famed doctor and veterinarian of Queen Victoria’s England, hermits himself away behind the high walls of Dolittle Manor with only his menagerie of exotic animals for company. But when the young queen falls gravely ill, a reluctant Dolittle is forced to set sail on an epic adventure to a mythical island in search of a cure, regaining his wit and courage as he crosses old adversaries and discovers wondrous creatures.",en,1093,6.8 29,Frozen II,"Elsa, Anna, Kristoff and Olaf head far into the forest to learn the truth about an ancient mystery of their kingdom.",en,3626,7.1 30,Harry Potter and the Deathly Hallows: Part 2,"Harry, Ron and Hermione continue their quest to vanquish the evil Voldemort once and for all. Just as things begin to look hopeless for the young wizards, Harry discovers a trio of magical objects that endow him with powers to rival Voldemort's formidable skills.",en,13267,8.1 31,Cars,"Lightning McQueen, a hotshot rookie race car driven to succeed, discovers that life is about the journey, not the finish line, when he finds himself unexpectedly detoured in the sleepy Route 66 town of Radiator Springs. On route across the country to the big Piston Cup Championship in California to compete against two seasoned pros, McQueen gets to know the town's offbeat characters.",en,8997,6.8 32,Teen Titans: The Judas Contract,Tara Markov is a girl who has power over earth and stone; she is also more than she seems. Is the newest Teen Titan an ally or a threat? And what are the mercenary Deathstroke's plans for the Titans?,en,301,7.2 33,Digimon Adventure: Last Evolution Kizuna,"Tai is now a university student, living alone, working hard at school, and working every day, but with his future still undecided. Meanwhile, Matt and others continue to work on Digimon incidents and activities that help people with their partner Digimon. When an unprecedented phenomenon occurs, the DigiDestined discover that when they grow up, their relationship with their partner Digimon will come closer to an end. As a countdown timer activates on the Digivice, they realize that the more they fight with their partner Digimon, the faster their bond breaks. Will they fight for others and lose their partner? The time to choose and decide is approaching fast. There is a short time before “chosen children” will become adults. This is the last adventure of Tai and Agumon.",ja,7,4.8 34,Contagion,"As an epidemic of a lethal airborne virus - that kills within days - rapidly grows, the worldwide medical community races to find a cure and control the panic that spreads faster than the virus itself.",en,3163,6.5 35,30 Days of Night: Dark Days,"After surviving the incidents in Barrow, Alaska, Stella Olemaun relocates to Los Angeles, where she intentionally attracts the attention of the local vampire population in order to avenge the death of her husband, Eben.",en,264,4.8 36,The Traitor,"Palermo, Sicily, 1980. Mafia member Tommaso Buscetta decides to move to Brazil with his family fleeing the constant war between the different clans of the criminal organization. But when, after living several misfortunes, he is forced to return to Italy, he makes a bold decision that will change his life and the destiny of Cosa Nostra forever.",it,509,7.8 37,Trolls World Tour,"Queen Poppy and Branch make a surprising discovery — there are other Troll worlds beyond their own, and their distinct differences create big clashes between these various tribes. When a mysterious threat puts all of the Trolls across the land in danger, Poppy, Branch, and their band of friends must embark on an epic quest to create harmony among the feuding Trolls to unite them against certain doom.",en,40,8.4 38,Harry Potter and the Philosopher's Stone,"Harry Potter has lived under the stairs at his aunt and uncle's house his whole life. But on his 11th birthday, he learns he's a powerful wizard -- with a place waiting for him at the Hogwarts School of Witchcraft and Wizardry. As he learns to harness his newfound powers with the help of the school's kindly headmaster, Harry uncovers the truth about his parents' deaths -- and about the villain who's to blame.",en,16220,7.9 39,1917,"At the height of the First World War, two young British soldiers must cross enemy territory and deliver a message that will stop a deadly attack on hundreds of soldiers.",en,4039,7.9 40,Harry Potter and the Deathly Hallows: Part 1,"Harry, Ron and Hermione walk away from their last year at Hogwarts to find and destroy the remaining Horcruxes, putting an end to Voldemort's bid for immortality. But with Harry's beloved Dumbledore dead and Voldemort's unscrupulous Death Eaters on the loose, the world is more dangerous than ever.",en,12395,7.8 41,The Gentlemen,"American expat Mickey Pearson has built a highly profitable marijuana empire in London. When word gets out that he’s looking to cash out of the business forever it triggers plots, schemes, bribery and blackmail in an attempt to steal his domain out from under him.",en,723,7.8 42,Coffee & Kareem,A Detroit cop reluctantly teams with his girlfriend's 11-year-old son to clear his name and take down the city's most ruthless criminal.,en,62,5.2 43,Aladdin,A kindhearted street urchin named Aladdin embarks on a magical adventure after finding a lamp that releases a wisecracking genie while a power-hungry Grand Vizier vies for the same lamp that has the power to make their deepest wishes come true.,en,5357,7.1 44,Brahms: The Boy II,"After a family moves into the Heelshire Mansion, their young son soon makes friends with a life-like doll called Brahms.",en,208,6.3 45,The Naked Gun: From the Files of Police Squad!,"When the incompetent Officer Frank Drebin seeks the ruthless killer of his partner, he stumbles upon an attempt to assassinate Queen Elizabeth.",en,2084,7.2 46,The Lion King,"Simba idolizes his father, King Mufasa, and takes to heart his own royal destiny. But not everyone in the kingdom celebrates the new cub's arrival. Scar, Mufasa's brother—and former heir to the throne—has plans of his own. The battle for Pride Rock is ravaged with betrayal, tragedy and drama, ultimately resulting in Simba's exile. With help from a curious pair of newfound friends, Simba will have to figure out how to grow up and take back what is rightfully his.",en,5171,7.1 47,Interstellar,Interstellar chronicles the adventures of a group of explorers who make use of a newly discovered wormhole to surpass the limitations on human space travel and conquer the vast distances involved in an interstellar voyage.,en,21589,8.3 48,John Wick: Chapter 3 - Parabellum,"Super-assassin John Wick returns with a $14 million price tag on his head and an army of bounty-hunting killers on his trail. After killing a member of the shadowy international assassin’s guild, the High Table, John Wick is excommunicado, but the world’s most ruthless hit men and women await his every turn.",en,4327,7.2 49,Knives Out,"When renowned crime novelist Harlan Thrombey is found dead at his estate just after his 85th birthday, the inquisitive and debonair Detective Benoit Blanc is mysteriously enlisted to investigate. From Harlan's dysfunctional family to his devoted staff, Blanc sifts through a web of red herrings and self-serving lies to uncover the truth behind Harlan's untimely death.",en,3325,7.8 50,John Wick,Ex-hitman John Wick comes out of retirement to track down the gangsters that took everything from him.,en,12153,7.2 51,The Call of the Wild,"Buck is a big-hearted dog whose blissful domestic life is turned upside down when he is suddenly uprooted from his California home and transplanted to the exotic wilds of the Yukon during the Gold Rush of the 1890s. As the newest rookie on a mail delivery dog sled team—and later its leader—Buck experiences the adventure of a lifetime, ultimately finding his true place in the world and becoming his own master.",en,389,6.8 52,Milea: Suara dari Dilan,"Before the grand reunion, Dilan decided to write and retell his love story with Milea. This is Dilan's way of remembering Milea.",id,14,5.5 53,Kamen Rider Reiwa: The First Generation,"The world of Kamen Rider Zero-One and the world of Kamen Rider Zi-O. The two heroes live in different worlds, but what is waiting for them after crossing time and space into a single world? Union or conflict? This winter marks a new legend in the history of Kamen Rider.",ja,2,5.5 54,Just Mercy,"The powerful true story of Harvard-educated lawyer Bryan Stevenson, who goes to Alabama to defend the disenfranchised and wrongly condemned — including Walter McMillian, a man sentenced to death despite evidence proving his innocence. Bryan fights tirelessly for Walter with the system stacked against them.",en,422,8.1 55,Jurassic World: Fallen Kingdom,"Three years after the demise of Jurassic World, a volcanic eruption threatens the remaining dinosaurs on the isla Nublar, so Claire Dearing, the former park manager, recruits Owen Grady to help prevent the extinction of the dinosaurs once again.",en,7226,6.5 56,The Coma,"After a colossal and mysterious accident a young talented architect comes back to his senses in a very odd world that only resembles the reality. This world is based on the memories of the ones who live in it - people who are currently finding themselves in a deep coma. Human memory is spotty, chaotic and unstable. The same is the COMA - odd collection of memories and recollections - cities, glaciers and rivers can all be found in one room. All the laws of physics can be broken. The architect must find out the exact laws and regulations of COMA as he fights for his life, meets the love of his life and keeps on looking for the exit to the real world which he will have to get acquainted with all over again after the experience of COMA.",ru,15,5.9 57,Tales from the Darkside: The Movie,"The first segment features an animated mummy stalking selected student victims; the second tale tells the story of a ""cat from hell"" who cannot be killed and leaves a trail of victims behind it; the third story is about a man who witnesses a bizarre killing and promises never to tell what he saw and the ""in-between"" bit is the story of a woman preparing to cook her newspaper boy for supper.",en,173,6.1 58,Journey to China: The Mystery of Iron Mask,"The Russian Czar Peter the Great commissions Jonathan Green, an English traveller, to map the Far East territories of the Russian Empire. Green sets off on yet another long journey, full of unbelievable adventures, which eventually leads him to China. On his way, the famous cartographer makes breath-taking discoveries, meets mysterious creatures, Chinese princesses, deadly masters of oriental martial arts, and even Lun Van, the King of Dragons, himself. What could be more perilous than looking into the eyes of Viy? Only meeting him again… What will prevail this time — the unflinching scepticism of the scientist or ancient black magic, which has already gained influence over the Far East Lands?",ru,46,5.4 59,Escape from Pretoria,"Two white South Africans, imprisoned for working on behalf of the ANC, are determined to escape from Pretoria's notorious white man's 'Robben Island' Prison.",en,76,6.6 60,Atlantis: The Lost Empire,"The world's most highly qualified crew of archaeologists and explorers is led by historian Milo Thatch as they board the incredible 1,000-foot submarine Ulysses and head deep into the mysteries of the sea. The underwater expedition takes an unexpected turn when the team's mission must switch from exploring Atlantis to protecting it.",en,2990,6.9 61,Spider-Man: Far from Home,"Peter Parker and his friends go on a summer trip to Europe. However, they will hardly be able to rest - Peter will have to agree to help Nick Fury uncover the mystery of creatures that cause natural disasters and destruction throughout the continent.",en,6658,7.6 62,Fast & Furious Presents: Hobbs & Shaw,"Ever since US Diplomatic Security Service Agent Hobbs and lawless outcast Shaw first faced off, they just have swapped smacks and bad words. But when cyber-genetically enhanced anarchist Brixton's ruthless actions threaten the future of humanity, both join forces to defeat him. (A spin-off of “The Fate of the Furious,” focusing on Johnson's Luke Hobbs and Statham's Deckard Shaw.)",en,3058,6.7 63,Avengers: Infinity War,"As the Avengers and their allies have continued to protect the world from threats too large for any one hero to handle, a new danger has emerged from the cosmic shadows: Thanos. A despot of intergalactic infamy, his goal is to collect all six Infinity Stones, artifacts of unimaginable power, and use them to inflict his twisted will on all of reality. Everything the Avengers have fought for has led up to this moment - the fate of Earth and existence itself has never been more uncertain.",en,17408,8.3 64,Star Wars,Princess Leia is captured and held hostage by the evil Imperial forces in their effort to take over the galactic Empire. Venturesome Luke Skywalker and dashing captain Han Solo team together with the loveable robot duo R2-D2 and C-3PO to rescue the beautiful princess and restore peace and justice in the Empire.,en,13223,8.2 65,Geostorm,"After an unprecedented series of natural disasters threatened the planet, the world's leaders came together to create an intricate network of satellites to control the global climate and keep everyone safe. But now, something has gone wrong: the system built to protect Earth is attacking it, and it becomes a race against the clock to uncover the real threat before a worldwide geostorm wipes out everything and everyone along with it.",en,2700,5.8 66,Ford v Ferrari,"American car designer Carroll Shelby and the British-born driver Ken Miles work together to battle corporate interference, the laws of physics, and their own personal demons to build a revolutionary race car for Ford Motor Company and take on the dominating race cars of Enzo Ferrari at the 24 Hours of Le Mans in France in 1966.",en,2235,7.8 67,Little Women,Four sisters come of age in America in the aftermath of the Civil War.,en,1689,7.9 68,I Still Believe,The true-life story of Christian music star Jeremy Camp and his journey of love and loss that looks to prove there is always hope.,en,54,7.4 69,Harry Potter and the Chamber of Secrets,"Cars fly, trees fight back, and a mysterious house-elf comes to warn Harry Potter at the start of his second year at Hogwarts. Adventure and danger await when bloody writing on a wall announces: The Chamber Of Secrets Has Been Opened. To save Hogwarts will require all of Harry, Ron and Hermione’s magical abilities and courage.",en,13662,7.7 70,13 Hours: The Secret Soldiers of Benghazi,An American Ambassador is killed during an attack at a U.S. compound in Libya as a security team struggles to make sense out of the chaos.,en,1885,7.1 71,Dragon Ball Z: Fusion Reborn,"Not paying attention to his job, a young demon allows the evil cleansing machine to overflow and explode, turning the young demon into the infamous monster Janemba. Goku and Vegeta make solo attempts to defeat the monster, but realize their only option is fusion.",ja,387,7.4 72,Freddy's Dead: The Final Nightmare,"Just when you thought it was safe to sleep, Freddy Krueger returns in this sixth installment of the Nightmare on Elm Street films, as psychologist Maggie Burroughs, tormented by recurring nightmares, meets a patient with the same horrific dreams. Their quest for answers leads to a certain house on Elm Street -- where the nightmares become reality.",en,656,5.2 73,Spenser Confidential,"Spenser, a former Boston patrolman who just got out from prison, teams up with Hawk, an aspiring fighter, to unravel the truth behind the death of two police officers.",en,702,6.7 74,Terminator: Dark Fate,"Decades after Sarah Connor prevented Judgment Day, a lethal new Terminator is sent to eliminate the future leader of the resistance. In a fight to save mankind, battle-hardened Sarah Connor teams up with an unexpected ally and an enhanced super soldier to stop the deadliest Terminator yet.",en,1930,6.4 75,Avengers: Endgame,"After the devastating events of Avengers: Infinity War, the universe is in ruins due to the efforts of the Mad Titan, Thanos. With the help of remaining allies, the Avengers must assemble once more in order to undo Thanos' actions and restore order to the universe once and for all, no matter what consequences may be in store.",en,12215,8.3 76,365 Days,"Laura, in order to save her relationship from falling apart, goes to Sicily, where she meets Massimo. A dangerous man, the head of a mafia family, kidnaps her and gives 365 days to love him.",pl,100,5.4 77,Cinderella III: A Twist in Time,"When Lady Tremaine steals the Fairy Godmother's wand and changes history, it's up to Cinderella to restore the timeline and reclaim her prince.",en,721,6.3 78,Once Upon a Time… in Hollywood,"Los Angeles, 1969. TV star Rick Dalton, a struggling actor specializing in westerns, and stuntman Cliff Booth, his best friend, try to survive in a constantly changing movie industry. Dalton is the neighbor of the young and promising actress and model Sharon Tate, who has just married the prestigious Polish director Roman Polanski…",en,5466,7.5 79,Jojo Rabbit,"A World War II satire that follows a lonely German boy whose world view is turned upside down when he discovers his single mother is hiding a young Jewish girl in their attic. Aided only by his idiotic imaginary friend, Adolf Hitler, Jojo must confront his blind nationalism.",en,2870,8.1 80,Inception,"Cobb, a skilled thief who commits corporate espionage by infiltrating the subconscious of his targets is offered a chance to regain his old life as payment for a task considered to be impossible: ""inception"", the implantation of another person's idea into a target's subconscious.",en,25148,8.3 81,Vivarium,"A young woman and her fiancé are in search of the perfect starter home. After following a mysterious real estate agent to a new housing development, the couple finds themselves trapped in a maze of identical houses and forced to raise an otherworldly child.",en,107,5.6 82,Captain Marvel,"The story follows Carol Danvers as she becomes one of the universe’s most powerful heroes when Earth is caught in the middle of a galactic war between two alien races. Set in the 1990s, Captain Marvel is an all-new adventure from a previously unseen period in the history of the Marvel Cinematic Universe.",en,8788,7 83,Speed Racer,"Speed Racer is the tale of a young and brilliant racing driver. When corruption in the racing leagues costs his brother his life, he must team up with the police and the mysterious Racer X to bring an end to the corruption and criminal activities. Inspired by the cartoon series.",en,799,5.8 84,Cinderella II: Dreams Come True,"As a newly crowned princess, Cinderella quickly learns that life at the Palace - and her royal responsibilities - are more challenging than she had imagined. In three heartwarming tales, Cinderella calls on her animal friends and her Fairy Godmother to help as she brings her own grace and charm to her regal role and discovers that being true to yourself is the best way to make your dreams come true.",en,859,5.9 85,Star Wars: The Last Jedi,"Rey develops her newly discovered abilities with the guidance of Luke Skywalker, who is unsettled by the strength of her powers. Meanwhile, the Resistance prepares to do battle with the First Order.",en,10173,7 86,The Avengers,"When an unexpected enemy emerges and threatens global safety and security, Nick Fury, director of the international peacekeeping agency known as S.H.I.E.L.D., finds himself in need of a team to pull the world back from the brink of disaster. Spanning the globe, a daring recruitment effort begins!",en,21893,7.7 87,Pirates of the Caribbean: The Curse of the Black Pearl,"Jack Sparrow, a freewheeling 18th-century pirate, quarrels with a rival pirate bent on pillaging Port Royal. When the governor's daughter is kidnapped, Sparrow decides to help the girl's love save her.",en,14032,7.7 88,The Dark Knight,"Batman raises the stakes in his war on crime. With the help of Lt. Jim Gordon and District Attorney Harvey Dent, Batman sets out to dismantle the remaining criminal organizations that plague the streets. The partnership proves to be effective, but they soon find themselves prey to a reign of chaos unleashed by a rising criminal mastermind known to the terrified citizens of Gotham as the Joker.",en,21561,8.4 89,Cannibal Hookers,"Kirsten escaped a religious cult but is still living a dangerous life, turning tricks in Hollywood to get by. She crosses paths with the notorious Cannibal Hookers and joins their man-eating ranks. A renegade priest and a bounty hunter are searching the dark recesses of Los Angeles for Kirsten and the cannibal hookers. Will Kirsten and her blood lusting friends survive their predators to prey another night?",en,0,0 90,Home Alone 2: Lost in New York,"Instead of flying to Florida with his folks, Kevin ends up alone in New York, where he gets a hotel room with his dad's credit card—despite problems from a clerk and meddling bellboy. But when Kevin runs into his old nemeses, the Wet Bandits, he's determined to foil their plans to rob a toy store on Christmas eve.",en,5830,6.6 91,Gifted,"Frank, a single man raising his child prodigy niece Mary, is drawn into a custody battle with his mother.",en,2575,7.9 92,The Way Back,"A former basketball all-star, who has lost his wife and family foundation in a struggle with addiction attempts to regain his soul and salvation by becoming the coach of a disparate ethnically mixed high school basketball team at his alma mater.",en,92,6.6 93,Guns Akimbo,An ordinary guy suddenly finds himself forced to fight a gladiator-like battle for a dark website that streams the violence for viewers. Miles must fight heavily armed Nix and also save his kidnapped ex-girlfriend.,en,358,6.4 94,Dark Phoenix,"The X-Men face their most formidable and powerful foe when one of their own, Jean Grey, starts to spiral out of control. During a rescue mission in outer space, Jean is nearly killed when she's hit by a mysterious cosmic force. Once she returns home, this force not only makes her infinitely more powerful, but far more unstable. The X-Men must now band together to save her soul and battle aliens that want to use Grey's new abilities to rule the galaxy.",en,3159,6 95,Blade Runner 2049,"Thirty years after the events of the first film, a new blade runner, LAPD Officer K, unearths a long-buried secret that has the potential to plunge what's left of society into chaos. K's discovery leads him on a quest to find Rick Deckard, a former LAPD blade runner who has been missing for 30 years.",en,7946,7.4 96,The Shawshank Redemption,"Framed in the 1940s for the double murder of his wife and her lover, upstanding banker Andy Dufresne begins a new life at the Shawshank prison, where he puts his accounting skills to work for an amoral warden. During his long stretch in prison, Dufresne comes to be admired by the other inmates -- including an older prisoner named Red -- for his integrity and unquenchable sense of hope.",en,15522,8.7 97,The Other Lamb,"A haunting and nightmarish tale that tells the story of Selah, a young girl born into a repressive cult known as the Flock. The members of the Flock – all women and female children– live in a rural compound, and are led by one man, known only as Shepherd. Selah, a daughter who is on the cusp of teenage-hood, is given the great honor of participating in the sacred ritual of the birthing of the lambs – upon which they depend for survival – where she has a shocking and otherworldly experience. She begins to have strange visions that make her question her own reality, and everything the Shepherd has taught her and her sisters.",en,6,7.3 98,Recep İvedik 6,,tr,21,4.4 99,Togo,"The untold true story set in the winter of 1925 that takes you across the treacherous terrain of the Alaskan tundra for an exhilarating and uplifting adventure that will test the strength, courage and determination of one man, Leonhard Seppala, and his lead sled dog, Togo.",en,403,8.1 100,Sub Rosa,"Bullied by his father to grow up, a teen fights between the love of his step-mother over his father's life.",en,2,9 101,The Bourne Ultimatum,"Bourne is brought out of hiding once again by reporter Simon Ross who is trying to unveil Operation Blackbriar, an upgrade to Project Treadstone, in a series of newspaper columns. Information from the reporter stirs a new set of memories, and Bourne must finally uncover his dark past while dodging The Company's best efforts to eradicate him.",en,5015,7.4 102,Spies in Disguise,"Super spy Lance Sterling and scientist Walter Beckett are almost exact opposites. Lance is smooth, suave and debonair. Walter is… not. But what Walter lacks in social skills he makes up for in smarts and invention, creating the awesome gadgets Lance uses on his epic missions. But when events take an unexpected turn, Walter and Lance suddenly have to rely on each other in a whole new way.",en,650,7.6 103,The Maze Runner,"Set in a post-apocalyptic world, young Thomas is deposited in a community of boys after his memory is erased, soon learning they're all trapped in a maze that will require him to join forces with fellow “runners” for a shot at escape.",en,11786,7.1 104,My Spy,"A hardened CIA operative finds himself at the mercy of a precocious 9-year-old girl, having been sent undercover to surveil her family.",en,23,7 105,Toy Story 4,"Woody has always been confident about his place in the world and that his priority is taking care of his kid, whether that's Andy or Bonnie. But when Bonnie adds a reluctant new toy called ""Forky"" to her room, a road trip adventure alongside old and new friends will show Woody how big the world can be for a toy.",en,4257,7.6 106,The Hobbit: The Battle of the Five Armies,"Immediately after the events of The Desolation of Smaug, Bilbo and the dwarves try to defend Erebor's mountain of treasure from others who claim it: the men of the ruined Laketown and the elves of Mirkwood. Meanwhile an army of Orcs led by Azog the Defiler is marching on Erebor, fueled by the rise of the dark lord Sauron. Dwarves, elves and men must unite, and the hope for Middle-Earth falls into Bilbo's hands.",en,9508,7.3 107,To All the Boys I've Loved Before,Lara Jean's love life goes from imaginary to out of control when her secret letters to every boy she's ever fallen for are mysteriously mailed out.,en,235,7.4 108,Anacondas: Trail of Blood,"A genetically created Anaconda, cut in half, regenerates itself into two aggressive giant snakes, due to the Blood Orchid.",en,133,4.2 109,Harry Potter and the Half-Blood Prince,"As Harry begins his sixth year at Hogwarts, he discovers an old book marked as 'Property of the Half-Blood Prince', and begins to learn more about Lord Voldemort's dark past.",en,12172,7.7 110,The Godfather,"Spanning the years 1945 to 1955, a chronicle of the fictional Italian-American Corleone crime family. When organized crime family patriarch, Vito Corleone barely survives an attempt on his life, his youngest son, Michael steps in to take care of the would-be killers, launching a campaign of bloody revenge.",en,11768,8.7 111,Frozen,"Young princess Anna of Arendelle dreams about finding true love at her sister Elsa’s coronation. Fate takes her on a dangerous journey in an attempt to end the eternal winter that has fallen over the kingdom. She's accompanied by ice delivery man Kristoff, his reindeer Sven, and snowman Olaf. On an adventure where she will find out what friendship, courage, family, and true love really means.",en,11466,7.3 112,Red Shoes and the Seven Dwarfs,"Princes who have been turned into Dwarfs seek the red shoes of a lady in order to break the spell, although it will not be easy.",en,117,6.4 113,Spider-Man: Homecoming,"Following the events of Captain America: Civil War, Peter Parker, with the help of his mentor Tony Stark, tries to balance his life as an ordinary high school student in Queens, New York City, with fighting crime as his superhero alter ego Spider-Man as a new threat, the Vulture, emerges.",en,13766,7.4 114,Harry Potter and the Order of the Phoenix,"Returning for his fifth year of study at Hogwarts, Harry is stunned to find that his warnings about the return of Lord Voldemort have been ignored. Left with no choice, Harry takes matters into his own hands, training a small group of students – dubbed 'Dumbledore's Army' – to defend themselves against the dark arts.",en,12457,7.7 115,Doctor Sleep,"Still irrevocably scarred by the trauma he endured as a child at the Overlook, Dan Torrance has fought to find some semblance of peace. But that peace is shattered when he encounters Abra, a courageous teenager with her own powerful extrasensory gift, known as the 'shine'. Instinctively recognising that Dan shares her power, Abra has sought him out, desperate for his help against the merciless Rose the Hat and her followers.",en,1423,7.1 116,Harry Potter and the Goblet of Fire,"Harry starts his fourth year at Hogwarts, competes in the treacherous Triwizard Tournament and faces the evil Lord Voldemort. Ron and Hermione help Harry manage the pressure – but Voldemort lurks, awaiting his chance to destroy Harry and all that he stands for.",en,12812,7.8 117,Captain America: Civil War,"Following the events of Age of Ultron, the collective governments of the world pass an act designed to regulate all superhuman activity. This polarizes opinion amongst the Avengers, causing two factions to side with Iron Man or Captain America, which causes an epic battle between former allies.",en,15755,7.4 118,Big Hero 6,"The special bond that develops between plus-sized inflatable robot Baymax, and prodigy Hiro Hamada, who team up with a group of friends to form a band of high-tech heroes.",en,11350,7.8 119,The Banker,"In the 1960s, two entrepreneurs hatch an ingenious business plan to fight for housing integration—and equal access to the American Dream.",en,112,7.4 120,Rambo: Last Blood,"After fighting his demons for decades, John Rambo now lives in peace on his family ranch in Arizona, but his rest is interrupted when Gabriela, the granddaughter of his housekeeper María, disappears after crossing the border into Mexico to meet her biological father. Rambo, who has become a true father figure for Gabriela over the years, undertakes a desperate and dangerous journey to find her.",en,1461,6.1 121,The Lord of the Rings: The Fellowship of the Ring,"Young hobbit Frodo Baggins, after inheriting a mysterious ring from his uncle Bilbo, must leave his home in order to keep it from falling into the hands of its evil creator. Along the way, a fellowship is formed to protect the ringbearer and make sure that the ring arrives at its final destination: Mt. Doom, the only place where it can be destroyed.",en,16434,8.3 122,Guardians of the Galaxy Vol. 2,The Guardians must fight to keep their newfound family together as they unravel the mysteries of Peter Quill's true parentage.,en,14406,7.6 123,Léon: The Professional,"Léon, the top hit man in New York, has earned a rep as an effective ""cleaner"". But when his next-door neighbors are wiped out by a loose-cannon DEA agent, he becomes the unwilling custodian of 12-year-old Mathilda. Before long, Mathilda's thoughts turn to revenge, and she considers following in Léon's footsteps.",en,9055,8.3 124,Maleficent: Mistress of Evil,"Maleficent and her goddaughter Aurora begin to question the complex family ties that bind them as they are pulled in different directions by impending nuptials, unexpected allies, and dark new forces at play.",en,2375,7.3 125,Rush Hour 2,"It's vacation time for Carter as he finds himself alongside Lee in Hong Kong wishing for more excitement. While Carter wants to party and meet the ladies, Lee is out to track down a Triad gang lord who may be responsible for killing two men at the American Embassy. Things get complicated as the pair stumble onto a counterfeiting plot. The boys are soon up to their necks in fist fights and life-threatening situations. A trip back to the U.S. may provide the answers about the bombing, the counterfeiting, and the true allegiance of sexy customs agent Isabella.",en,2274,6.6 126,Countdown,"A young nurse downloads an app that tells her she only has three days to live. With time ticking away and a mysterious figure haunting her, she must find a way to save her life before time runs out.",en,565,6.4 127,Thor,"Against his father Odin's will, The Mighty Thor - a powerful but arrogant warrior god - recklessly reignites an ancient war. Thor is cast down to Earth and forced to live among humans as punishment. Once here, Thor learns what it takes to be a true hero when the most dangerous villain of his world sends the darkest forces of Asgard to invade Earth.",en,14452,6.7 128,Deadpool,"Deadpool tells the origin story of former Special Forces operative turned mercenary Wade Wilson, who after being subjected to a rogue experiment that leaves him with accelerated healing powers, adopts the alter ego Deadpool. Armed with his new abilities and a dark, twisted sense of humor, Deadpool hunts down the man who nearly destroyed his life.",en,22225,7.6 129,Twilight,"When Bella Swan moves to a small town in the Pacific Northwest to live with her father, she meets the reclusive Edward Cullen, a mysterious classmate who reveals himself to be a 108-year-old vampire. Despite Edward's repeated cautions, Bella can't help but fall in love with him, a fatal move that endangers her own life when a coven of bloodsuckers try to challenge the Cullen clan.",en,8527,6.1 130,The Great Mouse Detective,"When the diabolical Professor Ratigan kidnaps London's master toymaker, the brilliant master of disguise Basil of Baker Street and his trusted sidekick Dawson try to elude the ultimate trap and foil the perfect crime.",en,946,7.1 131,Survivor,"A Foreign Service Officer in London tries to prevent a terrorist attack set to hit New York, but is forced to go on the run when she is framed for crimes she did not commit.",en,643,5.5 132,Avatar,"In the 22nd century, a paraplegic Marine is dispatched to the moon Pandora on a unique mission, but becomes torn between following orders and protecting an alien civilization.",en,20778,7.4 133,Deadpool 2,Wisecracking mercenary Deadpool battles the evil and powerful Cable and other bad guys to save a boy's life.,en,10796,7.5 134,Avengers: Age of Ultron,"When Tony Stark tries to jumpstart a dormant peacekeeping program, things go awry and Earth’s Mightiest Heroes are put to the ultimate test as the fate of the planet hangs in the balance. As the villainous Ultron emerges, it is up to The Avengers to stop him from enacting his terrible plans, and soon uneasy alliances and unexpected action pave the way for an epic and unique global adventure.",en,15376,7.3 135,Gold Dust,"Along the Mexico border, two friends search for a ghost ship rumored to be buried in the desert sand. In the same area, drug lords employ children; the duo must decide between going after the ship or saving a young girl held captive.",en,0,0 136,Black Panther,"King T'Challa returns home from America to the reclusive, technologically advanced African nation of Wakanda to serve as his country's new leader. However, T'Challa soon finds that he is challenged for the throne by factions within his own country as well as without. Using powers reserved to Wakandan kings, T'Challa assumes the Black Panther mantel to join with girlfriend Nakia, the queen-mother, his princess-kid sister, members of the Dora Milaje (the Wakandan 'special forces') and an American secret agent, to prevent Wakanda from being dragged into a world war.",en,14662,7.4 137,Desire,"In a social context deteriorated by a countrywide economic crisis, the life of several people will be turned upside down after they meet Cecile, a character who symbolizes desire.",fr,215,4.7 138,Whiplash,"Under the direction of a ruthless instructor, a talented young drummer begins to pursue perfection at any cost, even his humanity.",en,9318,8.4 139,Family Guy Presents Stewie Griffin: The Untold Story,"The major sub-plot circles around the youngest Griffin, Stewie, who has a near-death experience at a pool when a lifeguard chair falls on him, but he survives. After having a vision of being in Hell, he decides to change his ways, but this doesn't last long. While watching television, he and Brian spot a man that looks like Stewie. Brian is convinced that he is Stewie's real father, until Stewie learns that the man is actually himself as an adult, taking a vacation from his own time period. Baby Stewie visits thirty years later to discover that his adult self, going by the name Stu, is a single blue-collar middle-aged virgin working at a Circuit City-type store. Meanwhile, Peter and Lois are trying to teach their two older kids, Meg and Chris, to date. In the future, Chris, who hasn't changed much, is working as a cop and is married to a foul-mouthed hustler named Vanessa. Meg is now called Ron, since she had a sex-change after college. Written by pepperann210",en,219,7 140,Harry Potter and the Prisoner of Azkaban,"Harry, Ron and Hermione return to Hogwarts for another magic-filled year. Harry comes face to face with danger yet again, this time in the form of escaped convict, Sirius Black—and turns to sympathetic Professor Lupin for help.",en,13381,8 141,Thor: Ragnarok,"Thor is imprisoned on the other side of the universe and finds himself in a race against time to get back to Asgard to stop Ragnarok, the destruction of his home-world and the end of Asgardian civilization, at the hands of an all-powerful new threat, the ruthless Hela.",en,13408,7.5 142,Gone Girl,"With his wife's disappearance having become the focus of an intense media circus, a man sees the spotlight turned on him when it's suspected that he may not be innocent.",en,12120,7.9 143,Terminator Genisys,"The year is 2029. John Connor, leader of the resistance continues the war against the machines. At the Los Angeles offensive, John's fears of the unknown future begin to emerge when TECOM spies reveal a new plot by SkyNet that will attack him from both fronts; past and future, and will ultimately change warfare forever.",en,5958,5.9 144,23-F: la película,"The failed coup d'état of February 23, 1981, which began with the capture of the Congress of Deputies and ended with the release of parliamentarians, put at serious risk the Spanish democracy.",es,20,5.9 145,The Hunger Games: Mockingjay - Part 1,Katniss Everdeen reluctantly becomes the symbol of a mass rebellion against the autocratic Capitol.,en,11191,6.8 146,Bad Boys,"Marcus Burnett is a hen-pecked family man. Mike Lowry is a foot-loose and fancy free ladies' man. Both are Miami policemen, and both have 72 hours to reclaim a consignment of drugs stolen from under their station's nose. To complicate matters, in order to get the assistance of the sole witness to a murder, they have to pretend to be each other.",en,3701,6.7 147,Bohemian Rhapsody,"Singer Freddie Mercury, guitarist Brian May, drummer Roger Taylor and bass guitarist John Deacon take the music world by storm when they form the rock 'n' roll band Queen in 1970. Hit songs become instant classics. When Mercury's increasingly wild lifestyle starts to spiral out of control, Queen soon faces its greatest challenge yet – finding a way to keep the band together amid the success and excess.",en,10483,8 148,Die Hard: With a Vengeance,"New York detective John McClane is back and kicking bad-guy butt in the third installment of this action-packed series, which finds him teaming with civilian Zeus Carver to prevent the loss of innocent lives. McClane thought he'd seen it all, until a genius named Simon engages McClane, his new ""partner"" -- and his beloved city -- in a deadly game that demands their concentration.",en,3739,7.1 149,The Irishman,"Pennsylvania, 1956. Frank Sheeran, a war veteran of Irish origin who works as a truck driver, accidentally meets mobster Russell Bufalino. Once Frank becomes his trusted man, Bufalino sends him to Chicago with the task of helping Jimmy Hoffa, a powerful union leader related to organized crime, with whom Frank will maintain a close friendship for nearly twenty years.",en,3107,7.7 150,A Nightmare on Elm Street Part 2: Freddy's Revenge,"A new family moves into the house on Elm Street, and before long, the kids are again having nightmares about deceased child murderer Freddy Krueger. This time, Freddy attempts to possess a teenage boy to cause havoc in the real world, and can only be overcome if the boy's sweetheart can master her fear.",en,927,5.8 151,Saving Private Ryan,"As U.S. troops storm the beaches of Normandy, three brothers lie dead on the battlefield, with a fourth trapped behind enemy lines. Ranger captain John Miller and seven men are tasked with penetrating German-held territory and bringing the boy home.",en,9781,8.1 152,Spirited Away,"A young girl, Chihiro, becomes trapped in a strange new world of spirits. When her parents undergo a mysterious transformation, she must call upon the courage she never knew she had to free her family.",ja,9009,8.5 153,How to Train Your Dragon: The Hidden World,"As Hiccup fulfills his dream of creating a peaceful dragon utopia, Toothless’ discovery of an untamed, elusive mate draws the Night Fury away. When danger mounts at home and Hiccup’s reign as village chief is tested, both dragon and rider must make impossible decisions to save their kind.",en,3310,7.7 154,El Camino: A Breaking Bad Movie,"In the wake of his dramatic escape from captivity, Jesse Pinkman must come to terms with his past in order to forge some kind of future.",en,2241,6.9 155,Midway,"The story of the Battle of Midway, and the leaders and soldiers who used their instincts, fortitude and bravery to overcome massive odds.",en,611,6.9 156,War Dogs,"Based on the true story of two young men, David Packouz and Efraim Diveroli, who won a $300 million contract from the Pentagon to arm America's allies in Afghanistan.",en,2782,6.8 157,"Alexander and the Terrible, Horrible, No Good, Very Bad Day","Alexander's day begins with gum stuck in his hair, followed by more calamities. Though he finds little sympathy from his family and begins to wonder if bad things only happen to him, his mom, dad, brother, and sister all find themselves living through their own terrible, horrible, no good, very bad day.",en,924,6.2 158,Inside Out,"Growing up can be a bumpy road, and it's no exception for Riley, who is uprooted from her Midwest life when her father starts a new job in San Francisco. Riley's guiding emotions— Joy, Fear, Anger, Disgust and Sadness—live in Headquarters, the control centre inside Riley's mind, where they help advise her through everyday life and tries to keep things positive, but the emotions conflict on how best to navigate a new city, house and school.",en,14458,7.9 159,Teenage Mutant Ninja Turtles,The city needs heroes. Darkness has settled over New York City as Shredder and his evil Foot Clan have an iron grip on everything from the police to the politicians. The future is grim until four unlikely outcast brothers rise from the sewers and discover their destiny as Teenage Mutant Ninja Turtles. The Turtles must work with fearless reporter April and her wise-cracking cameraman Vern Fenwick to save the city and unravel Shredder's diabolical plan.,en,4726,5.8 160,"Kamen Rider Zi-O NEXT TIME: Geiz, Majesty","Geiz, Majesty is the first installment of the Kamen Rider Zi-O NEXT TIME series of V-Cinema films for Kamen Rider Zi-O. It focuses on the character Geiz Myokoin.",ja,7,6.9 161,The Matrix,"Set in the 22nd century, The Matrix tells the story of a computer hacker who joins a group of underground insurgents fighting the vast and powerful computers who now rule the earth.",en,16481,8.1 162,Glass,"In a series of escalating encounters, former security guard David Dunn uses his supernatural abilities to track Kevin Wendell Crumb, a disturbed man who has twenty-four personalities. Meanwhile, the shadowy presence of Elijah Price emerges as an orchestrator who holds secrets critical to both men.",en,4823,6.6 163,Death Race: Beyond Anarchy,Black Ops specialist Connor Gibson infiltrates a maximum security prison to take down legendary driver Frankenstein in a violent and brutal car race.,en,114,5.8 164,The Terminator,"In the post-apocalyptic future, reigning tyrannical supercomputers teleport a cyborg assassin known as the ""Terminator"" back to 1984 to kill Sarah Connor, whose unborn son is destined to lead insurgents against 21st century mechanical hegemony. Meanwhile, the human-resistance movement dispatches a lone warrior to safeguard Sarah. Can he stop the virtually indestructible killing machine?",en,7989,7.5 165,The Martian,"During a manned mission to Mars, Astronaut Mark Watney is presumed dead after a fierce storm and left behind by his crew. But Watney has survived and finds himself stranded and alone on the hostile planet. With only meager supplies, he must draw upon his ingenuity, wit and spirit to subsist and find a way to signal to Earth that he is alive.",en,13712,7.7 166,John Wick: Chapter 2,"John Wick is forced out of retirement by a former associate looking to seize control of a shadowy international assassins’ guild. Bound by a blood oath to aid him, Wick travels to Rome and does battle against some of the world’s most dangerous killers.",en,7360,7.1 167,Iron Man,"After being held captive in an Afghan cave, billionaire engineer Tony Stark creates a unique weaponized suit of armor to fight evil.",en,17828,7.6 168,It Chapter Two,"27 years after overcoming the malevolent supernatural entity Pennywise, the former members of the Losers' Club, who have grown up and moved away from Derry, are brought back together by a devastating phone call.",en,3618,6.8 169,Angel Has Fallen,"After a treacherous attack, Secret Service agent Mike Banning is charged with attempting to assassinate President Trumbull. Chased by his own colleagues and the FBI, Banning begins a race against the clock to clear his name.",en,1387,6.2 170,Howl's Moving Castle,"When Sophie, a shy young woman, is cursed with an old body by a spiteful witch, her only chance of breaking the spell lies with a self-indulgent yet insecure young wizard and his companions in his legged, walking castle.",ja,4755,8.4 171,Motherless Brooklyn,"New York City, 1957. Lionel Essrog, a private detective living with Tourette syndrome, tries to solve the murder of his mentor and best friend, armed only with vague clues and the strength of his obsessive mind…",en,430,6.9 172,Ready or Not,A bride's wedding night takes a sinister turn when her eccentric new in-laws force her to take part in a terrifying game.,en,1312,6.9 173,Batman Begins,"Driven by tragedy, billionaire Bruce Wayne dedicates his life to uncovering and defeating the corruption that plagues his home, Gotham City. Unable to work within the system, he instead creates a new identity, a symbol of fear for the criminal underworld - The Batman.",en,13889,7.7 174,Last Christmas,"Kate is a young woman who has a habit of making bad decisions, and her last date with disaster occurs after she accepts work as Santa's elf for a department store. However, after she meets Tom there, her life takes a new turn.",en,713,7.1 175,Ant-Man and the Wasp,"Just when his time under house arrest is about to end, Scott Lang once again puts his freedom at risk to help Hope van Dyne and Dr. Hank Pym dive into the quantum realm and try to accomplish, against time and any chance of success, a very dangerous rescue mission.",en,7991,7 176,Alita: Battle Angel,"When Alita awakens with no memory of who she is in a future world she does not recognize, she is taken in by Ido, a compassionate doctor who realizes that somewhere in this abandoned cyborg shell is the heart and soul of a young woman with an extraordinary past.",en,4610,7 177,Zootopia,"Determined to prove herself, Officer Judy Hopps, the first bunny on Zootopia's police force, jumps at the chance to crack her first case - even if it means partnering with scam-artist fox Nick Wilde to solve the mystery.",en,11320,7.7 178,Ready Player One,"When the creator of a popular video game system dies, a virtual contest is created to compete for his fortune.",en,8774,7.6 179,Star Wars: The Force Awakens,"Thirty years after defeating the Galactic Empire, Han Solo and his allies face a new threat from the evil Kylo Ren and his army of Stormtroopers.",en,14077,7.4 180,Titanic,"101-year-old Rose DeWitt Bukater tells the story of her life aboard the Titanic, 84 years later. A young Rose boards the ship with her mother and fiancé. Meanwhile, Jack Dawson and Fabrizio De Rossi win third-class tickets aboard the ship. Rose tells the whole story from Titanic's departure through to its death—on its first and last voyage—on April 15, 1912.",en,16434,7.8 181,The Decline,"Anticipating a disaster, Antoine, a father, attends a survivalist training given by Alain in his autonomous hideout. In fear of a natural, economic or social crisis, the group trains to face the different possible apocalyptic scenarios. But the disaster they will experience will not be the one they predicted.",fr,95,6.8 182,The Grudge,"After a young mother murders her family in her own house, a detective attempts to investigate the mysterious case, only to discover that the house is cursed by a vengeful ghost. Now targeted by the demonic spirits, the detective must do anything to protect herself and her family from harm.",en,361,5.7 183,Beauty and the Beast,A live-action adaptation of Disney's version of the classic tale of a cursed prince and a beautiful young woman who helps him break the spell.,en,12110,6.9 184,Cinderella,"When her father unexpectedly passes away, young Ella finds herself at the mercy of her cruel stepmother and her daughters. Never one to give up hope, Ella's fortunes begin to change after meeting a dashing stranger in the woods.",en,5124,6.7 185,The Purge: Anarchy,"One night per year, the government sanctions a 12-hour period in which citizens can commit any crime they wish -- including murder -- without fear of punishment or imprisonment. Leo, a sergeant who lost his son, plans a vigilante mission of revenge during the mayhem. However, instead of a death-dealing avenger, he becomes the unexpected protector of four innocent strangers who desperately need his help if they are to survive the night.",en,4289,6.6 186,Dante's Inferno: An Animated Epic,"Dante journeys through the nine circles of Hell -- limbo, lust, gluttony, greed, anger, heresy, violence, fraud and treachery -- in search of his true love, Beatrice. An animated version of the video game of the same name.",en,133,6.1 187,Zombieland: Double Tap,"Columbus, Tallahassee, Wichita, and Little Rock move to the American heartland as they face off against evolved zombies, fellow survivors, and the growing pains of the snarky makeshift family.",en,1942,7 188,Justice League,"Fuelled by his restored faith in humanity and inspired by Superman's selfless act, Bruce Wayne and Diana Prince assemble a team of metahumans consisting of Barry Allen, Arthur Curry and Victor Stone to face the catastrophic threat of Steppenwolf and the Parademons who are on the hunt for three Mother Boxes on Earth.",en,8719,6.2 189,Gemini Man,"Ageing assassin, Henry Brogen tries to get out of the business but finds himself in the ultimate battle—fighting his own clone who is 25 years younger than him, and at the peak of his abilities.",en,1748,6.1 190,Split,"Though Kevin has evidenced 23 personalities to his trusted psychiatrist, Dr. Fletcher, there remains one still submerged who is set to materialize and dominate all the others. Compelled to abduct three teenage girls led by the willful, observant Casey, Kevin reaches a war for survival among all of those contained within him — as well as everyone around him — as the walls between his compartments shatter apart.",en,11993,7.3 191,The Turning,"A young woman quits her teaching job to be a private tutor (governess) for a wealthy young heiress who witnessed her parent's tragic death. Shortly after arriving, the girl's degenerate brother is sent home from his boarding school. The tutor has some strange, unexplainable experiences in the house and begins to suspect there is more to their story.",en,59,5.6 192,Fantastic Beasts: The Crimes of Grindelwald,"Gellert Grindelwald has escaped imprisonment and has begun gathering followers to his cause—elevating wizards above all non-magical beings. The only one capable of putting a stop to him is the wizard he once called his closest friend, Albus Dumbledore. However, Dumbledore will need to seek help from the wizard who had thwarted Grindelwald once before, his former student Newt Scamander, who agrees to help, unaware of the dangers that lie ahead. Lines are drawn as love and loyalty are tested, even among the truest friends and family, in an increasingly divided wizarding world.",en,6394,6.8 193,The Wolf of Wall Street,"A New York stockbroker refuses to cooperate in a large securities fraud case involving corruption on Wall Street, corporate banking world and mob infiltration. Based on Jordan Belfort's autobiography.",en,14617,8 194,The Lord of the Rings: The Two Towers,"Frodo and Sam are trekking to Mordor to destroy the One Ring of Power while Gimli, Legolas and Aragorn search for the orc-captured Merry and Pippin. All along, nefarious wizard Saruman awaits the Fellowship members at the Orthanc Tower in Isengard.",en,14175,8.3 195,Trainspotting,"Mark Renton, deeply immersed in the Edinburgh drug scene, tries to clean up and get out, despite the allure of the drugs and influence of friends.",en,6285,8 196,Chappie,"Every child comes into the world full of promise, and none more so than Chappie: he is gifted, special, a prodigy. Like any child, Chappie will come under the influence of his surroundings—some good, some bad—and he will rely on his heart and soul to find his way in the world and become his own man. But there's one thing that makes Chappie different from any one else: he is a robot.",en,5516,6.7 197,The Lord of the Rings: The Return of the King,"Aragorn is revealed as the heir to the ancient kings as he, Gandalf and the other members of the broken fellowship struggle to save Gondor from Sauron's forces. Meanwhile, Frodo and Sam take the ring closer to the heart of Mordor, the dark lord's realm.",en,14987,8.4 198,Pirates of the Caribbean: Dead Man's Chest,"Captain Jack Sparrow works his way out of a blood debt with the ghostly Davey Jones, he also attempts to avoid eternal damnation.",en,10619,7.2 199,Nater Guru,"A man tries to bring together his girlfriends' parents, who have been estranged for fifteen years because of disagreements, misunderstandings, and pride.",en,1,6 200,Fury,"In the last months of World War II, as the Allies make their final push in the European theatre, a battle-hardened U.S. Army sergeant named 'Wardaddy' commands a Sherman tank called 'Fury' and its five-man crew on a deadly mission behind enemy lines. Outnumbered and outgunned, Wardaddy and his men face overwhelming odds in their heroic attempts to strike at the heart of Nazi Germany.",en,7692,7.4 201,Fifty Shades of Grey,"When college senior Anastasia Steele steps in for her sick roommate to interview prominent businessman Christian Grey for their campus paper, little does she realize the path her life will take. Christian, as enigmatic as he is rich and powerful, finds himself strangely drawn to Ana, and she to him. Though sexually inexperienced, Ana plunges headlong into an affair -- and learns that Christian's true sexual proclivities push the boundaries of pain and pleasure.",en,7342,5.6 202,The Butterfly Effect 3: Revelations,"The story revolves around a man trying to uncover the mysterious death of his girlfriend and save an innocent man from the death chamber in the process, by using his unique power to time travel. However in attempting to do this, he also frees a spiteful serial-killer.",en,309,5.3 203,My Neighbor Totoro,"Two sisters move to the country with their father in order to be closer to their hospitalized mother, and discover the surrounding trees are inhabited by Totoros, magical spirits of the forest. When the youngest runs away from home, the older sister seeks help from the spirits to find her.",ja,4186,8.1 204,The Occupant,"An unemployed executive is forced to sell his apartment. When he discovers that he still has the keys, he becomes obsessed with the family that now lives there and decides to recover the life he has lost, at any price.",es,232,6.3 205,Your Name.,"High schoolers Mitsuha and Taki are complete strangers living separate lives. But one night, they suddenly switch places. Mitsuha wakes up in Taki’s body, and he in hers. This bizarre occurrence continues to happen randomly, and the two must adjust their lives around each other.",ja,5225,8.5 206,Aladdin,"Princess Jasmine grows tired of being forced to remain in the palace, so she sneaks out into the marketplace, in disguise, where she meets street-urchin Aladdin. The couple falls in love, although Jasmine may only marry a prince. After being thrown in jail, Aladdin becomes embroiled in a plot to find a mysterious lamp, with which the evil Jafar hopes to rule the land.",en,7599,7.6 207,The Lion King,"A young lion prince is cast out of his pride by his cruel uncle, who claims he killed his father. While the uncle rules with an iron paw, the prince grows up beyond the Savannah, living by a philosophy: No worries for the rest of your days. But when his past comes to haunt him, the young prince must decide his fate: Will he remain an outcast or face his demons and become what he needs to be?",en,11952,8.3 208,Tomorrowland,"Bound by a shared destiny, a bright, optimistic teen bursting with scientific curiosity and a former boy-genius inventor jaded by disillusionment embark on a danger-filled mission to unearth the secrets of an enigmatic place somewhere in time and space that exists in their collective memory as ""Tomorrowland.""",en,5110,6.2 209,The Hobbit: An Unexpected Journey,"Bilbo Baggins, a hobbit enjoying his quiet life, is swept into an epic quest by Gandalf the Grey and thirteen dwarves who seek to reclaim their mountain home from Smaug, the dragon.",en,13243,7.2 210,Man on Fire,"Jaded ex-CIA operative John Creasy reluctantly accepts a job as the bodyguard for a 10-year-old girl in Mexico City. They clash at first, but eventually bond, and when she's kidnapped he's consumed by fury and will stop at nothing to save her life.",en,2900,7.4 211,Spider-Man: Into the Spider-Verse,"Miles Morales is juggling his life between being a high school student and being a spider-man. When Wilson ""Kingpin"" Fisk uses a super collider, others from across the Spider-Verse are transported to this dimension.",en,6735,8.4 212,Venom,"Investigative journalist Eddie Brock attempts a comeback following a scandal, but accidentally becomes the host of Venom, a violent, super powerful alien symbiote. Soon, he must rely on his newfound powers to protect the world from a shadowy organization looking for a symbiote of their own.",en,8315,6.6 213,Run All Night,"Brooklyn mobster and prolific hit man Jimmy Conlon has seen better days. Longtime best friend of a mob boss, Jimmy is haunted by the sins of his past—as well as a dogged police detective who’s been one step behind Jimmy for 30 years. But when Jimmy’s estranged son becomes a target, Jimmy must make a choice between the crime family he chose and the real family he abandoned long ago. Now, with nowhere safe to turn, Jimmy has just one night to figure out exactly where his loyalties lie and to see if he can finally make things right.",en,1877,6.4 214,Cowboy Bebop: The Movie,"The year is 2071. Following a terrorist bombing, a deadly virus is released on the populace of Mars and the government has issued the largest bounty in history, for the capture of whoever is behind it. The bounty hunter crew of the spaceship Bebop; Spike, Faye, Jet and Ed, take the case with hopes of cashing in the bounty. However, the mystery surrounding the man responsible, Vincent, goes deeper than they ever imagined, and they aren't the only ones hunting him.",ja,379,7.7 215,Maze Runner: The Death Cure,"Thomas leads his group of escaped Gladers on their final and most dangerous mission yet. To save their friends, they must break into the legendary Last City, a WCKD-controlled labyrinth that may turn out to be the deadliest maze of all. Anyone who makes it out alive will get answers to the questions the Gladers have been asking since they first arrived in the maze.",en,4630,7 216,Pulp Fiction,"A burger-loving hit man, his philosophical partner, a drug-addled gangster's moll and a washed-up boxer converge in this sprawling, comedic crime caper. Their adventures unfurl in three stories that ingeniously trip back and forth in time.",en,18003,8.5 217,Charlie's Angels,"When a systems engineer blows the whistle on a dangerous technology, Charlie's Angels from across the globe are called into action, putting their lives on the line to protect society.",en,905,6.5 218,How to Train Your Dragon: Homecoming,"It's been ten years since the dragons moved to the Hidden World, and even though Toothless doesn't live in New Berk anymore, Hiccup continues the holiday traditions he once shared with his best friend. But the Vikings of New Berk were beginning to forget about their friendship with dragons. Hiccup, Astrid, and Gobber know just what to do to keep the dragons in the villagers' hearts. And across the sea, the dragons have a plan of their own...",en,169,8.2 219,Shrek,"It ain't easy bein' green -- especially if you're a likable (albeit smelly) ogre named Shrek. On a mission to retrieve a gorgeous princess from the clutches of a fire-breathing dragon, Shrek teams up with an unlikely compatriot -- a wisecracking donkey.",en,10170,7.6 220,Finding Nemo,"Nemo, an adventurous young clownfish, is unexpectedly taken from his Great Barrier Reef home to a dentist's office aquarium. It's up to his worrisome father Marlin and a friendly but forgetful fish Dory to bring Nemo home -- meeting vegetarian sharks, surfer dude turtles, hypnotic jellyfish, hungry seagulls, and more along the way.",en,13179,7.8 221,Thor: The Dark World,"Thor fights to restore order across the cosmos… but an ancient race led by the vengeful Malekith returns to plunge the universe back into darkness. Faced with an enemy that even Odin and Asgard cannot withstand, Thor must embark on his most perilous and personal journey yet, one that will reunite him with Jane Foster and force him to sacrifice everything to save us all.",en,11572,6.6 222,Solo: A Star Wars Story,"Through a series of daring escapades deep within a dark and dangerous criminal underworld, Han Solo meets his mighty future copilot Chewbacca and encounters the notorious gambler Lando Calrissian.",en,5038,6.6 223,Beauty and the Beast,"Follow the adventures of Belle, a bright young woman who finds herself in the castle of a prince who's been turned into a mysterious beast. With the help of the castle's enchanted staff, Belle soon learns the most important lesson of all -- that true beauty comes from within.",en,6626,7.7 224,Godzilla: King of the Monsters,"Follows the heroic efforts of the crypto-zoological agency Monarch as its members face off against a battery of god-sized monsters, including the mighty Godzilla, who collides with Mothra, Rodan, and his ultimate nemesis, the three-headed King Ghidorah. When these ancient super-species - thought to be mere myths - rise again, they all vie for supremacy, leaving humanity's very existence hanging in the balance.",en,2312,6.2 225,Portrait of a Lady on Fire,"On an isolated island in Brittany at the end of the eighteenth century, a female painter is obliged to paint a wedding portrait of a young woman.",fr,477,8.2 226,Pokémon Detective Pikachu,"In a world where people collect pocket-size monsters (Pokémon) to do battle, a boy comes across an intelligent monster who seeks to be a detective.",en,3514,6.9 227,Terminator 2: Judgment Day,"Nearly 10 years have passed since Sarah Connor was targeted for termination by a cyborg from the future. Now her son, John, the future leader of the resistance, is the target for a newer, more deadly terminator. Once again, the resistance has managed to send a protector back to attempt to save John and his mother Sarah.",en,7788,8 228,Blade Runner,"In the smog-choked dystopian Los Angeles of 2019, blade runner Rick Deckard is called out of retirement to terminate a quartet of replicants who have escaped to Earth seeking their creator for a way to extend their short life spans.",en,8726,7.9 229,Iron Man 2,"With the world now aware of his dual life as the armored superhero Iron Man, billionaire inventor Tony Stark faces pressure from the government, the press and the public to share his technology with the military. Unwilling to let go of his invention, Stark, with Pepper Potts and James 'Rhodey' Rhodes at his side, must forge new alliances – and confront powerful enemies.",en,14028,6.8 230,Men in Black,"After a police chase with an otherworldly being, a New York City cop is recruited as an agent in a top-secret organization established to monitor and police alien activity on Earth: the Men in Black. Agent Kay and new recruit Agent Jay find themselves in the middle of a deadly plot by an intergalactic terrorist who has arrived on Earth to assassinate two ambassadors from opposing galaxies.",en,9190,7.1 231,The Imitation Game,"Based on the real life story of legendary cryptanalyst Alan Turing, the film portrays the nail-biting race against time by Turing and his brilliant team of code-breakers at Britain's top-secret Government Code and Cypher School at Bletchley Park, during the darkest days of World War II.",en,11730,8.1 232,Birdman or (The Unexpected Virtue of Ignorance),"A fading actor best known for his portrayal of a popular superhero attempts to mount a comeback by appearing in a Broadway play. As opening night approaches, his attempts to become more altruistic, rebuild his career, and reconnect with friends and family prove more difficult than expected.",en,9007,7.5 233,The Godfather: Part II,"In the continuing saga of the Corleone crime family, a young Vito Corleone grows up in Sicily and in 1910s New York. In the 1950s, Michael Corleone attempts to expand the family business into Las Vegas, Hollywood and Cuba.",en,6917,8.5 234,Jocks,"The coach of a college tennis team is given an ultimatum: put together a winning team, or else.",en,8,4.8 235,One Day,"A romantic comedy centered on Dexter and Emma, who first meet during their graduation in 1988 and proceed to keep in touch regularly. The film follows what they do on July 15 annually, usually doing something together.",en,2430,7.2 236,Re: Zero kara Hajimeru Isekai Seikatsu - Memory Snow,"Subaru and friends finally get a moment of peace, and Subaru goes on a certain secret mission that he must not let anyone find out about! However, even though Subaru is wearing a disguise, Petra and other children of the village immediately figure out who he is. Now that his mission was exposed within five seconds of it starting, what will happen with Subaru's ""date course"" with Emilia?",ja,44,6.4 237,300: Rise of an Empire,"Greek general Themistokles attempts to unite all of Greece by leading the charge that will change the course of the war. Themistokles faces the massive invading Persian forces led by mortal-turned-god, Xerxes and Artemesia, the vengeful commander of the Persian navy.",en,4359,6.1 238,Hotel Belgrad,"Paul is a wealthy heir and owner of the prestigious Belgrade Hotel. When into his world The Belgrade mafia, crazy bride, old love and quirky family are involved, his leisurely life turns into an adventure where anything is possible.",ru,7,8.2 239,The Shining,"Jack Torrance accepts a caretaker job at the Overlook Hotel, where he, along with his wife Wendy and their son Danny, must live isolated from the rest of the world for the winter. But they aren't prepared for the madness that lurks within.",en,10494,8.2 240,The Next Three Days,"A married couple's life is turned upside down when the wife is accused of a murder. Lara Brennan is arrested for murdering her boss with whom she had an argument. It seems she was seen leaving the scene of the crime and her fingerprints were on the murder weapon. Her husband, John would spend the next few years trying to get her released, but there's no evidence that negates the evidence against her. And when the strain of being separated from her family, especially her son, gets to her, John decides to break her out. So he does a lot of research to find a way.",en,1734,7 241,Brave,"Brave is set in the mystical Scottish Highlands, where Mérida is the princess of a kingdom ruled by King Fergus and Queen Elinor. An unruly daughter and an accomplished archer, Mérida one day defies a sacred custom of the land and inadvertently brings turmoil to the kingdom. In an attempt to set things right, Mérida seeks out an eccentric old Wise Woman and is granted an ill-fated wish. Also figuring into Mérida’s quest — and serving as comic relief — are the kingdom’s three lords: the enormous Lord MacGuffin, the surly Lord Macintosh, and the disagreeable Lord Dingwall.",en,9278,7 242,Ip Man 4: The Finale,"Following the death of his wife, Ip Man travels to San Francisco to ease tensions between the local kung fu masters and his star student, Bruce Lee, while searching for a better future for his son.",cn,398,6 243,Fight Club,"A ticking-time-bomb insomniac and a slippery soap salesman channel primal male aggression into a shocking new form of therapy. Their concept catches on, with underground ""fight clubs"" forming in every town, until an eccentric gets in the way and ignites an out-of-control spiral toward oblivion.",en,18664,8.4 244,A Quiet Place,A family is forced to live in silence while hiding from creatures that hunt by sound.,en,7607,7.3 245,Uncut Gems,"A charismatic New York City jeweler always on the lookout for the next big score makes a series of high-stakes bets that could lead to the windfall of a lifetime. Howard must perform a precarious high-wire act, balancing business, family, and encroaching adversaries on all sides in his relentless pursuit of the ultimate win.",en,1517,7.2 246,"Monsters, Inc.","James Sullivan and Mike Wazowski are monsters, they earn their living scaring children and are the best in the business... even though they're more afraid of the children than they are of them. When a child accidentally enters their world, James and Mike suddenly find that kids are not to be afraid of and they uncover a conspiracy that could threaten all children across the world.",en,12298,7.8 247,Arrival,"Taking place after alien crafts land around the world, an expert linguist is recruited by the military to determine whether they come in peace or are a threat.",en,11862,7.5 248,The Shape of Water,"An other-worldly story, set against the backdrop of Cold War era America circa 1962, where a mute janitor working at a lab falls in love with an amphibious man being held captive there and devises a plan to help him escape.",en,8436,7.2 249,Abominable,"A group of misfits encounter a young Yeti named Everest, and they set off to reunite the magical creature with his family on the mountain of his namesake.",en,673,7.3 250,Maleficent,"A beautiful, pure-hearted young woman, Maleficent has an idyllic life growing up in a peaceable forest kingdom, until one day when an invading army threatens the harmony of the land. Maleficent rises to be the land's fiercest protector, but she ultimately suffers a ruthless betrayal – an act that begins to turn her heart into stone. Bent on revenge, Maleficent faces an epic battle with the invading King's successor and, as a result, places a curse upon his newborn infant Aurora. As the child grows, Maleficent realizes that Aurora holds the key to peace in the kingdom – and to Maleficent's true happiness as well.",en,9539,7.1 251,Stargirl,"Leo Borlock is an average student at Mica High School. He gets decent grades, is a member of the school's marching band and has always been content flying under the radar. But all that changes when he meets Stargirl Caraway, a confident and colorful new student with a penchant for the ukulele, who stands out in a crowd. She is kind, finds magic in the mundane and touches the lives of others with the simplest of gestures. Her eccentricities and infectious personality charm Leo and the student body, and she quickly goes from being ignored and ridiculed to accepted and praised, then back again, sending Leo on a rollercoaster ride of emotions.",en,127,7.7 252,Schindler's List,The true story of how businessman Oskar Schindler saved over a thousand Jewish lives from the Nazis while they worked as slaves in his factory during World War II.,en,9366,8.6 253,Self/less,An extremely wealthy elderly man dying from cancer undergoes a radical medical procedure that transfers his consciousness to the body of a healthy young man but everything may not be as good as it seems when he starts to uncover the mystery of the body's origins and the secret organization that will kill to keep its secrets.,en,2048,6.3 254,Back to the Future,"Eighties teenager Marty McFly is accidentally sent back in time to 1955, inadvertently disrupting his parents' first meeting and attracting his mother's romantic interest. Marty must repair the damage to history by rekindling his parents' romance and - with the help of his eccentric inventor friend Doc Brown - return to 1985.",en,12658,8.3 255,Coco,"Despite his family’s baffling generations-old ban on music, Miguel dreams of becoming an accomplished musician like his idol, Ernesto de la Cruz. Desperate to prove his talent, Miguel finds himself in the stunning and colorful Land of the Dead following a mysterious chain of events. Along the way, he meets charming trickster Hector, and together, they set off on an extraordinary journey to unlock the real story behind Miguel's family history.",en,10748,8.2 256,Alien,"During its return to the earth, commercial spaceship Nostromo intercepts a distress signal from a distant planet. When a three-member team of the crew discovers a chamber containing thousands of eggs on the planet, a creature inside one of the eggs attacks an explorer. The entire crew is unaware of the impending nightmare set to descend upon them when the alien parasite planted inside its unfortunate host is birthed.",en,8756,8.1 257,Dolemite Is My Name,"The story of Rudy Ray Moore, who created the iconic big screen pimp character Dolemite in the 1970s.",en,515,7.2 258,I Am Legend,"Robert Neville is a scientist who was unable to stop the spread of the terrible virus that was incurable and man-made. Immune, Neville is now the last human survivor in what is left of New York City and perhaps the world. For three years, Neville has faithfully sent out daily radio messages, desperate to find any other survivors who might be out there. But he is not alone.",en,10295,7.1 259,The Nun,"When a young nun at a cloistered abbey in Romania takes her own life, a priest with a haunted past and a novitiate on the threshold of her final vows are sent by the Vatican to investigate. Together they uncover the order’s unholy secret. Risking not only their lives but their faith and their very souls, they confront a malevolent force in the form of the same demonic nun that first terrorized audiences in “The Conjuring 2” as the abbey becomes a horrific battleground between the living and the damned.",en,3566,5.6 260,Pirates of the Caribbean: On Stranger Tides,"Captain Jack Sparrow crosses paths with a woman from his past, and he's not sure if it's love -- or if she's a ruthless con artist who's using him to find the fabled Fountain of Youth. When she forces him aboard the Queen Anne's Revenge, the ship of the formidable pirate Blackbeard, Jack finds himself on an unexpected adventure in which he doesn't know who to fear more: Blackbeard or the woman from his past.",en,9618,6.4 261,Green Book,"Tony Lip, a bouncer in 1962, is hired to drive pianist Don Shirley on a tour through the Deep South in the days when African Americans, forced to find alternate accommodations and services due to segregation laws below the Mason-Dixon Line, relied on a guide called The Negro Motorist Green Book.",en,5852,8.3 262,Shazam!,A boy is given the ability to become an adult superhero in times of need with a single magic word.,en,4619,7 263,The Mask,"When timid bank clerk Stanley Ipkiss discovers a magical mask containing the spirit of the Norse god Loki, his entire life changes. While wearing the mask, Ipkiss becomes a supernatural playboy exuding charm and confidence which allows him to catch the eye of local nightclub singer Tina Carlyle. Unfortunately, under the mask's influence, Ipkiss also robs a bank, which angers junior crime lord Dorian Tyrell, whose goons get blamed for the heist.",en,6315,6.8 264,The Equalizer,"McCall believes he has put his mysterious past behind him and dedicated himself to beginning a new, quiet life. But when he meets Teri, a young girl under the control of ultra-violent Russian gangsters, he can’t stand idly by – he has to help her. Armed with hidden skills that allow him to serve vengeance against anyone who would brutalize the helpless, McCall comes out of his self-imposed retirement and finds his desire for justice reawakened. If someone has a problem, if the odds are stacked against them, if they have nowhere else to turn, McCall will help. He is The Equalizer.",en,5681,7.2 265,Fantastic Beasts and Where to Find Them,"In 1926, Newt Scamander arrives at the Magical Congress of the United States of America with a magically expanded briefcase, which houses a number of dangerous creatures and their habitats. When the creatures escape from the briefcase, it sends the American wizarding authorities after Newt, and threatens to strain even further the state of magical and non-magical relations.",en,13596,7.4 266,The Lighthouse,Two lighthouse keepers try to maintain their sanity while living on a remote and mysterious New England island in the 1890s.,en,1097,7.7 267,6 Underground,"After faking his death, a tech billionaire recruits a team of international operatives for a bold and bloody mission to take down a brutal dictator.",en,1826,6.2 268,Batman v Superman: Dawn of Justice,"Fearing the actions of a god-like Super Hero left unchecked, Gotham City’s own formidable, forceful vigilante takes on Metropolis’s most revered, modern-day savior, while the world wrestles with what sort of hero it really needs. And with Batman and Superman at war with one another, a new threat quickly arises, putting mankind in greater danger than it’s ever known before.",en,13018,5.8 269,After,"Tessa Young is a dedicated student, dutiful daughter and loyal girlfriend to her high school sweetheart. Entering her first semester of college, Tessa's guarded world opens up when she meets Hardin Scott, a mysterious and brooding rebel who makes her question all she thought she knew about herself -- and what she wants out of life.",en,2671,6.1 270,Life of Pi,"The story of an Indian boy named Pi, a zookeeper's son who finds himself in the company of a hyena, zebra, orangutan, and a Bengal tiger after a shipwreck sets them adrift in the Pacific Ocean.",en,9638,7.3 271,Braveheart,"Enraged at the slaughter of Murron, his new bride and childhood love, Scottish warrior William Wallace slays a platoon of the local English lord's soldiers. This leads the village to revolt and, eventually, the entire country to rise up against English rule.",en,6319,7.9 272,Logan,"In the near future, a weary Logan cares for an ailing Professor X in a hideout on the Mexican border. But Logan's attempts to hide from the world and his legacy are upended when a young mutant arrives, pursued by dark forces.",en,13822,7.8 273,Jurassic World,"Twenty-two years after the events of Jurassic Park, Isla Nublar now features a fully functioning dinosaur theme park, Jurassic World, as originally envisioned by John Hammond.",en,15292,6.6 274,Lady and the Tramp,"The love story between a pampered Cocker Spaniel named Lady and a streetwise mongrel named Tramp. Lady finds herself out on the street after her owners have a baby and is saved from a pack by Tramp, who tries to show her to live her life footloose and collar-free.",en,371,7.1 275,Color Out of Space,"The Gardner family moves to a remote farmstead in rural New England to escape the hustle of the 21st century. They are busy adapting to their new life when a meteorite crashes into their front yard, melts into the earth, and infects both the land and the properties of space-time with a strange, otherworldly colour. To their horror, the family discovers this alien force is gradually mutating every life form that it touches—including them.",en,156,6.4 276,Aquaman,"Once home to the most advanced civilization on Earth, Atlantis is now an underwater kingdom ruled by the power-hungry King Orm. With a vast army at his disposal, Orm plans to conquer the remaining oceanic people and then the surface world. Standing in his way is Arthur Curry, Orm's half-human, half-Atlantean brother and true heir to the throne.",en,8285,6.8 277,Dr. No,"In the film that launched the James Bond saga, Agent 007 battles mysterious Dr. No, a scientific genius bent on destroying the U.S. space program. As the countdown to disaster begins, Bond must go to Jamaica, where he encounters beautiful Honey Ryder, to confront a megalomaniacal villain in his massive island headquarters.",en,1931,7 278,Rampage,"Primatologist Davis Okoye shares an unshakable bond with George, the extraordinarily intelligent, silverback gorilla who has been in his care since birth. But a rogue genetic experiment gone awry mutates this gentle ape into a raging creature of enormous size. To make matters worse, it’s soon discovered there are other similarly altered animals. As these newly created alpha predators tear across North America, destroying everything in their path, Okoye teams with a discredited genetic engineer to secure an antidote, fighting his way through an ever-changing battlefield, not only to halt a global catastrophe but to save the fearsome creature that was once his friend.",en,4039,6.3 279,Insurgent,Beatrice Prior must confront her inner demons and continue her fight against a powerful alliance which threatens to tear her society apart.,en,7485,6.3 280,Fifty Shades Freed,"Believing they have left behind shadowy figures from their past, newlyweds Christian and Ana fully embrace an inextricable connection and shared life of luxury. But just as she steps into her role as Mrs. Grey and he relaxes into an unfamiliar stability, new threats could jeopardize their happy ending before it even begins.",en,4217,6.3 281,Doctor Strange,"After his career is destroyed, a brilliant but arrogant surgeon gets a new lease on life when a sorcerer takes him under her wing and trains him to defend the world against evil.",en,14508,7.4 282,Exodus: Gods and Kings,"The defiant leader Moses rises up against the Egyptian Pharaoh Ramses, setting 400,000 slaves on a monumental journey of escape from Egypt and its terrifying cycle of deadly plagues.",en,3218,5.8 283,Alien: Covenant,"Bound for a remote planet on the far side of the galaxy, the crew of the colony ship 'Covenant' discovers what is thought to be an uncharted paradise, but is actually a dark, dangerous world—which has a sole inhabitant: the 'synthetic', David, survivor of the doomed Prometheus expedition.",en,5566,5.9 284,Spectre,"A cryptic message from Bond’s past sends him on a trail to uncover a sinister organization. While M battles political forces to keep the secret service alive, Bond peels back the layers of deceit to reveal the terrible truth behind SPECTRE.",en,7383,6.5 285,Mad Max: Fury Road,"An apocalyptic story set in the furthest reaches of our planet, in a stark desert landscape where humanity is broken, and most everyone is crazed fighting for the necessities of life. Within this world exist two rebels on the run who just might be able to restore order.",en,15987,7.5 286,Beautiful Creatures,"Ethan Wate just wants to get to know Lena Duchannes better, but unbeknownst to him, Lena has strange powers. As Lena's 16th birthday approaches she might decide her fate, to be good or evil. A choice which will impact her relationship forever.",en,2096,5.8 287,Mission: Impossible - Fallout,"When an IMF mission ends badly, the world is faced with dire consequences. As Ethan Hunt takes it upon himself to fulfill his original briefing, the CIA begin to question his loyalty and his motives. The IMF team find themselves in a race against time, hunted by assassins while trying to prevent a global catastrophe.",en,4808,7.3 288,The Twilight Saga: New Moon,"Forks, Washington resident Bella Swan is reeling from the departure of her vampire love, Edward Cullen, and finds comfort in her friendship with Jacob Black, a werewolf. But before she knows it, she's thrust into a centuries-old conflict, and her desire to be with Edward at any cost leads her to take greater and greater risks.",en,5970,5.8 289,Pirates of the Caribbean: At World's End,"Captain Barbossa, long believed to be dead, has come back to life and is headed to the edge of the Earth with Will Turner and Elizabeth Swann. But nothing is quite as it seems.",en,9463,7.2 290,Inglourious Basterds,"In Nazi-occupied France during World War II, a group of Jewish-American soldiers known as ""The Basterds"" are chosen specifically to spread fear throughout the Third Reich by scalping and brutally killing Nazis. The Basterds, lead by Lt. Aldo Raine soon cross paths with a French-Jewish teenage girl who runs a movie theater in Paris which is targeted by the soldiers.",en,14152,8.2 291,Bombshell,Bombshell is a revealing look inside the most powerful and controversial media empire of all time; and the explosive story of the women who brought down the infamous man who created it.,en,649,6.8 292,Kong: Skull Island,"Explore the mysterious and dangerous home of the king of the apes as a team of explorers ventures deep inside the treacherous, primordial island.",en,6930,6.4 293,Incredibles 2,"Elastigirl springs into action to save the day, while Mr. Incredible faces his greatest challenge yet – taking care of the problems of his three children.",en,7970,7.5 294,The Invisible Guest,"A young businessman wakes up in a hotel room, locked from the inside, along with his lover, who was murdered while he was unconscious. He hires a prestigious lawyer, and over the course of one evening, they must work together to build a defense case for him before he is taken to jail.",es,2227,8.2 295,"I, Robot","In 2035, where robots are common-place and abide by the three laws of robotics, a techno-phobic cop investigates an apparent suicide. Suspecting that a robot may be responsible for the death, his investigation leads him to believe that humanity may be in danger.",en,7897,6.9 296,Outbreak,"A deadly airborne virus finds its way into the USA and starts killing off people at an epidemic rate. Col Sam Daniels' job is to stop the virus spreading from a small town, which must be quarantined, and to prevent an over reaction by the White House.",en,1166,6.5 297,Saw,"Obsessed with teaching his victims the value of life, a deranged, sadistic serial killer abducts the morally wayward. Once captured, they must face impossible choices in a horrific game of survival. The victims must fight to win their lives back, or die trying...",en,5541,7.4 298,The Purge,"Given the country's overcrowded prisons, the U.S. government begins to allow 12-hour periods of time in which all illegal activity is legal. During one of these free-for-alls, a family must protect themselves from a home invasion.",en,5572,6.2 299,Indiana Jones and the Temple of Doom,"After arriving in India, Indiana Jones is asked by a desperate village to find a mystical stone. He agrees – and stumbles upon a secret cult plotting a terrible plan in the catacombs of an ancient palace.",en,5621,7.2 300,Snow White and the Seven Dwarfs,"A beautiful girl, Snow White, takes refuge in the forest in the house of seven dwarfs to hide from her stepmother, the wicked Queen. The Queen is jealous because she wants to be known as ""the fairest in the land,"" and Snow White's beauty surpasses her own.",en,4857,7.1 301,Marriage Story,"A stage director and an actress struggle through a grueling, coast-to-coast divorce that pushes them to their personal extremes.",en,3056,7.9 302,The Final Scream,"Aspiring actress, Kia Anderson, is about to learn that the final callback for a horror Feature Film is something more than she could ever of imagined - Something sinister is awaiting for Kia.",en,0,0 303,The Hidden Face,A Spanish orchestra conductor deals with the mysterious disappearance of his girlfriend.,es,476,7.1 304,Trolls,"Lovable and friendly, the trolls love to play around. But one day, a mysterious giant shows up to end the party. Poppy, the optimistic leader of the Trolls, and her polar opposite, Branch, must embark on an adventure that takes them far beyond the only world they’ve ever known.",en,2150,6.6 305,Amélie,"At a tiny Parisian café, the adorable yet painfully shy Amélie (Audrey Tautou) accidentally discovers a gift for helping others. Soon Amelie is spending her days as a matchmaker, guardian angel, and all-around do-gooder. But when she bumps into a handsome stranger, will she find the courage to become the star of her very own love story?",fr,7266,7.9 306,Dark Waters,"A tenacious attorney uncovers a dark secret that connects a growing number of unexplained deaths due to one of the world's largest corporations. In the process, he risks everything — his future, his family, and his own life — to expose the truth.",en,391,7.4 307,Toy Story,"Led by Woody, Andy's toys live happily in his room until Andy's birthday brings Buzz Lightyear onto the scene. Afraid of losing his place in Andy's heart, Woody plots against Buzz. But when circumstances separate Buzz and Woody from their owner, the duo eventually learns to put aside their differences.",en,11962,7.9 308,Casino Royale,"Le Chiffre, a banker to the world's terrorists, is scheduled to participate in a high-stakes poker game in Montenegro, where he intends to use his winnings to establish his financial grip on the terrorist market. M sends Bond—on his maiden mission as a 00 Agent—to attend this game and prevent Le Chiffre from winning. With the help of Vesper Lynd and Felix Leiter, Bond enters the most important poker game in his already dangerous career.",en,6861,7.5 309,Dumbo,"A young elephant, whose oversized ears enable him to fly, helps save a struggling circus, but when the circus plans a new venture, Dumbo and his friends discover dark secrets beneath its shiny veneer.",en,2556,6.6 310,Midsommar,"Several friends travel to Sweden to study as anthropologists a summer festival that is held every ninety years in the remote hometown of one of them. What begins as a dream vacation in a place where the sun never sets, gradually turns into a dark nightmare as the mysterious inhabitants invite them to participate in their disturbing festive activities.",en,2005,7.1 311,Wonder Woman,An Amazon princess comes to the world of Man in the grips of the First World War to confront the forces of evil and bring an end to human conflict.,en,14238,7.3 312,Call Me by Your Name,"In 1980s Italy, a relationship begins between seventeen-year-old teenage Elio and the older adult man hired as his father's research assistant.",en,6452,8.3 313,The Matrix Revolutions,The human city of Zion defends itself against the massive invasion of the machines as Neo fights to end the war at another front while also opposing the rogue Agent Smith.,en,5937,6.6 314,Iron Man 3,"When Tony Stark's world is torn apart by a formidable terrorist called the Mandarin, he starts an odyssey of rebuilding and retribution.",en,15780,6.9 315,Airplane!,"Alcoholic pilot, Ted Striker has developed a fear of flying due to wartime trauma, but nevertheless boards a passenger jet in an attempt to woo back his stewardess girlfriend. Food poisoning decimates the passengers and crew, leaving it up to Striker to land the plane with the help of a glue-sniffing air traffic controller and Striker's vengeful former Air Force captain, who must both talk him down.",en,2449,7.3 316,Jumanji: Welcome to the Jungle,"The tables are turned as four teenagers are sucked into Jumanji's world - pitted against rhinos, black mambas and an endless variety of jungle traps and puzzles. To survive, they'll play as characters from the game.",en,8921,6.7 317,Hercules,"Fourteen hundred years ago, a tormented soul walked the earth that was neither man nor god. Hercules was the powerful son of the god king Zeus, for this he received nothing but suffering his entire life. After twelve arduous labors and the loss of his family, this dark, world-weary soul turned his back on the gods finding his only solace in bloody battle. Over the years he warmed to the company of six similar souls, their only bond being their love of fighting and presence of death. These men and woman never question where they go to fight or why or whom, just how much they will be paid. Now the King of Thrace has hired these mercenaries to train his men to become the greatest army of all time. It is time for this bunch of lost souls to finally have their eyes opened to how far they have fallen when they must train an army to become as ruthless and blood thirsty as their reputation has become.",en,2891,5.7 318,47 Meters Down: Uncaged,"Four teenage girls go on a diving adventure to explore a submerged Mayan city. Once inside, their rush of excitement turns into a jolt of terror as they discover the sunken ruins are a hunting ground for deadly great white sharks. With their air supply steadily dwindling, the friends must navigate the underwater labyrinth of claustrophobic caves and eerie tunnels in search of a way out of their watery hell.",en,524,5.1 319,Jurassic Galaxy,"In the near future, a ship of space explorers crash land on an unknown planet. They're soon met with some of their worst fears as they discover the planet is inhabited by monstrous dinosaurs.",en,50,4.1 320,The Panti Sisters,Three gay sons are called back by their estranged and terminally ill father and given an offer they can't refuse: a ₱300 million inheritance in exchange for each of them giving him a grandchild.,tl,3,2.7 321,Ponyo,"The son of a sailor, 5-year old Sosuke lives a quiet life on an oceanside cliff with his mother Lisa. One fateful day, he finds a beautiful goldfish trapped in a bottle on the beach and upon rescuing her, names her Ponyo. But she is no ordinary goldfish. The daughter of a masterful wizard and a sea goddess, Ponyo uses her father's magic to transform herself into a young girl and quickly falls in love with Sosuke, but the use of such powerful sorcery causes a dangerous imbalance in the world. As the moon steadily draws nearer to the earth and Ponyo's father sends the ocean's mighty waves to find his daughter, the two children embark on an adventure of a lifetime to save the world and fulfill Ponyo's dreams of becoming human.",ja,2098,7.7 322,The Flu,"A case of the flu quickly morphs into a pandemic. As the death toll mounts and the living panic, the government plans extreme measures to contain it.",ko,519,7.5 323,Hustlers,A crew of savvy former strip club employees band together to turn the tables on their Wall Street clients.,en,1288,6.3 324,World War Z,"Life for former United Nations investigator Gerry Lane and his family seems content. Suddenly, the world is plagued by a mysterious infection turning whole human populations into rampaging mindless zombies. After barely escaping the chaos, Lane is persuaded to go on a mission to investigate this disease. What follows is a perilous trek around the world where Lane must brave horrific dangers and long odds to find answers before human civilization falls.",en,10375,6.7 325,Skyfall,"When Bond's latest assignment goes gravely wrong and agents around the world are exposed, MI6 is attacked forcing M to relocate the agency. These events cause her authority and position to be challenged by Gareth Mallory, the new Chairman of the Intelligence and Security Committee. With MI6 now compromised from both inside and out, M is left with one ally she can trust: Bond. 007 takes to the shadows - aided only by field agent, Eve - following a trail to the mysterious Silva, whose lethal and hidden motives have yet to reveal themselves.",en,11393,7.1 326,The Keeper of Lost Causes,"Denmark, 2013. Police officers Carl Mørck and Hafez el-Assad, sole members of Department Q, which is focused on closing cold cases, investigate the disappearance of politician Merete Lynggaard, vanished when she and her brother were traveling aboard a ferry five years ago.",da,391,7.2 327,Crawl,"When a huge hurricane hits her hometown in Florida, Haley ignores evacuation orders to look for her father. After finding him badly wounded, both are trapped by the flood. With virtually no time to escape the storm, they discover that rising water levels are the least of their problems.",en,1325,6.2 328,Battle Royale,"Remake of the japanese original 2000 movie directed by Kinji Fukasaku of the same name. amid concerns about its violence, is set in an apocalyptic future in which schools are overrun by uncontrolled violence; the government responds by organizing an annual Battle Royale, in which a school class is picked at random and students are pitted against each other on an abandoned island in a game of survival.",en,0,0 329,Close Encounters of the Third Kind,"After an encounter with UFOs, a line worker feels undeniably drawn to an isolated area in the wilderness where something spectacular is about to happen.",en,2426,7.4 330,The Matrix Reloaded,"Six months after the events depicted in The Matrix, Neo has proved to be a good omen for the free humans, as more and more humans are being freed from the matrix and brought to Zion, the one and only stronghold of the Resistance. Neo himself has discovered his superpowers including super speed, ability to see the codes of the things inside the matrix and a certain degree of pre-cognition. But a nasty piece of news hits the human resistance: 250,000 machine sentinels are digging to Zion and would reach them in 72 hours. As Zion prepares for the ultimate war, Neo, Morpheus and Trinity are advised by the Oracle to find the Keymaker who would help them reach the Source. Meanwhile Neo's recurrent dreams depicting Trinity's death have got him worried and as if it was not enough, Agent Smith has somehow escaped deletion, has become more powerful than before and has fixed Neo as his next target.",en,6614,6.9 331,The Golden Voyage of Sinbad,"Sinbad and his crew intercept a homunculus carrying a golden tablet. Koura, the creator of the homunculus and practitioner of evil magic, wants the tablet back and pursues Sinbad. Meanwhile Sinbad meets the Vizier who has another part of the interlocking golden map, and they mount a quest across the seas to solve the riddle of the map.",en,104,6.6 332,Sniper: Ultimate Kill,"Colombian drug kingpin Jesús Morales secretly pays for the services of a sniper nicknamed ""The Devil,"" capable of killing one-by-one the enemies of anyone who hires him. With no adversaries left alive, Morales grows stronger and gains control of more smuggling routes into the United States. The DEA, alarmed by this threat to the country, sends agent Kate Estrada, who has been following Morales for years, and Marine sniper Brandon Beckett to Colombia. Their mission: Kill ""The Devil"" and bring Morales back to the US to be tried for his crimes. The agents think they have everything under control, but Morales and ""The Devil"" have prepared plenty of surprises to keep the mission from succeeding.",en,84,6.2 333,6 Below: Miracle on the Mountain,An adrenaline seeking snowboarder gets lost in a massive winter storm in the back country of the High Sierras where he is pushed to the limits of human endurance and forced to battle his own personal demons as he fights for survival....,en,207,5.7 334,Camp Cold Brook,Strange events plague crew members of a reality TV show when they visit an infamous backwoods camp that was the site of a mass murder 20 years earlier.,en,0,0 335,Get Out,"Chris and his girlfriend Rose go upstate to visit her parents for the weekend. At first, Chris reads the family's overly accommodating behavior as nervous attempts to deal with their daughter's interracial relationship, but as the weekend progresses, a series of increasingly disturbing discoveries lead him to a truth that he never could have imagined.",en,10504,7.5 336,Rogue One: A Star Wars Story,A rogue band of resistance fighters unite for a mission to steal the Death Star plans and bring a new hope to the galaxy.,en,10571,7.5 337,Princess Mononoke,"Ashitaka, a prince of the disappearing Emishi people, is cursed by a demonized boar god and must journey to the west to find a cure. Along the way, he encounters San, a young human woman fighting to protect the forest, and Lady Eboshi, who is trying to destroy it. Ashitaka must find a way to bring balance to this conflict.",ja,4495,8.4 338,Se7en,"Two homicide detectives are on a desperate hunt for a serial killer whose crimes are based on the ""seven deadly sins"" in this dark and haunting film that takes viewers from the tortured remains of one victim to the next. The seasoned Det. Sommerset researches each sin in an effort to get inside the killer's mind, while his novice partner, Mills, scoffs at his efforts to unravel the case.",en,12482,8.3 339,The Fifth Element,"In 2257, a taxi driver is unintentionally given the task of saving a young girl who is part of the key that will ensure the survival of humanity.",en,7051,7.5 340,The Last Summer,"Standing on the precipice of adulthood, a group of friends navigate new relationships, while reexamining others, during their final summer before college.",en,905,6 341,Blade: Trinity,"For years, Blade has fought against the vampires in the cover of the night. But now, after falling into the crosshairs of the FBI, he is forced out into the daylight, where he is driven to join forces with a clan of human vampire hunters he never knew existed—The Nightstalkers. Together with Abigail and Hannibal, two deftly trained Nightstalkers, Blade follows a trail of blood to the ancient creature that is also hunting him—the original vampire, Dracula.",en,2365,5.8 342,Man of Steel,"A young boy learns that he has extraordinary powers and is not of this earth. As a young man, he journeys to discover where he came from and what he was sent here to do. But the hero in him must emerge if he is to save the world from annihilation and become the symbol of hope for all mankind.",en,10754,6.5 343,Angels & Demons,"Harvard symbologist Robert Langdon is recruited by the Vatican to investigate the apparent return of the Illuminati - a secret, underground organization - after four cardinals are kidnapped on the night of the papal conclave.",en,4483,6.6 344,A Star Is Born,"Seasoned musician Jackson Maine discovers — and falls in love with — struggling artist Ally. She has just about given up on her dream to make it big as a singer — until Jack coaxes her into the spotlight. But even as Ally's career takes off, the personal side of their relationship is breaking down, as Jack fights an ongoing battle with his own internal demons.",en,7627,7.5 345,Cats,A tribe of cats called the Jellicles must decide yearly which one will ascend to the Heaviside Layer and come back to a new Jellicle life.,en,298,4.4 346,Olympus Has Fallen,"When the White House (Secret Service Code: ""Olympus"") is captured by a terrorist mastermind and the President is kidnapped, disgraced former Presidential guard Mike Banning finds himself trapped within the building. As the national security team scrambles to respond, they are forced to rely on Banning's inside knowledge to help retake the White House, save the President and avert an even bigger disaster.",en,4633,6.3 347,The Perfect Date,"No beau? No problem! To earn money for college, a high schooler creates a dating app that lets him act as a stand-in boyfriend.",en,2051,6.3 348,Toy Story 3,"Woody, Buzz, and the rest of Andy's toys haven't been played with in years. With Andy about to go to college, the gang find themselves accidentally left at a nefarious day care center. The toys must band together to escape and return home to Andy.",en,9842,7.8 349,Raiders of the Lost Ark,"When Dr. Indiana Jones – the tweed-suited professor who just happens to be a celebrated archaeologist – is hired by the government to locate the legendary Ark of the Covenant, he finds himself up against the entire Nazi regime.",en,7645,7.9 350,Bumblebee,"On the run in the year 1987, Bumblebee finds refuge in a junkyard in a small Californian beach town. Charlie, on the cusp of turning 18 and trying to find her place in the world, discovers Bumblebee, battle-scarred and broken. When Charlie revives him, she quickly learns this is no ordinary yellow VW bug.",en,3479,6.6 351,The Shack,"After suffering a family tragedy, Mack Phillips spirals into a deep depression causing him to question his innermost beliefs. Facing a crisis of faith, he receives a mysterious letter urging him to an abandoned shack deep in the Oregon wilderness. Despite his doubts, Mack journeys to the shack and encounters an enigmatic trio of strangers led by a woman named Papa. Through this meeting, Mack finds important truths that will transform his understanding of his tragedy and change his life forever.",en,993,7.1 352,The Meg,"A deep sea submersible pilot revisits his past fears in the Mariana Trench, and accidentally unleashes the seventy foot ancestor of the Great White Shark believed to be extinct.",en,3675,5.9 353,The Hunger Games: Mockingjay - Part 2,"With the nation of Panem in a full scale war, Katniss confronts President Snow in the final showdown. Teamed with a group of her closest friends – including Gale, Finnick, and Peeta – Katniss goes off on a mission with the unit from District 13 as they risk their lives to stage an assassination attempt on President Snow who has become increasingly obsessed with destroying her. The mortal traps, enemies, and moral choices that await Katniss will challenge her more than any arena she faced in The Hunger Games.",en,8475,6.9 354,Star Wars: Episode III - Revenge of the Sith,The evil Darth Sidious enacts his final plan for unlimited power -- and the heroic Jedi Anakin Skywalker must choose a side.,en,8727,7.3 355,Night at the Museum: Secret of the Tomb,"When the magic powers of The Tablet of Ahkmenrah begin to die out, Larry Daley spans the globe, uniting favorite and new characters while embarking on an epic quest to save the magic before it is gone forever.",en,4045,6.2 356,American Pie Presents: The Naked Mile,"The movie will shift its focus on Erik Stifler, the cousin of Matt and Steve, a youngster who is nothing like his wild relations. Peer pressure starts to turn him to live up to the legacy of the other Stiflers when he attends the Naked Mile, a naked run across the college campus. Things get worse when he finds that his cousin Dwight is the life of the party down at the campus",en,1235,5.4 357,Dilwale Dulhania Le Jayenge,"Raj is a rich, carefree, happy-go-lucky second generation NRI. Simran is the daughter of Chaudhary Baldev Singh, who in spite of being an NRI is very strict about adherence to Indian values. Simran has left for India to be married to her childhood fiancé. Raj leaves for India with a mission at his hands, to claim his lady love under the noses of her whole family. Thus begins a saga.",hi,2230,8.8 358,Men in Black: International,"The Men in Black have always protected the Earth from the scum of the universe. In this new adventure, they tackle their biggest, most global threat to date: a mole in the Men in Black organization.",en,2307,5.9 359,Kingsman: The Golden Circle,"When an attack on the Kingsman headquarters takes place and a new villain rises, Eggsy and Merlin are forced to work together with the American agency known as the Statesman to save the world.",en,6385,6.9 360,The Interview,"Dave Skylark and his producer Aaron Rapaport run the celebrity tabloid show ""Skylark Tonight"". When they land an interview with a surprise fan, North Korean dictator Kim Jong-un, they are recruited by the CIA to turn their trip to Pyongyang into an assassination mission.",en,3993,6.2 361,Forrest Gump,"A man with a low IQ has accomplished great things in his life and been present during significant historic events—in each case, far exceeding what anyone imagined he could do. But despite all he has achieved, his one true love eludes him.",en,17344,8.4 362,Dunkirk,"The story of the miraculous evacuation of Allied soldiers from Belgium, Britain, Canada and France, who were cut off and surrounded by the German army from the beaches and harbour of Dunkirk between May 26th and June 4th 1940 during World War II.",en,10628,7.4 363,Block Z,"A pre-med student and her friends encounter the death of a patient that exhibited symptoms of rabies. They are soon faced with an even bigger problem as their patient comes back from the dead and infects the people on campus, causing a lockdown and trapping the students inside.",en,4,7.8 364,Kiss and Kill,"A reckless night of indiscretion and lust leads a woman into the dark world of blackmail and murder. When revealing photos of her sexual tryst unexpectedly surface, Katy is forced to submit to whatever her tormentors desire.",en,10,4.2 365,How to Train Your Dragon 2,"The thrilling second chapter of the epic How To Train Your Dragon trilogy brings back the fantastical world of Hiccup and Toothless five years later. While Astrid, Snotlout and the rest of the gang are challenging each other to dragon races (the island's new favorite contact sport), the now inseparable pair journey through the skies, charting unmapped territories and exploring new worlds. When one of their adventures leads to the discovery of a secret ice cave that is home to hundreds of new wild dragons and the mysterious Dragon Rider, the two friends find themselves at the center of a battle to protect the peace.",en,6433,7.7 366,Lady Bird,"A California high school student plans to escape from her family and small town by going to college in New York, much to the disapproval of wildly loving, deeply opinionated and strong-willed mother.",en,4848,7.3 367,The Kissing Booth,"When teenager Elle's first kiss leads to a forbidden romance with the hottest boy in high school, she risks her relationship with her best friend.",en,4124,7.2 368,No Time to Die,"Bond has left active service and is enjoying a tranquil life in Jamaica. His peace is short-lived when his old friend Felix Leiter from the CIA turns up asking for help. The mission to rescue a kidnapped scientist turns out to be far more treacherous than expected, leading Bond onto the trail of a mysterious villain armed with dangerous new technology.",en,0,0 369,The Hangover,"When three friends finally come to after a raucous night of bachelor-party revelry, they find a baby in the closet and a tiger in the bathroom. But they can't seem to locate their best friend, Doug – who's supposed to be tying the knot. Launching a frantic search for Doug, the trio perseveres through a nasty hangover to try to make it to the church on time.",en,11693,7.3 370,Sekaiichi Hatsukoi: Propose-hen,"When Ritsu Onodera changes jobs, looking for a fresh start, he's not exactly thrilled when his new boss turns out to be his old flame. Ritsu's determined to leave all that in the past—but how can he when his boss is just as determined that they have a future? Tired of accusations that family connections got him his current position, Ritsu Onodera quits his job as an editor at his father's company and transfers to Marukawa Publishing. Once there, he is assigned to the shojo manga editorial department—something he has no interest in and no experience with! Having sworn he'd never fall in love again, the last thing he wants to do is work on love stories. To make matters worse, it turns out that his overbearing boss, Masamune Takano, is actually his first love from high school!",ja,0,0 371,Saekano: How to Raise a Boring Girlfriend Fine,"The conclusion to the ""Saenai Heroine no Sodatekata"" series.",ja,4,4.5 372,The Devil Wears Prada,"Andy moves to New York to work in the fashion industry. Her boss is extremely demanding, cruel and won't let her succeed if she doesn't fit into the high class elegant look of their magazine.",en,7631,7.3 373,Ralph Breaks the Internet,"Video game bad guy Ralph and fellow misfit Vanellope von Schweetz must risk it all by traveling to the World Wide Web in search of a replacement part to save Vanellope's video game, Sugar Rush. In way over their heads, Ralph and Vanellope rely on the citizens of the internet — the netizens — to help navigate their way, including an entrepreneur named Yesss, who is the head algorithm and the heart and soul of trend-making site BuzzzTube.",en,3950,7.2 374,Despicable Me,"Villainous Gru lives up to his reputation as a despicable, deplorable and downright unlikable guy when he hatches a plan to steal the moon from the sky. But he has a tough time staying on task after three orphans land in his care.",en,11095,7.2 375,The Dark Knight Rises,"Following the death of District Attorney Harvey Dent, Batman assumes responsibility for Dent's crimes to protect the late attorney's reputation and is subsequently hunted by the Gotham City Police Department. Eight years later, Batman encounters the mysterious Selina Kyle and the villainous Bane, a new terrorist leader who overwhelms Gotham's finest. The Dark Knight resurfaces to protect a city that has branded him an enemy.",en,15616,7.7 376,Sing,A koala named Buster recruits his best friend to help him drum up business for his theater by hosting a singing competition.,en,4955,7 377,Descendants 3,The teenagers of Disney's most infamous villains return to the Isle of the Lost to recruit a new batch of villainous offspring to join them at Auradon Prep.,en,577,7.8 378,Red Sparrow,"Prima ballerina, Dominika Egorova faces a bleak and uncertain future after she suffers an injury that ends her career. She soon turns to Sparrow School, a secret intelligence service that trains exceptional young people to use their minds and bodies as weapons. Dominika emerges as the most dangerous Sparrow after completing the sadistic training process. As she comes to terms with her new abilities, she meets a CIA agent who tries to convince her that he is the only person she can trust.",en,4016,6.5 379,The Boy in the Striped Pyjamas,"When his family moves from their home in Berlin to a strange new house in Poland, young Bruno befriends Shmuel, a boy who lives on the other side of the fence where everyone seems to be wearing striped pajamas. Unaware of Shmuel's fate as a Jewish prisoner or the role his own Nazi father plays in his imprisonment, Bruno embarks on a dangerous journey inside the camp's walls.",en,4251,7.8 380,The November Man,An ex- CIA operative is brought back in on a very personal mission and finds himself pitted against his former pupil in a deadly game involving high level CIA officials and the Russian president-elect.,en,953,6.1 381,Alice in Wonderland,"On a golden afternoon, young Alice follows a White Rabbit, who disappears down a nearby rabbit hole. Quickly following him, she tumbles into the burrow - and enters the merry, topsy-turvy world of Wonderland! Memorable songs and whimsical escapades highlight Alice's journey, which culminates in a madcap encounter with the Queen of Hearts - and her army of playing cards!",en,3855,7.1 382,Despicable Me 2,Gru is recruited by the Anti-Villain League to help deal with a powerful new super criminal.,en,8123,6.9 383,Lucy,"A woman, accidentally caught in a dark deal, turns the tables on her captors and transforms into a merciless warrior evolved beyond human logic.",en,11249,6.3 384,Cold Pursuit,"The quiet family life of Nels Coxman, a snowplow driver, is upended after his son's murder. Nels begins a vengeful hunt for Viking, the drug lord he holds responsible for the killing, eliminating Viking's associates one by one. As Nels draws closer to Viking, his actions bring even more unexpected and violent consequences, as he proves that revenge is all in the execution.",en,1372,5.5 385,Quantum of Solace,"Quantum of Solace continues the adventures of James Bond after Casino Royale. Betrayed by Vesper, the woman he loved, 007 fights the urge to make his latest mission personal. Pursuing his determination to uncover the truth, Bond and M interrogate Mr. White, who reveals that the organization that blackmailed Vesper is far more complex and dangerous than anyone had imagined.",en,5058,6.3 386,The Secret Life of Pets 2,"Max the terrier must cope with some major life changes when his owner gets married and has a baby. When the family takes a trip to the countryside, nervous Max has numerous run-ins with canine-intolerant cows, hostile foxes and a scary turkey. Luckily for Max, he soon catches a break when he meets Rooster, a gruff farm dog who tries to cure the lovable pooch of his neuroses.",en,1289,6.8 387,Ratatouille,"A rat named Remy dreams of becoming a great French chef despite his family's wishes and the obvious problem of being a rat in a decidedly rodent-phobic profession. When fate places Remy in the sewers of Paris, he finds himself ideally situated beneath a restaurant made famous by his culinary hero, Auguste Gusteau. Despite the apparent dangers of being an unlikely - and certainly unwanted - visitor in the kitchen of a fine French restaurant, Remy's passion for cooking soon sets into motion a hilarious and exciting rat race that turns the culinary world of Paris upside down.",en,10474,7.7 388,The Circle,"A young tech worker takes a job at a powerful Internet corporation, quickly rises up the company's ranks, and soon finds herself in a perilous situation concerning privacy, surveillance and freedom. She comes to learn that her decisions and actions will determine the future of humanity.",en,3123,5.5 389,Predator,"Dutch and his group of commandos are hired by the CIA to rescue downed airmen from guerillas in a Central American jungle. The mission goes well but as they return they find that something is hunting them. Nearly invisible, it blends in with the forest, taking trophies from the bodies of its victims as it goes along. Occasionally seeing through its eyes, the audience sees it is an intelligent alien hunter, hunting them for sport, killing them off one at a time.",en,4339,7.4 390,GoldenEye,"When a powerful satellite system falls into the hands of Alec Trevelyan, AKA Agent 006, a former ally-turned-enemy, only James Bond can save the world from an awesome space weapon that -- in one short pulse -- could destroy the earth! As Bond squares off against his former compatriot, he also battles Trevelyan's stunning ally, Xenia Onatopp, an assassin who uses pleasure as her ultimate weapon.",en,2262,6.8 391,Fantasy Island,"A group of contest winners arrive at an island hotel to live out their dreams, only to find themselves trapped in nightmare scenarios.",en,180,5.1 392,2001: A Space Odyssey,"Humanity finds a mysterious object buried beneath the lunar surface and sets off to find its origins with the help of HAL 9000, the world's most advanced super computer.",en,6929,8.1 393,Bruce Almighty,"Bruce Nolan toils as a 'human interest' television reporter in Buffalo, N.Y., but despite his high ratings and the love of his beautiful girlfriend, Bruce remains unfulfilled. At the end of the worst day in his life, he angrily ridicules God—and the Almighty responds, endowing Bruce with all of His divine powers.",en,7248,6.7 394,Arjona Circo Soledad en Vivo,,es,0,0 395,Goldfinger,"Special agent 007 comes face to face with one of the most notorious villains of all time, and now he must outwit and outgun the powerful tycoon to prevent him from cashing in on a devious scheme to raid Fort Knox -- and obliterate the world's economy.",en,2014,7.4 396,Django Unchained,"With the help of a German bounty hunter, a freed slave sets out to rescue his wife from a brutal Mississippi plantation owner.",en,17864,8.1 397,The Transporter Refueled,"The fast-paced action movie is again set in the criminal underworld in France, where Frank Martin is known as The Transporter, because he is the best driver and mercenary money can buy. In this installment, he meets Anna and they attempt to take down a group of ruthless Russian human traffickers who also have kidnapped Frank’s father.",en,950,5.5 398,The Expendables,"Barney Ross leads a band of highly skilled mercenaries including knife enthusiast Lee Christmas, a martial arts expert, heavy weapons specialist, demolitionist, and a loose-cannon sniper. When the group is commissioned by the mysterious Mr. Church to assassinate the dictator of a small South American island, Barney and Lee visit the remote locale to scout out their opposition and discover the true nature of the conflict engulfing the city.",en,5203,6.1 399,Memento,"Leonard Shelby is tracking down the man who raped and murdered his wife. The difficulty of locating his wife's killer, however, is compounded by the fact that he suffers from a rare, untreatable form of short-term memory loss. Although he can recall details of life before his accident, Leonard cannot remember what happened fifteen minutes ago, where he's going, or why.",en,9080,8.2 400,The Grand Budapest Hotel,"The Grand Budapest Hotel tells of a legendary concierge at a famous European hotel between the wars and his friendship with a young employee who becomes his trusted protégé. The story involves the theft and recovery of a priceless Renaissance painting, the battle for an enormous family fortune and the slow and then sudden upheavals that transformed Europe during the first half of the 20th century.",en,9326,8 401,Mamu (and a Mother Too),"A transgender sex worker in her late 40s along Fields Avenue whose only aspiration is to have breast implants for her profession unexpectedly assumes the role of a mother to her orphaned niece, a transgender youth who is only beginning to discover her own sexuality. As she works more shifts to save for her implants, troubles arise when she begins to feel the weight of her struggles – being an aging sex worker in fast-evolving society, a partner to her young fiancé, and a parent to a teenager she just met. Her difficult confrontations eventually lead her to a new attitude towards life, and a unique recipe to a famous Kapampangan dish, Sisig.",tl,1,10 402,Guardians of the Galaxy,"Light years from Earth, 26 years after being abducted, Peter Quill finds himself the prime target of a manhunt after discovering an orb wanted by Ronan the Accuser.",en,20132,7.9 403,Inferno,"After waking up in a hospital with amnesia, professor Robert Langdon and a doctor must race against time to foil a deadly global plot.",en,4328,5.9 404,Gattaca,"In a future society in the era of indefinite eugenics, humans are set on a life course depending on their DNA. Young Vincent Freeman is born with a condition that would prevent him from space travel, yet is determined to infiltrate the GATTACA space program.",en,3802,7.5 405,Edward Scissorhands,A small suburban town receives a visit from a castaway unfinished science experiment named Edward.,en,8521,7.7 406,Child 44,"Set in Stalin-era Soviet Union, a disgraced MGB agent is dispatched to investigate a series of child murders -- a case that begins to connect with the very top of party leadership.",en,1031,6.2 407,Dracula Untold,"Vlad Tepes is a great hero, but when he learns the Sultan is preparing for battle and needs to form an army of 1,000 boys, he vows to find a way to protect his family. Vlad turns to dark forces in order to get the power to destroy his enemies and agrees to go from hero to monster as he's turned into the mythological vampire, Dracula.",en,4149,6.3 408,Borg vs McEnroe,"The Swedish Björn Borg and the American John McEnroe, the best tennis players in the world, maintain a legendary duel during the 1980 Wimbledon tournament.",sv,713,7 409,Transformers: Age of Extinction,"As humanity picks up the pieces, following the conclusion of ""Transformers: Dark of the Moon,"" Autobots and Decepticons have all but vanished from the face of the planet. However, a group of powerful, ingenious businessman and scientists attempt to learn from past Transformer incursions and push the boundaries of technology beyond what they can control - all while an ancient, powerful Transformer menace sets Earth in his cross-hairs.",en,5366,5.8 410,Us,"Husband and wife Gabe and Adelaide Wilson take their kids to their beach house expecting to unplug and unwind with friends. But as night descends, their serenity turns to tension and chaos when some shocking visitors arrive uninvited.",en,3655,7 411,The Departed,"To take down South Boston's Irish Mafia, the police send in one of their own to infiltrate the underworld, not realizing the syndicate has done likewise. While an undercover cop curries favor with the mob kingpin, a career criminal rises through the police ranks. But both sides soon discover there's a mole among them.",en,9242,8.1 412,The Twilight Saga: Breaking Dawn - Part 1,The new found married bliss of Bella Swan and vampire Edward Cullen is cut short when a series of betrayals and misfortunes threatens to destroy their world.,en,5815,6 413,La La Land,"Mia, an aspiring actress, serves lattes to movie stars in between auditions and Sebastian, a jazz musician, scrapes by playing cocktail party gigs in dingy bars, but as success mounts they are faced with decisions that begin to fray the fragile fabric of their love affair, and the dreams they worked so hard to maintain in each other threaten to rip them apart.",en,11536,7.9 414,Lady and the Tramp,"Lady, a golden cocker spaniel, meets up with a mongrel dog who calls himself the Tramp. He is obviously from the wrong side of town, but happenings at Lady's home make her decide to travel with him for a while.",en,3342,7.1 415,Spider-Man,"After being bitten by a genetically altered spider, nerdy high school student Peter Parker is endowed with amazing powers to become the Amazing superhero known as Spider-Man.",en,11429,7.1 416,The Jungle Book,"A man-cub named Mowgli fostered by wolves. After a threat from the tiger Shere Khan, Mowgli is forced to flee the jungle, by which he embarks on a journey of self discovery with the help of the panther, Bagheera and the free-spirited bear, Baloo.",en,5899,6.8 417,Die Hard,"NYPD cop John McClane's plan to reconcile with his estranged wife is thrown for a serious loop when, minutes after he arrives at her office, the entire building is overtaken by a group of terrorists. With little help from the LAPD, wisecracking McClane sets out to single-handedly rescue the hostages and bring the bad guys down.",en,7019,7.7 418,"The Good, the Bad and the Ugly","While the Civil War rages between the Union and the Confederacy, three men – a quiet loner, a ruthless hit man and a Mexican bandit – comb the American Southwest in search of a strongbox containing $200,000 in stolen gold.",it,4812,8.4 419,RED,"When his peaceful life is threatened by a high-tech assassin, former black-ops agent, Frank Moses reassembles his old team in a last ditch effort to survive and uncover his assailants.",en,4587,6.7 420,Suburbicon,"In the quiet family town of Suburbicon during the 1950s, the best and worst of humanity is hilariously reflected through the deeds of seemingly ordinary people. When a home invasion turns deadly, a picture-perfect family turns to blackmail, revenge and murder.",en,1053,5.8 421,The Twilight Saga: Breaking Dawn - Part 2,"After the birth of Renesmee, the Cullens gather other vampire clans in order to protect the child from a false allegation that puts the family in front of the Volturi.",en,5871,6.3 422,Gladiator,"In the year 180, the death of emperor Marcus Aurelius throws the Roman Empire into chaos. Maximus is one of the Roman army's most capable and trusted generals and a key advisor to the emperor. As Marcus' devious son Commodus ascends to the throne, Maximus is set to be executed. He escapes, but is captured by slave traders. Renamed Spaniard and forced to become a gladiator, Maximus must battle to the death with other men for the amusement of paying audiences.",en,11316,8.1 423,Prometheus,"A team of explorers discover a clue to the origins of mankind on Earth, leading them on a journey to the darkest corners of the universe. There, they must fight a terrifying battle to save the future of the human race.",en,8178,6.4 424,Me Before You,"A small town girl is caught between dead-end jobs. A high-profile, successful man becomes wheelchair bound following an accident. The man decides his life is not worth living until the girl is hired for six months to be his new caretaker. Worlds apart and trapped together by circumstance, the two get off to a rocky start. But the girl becomes determined to prove to the man that life is worth living and as they embark on a series of adventures together, each finds their world changing in ways neither of them could begin to imagine.",en,6941,7.8 425,Lupinranger VS Patranger VS Kyuranger,"""Lupinranger VS Patranger VS Kyuranger"" is an upcoming V-Cinext film between Kaitou Sentai Lupinranger VS Keisatsu Sentai Patranger and Uchu Sentai Kyuranger. The story begins when the Lupinrangers, Kairi, Touma, and Umika, are kidnapped by someone mysterious. The Patrangers are then tasked with an Abduction Case to find the missing thieves, where they run into the Kyuranger team as they pass through space. Just who exactly kidnapped them? And why did the 12 Kyurangers return to space?",ja,2,5.5 426,Train to Busan,"Martial law is declared when a mysterious viral outbreak pushes Korea into a state of emergency. Those on an express train to Busan, a city that has successfully fended off the viral outbreak, must fight for their own survival…",ko,3324,7.7 427,Insidious: The Last Key,"Parapsychologist Elise Rainier and her team travel to Five Keys, NM, to investigate a man’s claim of a haunting. Terror soon strikes when Rainier realizes that the house he lives in was her family’s old home.",en,1612,6.1 428,Richard Jewell,"Directed by Clint Eastwood and based on true events, ""Richard Jewell"" is a story of what happens when what is reported as fact obscures the truth. ""There is a bomb in Centennial Park. You have thirty minutes."" The world is first introduced to Richard Jewell as the security guard who reports finding the device at the 1996 Atlanta bombing-his report making him a hero whose swift actions save countless lives. But within days, the law enforcement wannabe becomes the FBI's number one suspect, vilified by press and public alike, his life ripped apart. Richard Jewell thinks quick, works fast, and saves hundreds, perhaps thousands, of lives after a domestic terrorist plants several pipe bombs and they explode during a concert, only to be falsely suspected of the crime by sloppy FBI work and sensational media coverage.",en,669,7.5 429,Tomb Raider,"Lara Croft, the fiercely independent daughter of a missing adventurer, must push herself beyond her limits when she finds herself on the island where her father disappeared.",en,5163,6.3 430,Mission: Impossible - Ghost Protocol,"Ethan Hunt and his team are racing against time to track down a dangerous terrorist named Hendricks, who has gained access to Russian nuclear launch codes and is planning a strike on the United States. An attempt to stop him ends in an explosion causing severe destruction to the Kremlin and the IMF to be implicated in the bombing, forcing the President to disavow them. No longer being aided by the government, Ethan and his team chase Hendricks around the globe, although they might still be too late to stop a disaster.",en,6763,7 431,Taken 3,"Ex-government operative Bryan Mills finds his life is shattered when he's falsely accused of a murder that hits close to home. As he's pursued by a savvy police inspector, Mills employs his particular set of skills to track the real killer and exact his unique brand of justice.",en,3863,6.2 432,Dreams of Darkness,"Devastated by the disappearance of his wife, Derek Fabry enters a nightmarish world of the occult, erotic evil, and supernatural seduction as he tries to unravel the mystery of her vanishing.",en,0,0 433,Ted 2,"Newlywed couple Ted and Tami-Lynn want to have a baby, but in order to qualify to be a parent, Ted will have to prove he's a person in a court of law.",en,4743,6.2 434,Rambo: First Blood Part II,John Rambo is released from prison by the government for a top-secret covert mission to the last place on Earth he'd want to return - the jungles of Vietnam.,en,2000,6.5 435,Pirates of the Caribbean: Dead Men Tell No Tales,"Thrust into an all-new adventure, a down-on-his-luck Capt. Jack Sparrow feels the winds of ill-fortune blowing even more strongly when deadly ghost sailors led by his old nemesis, the evil Capt. Salazar, escape from the Devil's Triangle. Jack's only hope of survival lies in seeking out the legendary Trident of Poseidon, but to find it, he must forge an uneasy alliance with a brilliant and beautiful astronomer and a headstrong young man in the British navy.",en,7888,6.6 436,Ne Zha,"The Primus extracts a Mixed Yuan Bead into a Spirit Seed and a Demon Pill. The Spirit Seed can be reincarnated as a human to help King Zhou establish a new dynasty, whereas the Demon Pill will create a devil threatening humanity. Ne Zha is the one who is destined to be the hero, but instead he becomes a devil incarnate, because the Spirit Seed and a Demon Pill are switched.",zh,39,7.3 437,Resistance,The story of a group of Jewish Boy Scouts who worked with the French Resistance to save the lives of ten thousand orphans during World War II.,en,3,7.7 438,Maze Runner: The Scorch Trials,"Thomas and his fellow Gladers face their greatest challenge yet: searching for clues about the mysterious and powerful organization known as WCKD. Their journey takes them to the Scorch, a desolate landscape filled with unimaginable obstacles. Teaming up with resistance fighters, the Gladers take on WCKD’s vastly superior forces and uncover its shocking plans for them all.",en,7065,6.6 439,Bad Boys II,"Out-of-control, trash-talking buddy cops Marcus Burnett and Mike Lowrey of the Miami Narcotics Task Force reunite, and bullets fly, cars crash and laughs explode as they pursue a whacked-out drug lord from the streets of Miami to the barrios of Cuba. But the real fireworks result when Marcus discovers that playboy Mike is secretly romancing Marcus’ sexy sister.",en,3236,6.5 440,Star Trek,"The fate of the galaxy rests in the hands of bitter rivals. One, James Kirk, is a delinquent, thrill-seeking Iowa farm boy. The other, Spock, a Vulcan, was raised in a logic-based society that rejects all emotion. As fiery instinct clashes with calm reason, their unlikely but powerful partnership is the only thing capable of leading their crew through unimaginable danger, boldly going where no one has gone before. The human adventure has begun again.",en,6995,7.4 441,Jupiter Ascending,"In a universe where human genetic material is the most precious commodity, an impoverished young Earth woman becomes the key to strategic maneuvers and internal strife within a powerful dynasty…",en,4877,5.3 442,Horrible Bosses 2,"Dale, Kurt and Nick decide to start their own business but things don't go as planned because of a slick investor, prompting the trio to pull off a harebrained and misguided kidnapping scheme.",en,2882,6.1 443,Terminator 3: Rise of the Machines,"It's been 10 years since John Connor saved Earth from Judgment Day, and he's now living under the radar, steering clear of using anything Skynet can trace. That is, until he encounters T-X, a robotic assassin ordered to finish what T-1000 started. Good thing Connor's former nemesis, the Terminator, is back to aid the now-adult Connor … just like he promised.",en,4149,6 444,Furious 7,Deckard Shaw seeks revenge against Dominic Toretto and his family for his comatose brother.,en,7268,7.3 445,Dora and the Lost City of Gold,"Dora, a girl who has spent most of her life exploring the jungle with her parents, now must navigate her most dangerous adventure yet: high school. Always the explorer, Dora quickly finds herself leading Boots (her best friend, a monkey), Diego, and a rag tag group of teens on an adventure to save her parents and solve the impossible mystery behind a lost Inca civilization.",en,642,6.5 446,An Officer and a Spy,"In 1894, French Captain Alfred Dreyfus is wrongfully convicted of treason and sentenced to life imprisonment at Devil's Island.",fr,459,7.2 447,Shutter Island,"World War II soldier-turned-U.S. Marshal Teddy Daniels investigates the disappearance of a patient from a hospital for the criminally insane, but his efforts are compromised by his troubling visions and also by a mysterious doctor.",en,14891,8.1 448,Space Jam,"Swackhammer, an evil alien theme park owner, needs a new attraction at Moron Mountain. When his gang, the Nerdlucks, heads to Earth to kidnap Bugs Bunny and the Looney Tunes, Bugs challenges them to a basketball game to determine their fate. The aliens agree, but they steal the powers of NBA basketball players, including Larry Bird and Charles Barkley -- so Bugs gets some help from superstar Michael Jordan.",en,3368,6.7 449,Nightcrawler,"When Lou Bloom, desperate for work, muscles into the world of L.A. crime journalism, he blurs the line between observer and participant to become the star of his own story. Aiding him in his effort is Nina, a TV-news veteran.",en,6790,7.7 450,Ocean's Eight,"Debbie Ocean, a criminal mastermind, gathers a crew of female thieves to pull off the heist of the century at New York's annual Met Gala.",en,4698,6.9 451,Monty Python and the Holy Grail,"King Arthur, accompanied by his squire, recruits his Knights of the Round Table, including Sir Bedevere the Wise, Sir Lancelot the Brave, Sir Robin the Not-Quite-So-Brave-As-Sir-Lancelot and Sir Galahad the Pure. On the way, Arthur battles the Black Knight who, despite having had all his limbs chopped off, insists he can still fight. They reach Camelot, but Arthur decides not to enter, as ""it is a silly place"".",en,3530,7.8 452,Shipwrecked,A young Norwegian boy in 1850s England goes to work as a cabin boy and discovers some of his shipmates are actually pirates.,no,26,6.7 453,Shrek the Third,"The King of Far Far Away has died and Shrek and Fiona are to become King & Queen. However, Shrek wants to return to his cozy swamp and live in peace and quiet, so when he finds out there is another heir to the throne, they set off to bring him back to rule the kingdom.",en,5638,6.2 454,Chance,"Chance Matthew Modine (Full Metal Jacket/Stranger Things) stars as a youth baseball coach in the film Chance, a true story of a teenage love triangle leading to one of the two boys' tragic death - told through the lens of elite youth baseball. Sections of the film feature the fun of 6-year-old tee-ball and the best baseball ever seen on screen from 12-year-olds. Tanner Buchanan (Designated Survivor/Cobra Kai), Amanda Leighton (This is Us/The Fosters) and Blake Cooper (The Maze Runner, Measure of a Man) star in teenage roles. All score is original by Ian Honeyman and performed, with original lyrics written and performed by Angela Parrish from La La Land. Genres include Family/Sports/Faith/Drama.",en,0,0 455,American Pie,"At a high-school party, four friends find that losing their collective virginity isn't as easy as they had thought. But they still believe that they need to do so before college. To motivate themselves, they enter a pact to all ""score."" by their senior prom.",en,5003,6.5 456,Uncorked,Elijah must balance his dream of becoming a master sommelier with his father's expectations that he carry on the family's Memphis BBQ joint.,en,51,6.4 457,The Mummy,Dashing legionnaire Rick O'Connell stumbles upon the hidden ruins of Hamunaptra while in the midst of a battle to claim the area in 1920s Egypt. It has been over three thousand years since former High Priest Imhotep suffered a fate worse than death as a punishment for a forbidden love—along with a curse that guarantees eternal doom upon the world if he is ever awoken.,en,5668,6.8 458,Percy Jackson & the Olympians: The Lightning Thief,"Accident prone teenager, Percy discovers he's actually a demi-God, the son of Poseidon, and he is needed when Zeus' lightning is stolen. Percy must master his new found skills in order to prevent a war between the Gods that could devastate the entire world.",en,4958,6.1 459,GoodFellas,"The true story of Henry Hill, a half-Irish, half-Sicilian Brooklyn kid who is adopted by neighbourhood gangsters at an early age and climbs the ranks of a Mafia family under the guidance of Jimmy Conway.",en,6994,8.4 460,Shrek 2,"Shrek, Fiona and Donkey set off to Far, Far Away to meet Fiona's mother and father. But not everyone is happy. Shrek and the King find it hard to get along, and there's tension in the marriage. The fairy godmother discovers that Shrek has married Fiona instead of her Son Prince Charming and sets about destroying their marriage.",en,7302,7 461,Hacksaw Ridge,"WWII American Army Medic Desmond T. Doss, who served during the Battle of Okinawa, refuses to kill people and becomes the first Conscientious Objector in American history to receive the Congressional Medal of Honor.",en,7735,8.1 462,The Prestige,"A mysterious story of two magicians whose intense rivalry leads them on a life-long battle for supremacy -- full of obsession, deceit and jealousy with dangerous and deadly consequences.",en,9617,8.2 463,Balkan Line,"NATO bombs Belgrad March 1999. June: Serb army pulls out of Kosovo. Some Muslims believe they can now plunder, rape and kill Serbs in Kosovo. 5 Russians are to take Kosovo's airport from such bandits and hold it until peacekeepers arrive.",ru,53,7.3 464,Life Is Beautiful,A touching story of an Italian book seller of Jewish ancestry who lives in his own little fairy tale. His creative and happy life would come to an abrupt halt when his entire family is deported to a concentration camp during World War II. While locked up he tries to convince his son that the whole thing is just a game.,it,8477,8.5 465,The Hobbit: The Desolation of Smaug,"The Dwarves, Bilbo and Gandalf have successfully escaped the Misty Mountains, and Bilbo has gained the One Ring. They all continue their journey to get their gold back from the Dragon, Smaug.",en,8672,7.6 466,San Andreas,"In the aftermath of a massive earthquake in California, a rescue-chopper pilot makes a dangerous journey across the state in order to rescue his estranged daughter.",en,5750,6.1 467,Men in Black II,"Kay and Jay reunite to provide our best, last and only line of defense against a sinister seductress who levels the toughest challenge yet to the MIB's untarnished mission statement – protecting Earth from the scum of the universe. It's been four years since the alien-seeking agents averted an intergalactic disaster of epic proportions. Now it's a race against the clock as Jay must convince Kay – who not only has absolutely no memory of his time spent with the MIB, but is also the only living person left with the expertise to save the galaxy – to reunite with the MIB before the earth submits to ultimate destruction.",en,6659,6.3 468,How to Train Your Dragon,"As the son of a Viking leader on the cusp of manhood, shy Hiccup Horrendous Haddock III faces a rite of passage: he must kill a dragon to prove his warrior mettle. But after downing a feared dragon, he realizes that he no longer wants to destroy it, and instead befriends the beast – which he names Toothless – much to the chagrin of his warrior father",en,8738,7.7 469,Violet Evergarden: Eternity and the Auto Memory Doll,"Isabella, the daughter of the noble York family, is enrolled in an all-girls academy to be groomed into a dame worthy of nobility. However, she has given up on her future, seeing the prestigious school as nothing more than a prison from the outside world. Her family notices her struggling in her lessons and decides to hire Violet Evergarden to personally tutor her under the guise of a handmaiden. At first, Isabella treats Violet coldly. Violet seems to be able to do everything perfectly, leading Isabella to assume that she was born with a silver spoon. After some time, Isabella begins to realize that Violet has had her own struggles and starts to open up to her. Isabella soon reveals that she has lost contact with her beloved younger sister, whom she yearns to see again. Having experienced the power of words through her past clientele, Violet asks if Isabella wishes to write a letter to Taylor. Will Violet be able to help Isabella convey her feelings to her long-lost sister?",ja,40,8.2 470,Dragonheart: Vengeance,"Lukas, a young farmer whose family is killed by savage raiders in the countryside, sets out on an epic quest for revenge, forming an unlikely trio with a majestic dragon and a swashbuckling, sword-fighting mercenary, Darius.",en,73,5.9 471,The Incredible Hulk,"Scientist Bruce Banner scours the planet for an antidote to the unbridled force of rage within him: the Hulk. But when the military masterminds who dream of exploiting his powers force him back to civilization, he finds himself coming face to face with a new, deadly foe.",en,7287,6.2 472,The Incredibles,"Bob Parr has given up his superhero days to log in time as an insurance adjuster and raise his three children with his formerly heroic wife in suburbia. But when he receives a mysterious assignment, it's time to get back into costume.",en,12021,7.7 473,Scary Stories to Tell in the Dark,"Mill Valley, Pennsylvania, Halloween night, 1968. After playing a joke on a school bully, Sarah and her friends decide to sneak into a supposedly haunted house that once belonged to the powerful Bellows family, unleashing dark forces that they will be unable to control.",en,1065,6.3 474,Bird Box,"Five years after an ominous unseen presence drives most of society to suicide, a survivor and her two children make a desperate bid to reach safety.",en,6173,6.9 475,X-Men: Days of Future Past,The ultimate X-Men ensemble fights a war for the survival of the species across two time periods as they join forces with their younger selves in an epic battle that must change the past – to save our future.,en,11109,7.5 476,Grace,"In the wake of a horrific car accident that kills her husband, Michael, expectant mother Madeline Matheson discovers that her daughter, Grace, has died in the womb. Ignoring her doctor's warnings that the fetus must be removed from her body, a grief-stricken Matheson demands to carry the child to term -- even if it endangers her own life to do so. Curiously, little Grace emerges undead -- and with a craving for human blood.",en,76,5.1 477,Grease,"Australian good girl Sandy and greaser Danny fell in love over the summer. But when they unexpectedly discover they're now in the same high school, will they be able to rekindle their romance despite their eccentric friends?",en,4540,7.4 478,The Passion of the Christ,A graphic portrayal of the last twelve hours of Jesus of Nazareth's life.,en,2098,7.1 479,Star Wars: Episode I - The Phantom Menace,"Anakin Skywalker, a young slave strong with the Force, is discovered on Tatooine. Meanwhile, the evil Sith have returned, enacting their plot for revenge against the Jedi.",en,9332,6.4 480,Lion,"A five-year-old Indian boy gets lost on the streets of Calcutta, thousands of kilometers from home. He survives many challenges before being adopted by a couple in Australia; 25 years later, he sets out to find his lost family.",en,4499,8.1 481,The Fate of the Furious,"When a mysterious woman seduces Dom into the world of crime and a betrayal of those closest to him, the crew face trials that will test them as never before.",en,7047,6.9 482,Playing with Love,"Laura and Fabrizio have been meeting every summer in the forest by her parent's summer home. Fabrizio is a solitary boy with only his dog for company; Laura a sweet but unconfident child. This summer new aspects enter into their story as both are growing up. Laura is falling in love with Fabrizio, while he displays a new sexual awareness of her masked by his malice. Things develop further when they meet Sylvia who, unlike the innocent Laura, is confident and assertive. Fabrizio develops a fascination with her, eventually bribing Laura to fetch her to the forest to join them in play.",it,35,5.3 483,The First Purge,"To push the crime rate below one percent for the rest of the year, the New Founding Fathers of America test a sociological theory that vents aggression for one night in one isolated community. But when the violence of oppressors meets the rage of the others, the contagion will explode from the trial-city borders and spread across the nation.",en,2255,5.8 484,The Amazing Spider-Man,"Peter Parker is an outcast high schooler abandoned by his parents as a boy, leaving him to be raised by his Uncle Ben and Aunt May. Like most teenagers, Peter is trying to figure out who he is and how he got to be the person he is today. As Peter discovers a mysterious briefcase that belonged to his father, he begins a quest to understand his parents' disappearance – leading him directly to Oscorp and the lab of Dr. Curt Connors, his father's former partner. As Spider-Man is set on a collision course with Connors' alter ego, The Lizard, Peter will make life-altering choices to use his powers and shape his destiny to become a hero.",en,11561,6.5 485,The Hunt,"A teacher lives a lonely life, all the while struggling over his son’s custody. His life slowly gets better as he finds love and receives good news from his son, but his new luck is about to be brutally shattered by an innocent little lie.",da,1899,8.1 486,From Russia with Love,"Agent 007 is back in the second installment of the James Bond series, this time battling a secret crime organization known as SPECTRE. Russians Rosa Klebb and Kronsteen are out to snatch a decoding device known as the Lektor, using the ravishing Tatiana to lure Bond into helping them. Bond willingly travels to meet Tatiana in Istanbul, where he must rely on his wits to escape with his life in a series of deadly encounters with the enemy",en,1573,7.1 487,V for Vendetta,"In a world in which Great Britain has become a fascist state, a masked vigilante known only as “V” conducts guerrilla warfare against the oppressive British government. When V rescues a young woman from the secret police, he finds in her an ally with whom he can continue his fight to free the people of Britain.",en,9336,7.9 488,The Cat Returns,"Haru, a schoolgirl bored by her ordinary routine, saves the life of an unusual cat and suddenly her world is transformed beyond anything she ever imagined. The Cat King rewards her good deed with a flurry of presents, including a very shocking proposal of marriage to his son! Haru embarks on an unexpected journey to the Kingdom of Cats where her eyes are opened to a whole other world.",ja,909,7.2 489,Total Recall,"Welcome to Rekall, the company that can turn your dreams into real memories. For a factory worker named Douglas Quaid, even though he's got a beautiful wife who he loves, the mind-trip sounds like the perfect vacation from his frustrating life - real memories of life as a super-spy might be just what he needs. But when the procedure goes horribly wrong, Quaid becomes a hunted man. Finding himself on the run from the police - controlled by Chancellor Cohaagen, the leader of the free world - Quaid teams up with a rebel fighter to find the head of the underground resistance and stop Cohaagen. The line between fantasy and reality gets blurred and the fate of his world hangs in the balance as Quaid discovers his true identity, his true love, and his true fate.",en,3867,5.9 490,Master and Commander: The Far Side of the World,"After an abrupt and violent encounter with a French warship inflicts severe damage upon his ship, a captain of the British Royal Navy begins a chase over two oceans to capture or destroy the enemy, though he must weigh his commitment to duty and ferocious pursuit of glory against the safety of his devoted crew, including the ship's thoughtful surgeon, his best friend.",en,1741,7.1 491,Hotel Transylvania 2,"When the old-old-old-fashioned vampire Vlad arrives at the hotel for an impromptu family get-together, Hotel Transylvania is in for a collision of supernatural old-school and modern day cool.",en,3637,6.7 492,X-Men: Apocalypse,"After the re-emergence of the world's first mutant, world-destroyer Apocalypse, the X-Men must unite to defeat his extinction level plan.",en,9237,6.5 493,Rocketman,"The story of Elton John's life, from his years as a prodigy at the Royal Academy of Music through his influential and enduring musical partnership with Bernie Taupin.",en,2026,7.4 494,The Little Mermaid,This colorful adventure tells the story of an impetuous mermaid princess named Ariel who falls in love with the very human Prince Eric and puts everything on the line for the chance to be with him. Memorable songs and characters -- including the villainous sea witch Ursula.,en,4923,7.3 495,Ultras,An aging soccer fanatic faces down the reality of his past while struggling to give himself and a young follower very different futures.,it,203,6.4 496,Baby Driver,"After being coerced into working for a crime boss, a young getaway driver finds himself taking part in a heist doomed to fail.",en,9616,7.4 497,Mulan,"A tomboyish girl disguises herself as a young man so she can fight with the Imperial Chinese Army against the invading Huns. With help from wise-cracking dragon Mushu, Mulan just might save her country -- and win the heart of handsome Captain Li Shang.",en,5689,7.9 498,Up,"Carl Fredricksen spent his entire life dreaming of exploring the globe and experiencing life to its fullest. But at age 78, life seems to have passed him by, until a twist of fate (and a persistent 8-year old Wilderness Explorer named Russell) gives him a new lease on life.",en,13941,7.9 499,Sexual Chronicles of a French Family,Three generations of a French family open up about their sexual experiences and desires after young Romain is caught masturbating in his biology class.,fr,177,4.5 500,Battleship,"When mankind beams a radio signal into space, a reply comes from ‘Planet G’, in the form of several alien crafts that splash down in the waters off Hawaii. Lieutenant Alex Hopper is a weapons officer assigned to the USS John Paul Jones, part of an international naval coalition which becomes the world's last hope for survival as they engage the hostile alien force of unimaginable strength. While taking on the invaders, Hopper must also try to live up to the potential that his brother, and his fiancée's father, Admiral Shane, expect of him.",en,3624,5.7 501,Mystic River,The lives of three men who were childhood friends are shattered when one of them has a family tragedy.,en,3735,7.7 502,Star Wars: Episode II - Attack of the Clones,"Following an assassination attempt on Senator Padmé Amidala, Jedi Knights Anakin Skywalker and Obi-Wan Kenobi investigate a mysterious plot that could change the galaxy forever.",en,8446,6.5 503,Ghostbusters,"After losing their academic posts at a prestigious university, a team of parapsychologists goes into business as proton-pack-toting ""ghostbusters"" who exterminate ghouls, hobgoblins and supernatural pests of all stripes. An ad campaign pays off when a knockout cellist hires the squad to purge her swanky digs of demons that appear to be living in her refrigerator.",en,5401,7.4 504,Ocean's Eleven,"Less than 24 hours into his parole, charismatic thief Danny Ocean is already rolling out his next plan: In one night, Danny's hand-picked crew of specialists will attempt to steal more than $150 million from three Las Vegas casinos. But to score the cash, Danny risks his chances of reconciling with ex-wife, Tess.",en,7578,7.4 505,The Informer,"In New York, former convict Pete Koslow, related to the Polish mafia, must deal with both Klimek the General, his ruthless boss, and the twisted ambitions of two federal agents, as he tries to survive and protect the lives of his loved ones…",en,137,6.4 506,Barbie as The Princess & the Pauper,"In her first animated musical featuring seven original songs, Barbie comes to life in this modern re-telling of a classic tale of mistaken identity and the power of friendship. Based on the story by Mark Twain,",en,733,7.4 507,Cinderella,"Cinderella has faith her dreams of a better life will come true. With help from her loyal mice friends and a wave of her Fairy Godmother's wand, Cinderella's rags are magically turned into a glorious gown and off she goes to the Royal Ball. But when the clock strikes midnight, the spell is broken, leaving a single glass slipper... the only key to the ultimate fairy-tale ending!",en,4308,7 508,Independence Day,"On July 2, a giant alien mothership enters orbit around Earth and deploys several dozen saucer-shaped 'destroyer' spacecraft that quickly lay waste to major cities around the planet. On July 3, the United States conducts a coordinated counterattack that fails. On July 4, a plan is devised to gain access to the interior of the alien mothership in space, in order to plant a nuclear missile.",en,6205,6.8 509,Death Wish,A mild-mannered father is transformed into a killing machine after his family is torn apart by a violent act.,en,1514,6 510,The Invasion,"Washington, D.C. psychologist Carol Bennell and her colleague Dr. Ben Driscoll are the only two people on Earth who are aware of an epidemic running rampant through the city. They discover an alien virus aboard a crashed space shuttle that transforms anyone who comes into contact with it into unfeeling drones while they sleep. Carol realizes her son holds the key to stopping the spread of the plague and she races to find him before it is too late.",en,817,5.9 511,Shark Tale,"Oscar is a small fish whose big aspirations often get him into trouble. Meanwhile, Lenny is a great white shark with a surprising secret that no sea creature would guess: He's a vegetarian. When a lie turns Oscar into an improbable hero and Lenny becomes an outcast, the two form an unlikely friendship.",en,4255,5.9 512,Superman: Red Son,"Set in the thick of the Cold War, Red Son introduces us to a Superman who landed in the USSR during the 1950s and grows up to become a Soviet symbol that fights for the preservation of Stalin’s brand of communism.",en,190,7.4 513,The Amazing Spider-Man 2,"For Peter Parker, life is busy. Between taking out the bad guys as Spider-Man and spending time with the person he loves, Gwen Stacy, high school graduation cannot come quickly enough. Peter has not forgotten about the promise he made to Gwen’s father to protect her by staying away, but that is a promise he cannot keep. Things will change for Peter when a new villain, Electro, emerges, an old friend, Harry Osborn, returns, and Peter uncovers new clues about his past.",en,8174,6.4 514,Escape from New York,"In 1997, the island of Manhattan has been walled off and turned into a giant maximum security prison within which the country's worst criminals are left to form their own anarchic society. However, when the President of the United States crash lands on the island, the authorities turn to a former soldier and current convict to rescue him.",en,1628,7.1 515,WALL·E,"WALL·E is the last robot left on an Earth that has been overrun with garbage and all humans have fled to outer space. For 700 years he has continued to try and clean up the mess, but has developed some rather interesting human-like qualities. When a ship arrives with a sleek new type of robot, WALL·E thinks he's finally found a friend and stows away on the ship when it leaves.",en,12341,8 516,Brightburn,"What if a child from another world crash-landed on Earth, but instead of becoming a hero to mankind, he proved to be something far more sinister?",en,1392,5.9 517,Wild Card,"When a Las Vegas bodyguard with lethal skills and a gambling problem gets in trouble with the mob, he has one last play… and it's all or nothing.",en,1081,5.5 518,The Equalizer 2,"Robert McCall, who serves an unflinching justice for the exploited and oppressed, embarks on a relentless, globe-trotting quest for vengeance when a long-time girl friend is murdered.",en,2630,6.5 519,Kill Bill: Vol. 1,"An assassin is shot by her ruthless employer, Bill, and other members of their assassination circle – but she lives to plot her vengeance.",en,11188,8 520,Star Trek Into Darkness,"When the crew of the Enterprise is called back home, they find an unstoppable force of terror from within their own organization has detonated the fleet and everything it stands for, leaving our world in a state of crisis. With a personal score to settle, Captain Kirk leads a manhunt to a war-zone world to capture a one man weapon of mass destruction. As our heroes are propelled into an epic chess game of life and death, love will be challenged, friendships will be torn apart, and sacrifices must be made for the only family Kirk has left: his crew.",en,6749,7.3 521,Escape Plan,"Ray Breslin is the world's foremost authority on structural security. After analyzing every high security prison and learning a vast array of survival skills so he can design escape-proof prisons, his skills are put to the test. He's framed and incarcerated in a master prison he designed himself. He needs to escape and find the person who put him behind bars.",en,3262,6.7 522,The Princess Diaries,"A socially awkward but very bright 15-year-old girl being raised by a single mom discovers that she is the princess of a small European country because of the recent death of her long-absent father, who, unknown to her, was the crown prince of Genovia. She must make a choice between continuing the life of a San Francisco teen or stepping up to the throne.",en,2997,6.8 523,Boyka: Undisputed IV,"In the fourth installment of the fighting franchise, Boyka is shooting for the big leagues when an accidental death in the ring makes him question everything he stands for. When he finds out the wife of the man he accidentally killed is in trouble, Boyka offers to fight in a series of impossible battles to free her from a life of servitude",en,575,6.4 524,Evan Almighty,"Junior congressman Evan Baxter, whose wish is to ""change the world"" is heard by none other than God. When God appears with the perplexing request to build an ark, Evan is sure he is losing it.",en,2783,5.5 525,Endless,"When madly in love high school graduates Riley (Alexandra Shipp) and Chris (Nicholas Hamilton) are separated by a tragic car accident, Riley blames herself for her boyfriend's death while Chris is stranded in limbo. Miraculously, the two find a way to connect. In a love story that transcends life and death, both Riley and Chris are forced to learn the hardest lesson of all: letting go.",en,0,0 526,G.I. Joe: Retaliation,"Framed for crimes against the country, the G.I. Joe team is terminated by Presidential order. This forces the G.I. Joes into not only fighting their mortal enemy Cobra; they are forced to contend with threats from within the government that jeopardize their very existence.",en,4158,5.5 527,Snatch,"The second film from British director Guy Ritchie. Snatch tells an obscure story similar to his first fast-paced crazy character-colliding filled film “Lock, Stock and Two Smoking Barrels.” There are two overlapping stories here – one is the search for a stolen diamond, and the other about a boxing promoter who’s having trouble with a psychotic gangster.",en,5497,7.8 528,The Commuter,"A businessman, on his daily commute home, gets unwittingly caught up in a criminal conspiracy that threatens not only his life but the lives of those around him.",en,2839,6.2 529,Eyes Wide Shut,"After Dr. Bill Harford's wife, Alice, admits to having sexual fantasies about a man she met, Bill becomes obsessed with having a sexual encounter. He discovers an underground sexual group and attends one of their meetings -- and quickly discovers that he is in over his head.",en,3267,7.4 530,The 420 Movie,"With grand ideas of leaving the broken city they grew up in, two sisters get roped into using their get rich idea to help their womanizing father save the city he loves from bankruptcy and a three foot tall Mexican Drug Lord.",en,0,0 531,Sherlock Holmes,"Eccentric consulting detective, Sherlock Holmes and Doctor John Watson battle to bring down a new nemesis and unravel a deadly plot that could destroy England.",en,10058,7.2 532,The Current War,Electricity titans Thomas Edison and George Westinghouse compete to create a sustainable system and market it to the American people,en,372,6.8 533,Away,"The story is about a boy traveling across an island on a motorcycle, trying to escape a dark spirit and get back home. Along the way he makes a series of connections with different animals and reflects on the possible ways he ended up on the island.",lv,6,8.7 534,Pan's Labyrinth,"Living with her tyrannical stepfather in a new home with her pregnant mother, 10-year-old Ofelia feels alone until she explores a decaying labyrinth guarded by a mysterious faun who claims to know her destiny. If she wishes to return to her real father, Ofelia must complete three terrifying tasks.",es,6918,7.7 535,Aliens,"When Ripley's lifepod is found by a salvage crew over 50 years later, she finds that terra-formers are on the very planet they found the alien species. When the company sends a family of colonists out to investigate her story—all contact is lost with the planet and colonists. They enlist Ripley and the colonial marines to return and search for answers.",en,5813,7.9 536,Nausicaä of the Valley of the Wind,"After a global war, the seaside kingdom known as the Valley of the Wind remains one of the last strongholds on Earth untouched by a poisonous jungle and the powerful insects that guard it. Led by the courageous Princess Nausicaä, the people of the Valley engage in an epic struggle to restore the bond between humanity and Earth.",ja,1766,7.9 537,Spider-Man 3,"The seemingly invincible Spider-Man goes up against an all-new crop of villains—including the shape-shifting Sandman. While Spider-Man’s superpowers are altered by an alien organism, his alter ego, Peter Parker, deals with nemesis Eddie Brock and also gets caught up in a love triangle.",en,8309,6.2 538,First Blood,"When former Green Beret John Rambo is harassed by local law enforcement and arrested for vagrancy, the Vietnam vet snaps, runs for the hills and rat-a-tat-tats his way into the action-movie hall of fame. Hounded by a relentless sheriff, Rambo employs heavy-handed guerilla tactics to shake the cops off his tail.",en,3392,7.4 539,Her,"In the not so distant future, Theodore, a lonely writer purchases a newly developed operating system designed to meet the user's every needs. To Theodore's surprise, a romantic relationship develops between him and his operating system. This unconventional love story blends science fiction and romance in a sweet tale that explores the nature of love and the ways that technology isolates and connects us all.",en,9338,7.9 540,The Empire Strikes Back,"The epic saga continues as Luke Skywalker, in hopes of defeating the evil Galactic Empire, learns the ways of the Jedi from aging master Yoda. But Darth Vader is more determined than ever to capture Luke. Meanwhile, rebel leader Princess Leia, cocky Han Solo, Chewbacca, and droids C-3PO and R2-D2 are thrown into various stages of capture, betrayal and despair.",en,11164,8.4 541,Dawn of the Planet of the Apes,"A group of scientists in San Francisco struggle to stay alive in the aftermath of a plague that is wiping out humanity, while Caesar tries to maintain dominance over his community of intelligent apes.",en,7829,7.3 542,The Predator,"When a kid accidentally triggers the universe's most lethal hunters' return to Earth, only a ragtag crew of ex-soldiers and a disgruntled female scientist can prevent the end of the human race.",en,2562,5.3 543,Never Rarely Sometimes Always,A pair of teenage girls in rural Pennsylvania travel to New York City to seek out medical help after an unintended pregnancy.,en,6,6.8 544,Zombieland,"Columbus has made a habit of running from what scares him. Tallahassee doesn't have fears. If he did, he'd kick their ever-living ass. In a world overrun by zombies, these two are perfectly evolved survivors. But now, they're about to stare down the most terrifying prospect of all: each other.",en,7992,7.2 545,Die Another Day,"Bond takes on a North Korean leader who undergoes DNA replacement procedures that allow him to assume different identities. American agent, Jinx Johnson assists Bond in his attempt to thwart the villain's plans to exploit a satellite that is powered by solar energy.",en,2113,5.9 546,Booksmart,"Two academic teenage superstars realize, on the eve of their high school graduation, that they should have worked less and played more. Determined to never fall short of their peers, the girls set out on a mission to cram four years of fun into one night.",en,1044,7.2 547,32 Malasana Street,"Inspired by real events, the story centers around a family in the 70s who settles in the Madrid neighborhood of Malasaña where their new house will become the worst of their nightmares.",es,28,5.9 548,It Follows,"After carefree teenager Jay sleeps with her new boyfriend, Hugh, for the first time, she learns that she is the latest recipient of a fatal curse that is passed from victim to victim via sexual intercourse. Death, Jay learns, will creep inexorably toward her as either a friend or a stranger. Jay's friends don't believe her seemingly paranoid ravings, until they too begin to see the phantom assassins and band together to help her flee or defend herself.",en,4013,6.5 549,A Beautiful Day in the Neighborhood,"An award-winning cynical journalist, Lloyd Vogel, begrudgingly accepts an assignment to write an Esquire profile piece on the beloved television icon Fred Rogers. After his encounter with Rogers, Vogel's perspective on life is transformed.",en,370,7.3 550,The Whistlers,"A Romanian police officer, determined to free from prison a crooked businessman who knows where a mobster's money is hidden, must learn the difficult ancestral whistling language (Silbo Gomero) used on the island of Gomera.",ro,23,6.4 551,Sin City: A Dame to Kill For,Some of Sin City's most hard-boiled citizens cross paths with a few of its more reviled inhabitants.,en,2522,6.3 552,The Da Vinci Code,"A murder in Paris’ Louvre Museum and cryptic clues in some of Leonardo da Vinci’s most famous paintings lead to the discovery of a religious mystery. For 2,000 years a secret society closely guards information that — should it come to light — could rock the very foundations of Christianity.",en,5952,6.7 553,Ilsa: She Wolf of the SS,Ilsa is a warden at a Nazi death camp that conducts experiments on prisoners. Ilsa's goal is to prove that woman can withstand more pain and suffering than men and should be allowed to fight on the front lines,en,87,5 554,Shooter,"A marksman living in exile is coaxed back into action after learning of a plot to kill the president. Ultimately double-crossed and framed for the attempt, he goes on the run to track the real killer and find out who exactly set him up, and why??",en,2728,7 555,Five Feet Apart,"Seventeen-year-old Stella spends most of her time in the hospital as a cystic fibrosis patient. Her life is full of routines, boundaries and self-control — all of which get put to the test when she meets Will, an impossibly charming teen who has the same illness. There's an instant flirtation, though restrictions dictate that they must maintain a safe distance between them. As their connection intensifies, so does the temptation to throw the rules out the window and embrace that attraction.",en,2044,8.1 556,Rocky,"When world heavyweight boxing champion, Apollo Creed wants to give an unknown fighter a shot at the title as a publicity stunt, his handlers choose palooka Rocky Balboa, an uneducated collector for a Philadelphia loan shark. Rocky teams up with trainer Mickey Goldmill to make the most of this once in a lifetime break.",en,4454,7.7 557,Beetlejuice,"Thanks to an untimely demise via drowning, a young couple end up as poltergeists in their New England farmhouse, where they fail to meet the challenge of scaring away the insufferable new owners, who want to make drastic changes. In desperation, the undead newlyweds turn to an expert frightmeister, but he's got a diabolical agenda of his own.",en,3672,7.3 558,Saw IV,"Jigsaw and his apprentice Amanda are dead. Now, upon the news of Detective Kerry's murder, two seasoned FBI profilers, Agent Strahm and Agent Perez, arrive in the terrified community to assist the veteran Detective Hoffman in sifting through Jigsaw's latest grisly remains and piecing together the puzzle. However, when SWAT Commander Rigg is abducted and thrust into a game, the last officer untouched by Jigsaw has but ninety minutes to overcome a series of demented traps and save an old friend...or face the deadly consequences.",en,2161,6.1 559,Steven Universe: The Movie,"Two years after the events of ""Change Your Mind"", Steven (now 16 years old) and his friends are ready to enjoy the rest of their lives peacefully. However, all of that changes when a new sinister Gem arrives, armed with a giant drill that saps the life force of all living things on Earth. In their biggest challenge ever, the Crystal Gems must work together to save all organic life on Earth within 48 hours.",en,144,8.8 560,The Addams Family,"The Addams family's lives begin to unravel when they face-off against a treacherous, greedy crafty reality-TV host while also preparing for their extended family to arrive for a major celebration.",en,730,6.3 561,Sicario,An idealistic FBI agent is enlisted by a government task force to aid in the escalating war against drugs at the border area between the U.S. and Mexico.,en,5100,7.3 562,Cast Away,"Chuck, a top international manager for FedEx, and Kelly, a Ph.D. student, are in love and heading towards marriage. Then Chuck's plane to Malaysia ditches at sea during a terrible storm. He's the only survivor, and he washes up on a tiny island with nothing but some flotsam and jetsam from the aircraft's cargo.",en,7073,7.6 563,Hanna,"A 16-year-old girl raised by her father to be the perfect assassin is dispatched on a mission across Europe. Tracked by a ruthless operatives, she faces startling revelations about her existence and questions about her humanity.",en,2166,6.6 564,Edge of Tomorrow,"Major Bill Cage is an officer who has never seen a day of combat when he is unceremoniously demoted and dropped into combat. Cage is killed within minutes, managing to take an alpha alien down with him. He awakens back at the beginning of the same day and is forced to fight and die again... and again - as physical contact with the alien has thrown him into a time loop.",en,9129,7.6 565,Top Gun,"For Lieutenant Pete 'Maverick' Mitchell and his friend and co-pilot Nick 'Goose' Bradshaw, being accepted into an elite training school for fighter pilots is a dream come true. But a tragedy, as well as personal demons, will threaten Pete's dreams of becoming an ace pilot.",en,3690,6.9 566,Blackhat,A man is released from prison to help American and Chinese authorities pursue a mysterious cyber criminal. The dangerous search leads them from Chicago to Hong Kong.,en,1277,5.3 567,Goosebumps,"After moving to a small town, Zach Cooper finds a silver lining when he meets next door neighbor Hannah, the daughter of bestselling Goosebumps series author R.L. Stine. When Zach unintentionally unleashes real monsters from their manuscripts and they begin to terrorize the town, it’s suddenly up to Stine, Zach and Hannah to get all of them back in the books where they belong.",en,2509,6.2 568,Miss Bala,"Gloria finds a power she never knew she had when she is drawn into a dangerous world of cross-border crime. Surviving will require all of her cunning, inventiveness, and strength.",en,175,6.2 569,The Captive,"Eight years after the disappearance of Cassandra, some disturbing incidents seem to indicate that she's still alive. Police, parents and Cassandra herself, will try to unravel the mystery of her disappearance.",en,689,5.9 570,The Greatest Showman,"The story of American showman P.T. Barnum, founder of the circus that became the famous traveling Ringling Bros. and Barnum & Bailey Circus.",en,6091,8 571,Let's Be Cops,"It's the ultimate buddy cop movie except for one thing: they're not cops. When two struggling pals dress as police officers for a costume party, they become neighborhood sensations. But when these newly-minted “heroes” get tangled in a real life web of mobsters and dirty detectives, they must put their fake badges on the line.",en,1864,6.3 572,Frankenstein,Dr Henry Frankenstein is obsessed with assembling a living being from parts of several exhumed corpses.,en,746,7.5 573,Tarzan,"Tarzan was a small orphan who was raised by an ape named Kala since he was a child. He believed that this was his family, but on an expedition Jane Porter is rescued by Tarzan. He then finds out that he's human. Now Tarzan must make the decision as to which family he should belong to...",en,4292,7.4 574,Lost Girls,"When Mari Gilbert's daughter disappears, police inaction drives her own investigation into the gated Long Island community where Shannan was last seen. Her search brings attention to over a dozen murdered sex workers.",en,216,6.2 575,Alien³,"After escaping with Newt and Hicks from the alien planet, Ripley crash lands on Fiorina 161, a prison planet and host to a correctional facility. Unfortunately, although Newt and Hicks do not survive the crash, a more unwelcome visitor does. The prison does not allow weapons of any kind, and with aid being a long time away, the prisoners must simply survive in any way they can.",en,3241,6.3 576,"The Chronicles of Narnia: The Lion, the Witch and the Wardrobe","Siblings Lucy, Edmund, Susan and Peter step through a magical wardrobe and find the land of Narnia. There, the they discover a charming, once peaceful kingdom that has been plunged into eternal winter by the evil White Witch, Jadis. Aided by the wise and magnificent lion, Aslan, the children lead Narnia into a spectacular, climactic battle to be free of the Witch's glacial powers forever.",en,6708,7.1 577,Pride & Prejudice,"A story of love and life among the landed English gentry during the Georgian era. Mr. Bennet is a gentleman living in Hertfordshire with his overbearing wife and five daughters, but if he dies their house will be inherited by a distant cousin whom they have never met, so the family's future happiness and security is dependent on the daughters making good marriages.",en,4022,7.9 578,One Flew Over the Cuckoo's Nest,"While serving time for insanity at a state mental hospital, implacable rabble-rouser, Randle Patrick McMurphy, inspires his fellow patients to rebel against the authoritarian rule of head nurse, Mildred Ratched.",en,6442,8.4 579,The Dividing Wall,"A 50's Chinese film that reflects the post-war society, expresses idealism and pleads to return to the homeland.",zh,0,0 580,Seventh Son,"John Gregory, who is a seventh son of a seventh son and also the local spook, has protected the country from witches, boggarts, ghouls and all manner of things that go bump in the night. However John is not young anymore, and has been seeking an apprentice to carry on his trade. Most have failed to survive. The last hope is a young farmer's son named Thomas Ward. Will he survive the training to become the spook that so many others couldn't?",en,1721,5.3 581,Sully,"On 15 January 2009, the world witnessed the 'Miracle on the Hudson' when Captain 'Sully' Sullenberger glided his disabled plane onto the frigid waters of the Hudson River, saving the lives of all 155 aboard. However, even as Sully was being heralded by the public and the media for his unprecedented feat of aviation skill, an investigation was unfolding that threatened to destroy his reputation and career.",en,4569,7.1 582,The Hustle,"Two female scam artists, one low rent and the other high class, compete to swindle a naïve tech prodigy out of his fortune. A remake of the 1988 comedy ""Dirty Rotten Scoundrels.""",en,1115,6.1 583,A Cinderella Story: If the Shoe Fits,A contemporary musical version of the classic Cinderella story in which the servant step daughter hope to compete in a musical competition for a famous pop star.,en,445,6.8 584,Indiana Jones and the Kingdom of the Crystal Skull,"Set during the Cold War, the Soviets – led by sword-wielding Irina Spalko – are in search of a crystal skull which has supernatural powers related to a mystical Lost City of Gold. After being captured and then escaping from them, Indy is coerced to head to Peru at the behest of a young man whose friend – and Indy's colleague – Professor Oxley has been captured for his knowledge of the skull's whereabouts.",en,5165,5.9 585,Licence to Kill,"When drug lord Franz Sanchez exacts his brutal vengeance on Bond's friend Felix Leiter, 007 resigns from the British Secret Service and begins a fierce vendetta against the master criminal. Bond won't be satisfied until Sanchez is defeated, and to accomplish this aim he allies himself with a beautiful pilot and Sanchez's sexy girlfriend.",en,1043,6.2 586,American Beauty,"Lester Burnham, a depressed suburban father in a mid-life crisis, decides to turn his hectic life around after developing an infatuation with his daughter's attractive friend.",en,7666,8 587,Jaws,"When an insatiable great white shark terrorizes the townspeople of Amity Island, the police chief, an oceanographer and a grizzled shark hunter seek to destroy the blood-thirsty beast.",en,5970,7.6 588,For Your Eyes Only,A British spy ship has sunk and on board was a hi-tech encryption device. James Bond is sent to find the device that holds British launching instructions before the enemy Soviets get to it first.,en,943,6.5 589,Sausage Party,Frank leads a group of supermarket products on a quest to discover the truth about their existence and what really happens when they become chosen to leave the grocery store.,en,4876,5.6 590,Indiana Jones and the Last Crusade,"When Dr. Henry Jones Sr. suddenly goes missing while pursuing the Holy Grail, eminent archaeologist Indiana must team up with Marcus Brody, Sallah and Elsa Schneider to follow in his father's footsteps and stop the Nazis from recovering the power of eternal life.",en,6201,7.8 591,Full Metal Jacket,A pragmatic U.S. Marine observes the dehumanizing effects the U.S.-Vietnam War has on his fellow recruits from their brutal boot camp training to the bloody street fighting in Hue.,en,6208,8.1 592,A Clockwork Orange,"In a near-future Britain, young Alexander DeLarge and his pals get their kicks beating and raping anyone they please. When not destroying the lives of others, Alex swoons to the music of Beethoven. The state, eager to crack down on juvenile crime, gives an incarcerated Alex the option to undergo an invasive procedure that'll rob him of all personal agency. In a time when conscience is a commodity, can Alex change his tune?",en,8012,8.2 593,Kajillionaire,A woman's life is turned upside down when her criminal parents invite an outsider to join them on a major heist they're planning.,en,0,0 594,Wreck-It Ralph,"Wreck-It Ralph is the 9-foot-tall, 643-pound villain of an arcade video game named Fix-It Felix Jr., in which the game's titular hero fixes buildings that Ralph destroys. Wanting to prove he can be a good guy and not just a villain, Ralph escapes his game and lands in Hero's Duty, a first-person shooter where he helps the game's hero battle against alien invaders. He later enters Sugar Rush, a kart racing game set on tracks made of candies, cookies and other sweets. There, Ralph meets Vanellope von Schweetz who has learned that her game is faced with a dire threat that could affect the entire arcade, and one that Ralph may have inadvertently started.",en,8732,7.3 595,Penguins of Madagascar,"Skipper, Kowalski, Rico and Private join forces with undercover organization The North Wind to stop the villainous Dr. Octavius Brine from destroying the world as we know it.",en,2570,6.4 596,Black and Blue,"A fast-paced action thriller about a rookie cop who inadvertently captures the murder of a young drug dealer on her body cam. After realizing that the murder was committed by corrupt cops, she teams up with the one person from her community who is willing to help her as she tries to escape both the criminals out for revenge and the police who are desperate to destroy the incriminating footage.",en,249,6.4 597,The Lorax,"A 12-year-old boy searches for the one thing that will enable him to win the affection of the girl of his dreams. To find it he must discover the story of the Lorax, the grumpy yet charming creature who fights to protect his world.",en,2106,6.4 598,Dragonfly,A grieving doctor is being contacted by his late wife through his patients near death experiences.,en,469,6.4 599,Trainwreck,"Having thought that monogamy was never possible, a commitment-phobic career woman may have to face her fears when she meets a good guy.",en,1904,5.8 600,Elephant,"Disneynature’s Elephant follows African elephant Shani and her spirited son Jomo as their herd make an epic journey hundreds of miles across the vast Kalahari Desert. Led by their great matriarch, Gaia, the family faces brutal heat, dwindling resources and persistent predators, as they follow in their ancestors’ footsteps on a quest to reach a lush, green paradise.",en,3,6.3 601,Black Widow,"Natasha Romanoff, also known as Black Widow, confronts the darker parts of her ledger when a dangerous conspiracy with ties to her past arises. Pursued by a force that will stop at nothing to bring her down, Natasha must deal with her history as a spy and the broken relationships left in her wake long before she became an Avenger.",en,0,0 602,Now You See Me,An FBI agent and an Interpol detective track a team of illusionists who pull off bank heists during their performances and reward their audiences with the money.,en,11105,7.4 603,The Salt of Tears,"Luc travels to Paris for the first time to sit the entrance exam for a carpentry school. There he meets Djemila, a young worker with whom he enjoys a short romance, before returning to his home town and beginning a relationship with Geneviève, whom he has known since childhood. Caught between two passions, Luc runs, resolving to fulfil his father's dreams by devoting himself to his future... until finally, he experiences true love.",fr,0,0 604,Hotel Transylvania 3: Summer Vacation,"Dracula, Mavis, Johnny and the rest of the Drac Pack take a vacation on a luxury Monster Cruise Ship, where Dracula falls in love with the ship’s captain, Ericka, who’s secretly a descendant of Abraham Van Helsing, the notorious monster slayer.",en,2109,6.8 605,Gadar: Ek Prem Katha,"Amongst the communal riots that erupt in the city, Tara shelters a wayward Sakina from a crazed mob and a bond that blossoms into love is created. The two eventually get married and have a son. The happy family, now living in Amritsar, gets the shock of their lives when Sakina learns that her father (Amrish Puri), whom she previously believed died in the riots back in Amritsar, is still alive after seeing his picture in a tattered, old newspaper. Upon contacting him, Sakina's father, now the mayor of Lahore in Pakistan, arranges for his daughter to arrive in Lahore to see him. Sakina leaves for Lahore minus Tara and her son, and upon reaching the city, learns of her father's plans for her - plans that include forcing Sakina to forget about her family and start life anew in Pakistan. Then begins an extraordinary journey which will lead Tara to cross the border into Pakistan to find his love Sakina",hi,20,6.8 606,Valerian and the City of a Thousand Planets,"In the 28th century, Valerian and Laureline are special operatives charged with keeping order throughout the human territories. On assignment from the Minister of Defense, the two undertake a mission to Alpha, an ever-expanding metropolis where species from across the universe have converged over centuries to share knowledge, intelligence, and cultures. At the center of Alpha is a mysterious dark force which threatens the peaceful existence of the City of a Thousand Planets, and Valerian and Laureline must race to identify the menace and safeguard not just Alpha, but the future of the universe.",en,4800,6.5 607,Ice Princess,"With the help of her coach, her parents, and the boy who drives the Zamboni machine, nothing can stop Casey from realizing her dream to be a champion figure skater.",en,581,6.1 608,Love The Way You Are,"Opposites clash when spunky girl next door Lin Lin meets eccentric nerd Yuke. Despite being neighbors and schoolmates since childhood, Lin Lin and Yuke barely know each other. When the pair are both admitted into the same university, Lin Lin discovers that Yuke harbors a secret crush for campus beauty, Ruting. Ever the busybody, Lin Lin decides to matchmake Yuke and Ruting, only to find herself gradually falling for Yuke.",zh,5,5.2 609,12 Angry Men,"The defense and the prosecution have rested and the jury is filing into the jury room to decide if a young Spanish-American is guilty or innocent of murdering his father. What begins as an open and shut case soon becomes a mini-drama of each of the jurors' prejudices and preconceptions about the trial, the accused, and each other.",en,4486,8.4 610,Showgirls,A young drifter named Nomi arrives in Las Vegas to become a dancer and soon sets about clawing and pushing her way to become a top showgirl.,en,563,5.2 611,Superintelligence,Carol Peters' life is turned upside down when she is selected for observation by the world's first superintelligence - a form of artificial intelligence that may or may not take over the world.,en,0,0 612,Abou Leila,"Algeria, 1994. S. and Lotfi, two friends from childhood, travel through the desert looking for Abou Leila, a dangerous terrorist on the run. Their quest seems absurd, given that the Sahara has not been affected by the wave of attacks. Lofti has only one priority : to keep S. as far from the capital as possible, knowing his friend is too fragile to face more bloodshed. But the closest they get into the desert, the more they will be confronted with their own violence.",ar,0,0 613,Psycho,"When larcenous real estate clerk Marion Crane goes on the lam with a wad of cash and hopes of starting a new life, she ends up at the notorious Bates Motel, where manager Norman Bates cares for his housebound mother. The place seems quirky, but fine… until Marion decides to take a shower.",en,5971,8.4 614,The Man with the Golden Gun,"Cool government operative James Bond searches for a stolen invention that can turn the sun's heat into a destructive weapon. He soon crosses paths with the menacing Francisco Scaramanga, a hit man so skilled he has a seven-figure working fee. Bond then joins forces with the swimsuit-clad Mary Goodnight, and together they track Scaramanga to a tropical isle hideout where the killer-for-hire lures the slick spy into a deadly maze for a final duel.",en,1043,6.5 615,Kubo and the Two Strings,"Kubo mesmerizes the people in his village with his magical gift for spinning wild tales with origami. When he accidentally summons an evil spirit seeking vengeance, Kubo is forced to go on a quest to solve the mystery of his fallen samurai father and his mystical weaponry, as well as discover his own magical powers.",en,2370,7.7 616,Tomorrow Never Dies,A deranged media mogul is staging international incidents to pit the world’s superpowers against each other. Now 007 must take on this evil mastermind in an adrenaline-charged battle to end his reign of terror and prevent global pandemonium.,en,1791,6.3 617,National Treasure,"Modern treasure hunters, led by archaeologist Ben Gates, search for a chest of riches rumored to have been stashed away by George Washington, Thomas Jefferson and Benjamin Franklin during the Revolutionary War. The chest's whereabouts may lie in secret clues embedded in the Constitution and the Declaration of Independence, and Gates is in a race to find the gold before his enemies do.",en,4077,6.5 618,Wonder,"The story of August Pullman – a boy with facial differences – who enters fifth grade, attending a mainstream elementary school for the first time.",en,4839,8.2 619,The Accountant,"As a math savant uncooks the books for a new client, the Treasury Department closes in on his activities and the body count starts to rise.",en,3812,7 620,The Gods Must Be Crazy,A Coca-Cola bottle dropped from an airplane raises havoc among a normally peaceful tribe of African bushmen who believe it to be a utensil of the gods.,en,540,7.1 621,Slumdog Millionaire,"Jamal Malik is an impoverished Indian teen who becomes a contestant on the Hindi version of ‘Who Wants to Be a Millionaire?’ but, after he wins, he is suspected of cheating.",en,6937,7.7 622,One Hundred and One Dalmatians,"When a litter of dalmatian puppies are abducted by the minions of Cruella De Vil, the parents must find them before she uses them for a diabolical fashion statement.",en,4022,7.1 623,Troy,"In year 1250 B.C. during the late Bronze age, two emerging nations begin to clash. Paris, the Trojan prince, convinces Helen, Queen of Sparta, to leave her husband Menelaus, and sail with him back to Troy. After Menelaus finds out that his wife was taken by the Trojans, he asks his brother Agamemnom to help him get her back. Agamemnon sees this as an opportunity for power. So they set off with 1,000 ships holding 50,000 Greeks to Troy. With the help of Achilles, the Greeks are able to fight the never before defeated Trojans.",en,6350,7 624,The Bourne Identity,"Wounded to the brink of death and suffering from amnesia, Jason Bourne is rescued at sea by a fisherman. With nothing to go on but a Swiss bank account number, he starts to reconstruct his life, but finds that many people he encounters want him dead. However, Bourne realizes that he has the combat and mental skills of a world-class spy—but who does he work for?",en,6072,7.4 625,Les Misérables,"Inspired by the 2005 riots in Paris, Stéphane, a recent transplant to the impoverished suburb of Montfermeil, joins the local anti-crime squad. Working alongside his unscrupulous colleagues Chris and Gwada, Stéphane struggles to maintain order amidst the mounting tensions between local gangs. When an arrest turns unexpectedly violent, the three officers must reckon with the aftermath and keep the neighborhood from spiraling out of control.",fr,394,8 626,Scooby-Doo! and the Monster of Mexico,"A friend of Fred's, Alejo Otero, invites the Scooby gang to Veracruz, Mexico. There they find a monster, El Chupacabra, terrorizing the town.",en,104,6.5 627,No Country for Old Men,"Llewelyn Moss stumbles upon dead bodies, $2 million and a hoard of heroin in a Texas desert, but methodical killer Anton Chigurh comes looking for it, with local sheriff Ed Tom Bell hot on his trail. The roles of prey and predator blur as the violent pursuit of money and justice collide.",en,6954,7.9 628,First Man,"A look at the life of the astronaut, Neil Armstrong, and the legendary space mission that led him to become the first man to walk on the Moon on July 20, 1969.",en,3195,7.1 629,Batman,"The Dark Knight of Gotham City begins his war on crime with his first major enemy being the clownishly homicidal Joker, who has seized control of Gotham's underworld.",en,4558,7.2 630,Shit & Champagne,"SF stripper Champagne White (multi-hyphenate talent D’Arcy Drollinger) is expecting her boyfriend Rod to pop the question; when he instead tells her of a secret plot involving booty bumps and the world’s largest retail store, it’s up to the intrepid ecdysiast to save the day. With a lo-fi ’70s exploitation film aesthetic and a John Waters-esque sense of the absurd and outrageous, Drollinger adapts his wildly successful stage show for the big screen, featuring extraordinary talent from the world of drag, including Alaska 5000 from RuPaul’s Drag Race.",en,0,0 631,Ace Ventura: Pet Detective,"He's Ace Ventura: Pet Detective. Jim Carrey is on the case to find the Miami Dolphins' missing mascot and quarterback Dan Marino. He goes eyeball to eyeball with a man-eating shark, stakes out the Miami Dolphins and woos and wows the ladies. Whether he's undercover, under fire or underwater, he always gets his man… or beast!",en,3596,6.5 632,E.T. the Extra-Terrestrial,"After a gentle alien becomes stranded on Earth, the being is discovered and befriended by a young boy named Elliott. Bringing the extraterrestrial into his suburban California house, Elliott introduces E.T., as the alien is dubbed, to his brother and his little sister, Gertie, and the children decide to keep its existence a secret. Soon, however, E.T. falls ill, resulting in government intervention and a dire situation for both Elliott and the alien.",en,7449,7.5 633,Star Trek Beyond,"The USS Enterprise crew explores the furthest reaches of uncharted space, where they encounter a mysterious new enemy who puts them and everything the Federation stands for to the test.",en,4696,6.7 634,Kiki's Delivery Service,"A young witch, on her mandatory year of independent life, finds fitting into a new community difficult while she supports herself by running an air courier service.",ja,1933,7.8 635,Ghost in the Shell,"In the near future, Major (Scarlett Johansson) is the first of her kind: A human saved from a terrible crash, who is cyber-enhanced to be a perfect soldier devoted to stopping the world's most dangerous criminals. When terrorism reaches a new level that includes the ability to hack into people's minds and control them, Major is uniquely qualified to stop it. As she prepares to face a new enemy, Major discovers that she has been lied to: her life was not saved, it was stolen. She will stop at nothing to recover her past, find out who did this to her and stop them before they do it to others. Based on the internationally acclaimed Japanese Manga, ""The Ghost in the Shell.""",en,5741,6 636,War for the Planet of the Apes,"Caesar and his apes are forced into a deadly conflict with an army of humans led by a ruthless Colonel. After the apes suffer unimaginable losses, Caesar wrestles with his darker instincts and begins his own mythic quest to avenge his kind. As the journey finally brings them face to face, Caesar and the Colonel are pitted against each other in an epic battle that will determine the fate of both their species and the future of the planet.",en,5859,7.1 637,Ted,"John Bennett, a man whose childhood wish of bringing his teddy bear to life came true, now must decide between keeping the relationship with the bear or his girlfriend, Lori.",en,8600,6.3 638,A Beautiful Mind,"John Nash is a brilliant but asocial mathematician fighting schizophrenia. After he accepts secret work in cryptography, his life takes a turn for the nightmarish.",en,6383,7.8 639,G.I. Joe: The Rise of Cobra,"From the Egyptian desert to deep below the polar ice caps, the elite G.I. JOE team uses the latest in next-generation spy and military equipment to fight the corrupt arms dealer Destro and the growing threat of the mysterious Cobra organization to prevent them from plunging the world into chaos.",en,3155,5.7 640,The Princess Diaries 2: Royal Engagement,"Mia Thermopolis is now a college graduate and on her way to Genovia to take up her duties as princess. Her best friend Lilly also joins her for the summer. Mia continues her 'princess lessons'- riding horses side-saddle, archery, and other royal. But her complicated life is turned upside down once again when she not only learns that she is to take the crown as queen earlier than expected...",en,2080,6.5 641,Mad Max 2,"Max Rockatansky returns as the heroic loner who drives the dusty roads of a postapocalyptic Australian Outback in an unending search for gasoline. Arrayed against him and the other scraggly defendants of a fuel-depot encampment are the bizarre warriors commanded by the charismatic Lord Humungus, a violent leader whose scruples are as barren as the surrounding landscape.",en,1983,7.4 642,The Postcard Killings,"A New York detective teams investigates the death of his daughter who was murdered while on her honeymoon in London, and recruits the help of Scandinavian journalist when other couples throughout Europe suffer a similar fate.",en,19,6.3 643,Hulk,"Bruce Banner, a genetics researcher with a tragic past, suffers massive radiation exposure in his laboratory that causes him to transform into a raging green monster when he gets angry.",en,3677,5.4 644,Ustica: The Missing Paper,,it,26,5.6 645,Once Upon a Time in America,"A former Prohibition-era Jewish gangster returns to the Lower East Side of Manhattan over thirty years later, where he once again must confront the ghosts and regrets of his old life.",en,2774,8.4 646,Atomic Blonde,An undercover MI6 agent is sent to Berlin during the Cold War to investigate the murder of a fellow agent and recover a missing list of double agents.,en,4221,6.3 647,Warcraft,"The peaceful realm of Azeroth stands on the brink of war as its civilization faces a fearsome race of invaders: orc warriors fleeing their dying home to colonize another. As a portal opens to connect the two worlds, one army faces destruction and the other faces extinction. From opposing sides, two heroes are set on a collision course that will decide the fate of their family, their people, and their home.",en,4606,6.3 648,Ex Machina,"Caleb, a 26 year old coder at the world's largest internet company, wins a competition to spend a week at a private mountain retreat belonging to Nathan, the reclusive CEO of the company. But when Caleb arrives at the remote location he finds that he will have to participate in a strange and fascinating experiment in which he must interact with the world's first true artificial intelligence, housed in the body of a beautiful robot girl.",en,8999,7.6 649,Mission: Impossible - Rogue Nation,"Ethan and team take on their most impossible mission yet—eradicating 'The Syndicate', an International and highly-skilled rogue organisation committed to destroying the IMF.",en,5835,7.1 650,Serenity,"The quiet life of Baker Dill, a fishing boat captain who lives on the isolated Plymouth Island, where he spends his days obsessed with capturing an elusive tuna while fighting his personal demons, is interrupted when someone from his past comes to him searching for help.",en,822,5.3 651,The Deer Hunter,"A group of working-class friends decides to enlist in the Army during the Vietnam War and finds it to be hellish chaos -- not the noble venture they imagined. Before they left, Steven married his pregnant girlfriend -- and Michael and Nick were in love with the same woman. But all three are different men upon their return.",en,2075,8 652,Toy Story 2,"Andy heads off to Cowboy Camp, leaving his toys to their own devices. Things shift into high gear when an obsessive toy collector named Al McWhiggen, owner of Al's Toy Barn kidnaps Woody. Andy's toys mount a daring rescue mission, Buzz Lightyear meets his match and Woody has to decide where he and his heart truly belong.",en,8974,7.5 653,Triple Threat,"A crime syndicate places a hit on a billionaire's daughter, making her the target of an elite assassin squad. A small band of down-and-out mercenaries protects her, fighting tooth and nail to stop the assassins from reaching their target.",en,242,6.2 654,Pitch Perfect 2,"The Bellas are back, and they are better than ever. After being humiliated in front of none other than the President of the United States of America, the Bellas are taken out of the Aca-Circuit. In order to clear their name, and regain their status, the Bellas take on a seemingly impossible task: winning an international competition no American team has ever won. In order to accomplish this monumental task, they need to strengthen the bonds of friendship and sisterhood and blow away the competition with their amazing aca-magic! With all new friends and old rivals tagging along for the trip, the Bellas can hopefully accomplish their dreams.",en,3993,6.9 655,A Bug's Life,"On behalf of ""oppressed bugs everywhere,"" an inventive ant named Flik hires a troupe of warrior bugs to defend his bustling colony from a horde of freeloading grasshoppers led by the evil-minded Hopper.",en,5802,6.9 656,A Tale of Two Sisters,"A recently released patient from a mental institution returns home with her sister, only to face disturbing events between her stepmother and the ghosts haunting their house- all of which are connected to a dark past in the family's history.",ko,528,7.1 657,Jexi,"Phil's new phone comes with an unexpected feature, Jexi...an A.I. determined to keep him all to herself in a comedy about what can happen when you love your phone more than all else.",en,283,7.1 658,The Hunger Games: Catching Fire,"Katniss Everdeen has returned home safe after winning the 74th Annual Hunger Games along with fellow tribute Peeta Mellark. Winning means that they must turn around and leave their family and close friends, embarking on a ""Victor's Tour"" of the districts. Along the way Katniss senses that a rebellion is simmering, but the Capitol is still very much in control as President Snow prepares the 75th Annual Hunger Games (The Quarter Quell) - a competition that could change Panem forever.",en,12366,7.4 659,21 Bridges,"An embattled NYPD detective, is thrust into a citywide manhunt for a pair of cop killers after uncovering a massive and unexpected conspiracy. As the night unfolds, lines become blurred on who he is pursuing, and who is in pursuit of him.",en,481,6.6 660,28 Days Later,"Twenty-eight days after a killer virus was accidentally unleashed from a British research facility, a small group of London survivors are caught in a desperate struggle to protect themselves from the infected. Carried by animals and humans, the virus turns those it infects into homicidal maniacs -- and it's absolutely impossible to contain.",en,3851,7.2 661,Twelve Monkeys,"In the year 2035, convict James Cole reluctantly volunteers to be sent back in time to discover the origin of a deadly virus that wiped out nearly all of the earth's population and forced the survivors into underground communities. But when Cole is mistakenly sent to 1990 instead of 1996, he's arrested and locked up in a mental hospital. There he meets psychiatrist Dr. Kathryn Railly, and patient Jeffrey Goines, the son of a famous virus expert, who may hold the key to the mysterious rogue group, the Army of the 12 Monkeys, thought to be responsible for unleashing the killer disease.",en,4997,7.6 662,Now You See Me 2,"One year after outwitting the FBI and winning the public’s adulation with their mind-bending spectacles, the Four Horsemen resurface only to find themselves face to face with a new enemy who enlists them to pull off their most dangerous heist yet.",en,7653,6.8 663,Hellboy,"Hellboy comes to England, where he must defeat Nimue, Merlin's consort and the Blood Queen. But their battle will bring about the end of the world, a fate he desperately tries to turn away.",en,1697,5.3 664,Four Kids and It,A group of kids on holiday in Cornwall meet a magical creature on the beach with the power to grant wishes.,en,1,10 665,What Happened to Monday,"In a world where families are limited to one child due to overpopulation, a set of identical septuplets must avoid being put to a long sleep by the government and dangerous infighting while investigating the disappearance of one of their own.",en,3998,7.2 666,Skyscraper,"Framed and on the run, a former FBI agent must save his family from a blazing fire in the world's tallest building.",en,2826,6.2 667,Mr. & Mrs. Smith,"After five (or six) years of vanilla-wedded bliss, ordinary suburbanites John and Jane Smith are stuck in a huge rut. Unbeknownst to each other, they are both coolly lethal, highly-paid assassins working for rival organisations. When they discover they're each other's next target, their secret lives collide in a spicy, explosive mix of wicked comedy, pent-up passion, nonstop action and high-tech weaponry.",en,6602,6.6 668,Annabelle Comes Home,"Determined to keep Annabelle from wreaking more havoc, demonologists Ed and Lorraine Warren bring the possessed doll to the locked artifacts room in their home, placing her “safely” behind sacred glass and enlisting a priest’s holy blessing. But an unholy night of horror awaits as Annabelle awakens the evil spirits in the room, who all set their sights on a new target—the Warrens' ten-year-old daughter, Judy, and her friends.",en,1401,6.2 669,Mulan,"When the Emperor of China issues a decree that one man per family must serve in the Imperial Chinese Army to defend the country from Huns, Hua Mulan, the eldest daughter of an honored warrior, steps in to take the place of her ailing father. She is spirited, determined and quick on her feet. Disguised as a man by the name of Hua Jun, she is tested every step of the way and must harness her innermost strength and embrace her true potential.",en,0,0 670,Predator 2,"Ten years after a band of mercenaries first battled a vicious alien, the invisible creature from another world has returned to Earth—and this time, it’s drawn to the gang-ruled and ravaged city of Los Angeles. When it starts murdering drug dealers, detective-lieutenant Mike Harrigan and his police force set out to capture the creature, ignoring warnings from a mysterious government agent to stay away.",en,1586,6 671,Mission: Impossible,"When Ethan Hunt, the leader of a crack espionage team whose perilous operation has gone awry with no explanation, discovers that a mole has penetrated the CIA, he's surprised to learn that he's the No. 1 suspect. To clear his name, Hunt now must ferret out the real double agent and, in the process, even the score.",en,5387,6.9 672,Jack Reacher: Never Go Back,"Jack Reacher must uncover the truth behind a major government conspiracy in order to clear his name. On the run as a fugitive from the law, Reacher uncovers a potential secret from his past that could change his life forever.",en,3096,5.7 673,Madagascar,"Alex the lion is the king of the urban jungle, the main attraction at New York’s Central Park Zoo. He and his best friends—Marty the zebra, Melman the giraffe and Gloria the hippo—have spent their whole lives in blissful captivity before an admiring public and with regular meals provided for them. Not content to leave well enough alone, Marty lets his curiosity get the better of him and makes his escape—with the help of some prodigious penguins—to explore the world.",en,7149,6.8 674,Moonlight,"The tender, heartbreaking story of a young man’s struggle to find himself, told across three defining chapters in his life as he experiences the ecstasy, pain, and beauty of falling in love, while grappling with his own sexuality.",en,4678,7.4 675,The Big Short,The men who made millions from a global economic meltdown.,en,5506,7.3 676,Altered Carbon: Resleeved,"On the planet Latimer, Takeshi Kovacs must protect a tattooist while investigating the death of a yakuza boss alongside a no-nonsense CTAC.",ja,76,6.3 677,Under The Stars Of Paris,"Christine’s life has not been easy lately. Her lonely routine is divided between free food banks distributions and wandering the streets. On a cold winter night she founds Suli, an 8-year-old Eritrean boy, sobbing in front of her shelter. Christine understands that he is lost and has been separated from his mother. Bounded by their marginal condition, they embark together on an emotional journey to find Suli’s mother in the underground world of Paris...",fr,0,0 678,Die Hard 2,"Off-duty cop John McClane is gripped with a feeling of déjà vu when, on a snowy Christmas Eve in the nation’s capital, terrorists seize a major international airport, holding thousands of holiday travelers hostage. Renegade military commandos led by a murderous rogue officer plot to rescue a drug lord from justice and are prepared for every contingency except one: McClane’s smart-mouthed heroics.",en,3401,6.8 679,Charlie and the Chocolate Factory,"A young boy wins a tour through the most magnificent chocolate factory in the world, led by the world's most unusual candy maker.",en,9580,7 680,Den of Thieves,A gritty crime saga which follows the lives of an elite unit of the LA County Sheriff's Dept. and the state's most successful bank robbery crew as the outlaws plan a seemingly impossible heist on the Federal Reserve Bank.,en,1568,6.6 681,Dogtooth,"Three teenagers are confined to an isolated country estate that could very well be on another planet. The trio spend their days listening to endless homemade tapes that teach them a whole new vocabulary. Any word that comes from beyond their family abode is instantly assigned a new meaning. Hence 'the sea' refers to a large armchair and 'zombies' are little yellow flowers. Having invented a brother whom they claim to have ostracized for his disobedience, the uber-controlling parents terrorize their offspring into submission.",el,949,7.2 682,Bad Sister,"As a top student at St. Adeline's Catholic Boarding School, Zoe senses that something is not quite right about the school's new nun-- a sense proven to be true when it is revealed the ""good' nun is an imposter with a fatal attraction to Zoe's brother.",en,14,4.3 683,Castle in the Sky,A young boy and a girl with a magic crystal must race against pirates and foreign agents in a search for a legendary floating castle.,ja,2148,8 684,Wolf Warrior 3,The third movie about a Chinese special force soldier with extraordinary marksmanship.,zh,0,0 685,Batman Returns,"Having defeated the Joker, Batman now faces the Penguin—a warped and deformed individual who is intent on being accepted into Gotham society, with the help of Max Schreck, a crooked businessman, whom he coerces into helping him run for the position of Mayor of Gotham, while they both attempt to frame Batman in a different light. Batman must attempt to clear his name, all while also deciding just what must be done with the mysterious Catwoman slinking about.",en,3790,6.8 686,Oblivion,"Jack Harper is one of the last few drone repairmen stationed on Earth. Part of a massive operation to extract vital resources after decades of war with a terrifying threat known as the Scavs, Jack’s mission is nearly complete. His existence is brought crashing down when he rescues a beautiful stranger from a downed spacecraft. Her arrival triggers a chain of events that forces him to question everything he knows and puts the fate of humanity in his hands.",en,7591,6.5 687,The Pianist,"The true story of pianist Władysław Szpilman's experiences in Warsaw during the Nazi occupation. When the Jews of the city find themselves forced into a ghetto, Szpilman finds work playing in a café; and when his family is deported in 1942, he stays behind, works for a while as a laborer, and eventually goes into hiding in the ruins of the war-torn city.",en,4965,8.3 688,Logan Lucky,"Trying to reverse a family curse, brothers Jimmy and Clyde Logan set out to execute an elaborate robbery during the legendary Coca-Cola 600 race at the Charlotte Motor Speedway.",en,2014,6.7 689,Mortal Engines,"Many thousands of years in the future, Earth’s cities roam the globe on huge wheels, devouring each other in a struggle for ever diminishing resources. On one of these massive traction cities, the old London, Tom Natsworthy has an unexpected encounter with a mysterious young woman from the wastelands who will change the course of his life forever.",en,2589,6.1 690,The World Is Not Enough,"Greed, revenge, world dominance and high-tech terrorism – it's all in a day's work for Bond, who's on a mission to protect a beautiful oil heiress from a notorious terrorist. In a race against time that culminates in a dramatic submarine showdown, Bond works to defuse the international power struggle that has the world's oil supply hanging in the balance.",en,1666,6.2 691,The Man Who Knew Too Much,"A widescreen, Technicolor remake by Hitchcock of his 1934 film of the same title. A couple vacationing in Morocco with their young son accidentally stumble upon an assassination plot. When the child is kidnapped to ensure their silence, they have to take matters into their own hands to save him.",en,688,7.6 692,Bambi,"Bambi's tale unfolds from season to season as the young prince of the forest learns about life, love, and friends.",en,3574,7 693,The Chronicles of Narnia: The Voyage of the Dawn Treader,"This time around Edmund and Lucy Pevensie, along with their pesky cousin Eustace Scrubb find themselves swallowed into a painting and on to a fantastic Narnian ship headed for the very edges of the world.",en,3716,6.4 694,Saw III,"Jigsaw has disappeared. Along with his new apprentice Amanda, the puppet-master behind the cruel, intricate games that have terrified a community and baffled police has once again eluded capture and vanished. While city detective scramble to locate him, Doctor Lynn Denlon and Jeff Reinhart are unaware that they are about to become the latest pawns on his vicious chessboard.",en,2603,6.4 695,Tangled,"When the kingdom's most wanted-and most charming-bandit Flynn Rider hides out in a mysterious tower, he's taken hostage by Rapunzel, a beautiful and feisty tower-bound teen with 70 feet of magical, golden hair. Flynn's curious captor, who's looking for her ticket out of the tower where she's been locked away for years, strikes a deal with the handsome thief and the unlikely duo sets off on an action-packed escapade, complete with a super-cop horse, an over-protective chameleon and a gruff gang of pub thugs.",en,7393,7.6 696,Autonomes,,fr,0,0 697,Moonraker,"After Drax Industries' Moonraker space shuttle is hijacked, secret agent James Bond is assigned to investigate, traveling to California to meet the company's owner, the mysterious Hugo Drax. With the help of scientist Dr. Holly Goodhead, Bond soon uncovers Drax's nefarious plans for humanity, all the while fending off an old nemesis, Jaws, and venturing to Venice, Rio, the Amazon...and outer space.",en,1038,6.1 698,War of the Worlds,"Ray Ferrier is a divorced dockworker and less-than-perfect father. Soon after his ex-wife and her new husband drop off his teenage son and young daughter for a rare weekend visit, a strange and powerful lightning storm touches down.",en,5127,6.4 699,Stuber,"After crashing his car, a cop who's recovering from eye surgery recruits an Uber driver to help him catch a heroin dealer. The mismatched pair soon find themselves in for a wild day of stakeouts and shootouts as they encounter the city's seedy side.",en,536,6.8 700,Rocky V,"A lifetime of taking shots has ended Rocky’s career, and a crooked accountant has left him broke. Inspired by the memory of his trainer, however, Rocky finds glory in training and takes on an up-and-coming boxer.",en,1758,5.6 701,Suicide Squad,"From DC Comics comes the Suicide Squad, an antihero team of incarcerated supervillains who act as deniable assets for the United States government, undertaking high-risk black ops missions in exchange for commuted prison sentences.",en,15125,5.9 702,The Secret World of Arrietty,"14-year-old Arrietty and the rest of the Clock family live in peaceful anonymity as they make their own home from items ""borrowed"" from the house's human inhabitants. However, life changes for the Clocks when a human boy discovers Arrietty.",ja,1492,7.6 703,Cinema Paradiso,"A filmmaker recalls his childhood, when he fell in love with the movies at his village's theater and formed a deep friendship with the theater's projectionist.",it,2141,8.4 704,Ice Age,"With the impending ice age almost upon them, a mismatched trio of prehistoric critters – Manny the woolly mammoth, Diego the saber-toothed tiger and Sid the giant sloth – find an orphaned infant and decide to return it to its human parents. Along the way, the unlikely allies become friends but, when enemies attack, their quest takes on far nobler aims.",en,8596,7.3 705,Serenity,"When the renegade crew of Serenity agrees to hide a fugitive on their ship, they find themselves in an action-packed battle between the relentless military might of a totalitarian regime who will destroy anything – or anyone – to get the girl back and the bloodthirsty creatures who roam the uncharted areas of space. But... the greatest danger of all may be on their ship.",en,2194,7.4 706,Miss Potter,"The story of Beatrix Potter, the author of the beloved and best-selling children's book, 'The Tale of Peter Rabbit', and her struggle for love, happiness and success.",en,287,6.4 707,St. Vincent,"A young boy whose parents just divorced finds an unlikely friend and mentor in the misanthropic, bawdy, hedonistic, war veteran who lives next door.",en,1203,7.1 708,My Cousin,The CEO of an international wine company must meet with his bumbling cousin to renew the contract allowing him to manage the brand. Snafus ensue.,fr,0,0 709,The Bourne Supremacy,"When a CIA operation to purchase classified Russian documents is blown by a rival agent, who then shows up in the sleepy seaside village where Bourne and Marie have been living. The pair run for their lives and Bourne, who promised retaliation should anyone from his former life attempt contact, is forced to once again take up his life as a trained assassin to survive.",en,4923,7.3 710,A Cinderella Story: Christmas Wish,"Kat is an aspiring singer-songwriter who dreams of making it big. However, her dreams are stalled by her reality: a conniving and cruel stepfamily and a demoralizing job working as a singing elf at billionaire Terrence Wintergarden’s Santa Land.",en,327,6.7 711,Alvin and the Chipmunks: The Squeakquel,"Pop sensations Alvin, Simon and Theodore end up in the care of Dave Seville's twenty-something nephew Toby. The boys must put aside music super stardom to return to school, and are tasked with saving the school's music program by winning the $25,000 prize in a battle of the bands. But the Chipmunks unexpectedly meet their match in three singing chipmunks known as The Chipettes - Brittany, Eleanor and Jeanette. Romantic and musical sparks are ignited when the Chipmunks and Chipettes square off.",en,1794,5.5 712,Crazy Rich Asians,"An American-born Chinese economics professor accompanies her boyfriend to Singapore for his best friend's wedding, only to get thrust into the lives of Asia's rich and famous.",en,1986,7.1 713,The Wedding Ringer,"Doug Harris is a loveable but socially awkward groom-to-be with a problem: he has no best man. With less than two weeks to go until he marries the girl of his dreams, Doug is referred to Jimmy Callahan, owner and CEO of Best Man, Inc., a company that provides flattering best men for socially challenged guys in need. What ensues is a hilarious wedding charade as they try to pull off the big con, and an unexpected budding bromance between Doug and his fake best man Jimmy.",en,1080,6.5 714,The Huntsman: Winter's War,"As two evil sisters prepare to conquer the land, two renegades—Eric the Huntsman, who aided Snow White in defeating Ravenna in Snowwhite and the Huntsman, and his forbidden lover, Sara—set out to stop them.",en,3380,6.2 715,The Two Popes,"Frustrated with the direction of the church, Cardinal Bergoglio requests permission to retire in 2012 from Pope Benedict. Instead, facing scandal and self-doubt, the introspective Pope Benedict summons his harshest critic and future successor to Rome to reveal a secret that would shake the foundations of the Catholic Church.",en,1393,7.6 716,Hereditary,"When Ellen, the matriarch of the Graham family, passes away, her daughter’s family begins to unravel cryptic and increasingly terrifying secrets about their ancestry.",en,3612,7.1 717,RED 2,Retired C.I.A. agent Frank Moses reunites his unlikely team of elite operatives for a global quest to track down a missing portable nuclear device.,en,2659,6.5 718,Annie,"Annie is a young, happy foster kid who's also tough enough to make her way on the streets of New York in 2014. Originally left by her parents as a baby with the promise that they'd be back for her someday, it's been a hard knock life ever since with her mean foster mom Miss Hannigan. But everything's about to change when the hard-nosed tycoon and New York mayoral candidate Will Stacks—advised by his brilliant VP and his shrewd and scheming campaign advisor—makes a thinly-veiled campaign move and takes her in. Stacks believes he's her guardian angel, but Annie's self-assured nature and bright, sun-will-come-out-tomorrow outlook on life just might mean it's the other way around.",en,896,6.2 719,Sex Tape,"When Jay and Annie first got together, their romantic connection was intense – but ten years and two kids later, the flame of their love needs a spark. To kick things up a notch, they decide – why not? – to make a video of themselves trying out every position in The Joy of Sex in one marathon three-hour session. It seems like a great idea – until they discover that their most private video is no longer private. With their reputations on the line, they know they’re just one click away from being laid bare to the world... but as their race to reclaim their video leads to a night they'll never forget, they'll find that their video will expose even more than they bargained for.",en,3021,5.3 720,American Pie Presents: Band Camp,"Everyone has 'moved on', except for Sherman and Jim Levenstein's still understanding father. Little Matt Stiffler wants to join his older brother Steve's business and, after everything Matt has heard from Jim's band-geek wife, he plans to go back to band camp and make a video of his own.",en,1324,5.3 721,Danger Close,"Vietnam War, 1966. Australia and New Zealand send troops to support the United States and South Vietnamese in their fight against the communist North. Soldiers are very young men, recruits and volunteers who have never been involved in a combat. On August 18th, members of Delta Company will face the true horror of a ruthless battle among the trees of a rubber plantation called Long Tân. They are barely a hundred. The enemy is a human wave ready to destroy them.",en,59,6.5 722,Mission: Impossible III,"Retired from active duty to train new IMF agents, Ethan Hunt is called back into action to confront sadistic arms dealer, Owen Davian. Hunt must try to protect his girlfriend while working with his new team to complete the mission.",en,4104,6.6 723,The Revenant,"In the 1820s, a frontiersman, Hugh Glass, sets out on a path of vengeance against those who left him for dead after a bear mauling.",en,12641,7.5 724,Taxi Driver,"A mentally unstable Vietnam War veteran works as a night-time taxi driver in New York City where the perceived decadence and sleaze feed his urge for violent action, attempting to save a preadolescent prostitute in the process.",en,6498,8.2 725,Jason Bourne,The most dangerous former operative of the CIA is drawn out of hiding to uncover hidden truths about his past.,en,3959,6.2 726,Allegiant,Beatrice Prior and Tobias Eaton venture into the world outside of the fence and are taken into protective custody by a mysterious agency known as the Bureau of Genetic Welfare.,en,4654,6 727,Salt,"As a CIA officer, Evelyn Salt swore an oath to duty, honor and country. Her loyalty will be tested when a defector accuses her of being a Russian spy. Salt goes on the run, using all her skills and years of experience as a covert operative to elude capture. Salt's efforts to prove her innocence only serve to cast doubt on her motives, as the hunt to uncover the truth behind her identity continues and the question remains: ""Who is Salt?""",en,3663,6.3 728,My Hero Academia: Heroes Rising,"Class 1-A visits Nabu Island where they finally get to do some real hero work. The place is so peaceful that it's more like a vacation … until they're attacked by a villain with an unfathomable Quirk! His power is eerily familiar, and it looks like Shigaraki had a hand in the plan. But with All Might retired and citizens' lives on the line, there's no time for questions. Deku and his friends are the next generation of heroes, and they're the island's only hope.",ja,33,8 729,The Jungle Book,"The boy Mowgli makes his way to the man-village with Bagheera, the wise panther. Along the way he meets jazzy King Louie, the hypnotic snake Kaa and the lovable, happy-go-lucky bear Baloo, who teaches Mowgli ""The Bare Necessities"" of life and the true meaning of friendship.",en,4162,7.3 730,Shrek Forever After,"A bored and domesticated Shrek pacts with deal-maker Rumpelstiltskin to get back to feeling like a real ogre again, but when he's duped and sent to a twisted version of Far Far Away—where Rumpelstiltskin is king, ogres are hunted, and he and Fiona have never met—he sets out to restore his world and reclaim his true love.",en,4465,6.2 731,Power Rangers,"Saban's Power Rangers follows five ordinary teens who must become something extraordinary when they learn that their small town of Angel Grove — and the world — is on the verge of being obliterated by an alien threat. Chosen by destiny, our heroes quickly discover they are the only ones who can save the planet. But to do so, they will have to overcome their real-life issues and before it’s too late, band together as the Power Rangers.",en,3022,6.2 732,Sathuranga Vettai 2,"Sathuranga Vettai 2 (""Chess Hunt 2"") is an upcoming Indian Tamil-language thriller film directed by Nirmal Kumar and written by Vinoth.",ta,0,0 733,Chinatown,"Private eye Jake Gittes lives off of the murky moral climate of sunbaked, pre-World War II Southern California. Hired by a beautiful socialite to investigate her husband's extra-marital affair, Gittes is swept into a maelstrom of double dealings and deadly deceits, uncovering a web of personal and political scandals that come crashing together.",en,2112,7.9 734,Robin Hood,A war-hardened Crusader and his Moorish commander mount an audacious revolt against the corrupt English crown.,en,1740,5.8 735,"Three Billboards Outside Ebbing, Missouri","After seven months have passed without a culprit in her daughter's murder case, Mildred Hayes makes a bold move, painting three signs leading into her town with a controversial message directed at Bill Willoughby, the town's revered chief of police. When his second-in-command Officer Jason Dixon, an immature mother's boy with a penchant for violence, gets involved, the battle between Mildred and Ebbing's law enforcement is only exacerbated.",en,6435,8.1 736,Maggie,"There's a deadly zombie epidemic threatening humanity, but Wade, a small-town farmer and family man, refuses to accept defeat even when his daughter Maggie becomes infected. As Maggie's condition worsens and the authorities seek to eradicate those with the virus, Wade is pushed to the limits in an effort to protect her. Joely Richardson co-stars in this post-apocalyptic thriller.",en,1075,5.3 737,Rage,"When the Russian mob kidnaps the daughter of a reformed criminal, he rounds up his old crew and seeks his own brand of justice.",en,477,5.2 738,The Goonies,"A young teenager named Mikey Walsh finds an old treasure map in his father's attic. Hoping to save their homes from demolition, Mikey and his friends Data Wang, Chunk Cohen, and Mouth Devereaux run off on a big quest to find the secret stash of Pirate One-Eyed Willie.",en,3380,7.5 739,Scarface,"After getting a green card in exchange for assassinating a Cuban government official, Tony Montana stakes a claim on the drug trade in Miami. Viciously murdering anyone who stands in his way, Tony eventually becomes the biggest drug lord in the state, controlling nearly all the cocaine that comes through Miami. But increased pressure from the police, wars with Colombian drug cartels and his own drug-fueled paranoia serve to fuel the flames of his eventual downfall.",en,6470,8.1 740,Three Steps Above Heaven,"Story of two young people who belong to different worlds. It is the chronicle of a love improbable, almost impossible but inevitable dragging in a frantic journey they discover the first great love. Babi (Maria Valverde) is a girl from upper-middle class that is educated in goodness and innocence . Hache (Mario Casas) is a rebellious boy, impulsive, unconscious, has a appetite for risk and danger embodied in endless fights and illegal motorbike races, the limit of common sense",es,1191,7.5 741,All the Bright Places,Two teens facing personal struggles form a powerful bond as they embark on a cathartic journey chronicling the wonders of Indiana.,en,768,7.5 742,Malena,"On the day in 1940 that Italy enters the war, two things happen to the 12-year-old Renato: he gets his first bike, and he gets his first look at Malèna. She is a beautiful, silent outsider who's moved to this Sicilian town to be with her husband, Nico. He promptly goes off to war, leaving her to the lustful eyes of the men and the sharp tongues of the women. During the next few years, as Renato grows toward manhood, he watches Malèna suffer and prove her mettle. He sees her loneliness, then grief when Nico is reported dead, the effects of slander on her relationship with her father, her poverty and search for work, and final humiliations. Will Renato learn courage from Malèna and stand up for her?",it,819,7.1 743,The Big Lebowski,"Jeffrey ""The Dude"" Lebowski, a Los Angeles slacker who only wants to bowl and drink White Russians, is mistaken for another Jeffrey Lebowski, a wheelchair-bound millionaire, and finds himself dragged into a strange series of events involving nihilists, adult film producers, ferrets, errant toes, and large sums of money.",en,6738,7.9 744,Mile 22,"An elite group of American operatives, aided by a top-secret tactical command team, must transport an asset who holds life-threatening information to an extraction point 22 miles away through the hostile streets of an Asian city.",en,1243,6.1 745,Blade,"When Blade's mother was bitten by a vampire during pregnancy, she did not know that she gave her son a special gift while dying—all the good vampire attributes in combination with the best human skills. Blade and his mentor battle an evil vampire rebel who plans to take over the outdated vampire council, capture Blade and resurrect a voracious blood god.",en,3596,6.6 746,Murder on the Orient Express,Genius Belgian detective Hercule Poirot investigates the murder of an American tycoon aboard the Orient Express train.,en,6408,6.7 747,The Great Wall,European mercenaries searching for black powder become embroiled in the defense of the Great Wall of China against a horde of monstrous creatures.,en,3190,5.8 748,Gnomeo & Juliet,"A version of Shakespeare's play, set in the world of warring indoor and outdoor gnomes. Garden gnomes Gnomeo and Juliet have as many obstacles to overcome as their quasi namesakes when they are caught up in a feud between neighbors. But with plastic pink flamingos and lawnmower races in the mix, can this young couple find lasting happiness?",en,1267,5.8 749,Game Night,"Max and Annie's weekly game night gets kicked up a notch when Max's brother Brooks arranges a murder mystery party -- complete with fake thugs and federal agents. So when Brooks gets kidnapped, it's all supposed to be part of the game. As the competitors set out to solve the case, they start to learn that neither the game nor Brooks are what they seem to be. The friends soon find themselves in over their heads as each twist leads to another unexpected turn over the course of one chaotic night.",en,3417,6.9 750,Divergent,"In a world divided into factions based on personality types, Tris learns that she's been classified as Divergent and won't fit in. When she discovers a plot to destroy Divergents, Tris and the mysterious Four must find out what makes Divergents dangerous before it's too late.",en,9228,6.9 751,Jack the Giant Slayer,"The story of an ancient war that is reignited when a young farmhand unwittingly opens a gateway between our world and a fearsome race of giants. Unleashed on the Earth for the first time in centuries, the giants strive to reclaim the land they once lost, forcing the young man, Jack into the battle of his life to stop them. Fighting for a kingdom, its people, and the love of a brave princess, he comes face to face with the unstoppable warriors he thought only existed in legend–and gets the chance to become a legend himself.",en,3683,5.7 752,Once Upon a Time in the West,A widow whose land and life are in danger as the railroad is getting closer and closer to taking them over. A mysterious harmonica player joins forces with a desperado to protect the woman and her land.,it,2421,8.3 753,Pinocchio,"Lonely toymaker Geppetto has his wishes answered when the Blue Fairy arrives to bring his wooden puppet Pinocchio to life. Before becoming a real boy, however, Pinocchio must prove he's worthy as he sets off on an adventure with his whistling sidekick and conscience, Jiminy Cricket.",en,3596,7 754,Madagascar: Escape 2 Africa,"Alex, Marty, and other zoo animals find a way to escape from Madagascar when the penguins reassemble a wrecked airplane. The precariously repaired craft stays airborne just long enough to make it to the African continent. There the New Yorkers encounter members of their own species for the first time. Africa proves to be a wild place, but Alex and company wonder if it is better than their Central Park home.",en,4419,6.4 755,Terminator Salvation,"All grown up in post-apocalyptic 2018, John Connor must lead the resistance of humans against the increasingly dominating militaristic robots. But when Marcus Wright appears, his existence confuses the mission as Connor tries to determine whether Wright has come from the future or the past -- and whether he's friend or foe.",en,4273,6 756,The Elephant Man,"A Victorian surgeon rescues a heavily disfigured man being mistreated by his ""owner"" as a side-show freak. Behind his monstrous façade, there is revealed a person of great intelligence and sensitivity. Based on the true story of Joseph Merrick (called John Merrick in the film), a severely deformed man in 19th century London.",en,1801,8.1 757,Pixels,Video game experts are recruited by the military to fight 1980s-era video game characters who've attacked New York.,en,4993,5.6 758,Queen & Slim,"While on a forgettable first date together in Ohio, a black man and a black woman are pulled over for a minor traffic infraction. The situation escalates, with sudden and tragic results, when the man kills the police officer in self-defense. Terrified and in fear for their lives, the man, a retail employee, and the woman, a criminal defense lawyer, are forced to go on the run. But the incident is captured on video and goes viral, and the couple unwittingly become a symbol of trauma, terror, grief and pain for people across the country.",en,158,7.3 759,2 Fingers Honey,"The story of a couple trying to realize a honeymoon in the Caribbean, but a mistake in buying tickets makes them wander the streets of southern Albania experiencing many different vicissitudes.",sq,3,5.5 760,The Lion King II: Simba's Pride,"The circle of life continues for Simba, now fully grown and in his rightful place as the king of Pride Rock. Simba and Nala have given birth to a daughter, Kiara who's as rebellious as her father was. But Kiara drives her parents to distraction when she catches the eye of Kovu, the son of the evil lioness, Zira. Will Kovu steal Kiara's heart?",en,2801,6.9 761,Into the Woods,"In a woods filled with magic and fairy tale characters, a baker and his wife set out to end the curse put on them by their neighbor, a spiteful witch.",en,3225,5.7 762,The Purge: Election Year,"Two years after choosing not to kill the man who killed his son, former police sergeant Leo Barnes has become head of security for Senator Charlene Roan, the front runner in the next Presidential election due to her vow to eliminate the Purge. On the night of what should be the final Purge, a betrayal from within the government forces Barnes and Roan out onto the street where they must fight to survive the night.",en,3320,6.3 763,Paddington 2,"Paddington, now happily settled with the Browns, picks up a series of odd jobs to buy the perfect present for his Aunt Lucy, but it is stolen.",en,1116,7.5 764,RoboCop,"In a violent, near-apocalyptic Detroit, evil corporation Omni Consumer Products wins a contract from the city government to privatize the police force. To test their crime-eradicating cyborgs, the company leads street cop Alex Murphy into an armed confrontation with crime lord Boddicker so they can use his body to support their untested RoboCop prototype. But when RoboCop learns of the company's nefarious plans, he turns on his masters.",en,2827,7.2 765,Original Sin,"A young man is plunged into a life of subterfuge, deceit and mistaken identity in pursuit of a femme fatale whose heart is never quite within his grasp",en,451,5.9 766,Scarlet Innocence,"A university professor gradually succumbing to blindness is entranced by an obsessive love, in this modern-day adaptation of a classic Korean fairy tale.",ko,23,6.2 767,Saw II,"When a new murder victim is discovered with all the signs of Jigsaw's hand, Detective Eric Matthews begins a full investigation and apprehends Jigsaw with little effort. But for Jigsaw, getting caught is just another part of his plan. Eight more of his victims are already fighting for their lives and now it's time for Matthews to join the game.",en,3117,6.5 768,Bee Movie,"Barry B. Benson, a bee who has just graduated from college, is disillusioned at his lone career choice: making honey. On a special trip outside the hive, Barry's life is saved by Vanessa, a florist in New York City. As their relationship blossoms, he discovers humans actually eat honey, and subsequently decides to sue us.",en,3007,5.9 769,Tout nous sourit,,fr,0,0 770,Yesterday,"Jack Malik is a struggling singer-songwriter in an English seaside town whose dreams of fame are rapidly fading, despite the fierce devotion and support of his childhood best friend, Ellie. After a freak bus accident during a mysterious global blackout, Jack wakes up to discover that he's the only person on Earth who can remember The Beatles.",en,1542,6.7 771,Forbidden Planet,"Captain Adams and the crew of the Starship C57D travel to planet Altair 4 in search of the spaceship ""Bellerophon"" that has been missing for 20 years. To their surprise, they are expected.",en,469,7.3 772,The Green Mile,"A supernatural tale set on death row in a Southern prison, where gentle giant John Coffey possesses the mysterious power to heal people's ailments. When the cell block's head guard, Paul Edgecomb, recognizes Coffey's miraculous gift, he tries desperately to help stave off the condemned man's execution.",en,9740,8.5 773,The Theory of Everything,"The Theory of Everything is the extraordinary story of one of the world’s greatest living minds, the renowned astrophysicist Stephen Hawking, who falls deeply in love with fellow Cambridge student Jane Wilde.",en,7333,7.9 774,The Handmaiden,"1930s Korea, in the period of Japanese occupation, a young woman is hired as a handmaiden to a Japanese heiress who lives a secluded life on a large countryside estate with her domineering uncle. But, the maid has a secret: she is a pickpocket recruited by a swindler posing as a Japanese count to help him seduce the heiress to elope with him, rob her of her fortune, and lock her up in a madhouse. The plan seems to proceed according to plan until the women discover some unexpected emotions.",ko,1622,8.3 775,In the Tall Grass,"After hearing a child screaming for help from the green depths of a vast field of tall grass, Becky, a pregnant woman, and Cal, her brother, park their car near a mysterious abandoned church and recklessly enter the field, discovering that they are not alone and because of some reason they are unable of escaping a completely inextricable vegetable labyrinth.",en,962,5.5 776,The Great Gatsby,"An adaptation of F. Scott Fitzgerald's Long Island-set novel, where Midwesterner Nick Carraway is lured into the lavish world of his neighbor, Jay Gatsby. Soon enough, however, Carraway will see through the cracks of Gatsby's nouveau riche existence, where obsession, madness, and tragedy await.",en,8239,7.4 777,Footloose,"Ren MacCormack is transplanted from Boston to the small southern town of Bomont where loud music and dancing are prohibited. Not one to bow to the status quo, Ren challenges the ban, revitalizing the town and falling in love with the minister’s troubled daughter Ariel in the process.",en,1073,6.6 778,Unbreakable,"An ordinary man makes an extraordinary discovery when a train accident leaves his fellow passengers dead — and him unscathed. The answer to this mystery could lie with the mysterious Elijah Price, a man who suffers from a disease that renders his bones as fragile as glass.",en,5971,7.1 779,Rise of the Planet of the Apes,"Scientist Will Rodman is determined to find a cure for Alzheimer's, the disease which has slowly consumed his father. Will feels certain he is close to a breakthrough and tests his latest serum on apes, noticing dramatic increases in intelligence and brain activity in the primate subjects – especially Caesar, his pet chimpanzee.",en,8098,7.2 780,Outcast,"A mysterious warrior teams up with the daughter and son of a deposed Chinese Emperor to defeat their cruel Uncle, who seeks their deaths.",en,263,4.8 781,American Sniper,"U.S. Navy SEAL Chris Kyle takes his sole mission—protect his comrades—to heart and becomes one of the most lethal snipers in American history. His pinpoint accuracy not only saves countless lives but also makes him a prime target of insurgents. Despite grave danger and his struggle to be a good husband and father to his family back in the States, Kyle serves four tours of duty in Iraq. However, when he finally returns home, he finds that he cannot leave the war behind.",en,8731,7.4 782,Madagascar 3: Europe's Most Wanted,"Animal pals Alex, Marty, Melman, and Gloria are still trying to make it back to New York's Central Park Zoo. They are forced to take a detour to Europe to find the penguins and chimps who broke the bank at a Monte Carlo casino. When French animal-control officer Capitaine Chantel DuBois picks up their scent, Alex and company are forced to hide out in a traveling circus.",en,3896,6.5 783,Texas Chainsaw 3D,"A young woman learns that she has inherited a Texas estate from her deceased grandmother. After embarking on a road trip with friends to uncover her roots, she finds she is the sole owner of a lavish, isolated Victorian mansion. But her newfound wealth comes at a price as she stumbles upon a horror that awaits her in the mansion’s dank cellars.",en,923,5.3 784,American Pie 2,"The whole gang are back and as close as ever. They decide to get even closer by spending the summer together at a beach house. They decide to hold the biggest party ever to be seen, even if the preparation doesn't always go to plan. Especially when Stifler, Finch and Jim become more close to each other than they ever want to be and when Jim mistakes super glue for lubricant...",en,3087,6.1 785,Peter Pan,"Leaving the safety of their nursery behind, Wendy, Michael and John follow Peter Pan to a magical world where childhood lasts forever. But while in Neverland, the kids must face Captain Hook and foil his attempts to get rid of Peter for good.",en,3501,7.2 786,Armageddon,"When an asteroid threatens to collide with Earth, NASA honcho Dan Truman determines the only way to stop it is to drill into its surface and detonate a nuclear bomb. This leads him to renowned driller Harry Stamper, who agrees to helm the dangerous space mission provided he can bring along his own hotshot crew. Among them is the cocksure A.J. who Harry thinks isn't good enough for his daughter, until the mission proves otherwise.",en,4945,6.7 787,The Sword in the Stone,"Wart is a young boy who aspires to be a knight's squire. On a hunting trip he falls in on Merlin, a powerful but amnesiac wizard who has plans for him beyond mere squiredom. He starts by trying to give him an education, believing that once one has an education, one can go anywhere. Needless to say, it doesn't quite work out that way.",de,2430,7.2 788,The Concubine,"Living a torturous life of poverty and barely able to survive, Hwa-yeon decides to offer herself as one of the king’s concubines. Once inside the royal palace, two men are immediately seized by the woman -- the Grand Prince Seong-won, a megalomaniacal ruler drunk with power and lust, and Kwon-yoo, who has everything to lose if his desire for Hwa-yeon is exposed.",ko,36,6 789,The Absent One,"Denmark, 2014. A former police officer asks Carl Mørck, head of Department Q, to find out who brutally killed his young twins in 1994. Although a local inhabitant confessed and was convicted of murder, Carl and his partner Assad soon realize that there is something in the case resolution that is terribly wrong.",da,327,7.2 790,Apocalypse Now,"At the height of the Vietnam war, Captain Benjamin Willard is sent on a dangerous mission that, officially, ""does not exist, nor will it ever exist."" His goal is to locate - and eliminate - a mysterious Green Beret Colonel named Walter Kurtz, who has been leading his personal army on illegal guerrilla missions into enemy territory.",en,4705,8.3 791,The Thing,"In remote Antarctica, a group of American research scientists are disturbed at their base camp by a helicopter shooting at a sled dog. When they take in the dog, it brutally attacks both human beings and canines in the camp and they discover that the beast can assume the shape of its victims. A resourceful helicopter pilot and the camp doctor lead the camp crew in a desperate, gory battle against the vicious creature before it picks them all off, one by one.",en,3655,8 792,The Usual Suspects,"Held in an L.A. interrogation room, Verbal Kint attempts to convince the feds that a mythic crime lord, Keyser Soze, not only exists, but was also responsible for drawing him and his four partners into a multi-million dollar heist that ended with an explosion in San Pedro harbor – leaving few survivors. Verbal lures his interrogators with an incredible story of the crime lord's almost supernatural prowess.",en,6462,8.2 793,The King,"England, 15th century. Hal, a capricious prince who lives among the populace far from court, is forced by circumstances to reluctantly accept the throne and become Henry V.",en,1220,7.2 794,Hidden Figures,"The untold story of Katherine G. Johnson, Dorothy Vaughan and Mary Jackson – brilliant African-American women working at NASA and serving as the brains behind one of the greatest operations in history – the launch of astronaut John Glenn into orbit. The visionary trio crossed all gender and race lines to inspire generations to dream big.",en,5597,8 795,Safe House,"A dangerous CIA renegade resurfaces after a decade on the run. When the safe house he's remanded to is attacked by mercenaries, a rookie operative escapes with him. Now, the unlikely allies must stay alive long enough to uncover who wants them dead.",en,2250,6.4 796,Shaun of the Dead,"Shaun lives a supremely uneventful life, which revolves around his girlfriend, his mother, and, above all, his local pub. This gentle routine is threatened when the dead return to life and make strenuous attempts to snack on ordinary Londoners.",en,5340,7.5 797,Instant Family,"When Pete and Ellie decide to start a family, they stumble into the world of foster care adoption. They hope to take in one small child but when they meet three siblings, including a rebellious 15 year old girl, they find themselves speeding from zero to three kids overnight.",en,1321,7.5 798,Total Recall,"Construction worker Douglas Quaid discovers a memory chip in his brain during a virtual-reality trip. He also finds that his past has been invented to conceal a plot of planetary domination. Soon, he's off to Mars to find out who he is and who planted the chip.",en,3329,7.2 799,The Hangover Part II,"The Hangover crew heads to Thailand for Stu's wedding. After the disaster of a bachelor party in Las Vegas last year, Stu is playing it safe with a mellow pre-wedding brunch. However, nothing goes as planned and Bangkok is the perfect setting for another adventure with the rowdy group.",en,7054,6.4 800,Scary Movie,"Following on the heels of popular teen-scream horror movies, with uproarious comedy and biting satire. Marlon and Shawn Wayans, Shannon Elizabeth and Carmen Electra pitch in to skewer some of Hollywood's biggest blockbusters, including Scream, I Know What You Did Last Summer, The Matrix, American Pie and The Blair Witch Project.",en,4021,6.2 801,RoboCop,"In RoboCop, the year is 2028 and multinational conglomerate OmniCorp is at the center of robot technology. Overseas, their drones have been used by the military for years, but have been forbidden for law enforcement in America. Now OmniCorp wants to bring their controversial technology to the home front, and they see a golden opportunity to do it. When Alex Murphy – a loving husband, father and good cop doing his best to stem the tide of crime and corruption in Detroit – is critically injured, OmniCorp sees their chance to build a part-man, part-robot police officer. OmniCorp envisions a RoboCop in every city and even more billions for their shareholders, but they never counted on one thing: there is still a man inside the machine.",en,3788,5.8 802,Shot Caller,A newly-released prison gangster is forced by the leaders of his gang to orchestrate a major crime with a brutal rival gang on the streets of Southern California.,en,1242,6.9 803,Lethal Weapon,"Veteran buttoned-down LAPD detective Roger Murtaugh is partnered with unhinged cop Martin Riggs, who -- distraught after his wife's death -- has a death wish and takes unnecessary risks with criminals at every turn. The odd couple embark on their first homicide investigation as partners, involving a young woman known to Murtaugh with ties to a drug and prostitution ring.",en,2665,7.2 804,Sixteen Candles,A teenage girl deals with her parents forgetting her birthday and a crush on her high school's heartthrob.,en,1281,6.8 805,Play,"In 1993, Max was 13 when he was offered his first camera. For 25 years he will not stop filming. The bunch of friends, the loves, the successes, the failures. From the 90s to the 2010s, it is the portrait of a whole generation that is emerging through its objective.",fr,81,7.5 806,Zodiac,"The true story of the investigation of the ""Zodiac Killer"", a serial killer who terrified the San Francisco Bay Area, taunting police with his ciphers and letters. The case becomes an obsession for three men as their lives and careers are built and destroyed by the endless trail of clues.",en,5521,7.5 807,Anna,Beneath Anna Poliatova's striking beauty lies a secret that will unleash her indelible strength and skill to become one of the world's most feared government assassins.,fr,982,6.6 808,Men in Black 3,"Agents J and K are back...in time. J has seen some inexplicable things in his 15 years with the Men in Black, but nothing, not even aliens, perplexes him as much as his wry, reticent partner. But when K's life and the fate of the planet are put at stake, Agent J will have to travel back in time to put things right. J discovers that there are secrets to the universe that K never told him - secrets that will reveal themselves as he teams up with the young Agent K to save his partner, the agency, and the future of humankind.",en,7245,6.4 809,Dinosaur,An orphaned dinosaur raised by lemurs joins an arduous trek to a sancturary after a meteorite shower destroys his family home.,en,1413,6.4 810,Dracula,"When Dracula leaves the captive Jonathan Harker and Transylvania for London in search of Mina Harker—the reincarnation of Dracula's long-dead wife, Elisabeta—obsessed vampire hunter Dr. Van Helsing sets out to end the madness.",en,2767,7.4 811,The Hunchback of Notre Dame,"When Quasimodo defies the evil Frollo and ventures out to the Festival of Fools, the cruel crowd jeers him. Rescued by fellow outcast the gypsy Esmeralda, Quasi soon finds himself battling to save the people and the city he loves.",en,3180,7 812,Fantastic Four: Rise of the Silver Surfer,"The Fantastic Four return to the big screen as a new and all powerful enemy threatens the Earth. The seemingly unstoppable 'Silver Surfer', but all is not what it seems and there are old and new enemies that pose a greater threat than the intrepid superheroes realize.",en,5458,5.5 813,About Time,"The night after another unsatisfactory New Year party, Tim's father tells his son that the men in his family have always had the ability to travel through time. Tim can't change history, but he can change what happens and has happened in his own life – so he decides to make his world a better place... by getting a girlfriend. Sadly, that turns out not to be as easy as he thinks.",en,4795,7.9 814,Irrational Man,"On a small town college campus, a philosophy professor in existential crisis gives his life new purpose when he enters into a relationship with his student.",en,1542,6.5 815,Police Academy 2: Their First Assignment,"Officer Carey Mahoney and his cohorts have finally graduated from the Police Academy and are about to hit the streets on their first assignment. Question is, are they ready to do battle with a band of graffiti-tagging terrorists? Time will tell, but don't sell short this cheerful band of doltish boys in blue.",en,800,6 816,Gremlins,"When Billy Peltzer is given a strange but adorable pet named Gizmo for Christmas, he inadvertently breaks the three important rules of caring for a Mogwai, and unleashes a horde of mischievous gremlins on a small town.",en,3912,7.1 817,Ocean's Twelve,"Danny Ocean reunites with his old flame and the rest of his merry band of thieves in carrying out three huge heists in Rome, Paris and Amsterdam – but a Europol agent is hot on their heels.",en,4602,6.5 818,To All the Boys: P.S. I Still Love You,Lara Jean and Peter have just taken their romance from pretend to officially real when another recipient of one of her love letters enters the picture.,en,1015,6.9 819,xXx: Return of Xander Cage,"Extreme athlete turned government operative Xander Cage comes out of self-imposed exile, thought to be long dead, and is set on a collision course with deadly alpha warrior Xiang and his team in a race to recover a sinister and seemingly unstoppable weapon known as Pandora's Box. Recruiting an all-new group of thrill-seeking cohorts, Xander finds himself enmeshed in a deadly conspiracy that points to collusion at the highest levels of world governments.",en,2751,5.6 820,On Her Majesty's Secret Service,"James Bond tracks his archnemesis, Ernst Blofeld, to a mountaintop retreat where he is training an army of beautiful, lethal women. Along the way, Bond falls for Italian contessa Tracy Draco, and marries her in order to get closer to Blofeld.",en,949,6.5 821,Apollo 13,"The true story of technical troubles that scuttle the Apollo 13 lunar mission in 1971, risking the lives of astronaut Jim Lovell and his crew, with the failed journey turning into a thrilling saga of heroism. Drifting more than 200,000 miles from Earth, the astronauts work furiously with the ground crew to avert tragedy.",en,3340,7.4 822,The Secret Life of Walter Mitty,A timid magazine photo manager who lives life vicariously through daydreams embarks on a true-life adventure when a negative goes missing.,en,5402,7.1 823,The Peanut Butter Falcon,A down-on-his-luck crab fisherman embarks on a journey to get a young man with Down syndrome to a professional wrestling school in rural North Carolina and away from the retirement home where he’s lived for the past two and a half years.,en,303,7.5 824,Long Shot,"Fred Flarsky is a gifted and free-spirited journalist who has a knack for getting into trouble. Charlotte Field is one of the most influential women in the world -- a smart, sophisticated and accomplished politician. When Fred unexpectedly runs into Charlotte, he soon realizes that she was his former baby sitter and childhood crush. When Charlotte decides to make a run for the presidency, she impulsively hires Fred as her speechwriter -- much to the dismay of her trusted advisers.",en,1064,6.6 825,Absolutely Anything,Eccentric aliens give a man the power to do anything he wants to determine if Earth is worth saving.,en,801,5.7 826,Star Trek: The Motion Picture,"When a destructive space entity is spotted approaching Earth, Admiral Kirk resumes command of the Starship Enterprise in order to intercept, examine, and hopefully stop it.",en,963,6.4 827,Sphere,"The OSSA discovers a spacecraft thought to be at least 300 years old at the bottom of the ocean. Immediately following the discovery, they decide to send a team down to the depths of the ocean to study the space craft. They are the best of best, smart and logical, and the perfect choice to learn more about the spacecraft.",en,918,6 828,Children of Men,"In 2027, in a chaotic world in which humans can no longer procreate, a former activist agrees to help transport a miraculously pregnant woman to a sanctuary at sea, where her child's birth may help scientists save the future of humankind.",en,4239,7.6 829,About Love,"Beautiful Nina lives happily married, as she saw it, with intelligent Alexandr, professor of Sinology. But the debt for the mortgage begins to disturb the relationship of spouses. One day she meets Sergey, the head of the bank where her husband has debt, and so begins their passionate relationships. The story of the relationship with married Sergey hardly promises happiness, but Nina realizes that for the first time she feels true love.",ru,6,6.2 830,Fractured,"Driving cross-country, Ray and his wife and daughter stop at a highway rest area where his daughter falls and breaks her arm. After a frantic rush to the hospital and a clash with the check-in nurse, Ray is finally able to get her to a doctor. While the wife and daughter go downstairs for an MRI, Ray, exhausted, passes out in a chair in the lobby. Upon waking up, they have no record or knowledge of Ray's family ever being checked in.",en,926,6.6 831,Playing It Cool,"The story of a young man disillusioned by love who meets a breathtaking young woman at a charity dinner by pretending to be a philanthropist. Turns out that she’s engaged to a guy who doesn’t like her going on dates. Challenged by the chase, and egged on by his eclectic friends, he feigns a platonic relationship in order to keep seeing her as he tries to conquer her heart",en,480,5.9 832,Before I Go to Sleep,"A woman wakes up every day, remembering nothing as a result of a traumatic accident in her past. One day, new terrifying truths emerge that force her to question everyone around her.",en,1256,6.5 833,Secret Society of Second Born Royals,It follows Sam's adventures at a top-secret training program for a new class of second-born royals tasked with saving the world.,en,0,0 834,The Truman Show,"Truman Burbank is the star of The Truman Show, a 24-hour-a-day reality TV show that broadcasts every aspect of his life without his knowledge. His entire life has been an unending soap opera for consumption by the rest of the world. And everyone he knows, including his wife and his best friend is really an actor, paid to be part of his life.",en,10955,8.1 835,I Am Number Four,"A teenage fugitive with an incredible secret races to stay one step ahead of the mysterious forces seeking destroy him in this sci-fi action thriller. With three dead and one on the run, the race to find the elusive Number Four begins. Outwardly normal teen John Smith never gets too comfortable in the same identity, and along with his guardian, Henri, he is constantly moving from town to town. With each passing day, John gains a stronger grasp on his extraordinary new powers, and his bond to the beings that share his fantastic fate grows stronger.",en,3199,6.1 836,Good Boys,A group of young boys on the cusp of becoming teenagers embark on an epic quest to fix their broken drone before their parents get home.,en,747,6.6 837,Night at the Museum: Battle of the Smithsonian,"Hapless museum night watchman Larry Daley must help his living, breathing exhibit friends out of a pickle now that they've been transferred to the archives at the Smithsonian Institution. Larry's (mis)adventures this time include close encounters with Amelia Earhart, Abe Lincoln and Ivan the Terrible.",en,4668,6.1 838,Sleeping Beauty,"A beautiful princess born in a faraway kingdom is destined by a terrible curse to prick her finger on the spindle of a spinning wheel and fall into a deep sleep that can only be awakened by true love's first kiss. Determined to protect her, her parents ask three fairies to raise her in hiding. But the evil Maleficent is just as determined to seal the princess's fate.",en,3379,6.9 839,Fighting with My Family,"Born into a tight-knit wrestling family, Paige and her brother Zak are ecstatic when they get the once-in-a-lifetime opportunity to try out for the WWE. But when only Paige earns a spot in the competitive training program, she must leave her loved ones behind and face this new cutthroat world alone. Paige's journey pushes her to dig deep and ultimately prove to the world that what makes her different is the very thing that can make her a star.",en,769,6.7 840,Black Snake Moan,"A God-fearing bluesman takes to a wild young woman who, as a victim of childhood sexual abuse, is looking everywhere for love, but never quite finding it.",en,471,6.8 841,Mars Attacks!,"'We come in peace' is not what those green men from Mars mean when they invade our planet, armed with irresistible weapons and a cruel sense of humor. This star studded cast must play victim to the alien’s fun and games in this comedy homage to science fiction films of the '50s and '60s.",en,3403,6.3 842,The Truth,"Fabienne is a star; a star of French cinema. She reigns amongst men who love and admire her. When she publishes her memoirs, her daughter Lumir returns from New York to Paris with her husband and young child. The reunion between mother and daughter will quickly turn to confrontation: truths will be told, accounts settled, loves and resentments confessed.",fr,65,6.2 843,The Mummy Returns,"Rick and Evelyn O’Connell, along with their 8-year-old son Alex, discover the key to the legendary Scorpion King’s might: the fabled Bracelet of Anubis. Unfortunately, a newly resurrected Imhotep has designs on the bracelet as well, and isn’t above kidnapping its new bearer, Alex, to gain control of Anubis’s otherworldly army.",en,4462,6.2 844,The Good Liar,"Career con man Roy sets his sights on his latest mark: recently widowed Betty, worth millions. And he means to take it all. But as the two draw closer, what should have been another simple swindle takes on the ultimate stakes.",en,265,6.7 845,Pet Sematary,"Dr. Louis Creed and his wife, Rachel, move from Boston to Ludlow, in rural Maine, with their two young children. Hidden in the woods near the new family home, Ellie, their eldest daughter, discovers a mysterious cemetery where the pets of community members are buried.",en,1846,5.8 846,Event Horizon,"In 2047 a group of astronauts are sent to investigate and salvage the starship 'Event Horizon' which disappeared mysteriously 7 years before on its maiden voyage. With its return, the crew of the 'Lewis and Clark' discovers the real truth behind the disappearance of the 'Event Horizon' – and something even more terrifying.",en,1433,6.5 847,Alice in Wonderland,"Alice, an unpretentious and individual 19-year-old, is betrothed to a dunce of an English nobleman. At her engagement party, she escapes the crowd to consider whether to go through with the marriage and falls down a hole in the garden after spotting an unusual rabbit. Arriving in a strange and surreal place called 'Underland,' she finds herself in a world that resembles the nightmares she had as a child, filled with talking animals, villainous queens and knights, and frumious bandersnatches. Alice realizes that she is there for a reason – to conquer the horrific Jabberwocky and restore the rightful queen to her throne.",en,9908,6.6 848,Barbie and the Diamond Castle,"Liana and Alexa (Barbie and Teresa) are best friends who share everything, including their love of singing. One day while walking through the forest home from the village, the girls meet an old beggar who gives them a magical mirror. As they clean the mirror and sing, a musical apprentice muse named Melody appears in the mirror's surface, and tells the girls about the secret of the Diamond Castle.",en,459,7.1 849,Official Secrets,The true story of British intelligence whistleblower Katharine Gun who—prior to the 2003 Iraq invasion—leaked a top-secret NSA memo exposing a joint US-UK illegal spying operation against members of the UN Security Council. The memo proposed blackmailing member states into voting for war.,en,180,7.2 850,Hot Bot,Hot Bot is the hilarious journey of two sexually repressed and unpopular teenage geeks who accidentally discover a life-like super-model sex bot (Bardot).,en,135,4.1 851,Left Behind,A small group of survivors are left behind after millions of people suddenly vanish during the rapture and the world is plunged into chaos and destruction.,en,851,4.1 852,Annihilation,"A biologist signs up for a dangerous, secret expedition into a mysterious zone where the laws of nature don't apply.",en,5366,6.3 853,The Man from U.N.C.L.E.,"At the height of the Cold War, a mysterious criminal organization plans to use nuclear weapons and technology to upset the fragile balance of power between the United States and Soviet Union. CIA agent Napoleon Solo and KGB agent Illya Kuryakin are forced to put aside their hostilities and work together to stop the evildoers in their tracks. The duo's only lead is the daughter of a missing German scientist, whom they must find soon to prevent a global catastrophe.",en,4002,7.1 854,Sicario: Day of the Soldado,Agent Matt Graver teams up with operative Alejandro Gillick to prevent Mexican drug cartels from smuggling terrorists across the United States border.,en,1711,6.7 855,Noah,A man who suffers visions of an apocalyptic deluge takes measures to protect his family from the coming flood.,en,4419,5.6 856,All Things Fair,This film is set in 1943 when the whole of Europe was embroiled in WWII. It deals with attraction of a 15 year old boy Stig to his teacher Viola. The whole movie revolves around the sexual encounters between Stig and Viola and how he eventually grows out of it.,sv,53,6.4 857,Darkest Hour,"A thrilling and inspiring true story begins on the eve of World War II as, within days of becoming Prime Minister of Great Britain, Winston Churchill must face one of his most turbulent and defining trials: exploring a negotiated peace treaty with Nazi Germany, or standing firm to fight for the ideals, liberty and freedom of a nation. As the unstoppable Nazi forces roll across Western Europe and the threat of invasion is imminent, and with an unprepared public, a skeptical King, and his own party plotting against him, Churchill must withstand his darkest hour, rally a nation, and attempt to change the course of world history.",en,3220,7.3 858,The Lion King 1½,Timon the meerkat and Pumbaa the warthog are best pals and the unsung heroes of the African savanna. This prequel to the smash Disney animated adventure takes you back -- way back -- before Simba's adventure began. You'll find out all about Timon and Pumbaa and tag along as they search for the perfect home and attempt to raise a rambunctious lion cub.,en,1936,6.5 859,The Expendables 2,"Mr. Church reunites the Expendables for what should be an easy paycheck, but when one of their men is murdered on the job, their quest for revenge puts them deep in enemy territory and up against an unexpected threat.",en,4670,6.2 860,Return of the Jedi,"Luke Skywalker leads a mission to rescue his friend Han Solo from the clutches of Jabba the Hutt, while the Emperor seeks to destroy the Rebellion once and for all with a second dreaded Death Star.",en,9292,8 861,Spy,"A desk-bound CIA analyst volunteers to go undercover to infiltrate the world of a deadly arms dealer, and prevent diabolical global disaster.",en,4292,6.8 862,The Human Centipede (First Sequence),"During a stopover in Germany in the middle of a carefree road trip through Europe, two American girls find themselves alone at night when their car breaks down in the woods. Searching for help at a nearby villa, they are wooed into the clutches of a deranged retired surgeon who explains his mad scientific vision to his captives' utter horror. They are to be the subjects of his sick lifetime fantasy: to be the first to connect people, one to the next, and in doing so bring to life ""the human centipede.""",en,1122,4.9 863,Dark Shadows,Vampire Barnabas Collins is inadvertently freed from his tomb and emerges into the very changed world of 1972. He returns to Collinwood Manor to find that his once-grand estate and family have fallen into ruin.,en,4849,5.9 864,Klaus,"When Jesper distinguishes himself as the Postal Academy's worst student, he is sent to Smeerensburg, a small village located on an icy island above the Arctic Circle, where grumpy inhabitants barely exchange words, let alone letters. Jesper is about to give up and abandon his duty as a postman when he meets local teacher Alva and Klaus, a mysterious carpenter who lives alone in a cabin full of handmade toys.",en,1536,8.3 865,Cars 3,"Blindsided by a new generation of blazing-fast racers, the legendary Lightning McQueen is suddenly pushed out of the sport he loves. To get back in the game, he will need the help of an eager young race technician with her own plan to win, inspiration from the late Fabulous Hudson Hornet, and a few unexpected turns. Proving that #95 isn't through yet will test the heart of a champion on Piston Cup Racing’s biggest stage!",en,3097,6.7 866,A Most Violent Year,"A thriller set in New York City during the winter of 1981, statistically one of the most violent years in the city's history, and centered on a the lives of an immigrant and his family trying to expand their business and capitalize on opportunities as the rampant violence, decay, and corruption of the day drag them in and threaten to destroy all they have built.",en,894,6.6 867,The Day the Earth Stood Still,An alien and a robot land on Earth after World War II and tell mankind to be peaceful or face destruction.,en,578,7.5 868,The Book of Life,"The journey of Manolo, a young man who is torn between fulfilling the expectations of his family and following his heart. Before choosing which path to follow, he embarks on an incredible adventure that spans three fantastical worlds where he must face his greatest fears.",en,1433,7.4 869,Untitled Spider-Man 3,"Sony Pictures Entertainment and The Walt Disney Company jointly announced that Marvel Studios and its President Kevin Feige will produce the third film in the ""Spider-Man: Homecoming"" series, starring Tom Holland. The film is scheduled to release on July 16, 2021. The third film will see Peter Parker deal with the aftermath of Quentin Beck outing him as Spider-man, but also to deal with a familiar old foe who has joined a group of ""Sinister Six"" villains.",en,0,0 870,"Batman: The Dark Knight Returns, Part 1","Batman has not been seen for ten years. A new breed of criminal ravages Gotham City, forcing 55-year-old Bruce Wayne back into the cape and cowl. But, does he still have what it takes to fight crime in a new era?",en,839,7.7 871,Gone with the Wind,"The spoiled daughter of a well-to-do plantation owner is forced to use every means at her disposal to claw her way out of poverty, following Maj. Gen. William Sherman's destructive ""March to the Sea,” during the American Civil War.",en,2117,7.9 872,Night at the Museum,"Chaos reigns at the natural history museum when night watchman Larry Daley accidentally stirs up an ancient curse, awakening Attila the Hun, an army of gladiators, a Tyrannosaurus rex and other exhibits.",en,6832,6.5 873,Hunter Killer,"Captain Glass of the USS Arkansas discovers that a coup d'état is taking place in Russia, so he and his crew join an elite group working on the ground to prevent a war.",en,967,6.4 874,Insidious: Chapter 3,"A twisted new tale of terror begins for a teenage girl and her family, and revealing more mysteries of the otherworldly realm, 'The Further'.",en,2128,6.3 875,Miss Peregrine's Home for Peculiar Children,A teenager finds himself transported to an island where he must help protect a group of orphans with special powers from creatures intent on destroying them.,en,6978,6.7 876,When Marnie Was There,"Upon being sent to live with relatives in the countryside due to an illness, an emotionally distant adolescent girl becomes obsessed with an abandoned mansion and infatuated with a girl who lives there - a girl who may or may not be real.",ja,806,7.9 877,The Fault in Our Stars,"Despite the tumor-shrinking medical miracle that has bought her a few years, Hazel has never been anything but terminal, her final chapter inscribed upon diagnosis. But when a patient named Augustus Waters suddenly appears at Cancer Kid Support Group, Hazel's story is about to be completely rewritten.",en,8218,7.6 878,American Wedding,"With high school a distant memory, Jim and Michelle are getting married -- and in a hurry, since Jim's grandmother is sick and wants to see him walk down the aisle -- prompting Stifler to throw the ultimate bachelor party. And Jim's dad is reliable as ever, doling out advice no one wants to hear.",en,2594,6.1 879,10 Things I Hate About You,"On the first day at his new school, Cameron instantly falls for Bianca, the gorgeous girl of his dreams. The only problem is that Bianca is forbidden to date until her ill-tempered, completely un-dateable older sister Kat goes out, too. In an attempt to solve his problem, Cameron singles out the only guy who could possibly be a match for Kat: a mysterious bad boy with a nasty reputation of his own.",en,4775,7.5 880,Poltergeist,"Legendary filmmaker Sam Raimi and director Gil Kenan reimagine and contemporize the classic tale about a family whose suburban home is invaded by angry spirits. When the terrifying apparitions escalate their attacks and take the youngest daughter, the family must come together to rescue her.",en,1634,5.1 881,Prisoners,"When Keller Dover's daughter and her friend go missing, he takes matters into his own hands as the police pursue multiple leads and the pressure mounts. But just how far will this desperate father go to protect his family?",en,6704,8 882,Sweeney Todd: The Demon Barber of Fleet Street,"The infamous story of Benjamin Barker, a.k.a Sweeney Todd, who sets up a barber shop down in London which is the basis for a sinister partnership with his fellow tenant, Mrs. Lovett. Based on the hit Broadway musical.",en,3749,7.1 883,Happy Death Day 2U,"Collegian Tree Gelbman wakes up in horror to learn that she's stuck in a parallel universe. Her boyfriend Carter is now with someone else, and her friends and fellow students seem to be completely different versions of themselves. When Tree discovers that Carter's roommate has been altering time, she finds herself once again the target of a masked killer. When the psychopath starts to go after her inner circle, Tree soon realizes that she must die over and over again to save everyone.",en,1728,6.2 884,True Grit,"The murder of her father sends a teenage tomboy on a mission of 'justice', which involves avenging her father's death. She recruits a tough old marshal, 'Rooster' Cogburn because he has 'true grit', and a reputation of getting the job done.",en,410,7.4 885,Ma,"Sue Ann is a loner who keeps to herself in her quiet Ohio town. One day, she is asked by Maggie, a new teenager in town, to buy some booze for her and her friends, and Sue Ann sees the chance to make some unsuspecting, if younger, friends of her own.",en,873,5.6 886,American Psycho,"A wealthy New York investment banking executive hides his alternate psychopathic ego from his co-workers and friends as he escalates deeper into his illogical, gratuitous fantasies.",en,5225,7.4 887,Stray Dolls,Riz is a recent South Asian immigrant who takes a job at a seedy motel in a bid to start over in America. The motel’s other employees and guests pull her back into a life she preferred to leave behind.,en,0,0 888,The Scorpion King,"In ancient Egypt, peasant Mathayus is hired to exact revenge on the powerful Memnon and the sorceress Cassandra, who are ready to overtake Balthazar's village. Amid betrayals, thieves, abductions and more, Mathayus strives to bring justice to his complicated world.",en,1937,5.4 889,Fantastic Four,"During a space voyage, four scientists are altered by cosmic rays: Reed Richards gains the ability to stretch his body; Sue Storm can become invisible; Johnny Storm controls fire; and Ben Grimm is turned into a super-strong … thing. Together, these ""Fantastic Four"" must now thwart the evil plans of Dr. Doom and save the world from certain destruction.",en,6376,5.7 890,King Arthur: Legend of the Sword,"When the child Arthur’s father is murdered, Vortigern, Arthur’s uncle, seizes the crown. Robbed of his birthright and with no idea who he truly is, Arthur comes up the hard way in the back alleys of the city. But once he pulls the sword Excalibur from the stone, his life is turned upside down and he is forced to acknowledge his true legacy... whether he likes it or not.",en,3806,6.5 891,Hard Target,"When a woman's father goes missing, she enlists a local to aid in her search. The pair soon discover that her father has died at the hands of a wealthy sportsman who hunts homeless men as a form of recreation.",en,510,6.2 892,Where Eagles Dare,"World War II is raging, and an American general has been captured and is being held hostage in the Schloss Adler, a Bavarian castle that's nearly impossible to breach. It's up to a group of skilled Allied soldiers to liberate the general before it's too late.",en,459,7.7 893,The Man Who Knew Infinity,"Growing up poor in Madras, India, Srinivasa Ramanujan Iyengar earns admittance to Cambridge University during WWI, where he becomes a pioneer in mathematical theories with the guidance of his professor, G.H. Hardy.",en,842,7.2 894,I Want You,"Sequel to ""Three Steps Above Heaven"". The sexy Gin is the new love of Hache, but this can not forget his former girlfriend, so the love triangle is inevitable.",es,795,6.9 895,Perfume: The Story of a Murderer,"Jean-Baptiste Grenouille, born in the stench of 18th century Paris, develops a superior olfactory sense, which he uses to create the world's finest perfumes. However, his work takes a dark turn as he tries to preserve scents in the search for the ultimate perfume.",en,2639,7.2 896,Gretel & Hansel,"A long time ago in a distant fairy tale countryside, a young girl leads her little brother into a dark wood in desperate search of food and work, only to stumble upon a nexus of terrifying evil.",en,84,5.7 897,The Spy Who Loved Me,Russian and British submarines with nuclear missiles on board both vanish from sight without a trace. England and Russia both blame each other as James Bond tries to solve the riddle of the disappearing ships. But the KGB also has an agent on the case.,en,986,6.7 898,The Aristocats,"When Madame Adelaide Bonfamille leaves her fortune to Duchess and her children—Bonfamille’s beloved family of cats—the butler plots to steal the money and kidnaps the legatees, leaving them out on a country road. All seems lost until the wily Thomas O’Malley Cat and his jazz-playing alley cats come to the aristocats’ rescue.",en,3263,7.3 899,Kung Fu Panda,"When the Valley of Peace is threatened, lazy Po the panda discovers his destiny as the ""chosen one"" and trains to become a kung fu hero, but transforming the unsleek slacker into a brave warrior won't be easy. It's up to Master Shifu and the Furious Five -- Tigress, Crane, Mantis, Viper and Monkey -- to give it a try.",en,7060,7.1 900,28 Weeks Later,"The inhabitants of the British Isles have lost their battle against the onslaught of disease, as the deadly rage virus has killed every citizen there. Six months later, a group of Americans dare to set foot on the isles, convinced the danger has come and gone. But it soon becomes all too clear that the scourge continues to live, waiting to pounce on its next victims.",en,2484,6.5 901,The Exorcist,"12-year-old Regan MacNeil begins to adapt an explicit new personality as strange events befall the local area of Georgetown. Her mother becomes torn between science and superstition in a desperate bid to save her daughter, and ultimately turns to her last hope: Father Damien Karras, a troubled priest who is struggling with his own faith.",en,4567,7.6 902,Rango,"When Rango, a lost family pet, accidentally winds up in the gritty, gun-slinging town of Dirt, the less-than-courageous lizard suddenly finds he stands out. Welcomed as the last hope the town has been waiting for, new Sheriff Rango is forced to play his new role to the hilt.",en,3917,6.6 903,District 9,"Thirty years ago, aliens arrive on Earth. Not to conquer or give aid, but to find refuge from their dying planet. Separated from humans in a South African area called District 9, the aliens are managed by Multi-National United, which is unconcerned with the aliens' welfare but will do anything to master their advanced technology. When a company field agent contracts a mysterious virus that begins to alter his DNA, there is only one place he can hide: District 9.",en,6144,7.4 904,The Adventures of Tintin,"Intrepid young reporter, Tintin, and his loyal dog, Snowy, are thrust into a world of high adventure when they discover a ship carrying an explosive secret. As Tintin is drawn into a centuries-old mystery, Ivan Ivanovitch Sakharine suspects him of stealing a priceless treasure. Tintin and Snowy, with the help of salty, cantankerous Captain Haddock and bumbling detectives, Thompson and Thomson, travel half the world, one step ahead of their enemies, as Tintin endeavors to find the Unicorn, a sunken ship that may hold a vast fortune, but also an ancient curse.",en,3657,6.8 905,The Living Daylights,James Bond helps a Russian General escape into the west. He soon finds out that the KGB wants to kill him for helping the General. A little while later the General is kidnapped from the Secret Service leading 007 to be suspicious.,en,886,6.4 906,Independence Day: Resurgence,"We always knew they were coming back. Using recovered alien technology, the nations of Earth have collaborated on an immense defense program to protect the planet. But nothing can prepare us for the aliens’ advanced and unprecedented force. Only the ingenuity of a few brave men and women can bring our world back from the brink of extinction.",en,4267,5.1 907,Meet Joe Black,"When the grim reaper comes to collect the soul of megamogul Bill Parrish, he arrives with a proposition: Host him for a ""vacation"" among the living in trade for a few more days of existence. Parrish agrees, and using the pseudonym Joe Black, Death begins taking part in Parrish's daily agenda and falls in love with the man's daughter. Yet when Black's holiday is over, so is Parrish's life.",en,2684,7.1 908,Saw VI,"Special Agent Strahm is dead, and Detective Hoffman has emerged as the unchallenged successor to Jigsaw's legacy. However, when the FBI draws closer to Hoffman, he is forced to set a game into motion, and Jigsaw's grand scheme is finally understood.",en,1816,6.2 909,Addams Family Values,"Siblings Wednesday and Pugsley Addams will stop at nothing to get rid of Pubert, the new baby boy adored by parents Gomez and Morticia. Things go from bad to worse when the new ""black widow"" nanny, Debbie Jellinsky, launches her plan to add Fester to her collection of dead husbands.",en,1518,6.7 910,Ghostbusters,"Following a ghost invasion of Manhattan, paranormal enthusiasts Erin Gilbert and Abby Yates, nuclear engineer Jillian Holtzmann, and subway worker Patty Tolan band together to stop the otherworldly threat.",en,4266,5.4 911,Diamonds Are Forever,Diamonds are stolen only to be sold again in the international market. James Bond infiltrates a smuggling mission to find out who's guilty. The mission takes him to Las Vegas where Bond meets his archenemy Blofeld.,en,1095,6.4 912,The Butterfly Effect,"A young man struggles to access sublimated childhood memories. He finds a technique that allows him to travel back into the past, to occupy his childhood body and change history. However, he soon finds that every change he makes has unexpected consequences.",en,4478,7.5 913,Nocturnal Animals,"Susan Morrow receives a book manuscript from her ex-husband – a man she left 20 years earlier – asking for her opinion of his writing. As she reads, she is drawn into the fictional life of Tony Hastings, a mathematics professor whose family vacation turns violent.",en,4979,7.4 914,Oldboy,"With no clue how he came to be imprisoned, drugged and tortured for 15 years, a desperate businessman seeks revenge on his captors.",ko,4542,8.2 915,I Still See You,"A spellbinding and romantic supernatural thriller. Ten years after an apocalyptic event left the world haunted by ghosts, Roni receives a threatening message from beyond the grave. Joining forces with a mysterious classmate, Kirk, Roni descends into a shadow world that blurs the bounds of the living and the dead-and begins a desperate race against time to stop a cunning killer.",en,335,6.7 916,Halloween,"Fifteen years after murdering his sister on Halloween Night 1963, Michael Myers escapes from a mental hospital and returns to the small town of Haddonfield, Illinois to kill again.",en,2844,7.5 917,Dragon Ball Z: Battle of Gods,"The events of Battle of Gods take place some years after the battle with Majin Buu, which determined the fate of the entire universe. After awakening from a long slumber, Beerus, the God of Destruction is visited by Whis, his attendant and learns that the galactic overlord Frieza has been defeated by a Super Saiyan from the North Quadrant of the universe named Goku, who is also a former student of the North Kai. Ecstatic over the new challenge, Goku ignores King Kai's advice and battles Beerus, but he is easily overwhelmed and defeated. Beerus leaves, but his eerie remark of ""Is there nobody on Earth more worthy to destroy?"" lingers on. Now it is up to the heroes to stop the God of Destruction before all is lost.",ja,842,6.6 918,Watchmen,"In a gritty and alternate 1985 the glory days of costumed vigilantes have been brought to a close by a government crackdown, but after one of the masked veterans is brutally murdered, an investigation into the killer is initiated. The reunited heroes set out to prevent their own destruction, but in doing so uncover a sinister plot that puts all of humanity in grave danger.",en,5795,7.3 919,Hancock,"Hancock is a down-and-out superhero who's forced to employ a PR expert to help repair his image when the public grows weary of all the damage he's inflicted during his lifesaving heroics. The agent's idea of imprisoning the antihero to make the world miss him proves successful, but will Hancock stick to his new sense of purpose or slip back into old habits?",en,6241,6.3 920,The Dark Tower,"The last Gunslinger, Roland Deschain, has been locked in an eternal battle with Walter O’Dim, also known as the Man in Black, determined to prevent him from toppling the Dark Tower, which holds the universe together. With the fate of the worlds at stake, good and evil will collide in the ultimate battle as only Roland can defend the Tower from the Man in Black.",en,3328,5.6 921,Nymphomaniac: Vol. II,The continuation of Joe's sexually dictated life delves into the darker aspects of her adult life and what led to her being in Seligman's care.,en,1813,6.8 922,Murder Mystery,"After attending a gathering on a billionaire's yacht during a European vacation, a New York cop and his wife become prime suspects when he's murdered.",en,2004,6.3 923,Addicted,A gallerist risks her family and flourishing career when she enters into an affair with a talented painter and slowly loses control of her life.,en,136,5.3 924,Fantastic Four,"Four young outsiders teleport to a dangerous universe, which alters their physical form in shocking ways. Their lives irrevocably upended, the team must learn to harness their daunting new abilities and work together to save Earth from a former friend turned enemy.",en,4228,4.4 925,Pearl Harbor,"The lifelong friendship between Rafe McCawley and Danny Walker is put to the ultimate test when the two ace fighter pilots become entangled in a love triangle with beautiful Naval nurse Evelyn Johnson. But the rivalry between the friends-turned-foes is immediately put on hold when they find themselves at the center of Japan's devastating attack on Pearl Harbor on Dec. 7, 1941.",en,3961,6.8 926,Alice Through the Looking Glass,"In the sequel to Tim Burton's ""Alice in Wonderland"", Alice Kingsleigh returns to Underland and faces a new adventure in saving the Mad Hatter.",en,4438,6.5 927,Begin Again,A long-lost music producer finds a singer-songwriter right about the moment he has given up all hope on life.,en,2366,7.2 928,Hansel & Gretel: Witch Hunters,"After getting a taste for blood as children, Hansel and Gretel have become the ultimate vigilantes, hell-bent on retribution. Now, unbeknownst to them, Hansel and Gretel have become the hunted, and must face an evil far greater than witches... their past.",en,4925,5.9 929,A Walk to Remember,"When the popular, restless Landon Carter is forced to participate in the school drama production he falls in love with Jamie Sullivan, the daughter of the town's minister. Jamie has a ""to-do"" list for her life and also a very big secret she must keep from Landon.",en,2499,7.6 930,Species,"In 1993, the Search for Extra Terrestrial Intelligence Project receives a transmission detailing an alien DNA structure, along with instructions on how to splice it with human DNA. The result is Sil, a sensual but deadly creature who can change from a beautiful woman to an armour-plated killing machine in the blink of an eye.",en,835,5.7 931,The Crow,"Exactly one year after young rock guitarist Eric Draven and his fiancée are brutally killed by a ruthless gang of criminals, Draven -- watched over by a hypnotic crow -- returns from the grave to exact revenge.",en,2171,7.5 932,mother!,"A couple's relationship is tested when uninvited guests arrive at their home, disrupting their tranquil existence.",en,4224,7 933,The Intouchables,A true story of two men who should never have met – a quadriplegic aristocrat who was injured in a paragliding accident and a young man from the projects.,fr,11290,8.2 934,Monsters University,A look at the relationship between Mike and Sulley during their days at Monsters University — when they weren't necessarily the best of friends.,en,7175,7 935,Inside Man,"When an armed, masked gang enter a Manhattan bank, lock the doors and take hostages, the detective assigned to effect their release enters negotiations preoccupied with corruption charges he is facing.",en,3388,7.4 936,Blade II,"A rare mutation has occurred within the vampire community - The Reaper. A vampire so consumed with an insatiable bloodlust that they prey on vampires as well as humans, transforming victims who are unlucky enough to survive into Reapers themselves. Blade is asked by the Vampire Nation for his help in preventing a nightmare plague that would wipe out both humans and vampires.",en,2910,6.4 937,Code 8,"In Lincoln City, some inhabitants have extraordinary abilities. Most live below the poverty line, under the close surveillance of a heavily militarized police force. Connor, a construction worker with powers, involves with a criminal gang to help his ailing mother. (Based on the short film “Code 8,” 2016.)",en,378,6.2 938,21 Jump Street,"In high school, Schmidt was a dork and Jenko was the popular jock. After graduation, both of them joined the police force and ended up as partners riding bicycles in the city park. Since they are young and look like high school students, they are assigned to an undercover unit to infiltrate a drug ring that is supplying high school students synthetic drugs.",en,7289,6.8 939,Rocky IV,"After iron man Drago, a highly intimidating 6-foot-5, 261-pound Soviet athlete, kills Apollo Creed in an exhibition match, Rocky comes to the heart of Russia for 15 pile-driving boxing rounds of revenge.",en,2430,6.9 940,Red Dwarf: The Promised Land,"The posse meet three cat clerics who worship Lister as their God. Lister vows to help them as they're being hunted by Rodon, the ruthless feral cat leader who has vowed to wipe out all cats who worship anyone but him.",en,0,0 941,12 Years a Slave,"In the pre-Civil War United States, Solomon Northup, a free black man from upstate New York, is abducted and sold into slavery. Facing cruelty as well as unexpected kindnesses Solomon struggles not only to stay alive, but to retain his dignity. In the twelfth year of his unforgettable odyssey, Solomon’s chance meeting with a Canadian abolitionist will forever alter his life.",en,7654,7.9 942,300,"Based on Frank Miller's graphic novel, ""300"" is very loosely based the 480 B.C. Battle of Thermopylae, where the King of Sparta led his army against the advancing Persians; the battle is said to have inspired all of Greece to band together against the Persians, and helped usher in the world's first democracy.",en,9190,7.1 943,A Simple Favor,"Stephanie, a dedicated mother and popular vlogger, befriends Emily, a mysterious upper-class woman whose son Nicky attends the same school as Miles, Stephanie's son. When Emily asks her to pick Nicky up from school and then disappears, Stephanie undertakes an investigation that will dive deep into Emily's cloudy past.",en,2462,6.6 944,Fate/Stay Night: Heaven's Feel III. Spring Song,"Theatrical-release adaptation of the visual novel ""Fate/stay night"", following the third and final route. (Final part of a trilogy.)",ja,0,0 945,Black Christmas,"Hawthorne College is winding down for the holidays, yet one by one, sorority girls are being picked off. Riley Stone, a girl dealing with her own trauma, begins to notice and tries to save her friends before they too are picked off.",en,154,4.4 946,The Prince of Egypt,"This is the extraordinary tale of two brothers named Moses and Ramses, one born of royal blood, and one an orphan with a secret past. Growing up the best of friends, they share a strong bond of free-spirited youth and good-natured rivalry. But the truth will ultimately set them at odds, as one becomes the ruler of the most powerful empire on earth, and the other the chosen leader of his people! Their final confrontation will forever change their lives and the world.",en,2304,7.1 947,Dredd,"In the future, America is a dystopian wasteland. The latest scourge is Ma-Ma, a prostitute-turned-drug pusher with a dangerous new drug and aims to take over the city. The only possibility of stopping her is an elite group of urban police called Judges, who combine the duties of judge, jury and executioner to deliver a brutal brand of swift justice. But even the top-ranking Judge, Dredd, discovers that taking down Ma-Ma isn’t as easy as it seems in this explosive adaptation of the hugely popular comic series.",en,3179,6.7 948,Baywatch,"Devoted lifeguard Mitch Buchannon butts heads with a brash new recruit. Together, they uncover a local criminal plot that threatens the future of the Bay.",en,5512,6.1 949,Overboard,"Heiress, Joanna Stayton hires carpenter, Dean Proffitt to build a closet on her yacht -- and refuses to pay him for the project when it's done. But after Joanna accidentally falls overboard and loses her memory, Dean sees an opportunity to get even.",en,469,6.8 950,Dragon Ball Z: Broly – The Legendary Super Saiyan,"As Goku investigates the destruction of the Southern Galaxy, Vegeta is taken to be King of the New Planet Vegeta, and to destroy the Legendary Super Saiyan, Broly.",ja,442,7.2 951,Fatherhood,A father brings up his baby girl as a single dad after the unexpected death of his wife who died a day after their daughter's birth.,en,0,0 952,Good Will Hunting,"Will Hunting has a genius-level IQ but chooses to work as a janitor at MIT. When he solves a difficult graduate-level math problem, his talents are discovered by Professor Gerald Lambeau, who decides to help the misguided youth reach his potential. When Will is arrested for attacking a police officer, Professor Lambeau makes a deal to get leniency for him if he will get treatment from therapist Sean Maguire.",en,7033,8.1 953,American History X,"Derek Vineyard is paroled after serving 3 years in prison for killing two thugs who tried to break into/steal his truck. Through his brother, Danny Vineyard's narration, we learn that before going to prison, Derek was a skinhead and the leader of a violent white supremacist gang that committed acts of racial crime throughout L.A. and his actions greatly influenced Danny. Reformed and fresh out of prison, Derek severs contact with the gang and becomes determined to keep Danny from going down the same violent path as he did.",en,7077,8.4 954,The Angry Birds Movie,"An island populated entirely by happy, flightless birds or almost entirely. In this paradise, Red, a bird with a temper problem, speedy Chuck, and the volatile Bomb have always been outsiders. But when the island is visited by mysterious green piggies, it’s up to these unlikely outcasts to figure out what the pigs are up to.",en,2196,6.1 955,Insidious: Chapter 2,The haunted Lambert family seeks to uncover the mysterious childhood secret that has left them dangerously connected to the spirit world.,en,2608,6.6 956,Taken,"While vacationing with a friend in Paris, an American girl is kidnapped by a gang of human traffickers intent on selling her into forced prostitution. Working against the clock, her ex-spy father must pull out all the stops to save her. But with his best years possibly behind him, the job may be more than he can handle.",en,7592,7.3 957,Black Swan,A journey through the psyche of a young ballerina whose starring role as the duplicitous swan queen turns out to be a part for which she becomes frighteningly perfect.,en,9537,7.6 958,Lost in Translation,"Two lost souls visiting Tokyo -- the young, neglected wife of a photographer and a washed-up movie star shooting a TV commercial -- find an odd solace and pensive freedom to be real in each other's company, away from their lives in America.",en,4124,7.4 959,Mirror Mirror,"After she spends all her money, an evil enchantress queen schemes to marry a handsome, wealthy prince. There's just one problem - he's in love with a beautiful princess, Snow White. Now, joined by seven rebellious dwarves, Snow White launches an epic battle of good vs. evil...",en,2395,5.9 960,Dances with Wolves,"Wounded Civil War soldier, John Dunbar tries to commit suicide—and becomes a hero instead. As a reward, he's assigned to his dream post, a remote junction on the Western frontier, and soon makes unlikely friends with the local Sioux tribe.",en,2375,7.8 961,Snowpiercer,"In a future where a failed global-warming experiment kills off most life on the planet, a class system evolves aboard the Snowpiercer, a train that travels around the globe via a perpetual-motion engine.",ko,5715,6.8 962,Pain and Glory,"Salvador Mallo, a filmmaker in the twilight of his career, remembers his life: his mother, his lovers, the actors he worked with. The sixties in a small village in Valencia, the eighties in Madrid, the present, when he feels an immeasurable emptiness, facing his mortality, the incapability of continuing filming, the impossibility of separating creation from his own life. The need of narrating his past can be his salvation.",es,854,7.5 963,Flushed Away,"London high-society mouse, Roddy is flushed down the toilet by Sid, a common sewer rat. Hang on for a madcap adventure deep in the sewer bowels of Ratropolis, where Roddy meets the resourceful Rita, the rodent-hating Toad and his faithful thugs, Spike and Whitey.",en,2309,6.1 964,Dial M for Murder,"An ex-tennis pro carries out a plot to have his wife murdered after discovering she is having an affair, and assumes she will soon leave him for the other man anyway. When things go wrong, he improvises a new plan—to frame her for murder instead.",en,1338,8 965,Mulan II,"Fa Mulan gets the surprise of her young life when her love, Captain Li Shang asks for her hand in marriage. Before the two can have their happily ever after, the Emperor assigns them a secret mission, to escort three princesses to Chang'an, China. Mushu is determined to drive a wedge between the couple after he learns that he will lose his guardian job if Mulan marries into the Li family.",en,1315,6.3 966,Lord of War,"Yuri Orlov is a globetrotting arms dealer and, through some of the deadliest war zones, he struggles to stay one step ahead of a relentless Interpol agent, his business rivals and even some of his customers who include many of the world’s most notorious dictators. Finally, he must also face his own conscience.",en,2654,7.2 967,A Cure for Wellness,"An ambitious young executive is sent to retrieve his company's CEO from an idyllic but mysterious ""wellness center"" at a remote location in the Swiss Alps but soon suspects that the spa's miraculous treatments are not what they seem.",en,2439,6.2 968,Inherent Vice,"In Los Angeles at the turn of the 1970s, drug-fueled detective Larry ""Doc"" Sportello investigates the disappearance of an ex-girlfriend.",en,1615,6.6 969,Only the Brave,Members of the Granite Mountain Hotshots battle deadly wildfires to save an Arizona town.,en,747,7.1 970,Capernaum,"Zain, a 12-year-old boy scrambling to survive on the streets of Beirut, sues his parents for having brought him into such an unjust world, where being a refugee with no documents means that your rights can easily be denied.",ar,666,8.2 971,Cheeky,"While scouting out apartments in London for her Venetian boyfriend, Carla rents an apartment that overlooks the Thames. There she meet the lesbian hyper-horny real estate agent Moira.",it,100,5.6 972,The Main Event,"After discovering a magical mask, an 11-year-old aspiring wrestler enters a competition to become the next WWE superstar by using special powers from a magical mask.",en,0,0 973,Allied,"In 1942, an intelligence officer in North Africa encounters a female French Resistance fighter on a deadly mission behind enemy lines. When they reunite in London, their relationship is tested by the pressures of war.",en,3173,6.7 974,The Island,"In 2019, Lincoln Six-Echo is a resident of a seemingly ""Utopian"" but contained facility. Like all of the inhabitants of this carefully-controlled environment, Lincoln hopes to be chosen to go to The Island — reportedly the last uncontaminated location on the planet. But Lincoln soon discovers that everything about his existence is a lie.",en,3459,6.6 975,Big Fish,"Throughout his life Edward Bloom has always been a man of big appetites, enormous passions and tall tales. In his later years, he remains a huge mystery to his son, William. Now, to get to know the real man, Will begins piecing together a true picture of his father from flashbacks of his amazing adventures.",en,4535,7.8 976,"Batman: The Dark Knight Returns, Part 2",Batman has stopped the reign of terror that The Mutants had cast upon his city. Now an old foe wants a reunion and the government wants The Man of Steel to put a stop to Batman.,en,815,7.9 977,Blow,"A boy named George Jung grows up in a struggling family in the 1950's. His mother nags at her husband as he is trying to make a living for the family. It is finally revealed that George's father cannot make a living and the family goes bankrupt. George does not want the same thing to happen to him, and his friend Tuna, in the 1960's, suggests that he deal marijuana. He is a big hit in California in the 1960's, yet he goes to jail, where he finds out about the wonders of cocaine. As a result, when released, he gets rich by bringing cocaine to America. However, he soon pays the price.",en,2742,7.4 978,Planet of the Apes,"After a spectacular crash-landing on an uncharted planet, brash astronaut Leo Davidson finds himself trapped in a savage world where talking apes dominate the human race. Desperate to find a way home, Leo must evade the invincible gorilla army led by Ruthless General Thade.",en,2487,5.7 979,The Favourite,"England, early 18th century. The close relationship between Queen Anne and Sarah Churchill is threatened by the arrival of Sarah's cousin, Abigail Hill, resulting in a bitter rivalry between the two cousins to be the Queen's favourite.",en,3096,7.6 980,Catch Me If You Can,"A true story about Frank Abagnale Jr. who, before his 19th birthday, successfully conned millions of dollars worth of checks as a Pan Am pilot, doctor, and legal prosecutor. An FBI agent makes it his mission to put him behind bars. But Frank not only eludes capture, he revels in the pursuit.",en,8784,7.9 981,How It Ends,A desperate father tries to return home to his pregnant wife after a mysterious apocalyptic event turns everything to chaos.,en,1188,5.2 982,The Mortal Instruments: City of Bones,"In New York City, Clary Fray, a seemingly ordinary teenager, learns that she is descended from a line of Shadowhunters — half-angel warriors who protect humanity from evil forces. After her mother disappears, Clary joins forces with a group of Shadowhunters and enters Downworld, an alternate realm filled with demons, vampires, and a host of other creatures. Clary and her companions must find and protect an ancient cup that holds the key to her mother's future.",en,3298,6.3 983,The Nutcracker and the Four Realms,"A young girl is transported into a magical world of gingerbread soldiers and an army of mice. In Disney’s magical take on the classic The Nutcracker, Clara wants a one-of-a-kind key that will unlock a box holding a priceless gift. A golden thread presented at her godfather’s holiday party leads her to the coveted key—which promptly disappears into a strange and mysterious parallel world. There Clara encounters a soldier, a gang of mice and the regents of three magical Realms. But she must brave the ominous Fourth Realm, home to the tyrant Mother Ginger, to retrieve her key and return harmony to the unstable world.",en,1357,6 984,The Chronicles of Narnia: Prince Caspian,"One year after their incredible adventures in the Lion, the Witch and the Wardrobe, Peter, Edmund, Lucy and Susan Pevensie return to Narnia to aid a young prince whose life has been threatened by the evil King Miraz. Now, with the help of a colorful cast of new characters, including Trufflehunter the badger and Nikabrik the dwarf, the Pevensie clan embarks on an incredible quest to ensure that Narnia is returned to its rightful heir.",en,4126,6.6 985,Annabelle: Creation,"Several years after the tragic death of their little girl, a doll maker and his wife welcome a nun and several girls from a shuttered orphanage into their home, soon becoming the target of the doll maker's possessed creation—Annabelle.",en,3684,6.5 986,Superman,"Mild-mannered Clark Kent works as a reporter at the Daily Planet alongside his crush, Lois Lane. Clark must summon his superhero alter-ego when the nefarious Lex Luthor launches a plan to take over the world.",en,2175,7.1 987,Ghost Rider: Spirit of Vengeance,"When the devil resurfaces with aims to take over the world in human form, Johnny Blaze reluctantly comes out of hiding to transform into the flame-spewing supernatural hero Ghost Rider -- and rescue a 10-year-old boy from an unsavory end.",en,2307,4.8 988,Cloudy with a Chance of Meatballs,"Inventor Flint Lockwood creates a machine that makes clouds rain food, enabling the down-and-out citizens of Chewandswallow to feed themselves. But when the falling food reaches gargantuan proportions, Flint must scramble to avert disaster. Can he regain control of the machine and put an end to the wild weather before the town is destroyed?",en,3800,6.5 989,The Loft,"For five men, the opportunity to share a penthouse in the city -- in which to carry on extramarital affairs -- is a dream come true, until the dead body of an unknown woman turns up. Realizing that her killer must be one of their group, the men are gripped by paranoia as each one suspects another. Friendships are tested, loyalties are questioned, and marriages crumble while fear and suspicion run rampant.",en,873,6.4 990,The Lego Movie 2: The Second Part,"It's been five years since everything was awesome and the citizens are facing a huge new threat: LEGO DUPLO® invaders from outer space, wrecking everything faster than they can rebuild.",en,1015,6.6 991,Reservoir Dogs,"A botched robbery indicates a police informant, and the pressure mounts in the aftermath at a warehouse. Crime begets violence as the survivors -- veteran Mr. White, newcomer Mr. Orange, psychopathic parolee Mr. Blonde, bickering weasel Mr. Pink and Nice Guy Eddie -- unravel.",en,8794,8.2 992,A View to a Kill,A newly developed microchip designed by Zorin Industries for the British Government that can survive the electromagnetic radiation caused by a nuclear explosion has landed in the hands of the KGB. James Bond must find out how and why. His suspicions soon lead him to big industry leader Max Zorin.,en,995,6.2 993,Halloween,"Laurie Strode comes to her final confrontation with Michael Myers, the masked figure who has haunted her since she narrowly escaped his killing spree on Halloween night four decades ago.",en,2605,6.4 994,Judy,"Winter 1968 and showbiz legend Judy Garland arrives in Swinging London to perform a five-week sold-out run at The Talk of the Town. It is 30 years since she shot to global stardom in The Wizard of Oz, but if her voice has weakened, its dramatic intensity has only grown. As she prepares for the show, battles with management, charms musicians and reminisces with friends and adoring fans, her wit and warmth shine through. Even her dreams of love seem undimmed as she embarks on a whirlwind romance with Mickey Deans, her soon-to-be fifth husband.",en,394,6.9 995,You Only Live Twice,A mysterious spacecraft captures Russian and American space capsules and brings the two superpowers to the brink of war. James Bond investigates the case in Japan and comes face to face with his archenemy Blofeld.,en,1097,6.6 996,Dead Poets Society,"At an elite, old-fashioned boarding school in New England, a passionate English teacher inspires his students to rebel against convention and seize the potential of every day, courting the disdain of the stern headmaster.",en,6854,8.3 997,Ratsasan,"A serial killer is murdering school girls, and a newbie cop has to track him down before the victim count increases.",ta,47,8 998,Vivre sans eux,,fr,0,0 999,Scooby-Doo 2: Monsters Unleashed,"When Mystery, Inc. are guests of honor at the grand opening of the Coolsville Museum of Criminology, a masked villain shows up and creates havoc before stealing the costumes of the gang's most notorious villains...Could it be that their nemesis, mad scientist Jonathan Jacobo has returned and is trying to recreate their deadliest foes?",en,1521,5.7 1000,Something Something… Unakkum Enakkum,"The story starts in a jail, where prisoner Muthupandi (Prabhu) is being interrogated by a police officer (Vijayakumar) about his past. Without hesitation, Muthupandi tells. Muthupandi is a son of the soil, a very hard working self-made man who dotes on his only sister Kavitha (Trisha Krishnan). Their mother died very early and it was Muthupandi who brought up Kavitha with a lot of love and affection. Kavitha’s best friend Lalitha (Richa Pallod), who is getting married, takes Kavitha to her house for the nuptial ceremony. As an emotional scene prior to this notes, this is the first time Kavitha has been away from her brother.",ta,16,6.6 ================================================ FILE: bin/single-node/examples/datasets/popular-movies-of-imdb/description.txt ================================================ This dataset is from Kaggle at https://www.kaggle.com/datasets/sankha1998/tmdb-top-10000-popular-movies-dataset ================================================ FILE: bin/single-node/examples/load-examples.sh ================================================ #!/bin/bash # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Unified script to load example datasets and workflows into Texera. # This script is designed to run inside a Docker container as a one-shot job. set -euo pipefail # Configuration from environment variables with defaults TEXERA_DASHBOARD_SERVICE_URL=${TEXERA_DASHBOARD_SERVICE_URL:-"http://dashboard-service:8080/api"} TEXERA_FILE_SERVICE_URL=${TEXERA_FILE_SERVICE_URL:-"http://file-service:9092/api"} USERNAME=${TEXERA_EXAMPLE_USERNAME:-"texera"} PASSWORD=${TEXERA_EXAMPLE_PASSWORD:-"texera"} # In Texera, registration sets email = username OWNER_EMAIL="$USERNAME" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" DATASET_DIR="$SCRIPT_DIR/datasets" WORKFLOW_DIR="$SCRIPT_DIR/workflows" MAX_RETRIES=60 RETRY_INTERVAL=5 # Color codes for output GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[0;33m' NC='\033[0m' print_status() { echo -e "${GREEN}[INFO]${NC} $1" } print_error() { echo -e "${RED}[ERROR]${NC} $1" } print_warn() { echo -e "${YELLOW}[WARN]${NC} $1" } # Wait for a service to become healthy wait_for_service() { local service_name="$1" local health_url="$2" local retries=0 print_status "Waiting for $service_name to be ready..." while [ $retries -lt $MAX_RETRIES ]; do HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$health_url" 2>/dev/null || echo "000") if [ "$HTTP_CODE" = "200" ]; then print_status "$service_name is healthy!" return 0 fi retries=$((retries + 1)) print_status "Waiting for $service_name... (attempt $retries/$MAX_RETRIES, status: $HTTP_CODE)" sleep $RETRY_INTERVAL done print_error "$service_name did not become healthy after $MAX_RETRIES attempts" return 1 } # Authenticate and obtain JWT token authenticate() { print_status "Logging in as $USERNAME..." LOGIN_RESPONSE=$(curl -s -X POST "$TEXERA_DASHBOARD_SERVICE_URL/auth/login" \ -H "Content-Type: application/json" \ -d "{\"username\": \"$USERNAME\", \"password\": \"$PASSWORD\"}") if echo "$LOGIN_RESPONSE" | grep -q '"accessToken"'; then TOKEN=$(echo "$LOGIN_RESPONSE" | grep -o '"accessToken":"[^"]*' | cut -d'"' -f4) print_status "Login successful" return 0 fi print_status "User doesn't exist, attempting to register..." REGISTER_RESPONSE=$(curl -s -X POST "$TEXERA_DASHBOARD_SERVICE_URL/auth/register" \ -H "Content-Type: application/json" \ -d "{\"username\": \"$USERNAME\", \"password\": \"$PASSWORD\"}") if echo "$REGISTER_RESPONSE" | grep -q '"accessToken"'; then TOKEN=$(echo "$REGISTER_RESPONSE" | grep -o '"accessToken":"[^"]*' | cut -d'"' -f4) print_status "Registration successful" return 0 fi print_error "Authentication failed" return 1 } # Load all datasets from the datasets directory load_datasets() { if [ ! -d "$DATASET_DIR" ]; then print_warn "Dataset directory '$DATASET_DIR' not found, skipping datasets" return 0 fi # Get list of existing datasets LIST_RESPONSE=$(curl -s -X GET "$TEXERA_FILE_SERVICE_URL/dataset/list" \ -H "Authorization: Bearer $TOKEN") for dataset_folder in "$DATASET_DIR"/*/; do [ -d "$dataset_folder" ] || continue DATASET_NAME=$(basename "$dataset_folder") print_status "Processing dataset: $DATASET_NAME" # Check if dataset already exists if echo "$LIST_RESPONSE" | grep -q "\"name\":\"$DATASET_NAME\""; then print_status "Dataset '$DATASET_NAME' already exists, skipping" continue fi # Read description.txt if available DATASET_DESCRIPTION="" if [ -f "$dataset_folder/description.txt" ]; then DATASET_DESCRIPTION=$(<"$dataset_folder/description.txt") fi # Create dataset CREATE_RESPONSE=$(curl -s -X POST "$TEXERA_FILE_SERVICE_URL/dataset/create" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d "{ \"datasetName\": \"$DATASET_NAME\", \"datasetDescription\": \"$DATASET_DESCRIPTION\", \"isDatasetPublic\": true }") DATASET_ID=$(echo "$CREATE_RESPONSE" | grep -o '"did":[0-9]*' | cut -d':' -f2) if [ -z "$DATASET_ID" ]; then print_error "Failed to create dataset '$DATASET_NAME'" continue fi print_status "Created dataset '$DATASET_NAME' with ID $DATASET_ID" # Upload files using the multipart upload API (avoids presigned URL issues in Docker) for file in "$dataset_folder"*; do [ -f "$file" ] || continue FILENAME=$(basename "$file") [ "$FILENAME" = "description.txt" ] && continue print_status "Uploading file: $FILENAME" ENCODED_NAME=$(echo "$FILENAME" | sed 's/ /%20/g') FILE_SIZE=$(wc -c < "$file" | tr -d ' ') # Step 1: Initialize multipart upload (single part for small files) INIT_RESPONSE=$(curl -s -X POST \ "$TEXERA_FILE_SERVICE_URL/dataset/multipart-upload?type=init&ownerEmail=$OWNER_EMAIL&datasetName=$DATASET_NAME&filePath=$ENCODED_NAME&fileSizeBytes=$FILE_SIZE&partSizeBytes=$FILE_SIZE" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json") if ! echo "$INIT_RESPONSE" | grep -q '"missingParts"'; then print_error "Failed to init upload for $FILENAME: $INIT_RESPONSE" continue fi # Step 2: Upload the single part HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ "$TEXERA_FILE_SERVICE_URL/dataset/multipart-upload/part?ownerEmail=$OWNER_EMAIL&datasetName=$DATASET_NAME&filePath=$ENCODED_NAME&partNumber=1" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/octet-stream" \ -H "Content-Length: $FILE_SIZE" \ --data-binary "@$file") if [ "$HTTP_CODE" != "200" ]; then print_error "Failed to upload part for $FILENAME (HTTP $HTTP_CODE)" # Abort the upload session curl -s -o /dev/null -X POST \ "$TEXERA_FILE_SERVICE_URL/dataset/multipart-upload?type=abort&ownerEmail=$OWNER_EMAIL&datasetName=$DATASET_NAME&filePath=$ENCODED_NAME" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" continue fi # Step 3: Finish the multipart upload FINISH_RESPONSE=$(curl -s -X POST \ "$TEXERA_FILE_SERVICE_URL/dataset/multipart-upload?type=finish&ownerEmail=$OWNER_EMAIL&datasetName=$DATASET_NAME&filePath=$ENCODED_NAME" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json") if echo "$FINISH_RESPONSE" | grep -q '"message"'; then print_status "Uploaded $FILENAME" else print_error "Failed to finish upload for $FILENAME: $FINISH_RESPONSE" fi done # Create version print_status "Creating version for $DATASET_NAME" VERSION_RESPONSE=$(curl -s -X POST "$TEXERA_FILE_SERVICE_URL/dataset/$DATASET_ID/version/create" \ -H "Content-Type: text/plain" \ -H "Authorization: Bearer $TOKEN" \ -d "") if echo "$VERSION_RESPONSE" | grep -q '"datasetVersion"'; then print_status "Version created successfully for $DATASET_NAME" else print_error "Failed to create version for $DATASET_NAME" fi done print_status "All datasets processed" } # Load all workflows from the workflows directory load_workflows() { if [ ! -d "$WORKFLOW_DIR" ]; then print_warn "Workflow directory '$WORKFLOW_DIR' not found, skipping workflows" return 0 fi # Get list of existing workflows WORKFLOW_LIST_RESPONSE=$(curl -s -X GET "$TEXERA_DASHBOARD_SERVICE_URL/workflow/list" \ -H "Authorization: Bearer $TOKEN") for workflow_file in "$WORKFLOW_DIR"/*.json; do [ -f "$workflow_file" ] || continue workflow_name=$(basename "$workflow_file" .json) print_status "Processing workflow: $workflow_name" # Check if workflow already exists if echo "$WORKFLOW_LIST_RESPONSE" | grep -q "\"name\":\"$workflow_name\""; then print_status "Workflow '$workflow_name' already exists, skipping" continue fi # Parse and create workflow content=$(jq -c . "$workflow_file") if [ $? -ne 0 ]; then print_error "Failed to parse $workflow_file with jq" continue fi print_status "Creating workflow: $workflow_name" response=$(curl -s -X POST "$TEXERA_DASHBOARD_SERVICE_URL/workflow/create" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "{\"name\":\"$workflow_name\", \"content\": $(jq -Rs <<< "$content")}") if echo "$response" | grep -q '"wid"'; then wid=$(echo "$response" | grep -o '"wid":[0-9]*' | cut -d':' -f2) print_status "Workflow '$workflow_name' created with ID $wid" else print_error "Failed to create workflow '$workflow_name'" print_error "Response: $response" fi done print_status "All workflows processed" } # Main execution main() { print_status "=== Texera Example Data Loader ===" wait_for_service "Dashboard Service" "$TEXERA_DASHBOARD_SERVICE_URL/healthcheck" wait_for_service "File Service" "$TEXERA_FILE_SERVICE_URL/healthcheck" authenticate load_datasets load_workflows print_status "=== Example data loading complete ===" } main ================================================ FILE: bin/single-node/examples/workflows/[Example] Data Exploration on Movies Dataset.json ================================================ { "operators": [ { "operatorID": "CSVFileScan-operator-ddb8baf2-03e1-4b7a-9b88-719db6a21199", "operatorType": "CSVFileScan", "operatorVersion": "N/A", "operatorProperties": { "fileEncoding": "UTF_8", "customDelimiter": ",", "hasHeader": true, "fileName": "/texera/popular-movies-of-imdb/v1/TMDb_updated.csv", "offset": 0, "limit": 1000 }, "inputPorts": [], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Read Movie Dataset", "dynamicInputPorts": false, "dynamicOutputPorts": false, "viewResult": true }, { "operatorID": "PieChart-operator-4924a885-7e6d-4b3c-8463-588f1e2ecb92", "operatorType": "PieChart", "operatorVersion": "N/A", "operatorProperties": { "value": "#movies", "name": "original_language" }, "inputPorts": [ { "portID": "input-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "PieChart", "dynamicInputPorts": false, "dynamicOutputPorts": false }, { "operatorID": "Aggregate-operator-41c3fbf6-2846-417b-add2-386c34b0a07f", "operatorType": "Aggregate", "operatorVersion": "N/A", "operatorProperties": { "aggregations": [ { "aggFunction": "count", "attribute": "original_language", "result attribute": "#movies" } ], "groupByKeys": [ "original_language" ] }, "inputPorts": [ { "portID": "input-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Count # of Movies Per Language", "dynamicInputPorts": false, "dynamicOutputPorts": false }, { "operatorID": "Filter-operator-4ecc80e6-3840-46ff-afed-2ee638693a60", "operatorType": "Filter", "operatorVersion": "N/A", "operatorProperties": { "predicates": [ { "value": "en", "attribute": "original_language", "condition": "=" } ] }, "inputPorts": [ { "portID": "input-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Keep English Movies", "dynamicInputPorts": false, "dynamicOutputPorts": false, "viewResult": true }, { "operatorID": "WordCloud-operator-fdc7b86b-3bc2-44db-9334-d6767e30186d", "operatorType": "WordCloud", "operatorVersion": "N/A", "operatorProperties": { "topN": 100, "textColumn": "overview" }, "inputPorts": [ { "portID": "input-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Word Cloud of \"overview\"", "dynamicInputPorts": false, "dynamicOutputPorts": false }, { "operatorID": "Filter-operator-428eb866-abfb-4f7f-9d4f-5aeebd6d778b", "operatorType": "Filter", "operatorVersion": "N/A", "operatorProperties": { "predicates": [ { "value": "fr", "attribute": "original_language", "condition": "=" } ] }, "inputPorts": [ { "portID": "input-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Keep French Movies", "dynamicInputPorts": false, "dynamicOutputPorts": false, "viewResult": true }, { "operatorID": "WordCloud-operator-0526bc0b-66fe-48d8-aa41-67be71f98401", "operatorType": "WordCloud", "operatorVersion": "N/A", "operatorProperties": { "topN": 100, "textColumn": "overview" }, "inputPorts": [ { "portID": "input-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Word Cloud of \"overview\"", "dynamicInputPorts": false, "dynamicOutputPorts": false }, { "operatorID": "Projection-operator-9144f57d-0229-4afb-8a18-64e4d9ba8aa7", "operatorType": "Projection", "operatorVersion": "N/A", "operatorProperties": { "isDrop": true, "attributes": [ { "originalAttribute": "id" } ] }, "inputPorts": [ { "portID": "input-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Remove One Column", "dynamicInputPorts": false, "dynamicOutputPorts": false }, { "operatorID": "PythonUDFV2-operator-d785a88e-a5fe-45bb-9da3-b9acc5f368a4", "operatorType": "PythonUDFV2", "operatorVersion": "N/A", "operatorProperties": { "code": "from pytexera import *\n\nclass ProcessTupleOperator(UDFOperatorV2):\n \n @overrides\n def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n vote_count = tuple_[\"vote_count\"]\n vote_average = tuple_[\"vote_average\"]\n \n # Normalize vote_count to a 0–1 scale, capped at 1,000 votes\n # This gives us a weight factor indicating how trust-worthy the \"vote_average\" field is\n # Example: 200 votes → 0.2 ; 1,500 votes → 1.0\n weight = min(vote_count, 1000) / 1000\n\n # Final score is vote_average scaled by vote_count weight\n # A movie with a lower \"vote_count\" will be impacted more\n custom_score = vote_average * weight\n \n # Give a rating label based on the \"custom_score\"\n if custom_score < 2.0:\n label = \"Disaster\"\n elif custom_score < 4.0:\n label = \"Flop\"\n elif custom_score < 6.0:\n label = \"Average\"\n elif custom_score < 8.0:\n label = \"Hit\"\n else:\n label = \"Legendary\"\n \n # Assign \"custom_score\" and \"custom_rating\" to the tuple as new fields\n tuple_[\"custom_score\"] = round(custom_score, 2)\n tuple_[\"custom_rating\"] = label\n\n # Output the tuple\n yield tuple_\n\n", "workers": 2, "retainInputColumns": true, "outputColumns": [ { "attributeName": "custom_score", "attributeType": "double" }, { "attributeName": "custom_rating", "attributeType": "string" } ] }, "inputPorts": [ { "portID": "input-0", "displayName": "", "allowMultiInputs": true, "isDynamicPort": false, "dependencies": [] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Assign Custom Scores and Ratings", "dynamicInputPorts": true, "dynamicOutputPorts": true, "viewResult": true }, { "operatorID": "Aggregate-operator-671f7e75-6477-46bd-be40-484d3c496eb7", "operatorType": "Aggregate", "operatorVersion": "N/A", "operatorProperties": { "aggregations": [ { "aggFunction": "count", "attribute": "custom_rating", "result attribute": "#ratings" } ], "groupByKeys": [ "custom_rating" ] }, "inputPorts": [ { "portID": "input-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Count # of Rated Movies ", "dynamicInputPorts": false, "dynamicOutputPorts": false }, { "operatorID": "Aggregate-operator-ea972a84-7fca-499c-83f2-75f0bef56900", "operatorType": "Aggregate", "operatorVersion": "N/A", "operatorProperties": { "aggregations": [ { "aggFunction": "count", "attribute": "custom_rating", "result attribute": "#ratings" } ], "groupByKeys": [ "custom_rating" ] }, "inputPorts": [ { "portID": "input-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Count # of Rated Movies ", "dynamicInputPorts": false, "dynamicOutputPorts": false }, { "operatorID": "BarChart-operator-beb55708-e8d1-4e65-9b66-ace428e52f63", "operatorType": "BarChart", "operatorVersion": "N/A", "operatorProperties": { "categoryColumn": "custom_rating", "horizontalOrientation": false, "fields": "custom_rating", "value": "#ratings" }, "inputPorts": [ { "portID": "input-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Bar Chart", "dynamicInputPorts": false, "dynamicOutputPorts": false }, { "operatorID": "BarChart-operator-e38a512a-a74e-4f86-85ad-67c5c2cb0620", "operatorType": "BarChart", "operatorVersion": "N/A", "operatorProperties": { "categoryColumn": "custom_rating", "horizontalOrientation": false, "fields": "custom_rating", "value": "#ratings" }, "inputPorts": [ { "portID": "input-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Bar Chart", "dynamicInputPorts": false, "dynamicOutputPorts": false } ], "operatorPositions": { "CSVFileScan-operator-ddb8baf2-03e1-4b7a-9b88-719db6a21199": { "x": -121, "y": 239 }, "PieChart-operator-4924a885-7e6d-4b3c-8463-588f1e2ecb92": { "x": 637, "y": -59 }, "Aggregate-operator-41c3fbf6-2846-417b-add2-386c34b0a07f": { "x": 419, "y": -59 }, "Filter-operator-4ecc80e6-3840-46ff-afed-2ee638693a60": { "x": 478, "y": 240 }, "WordCloud-operator-fdc7b86b-3bc2-44db-9334-d6767e30186d": { "x": 639, "y": 159 }, "Filter-operator-428eb866-abfb-4f7f-9d4f-5aeebd6d778b": { "x": 496, "y": 592 }, "WordCloud-operator-0526bc0b-66fe-48d8-aa41-67be71f98401": { "x": 636, "y": 512 }, "Projection-operator-9144f57d-0229-4afb-8a18-64e4d9ba8aa7": { "x": 66, "y": 239 }, "PythonUDFV2-operator-d785a88e-a5fe-45bb-9da3-b9acc5f368a4": { "x": 280, "y": 240 }, "Aggregate-operator-671f7e75-6477-46bd-be40-484d3c496eb7": { "x": 636, "y": 671 }, "Aggregate-operator-ea972a84-7fca-499c-83f2-75f0bef56900": { "x": 638, "y": 320 }, "BarChart-operator-beb55708-e8d1-4e65-9b66-ace428e52f63": { "x": 786, "y": 320 }, "BarChart-operator-e38a512a-a74e-4f86-85ad-67c5c2cb0620": { "x": 770, "y": 671 } }, "links": [ { "linkID": "3fba9036-949b-41e6-b1ac-809074e95b3d", "source": { "operatorID": "Aggregate-operator-41c3fbf6-2846-417b-add2-386c34b0a07f", "portID": "output-0" }, "target": { "operatorID": "PieChart-operator-4924a885-7e6d-4b3c-8463-588f1e2ecb92", "portID": "input-0" } }, { "linkID": "8752abd8-3f8a-41d6-9780-9c2285308481", "source": { "operatorID": "Filter-operator-4ecc80e6-3840-46ff-afed-2ee638693a60", "portID": "output-0" }, "target": { "operatorID": "WordCloud-operator-fdc7b86b-3bc2-44db-9334-d6767e30186d", "portID": "input-0" } }, { "linkID": "link-5eccb964-e5ba-413f-a8bb-7de17dd15daa", "source": { "operatorID": "Filter-operator-428eb866-abfb-4f7f-9d4f-5aeebd6d778b", "portID": "output-0" }, "target": { "operatorID": "WordCloud-operator-0526bc0b-66fe-48d8-aa41-67be71f98401", "portID": "input-0" } }, { "linkID": "136b40b1-0f7a-421e-809d-746e55eaa397", "source": { "operatorID": "CSVFileScan-operator-ddb8baf2-03e1-4b7a-9b88-719db6a21199", "portID": "output-0" }, "target": { "operatorID": "Projection-operator-9144f57d-0229-4afb-8a18-64e4d9ba8aa7", "portID": "input-0" } }, { "linkID": "d3baeb00-7f51-49f3-ad81-ceaad4376085", "source": { "operatorID": "Projection-operator-9144f57d-0229-4afb-8a18-64e4d9ba8aa7", "portID": "output-0" }, "target": { "operatorID": "PythonUDFV2-operator-d785a88e-a5fe-45bb-9da3-b9acc5f368a4", "portID": "input-0" } }, { "linkID": "b7f78a90-b224-43d3-9891-628d92c8fe2b", "source": { "operatorID": "PythonUDFV2-operator-d785a88e-a5fe-45bb-9da3-b9acc5f368a4", "portID": "output-0" }, "target": { "operatorID": "Aggregate-operator-41c3fbf6-2846-417b-add2-386c34b0a07f", "portID": "input-0" } }, { "linkID": "9aa47d70-2fd0-4047-8326-e6ec0e3cb1a7", "source": { "operatorID": "PythonUDFV2-operator-d785a88e-a5fe-45bb-9da3-b9acc5f368a4", "portID": "output-0" }, "target": { "operatorID": "Filter-operator-4ecc80e6-3840-46ff-afed-2ee638693a60", "portID": "input-0" } }, { "linkID": "cbfc3d5a-08cd-4d45-94f4-9711855c839c", "source": { "operatorID": "PythonUDFV2-operator-d785a88e-a5fe-45bb-9da3-b9acc5f368a4", "portID": "output-0" }, "target": { "operatorID": "Filter-operator-428eb866-abfb-4f7f-9d4f-5aeebd6d778b", "portID": "input-0" } }, { "linkID": "2d18fc28-ef4b-4c71-8927-cfc3bb044868", "source": { "operatorID": "Filter-operator-428eb866-abfb-4f7f-9d4f-5aeebd6d778b", "portID": "output-0" }, "target": { "operatorID": "Aggregate-operator-671f7e75-6477-46bd-be40-484d3c496eb7", "portID": "input-0" } }, { "linkID": "bd807bb1-10f0-4d9e-b837-27a005e1d433", "source": { "operatorID": "Filter-operator-4ecc80e6-3840-46ff-afed-2ee638693a60", "portID": "output-0" }, "target": { "operatorID": "Aggregate-operator-ea972a84-7fca-499c-83f2-75f0bef56900", "portID": "input-0" } }, { "linkID": "link-2c07dc92-b0d9-495a-9620-1d893f8bb87c", "source": { "operatorID": "Aggregate-operator-ea972a84-7fca-499c-83f2-75f0bef56900", "portID": "output-0" }, "target": { "operatorID": "BarChart-operator-beb55708-e8d1-4e65-9b66-ace428e52f63", "portID": "input-0" } }, { "linkID": "853cf538-ed67-489b-b279-3b3cd447a1fc", "source": { "operatorID": "Aggregate-operator-671f7e75-6477-46bd-be40-484d3c496eb7", "portID": "output-0" }, "target": { "operatorID": "BarChart-operator-e38a512a-a74e-4f86-85ad-67c5c2cb0620", "portID": "input-0" } } ], "commentBoxes": [], "settings": { "dataTransferBatchSize": 400 } } ================================================ FILE: bin/single-node/examples/workflows/[Example] Machine Learning on Iris Dataset.json ================================================ { "operators": [ { "operatorID": "CSVFileScan-operator-e6f7be3d-492b-4cd1-8d74-dafa9fab94e9", "operatorType": "CSVFileScan", "operatorVersion": "N/A", "operatorProperties": { "fileEncoding": "UTF_8", "customDelimiter": ",", "hasHeader": true, "fileName": "/texera/iris-species/v1/Iris.csv" }, "inputPorts": [], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Read Iris Dataset", "dynamicInputPorts": false, "dynamicOutputPorts": false, "viewResult": true }, { "operatorID": "ScatterMatrixChart-operator-eb2b95c3-3fc1-4a6a-9a46-c3cee793e887", "operatorType": "ScatterMatrixChart", "operatorVersion": "N/A", "operatorProperties": { "Color": "Species", "Selected Attributes": [ "SepalLengthCm", "SepalWidthCm", "PetalLengthCm", "PetalWidthCm" ] }, "inputPorts": [ { "portID": "input-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Compare features", "dynamicInputPorts": false, "dynamicOutputPorts": false, "viewResult": false }, { "operatorID": "Projection-operator-f333f5a9-2894-428b-a51b-f90fa4b5d2de", "operatorType": "Projection", "operatorVersion": "N/A", "operatorProperties": { "isDrop": false, "attributes": [ { "originalAttribute": "SepalWidthCm" }, { "originalAttribute": "PetalWidthCm" }, { "alias": "", "originalAttribute": "Species" } ] }, "inputPorts": [ { "portID": "input-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Use SepalWidth + PedalWidth as training features", "dynamicInputPorts": false, "dynamicOutputPorts": false }, { "operatorID": "Projection-operator-70298736-c5ee-4223-9e4d-79cbd3cf6fa8", "operatorType": "Projection", "operatorVersion": "N/A", "operatorProperties": { "isDrop": false, "attributes": [ { "originalAttribute": "PetalLengthCm" }, { "originalAttribute": "PetalWidthCm" }, { "alias": "", "originalAttribute": "Species" } ] }, "inputPorts": [ { "portID": "input-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Use PedalWidth+PedalLength as training features", "dynamicInputPorts": false, "dynamicOutputPorts": false }, { "operatorID": "Split-operator-ad9de8f5-6a3f-4737-8870-7f9dbb75c996", "operatorType": "Split", "operatorVersion": "N/A", "operatorProperties": { "k": 70, "random": true, "seed": 1 }, "inputPorts": [ { "portID": "input-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false }, { "portID": "output-1", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Split", "dynamicInputPorts": true, "dynamicOutputPorts": true }, { "operatorID": "Split-operator-f7bebe72-aeb1-45ca-9b06-8d46d0c9d6b0", "operatorType": "Split", "operatorVersion": "N/A", "operatorProperties": { "k": 70, "random": true, "seed": 1 }, "inputPorts": [ { "portID": "input-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false }, { "portID": "output-1", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Split", "dynamicInputPorts": true, "dynamicOutputPorts": true }, { "operatorID": "SklearnPerceptron-operator-a7b059fb-1e65-4b80-8d26-7c9aa64b4b1e", "operatorType": "SklearnPerceptron", "operatorVersion": "N/A", "operatorProperties": { "countVectorizer": false, "tfidfTransformer": false, "target": "Species" }, "inputPorts": [ { "portID": "input-0", "displayName": "training", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] }, { "portID": "input-1", "displayName": "testing", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [ { "id": 0, "internal": false } ] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Linear Perceptron", "dynamicInputPorts": false, "dynamicOutputPorts": false }, { "operatorID": "SklearnPrediction-operator-a6479757-22d1-4138-ade3-4a09675633fa", "operatorType": "SklearnPrediction", "operatorVersion": "N/A", "operatorProperties": { "Model Attribute": "model", "Output Attribute Name": "prediction", "Ground Truth Attribute Name to Ignore": "Species" }, "inputPorts": [ { "portID": "input-0", "displayName": "model", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] }, { "portID": "input-1", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [ { "id": 0, "internal": false } ] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Sklearn Prediction", "dynamicInputPorts": false, "dynamicOutputPorts": false }, { "operatorID": "SklearnPrediction-operator-f0009a16-2033-4ff3-9736-6f858b93244b", "operatorType": "SklearnPrediction", "operatorVersion": "N/A", "operatorProperties": { "Model Attribute": "model", "Output Attribute Name": "prediction", "Ground Truth Attribute Name to Ignore": "Species" }, "inputPorts": [ { "portID": "input-0", "displayName": "model", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] }, { "portID": "input-1", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [ { "id": 0, "internal": false } ] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Sklearn Prediction", "dynamicInputPorts": false, "dynamicOutputPorts": false }, { "operatorID": "Scatterplot-operator-d3688238-2bd8-4aec-bec8-c5b0c9c5c566", "operatorType": "Scatterplot", "operatorVersion": "N/A", "operatorProperties": { "xLogScale": false, "yLogScale": false, "xColumn": "SepalWidthCm", "yColumn": "PetalWidthCm", "colorColumn": "prediction", "alpha": 1 }, "inputPorts": [ { "portID": "input-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Scatterplot", "dynamicInputPorts": false, "dynamicOutputPorts": false }, { "operatorID": "Scatterplot-operator-de9d36ca-1219-4cbc-a7fc-6ae203c8b003", "operatorType": "Scatterplot", "operatorVersion": "N/A", "operatorProperties": { "xLogScale": false, "yLogScale": false, "xColumn": "PetalLengthCm", "yColumn": "PetalWidthCm", "colorColumn": "prediction", "alpha": 1 }, "inputPorts": [ { "portID": "input-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Scatterplot", "dynamicInputPorts": false, "dynamicOutputPorts": false }, { "operatorID": "Split-operator-fb8bfb7e-a5af-4b25-8579-de02ae834f9f", "operatorType": "Split", "operatorVersion": "N/A", "operatorProperties": { "k": 70, "random": true, "seed": 1 }, "inputPorts": [ { "portID": "input-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false }, { "portID": "output-1", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Split", "dynamicInputPorts": true, "dynamicOutputPorts": true }, { "operatorID": "Split-operator-de75a27b-fc0f-4e62-ad6e-80a32f1d20d3", "operatorType": "Split", "operatorVersion": "N/A", "operatorProperties": { "k": 70, "random": true, "seed": 1 }, "inputPorts": [ { "portID": "input-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false }, { "portID": "output-1", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Split", "dynamicInputPorts": true, "dynamicOutputPorts": true }, { "operatorID": "SklearnDecisionTree-operator-065511ba-f6e2-4e62-ae31-183c07986c27", "operatorType": "SklearnDecisionTree", "operatorVersion": "N/A", "operatorProperties": { "countVectorizer": false, "tfidfTransformer": false, "target": "Species" }, "inputPorts": [ { "portID": "input-0", "displayName": "training", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [] }, { "portID": "input-1", "displayName": "testing", "allowMultiInputs": false, "isDynamicPort": false, "dependencies": [ { "id": 0, "internal": false } ] } ], "outputPorts": [ { "portID": "output-0", "displayName": "", "allowMultiInputs": false, "isDynamicPort": false } ], "showAdvanced": false, "isDisabled": false, "customDisplayName": "Decision Tree", "dynamicInputPorts": false, "dynamicOutputPorts": false } ], "operatorPositions": { "CSVFileScan-operator-e6f7be3d-492b-4cd1-8d74-dafa9fab94e9": { "x": -239, "y": 162 }, "ScatterMatrixChart-operator-eb2b95c3-3fc1-4a6a-9a46-c3cee793e887": { "x": 108, "y": -49 }, "Projection-operator-f333f5a9-2894-428b-a51b-f90fa4b5d2de": { "x": 28, "y": 162 }, "Projection-operator-70298736-c5ee-4223-9e4d-79cbd3cf6fa8": { "x": 24, "y": 458 }, "Split-operator-ad9de8f5-6a3f-4737-8870-7f9dbb75c996": { "x": 283, "y": 59 }, "Split-operator-f7bebe72-aeb1-45ca-9b06-8d46d0c9d6b0": { "x": 286, "y": 366 }, "SklearnPerceptron-operator-a7b059fb-1e65-4b80-8d26-7c9aa64b4b1e": { "x": 474, "y": 59 }, "SklearnPrediction-operator-a6479757-22d1-4138-ade3-4a09675633fa": { "x": 634, "y": 140 }, "SklearnPrediction-operator-f0009a16-2033-4ff3-9736-6f858b93244b": { "x": 635, "y": 432 }, "Scatterplot-operator-d3688238-2bd8-4aec-bec8-c5b0c9c5c566": { "x": 762, "y": 140 }, "Scatterplot-operator-de9d36ca-1219-4cbc-a7fc-6ae203c8b003": { "x": 764, "y": 432 }, "Split-operator-fb8bfb7e-a5af-4b25-8579-de02ae834f9f": { "x": 176, "y": 140 }, "Split-operator-de75a27b-fc0f-4e62-ad6e-80a32f1d20d3": { "x": 173, "y": 432 }, "SklearnDecisionTree-operator-065511ba-f6e2-4e62-ae31-183c07986c27": { "x": 463, "y": 366 } }, "links": [ { "linkID": "link-560a8dc4-b2c8-4575-abd8-2924b58f4a54", "source": { "operatorID": "Split-operator-ad9de8f5-6a3f-4737-8870-7f9dbb75c996", "portID": "output-0" }, "target": { "operatorID": "SklearnPerceptron-operator-a7b059fb-1e65-4b80-8d26-7c9aa64b4b1e", "portID": "input-0" } }, { "linkID": "3d479ba9-824c-4c63-a556-1ac678d0345d", "source": { "operatorID": "Split-operator-ad9de8f5-6a3f-4737-8870-7f9dbb75c996", "portID": "output-1" }, "target": { "operatorID": "SklearnPerceptron-operator-a7b059fb-1e65-4b80-8d26-7c9aa64b4b1e", "portID": "input-1" } }, { "linkID": "link-7fe7b7d2-cd88-4d15-bb4c-2ead085cc5a6", "source": { "operatorID": "SklearnPerceptron-operator-a7b059fb-1e65-4b80-8d26-7c9aa64b4b1e", "portID": "output-0" }, "target": { "operatorID": "SklearnPrediction-operator-a6479757-22d1-4138-ade3-4a09675633fa", "portID": "input-0" } }, { "linkID": "6b8a5349-88ba-4e4f-9fee-79a086a70545", "source": { "operatorID": "SklearnPrediction-operator-f0009a16-2033-4ff3-9736-6f858b93244b", "portID": "output-0" }, "target": { "operatorID": "Scatterplot-operator-de9d36ca-1219-4cbc-a7fc-6ae203c8b003", "portID": "input-0" } }, { "linkID": "38c7e897-0380-4963-aa18-93ef8b8149a5", "source": { "operatorID": "SklearnPrediction-operator-a6479757-22d1-4138-ade3-4a09675633fa", "portID": "output-0" }, "target": { "operatorID": "Scatterplot-operator-d3688238-2bd8-4aec-bec8-c5b0c9c5c566", "portID": "input-0" } }, { "linkID": "237de7f7-a798-423b-bc90-317dc0ba959a", "source": { "operatorID": "CSVFileScan-operator-e6f7be3d-492b-4cd1-8d74-dafa9fab94e9", "portID": "output-0" }, "target": { "operatorID": "Projection-operator-f333f5a9-2894-428b-a51b-f90fa4b5d2de", "portID": "input-0" } }, { "linkID": "487111f2-76d9-4488-84d0-d4504249306e", "source": { "operatorID": "CSVFileScan-operator-e6f7be3d-492b-4cd1-8d74-dafa9fab94e9", "portID": "output-0" }, "target": { "operatorID": "ScatterMatrixChart-operator-eb2b95c3-3fc1-4a6a-9a46-c3cee793e887", "portID": "input-0" } }, { "linkID": "3e013e60-8068-46a2-bbb5-246d2990b60c", "source": { "operatorID": "Projection-operator-f333f5a9-2894-428b-a51b-f90fa4b5d2de", "portID": "output-0" }, "target": { "operatorID": "Split-operator-fb8bfb7e-a5af-4b25-8579-de02ae834f9f", "portID": "input-0" } }, { "linkID": "d50fab28-ba45-4464-a69f-de48231a0d40", "source": { "operatorID": "Split-operator-fb8bfb7e-a5af-4b25-8579-de02ae834f9f", "portID": "output-0" }, "target": { "operatorID": "Split-operator-ad9de8f5-6a3f-4737-8870-7f9dbb75c996", "portID": "input-0" } }, { "linkID": "9740eafa-415a-465d-8764-cf60077394ff", "source": { "operatorID": "Split-operator-fb8bfb7e-a5af-4b25-8579-de02ae834f9f", "portID": "output-1" }, "target": { "operatorID": "SklearnPrediction-operator-a6479757-22d1-4138-ade3-4a09675633fa", "portID": "input-1" } }, { "linkID": "59aaf03f-0863-4fa8-9b4b-57493c37117f", "source": { "operatorID": "CSVFileScan-operator-e6f7be3d-492b-4cd1-8d74-dafa9fab94e9", "portID": "output-0" }, "target": { "operatorID": "Projection-operator-70298736-c5ee-4223-9e4d-79cbd3cf6fa8", "portID": "input-0" } }, { "linkID": "85a6bfff-d491-4ff7-8283-7c096528fb2f", "source": { "operatorID": "Projection-operator-70298736-c5ee-4223-9e4d-79cbd3cf6fa8", "portID": "output-0" }, "target": { "operatorID": "Split-operator-de75a27b-fc0f-4e62-ad6e-80a32f1d20d3", "portID": "input-0" } }, { "linkID": "a561e5b7-a6bb-4f6a-a380-c3e2de46a871", "source": { "operatorID": "Split-operator-de75a27b-fc0f-4e62-ad6e-80a32f1d20d3", "portID": "output-0" }, "target": { "operatorID": "Split-operator-f7bebe72-aeb1-45ca-9b06-8d46d0c9d6b0", "portID": "input-0" } }, { "linkID": "9d484f60-934d-4366-ada4-daa616b5b8a9", "source": { "operatorID": "Split-operator-de75a27b-fc0f-4e62-ad6e-80a32f1d20d3", "portID": "output-1" }, "target": { "operatorID": "SklearnPrediction-operator-f0009a16-2033-4ff3-9736-6f858b93244b", "portID": "input-1" } }, { "linkID": "link-17c55116-912e-42bd-a639-e62dd9a58f0a", "source": { "operatorID": "Split-operator-f7bebe72-aeb1-45ca-9b06-8d46d0c9d6b0", "portID": "output-0" }, "target": { "operatorID": "SklearnDecisionTree-operator-065511ba-f6e2-4e62-ae31-183c07986c27", "portID": "input-0" } }, { "linkID": "link-9cf6df5f-ac1c-48cb-9ef9-98ddf640c389", "source": { "operatorID": "SklearnDecisionTree-operator-065511ba-f6e2-4e62-ae31-183c07986c27", "portID": "output-0" }, "target": { "operatorID": "SklearnPrediction-operator-f0009a16-2033-4ff3-9736-6f858b93244b", "portID": "input-0" } }, { "linkID": "211feb88-6e03-4db0-b453-946f1fdbeb90", "source": { "operatorID": "Split-operator-f7bebe72-aeb1-45ca-9b06-8d46d0c9d6b0", "portID": "output-1" }, "target": { "operatorID": "SklearnDecisionTree-operator-065511ba-f6e2-4e62-ae31-183c07986c27", "portID": "input-1" } } ], "commentBoxes": [], "settings": { "dataTransferBatchSize": 400 } } ================================================ FILE: bin/single-node/litellm-config.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # LiteLLM model list for the single-node deployment. API keys are read # from environment variables passed to the container. To add more # models, follow the same shape — see https://docs.litellm.ai/docs/proxy/configs. litellm_settings: drop_params: true model_list: - model_name: claude-haiku-4.5 litellm_params: model: claude-haiku-4-5-20251001 api_key: "os.environ/ANTHROPIC_API_KEY" ================================================ FILE: bin/single-node/nginx.conf ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. events {} http { include mime.types; default_type application/octet-stream; # Suppress per-request access logs; errors still go to error_log. access_log off; server { listen 8080; location /api/compile { proxy_pass http://workflow-compiling-service:9090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /api/dataset { proxy_pass http://file-service:9092; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /api/access/dataset/ { proxy_pass http://file-service:9092; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /api/config { proxy_pass http://config-service:9094; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /api/computing-unit { proxy_pass http://workflow-computing-unit-managing-service:8888; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # LLM API traffic goes through access-control-service, which then forwards to LiteLLM. location = /api/models { proxy_pass http://access-control-service:9096; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /api/chat/ { proxy_pass http://access-control-service:9096; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /api/agents { proxy_pass http://agent-service:3001; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 1d; proxy_send_timeout 1d; } location /api/ { proxy_pass http://dashboard-service:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /wsapi/ { proxy_pass http://workflow-runtime-coordinator-service:8085; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; } # Fallback for all other routes location / { proxy_pass http://dashboard-service:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } } ================================================ FILE: bin/terminate-daemon.sh ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. red=`tput setaf 1` green=`tput setaf 2` reset=`tput sgr0` echo "${red}Terminating Shared Editing Server at $(pgrep -f y-websocket)...${reset}" kill -9 $(pgrep -f y-websocket-server) echo "${green}Terminated.${reset}" echo "${red}Terminating WorkflowCompilingService at $(pgrep -f WorkflowCompilingService)...${reset}" kill -9 $(pgrep -f WorkflowCompilingService) echo "${green}Terminated.${reset}" echo echo "${red}Terminating FileService at $(pgrep -f FileService)...${reset}" kill -9 $(pgrep -f FileService) echo "${green}Terminated.${reset}" echo echo "${red}Terminating ConfigService at $(pgrep -f ConfigService)...${reset}" kill -9 $(pgrep -f ConfigService) echo "${green}Terminated.${reset}" echo echo "${red}Terminating ComputingUnitManagingService at $(pgrep -f ComputingUnitManagingService)...${reset}" kill -9 $(pgrep -f ComputingUnitManagingService) echo "${green}Terminated.${reset}" echo echo "${red}Terminating TexeraWebApplication at $(pgrep -f TexeraWebApplication)...${reset}" kill -9 $(pgrep -f TexeraWebApplication) echo "${green}Terminated.${reset}" echo echo "${red}Terminating ComputingUnitMaster at $(pgrep -f ComputingUnitMaster)...${reset}" kill -9 $(pgrep -f ComputingUnitMaster) echo "${green}Terminated.${reset}" ================================================ FILE: bin/texera-web-application.dockerfile ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Apache Texera is an effort undergoing incubation at The Apache Software # Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is # required of all newly accepted projects until a further review indicates # that the infrastructure, communications, and decision-making process have # stabilized in a manner consistent with other successful ASF projects. # While incubation status is not necessarily a reflection of the # completeness or stability of the code, it does indicate that the project # has yet to be fully endorsed by the ASF. FROM node:24-bookworm AS build-frontend RUN apt-get update && apt-get install -y --no-install-recommends \ python3 build-essential git ca-certificates WORKDIR /frontend COPY frontend /frontend RUN rm -f /frontend/.yarnrc.yml RUN corepack enable && corepack prepare yarn@4.5.1 --activate && yarn set version --yarn-path 4.5.1 RUN echo "nodeLinker: node-modules" >> /frontend/.yarnrc.yml WORKDIR /frontend RUN yarn install && yarn run build FROM sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.3_2.13.11 AS build # Set working directory WORKDIR /texera # Copy modules for building the service COPY common/ common/ COPY amber/ amber/ COPY project/ project/ COPY build.sbt build.sbt COPY .jvmopts .jvmopts # Update system and install dependencies. python3-minimal is needed by # bin/licensing/concat_license_binary.py below. RUN apt-get update && apt-get install -y \ netcat \ unzip \ libpq-dev \ python3-minimal \ && apt-get clean # Add .git for runtime calls to jgit from OPversion COPY .git .git COPY LICENSE NOTICE DISCLAIMER ./ COPY licenses/ licenses/ COPY bin/licensing/ bin/licensing/ # Bring frontend/LICENSE-binary into this build stage so the per-image # LICENSE merge below can union it with amber/LICENSE-binary-java. COPY --from=build-frontend /frontend/LICENSE-binary amber/LICENSE-binary-frontend RUN sbt clean WorkflowExecutionService/dist # Unzip the texera binary RUN unzip amber/target/universal/amber-*.zip -d amber/target/ # Merge per-aspect LICENSE-binary files (java jars + frontend npm) into a # single LICENSE-binary-combined keyed by license group, for the runtime # image. Per-license-group merge keeps Scala/Java jars and Angular npm # packages inside the same Apache-2.0 / MIT / BSD / ... section instead # of stacking the inputs end-to-end. RUN python3 bin/licensing/concat_license_binary.py amber/LICENSE-binary-combined \ amber/LICENSE-binary-java \ amber/LICENSE-binary-frontend FROM eclipse-temurin:17-jre-jammy AS runtime WORKDIR /texera/amber # Copy built frontend files from the build-frontend stage to match FileAssetsBundle path (../../frontend/dist from /texera/amber) COPY --from=build-frontend /frontend/dist /frontend/dist # Copy the built texera binary from the build phase COPY --from=build /texera/.git /texera/amber/.git COPY --from=build /texera/amber/target/amber-* /texera/amber/ # Copy resources directories from build phase COPY --from=build /texera/amber/src/main/resources /texera/amber/src/main/resources COPY --from=build /texera/common/config/src/main/resources /texera/amber/common/config/src/main/resources # Copy ASF licensing files. LICENSE-binary and NOTICE-binary describe the # bundled third-party contents of this image and ship as /texera/LICENSE # and /texera/NOTICE; licenses/ holds the per-license full texts referenced # by LICENSE-binary. COPY --from=build /texera/amber/LICENSE-binary-combined /texera/LICENSE COPY --from=build /texera/amber/NOTICE-binary /texera/NOTICE COPY --from=build /texera/licenses /texera/licenses COPY --from=build /texera/DISCLAIMER /texera/ RUN groupadd --system --gid 1001 texera \ && useradd --system --uid 1001 --gid texera --home-dir /texera --no-create-home texera \ && chown -R texera:texera /texera /frontend USER texera CMD ["bin/texera-web-application"] EXPOSE 8080 ================================================ FILE: bin/utils/resolve-texera-home.sh ================================================ #!/usr/bin/env bash # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # # ------------------------------------------------------------- # resolve-texera-home.sh # ------------------------------------------------------------- # Determines TEXERA_HOME using the following priority: # 1. TEXERA_HOME environment variable (if already set) # 2. git repository root (if inside a git repo) # 3. parent directory if current dir is 'bin' # 4. current working directory (.) # # Prints the resolved TEXERA_HOME to stdout. # Logs human-readable messages via texera-logging.sh. # # Intended usage: # TEXERA_HOME="$(bin/resolve-texera-home.sh)" # # Exits with code 1 if resolution fails. # ------------------------------------------------------------- set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # --- Load logging helper --- # shellcheck source=bin/texera-logging.sh source "$SCRIPT_DIR/texera-logging.sh" resolve_texera_home() { # 1. TEXERA_HOME environment variable if [[ -n "${TEXERA_HOME:-}" ]]; then tx_info "TEXERA_HOME found in environment: $TEXERA_HOME" echo "$TEXERA_HOME" return 0 fi # 2. Git repository root (if any) if git -C . rev-parse --show-toplevel >/dev/null 2>&1; then local root root="$(git rev-parse --show-toplevel)" tx_info "TEXERA_HOME resolved via git repository root: $root" echo "$root" return 0 fi # 3. Parent directory if current folder is 'bin' local cwd cwd_basename cwd="$(pwd)" cwd_basename="$(basename "$cwd")" if [[ "$cwd_basename" == "bin" ]]; then local parent parent="$(dirname "$cwd")" tx_info "TEXERA_HOME resolved as parent of bin/: $parent" echo "$parent" return 0 fi # 4. Fallback to current working directory tx_warn "Falling back to current working directory as TEXERA_HOME: $cwd" echo "$cwd" } # --- Main execution --- if ! resolve_texera_home; then tx_error "Failed to resolve TEXERA_HOME." exit 1 fi ================================================ FILE: bin/utils/texera-logging.sh ================================================ #!/usr/bin/env bash # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # # ------------------------------------------------------------- # texera-logging.sh # ------------------------------------------------------------- # Shared logging utilities for all Texera bash scripts. # # Features: # • Colored, consistent logs prefixed with "Texera ▶" # • Logs go to STDERR by default (so STDOUT stays clean) # • Easy to override prefix or disable colors # # Env vars: # TEXERA_LOG_PREFIX="Texera ▸" # change prefix # NO_COLOR=1 # disable color # TEXERA_LOG_TO_STDERR=0 # send to STDOUT instead # ------------------------------------------------------------- # Prevent direct execution if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then printf 'Texera ▶ This file must be sourced, not executed.\n' >&2 exit 1 fi # --- Color setup --- _use_color=true if [[ -n "${NO_COLOR:-}" ]]; then _use_color=false fi if $_use_color; then readonly TL_COLOR_BLUE=$'\033[34m' readonly TL_COLOR_GREEN=$'\033[32m' readonly TL_COLOR_YELLOW=$'\033[33m' readonly TL_COLOR_RED=$'\033[31m' readonly TL_COLOR_BOLD=$'\033[1m' readonly TL_COLOR_RESET=$'\033[0m' else readonly TL_COLOR_BLUE="" TL_COLOR_GREEN="" TL_COLOR_YELLOW="" TL_COLOR_RED="" TL_COLOR_BOLD="" TL_COLOR_RESET="" fi # --- Prefix & output stream --- readonly TEXERA_LOG_PREFIX="${TEXERA_LOG_PREFIX:-Texera ▶}" readonly TL_PREFIX="${TL_COLOR_BOLD}${TEXERA_LOG_PREFIX}${TL_COLOR_RESET}" readonly _TL_TO_STDERR="${TEXERA_LOG_TO_STDERR:-1}" # --- Core emitter --- _tx_emit() { local _color="$1"; shift local _level="$1"; shift if [[ "$_TL_TO_STDERR" == "1" ]]; then printf '%s %s[%s]%s %s\n' "$TL_PREFIX" "$_color" "$_level" "$TL_COLOR_RESET" "$*" >&2 else printf '%s %s[%s]%s %s\n' "$TL_PREFIX" "$_color" "$_level" "$TL_COLOR_RESET" "$*" fi } # --- Public API --- tx_info() { _tx_emit "$TL_COLOR_BLUE" "INFO" "$*"; } tx_success() { _tx_emit "$TL_COLOR_GREEN" "SUCCESS" "$*"; } tx_warn() { _tx_emit "$TL_COLOR_YELLOW" "WARN" "$*"; } tx_error() { _tx_emit "$TL_COLOR_RED" "ERROR" "$*"; } export -f tx_info tx_success tx_warn tx_error ================================================ FILE: bin/workflow-compiling-service.dockerfile ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Apache Texera is an effort undergoing incubation at The Apache Software # Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is # required of all newly accepted projects until a further review indicates # that the infrastructure, communications, and decision-making process have # stabilized in a manner consistent with other successful ASF projects. # While incubation status is not necessarily a reflection of the # completeness or stability of the code, it does indicate that the project # has yet to be fully endorsed by the ASF. FROM sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.3_2.13.11 AS build # Set working directory WORKDIR /texera # Copy modules for building the service COPY common/ common/ COPY workflow-compiling-service/ workflow-compiling-service/ COPY project/ project/ COPY build.sbt build.sbt COPY .jvmopts .jvmopts # Update system and install dependencies RUN apt-get update && apt-get install -y \ netcat \ unzip \ libpq-dev \ && apt-get clean # Add .git for runtime calls to jgit from OPversion COPY .git .git COPY LICENSE NOTICE DISCLAIMER ./ COPY licenses/ licenses/ RUN sbt clean WorkflowCompilingService/dist # Unzip the texera binary RUN unzip workflow-compiling-service/target/universal/workflow-compiling-service-*.zip -d target/ FROM eclipse-temurin:17-jre-jammy AS runtime WORKDIR /texera # Copy the built texera binary from the build phase COPY --from=build /texera/.git /texera/.git COPY --from=build /texera/target/workflow-compiling-service-* /texera/ # Copy resources directories from build phase COPY --from=build /texera/common/config/src/main/resources /texera/common/config/src/main/resources COPY --from=build /texera/workflow-compiling-service/src/main/resources /texera/workflow-compiling-service/src/main/resources # Copy ASF licensing files. LICENSE-binary and NOTICE-binary describe the # bundled third-party contents of this image and ship as /texera/LICENSE # and /texera/NOTICE; licenses/ holds the per-license full texts referenced # by LICENSE-binary. COPY --from=build /texera/workflow-compiling-service/LICENSE-binary /texera/LICENSE COPY --from=build /texera/workflow-compiling-service/NOTICE-binary /texera/NOTICE COPY --from=build /texera/licenses /texera/licenses COPY --from=build /texera/DISCLAIMER /texera/ RUN groupadd --system --gid 1001 texera \ && useradd --system --uid 1001 --gid texera --home-dir /texera --no-create-home texera \ && chown -R texera:texera /texera USER texera CMD ["bin/workflow-compiling-service"] EXPOSE 9090 ================================================ FILE: bin/workflow-compiling-service.sh ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. target/workflow-compiling-service-*/bin/workflow-compiling-service ================================================ FILE: bin/workflow-computing-unit-managing-service.dockerfile ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Apache Texera is an effort undergoing incubation at The Apache Software # Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is # required of all newly accepted projects until a further review indicates # that the infrastructure, communications, and decision-making process have # stabilized in a manner consistent with other successful ASF projects. # While incubation status is not necessarily a reflection of the # completeness or stability of the code, it does indicate that the project # has yet to be fully endorsed by the ASF. FROM sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.3_2.13.11 AS build # Set working directory WORKDIR /texera # Copy modules for building the service COPY common/ common/ COPY computing-unit-managing-service/ computing-unit-managing-service/ COPY project/ project/ COPY build.sbt build.sbt COPY .jvmopts .jvmopts # Update system and install dependencies RUN apt-get update && apt-get install -y \ netcat \ unzip \ libpq-dev \ && apt-get clean # Add .git for runtime calls to jgit from OPversion COPY .git .git COPY LICENSE NOTICE DISCLAIMER ./ COPY licenses/ licenses/ RUN sbt clean ComputingUnitManagingService/dist # Unzip the texera binary RUN unzip computing-unit-managing-service/target/universal/computing-unit-managing-service-*.zip -d target/ FROM eclipse-temurin:17-jre-jammy AS runtime WORKDIR /texera COPY --from=build /texera/.git /texera/.git # Copy the built texera binary from the build phase COPY --from=build /texera/target/computing-unit-managing-service-* /texera/ # Copy resources directories from build phase COPY --from=build /texera/common/config/src/main/resources /texera/common/config/src/main/resources COPY --from=build /texera/computing-unit-managing-service/src/main/resources /texera/computing-unit-managing-service/src/main/resources # Copy ASF licensing files. LICENSE-binary and NOTICE-binary describe the # bundled third-party contents of this image and ship as /texera/LICENSE # and /texera/NOTICE; licenses/ holds the per-license full texts referenced # by LICENSE-binary. COPY --from=build /texera/computing-unit-managing-service/LICENSE-binary /texera/LICENSE COPY --from=build /texera/computing-unit-managing-service/NOTICE-binary /texera/NOTICE COPY --from=build /texera/licenses /texera/licenses COPY --from=build /texera/DISCLAIMER /texera/ RUN groupadd --system --gid 1001 texera \ && useradd --system --uid 1001 --gid texera --home-dir /texera --no-create-home texera \ && chown -R texera:texera /texera USER texera CMD ["bin/computing-unit-managing-service"] EXPOSE 8888 ================================================ FILE: bin/workflow-computing-unit.sh ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. ( cd amber && if [ ! -z $1 ] then target/texera-*/bin/computing-unit-master --cluster $1 else target/texera-*/bin/computing-unit-master fi ) ================================================ FILE: bin/y-websocket-server/Dockerfile ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Apache Texera is an effort undergoing incubation at The Apache Software # Foundation (ASF), sponsored by the Apache Incubator PMC. Incubation is # required of all newly accepted projects until a further review indicates # that the infrastructure, communications, and decision-making process have # stabilized in a manner consistent with other successful ASF projects. # While incubation status is not necessarily a reflection of the # completeness or stability of the code, it does indicate that the project # has yet to be fully endorsed by the ASF. # Use an official Node runtime as a parent image FROM node:latest # Set the working directory in the container WORKDIR /usr/src/app # Copy the current directory contents into the container at /usr/src/app COPY . . # Install any needed packages specified in package.json RUN npm install # Define environment variable ENV YPERSISTENCE=./leveldb ENV HOST=0.0.0.0 # Run y-websocket server when the container launches CMD ["npx", "y-websocket"] # Make port 1234 available to the world outside this container EXPOSE 1234 ================================================ FILE: bin/y-websocket-server/package.json ================================================ { "name": "shared-editing-service", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "y-websocket": "^2.0.0" } } ================================================ FILE: build.sbt ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. ThisBuild / organization := "org.apache.texera" ThisBuild / version := "1.1.0-incubating" ThisBuild / scalaVersion := "2.13.18" // Pull JDK 17+ JVM flags from .jvmopts so every JVM the build launches sees the same list. import com.typesafe.sbt.packager.universal.UniversalPlugin.autoImport.Universal ThisBuild / Test / javaOptions ++= JdkOptions.jvmFlags((ThisBuild / baseDirectory).value) // sbt-jacoco emits only HTML by default; add XML so Codecov can consume // per-module jacoco.xml at target/scala-2.13/jacoco/report/jacoco.xml. // JacocoPlugin defines a project-scoped default that overrides ThisBuild, // so this Seq is bundled into asfLicensingSettings (applied to every module). import com.github.sbt.jacoco.report.{JacocoReportFormats, JacocoReportSettings} lazy val coverageReportSettings = Seq( jacocoReportSettings := JacocoReportSettings() .withTitle("Apache Texera Coverage") .withFormats(JacocoReportFormats.ScalaHTML, JacocoReportFormats.XML) ) lazy val universalJvmFlagsSettings = Seq( Universal / javaOptions ++= JdkOptions.jvmFlags((ThisBuild / baseDirectory).value).map("-J" + _) ) // Per-module ASF licensing: each jar's META-INF/LICENSE describes only what is in that jar. // Modules without vendored code get Apache 2.0 only; workflow-operator includes mbknor attribution. // See project/AddMetaInfLicenseFiles.scala. // Dist-producing modules additionally override Universal / mappings in their own // build.sbt (not here) — see AddMetaInfLicenseFiles.distMappings. lazy val asfLicensingSettings = AddMetaInfLicenseFiles.defaultSettings ++ coverageReportSettings ++ universalJvmFlagsSettings lazy val asfLicensingSettingsWithVendored = AddMetaInfLicenseFiles.workflowOperatorSettings ++ coverageReportSettings ++ universalJvmFlagsSettings val jacksonVersion = "2.18.6" lazy val DAO = (project in file("common/dao")).settings(asfLicensingSettings) lazy val Config = (project in file("common/config")).settings(asfLicensingSettings) lazy val Auth = (project in file("common/auth")) .settings(asfLicensingSettings) .dependsOn(DAO, Config) lazy val ConfigService = (project in file("config-service")) .dependsOn(Auth, Config) .settings(asfLicensingSettings) .settings( dependencyOverrides ++= Seq( // override it as io.dropwizard 4 require 2.16.1 or higher "com.fasterxml.jackson.module" %% "jackson-module-scala" % jacksonVersion ) ) lazy val AccessControlService = (project in file("access-control-service")) .dependsOn(Auth, Config, DAO) .settings(asfLicensingSettings) .settings( dependencyOverrides ++= Seq( // override it as io.dropwizard 4 require 2.16.1 or higher "com.fasterxml.jackson.module" %% "jackson-module-scala" % jacksonVersion ) ) .configs(Test) .dependsOn(DAO % "test->test", Auth % "test->test") //This Scala module defines a pyb"..." macro-based DSL for composing Python code templates as an immutable PythonTemplateBuilder. //Used mainly for Python Native Operators lazy val PyBuilder = (project in file("common/pybuilder")) .settings(asfLicensingSettings) .configs(Test) .dependsOn(DAO % "test->test") // test scope dependency lazy val WorkflowCore = (project in file("common/workflow-core")) .settings(asfLicensingSettings) .dependsOn(DAO, Config, PyBuilder) .configs(Test) .dependsOn(DAO % "test->test") // test scope dependency lazy val ComputingUnitManagingService = (project in file("computing-unit-managing-service")) .dependsOn(WorkflowCore, Auth, Config) .settings(asfLicensingSettings) .settings( dependencyOverrides ++= Seq( // override it as io.dropwizard 4 require 2.16.1 or higher "com.fasterxml.jackson.module" %% "jackson-module-scala" % jacksonVersion ) ) lazy val FileService = (project in file("file-service")) .settings(asfLicensingSettings) .dependsOn(WorkflowCore, Auth, Config) .configs(Test) .dependsOn(DAO % "test->test") // test scope dependency .settings( dependencyOverrides ++= Seq( // override it as io.dropwizard 4 require 2.16.1 or higher "com.fasterxml.jackson.module" %% "jackson-module-scala" % jacksonVersion, "com.fasterxml.jackson.core" % "jackson-databind" % jacksonVersion, "org.glassfish.jersey.core" % "jersey-common" % "3.0.12" ) ) lazy val WorkflowOperator = (project in file("common/workflow-operator")).settings(asfLicensingSettingsWithVendored).dependsOn(WorkflowCore) lazy val WorkflowCompilingService = (project in file("workflow-compiling-service")) .dependsOn(WorkflowOperator, Config) .settings(asfLicensingSettings) .settings( dependencyOverrides ++= Seq( // override it as io.dropwizard 4 require 2.16.1 or higher "com.fasterxml.jackson.module" %% "jackson-module-scala" % jacksonVersion, "com.fasterxml.jackson.core" % "jackson-databind" % jacksonVersion, "org.glassfish.jersey.core" % "jersey-common" % "3.0.12" ) ) lazy val WorkflowExecutionService = (project in file("amber")) .dependsOn(WorkflowOperator, Auth, Config) .settings(asfLicensingSettings) .settings( dependencyOverrides ++= Seq( "com.fasterxml.jackson.core" % "jackson-core" % jacksonVersion, "com.fasterxml.jackson.core" % "jackson-databind" % jacksonVersion, "com.fasterxml.jackson.module" %% "jackson-module-scala" % jacksonVersion, "org.slf4j" % "slf4j-api" % "1.7.26", "org.eclipse.jetty" % "jetty-server" % "9.4.20.v20190813", "org.eclipse.jetty" % "jetty-servlet" % "9.4.20.v20190813", "org.eclipse.jetty" % "jetty-http" % "9.4.20.v20190813", // Netty dependency overrides to ensure compatibility with Arrow 14.0.1 // Arrow requires Netty 4.1.96.Final to avoid NoSuchFieldError: chunkSize "io.netty" % "netty-all" % "4.1.96.Final", "io.netty" % "netty-buffer" % "4.1.96.Final", "io.netty" % "netty-codec" % "4.1.96.Final", "io.netty" % "netty-codec-http" % "4.1.96.Final", "io.netty" % "netty-codec-http2" % "4.1.96.Final", "io.netty" % "netty-common" % "4.1.96.Final", "io.netty" % "netty-handler" % "4.1.96.Final", "io.netty" % "netty-resolver" % "4.1.96.Final", "io.netty" % "netty-transport" % "4.1.96.Final", "io.netty" % "netty-transport-native-unix-common" % "4.1.96.Final" ), libraryDependencies ++= Seq( "com.squareup.okhttp3" % "okhttp" % "4.10.0" force () // Force usage of OkHttp 4.10.0 ) ) .configs(Test) .dependsOn(DAO % "test->test", Auth % "test->test") // test scope dependency // root project definition lazy val TexeraProject = (project in file(".")) .aggregate( // common libraries Auth, Config, DAO, PyBuilder, WorkflowCore, WorkflowOperator, // services AccessControlService, ComputingUnitManagingService, ConfigService, FileService, WorkflowCompilingService, WorkflowExecutionService ) .settings( name := "texera", publishMavenStyle := true ) ================================================ FILE: codecov.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Codecov configuration for apache/texera. The repo uploads four flags # (frontend, scala, python, agent-service); without an explicit config, # defaults bite us in three places that this file fixes: # 1. Single-stack PRs leave non-uploaded flags as `?` in the comment # and drop them from the rollup. Carry forward instead. # 2. Coverage of generated code (protobuf), build output, and the # tests themselves dilutes the "real" coverage figure. Ignore # those paths. # 3. Default status checks fail on any project drop and require 100% # patch coverage, so non-test refactors get a red Codecov check # even when behavior is preserved. Allow 1% slack on project and # keep patch coverage informational. flag_management: default_rules: # If the current PR didn't run the job for a given flag, inherit # the latest report on the base branch instead of showing `?`. carryforward: true ignore: # Generated protobuf — line counts swamp real code. - "amber/src/main/python/proto/**" - "**/src_managed/**" # sbt JOOQ / scalapb-generated sources # Build / install output — never source-of-truth. - "**/target/**" - "frontend/dist/**" - "frontend/.angular/**" # Test files don't count toward source coverage. - "**/test_*.py" - "**/*.spec.ts" - "**/src/test/**" # sbt test source roots coverage: status: project: default: # Don't drop below the base-branch baseline, but tolerate # ≤1% jitter from re-runs / flaky tests / ignore-rule churn. target: auto threshold: 1% patch: default: # Surface patch coverage in the Codecov report but don't gate # PRs on it: a 100%-patch policy blocks plenty of legitimate # refactors / config-only changes that have no tests to add. informational: true comment: # `flag_management.default_rules.carryforward: true` above already # backfills missing flags from main, but Codecov's default still # hides those rows from the PR comment. Show them so a frontend-only # or python-only PR's table lists every flag — fresh-data rows track # the patch and carryforward'd rows render with `<ø>` to make their # unchanged status obvious. show_carryforward_flags: true ================================================ FILE: common/auth/build.sbt ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. ///////////////////////////////////////////////////////////////////////////// // Project Settings ///////////////////////////////////////////////////////////////////////////// name := "auth" enablePlugins(JavaAppPackaging) // Enable semanticdb for Scalafix ThisBuild / semanticdbEnabled := true ThisBuild / semanticdbVersion := scalafixSemanticdb.revision // Manage dependency conflicts by always using the latest revision ThisBuild / conflictManager := ConflictManager.latestRevision // Restrict parallel execution of tests to avoid conflicts Global / concurrentRestrictions += Tags.limit(Tags.Test, 1) ///////////////////////////////////////////////////////////////////////////// // Compiler Options ///////////////////////////////////////////////////////////////////////////// // Scala compiler options Compile / scalacOptions ++= Seq( "-Xelide-below", "WARNING", // Turn on optimizations with "WARNING" as the threshold "-feature", // Check feature warnings "-deprecation", // Check deprecation warnings "-Ywarn-unused:imports" // Check for unused imports ) ///////////////////////////////////////////////////////////////////////////// // Dependencies ///////////////////////////////////////////////////////////////////////////// // Core Dependencies libraryDependencies ++= Seq( "com.typesafe" % "config" % "1.4.6", // config reader "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5", // for LazyLogging "org.bitbucket.b_c" % "jose4j" % "0.9.6", // for jwt parser "jakarta.ws.rs" % "jakarta.ws.rs-api" % "3.0.0", // for JwtAuthFilter "jakarta.servlet" % "jakarta.servlet-api" % "5.0.0" % "provided", // for RequestLoggingFilter "org.eclipse.jetty" % "jetty-servlet" % "11.0.24" % "provided", // for FilterHolder "org.scalatest" %% "scalatest" % "3.2.17" % Test ) ================================================ FILE: common/auth/src/main/scala/org/apache/texera/auth/JwtAuth.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.auth import org.apache.texera.config.AuthConfig import org.apache.texera.dao.jooq.generated.tables.pojos.User import org.jose4j.jws.AlgorithmIdentifiers.HMAC_SHA256 import org.jose4j.jws.JsonWebSignature import org.jose4j.jwt.JwtClaims import org.jose4j.jwt.consumer.{JwtConsumer, JwtConsumerBuilder} import org.jose4j.keys.HmacKey import java.nio.charset.StandardCharsets // TODO: move this logic to Auth object JwtAuth { final val TOKEN_SECRET: String = AuthConfig.jwtSecretKey final val TOKEN_EXPIRE_TIME_IN_MINUTES: Int = AuthConfig.jwtExpirationMinutes val jwtConsumer: JwtConsumer = new JwtConsumerBuilder() .setAllowedClockSkewInSeconds(30) .setRequireExpirationTime() .setRequireSubject() .setVerificationKey(new HmacKey(TOKEN_SECRET.getBytes(StandardCharsets.UTF_8))) .setRelaxVerificationKeyValidation() .build def jwtToken(claims: JwtClaims): String = { val jws = new JsonWebSignature() jws.setPayload(claims.toJson) jws.setAlgorithmHeaderValue(HMAC_SHA256) jws.setKey(new HmacKey(TOKEN_SECRET.getBytes(StandardCharsets.UTF_8))) jws.getCompactSerialization } def jwtClaims(user: User, expireInDays: Int): JwtClaims = { val claims = new JwtClaims claims.setSubject(user.getName) claims.setClaim("userId", user.getUid) claims.setClaim("googleId", user.getGoogleId) claims.setClaim("email", user.getEmail) claims.setClaim("role", user.getRole) claims.setClaim("googleAvatar", user.getGoogleAvatar) claims.setExpirationTimeMinutesInTheFuture(TOKEN_EXPIRE_TIME_IN_MINUTES.toFloat) claims } } ================================================ FILE: common/auth/src/main/scala/org/apache/texera/auth/JwtAuthFilter.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.auth import com.typesafe.scalalogging.LazyLogging import jakarta.ws.rs.container.{ContainerRequestContext, ContainerRequestFilter} import jakarta.ws.rs.core.{HttpHeaders, SecurityContext} import jakarta.ws.rs.ext.Provider import org.apache.texera.dao.jooq.generated.enums.UserRoleEnum import java.security.Principal @Provider class JwtAuthFilter extends ContainerRequestFilter with LazyLogging { override def filter(requestContext: ContainerRequestContext): Unit = { val authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION) if (authHeader != null && authHeader.startsWith("Bearer ")) { val token = authHeader.substring(7) // Remove "Bearer " prefix val userOpt = JwtParser.parseToken(token) if (userOpt.isPresent) { val user = userOpt.get() requestContext.setSecurityContext(new SecurityContext { override def getUserPrincipal: Principal = user override def isUserInRole(role: String): Boolean = user.isRoleOf(UserRoleEnum.valueOf(role)) override def isSecure: Boolean = false override def getAuthenticationScheme: String = "Bearer" }) } else { logger.warn("Invalid JWT: Unable to parse token") } } } } ================================================ FILE: common/auth/src/main/scala/org/apache/texera/auth/JwtParser.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.auth import com.typesafe.scalalogging.LazyLogging import org.apache.texera.dao.jooq.generated.enums.UserRoleEnum import org.apache.texera.dao.jooq.generated.tables.pojos.User import org.jose4j.jwt.JwtClaims import org.jose4j.lang.UnresolvableKeyException import java.util.Optional /** Single source of truth for converting a verified JWT into a [[SessionUser]]. * * Verification reuses [[JwtAuth.jwtConsumer]] (same secret, same clock-skew * config). The claim set extracted here mirrors what [[JwtAuth.jwtClaims]] * writes when issuing a token. */ object JwtParser extends LazyLogging { /** Verify and parse a Bearer token string. */ def parseToken(token: String): Optional[SessionUser] = { try { Optional.of(claimsToSessionUser(JwtAuth.jwtConsumer.processToClaims(token))) } catch { case _: UnresolvableKeyException => logger.error("Invalid JWT Signature") Optional.empty() case e: Exception => logger.error(s"Failed to parse JWT: ${e.getMessage}") Optional.empty() } } /** Build a [[SessionUser]] from already-verified claims. Used by both * [[parseToken]] (which verifies then calls this) and amber's * `UserAuthenticator` (which the toastshaman filter calls after its own * signature verification). */ def claimsToSessionUser(claims: JwtClaims): SessionUser = { val userName = claims.getSubject val email = claims.getClaimValue("email", classOf[String]) // jose4j returns Long after JSON round-trip but the original setClaim // call writes Integer; widen via Number to handle both cases. val userId = claims.getClaimValue("userId", classOf[Number]).intValue() val role = UserRoleEnum.valueOf(claims.getClaimValue("role").asInstanceOf[String]) val googleId = claims.getClaimValue("googleId", classOf[String]) val googleAvatar = claims.getClaimValue("googleAvatar", classOf[String]) val user = new User( userId, userName, email, null, googleId, googleAvatar, role, null, null, null, null ) new SessionUser(user) } } ================================================ FILE: common/auth/src/main/scala/org/apache/texera/auth/RequestLoggingFilter.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.auth import jakarta.servlet._ import jakarta.servlet.http.{HttpServletRequest, HttpServletResponse} import org.slf4j.LoggerFactory /** * Servlet filter that logs HTTP requests through SLF4J at INFO level. * This replaces Dropwizard's built-in request log (which uses a separate * access log pipeline not controllable by log level) so that request logs * are fully controlled by the TEXERA_SERVICE_LOG_LEVEL environment variable. */ class RequestLoggingFilter extends Filter { private val logger = LoggerFactory.getLogger("org.eclipse.jetty.server.RequestLog") override def doFilter( request: ServletRequest, response: ServletResponse, chain: FilterChain ): Unit = { chain.doFilter(request, response) if (logger.isInfoEnabled) { val req = request.asInstanceOf[HttpServletRequest] val resp = response.asInstanceOf[HttpServletResponse] logger.info( s"""${req.getRemoteAddr} - "${req.getMethod} ${req.getRequestURI} ${req.getProtocol}" ${resp.getStatus}""" ) } } } object RequestLoggingFilter { /** * Registers the request logging filter on the given servlet context. * Usage: RequestLoggingFilter.register(environment.getApplicationContext) */ def register(context: org.eclipse.jetty.servlet.ServletContextHandler): Unit = { context.addFilter( new org.eclipse.jetty.servlet.FilterHolder(new RequestLoggingFilter), "/*", java.util.EnumSet.allOf(classOf[DispatcherType]) ) } } ================================================ FILE: common/auth/src/main/scala/org/apache/texera/auth/SessionUser.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.auth import org.apache.texera.dao.jooq.generated.enums.UserRoleEnum import org.apache.texera.dao.jooq.generated.tables.pojos.User import java.security.Principal class SessionUser(val user: User) extends Principal { def getUser: User = user override def getName: String = user.getName def getUid: Integer = user.getUid def getEmail: String = user.getEmail def getGoogleId: String = user.getGoogleId def isRoleOf(role: UserRoleEnum): Boolean = user.getRole == role } ================================================ FILE: common/auth/src/main/scala/org/apache/texera/auth/UserActivityTracker.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.auth import com.typesafe.scalalogging.LazyLogging import org.apache.texera.dao.SqlServer import org.apache.texera.dao.jooq.generated.Tables.USER_LAST_ACTIVE_TIME import java.time.{Duration, Instant, OffsetDateTime, ZoneOffset} import java.util.concurrent.{ ArrayBlockingQueue, ConcurrentHashMap, Executor, Executors, ScheduledExecutorService, ThreadFactory, ThreadPoolExecutor, TimeUnit } import scala.util.control.NonFatal /** Per-uid activity timestamp recorder. The actual DB upsert is throttled * by a per-uid in-memory cooldown so that a user hitting the API at high * RPS produces at most one USER_LAST_ACTIVE_TIME write per * `writeInterval`. The upsert itself runs on the supplied `executor` so * request threads never wait on DB latency. * * Class form (with injectable upsert / executor / clock) exists so the * cooldown/CAS logic can be unit-tested without a DB. The companion * object [[UserActivityTracker]] is the production singleton. */ class UserActivityTracker( writeInterval: Duration, upsertFn: (Integer, Instant) => Unit, executor: Executor, clock: () => Instant ) extends LazyLogging { private val lastClaimed = new ConcurrentHashMap[Integer, Instant]() // Eviction window: an entry is stale once 2*writeInterval has passed // since its last claim. The factor keeps in-cooldown entries safe from // eviction while still bounding `lastClaimed` for users who have gone // away. private val staleAfter: Duration = writeInterval.multipliedBy(2) /** Record the user as active. Lock-free; performs at most one upsert per * uid per `writeInterval`. Never propagates failures to the caller. */ def markActive(uid: Integer): Unit = { if (uid == null) return try { val now = clock() val prev = lastClaimed.get(uid) if (prev != null && Duration.between(prev, now).compareTo(writeInterval) < 0) return // CAS to claim the write slot for this uid. If another thread won // the race, drop this call. val claimed = if (prev == null) lastClaimed.putIfAbsent(uid, now) == null else lastClaimed.replace(uid, prev, now) if (!claimed) return executor.execute(() => try upsertFn(uid, now) catch { case NonFatal(e) => logger.warn(s"User activity upsert failed (uid=$uid)", e) } ) } catch { case NonFatal(e) => logger.warn(s"markActive failed (uid=$uid)", e) } } /** Drop entries whose last-claimed time is older than `2 * writeInterval`. * Bounds `lastClaimed` for long-lived processes with many distinct uids. * Safe to call concurrently with [[markActive]]. */ def evictStale(): Unit = { try { val cutoff = clock().minus(staleAfter) lastClaimed.entrySet().removeIf(e => e.getValue.isBefore(cutoff)) } catch { case NonFatal(e) => logger.warn("evictStale failed", e) } } /** Visible for tests. */ private[auth] def cooldownSize: Int = lastClaimed.size() } object UserActivityTracker extends LazyLogging { private val WRITE_INTERVAL: Duration = Duration.ofMinutes(5) // Bounded queue: under DB stalls or write storms, oldest pending tasks // are dropped (DiscardOldest). The next request from the same uid will // re-claim and re-write once cooldown elapses, so dropping a stale // pending write does not lose the activity signal long-term. private val WRITER_QUEUE_CAPACITY = 256 private val writer: Executor = new ThreadPoolExecutor( 1, 1, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue[Runnable](WRITER_QUEUE_CAPACITY), daemonThreadFactory("user-activity-writer"), new ThreadPoolExecutor.DiscardOldestPolicy ) private val instance = new UserActivityTracker( WRITE_INTERVAL, defaultUpsert, writer, () => Instant.now() ) // Periodic eviction of stale uid entries, running once per WRITE_INTERVAL. private val cleanup: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(daemonThreadFactory("user-activity-cleanup")) cleanup.scheduleAtFixedRate( () => instance.evictStale(), WRITE_INTERVAL.toMillis, WRITE_INTERVAL.toMillis, TimeUnit.MILLISECONDS ) /** Production entry point. Delegates to the singleton tracker. */ def markActive(uid: Integer): Unit = instance.markActive(uid) private def defaultUpsert(uid: Integer, ts: Instant): Unit = { val ctx = SqlServer.getInstance().createDSLContext() val odt = OffsetDateTime.ofInstant(ts, ZoneOffset.UTC) ctx .insertInto(USER_LAST_ACTIVE_TIME) .set(USER_LAST_ACTIVE_TIME.UID, uid) .set(USER_LAST_ACTIVE_TIME.LAST_ACTIVE_TIME, odt) .onConflict(USER_LAST_ACTIVE_TIME.UID) .doUpdate() .set(USER_LAST_ACTIVE_TIME.LAST_ACTIVE_TIME, odt) .execute() } private def daemonThreadFactory(name: String): ThreadFactory = (r: Runnable) => { val t = new Thread(r, name) t.setDaemon(true) t } } ================================================ FILE: common/auth/src/main/scala/org/apache/texera/auth/util/ComputingUnitAccess.scala ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package org.apache.texera.auth.util import org.apache.texera.dao.SqlServer import org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum import org.apache.texera.dao.jooq.generated.tables.daos.{ ComputingUnitUserAccessDao, WorkflowComputingUnitDao } import org.jooq.DSLContext import scala.jdk.CollectionConverters._ object ComputingUnitAccess { private def context: DSLContext = SqlServer .getInstance() .createDSLContext() def getComputingUnitAccess(cuid: Integer, uid: Integer): PrivilegeEnum = { val workflowComputingUnitDao = new WorkflowComputingUnitDao(context.configuration()) val unit = workflowComputingUnitDao.fetchOneByCuid(cuid) if (unit.getUid.equals(uid)) { return PrivilegeEnum.WRITE // owner has write access } val computingUnitUserAccessDao = new ComputingUnitUserAccessDao(context.configuration()) val accessOpt = computingUnitUserAccessDao .fetchByUid(uid) .asScala .find(_.getCuid.equals(cuid)) accessOpt match { case Some(access) => access.getPrivilege case None => PrivilegeEnum.NONE } } } ================================================ FILE: common/auth/src/main/scala/org/apache/texera/auth/util/HeaderField.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.auth.util object HeaderField { val UserComputingUnitAccess = "x-user-computing-unit-access" val UserId = "x-user-id" val UserName = "x-user-name" val UserEmail = "x-user-email" } ================================================ FILE: common/auth/src/test/scala/org/apache/texera/auth/JwtParserSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.auth import org.apache.texera.dao.jooq.generated.enums.UserRoleEnum import org.apache.texera.dao.jooq.generated.tables.pojos.User import org.jose4j.jws.AlgorithmIdentifiers.HMAC_SHA256 import org.jose4j.jws.JsonWebSignature import org.jose4j.jwt.JwtClaims import org.jose4j.keys.HmacKey import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import java.nio.charset.StandardCharsets class JwtParserSpec extends AnyFlatSpec with Matchers { private def buildClaims(): JwtClaims = { // Mirror exactly what JwtAuth.jwtClaims would write at issue time, so // this spec doubles as a contract test between the issuer and parser. val claims = new JwtClaims claims.setSubject("alice") claims.setClaim("userId", 42) claims.setClaim("googleId", "g-123") claims.setClaim("email", "alice@example.com") claims.setClaim("role", UserRoleEnum.ADMIN.name) claims.setClaim("googleAvatar", "avatar-blob") claims.setExpirationTimeMinutesInTheFuture(10f) claims } "JwtParser.claimsToSessionUser" should "populate every issued claim including googleAvatar" in { val user: User = JwtParser.claimsToSessionUser(buildClaims()).getUser user.getUid shouldBe 42 user.getName shouldBe "alice" user.getEmail shouldBe "alice@example.com" user.getGoogleId shouldBe "g-123" user.getGoogleAvatar shouldBe "avatar-blob" user.getRole shouldBe UserRoleEnum.ADMIN } it should "leave non-issued slots null (password, comment, accountCreation, affiliation, joiningReason)" in { val user: User = JwtParser.claimsToSessionUser(buildClaims()).getUser user.getPassword shouldBe null user.getComment shouldBe null user.getAccountCreationTime shouldBe null user.getAffiliation shouldBe null user.getJoiningReason shouldBe null } it should "round-trip a token issued by JwtAuth.jwtToken" in { val token = JwtAuth.jwtToken(buildClaims()) val parsed = JwtParser.parseToken(token) parsed.isPresent shouldBe true val u = parsed.get().getUser u.getUid shouldBe 42 u.getGoogleAvatar shouldBe "avatar-blob" } "JwtParser.parseToken" should "return empty on a structurally invalid token" in { JwtParser.parseToken("not-a-real-jwt").isPresent shouldBe false } it should "return empty when the token is signed with the wrong secret" in { val token = signWith(buildClaims(), "definitely-not-the-real-secret-for-testing-only") JwtParser.parseToken(token).isPresent shouldBe false } it should "return empty when the token is expired" in { val claims = new JwtClaims claims.setSubject("alice") claims.setClaim("userId", 42) claims.setClaim("role", UserRoleEnum.ADMIN.name) claims.setExpirationTimeMinutesInTheFuture(-10f) // expired 10 minutes ago val token = JwtAuth.jwtToken(claims) JwtParser.parseToken(token).isPresent shouldBe false } it should "return empty when the token has no subject claim" in { val claims = new JwtClaims claims.setClaim("userId", 42) claims.setClaim("role", UserRoleEnum.ADMIN.name) claims.setExpirationTimeMinutesInTheFuture(10f) val token = JwtAuth.jwtToken(claims) JwtParser.parseToken(token).isPresent shouldBe false } it should "return empty when the token has no exp claim" in { // The shared consumer is built with setRequireExpirationTime(); a token // missing exp must be rejected even if the signature is valid. val claims = new JwtClaims claims.setSubject("alice") claims.setClaim("userId", 42) claims.setClaim("role", UserRoleEnum.ADMIN.name) val token = JwtAuth.jwtToken(claims) JwtParser.parseToken(token).isPresent shouldBe false } it should "still accept a token expired within the 30s clock-skew window" in { val claims = buildClaims() // 5 seconds ago — well inside setAllowedClockSkewInSeconds(30). claims.setExpirationTimeMinutesInTheFuture(-0.083f) val token = JwtAuth.jwtToken(claims) JwtParser.parseToken(token).isPresent shouldBe true } it should "reject a token expired beyond the 30s clock-skew window" in { val claims = buildClaims() // 90 seconds ago — past the 30s allowance. claims.setExpirationTimeMinutesInTheFuture(-1.5f) val token = JwtAuth.jwtToken(claims) JwtParser.parseToken(token).isPresent shouldBe false } it should "return empty when the signature segment is tampered" in { val token = JwtAuth.jwtToken(buildClaims()) val parts = token.split('.') parts.length shouldBe 3 val tampered = s"${parts(0)}.${parts(1)}.${parts(2).reverse}" JwtParser.parseToken(tampered).isPresent shouldBe false } it should "return empty when the payload segment is tampered" in { // Re-base64 a claim with a different userId; the signature in parts(2) // covers parts(0).parts(1), so the rebuilt token won't verify. val token = JwtAuth.jwtToken(buildClaims()) val parts = token.split('.') val swappedClaims = new JwtClaims swappedClaims.setSubject("mallory") swappedClaims.setClaim("userId", 99999) swappedClaims.setClaim("role", UserRoleEnum.ADMIN.name) swappedClaims.setExpirationTimeMinutesInTheFuture(10f) val rebuiltPayload = java.util.Base64.getUrlEncoder.withoutPadding.encodeToString( swappedClaims.toJson.getBytes(StandardCharsets.UTF_8) ) val tampered = s"${parts(0)}.$rebuiltPayload.${parts(2)}" JwtParser.parseToken(tampered).isPresent shouldBe false } "JwtParser life cycle" should "round-trip distinct users without state leakage" in { val alice = buildClaims() val bob = new JwtClaims bob.setSubject("bob") bob.setClaim("userId", 7) bob.setClaim("googleId", "g-bob") bob.setClaim("email", "bob@example.com") bob.setClaim("role", UserRoleEnum.REGULAR.name) bob.setClaim("googleAvatar", "bob-avatar") bob.setExpirationTimeMinutesInTheFuture(10f) val aliceUser = JwtParser.parseToken(JwtAuth.jwtToken(alice)).get().getUser val bobUser = JwtParser.parseToken(JwtAuth.jwtToken(bob)).get().getUser aliceUser.getUid shouldBe 42 aliceUser.getName shouldBe "alice" aliceUser.getRole shouldBe UserRoleEnum.ADMIN bobUser.getUid shouldBe 7 bobUser.getName shouldBe "bob" bobUser.getRole shouldBe UserRoleEnum.REGULAR } it should "produce equivalent SessionUser objects when re-parsed multiple times" in { val token = JwtAuth.jwtToken(buildClaims()) val first = JwtParser.parseToken(token).get().getUser val second = JwtParser.parseToken(token).get().getUser first.getUid shouldBe second.getUid first.getName shouldBe second.getName first.getEmail shouldBe second.getEmail first.getGoogleAvatar shouldBe second.getGoogleAvatar first.getRole shouldBe second.getRole } /** Sign a JwtClaims payload with an arbitrary secret. Used to produce a * token whose signature won't verify against the real consumer's key. */ private def signWith(claims: JwtClaims, secret: String): String = { val jws = new JsonWebSignature jws.setPayload(claims.toJson) jws.setAlgorithmHeaderValue(HMAC_SHA256) jws.setKey(new HmacKey(secret.getBytes(StandardCharsets.UTF_8))) jws.getCompactSerialization } } ================================================ FILE: common/auth/src/test/scala/org/apache/texera/auth/UserActivityTrackerSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.auth import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import java.time.{Duration, Instant} import java.util.concurrent.{ConcurrentLinkedQueue, Executor} import java.util.concurrent.atomic.AtomicReference class UserActivityTrackerSpec extends AnyFlatSpec with Matchers { // Synchronous executor: runnable runs on the calling thread, so the // test can observe upsert invocations deterministically. private val sameThread: Executor = (cmd: Runnable) => cmd.run() private class Recorder { val calls = new ConcurrentLinkedQueue[(Integer, Instant)]() def upsert(uid: Integer, ts: Instant): Unit = { calls.add((uid, ts)); () } } private def makeTracker( writeInterval: Duration, recorder: Recorder, clock: AtomicReference[Instant] ) = new UserActivityTracker(writeInterval, recorder.upsert, sameThread, () => clock.get()) "UserActivityTracker" should "trigger an upsert on the first call for a uid" in { val recorder = new Recorder val now = Instant.parse("2026-01-01T00:00:00Z") val clock = new AtomicReference[Instant](now) val tracker = makeTracker(Duration.ofMinutes(5), recorder, clock) tracker.markActive(42) recorder.calls.size shouldBe 1 val (uid, ts) = recorder.calls.peek() uid shouldBe 42 ts shouldBe now } it should "skip upserts within the cooldown window" in { val recorder = new Recorder val t0 = Instant.parse("2026-01-01T00:00:00Z") val clock = new AtomicReference[Instant](t0) val tracker = makeTracker(Duration.ofMinutes(5), recorder, clock) tracker.markActive(42) clock.set(t0.plus(Duration.ofMinutes(2))) tracker.markActive(42) clock.set(t0.plus(Duration.ofMinutes(4).plusSeconds(59))) tracker.markActive(42) recorder.calls.size shouldBe 1 } it should "fire another upsert once the cooldown elapses" in { val recorder = new Recorder val t0 = Instant.parse("2026-01-01T00:00:00Z") val clock = new AtomicReference[Instant](t0) val tracker = makeTracker(Duration.ofMinutes(5), recorder, clock) tracker.markActive(42) clock.set(t0.plus(Duration.ofMinutes(5))) tracker.markActive(42) recorder.calls.size shouldBe 2 } it should "track different uids independently" in { val recorder = new Recorder val clock = new AtomicReference[Instant](Instant.parse("2026-01-01T00:00:00Z")) val tracker = makeTracker(Duration.ofMinutes(5), recorder, clock) tracker.markActive(1) tracker.markActive(2) tracker.markActive(3) recorder.calls.size shouldBe 3 } it should "treat null uid as a no-op" in { val recorder = new Recorder val clock = new AtomicReference[Instant](Instant.parse("2026-01-01T00:00:00Z")) val tracker = makeTracker(Duration.ofMinutes(5), recorder, clock) tracker.markActive(null) recorder.calls.size shouldBe 0 } it should "evict cooldown entries older than 2 * writeInterval" in { val recorder = new Recorder val t0 = Instant.parse("2026-01-01T00:00:00Z") val clock = new AtomicReference[Instant](t0) val tracker = makeTracker(Duration.ofMinutes(5), recorder, clock) tracker.markActive(1) tracker.markActive(2) tracker.cooldownSize shouldBe 2 // 9 minutes — under 2 * writeInterval (10), nothing evicted clock.set(t0.plus(Duration.ofMinutes(9))) tracker.evictStale() tracker.cooldownSize shouldBe 2 // 11 minutes — past 2 * writeInterval, both entries evicted clock.set(t0.plus(Duration.ofMinutes(11))) tracker.evictStale() tracker.cooldownSize shouldBe 0 } it should "swallow upsertFn exceptions instead of propagating to the caller" in { val t0 = Instant.parse("2026-01-01T00:00:00Z") val clock = new AtomicReference[Instant](t0) val throwing: (Integer, Instant) => Unit = (_, _) => throw new RuntimeException("simulated DB outage") val tracker = new UserActivityTracker(Duration.ofMinutes(5), throwing, sameThread, () => clock.get()) // Must not throw — the wrapper catches NonFatal from upsertFn. noException should be thrownBy tracker.markActive(42) } } ================================================ FILE: common/config/build.sbt ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. import scala.collection.Seq name := "config" enablePlugins(JavaAppPackaging) // Enable semanticdb for Scalafix ThisBuild / semanticdbEnabled := true ThisBuild / semanticdbVersion := scalafixSemanticdb.revision // Manage dependency conflicts by always using the latest revision ThisBuild / conflictManager := ConflictManager.latestRevision // Restrict parallel execution of tests to avoid conflicts Global / concurrentRestrictions += Tags.limit(Tags.Test, 1) ///////////////////////////////////////////////////////////////////////////// // Compiler Options ///////////////////////////////////////////////////////////////////////////// // Scala compiler options Compile / scalacOptions ++= Seq( "-Xelide-below", "WARNING", // Turn on optimizations with "WARNING" as the threshold "-feature", // Check feature warnings "-deprecation", // Check deprecation warnings "-Ywarn-unused:imports" // Check for unused imports ) ///////////////////////////////////////////////////////////////////////////// // Dependencies ///////////////////////////////////////////////////////////////////////////// // Core Dependencies libraryDependencies ++= Seq( "com.typesafe" % "config" % "1.4.6" // For configuration management ) ================================================ FILE: common/config/src/main/resources/application.conf ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # See PR https://github.com/Texera/texera/pull/3326 for configuration guidelines. constants { logging-queue-size-interval = 30000 logging-queue-size-interval = ${?CONSTANTS_LOGGING_QUEUE_SIZE_INTERVAL} num-worker-per-operator = 2 num-worker-per-operator = ${?CONSTANTS_NUM_WORKER_PER_OPERATOR} max-resolution-rows = 2000 max-resolution-rows = ${?CONSTANTS_MAX_RESOLUTION_ROWS} max-resolution-columns = 2000 max-resolution-columns = ${?CONSTANTS_MAX_RESOLUTION_COLUMNS} status-update-interval = 500 status-update-interval = ${?CONSTANTS_STATUS_UPDATE_INTERVAL} runtime-statistics-persistence-interval = 2000 runtime-statistics-persistence-interval = ${?CONSTANTS_RUNTIME_STATISTICS_PERSISTENCE_INTERVAL} } flow-control { max-credit-allowed-in-bytes-per-channel = 1600000000 # -1 to disable flow control max-credit-allowed-in-bytes-per-channel = ${?FLOW_CONTROL_MAX_CREDIT_ALLOWED_IN_BYTES_PER_CHANNEL} credit-poll-interval-in-ms = 200 credit-poll-interval-in-ms = ${?FLOW_CONTROL_CREDIT_POLL_INTERVAL_IN_MS} } network-buffering { default-data-transfer-batch-size = 400 default-data-transfer-batch-size = ${?NETWORK_BUFFERING_DEFAULT_DATA_TRANSFER_BATCH_SIZE} enable-adaptive-buffering = true enable-adaptive-buffering = ${?NETWORK_BUFFERING_ENABLE_ADAPTIVE_BUFFERING} adaptive-buffering-timeout-ms = 500 adaptive-buffering-timeout-ms = ${?NETWORK_BUFFERING_ADAPTIVE_BUFFERING_TIMEOUT_MS} } reconfiguration { enable-transactional-reconfiguration = false enable-transactional-reconfiguration = ${?RECONFIGURATION_ENABLE_TRANSACTIONAL_RECONFIGURATION} } cache { # [false, true] enabled = true enabled = ${?CACHE_ENABLED} } result-cleanup { ttl-in-seconds = 86400 # time to live for a collection is 2 days ttl-in-seconds = ${?RESULT_CLEANUP_TTL_IN_SECONDS} collection-check-interval-in-seconds = 86400 # 2 days collection-check-interval-in-seconds = ${?RESULT_CLEANUP_COLLECTION_CHECK_INTERVAL_IN_SECONDS} } web-server { workflow-state-cleanup-in-seconds = 30 workflow-state-cleanup-in-seconds = ${?WEB_SERVER_WORKFLOW_STATE_CLEANUP_IN_SECONDS} python-console-buffer-size = 100 python-console-buffer-size = ${?WEB_SERVER_PYTHON_CONSOLE_BUFFER_SIZE} console-message-max-display-length = 100 console-message-max-display-length = ${?WEB_SERVER_CONSOLE_MESSAGE_MAX_DISPLAY_LENGTH} workflow-result-pulling-in-seconds = 3 workflow-result-pulling-in-seconds = ${?WEB_SERVER_WORKFLOW_RESULT_PULLING_IN_SECONDS} clean-all-execution-results-on-server-start = false clean-all-execution-results-on-server-start = ${?WEB_SERVER_CLEAN_ALL_EXECUTION_RESULTS_ON_SERVER_START} max-workflow-websocket-request-payload-size-kb = 64 max-workflow-websocket-request-payload-size-kb = ${?MAX_WORKFLOW_WEBSOCKET_REQUEST_PAYLOAD_SIZE_KB} } fault-tolerance { # URI for storage, empty to disable logging. # Use absolute path only. for local file system, $AMBER_FOLDER will be interpolated to Amber folder path. # e.g. use "file://$AMBER_FOLDER/logs/recovery-logs/" for local logging. log-storage-uri = "" log-storage-uri = ${?FAULT_TOLERANCE_LOG_STORAGE_URI} log-flush-interval-ms = 0 # immediately flush log-flush-interval-ms = ${?FAULT_TOLERANCE_LOG_FLUSH_INTERVAL_MS} log-record-max-size-in-byte = 67108864 # 64MB log-record-max-size-in-byte = ${?FAULT_TOLERANCE_LOG_RECORD_MAX_SIZE_IN_BYTE} # limit for resend buffer length, if the resend buffer # getting too large, the workflow aborts during recovery to avoid OOM. # TODO: Remove this after introducing checkpoints. max-supported-resend-queue-length = 10000 max-supported-resend-queue-length = ${?FAULT_TOLERANCE_MAX_SUPPORTED_RESEND_QUEUE_LENGTH} delay-before-recovery = 3000 delay-before-recovery = ${?FAULT_TOLERANCE_DELAY_BEFORE_RECOVERY} hdfs-storage { address = "0.0.0.0:9870" address = ${?FAULT_TOLERANCE_HDFS_STORAGE_ADDRESS} } } schedule-generator { max-concurrent-regions = 1 max-concurrent-regions = ${?SCHEDULE_GENERATOR_MAX_CONCURRENT_REGIONS} use-global-search = false use-global-search = ${?SCHEDULE_GENERATOR_USE_GLOBAL_SEARCH} use-top-down-search = false use-top-down-search = ${?SCHEDULE_GENERATOR_USE_TOP_DOWN_SEARCH} search-timeout-milliseconds = 1000 search-timeout-milliseconds = ${?SCHEDULE_GENERATOR_SEARCH_TIMEOUT_MILLISECONDS} } ai-assistant-server { assistant = "none" assistant = ${?AI_ASSISTANT_SERVER_ASSISTANT} # Put your Ai Service authentication key here ai-service-key = "" ai-service-key = ${?AI_ASSISTANT_SERVER_AI_SERVICE_KEY} # Put your Ai service url here (If you are using OpenAI, then the url should be "https://api.openai.com/v1") ai-service-url = "" ai-service-url = ${?AI_ASSISTANT_SERVER_AI_SERVICE_URL} } ================================================ FILE: common/config/src/main/resources/auth.conf ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # See PR https://github.com/Texera/texera/pull/3326 for configuration guidelines. # Configuration for JWT Authentication. Currently it is used by the FileService to parse the given JWT Token auth { jwt { expiration-in-minutes = 10080 expiration-in-minutes = ${?AUTH_JWT_EXPIRATION_IN_MINUTES} 256-bit-secret = "8a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d" 256-bit-secret = ${?AUTH_JWT_SECRET} } } ================================================ FILE: common/config/src/main/resources/cluster.conf ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. pekko { # Loggers to register at boot time (pekko.event.Logging$DefaultLogger logs # to STDOUT) loggers = ["org.apache.pekko.event.slf4j.Slf4jLogger"] # Log level used by the configured loggers (see "loggers") as soon # as they have been started; before that, see "stdout-loglevel" # Options: OFF, ERROR, WARNING, INFO, DEBUG loglevel = "INFO" loglevel = ${?TEXERA_SERVICE_LOG_LEVEL} # Log level for the very basic logger activated during ActorSystem startup. # This logger prints the log messages to stdout (System.out). # Options: OFF, ERROR, WARNING, INFO, DEBUG stdout-loglevel = "INFO" # Filter of log events that is used by the LoggingAdapter before # publishing log events to the eventStream. logging-filter = "org.apache.pekko.event.slf4j.Slf4jLoggingFilter" actor { provider = cluster enable-additional-serialization-bindings = on allow-java-serialization = off serializers { kryo = "io.altoo.serialization.kryo.pekko.PekkoKryoSerializer" } serialization-bindings { "java.io.Serializable" = kryo "java.lang.Throwable" = pekko-misc } } remote { artery { transport = tcp canonical.hostname = "0.0.0.0" canonical.port = 0 advanced.maximum-frame-size = 30 MiB advanced.maximum-large-frame-size = 120 MiB } } cluster { seed-nodes = [] # auto downing is NOT safe for production deployments. # you may want to use it during development, read more about it in the docs. auto-down-unreachable-after = off downing-provider-class = "org.apache.pekko.cluster.sbr.SplitBrainResolverProvider" unreachable-nodes-reaper-interval = 5s gossip-interval = 10s leader-actions-interval = 10s gossip-time-to-live = 20s failure-detector { heartbeat-interval = 10s acceptable-heartbeat-pause = 50s expected-response-after = 30s } } } pekko-kryo-serialization.kryo-initializer = "org.apache.texera.amber.engine.common.AmberKryoInitializer" ================================================ FILE: common/config/src/main/resources/computing-unit.conf ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. computing-unit { # local computing unit configuration local { enabled = true enabled = ${?COMPUTING_UNIT_LOCAL_ENABLED} } # TODO: move the kubernetes configuration to here # whether sharing computing unit is allowed or not sharing { enabled = false enabled = ${?COMPUTING_UNIT_SHARING_ENABLED} } } ================================================ FILE: common/config/src/main/resources/default.conf ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # This file is used to configure the default appearance and features of the Texera. # These defaults are loaded into the database at startup or when resetting settings, and can be # overridden at runtime by administrators via the Admin Settings page. config-service { # Setting to true resets all site settings in the database to the defaults defined in this file. always-reset-configurations-to-default-values = false always-reset-configurations-to-default-values = ${?CONFIG_SERVICE_ALWAYS_RESET_CONFIGURATIONS_TO_DEFAULT_VALUES} } gui { logo { logo = "assets/logos/logo.png" logo = ${?GUI_LOGO_LOGO} mini_logo = "assets/logos/full_logo_small.png" mini_logo = ${?GUI_LOGO_MINI_LOGO} favicon = "assets/logos/favicon-32x32.png" favicon = ${?GUI_LOGO_FAVICON} } tabs { # config for hub tabs hub_enabled = true hub_enabled = ${?GUI_TABS_HUB_ENABLED} home_enabled = true home_enabled = ${?GUI_TABS_HOME_ENABLED} workflow_enabled = true workflow_enabled = ${?GUI_TABS_WORKFLOW_ENABLED} dataset_enabled = true dataset_enabled = ${?GUI_TABS_DATASET_ENABLED} # config for your work tabs your_work_enabled = true your_work_enabled = ${?GUI_TABS_YOUR_WORK_ENABLED} projects_enabled = false projects_enabled = ${?GUI_TABS_PROJECTS_ENABLED} workflows_enabled = true workflows_enabled = ${?GUI_TABS_WORKFLOWS_ENABLED} datasets_enabled = true datasets_enabled = ${?GUI_TABS_DATASETS_ENABLED} compute_enabled = true compute_enabled = ${?GUI_TABS_COMPUTE_ENABLED} quota_enabled = true quota_enabled = ${?GUI_TABS_QUOTA_ENABLED} forum_enabled = false forum_enabled = ${?GUI_TABS_FORUM_ENABLED} # config for about tab about_enabled = true about_enabled = ${?GUI_TABS_ABOUT_ENABLED} } } dataset { single_file_upload_max_size_mib = 20 single_file_upload_max_size_mib = ${?DATASET_SINGLE_FILE_UPLOAD_MAX_SIZE_MIB} max_number_of_concurrent_uploading_file = 3 max_number_of_concurrent_uploading_file = ${?MAX_NUMBER_OF_CONCURRENT_UPLOADING_FILE} # The maximum number of file chunks that can be held in the memory max_number_of_concurrent_uploading_file_chunks = 10 max_number_of_concurrent_uploading_file_chunks = ${?DATASET_MAX_NUMBER_OF_CONCURRENT_UPLOADING_FILE_CHUNKS} # the size of each chunk during the multipart upload of file multipart_upload_chunk_size_mib = 50 multipart_upload_chunk_size_mib = ${?DATASET_MULTIPART_UPLOAD_CHUNK_SIZE_MIB} } ================================================ FILE: common/config/src/main/resources/gui.conf ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # This file is used to configure the GUI shown in users' browser. # To add a new configuration: # 1. Add a new property, its default value and the environment variable name in the gui section in this file # 2. Add the corresponding parsing logic in core/config/src/main/scala/org/apache/texera/config/GuiConfig.scala # 3. Add the sending logic in core/src/main/scala/org/apache/texera/web/resource/UserConfigResource.scala # 4. Add the frontend definition in frontend/src/app/common/type/gui-config.ts and frontend/src/app/common/service/gui-config.service.mock.ts # To change the default value of a configuration: # 1. Change the default value in the gui section in this file # 2. Change the default value in frontend/src/app/common/service/gui-config.service.mock.ts gui { login { # whether local login is enabled local-login = true local-login = ${?GUI_LOGIN_LOCAL_LOGIN} # whether google login is enabled google-login = true google-login = ${?GUI_LOGIN_GOOGLE_LOGIN} # Can be configured as { username: "texera", password: "password" } # If configured, this will be automatically filled into the local login input box default-local-user { username = "" username = ${?GUI_LOGIN_DEFAULT_LOCAL_USER_USERNAME} password = "" password = ${?GUI_LOGIN_DEFAULT_LOCAL_USER_PASSWORD} } } workflow-workspace { # whether user preset feature is enabled, requires user system to be enabled user-preset-enabled = false user-preset-enabled = ${?GUI_WORKFLOW_WORKSPACE_USER_PRESET_ENABLED} # whether export execution result is supported export-execution-result-enabled = false export-execution-result-enabled = ${?GUI_WORKFLOW_WORKSPACE_EXPORT_EXECUTION_RESULT_ENABLED} # whether automatically correcting attribute name on change is enabled # see AutoAttributeCorrectionService for more details auto-attribute-correction-enabled = true auto-attribute-correction-enabled = ${?GUI_WORKFLOW_WORKSPACE_AUTO_ATTRIBUTE_CORRECTION_ENABLED} # default data transfer batch size for workflows default-data-transfer-batch-size = 400 default-data-transfer-batch-size = ${?GUI_WORKFLOW_WORKSPACE_DEFAULT_DATA_TRANSFER_BATCH_SIZE} # default execution mode for workflows, can be either MATERIALIZED or PIPELINED default-execution-mode = PIPELINED default-execution-mode = ${?GUI_WORKFLOW_WORKSPACE_DEFAULT_EXECUTION_MODE} # whether selecting files from datasets instead of the local file system. selecting-files-from-datasets-enabled = true selecting-files-from-datasets-enabled = ${?GUI_WORKFLOW_WORKSPACE_SELECTING_FILES_FROM_DATASETS_ENABLED} # whether workflow executions tracking feature is enabled workflow-executions-tracking-enabled = false workflow-executions-tracking-enabled = ${?GUI_WORKFLOW_WORKSPACE_WORKFLOW_EXECUTIONS_TRACKING_ENABLED} # whether linkBreakpoint is supported link-breakpoint-enabled = true link-breakpoint-enabled = ${?GUI_WORKFLOW_WORKSPACE_LINK_BREAKPOINT_ENABLED} # whether rendering jointjs components asynchronously async-rendering-enabled = false async-rendering-enabled = ${?GUI_WORKFLOW_WORKSPACE_ASYNC_RENDERING_ENABLED} # whether time-travel is enabled timetravel-enabled = false timetravel-enabled = ${?GUI_WORKFLOW_WORKSPACE_TIMETRAVEL_ENABLED} # Whether to connect to local or production shared editing server. Set to true if you have # reverse proxy set up for y-websocket. production-shared-editing-server = false production-shared-editing-server = ${?GUI_WORKFLOW_WORKSPACE_PRODUCTION_SHARED_EDITING_SERVER} # The port of the python language server. If not set, no port will be used in the final url python-language-server-port = 3000 python-language-server-port = ${?GUI_WORKFLOW_WORKSPACE_PYTHON_LANGUAGE_SERVER_PORT} # maximum number of console messages to store per operator operator-console-message-buffer-size = 100 operator-console-message-buffer-size = ${?GUI_WORKFLOW_WORKSPACE_OPERATOR_CONSOLE_MESSAGE_BUFFER_SIZE} # whether to send email notification when workflow execution is completed/failed/paused/killed workflow-email-notification-enabled = false workflow-email-notification-enabled = ${?GUI_WORKFLOW_WORKSPACE_WORKFLOW_EMAIL_NOTIFICATION_ENABLED} # amount of time to be elapsed in minutes before user is detected as inactive active-time-in-minutes = 15 active-time-in-minutes = ${?GUI_WORKFLOW_WORKSPACE_ACTIVE_TIME_IN_MINUTES} # whether AI copilot feature is enabled copilot-enabled = false copilot-enabled = ${?GUI_WORKFLOW_WORKSPACE_COPILOT_ENABLED} # the limit of columns to be displayed in the result table limit-columns = 15 limit-columns = ${?GUI_WORKFLOW_WORKSPACE_LIMIT_COLUMNS} } } ================================================ FILE: common/config/src/main/resources/kubernetes.conf ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. kubernetes { enabled = false enabled = ${?KUBERNETES_COMPUTING_UNIT_ENABLED} compute-unit-pool-name = "texera-workflow-computing-unit" compute-unit-pool-name = ${?KUBERNETES_COMPUTE_UNIT_POOL_NAME} compute-unit-pool-namespace = "texera-workflow-computing-unit-pool" compute-unit-pool-namespace = ${?KUBERNETES_COMPUTE_UNIT_POOL_NAMESPACE} compute-unit-service-name = "workflow-computing-unit-svc" compute-unit-service-name = ${?KUBERNETES_COMPUTE_UNIT_SERVICE_NAME} image-name = "bobbai/texera-workflow-computing-unit:dev" image-name = ${?KUBERNETES_IMAGE_NAME} image-pull-policy = "Always" image-pull-policy = ${?KUBERNETES_IMAGE_PULL_POLICY} port-num = 8085 # Configuration on how many computing units one user can create max-num-of-running-computing-units-per-user = 10 max-num-of-running-computing-units-per-user = ${?MAX_NUM_OF_RUNNING_COMPUTING_UNITS_PER_USER} computing-unit-cpu-limit-options = "1,2,4" computing-unit-cpu-limit-options = ${?KUBERNETES_COMPUTING_UNIT_CPU_LIMIT_OPTIONS} computing-unit-memory-limit-options = "1Gi,2Gi,4Gi" computing-unit-memory-limit-options = ${?KUBERNETES_COMPUTING_UNIT_MEMORY_LIMIT_OPTIONS} # GPU configuration computing-unit-gpu-limit-options = "0,1,2" computing-unit-gpu-limit-options = ${?KUBERNETES_COMPUTING_UNIT_GPU_LIMIT_OPTIONS} # GPU resource key used in Kubernetes (vendor-specific) computing-unit-gpu-resource-key = "nvidia.com/gpu" computing-unit-gpu-resource-key = ${?KUBERNETES_COMPUTING_UNIT_GPU_RESOURCE_KEY} } ================================================ FILE: common/config/src/main/resources/llm.conf ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # LLM Configuration llm { # Base URL for LiteLLM service base-url = "http://0.0.0.0:4000" base-url = ${?LITELLM_BASE_URL} # Master key for LiteLLM authentication master-key = "" master-key = ${?LITELLM_MASTER_KEY} } ================================================ FILE: common/config/src/main/resources/storage.conf ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # See PR https://github.com/Texera/texera/pull/3326 for configuration guidelines. storage { # Configuration for Apache Iceberg, used for storing the workflow results & stats iceberg { catalog { type = postgres # either hadoop, rest, or postgres type = ${?STORAGE_ICEBERG_CATALOG_TYPE} rest { uri = "http://localhost:8181/catalog" uri = ${?STORAGE_ICEBERG_CATALOG_REST_URI} warehouse-name = "texera" warehouse-name = ${?STORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME} s3-bucket = "texera-iceberg" s3-bucket = ${?STORAGE_ICEBERG_CATALOG_REST_S3_BUCKET} } postgres { # do not include scheme in the uri as Python and Java use different schemes uri-without-scheme = "localhost:5432/texera_iceberg_catalog" uri-without-scheme = ${?STORAGE_ICEBERG_CATALOG_POSTGRES_URI_WITHOUT_SCHEME} username = "postgres" username = ${?STORAGE_ICEBERG_CATALOG_POSTGRES_USERNAME} password = "postgres" password = ${?STORAGE_ICEBERG_CATALOG_POSTGRES_PASSWORD} } } table { result-namespace = "operator-port-result" result-namespace = ${?STORAGE_ICEBERG_TABLE_RESULT_NAMESPACE} console-messages-namespace = "operator-console-messages" console-messages-namespace = ${?STORAGE_ICEBERG_TABLE_CONSOLE_MESSAGES_NAMESPACE} runtime-statistics-namespace = "workflow-runtime-statistics" runtime-statistics-namespace = ${?STORAGE_ICEBERG_TABLE_RUNTIME_STATISTICS_NAMESPACE} commit { batch-size = 4096 # decide the buffer size of our IcebergTableWriter batch-size = ${?STORAGE_ICEBERG_TABLE_COMMIT_BATCH_SIZE} # retry configures the OCC parameter for concurrent write operations in Iceberg # Docs about Reliability in Iceberg: https://iceberg.apache.org/docs/1.7.1/reliability/ # Docs about full parameter list and their meaning: https://iceberg.apache.org/docs/1.7.1/configuration/#write-properties retry { num-retries = 10 num-retries = ${?STORAGE_ICEBERG_TABLE_COMMIT_NUM_RETRIES} min-wait-ms = 100 min-wait-ms = ${?STORAGE_ICEBERG_TABLE_COMMIT_MIN_WAIT_MS} max-wait-ms = 10000 max-wait-ms = ${?STORAGE_ICEBERG_TABLE_COMMIT_MAX_WAIT_MS} } } } } # Configurations of the LakeFS & S3 for dataset storage; # Default values are provided for each field, which you don't need to change them if you deployed LakeFS+S3 via docker-compose.yml in file-service/src/main/resources/docker-compose.yml lakefs { endpoint = "http://localhost:8000/api/v1" endpoint = ${?STORAGE_LAKEFS_ENDPOINT} auth { api-secret = "random_string_for_lakefs" api-secret = ${?STORAGE_LAKEFS_AUTH_API_SECRET} username = "AKIAIOSFOLKFSSAMPLES" username = ${?STORAGE_LAKEFS_AUTH_USERNAME} password = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" password = ${?STORAGE_LAKEFS_AUTH_PASSWORD} } block-storage { type = "s3" type = ${?STORAGE_LAKEFS_BLOCK_STORAGE_TYPE} bucket-name = "texera-dataset" bucket-name = ${?STORAGE_LAKEFS_BLOCK_STORAGE_BUCKET_NAME} } } s3 { endpoint = "http://localhost:9000" endpoint = ${?STORAGE_S3_ENDPOINT} region = "us-west-2" region = ${?STORAGE_S3_REGION} multipart { part-size = "16MB" part-size = ${?STORAGE_S3_MULTIPART_PART_SIZE} } auth { username = "texera_minio" username = ${?STORAGE_S3_AUTH_USERNAME} password = "password" password = ${?STORAGE_S3_AUTH_PASSWORD} } } # Configuration for Postgres, used for user system data & metadata storage jdbc { url = "jdbc:postgresql://localhost:5432/texera_db?currentSchema=texera_db,public" url = ${?STORAGE_JDBC_URL} # Some e2e test cases require the user system. To make sure running those test cases can pass the CI, and that # running them locally do not contaminate developers' own texera_db, we use another database with a different # name for running these test cases. url-for-test-cases = "jdbc:postgresql://localhost:5432/texera_db_for_test_cases?currentSchema=texera_db,public" url-for-test-cases = ${?STORAGE_JDBC_URL_FOR_TEST_CASES} username = "postgres" username = ${?STORAGE_JDBC_USERNAME} password = "postgres" password = ${?STORAGE_JDBC_PASSWORD} } } ================================================ FILE: common/config/src/main/resources/udf.conf ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. python { # python3 executable path path = "" path = ${?UDF_PYTHON_PATH} log { streamHandler { # handler output level level="INFO" level = ${?UDF_PYTHON_LOG_STREAMHANDLER_LEVEL} # handler log format format= "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} - {message}" format = ${?UDF_PYTHON_LOG_STREAMHANDLER_FORMAT} } fileHandler { # log directory dir= "/tmp/" dir = ${?UDF_PYTHON_LOG_FILEHANDLER_DIR} # handler output level level= "INFO" level = ${?UDF_PYTHON_LOG_FILEHANDLER_LEVEL} # handler log format format= "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} - {message}" format = ${?UDF_PYTHON_LOG_FILEHANDLER_FORMAT} } } } r { # Path to your R home here (if you want to use R-UDF) path = "" path = ${?UDF_R_PATH} } ================================================ FILE: common/config/src/main/resources/user-system.conf ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # See PR https://github.com/Texera/texera/pull/3326 for configuration guidelines. user-sys { admin-username = "texera" admin-username = ${?USER_SYS_ADMIN_USERNAME} admin-password = "texera" admin-password = ${?USER_SYS_ADMIN_PASSWORD} google { clientId = "" clientId = ${?USER_SYS_GOOGLE_CLIENT_ID} smtp { gmail = "" gmail = ${?USER_SYS_GOOGLE_SMTP_GMAIL} password = "" password = ${?USER_SYS_GOOGLE_SMTP_PASSWORD} } } domain = "" domain = ${?USER_SYS_DOMAIN} project-name = "Texera" project-name = ${?USER_SYS_PROJECT_NAME} invite-only = false invite-only = ${?USER_SYS_INVITE_ONLY} version-time-limit-in-minutes = 60 version-time-limit-in-minutes = ${?USER_SYS_VERSION_TIME_LIMIT_IN_MINUTES} } ================================================ FILE: common/config/src/main/scala/org/apache/texera/amber/config/ApplicationConfig.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.amber.config import com.typesafe.config.{Config, ConfigFactory} import java.io.File import java.net.URI object ApplicationConfig { private val configFile: File = new File("src/main/resources/application.conf") private var lastModifiedTime: Long = 0 private var conf: Config = ConfigFactory.load() // Perform lazy reload private def getConfSource: Config = { if (lastModifiedTime == configFile.lastModified()) { conf.resolve() } else { lastModifiedTime = configFile.lastModified() conf = ConfigFactory.parseFile(configFile).withFallback(ConfigFactory.load()) conf.resolve() } } // Constants val loggingQueueSizeInterval: Int = getConfSource.getInt("constants.logging-queue-size-interval") val MAX_RESOLUTION_ROWS: Int = getConfSource.getInt("constants.max-resolution-rows") val MAX_RESOLUTION_COLUMNS: Int = getConfSource.getInt("constants.max-resolution-columns") val numWorkerPerOperatorByDefault: Int = getConfSource.getInt("constants.num-worker-per-operator") val getStatusUpdateIntervalInMs: Long = getConfSource.getLong("constants.status-update-interval") val getRuntimeStatisticsPersistenceIntervalInMs: Long = getConfSource.getLong("constants.runtime-statistics-persistence-interval") // Flow control val maxCreditAllowedInBytesPerChannel: Long = getConfSource.getLong("flow-control.max-credit-allowed-in-bytes-per-channel") match { case -1L => Long.MaxValue case maxCredit => maxCredit } val creditPollingIntervalInMs: Int = getConfSource.getInt("flow-control.credit-poll-interval-in-ms") // Network buffering val defaultDataTransferBatchSize: Int = getConfSource.getInt("network-buffering.default-data-transfer-batch-size") val enableAdaptiveNetworkBuffering: Boolean = getConfSource.getBoolean("network-buffering.enable-adaptive-buffering") val adaptiveBufferingTimeoutMs: Int = getConfSource.getInt("network-buffering.adaptive-buffering-timeout-ms") // Reconfiguration val enableTransactionalReconfiguration: Boolean = getConfSource.getBoolean("reconfiguration.enable-transactional-reconfiguration") // Fault tolerance val faultToleranceLogFlushIntervalInMs: Long = getConfSource.getLong("fault-tolerance.log-flush-interval-ms") val faultToleranceLogRootFolder: Option[URI] = { Option(getConfSource.getString("fault-tolerance.log-storage-uri")) .filter(_.nonEmpty) .map(new URI(_)) } val isFaultToleranceEnabled: Boolean = faultToleranceLogRootFolder.nonEmpty // Scheduling val maxConcurrentRegions: Int = getConfSource.getInt("schedule-generator.max-concurrent-regions") val useGlobalSearch: Boolean = getConfSource.getBoolean("schedule-generator.use-global-search") val useTopDownSearch: Boolean = getConfSource.getBoolean("schedule-generator.use-top-down-search") val searchTimeoutMilliseconds: Int = getConfSource.getInt("schedule-generator.search-timeout-milliseconds") // Storage cleanup val sinkStorageTTLInSecs: Int = getConfSource.getInt("result-cleanup.ttl-in-seconds") val sinkStorageCleanUpCheckIntervalInSecs: Int = getConfSource.getInt("result-cleanup.collection-check-interval-in-seconds") // Web server val operatorConsoleBufferSize: Int = getConfSource.getInt("web-server.python-console-buffer-size") val consoleMessageDisplayLength: Int = getConfSource.getInt("web-server.console-message-max-display-length") val executionResultPollingInSecs: Int = getConfSource.getInt("web-server.workflow-result-pulling-in-seconds") val executionStateCleanUpInSecs: Int = getConfSource.getInt("web-server.workflow-state-cleanup-in-seconds") val cleanupAllExecutionResults: Boolean = getConfSource.getBoolean("web-server.clean-all-execution-results-on-server-start") val maxWorkflowWebsocketRequestPayloadSizeKb: Int = getConfSource.getInt("web-server.max-workflow-websocket-request-payload-size-kb") // AI Assistant val aiAssistantConfig: Option[Config] = if (getConfSource.hasPath("ai-assistant-server")) Some(getConfSource.getConfig("ai-assistant-server")) else None } ================================================ FILE: common/config/src/main/scala/org/apache/texera/amber/config/EnvironmentalVariable.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.amber.config // Environment variable names for all the *.conf files // TODO: currently these values are hard-coded, it would be good to have a way to dynamically load these names to avoid 2-copy object EnvironmentalVariable { // utility function to load the env var def get(name: String): Option[String] = { Option(System.getenv(name)) } /** * JVM related opts */ val ENV_JAVA_OPTS = "JAVA_OPTS" /** * FileService related endpoint */ val ENV_FILE_SERVICE_GET_PRESIGNED_URL_ENDPOINT = "FILE_SERVICE_GET_PRESIGNED_URL_ENDPOINT" val ENV_FILE_SERVICE_UPLOAD_ONE_FILE_TO_DATASET_ENDPOINT = "FILE_SERVICE_UPLOAD_ONE_FILE_TO_DATASET_ENDPOINT" /** * Auth related vars */ val ENV_USER_JWT_TOKEN = "USER_JWT_TOKEN" val ENV_AUTH_JWT_SECRET = "AUTH_JWT_SECRET" // JDBC val ENV_JDBC_URL = "STORAGE_JDBC_URL" val ENV_JDBC_USERNAME = "STORAGE_JDBC_USERNAME" val ENV_JDBC_PASSWORD = "STORAGE_JDBC_PASSWORD" // Iceberg Catalog val ENV_ICEBERG_CATALOG_TYPE = "STORAGE_ICEBERG_CATALOG_TYPE" val ENV_ICEBERG_CATALOG_REST_URI = "STORAGE_ICEBERG_CATALOG_REST_URI" val ENV_ICEBERG_CATALOG_REST_WAREHOUSE_NAME = "STORAGE_ICEBERG_CATALOG_REST_WAREHOUSE_NAME" // Iceberg Postgres Catalog val ENV_ICEBERG_CATALOG_POSTGRES_URI_WITHOUT_SCHEME = "STORAGE_ICEBERG_CATALOG_POSTGRES_URI_WITHOUT_SCHEME" val ENV_ICEBERG_CATALOG_POSTGRES_USERNAME = "STORAGE_ICEBERG_CATALOG_POSTGRES_USERNAME" val ENV_ICEBERG_CATALOG_POSTGRES_PASSWORD = "STORAGE_ICEBERG_CATALOG_POSTGRES_PASSWORD" // Iceberg Table val ENV_ICEBERG_TABLE_RESULT_NAMESPACE = "STORAGE_ICEBERG_TABLE_RESULT_NAMESPACE" val ENV_ICEBERG_TABLE_CONSOLE_MESSAGES_NAMESPACE = "STORAGE_ICEBERG_TABLE_CONSOLE_MESSAGES_NAMESPACE" val ENV_ICEBERG_TABLE_RUNTIME_STATISTICS_NAMESPACE = "STORAGE_ICEBERG_TABLE_RUNTIME_STATISTICS_NAMESPACE" val ENV_ICEBERG_TABLE_COMMIT_BATCH_SIZE = "STORAGE_ICEBERG_TABLE_COMMIT_BATCH_SIZE" val ENV_ICEBERG_TABLE_COMMIT_NUM_RETRIES = "STORAGE_ICEBERG_TABLE_COMMIT_NUM_RETRIES" val ENV_ICEBERG_TABLE_COMMIT_MIN_WAIT_MS = "STORAGE_ICEBERG_TABLE_COMMIT_MIN_WAIT_MS" val ENV_ICEBERG_TABLE_COMMIT_MAX_WAIT_MS = "STORAGE_ICEBERG_TABLE_COMMIT_MAX_WAIT_MS" // LakeFS val ENV_LAKEFS_ENDPOINT = "STORAGE_LAKEFS_ENDPOINT" val ENV_LAKEFS_AUTH_API_SECRET = "STORAGE_LAKEFS_AUTH_API_SECRET" val ENV_LAKEFS_AUTH_USERNAME = "STORAGE_LAKEFS_AUTH_USERNAME" val ENV_LAKEFS_AUTH_PASSWORD = "STORAGE_LAKEFS_AUTH_PASSWORD" val ENV_LAKEFS_BLOCK_STORAGE_TYPE = "STORAGE_LAKEFS_BLOCK_STORAGE_TYPE" val ENV_LAKEFS_BLOCK_STORAGE_BUCKET_NAME = "STORAGE_LAKEFS_BLOCK_STORAGE_BUCKET_NAME" // S3 val ENV_S3_ENDPOINT = "STORAGE_S3_ENDPOINT" val ENV_S3_REGION = "STORAGE_S3_REGION" val ENV_S3_AUTH_USERNAME = "STORAGE_S3_AUTH_USERNAME" val ENV_S3_AUTH_PASSWORD = "STORAGE_S3_AUTH_PASSWORD" /** * Variables in application.conf */ // Constants val ENV_CONSTANTS_LOGGING_QUEUE_SIZE_INTERVAL = "CONSTANTS_LOGGING_QUEUE_SIZE_INTERVAL" val ENV_CONSTANTS_NUM_WORKER_PER_OPERATOR = "CONSTANTS_NUM_WORKER_PER_OPERATOR" val ENV_CONSTANTS_MAX_RESOLUTION_ROWS = "CONSTANTS_MAX_RESOLUTION_ROWS" val ENV_CONSTANTS_MAX_RESOLUTION_COLUMNS = "CONSTANTS_MAX_RESOLUTION_COLUMNS" val ENV_CONSTANTS_STATUS_UPDATE_INTERVAL = "CONSTANTS_STATUS_UPDATE_INTERVAL" // Flow Control val ENV_FLOW_CONTROL_MAX_CREDIT_ALLOWED_IN_BYTES_PER_CHANNEL = "FLOW_CONTROL_MAX_CREDIT_ALLOWED_IN_BYTES_PER_CHANNEL" val ENV_FLOW_CONTROL_CREDIT_POLL_INTERVAL_IN_MS = "FLOW_CONTROL_CREDIT_POLL_INTERVAL_IN_MS" // Network Buffering val ENV_NETWORK_BUFFERING_DEFAULT_DATA_TRANSFER_BATCH_SIZE = "NETWORK_BUFFERING_DEFAULT_DATA_TRANSFER_BATCH_SIZE" val ENV_NETWORK_BUFFERING_ENABLE_ADAPTIVE_BUFFERING = "NETWORK_BUFFERING_ENABLE_ADAPTIVE_BUFFERING" val ENV_NETWORK_BUFFERING_ADAPTIVE_BUFFERING_TIMEOUT_MS = "NETWORK_BUFFERING_ADAPTIVE_BUFFERING_TIMEOUT_MS" // Reconfiguration val ENV_RECONFIGURATION_ENABLE_TRANSACTIONAL_RECONFIGURATION = "RECONFIGURATION_ENABLE_TRANSACTIONAL_RECONFIGURATION" // Cache val ENV_CACHE_ENABLED = "CACHE_ENABLED" // User System val ENV_USER_SYS_ENABLED = "USER_SYS_ENABLED" val ENV_USER_SYS_GOOGLE_CLIENT_ID = "USER_SYS_GOOGLE_CLIENT_ID" val ENV_USER_SYS_GOOGLE_SMTP_GMAIL = "USER_SYS_GOOGLE_SMTP_GMAIL" val ENV_USER_SYS_GOOGLE_SMTP_PASSWORD = "USER_SYS_GOOGLE_SMTP_PASSWORD" val ENV_USER_SYS_VERSION_TIME_LIMIT_IN_MINUTES = "USER_SYS_VERSION_TIME_LIMIT_IN_MINUTES" // Result Cleanup val ENV_RESULT_CLEANUP_TTL_IN_SECONDS = "RESULT_CLEANUP_TTL_IN_SECONDS" val ENV_RESULT_CLEANUP_COLLECTION_CHECK_INTERVAL_IN_SECONDS = "RESULT_CLEANUP_COLLECTION_CHECK_INTERVAL_IN_SECONDS" // Web Server val ENV_WEB_SERVER_WORKFLOW_STATE_CLEANUP_IN_SECONDS = "WEB_SERVER_WORKFLOW_STATE_CLEANUP_IN_SECONDS" val ENV_WEB_SERVER_PYTHON_CONSOLE_BUFFER_SIZE = "WEB_SERVER_PYTHON_CONSOLE_BUFFER_SIZE" val ENV_WEB_SERVER_WORKFLOW_RESULT_PULLING_IN_SECONDS = "WEB_SERVER_WORKFLOW_RESULT_PULLING_IN_SECONDS" val ENV_WEB_SERVER_CLEAN_ALL_EXECUTION_RESULTS_ON_SERVER_START = "WEB_SERVER_CLEAN_ALL_EXECUTION_RESULTS_ON_SERVER_START" val ENV_MAX_WORKFLOW_WEBSOCKET_REQUEST_PAYLOAD_SIZE_KB = "MAX_WORKFLOW_WEBSOCKET_REQUEST_PAYLOAD_SIZE_KB" // Fault Tolerance val ENV_FAULT_TOLERANCE_LOG_STORAGE_URI = "FAULT_TOLERANCE_LOG_STORAGE_URI" val ENV_FAULT_TOLERANCE_LOG_FLUSH_INTERVAL_MS = "FAULT_TOLERANCE_LOG_FLUSH_INTERVAL_MS" val ENV_FAULT_TOLERANCE_LOG_RECORD_MAX_SIZE_IN_BYTE = "FAULT_TOLERANCE_LOG_RECORD_MAX_SIZE_IN_BYTE" val ENV_FAULT_TOLERANCE_MAX_SUPPORTED_RESEND_QUEUE_LENGTH = "FAULT_TOLERANCE_MAX_SUPPORTED_RESEND_QUEUE_LENGTH" val ENV_FAULT_TOLERANCE_DELAY_BEFORE_RECOVERY = "FAULT_TOLERANCE_DELAY_BEFORE_RECOVERY" val ENV_FAULT_TOLERANCE_HDFS_STORAGE_ADDRESS = "FAULT_TOLERANCE_HDFS_STORAGE_ADDRESS" // Schedule Generator val ENV_SCHEDULE_GENERATOR_ENABLE_COST_BASED_SCHEDULE_GENERATOR = "SCHEDULE_GENERATOR_ENABLE_COST_BASED_SCHEDULE_GENERATOR" val ENV_SCHEDULE_GENERATOR_MAX_CONCURRENT_REGIONS = "SCHEDULE_GENERATOR_MAX_CONCURRENT_REGIONS" val ENV_SCHEDULE_GENERATOR_USE_GLOBAL_SEARCH = "SCHEDULE_GENERATOR_USE_GLOBAL_SEARCH" val ENV_SCHEDULE_GENERATOR_USE_TOP_DOWN_SEARCH = "SCHEDULE_GENERATOR_USE_TOP_DOWN_SEARCH" val ENV_SCHEDULE_GENERATOR_SEARCH_TIMEOUT_MILLISECONDS = "SCHEDULE_GENERATOR_SEARCH_TIMEOUT_MILLISECONDS" // AI Assistant Server val ENV_AI_ASSISTANT_SERVER_ASSISTANT = "AI_ASSISTANT_SERVER_ASSISTANT" val ENV_AI_ASSISTANT_SERVER_AI_SERVICE_KEY = "AI_ASSISTANT_SERVER_AI_SERVICE_KEY" val ENV_AI_ASSISTANT_SERVER_AI_SERVICE_URL = "AI_ASSISTANT_SERVER_AI_SERVICE_URL" } ================================================ FILE: common/config/src/main/scala/org/apache/texera/amber/config/PekkoConfig.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.amber.config import com.typesafe.config.{Config, ConfigFactory} object PekkoConfig { // Load configuration private val conf: Config = ConfigFactory.parseResources("cluster.conf").resolve() // Return the complete Pekko configuration with fallback to default application config def pekkoConfig: Config = conf.withFallback(ConfigFactory.defaultApplication()).resolve() } ================================================ FILE: common/config/src/main/scala/org/apache/texera/amber/config/PythonUtils.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.amber.config // Util function used by PveManager and PythonWorkflowWorker object PythonUtils { def getPythonExecutable: String = { val pythonPath = UdfConfig.pythonPath.trim if (pythonPath.isEmpty) "python3" else pythonPath } } ================================================ FILE: common/config/src/main/scala/org/apache/texera/amber/config/StorageConfig.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.amber.config import com.typesafe.config.{Config, ConfigFactory} import org.apache.texera.amber.util.ConfigParserUtil.parseSizeStringToBytes import java.nio.file.Path object StorageConfig { // Load configuration private val conf: Config = ConfigFactory.parseResources("storage.conf").resolve() // JDBC specifics val jdbcUrl: String = conf.getString("storage.jdbc.url") val jdbcUrlForTestCases: String = conf.getString("storage.jdbc.url-for-test-cases") val jdbcUsername: String = conf.getString("storage.jdbc.username") val jdbcPassword: String = conf.getString("storage.jdbc.password") // Iceberg specifics val icebergCatalogType: String = conf.getString("storage.iceberg.catalog.type") val icebergRESTCatalogUri: String = conf.getString("storage.iceberg.catalog.rest.uri") val icebergRESTCatalogWarehouseName: String = conf.getString("storage.iceberg.catalog.rest.warehouse-name") // Iceberg Postgres specifics val icebergPostgresCatalogUriWithoutScheme: String = conf.getString("storage.iceberg.catalog.postgres.uri-without-scheme") val icebergPostgresCatalogUsername: String = conf.getString("storage.iceberg.catalog.postgres.username") val icebergPostgresCatalogPassword: String = conf.getString("storage.iceberg.catalog.postgres.password") // Iceberg Table specifics val icebergTableResultNamespace: String = conf.getString("storage.iceberg.table.result-namespace") val icebergTableConsoleMessagesNamespace: String = conf.getString("storage.iceberg.table.console-messages-namespace") val icebergTableRuntimeStatisticsNamespace: String = conf.getString("storage.iceberg.table.runtime-statistics-namespace") val icebergTableCommitBatchSize: Int = conf.getInt("storage.iceberg.table.commit.batch-size") val icebergTableCommitNumRetries: Int = conf.getInt("storage.iceberg.table.commit.retry.num-retries") val icebergTableCommitMinRetryWaitMs: Int = conf.getInt("storage.iceberg.table.commit.retry.min-wait-ms") val icebergTableCommitMaxRetryWaitMs: Int = conf.getInt("storage.iceberg.table.commit.retry.max-wait-ms") // LakeFS specifics // lakefsEndpoint is a var because in test we need to override it to point to the test container var lakefsEndpoint: String = conf.getString("storage.lakefs.endpoint") val lakefsApiSecret: String = conf.getString("storage.lakefs.auth.api-secret") val lakefsUsername: String = conf.getString("storage.lakefs.auth.username") val lakefsPassword: String = conf.getString("storage.lakefs.auth.password") val lakefsBlockStorageType: String = conf.getString("storage.lakefs.block-storage.type") val lakefsBucketName: String = conf.getString("storage.lakefs.block-storage.bucket-name") // S3 specifics // s3Endpoint is a var because in test we need to override it to point to the test container var s3Endpoint: String = conf.getString("storage.s3.endpoint") val s3Region: String = conf.getString("storage.s3.region") val s3Username: String = conf.getString("storage.s3.auth.username") val s3Password: String = conf.getString("storage.s3.auth.password") val s3MultipartUploadPartSize: Long = parseSizeStringToBytes( conf.getString("storage.s3.multipart.part-size") ) // File storage configurations val fileStorageDirectoryPath: Path = Path .of(sys.env.getOrElse("TEXERA_HOME", ".")) .resolve("amber") .resolve("user-resources") .resolve("workflow-results") // JDBC val ENV_JDBC_URL = "STORAGE_JDBC_URL" val ENV_JDBC_USERNAME = "STORAGE_JDBC_USERNAME" val ENV_JDBC_PASSWORD = "STORAGE_JDBC_PASSWORD" // Iceberg Catalog val ENV_ICEBERG_CATALOG_TYPE = "STORAGE_ICEBERG_CATALOG_TYPE" val ENV_ICEBERG_CATALOG_REST_URI = "STORAGE_ICEBERG_CATALOG_REST_URI" // Iceberg Postgres Catalog val ENV_ICEBERG_CATALOG_POSTGRES_URI_WITHOUT_SCHEME = "STORAGE_ICEBERG_CATALOG_POSTGRES_URI_WITHOUT_SCHEME" val ENV_ICEBERG_CATALOG_POSTGRES_USERNAME = "STORAGE_ICEBERG_CATALOG_POSTGRES_USERNAME" val ENV_ICEBERG_CATALOG_POSTGRES_PASSWORD = "STORAGE_ICEBERG_CATALOG_POSTGRES_PASSWORD" // Iceberg Table val ENV_ICEBERG_TABLE_RESULT_NAMESPACE = "STORAGE_ICEBERG_TABLE_RESULT_NAMESPACE" val ENV_ICEBERG_TABLE_CONSOLE_MESSAGES_NAMESPACE = "STORAGE_ICEBERG_TABLE_CONSOLE_MESSAGES_NAMESPACE" val ENV_ICEBERG_TABLE_RUNTIME_STATISTICS_NAMESPACE = "STORAGE_ICEBERG_TABLE_RUNTIME_STATISTICS_NAMESPACE" val ENV_ICEBERG_TABLE_COMMIT_BATCH_SIZE = "STORAGE_ICEBERG_TABLE_COMMIT_BATCH_SIZE" val ENV_ICEBERG_TABLE_COMMIT_NUM_RETRIES = "STORAGE_ICEBERG_TABLE_COMMIT_NUM_RETRIES" val ENV_ICEBERG_TABLE_COMMIT_MIN_WAIT_MS = "STORAGE_ICEBERG_TABLE_COMMIT_MIN_WAIT_MS" val ENV_ICEBERG_TABLE_COMMIT_MAX_WAIT_MS = "STORAGE_ICEBERG_TABLE_COMMIT_MAX_WAIT_MS" // LakeFS val ENV_LAKEFS_ENDPOINT = "STORAGE_LAKEFS_ENDPOINT" val ENV_LAKEFS_AUTH_API_SECRET = "STORAGE_LAKEFS_AUTH_API_SECRET" val ENV_LAKEFS_AUTH_USERNAME = "STORAGE_LAKEFS_AUTH_USERNAME" val ENV_LAKEFS_AUTH_PASSWORD = "STORAGE_LAKEFS_AUTH_PASSWORD" val ENV_LAKEFS_BLOCK_STORAGE_TYPE = "STORAGE_LAKEFS_BLOCK_STORAGE_TYPE" val ENV_LAKEFS_BLOCK_STORAGE_BUCKET_NAME = "STORAGE_LAKEFS_BLOCK_STORAGE_BUCKET_NAME" // S3 val ENV_S3_ENDPOINT = "STORAGE_S3_ENDPOINT" val ENV_S3_REGION = "STORAGE_S3_REGION" val ENV_S3_AUTH_USERNAME = "STORAGE_S3_AUTH_USERNAME" val ENV_S3_AUTH_PASSWORD = "STORAGE_S3_AUTH_PASSWORD" } ================================================ FILE: common/config/src/main/scala/org/apache/texera/amber/config/UdfConfig.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.amber.config import com.typesafe.config.{Config, ConfigFactory} object UdfConfig { // Load configuration private val conf: Config = ConfigFactory.parseResources("udf.conf").resolve() // Python specifics val pythonPath: String = conf.getString("python.path") val pythonLogStreamHandlerLevel: String = conf.getString("python.log.streamHandler.level") val pythonLogStreamHandlerFormat: String = conf.getString("python.log.streamHandler.format") val pythonLogFileHandlerDir: String = conf.getString("python.log.fileHandler.dir") val pythonLogFileHandlerLevel: String = conf.getString("python.log.fileHandler.level") val pythonLogFileHandlerFormat: String = conf.getString("python.log.fileHandler.format") // R specifics val rPath: String = conf.getString("r.path") } ================================================ FILE: common/config/src/main/scala/org/apache/texera/amber/util/ConfigParserUtil.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.amber.util object ConfigParserUtil { def parseSizeStringToBytes(size: String): Long = { val sizePattern = """(\d+)([KMG]B)""".r size match { case sizePattern(value, unit) => val multiplier = unit match { case "KB" => 1024L case "MB" => 1024L * 1024 case "GB" => 1024L * 1024 * 1024 } value.toLong * multiplier case _ => throw new IllegalArgumentException( s"Invalid s3 multipart part-size format in StorageConfig.scala with value $size" ) } } } ================================================ FILE: common/config/src/main/scala/org/apache/texera/config/AuthConfig.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.config import com.typesafe.config.{Config, ConfigFactory} import java.security.SecureRandom object AuthConfig { // Load configuration private val conf: Config = ConfigFactory.parseResources("auth.conf").resolve() // Read jwt Expiration time in minutes final val jwtExpirationMinutes: Int = conf.getInt("auth.jwt.expiration-in-minutes") // For storing the generated/configured secret @volatile private var secretKey: String = _ // Read JWT secret key with support for random generation def jwtSecretKey: String = { synchronized { if (secretKey == null) { secretKey = conf.getString("auth.jwt.256-bit-secret").toLowerCase() match { case "random" => getRandomHexString case key => key } } } secretKey } private def getRandomHexString: String = { val bytes = 32 val r = new SecureRandom() val sb = new StringBuffer while (sb.length < bytes) sb.append(f"${r.nextInt()}%08x") sb.toString.substring(0, bytes) } } ================================================ FILE: common/config/src/main/scala/org/apache/texera/config/ComputingUnitConfig.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.config import com.typesafe.config.{Config, ConfigFactory} object ComputingUnitConfig { private val conf: Config = ConfigFactory.parseResources("computing-unit.conf").resolve() val localComputingUnitEnabled: Boolean = conf.getBoolean("computing-unit.local.enabled") val sharingComputingUnitEnabled: Boolean = conf.getBoolean("computing-unit.sharing.enabled") } ================================================ FILE: common/config/src/main/scala/org/apache/texera/config/DefaultsConfig.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.config import com.typesafe.config.{ConfigFactory, ConfigRenderOptions, ConfigValueType} import scala.jdk.CollectionConverters.CollectionHasAsScala object DefaultsConfig { private val conf = ConfigFactory.parseResources("default.conf").resolve() val reinit: Boolean = conf.getBoolean("config-service.always-reset-configurations-to-default-values") val allDefaults: Map[String, String] = { conf .entrySet() .asScala .map { entry => val shortKey = entry.getKey.split("\\.").last val value = entry.getValue.valueType() match { case ConfigValueType.STRING | ConfigValueType.NUMBER | ConfigValueType.BOOLEAN => entry.getValue.unwrapped().toString case _ => entry.getValue.render(ConfigRenderOptions.concise()) } shortKey -> value } .toMap } } ================================================ FILE: common/config/src/main/scala/org/apache/texera/config/GuiConfig.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.config import com.typesafe.config.{Config, ConfigFactory} object GuiConfig { private val conf: Config = ConfigFactory.parseResources("gui.conf").resolve() // GUI Configuration // GUI Login Configuration val guiLoginLocalLogin: Boolean = conf.getBoolean("gui.login.local-login") val guiLoginGoogleLogin: Boolean = conf.getBoolean("gui.login.google-login") val guiLoginDefaultLocalUserUsername: String = if (conf.hasPath("gui.login.default-local-user.username")) conf.getString("gui.login.default-local-user.username") else "" val guiLoginDefaultLocalUserPassword: String = if (conf.hasPath("gui.login.default-local-user.password")) conf.getString("gui.login.default-local-user.password") else "" // GUI Workflow Workspace Configuration val guiWorkflowWorkspaceUserPresetEnabled: Boolean = conf.getBoolean("gui.workflow-workspace.user-preset-enabled") val guiWorkflowWorkspaceExportExecutionResultEnabled: Boolean = conf.getBoolean("gui.workflow-workspace.export-execution-result-enabled") val guiWorkflowWorkspaceAutoAttributeCorrectionEnabled: Boolean = conf.getBoolean("gui.workflow-workspace.auto-attribute-correction-enabled") val guiWorkflowWorkspaceDefaultDataTransferBatchSize: Int = conf.getInt("gui.workflow-workspace.default-data-transfer-batch-size") val guiWorkflowWorkspaceDefaultExecutionMode: String = conf.getString("gui.workflow-workspace.default-execution-mode") val guiWorkflowWorkspaceSelectingFilesFromDatasetsEnabled: Boolean = conf.getBoolean("gui.workflow-workspace.selecting-files-from-datasets-enabled") val guiWorkflowWorkspaceWorkflowExecutionsTrackingEnabled: Boolean = conf.getBoolean("gui.workflow-workspace.workflow-executions-tracking-enabled") val guiWorkflowWorkspaceLinkBreakpointEnabled: Boolean = conf.getBoolean("gui.workflow-workspace.link-breakpoint-enabled") val guiWorkflowWorkspaceAsyncRenderingEnabled: Boolean = conf.getBoolean("gui.workflow-workspace.async-rendering-enabled") val guiWorkflowWorkspaceTimetravelEnabled: Boolean = conf.getBoolean("gui.workflow-workspace.timetravel-enabled") val guiWorkflowWorkspaceProductionSharedEditingServer: Boolean = conf.getBoolean("gui.workflow-workspace.production-shared-editing-server") val guiWorkflowWorkspacePythonLanguageServerPort: String = conf.getString("gui.workflow-workspace.python-language-server-port") val guiWorkflowWorkspaceOperatorConsoleMessageBufferSize: Int = conf.getInt("gui.workflow-workspace.operator-console-message-buffer-size") val guiWorkflowWorkspaceWorkflowEmailNotificationEnabled: Boolean = conf.getBoolean("gui.workflow-workspace.workflow-email-notification-enabled") val guiWorkflowWorkspaceActiveTimeInMinutes: Int = conf.getInt("gui.workflow-workspace.active-time-in-minutes") val guiWorkflowWorkspaceCopilotEnabled: Boolean = conf.getBoolean("gui.workflow-workspace.copilot-enabled") val guiWorkflowWorkspaceLimitColumns: Int = conf.getInt("gui.workflow-workspace.limit-columns") } ================================================ FILE: common/config/src/main/scala/org/apache/texera/config/KubernetesConfig.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.config import com.typesafe.config.{Config, ConfigFactory} object KubernetesConfig { private val conf: Config = ConfigFactory.parseResources("kubernetes.conf").resolve() val kubernetesComputingUnitEnabled: Boolean = conf.getBoolean("kubernetes.enabled") // Access the Kubernetes settings with environment variable fallback val computeUnitServiceName: String = conf.getString("kubernetes.compute-unit-service-name") val computeUnitPoolName: String = conf.getString("kubernetes.compute-unit-pool-name") val computeUnitPoolNamespace: String = conf.getString("kubernetes.compute-unit-pool-namespace") val computeUnitImageName: String = conf.getString("kubernetes.image-name") val computingUnitImagePullPolicy: String = conf.getString("kubernetes.image-pull-policy") val computeUnitPortNumber: Int = conf.getInt("kubernetes.port-num") val maxNumOfRunningComputingUnitsPerUser: Int = conf.getInt("kubernetes.max-num-of-running-computing-units-per-user") val cpuLimitOptions: List[String] = conf .getString("kubernetes.computing-unit-cpu-limit-options") .split(",") .map(_.trim) .filter(_.nonEmpty) .toList val memoryLimitOptions: List[String] = conf .getString("kubernetes.computing-unit-memory-limit-options") .split(",") .map(_.trim) .filter(_.nonEmpty) .toList val gpuLimitOptions: List[String] = conf .getString("kubernetes.computing-unit-gpu-limit-options") .split(",") .map(_.trim) .filter(_.nonEmpty) .toList // GPU resource key used directly in Kubernetes resource specifications val gpuResourceKey: String = conf.getString("kubernetes.computing-unit-gpu-resource-key") } ================================================ FILE: common/config/src/main/scala/org/apache/texera/config/LLMConfig.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.config import com.typesafe.config.{Config, ConfigFactory} object LLMConfig { private val conf: Config = ConfigFactory.parseResources("llm.conf").resolve() // LLM Service Configuration val baseUrl: String = conf.getString("llm.base-url") val masterKey: String = conf.getString("llm.master-key") } ================================================ FILE: common/config/src/main/scala/org/apache/texera/config/UserSystemConfig.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.config import com.typesafe.config.{Config, ConfigFactory} import java.util.logging.Logger object UserSystemConfig { private val conf: Config = ConfigFactory.parseResources("user-system.conf").resolve() private val logger = Logger.getLogger(getClass.getName) // User system val adminUsername: String = conf.getString("user-sys.admin-username") val adminPassword: String = conf.getString("user-sys.admin-password") val googleClientId: String = conf.getString("user-sys.google.clientId") val gmail: String = conf.getString("user-sys.google.smtp.gmail") val smtpPassword: String = conf.getString("user-sys.google.smtp.password") val inviteOnly: Boolean = conf.getBoolean("user-sys.invite-only") val projectName: String = conf.getString("user-sys.project-name") val workflowVersionCollapseIntervalInMinutes: Int = conf.getInt("user-sys.version-time-limit-in-minutes") val appDomain: Option[String] = { val domain = conf.getString("user-sys.domain").trim if (domain.isEmpty) { logger.warning( """ |======================================================= |[WARN] The user-sys.domain is not configured, and the "Sent from:" field in the email will not include a domain! |Please configure user-sys.domain=your.domain.com or set the environment variable/system property USER_SYS_DOMAIN |======================================================= |""".stripMargin ) None } else { Some(domain) } } } ================================================ FILE: common/dao/.gitignore ================================================ src/main/scala/org/apache/texera/dao/jooq/ ================================================ FILE: common/dao/build.sbt ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. ///////////////////////////////////////////////////////////////////////////// // Project Settings ///////////////////////////////////////////////////////////////////////////// name := "dao" enablePlugins(JavaAppPackaging) // Enable semanticdb for Scalafix ThisBuild / semanticdbEnabled := true ThisBuild / semanticdbVersion := scalafixSemanticdb.revision // Manage dependency conflicts by always using the latest revision ThisBuild / conflictManager := ConflictManager.latestRevision // Restrict parallel execution of tests to avoid conflicts Global / concurrentRestrictions += Tags.limit(Tags.Test, 1) ///////////////////////////////////////////////////////////////////////////// // JOOQ Code Generation ///////////////////////////////////////////////////////////////////////////// // Define JOOQ generation task lazy val jooqGenerate = taskKey[Seq[File]]("Generate JOOQ sources") jooqGenerate := { val log = streams.value.log log.info("Generating JOOQ classes...") try { import com.typesafe.config.{Config, ConfigFactory, ConfigParseOptions} import org.jooq.codegen.GenerationTool import org.jooq.meta.jaxb.{Configuration, Jdbc} import java.io.File import java.nio.file.{Files, Path} // Load jOOQ configuration XML (absolute path from DAO project) val jooqXmlPath: Path = baseDirectory.value.toPath.resolve("src").resolve("main").resolve("resources").resolve("jooq-conf.xml") val jooqConfig: Configuration = GenerationTool.load(Files.newInputStream(jooqXmlPath)) // Load storage.conf from the config project val storageConfPath: Path = baseDirectory.value.toPath .getParent .resolve("config") .resolve("src") .resolve("main") .resolve("resources") .resolve("storage.conf") val conf: Config = ConfigFactory .parseFile( new File(storageConfPath.toString), ConfigParseOptions.defaults().setAllowMissing(false) ) .resolve() // Extract JDBC configuration val jdbcConfig = conf.getConfig("storage.jdbc") val jooqJdbcConfig = new Jdbc jooqJdbcConfig.setDriver("org.postgresql.Driver") // Skip all the query params, otherwise it will omit the "texera_db." prefix on the field names. jooqJdbcConfig.setUrl(jdbcConfig.getString("url").split('?').head) jooqJdbcConfig.setUsername(jdbcConfig.getString("username")) jooqJdbcConfig.setPassword(jdbcConfig.getString("password")) jooqConfig.setJdbc(jooqJdbcConfig) // Generate the code GenerationTool.generate(jooqConfig) log.info("JOOQ code generation completed successfully") // Return the generated files val generatedDir = baseDirectory.value / "src" / "main" / "scala" / "org" / "apache" / "texera" / "dao" / "jooq" / "generated" if (generatedDir.exists()) { (generatedDir ** "*.java").get ++ (generatedDir ** "*.scala").get } else { Seq.empty } } catch { case e: Exception => log.warn(s"JOOQ code generation failed: ${e.getMessage}") log.warn("Continuing compilation with existing generated files...") Seq.empty } } // Add JOOQ generation to source generators Compile / sourceGenerators += jooqGenerate ///////////////////////////////////////////////////////////////////////////// // Compiler Options ///////////////////////////////////////////////////////////////////////////// // Scala compiler options Compile / scalacOptions ++= Seq( "-Xelide-below", "WARNING", // Turn on optimizations with "WARNING" as the threshold "-feature", // Check feature warnings "-deprecation", // Check deprecation warnings "-Ywarn-unused:imports" // Check for unused imports ) ///////////////////////////////////////////////////////////////////////////// // ScalaPB Configuration ///////////////////////////////////////////////////////////////////////////// // Exclude some proto files PB.generate / excludeFilter := "scalapb.proto" // Set the protoc version for ScalaPB ThisBuild / PB.protocVersion := "3.19.4" // ScalaPB code generation for .proto files Compile / PB.targets := Seq( scalapb.gen(singleLineToProtoString = true) -> (Compile / sourceManaged).value ) // Mark the ScalaPB-generated directory as a generated source root Compile / managedSourceDirectories += (Compile / sourceManaged).value // ScalaPB library dependencies libraryDependencies ++= Seq( "com.thesamet.scalapb" %% "scalapb-runtime" % scalapb.compiler.Version.scalapbVersion % "protobuf", "com.thesamet.scalapb" %% "scalapb-json4s" % "0.12.0" // For ScalaPB 0.11.x ) // Enable protobuf compilation in Test Test / PB.protoSources += PB.externalSourcePath.value ///////////////////////////////////////////////////////////////////////////// // Test-related Dependencies ///////////////////////////////////////////////////////////////////////////// libraryDependencies ++= Seq( "org.scalamock" %% "scalamock" % "5.2.0" % Test, // ScalaMock "org.scalatest" %% "scalatest" % "3.2.15" % Test, // ScalaTest "junit" % "junit" % "4.13.2" % Test, // JUnit "com.novocode" % "junit-interface" % "0.11" % Test, // SBT interface for JUnit "io.zonky.test" % "embedded-postgres" % "2.1.0" % Test // For mock postgres DB ) ///////////////////////////////////////////////////////////////////////////// // Jooq-related Dependencies ///////////////////////////////////////////////////////////////////////////// libraryDependencies ++= Seq( "org.jooq" % "jooq" % "3.16.23", ) ///////////////////////////////////////////////////////////////////////////// // Additional Dependencies ///////////////////////////////////////////////////////////////////////////// libraryDependencies ++= Seq( "org.postgresql" % "postgresql" % "42.7.10", ) ================================================ FILE: common/dao/src/main/resources/jooq-conf.xml ================================================ false false true true org.jooq.codegen.JavaGenerator org.jooq.meta.postgres.PostgresDatabase texera_db .* (pgroonga.*)|(test_.*)|(ignore_.*) TIMESTAMP (?i)TIMESTAMP org.apache.texera.dao.jooq.generated common/dao/src/main/scala ================================================ FILE: common/dao/src/main/scala/org/apache/texera/dao/SiteSettings.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.dao import org.jooq.impl.DSL import scala.util.Try /** * Read-side accessor for the `site_settings` key/value table that admin pages * write through. Centralises the "look up by key, parse, fall back on any * failure" pattern that previously lived inline in ConfigResource, * CSVScanSourceOpExec, and DatasetResource. * * Failures swallowed by the outer Try include: SqlServer not initialised * (e.g. on workers in distributed mode), no row for the key, and value that * can't be parsed. In all of these cases the caller's default takes over. */ object SiteSettings { def getInt(key: String, default: => Int): Int = readAndParse(key, default)(_.toInt) def getLong(key: String, default: => Long): Long = readAndParse(key, default)(_.toLong) private[dao] def parseOrDefault[T](raw: Option[String], default: T)(parse: String => T): T = raw.flatMap(s => Try(parse(s.trim)).toOption).getOrElse(default) private def readAndParse[T](key: String, default: => T)(parse: String => T): T = Try { val raw = SqlServer .getInstance() .createDSLContext() .select(DSL.field("value", classOf[String])) .from(DSL.table(DSL.name("texera_db", "site_settings"))) .where(DSL.field("key", classOf[String]).eq(key)) .fetchOneInto(classOf[String]) parseOrDefault(Option(raw), default)(parse) }.getOrElse(default) } ================================================ FILE: common/dao/src/main/scala/org/apache/texera/dao/SqlServer.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.dao import org.jooq.impl.DSL import org.jooq.{DSLContext, SQLDialect} import org.postgresql.ds.PGSimpleDataSource /** * SqlServer class that manages a connection to a PostgreSQL database using jOOQ. * * WARNING: Do not cache the DSLContext returned by `createDSLContext()` in a val or lazy val. * During testing, `MockTexeraDB` replaces the SqlServer instance between test classes. * A cached DSLContext will hold a stale reference to a dead database connection from a previous test class, * causing "Connection refused" errors when tests run together. * Use `def` to ensure the connection is looked up each time. * * @param url The database connection URL. * @param user The username for authenticating with the database. * @param password The password for authenticating with the database. */ class SqlServer private (url: String, user: String, password: String) { val SQL_DIALECT: SQLDialect = SQLDialect.POSTGRES private val dataSource: PGSimpleDataSource = new PGSimpleDataSource() var context: DSLContext = { dataSource.setUrl(url) dataSource.setUser(user) dataSource.setPassword(password) dataSource.setConnectTimeout(5) DSL.using(dataSource, SQL_DIALECT) } def createDSLContext(): DSLContext = context def replaceDSLContext(newContext: DSLContext): Unit = { context = newContext } } object SqlServer { private var instance: Option[SqlServer] = None def initConnection(url: String, user: String, password: String): Unit = { if (instance.isEmpty) { val server = new SqlServer(url, user, password) instance = Some(server) } } def getInstance(): SqlServer = { instance.get } def clearInstance(): Unit = { instance = None } /** * A utility function for create a transaction block using given sql context * @param dsl the sql context * @param block the code block to execute within the transaction * @tparam T the value will be returned by the code block * @return */ def withTransaction[T](dsl: DSLContext)(block: DSLContext => T): T = { var result: Option[T] = None dsl.transaction(configuration => { val ctx = DSL.using(configuration) result = Some(block(ctx)) }) result.getOrElse(throw new RuntimeException("Transaction failed without result!")) } } ================================================ FILE: common/dao/src/test/scala/org/apache/texera/dao/MockTexeraDB.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.dao import io.zonky.test.db.postgres.embedded.EmbeddedPostgres import org.jooq.impl.DSL import org.jooq.{DSLContext, SQLDialect} import java.nio.file.Paths import java.sql.{Connection, DriverManager} import scala.io.Source trait MockTexeraDB { private var dbInstance: Option[EmbeddedPostgres] = None private var dslContext: Option[DSLContext] = None private val database: String = "texera_db" private val username: String = "postgres" private val password: String = "" def executeScriptInJDBC(conn: Connection, script: String): Unit = { assert(dbInstance.nonEmpty) conn.prepareStatement(script).execute() conn.close() } def getDSLContext: DSLContext = { dslContext match { case Some(value) => value case None => throw new RuntimeException( "test database is not initialized. Did you call initializeDBAndReplaceDSLContext()?" ) } } def getDBInstance: EmbeddedPostgres = { dbInstance match { case Some(value) => value case None => throw new RuntimeException( "test database is not initialized. Did you call initializeDBAndReplaceDSLContext()?" ) } } def shutdownDB(): Unit = { dbInstance match { case Some(value) => value.close() dbInstance = None dslContext = None SqlServer.clearInstance() case None => // do nothing } } def initializeDBAndReplaceDSLContext(): Unit = { assert(dbInstance.isEmpty && dslContext.isEmpty) val driver = new org.postgresql.Driver() DriverManager.registerDriver(driver) val embedded = EmbeddedPostgres.builder().start() dbInstance = Some(embedded) val ddlPath = { Paths.get("sql/texera_ddl.sql").toRealPath() } val source = Source.fromFile(ddlPath.toString) val content = try { source.mkString } finally { source.close() } val parts: Array[String] = content.split("(?m)^CREATE DATABASE :\"DB_NAME\";") def removeCCommands(sql: String): String = sql.linesIterator .filterNot(_.trim.startsWith("\\c")) .mkString("\n") val createDBStatement = """DROP DATABASE IF EXISTS texera_db; |CREATE DATABASE texera_db;""".stripMargin executeScriptInJDBC(embedded.getPostgresDatabase.getConnection, createDBStatement) val texeraDB = embedded.getDatabase(username, database) var tablesAndIndexCreation = removeCCommands(parts(1)) // remove indexes creation for pgroonga because we cannot install the plugin val blockPattern = """(?s)-- START Fulltext search index creation \(DO NOT EDIT THIS LINE\).*?-- END Fulltext search index creation \(DO NOT EDIT THIS LINE\)\n?""".r // replace with native fulltext indexes val replacementText = """CREATE INDEX idx_workflow_name_description_content | ON workflow | USING GIN ( | to_tsvector('english', | COALESCE(name, '') || ' ' || | COALESCE(description, '') || ' ' || | COALESCE(content, '') | ) | ); | |CREATE INDEX idx_user_name | ON "user" | USING GIN ( | to_tsvector('english', | COALESCE(name, '') | ) | ); | |CREATE INDEX idx_user_project_name_description | ON project | USING GIN ( | to_tsvector('english', | COALESCE(name, '') || ' ' || | COALESCE(description, '') | ) | ); | |CREATE INDEX idx_dataset_name_description | ON dataset | USING GIN ( | to_tsvector('english', | COALESCE(name, '') || ' ' || | COALESCE(description, '') | ) | ); | |CREATE INDEX idx_dataset_version_name | ON dataset_version | USING GIN ( | to_tsvector('english', | COALESCE(name, '') | ) | );""".stripMargin tablesAndIndexCreation = blockPattern.replaceAllIn(tablesAndIndexCreation, replacementText).trim executeScriptInJDBC(texeraDB.getConnection, tablesAndIndexCreation) SqlServer.initConnection(embedded.getJdbcUrl(username, database), username, password) val sqlServerInstance = SqlServer.getInstance() dslContext = Some(DSL.using(texeraDB, SQLDialect.POSTGRES)) sqlServerInstance.replaceDSLContext(dslContext.get) } } ================================================ FILE: common/dao/src/test/scala/org/apache/texera/dao/SiteSettingsSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.dao import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers class SiteSettingsSpec extends AnyFlatSpec with Matchers { "parseOrDefault" should "return the parsed value when the raw string is present and valid" in { SiteSettings.parseOrDefault(Some("42"), 0)(_.toInt) shouldBe 42 } it should "return the default when the Option is None" in { SiteSettings.parseOrDefault(None, 99)(_.toInt) shouldBe 99 } it should "return the default when the string cannot be parsed" in { SiteSettings.parseOrDefault(Some("not-a-number"), 7)(_.toInt) shouldBe 7 } it should "trim whitespace before parsing" in { SiteSettings.parseOrDefault(Some(" 100 "), 0)(_.toInt) shouldBe 100 } it should "work for Long values" in { SiteSettings.parseOrDefault(Some("9999999999"), 0L)(_.toLong) shouldBe 9999999999L } } ================================================ FILE: common/pybuilder/build.sbt ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. import scala.collection.Seq ///////////////////////////////////////////////////////////////////////////// // Project Settings ///////////////////////////////////////////////////////////////////////////// name := "pybuilder" enablePlugins(JavaAppPackaging) // Enable semanticdb for Scalafix ThisBuild / semanticdbEnabled := true ThisBuild / semanticdbVersion := scalafixSemanticdb.revision // Manage dependency conflicts by always using the latest revision ThisBuild / conflictManager := ConflictManager.latestRevision // Restrict parallel execution of tests to avoid conflicts Global / concurrentRestrictions += Tags.limit(Tags.Test, 1) ///////////////////////////////////////////////////////////////////////////// // Compiler Options ///////////////////////////////////////////////////////////////////////////// // Scala compiler options Compile / scalacOptions ++= Seq( "-Xelide-below", "WARNING", // Turn on optimizations with "WARNING" as the threshold "-feature", // Check feature warnings "-deprecation", // Check deprecation warnings "-Ywarn-unused:imports" // Check for unused imports ) ///////////////////////////////////////////////////////////////////////////// // Test-related Dependencies ///////////////////////////////////////////////////////////////////////////// libraryDependencies ++= Seq( "org.scalamock" %% "scalamock" % "5.2.0" % Test, // ScalaMock "org.scalatest" %% "scalatest" % "3.2.15" % Test, // ScalaTest "junit" % "junit" % "4.13.2" % Test, // JUnit "com.novocode" % "junit-interface" % "0.11" % Test, // SBT interface for JUnit "io.github.classgraph" % "classgraph" % "4.8.184" % Test, "org.scala-lang" % "scala-compiler" % scalaVersion.value % Test ) ///////////////////////////////////////////////////////////////////////////// // Reflection-related Dependencies ///////////////////////////////////////////////////////////////////////////// libraryDependencies ++= Seq( "org.scala-lang" % "scala-reflect" % scalaVersion.value ) ================================================ FILE: common/pybuilder/src/main/scala/org/apache/texera/amber/pybuilder/BoundaryValidator.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.pybuilder import scala.reflect.macros.blackbox /** * Macro-only helper: validates boundaries for Encodable insertions. * * Compile-time: abort with good messages for direct Encodable args. * Runtime: for nested builders (unknown content at compile time), generate a check that throws if the builder contains Encodable chunks. */ final class BoundaryValidator[C <: blackbox.Context](val c: C) { import PythonLexerUtils._ import c.universe._ /** * Centralized, templatized error messages (Option A). * * NOTE: This object lives inside the class so it can freely use string templates * without any macro-context type gymnastics. */ private object BoundaryErrors { // Provide a hint that can differ between compile-time and runtime wording. sealed trait RendererHint { def text: String } case object CompileTimeHint extends RendererHint { override val text: String = "EncodableString renders as a Python expression (self.PythonTemplateDecoder.decode(...))" } case object RuntimeHint extends RendererHint { override val text: String = "EncodableString renders as a Python expression (self...decode(...))" } private def prefix(argNum1Based: Int): String = s"pyb interpolator: @EncodableStringAnnotation argument #$argNum1Based " def insideQuoted(argNum1Based: Int, hint: RendererHint): String = prefix(argNum1Based) + "appears inside a quoted Python string literal. " + s"${hint.text}, so it must not be placed inside quotes." def afterComment(argNum1Based: Int): String = prefix(argNum1Based) + "appears after a '#' comment marker on the same line." def badLeftNeighbor(argNum1Based: Int, ch: Char): String = prefix(argNum1Based) + s"must not be immediately adjacent to '$ch' on the left. " + "Add whitespace or punctuation to separate tokens." def badRightNeighbor(argNum1Based: Int, ch: Char): String = prefix(argNum1Based) + s"must not be immediately adjacent to '$ch' on the right. " + "Add whitespace or punctuation to separate tokens." } final case class CompileTimeContext( leftPart: String, rightPart: String, prefixSource: String, argIndex: Int, errorPos: Position ) final case class RuntimeContext( leftPart: String, rightPart: String, prefixSource: String, argIndex: Int ) def validateCompileTime(ctx: CompileTimeContext): Unit = { val prefixLine = lineTail(ctx.prefixSource) val argNum = ctx.argIndex + 1 if (hasUnclosedQuote(prefixLine)) { c.abort( ctx.errorPos, BoundaryErrors.insideQuoted(argNum, BoundaryErrors.CompileTimeHint) ) } if (hasCommentOutsideQuotes(prefixLine)) { c.abort( ctx.errorPos, BoundaryErrors.afterComment(argNum) ) } if (ctx.leftPart.nonEmpty) { val leftNeighbor = ctx.leftPart.charAt(ctx.leftPart.length - 1) if (isBadNeighbor(leftNeighbor)) { c.abort( ctx.errorPos, BoundaryErrors.badLeftNeighbor(argNum, leftNeighbor) ) } } if (ctx.rightPart.nonEmpty) { val rightNeighbor = ctx.rightPart.charAt(0) if (isBadNeighbor(rightNeighbor)) { c.abort( ctx.errorPos, BoundaryErrors.badRightNeighbor(argNum, rightNeighbor) ) } } } /** * Generate runtime checks for nested PythonTemplateBuilder args. * * This is only emitted when the boundary context is unsafe. The runtime guard is: * if (arg.containsEncodableString) throw ... */ def runtimeChecksForNestedBuilder(ctx: RuntimeContext, argIdent: Tree): List[Tree] = { val prefixLine = lineTail(ctx.prefixSource) val argNum = ctx.argIndex + 1 val insideQuoted = hasUnclosedQuote(prefixLine) val afterComment = hasCommentOutsideQuotes(prefixLine) val leftNeighborOpt: Option[Char] = if (ctx.leftPart.nonEmpty) Some(ctx.leftPart.charAt(ctx.leftPart.length - 1)) else None val rightNeighborOpt: Option[Char] = if (ctx.rightPart.nonEmpty) Some(ctx.rightPart.charAt(0)) else None val throwStmts = List.newBuilder[Tree] if (insideQuoted) { val msg = BoundaryErrors.insideQuoted(argNum, BoundaryErrors.RuntimeHint) throwStmts += q"throw new IllegalArgumentException(${Literal(Constant(msg))})" } if (afterComment) { val msg = BoundaryErrors.afterComment(argNum) throwStmts += q"throw new IllegalArgumentException(${Literal(Constant(msg))})" } leftNeighborOpt.foreach { ch => if (isBadNeighbor(ch)) { val msg = BoundaryErrors.badLeftNeighbor(argNum, ch) throwStmts += q"throw new IllegalArgumentException(${Literal(Constant(msg))})" } } rightNeighborOpt.foreach { ch => if (isBadNeighbor(ch)) { val msg = BoundaryErrors.badRightNeighbor(argNum, ch) throwStmts += q"throw new IllegalArgumentException(${Literal(Constant(msg))})" } } val throws = throwStmts.result() if (throws.isEmpty) Nil else { List(q""" if ($argIdent.containsEncodableString) { ..$throws } """) } } } ================================================ FILE: common/pybuilder/src/main/scala/org/apache/texera/amber/pybuilder/EncodableInspector.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.pybuilder import scala.reflect.macros.blackbox /** * Macro-only helper: inspects argument trees / types / symbols to decide if a value is Encodable-marked. * * NOTE: This must be context-bound because Tree/Type/Annotation are from `c.universe`. */ final class EncodableInspector[C <: blackbox.Context](val c: C) { import c.universe._ private val stringRendererTpe: Type = typeOf[ PythonTemplateBuilder.StringRenderer ] private val pythonTemplateBuilderTpe: Type = typeOf[PythonTemplateBuilder] // Previous/original approach: direct encodable args include values already wrapped as EncodableStringRenderer private val encodableStringRendererTpe: Type = typeOf[ PythonTemplateBuilder.EncodableStringRenderer ] // Keep this as a string so it also works if the annotation is referenced indirectly. private val encodableStringAnnotationFqn = "org.apache.texera.amber.pybuilder.EncodableStringAnnotation" /** * If we are pointing at a getter/accessor, hop to its accessed field symbol when possible. * * Why: Many annotations are placed on constructor params/fields, but call sites see the accessor. */ private def safeAccessed(sym: Symbol): Symbol = sym match { case termAccessor: TermSymbol if termAccessor.isAccessor => termAccessor.accessed case methodAccessor: MethodSymbol if methodAccessor.isAccessor => methodAccessor.accessed case _ => sym } /** True if an annotation instance is @EncodableStringAnn. */ private def annIsEncodableString(annotation: Annotation): Boolean = { val annotationType = annotation.tree.tpe annotationType != null && ( annotationType.typeSymbol.fullName == encodableStringAnnotationFqn || (annotationType <:< typeOf[EncodableStringAnnotation]) ) } /** * True if a [[Type]] carries @EncodableStringAnnotation as a TYPE_USE annotation (via [[java.lang.reflect.AnnotatedType]]). * * Walks common wrappers (existentials, refinements, type refs) to find nested annotations. */ private def typeHasEncodableString(typeToCheck: Type): Boolean = { def loop(t: Type): Boolean = { if (t == null) false else { val widened = t.dealias.widen widened match { case AnnotatedType(anns, underlying) => anns.exists(annIsEncodableString) || loop(underlying) case ExistentialType(_, underlying) => loop(underlying) case RefinedType(parents, _) => parents.exists(loop) case TypeRef(_, _, args) => args.exists(loop) case other => val sym = other.typeSymbol val symHasAnn = sym != null && sym != NoSymbol && sym.annotations.exists(annIsEncodableString) symHasAnn || other.typeArgs.exists(loop) } } } loop(typeToCheck) } /** * Checks @EncodableStringAnnotation on either: * - accessed symbol (field/param), or * - type (TYPE_USE), via [[java.lang.reflect.AnnotatedType]]. */ def treeHasEncodableString(tree: Tree): Boolean = { val rawSym = tree.symbol val symHasAnn = rawSym != null && rawSym != NoSymbol && { val accessed = safeAccessed(rawSym) accessed != null && accessed != NoSymbol && accessed.annotations.exists(annIsEncodableString) } val methodReturnHasAnn = rawSym != null && rawSym != NoSymbol && (rawSym match { case m: MethodSymbol => typeHasEncodableString(m.typeSignature.finalResultType) case _ => false }) symHasAnn || methodReturnHasAnn || (tree.tpe != null && typeHasEncodableString(tree.tpe)) } def isPythonTemplateBuilderArg(argExpr: c.Expr[Any]): Boolean = { val tpe = argExpr.tree.tpe tpe != null && (tpe.dealias.widen <:< pythonTemplateBuilderTpe) } def isStringRendererArg(argExpr: c.Expr[Any]): Boolean = { val tpe = argExpr.tree.tpe tpe != null && (tpe.dealias.widen <:< stringRendererTpe) } /** True if the arg is Encodable (direct argument, not a nested builder). */ def isDirectEncodableStringArg(argExpr: c.Expr[Any]): Boolean = { if (isPythonTemplateBuilderArg(argExpr)) false else { val tpe = argExpr.tree.tpe // Previous/original behavior: // - treat already-wrapped EncodableStringRenderer as encodable // - OR detect @EncodableStringAnnotation on symbol/type (tpe != null && (tpe.dealias.widen <:< encodableStringRendererTpe)) || treeHasEncodableString(argExpr.tree) } } /** * Wrap an argument expression as a [[PythonTemplateBuilder.StringRenderer]] AST node. * * Priority: * 1) If it's already a StringRenderer, keep it (cast). * 2) Else if Encodable-marked, wrap as EncodableStringRenderer. * 3) Else wrap as PyLiteralStringRenderer. */ def wrapArg(argExpr: c.Expr[Any]): Tree = { val argTree = argExpr.tree val argType = argTree.tpe if (argType != null && (argType.dealias.widen <:< stringRendererTpe)) { q"$argTree.asInstanceOf[_root_.org.apache.texera.amber.pybuilder.PythonTemplateBuilder.StringRenderer]" } else if (treeHasEncodableString(argTree)) { q"_root_.org.apache.texera.amber.pybuilder.PythonTemplateBuilder.EncodableStringRenderer($argTree.toString)" } else { q"_root_.org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PyLiteralStringRenderer($argTree.toString)" } } } ================================================ FILE: common/pybuilder/src/main/scala/org/apache/texera/amber/pybuilder/EncodableStringAnnotation.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.pybuilder; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE_USE, ElementType.LOCAL_VARIABLE, ElementType.METHOD }) public @interface EncodableStringAnnotation {} ================================================ FILE: common/pybuilder/src/main/scala/org/apache/texera/amber/pybuilder/PythonLexerUtils.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.pybuilder /** * Pure helpers used by the macro for quick, best-effort Python lexical checks. * * These are intentionally *not* macro-dependent, so they can be unit tested normally. */ object PythonLexerUtils { def isIdentChar(c: Char): Boolean = c.isLetterOrDigit || c == '_' /** Characters that would make an Encodable-expression splice ambiguous/invalid if adjacent. */ def isBadNeighbor(c: Char): Boolean = c == '\'' || c == '"' || isIdentChar(c) /** Returns the substring after the last newline (used to reason about the "current line" context). */ def lineTail(s: String): String = { val lastNewlineIndex = s.lastIndexOf('\n') if (lastNewlineIndex >= 0) s.substring(lastNewlineIndex + 1) else s } /** * Detect whether the provided line tail contains an unclosed single or double quote. * * This is not a full Python parser; it is a small state machine tracking quote mode and escapes. */ def hasUnclosedQuote(lineText: String): Boolean = { var inSingleQuotes = false var inDoubleQuotes = false var escaped = false var i = 0 while (i < lineText.length) { val ch = lineText.charAt(i) if (escaped) escaped = false else if (ch == '\\') escaped = true else if (!inDoubleQuotes && ch == '\'') inSingleQuotes = !inSingleQuotes else if (!inSingleQuotes && ch == '"') inDoubleQuotes = !inDoubleQuotes i += 1 } inSingleQuotes || inDoubleQuotes } /** * Detect whether the provided line tail contains a `#` that is outside of any quote context. * * If true, any Encodable-expression splice after that point would be inside a Python comment. */ def hasCommentOutsideQuotes(lineText: String): Boolean = { var inSingleQuotes = false var inDoubleQuotes = false var escaped = false var i = 0 while (i < lineText.length) { val ch = lineText.charAt(i) if (escaped) escaped = false else if (ch == '\\') escaped = true else if (!inDoubleQuotes && ch == '\'') inSingleQuotes = !inSingleQuotes else if (!inSingleQuotes && ch == '"') inDoubleQuotes = !inDoubleQuotes else if (!inSingleQuotes && !inDoubleQuotes && ch == '#') return true i += 1 } false } } ================================================ FILE: common/pybuilder/src/main/scala/org/apache/texera/amber/pybuilder/PythonTemplateBuilder.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.pybuilder import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.RenderMode.{Encode, Plain} import java.nio.charset.StandardCharsets import java.util.Base64 import scala.language.experimental.macros import scala.reflect.macros.blackbox /** * Convenience type aliases for strings passed into the [[PythonTemplateBuilder]] interpolator. * * Design intent: * - Some strings are “UI-provided” and must be rendered as a Python expression that decodes base64 at runtime. * - Other strings are regular Python source fragments and should be spliced in as-is. * * The macro distinguishes Encodable strings via a TYPE_USE annotation (`String @EncodableStringAnnotation`). */ object PyStringTypes { /** * Treated as an Encodable string by the macro via a TYPE_USE annotation. * * Example: * {{{ * import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableStringType * import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._ * * val label: EncodableStringType = "Hello" * val code = pyb"print($label)" * }}} */ type EncodableString = String @EncodableStringAnnotation /** * Normal python string (macro defaults to [[PythonLiteral]] when no [[EncodableStringAnnotation]] is present). * * This alias exists mostly for readability and symmetry with [[EncodableStringFactory]]. */ type PythonLiteral = String /** * Helper “constructor” and constants for [[EncodableString]]. * * Note: the object and members are annotated so downstream type inference tends * to keep the TYPE_USE annotation attached in common scenarios. */ @EncodableStringAnnotation object EncodableStringFactory { /** Wrap a raw Scala string as an Encodable-marked string. */ @EncodableStringAnnotation def apply(s: String): EncodableString = s /** Empty Encodable string (still Encodable-marked). */ @EncodableStringAnnotation val empty: EncodableString = "" } /** * Helper “constructor” and constants for [[PythonLiteral]]. * * This does not apply any Encodable semantics. It is regular Scala `String` usage. */ object PyLiteralFactory { /** Identity wrapper, used as a readability hint at call sites. */ def apply(s: String): PythonLiteral = s /** Empty python string. */ val empty: PythonLiteral = "" } } /** * =PythonTemplateBuilder: ergonomic Python codegen via `pyb"..."`= * * This module provides a tiny DSL for assembling Python source code from Scala while preserving two competing goals: * (1) developers want to write templates that look like normal Python, and (2) user-provided text must not be injected * into the emitted Python as raw literals that can break syntax or create ambiguous token boundaries. * * The core idea is that every value spliced into a `pyb"..."` template is first classified into one of two buckets: * * - '''Python literals''' (ordinary Scala strings or already-safe fragments) are inserted as-is. * - '''Encodable strings''' (typically UI-provided text) are base64-encoded at build time and rendered as a *Python * expression* that decodes at runtime, rather than being embedded as a Python string literal. * * This classification is driven by a TYPE_USE annotation: `String @EncodableStringAnnotation`. The annotation is defined * with a runtime retention and is allowed on fields, parameters, local variables, and type uses, so it survives many * common Scala typing patterns (e.g., inferred vals, constructor params, or aliases). Users normally do not construct the * annotation directly; instead, they use helper type aliases/factories in `PyStringTypes` for readability. * * ==Render modes== * * A `PythonTemplateBuilder` can be rendered in two modes: * * - `plain`: emit everything as raw text (useful for debugging or when you know all content is safe). * - `encode`: emit encodable chunks as Python decode expressions (the default `toString` behavior). * * Internally this is represented as a small sealed trait enum (`RenderMode.Plain` / `RenderMode.Encode`) rather than an * integer flag, to keep call sites self-documenting and avoid “magic numbers”. * * ==Chunk model (immutable, composable)== * * A builder is an immutable list of chunks: * * - `Text(value)` for literal template parts * - `Value(renderer)` for interpolated arguments that know how to render in each mode * * Two concrete renderers are provided: * * - `EncodableStringRenderer`: pre-encodes `stringValue` as base64 (UTF-8) once, and in `Encode` mode produces a Python * expression like `self.decode_python_template('')` given by [[wrapWithPythonDecoderExpr]]. * - `PyLiteralStringRenderer`: always emits the raw string value unchanged. * * Builders can be concatenated with `+` (builder + builder), which merges adjacent `Text` chunks for compactness. * Direct concatenation with a plain `String` is intentionally unsupported to prevent bypassing the macro’s safety checks. * * ==How the `pyb"..."` macro works== * * The `pyb` interpolator is implemented as a Scala macro. At compile time it receives: * * - the literal parts from the `StringContext` (the “gaps” around `$args`) * - the argument trees corresponding to each `$arg` * * The macro’s pipeline is: * * 1. '''Extract literal parts''' from the `StringContext` AST and ensure they are *string literals*. If any part is not * a literal, compilation aborts. This prevents “template text” from being computed dynamically where correctness and * boundary analysis would become unreliable. * * 2. '''Classify direct encodable arguments''' using `EncodableInspector`: * it inspects both the argument symbol and the argument type to determine whether the encodable annotation is present. * This includes a small “accessor hop” so that annotations placed on fields/constructor params are still visible when * call sites reference getters. * * 3. '''Compile-time boundary validation for direct encodables''': * if an argument is directly encodable (and not a nested builder), `BoundaryValidator.validateCompileTime` is run on * its surrounding literal context. The validator performs quick lexical checks on the current line: * * - the splice must not occur inside an unclosed single/double-quoted string * - the splice must not occur after a `#` comment marker * - the splice must not be immediately adjacent to identifier characters or quote characters on either side * * These restrictions exist because an Encodable string renders as a Python *expression*, not a Python string literal. * Putting an expression inside quotes, inside a comment, or glued to an identifier would either be invalid Python or * silently change tokenization in surprising ways. * * 4. '''Lower each argument into a builder''': * every `$arg` becomes a `PythonTemplateBuilder`. * * - If the argument is already a `PythonTemplateBuilder`, it is used directly. * - Otherwise, it is wrapped into a `StringRenderer` (`EncodableStringRenderer` or `PyLiteralStringRenderer`) and * turned into a minimal builder containing a single `Value(...)` chunk. * * Each argument is evaluated once and stored in a fresh local `val __pyb_argN` so that expensive expressions or * side-effects are not duplicated by expansion. * * 5. '''Runtime safety for nested builders''': * for arguments that are themselves `PythonTemplateBuilder`s, the macro cannot always know at compile time whether they * contain Encodable chunks (they may be computed, returned, or composed elsewhere). For these nested builders, the macro * conditionally emits runtime guards *only when the surrounding context is unsafe* (inside quotes, after comments, or * adjacent to “bad neighbor” characters). The guard pattern is: * * {{{ * if (__pyb_argN.containsEncodableString) throw new IllegalArgumentException("...") * }}} * * This preserves the ergonomics of composing builders while keeping the same safety contract as direct splices. * * 6. '''Assemble the final builder''': * the macro concatenates `text0 + arg0 + text1 + arg1 + ... + textN` into one `PythonTemplateBuilder`. * * ==Lexical checks (best-effort, intentionally small)== * * The boundary rules rely on `PythonLexerUtils`, a tiny state machine that scans only the “current line tail” to decide * whether quotes are unbalanced and whether a `#` begins a comment outside quotes. This is not a full Python parser. * It is deliberately lightweight so the macro stays fast and so the helpers can be unit-tested independently. * * ==Extensibility notes== * * The design keeps all rendering behavior behind `StringRenderer`, and keeps boundary policy in `BoundaryValidator`. * If new encoding schemes, alternate runtime decode helpers, or additional safety rules are needed, they can be introduced * without rewriting the template-building API. In particular, swapping `wrapWithPythonDecoderExpr` or adding new renderers * is a contained change: the macro only needs to decide *which renderer* to use, not *how it renders*. */ object PythonTemplateBuilder { // ===== render mode enum (no Ints) ===== def wrapWithPythonDecoderExpr(text: String): String = s"self.decode_python_template('$text')" sealed trait RenderMode extends Product with Serializable object RenderMode { case object Plain extends RenderMode case object Encode extends RenderMode } // ===== wrappers ===== /** * Base abstraction for values that can be spliced into a [[PythonTemplateBuilder]]. * * A [[StringRenderer]] knows how to render itself depending on `mode`. */ sealed trait StringRenderer extends Product with Serializable { def stringValue: String def render(mode: RenderMode): String } /** * Encodable string: encoded-mode wraps with [[wrapWithPythonDecoderExpr]], * plain-mode is raw `stringValue`. */ final case class EncodableStringRenderer(stringValue: String) extends StringRenderer { private val encodedB64: String = Base64.getEncoder.encodeToString(stringValue.getBytes(StandardCharsets.UTF_8)) override def render(mode: RenderMode): String = if (mode == Encode) wrapWithPythonDecoderExpr(encodedB64) else stringValue } /** * Python literal string: always raw `stringValue` regardless of mode. */ final case class PyLiteralStringRenderer(stringValue: String) extends StringRenderer { override def render(mode: RenderMode): String = stringValue } // ===== internal chunk model ===== private[pybuilder] sealed trait Chunk extends Product with Serializable private[pybuilder] final case class Text(value: String) extends Chunk private[pybuilder] final case class Value(value: StringRenderer) extends Chunk /** * Build a [[PythonTemplateBuilder]] from literal parts and already-wrapped args. * * @param literalParts raw StringContext parts (length = args + 1) * @param pyArgs args wrapped as [[StringRenderer]] */ private[amber] def fromInterpolated( literalParts: List[String], pyArgs: List[StringRenderer] ): PythonTemplateBuilder = { require( literalParts.length == pyArgs.length + 1, s"pyb interpolator mismatch: parts=${literalParts.length}, args=${pyArgs.length}" ) val chunkBuilder = List.newBuilder[Chunk] chunkBuilder += Text(literalParts.head) var argIndex = 0 while (argIndex < pyArgs.length) { chunkBuilder += Value(pyArgs(argIndex)) chunkBuilder += Text(literalParts(argIndex + 1)) argIndex += 1 } new PythonTemplateBuilder(compact(chunkBuilder.result())) } /** Merge adjacent text chunks. */ private def compact(chunksToCompact: List[Chunk]): List[Chunk] = chunksToCompact.foldRight(List.empty[Chunk]) { case (Text(leftText), Text(rightText) :: remaining) => Text(leftText + rightText) :: remaining case (chunk, compactedTail) => chunk :: compactedTail } /** Concatenate chunk lists, merging boundary text chunks when possible. */ private def concatChunks(leftChunks: List[Chunk], rightChunks: List[Chunk]): List[Chunk] = (leftChunks, rightChunks) match { case (Nil, _) => rightChunks case (_, Nil) => leftChunks case _ => (leftChunks.last, rightChunks.head) match { case (Text(leftText), Text(rightText)) => compact(leftChunks.dropRight(1) ::: Text(leftText + rightText) :: rightChunks.tail) case _ => leftChunks ::: rightChunks } } // ===== custom interpolator ===== /** Adds the `pyb"..."` string interpolator. */ implicit final class PythonTemplateBuilderStringContext(private val stringContext: StringContext) extends AnyVal { def pyb(argValues: Any*): PythonTemplateBuilder = macro Macros.pybImpl } object Macros { /** Macro entry point for `pyb"..."`. */ def pybImpl(macroCtx: blackbox.Context)( argValues: macroCtx.Expr[Any]* ): macroCtx.Expr[PythonTemplateBuilder] = { import macroCtx.universe._ // Stable, fully-qualified references as Trees/TypeTrees (NOT Strings) val PTBTerm: Tree = q"_root_.org.apache.texera.amber.pybuilder.PythonTemplateBuilder" val PTBType: Tree = tq"_root_.org.apache.texera.amber.pybuilder.PythonTemplateBuilder" val StringRendererTpt: Tree = tq"_root_.org.apache.texera.amber.pybuilder.PythonTemplateBuilder.StringRenderer" val inspector = new EncodableInspector[macroCtx.type](macroCtx) val validator = new BoundaryValidator[macroCtx.type](macroCtx) // --- extract literal parts from StringContext --- val literalPartTrees: List[Tree] = macroCtx.prefix.tree match { case Apply(_, List(Apply(_, rawPartTrees))) => rawPartTrees case prefixTree => macroCtx.abort( macroCtx.enclosingPosition, s"pyb interpolator: cannot extract StringContext parts from: ${showRaw(prefixTree)}" ) } // Ensure parts are string literals. literalPartTrees.foreach { case Literal(Constant(_: String)) => // ok case nonLiteral => macroCtx.abort( macroCtx.enclosingPosition, s"pyb interpolator requires literal parts; got: ${showRaw(nonLiteral)}" ) } val literalPartStrings: List[String] = literalPartTrees.map { case Literal(Constant(s: String)) => s } // --- compile-time boundary checks for *direct* Encodable args --- argValues.toList.zipWithIndex.foreach { case (argExpr, argIndex) if inspector.isDirectEncodableStringArg(argExpr) => val leftPart = literalPartStrings(argIndex) val rightPart = literalPartStrings(argIndex + 1) val prefixSource = literalPartStrings.take(argIndex + 1).mkString("") val errorPos = if (argExpr.tree.pos != NoPosition) argExpr.tree.pos else macroCtx.enclosingPosition validator.validateCompileTime( validator.CompileTimeContext(leftPart, rightPart, prefixSource, argIndex, errorPos) ) case _ => // no-op } // --- builders for literal parts and args --- val emptyRenderArgs = q"_root_.scala.List.empty[$StringRendererTpt]" def textBuilder(partTree: Tree): Tree = q"$PTBTerm.fromInterpolated(_root_.scala.List($partTree), $emptyRenderArgs)" val emptyStrLit: Tree = Literal(Constant("")) def valueBuilder(argExpr: macroCtx.Expr[Any]): Tree = { val wrapped = inspector.wrapArg(argExpr) q"$PTBTerm.fromInterpolated(_root_.scala.List($emptyStrLit, $emptyStrLit), _root_.scala.List($wrapped))" } val pythonTemplateBuilderTpe = typeOf[_root_.org.apache.texera.amber.pybuilder.PythonTemplateBuilder] def argAsBuilder(argExpr: macroCtx.Expr[Any]): Tree = { val argTree = argExpr.tree val argType = argTree.tpe if (argType != null && (argType.dealias.widen <:< pythonTemplateBuilderTpe)) { q"$argTree.asInstanceOf[$PTBType]" } else { valueBuilder(argExpr) } } // Evaluate each arg once. val evaluatedArgBuilders: List[Tree] = argValues.toList.zipWithIndex.map { case (argExpr, i) => val argValName = TermName(s"__pyb_arg$i") q"val $argValName: $PTBType = ${argAsBuilder(argExpr)}" } // Runtime boundary checks for nested PythonTemplateBuilders that *may* contain Encodable chunks. val nestedBuilderBoundaryChecks: List[Tree] = argValues.toList.zipWithIndex.flatMap { case (argExpr, argIndex) if inspector.isPythonTemplateBuilderArg(argExpr) => val leftPart = literalPartStrings(argIndex) val rightPart = literalPartStrings(argIndex + 1) val prefixSource = literalPartStrings.take(argIndex + 1).mkString("") val argIdent = Ident(TermName(s"__pyb_arg$argIndex")) validator.runtimeChecksForNestedBuilder( validator.RuntimeContext(leftPart, rightPart, prefixSource, argIndex), argIdent ) case _ => Nil } // Concatenate: text0 + arg0 + text1 + arg1 + ... + textN val renderTree: Tree = { val baseTree = textBuilder(literalPartTrees.head) argValues.toList.zipWithIndex.foldLeft(baseTree) { case (acc, (_, i)) => val argIdent = Ident(TermName(s"__pyb_arg$i")) val nextText = textBuilder(literalPartTrees(i + 1)) q"$acc + $argIdent + $nextText" } } val finalExpr: Tree = q""" { ..$evaluatedArgBuilders ..$nestedBuilderBoundaryChecks $renderTree } """ macroCtx.Expr[PythonTemplateBuilder](finalExpr) } } } /** * An immutable builder for Python source produced via `pyb"..."` interpolation. */ final class PythonTemplateBuilder private[pybuilder] ( private val chunks: List[PythonTemplateBuilder.Chunk] ) extends Serializable { import PythonTemplateBuilder._ def +(that: PythonTemplateBuilder): PythonTemplateBuilder = new PythonTemplateBuilder(concatChunks(this.chunks, that.chunks)) def +(that: String): PythonTemplateBuilder = throw new UnsupportedOperationException(s"Direct String concatenation is not supported $that") def plain: String = render(Plain) def encode: String = render(Encode) override def toString: String = encode def containsEncodableString: Boolean = chunks.exists { case Value(_: EncodableStringRenderer) => true case _ => false } private def render(renderMode: RenderMode): String = { val out = new java.lang.StringBuilder chunks.foreach { case Text(text) => out.append(text) case Value(renderer) => out.append(renderer.render(renderMode)) } out.toString.stripMargin .replace("\r\n", "\n") .replace("\r", "\n") } } ================================================ FILE: common/pybuilder/src/test/scala/org/apache/texera/amber/pybuilder/PythonLexerUtilsSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.pybuilder import org.scalatest.funsuite.AnyFunSuite class PythonLexerUtilsSpec extends AnyFunSuite { // -------- isIdentChar -------- test("isIdentChar: lowercase letter is identifier char") { assert(PythonLexerUtils.isIdentChar('a')) } test("isIdentChar: uppercase letter is identifier char") { assert(PythonLexerUtils.isIdentChar('Z')) } test("isIdentChar: digit is identifier char") { assert(PythonLexerUtils.isIdentChar('5')) } test("isIdentChar: underscore is identifier char") { assert(PythonLexerUtils.isIdentChar('_')) } test("isIdentChar: dash is not identifier char") { assert(!PythonLexerUtils.isIdentChar('-')) } test("isIdentChar: space is not identifier char") { assert(!PythonLexerUtils.isIdentChar(' ')) } test("isIdentChar: hash is not identifier char") { assert(!PythonLexerUtils.isIdentChar('#')) } // -------- isBadNeighbor -------- test("isBadNeighbor: single quote is bad neighbor") { assert(PythonLexerUtils.isBadNeighbor('\'')) } test("isBadNeighbor: double quote is bad neighbor") { assert(PythonLexerUtils.isBadNeighbor('"')) } test("isBadNeighbor: identifier chars are bad neighbors") { assert(PythonLexerUtils.isBadNeighbor('a')) assert(PythonLexerUtils.isBadNeighbor('Z')) assert(PythonLexerUtils.isBadNeighbor('0')) assert(PythonLexerUtils.isBadNeighbor('_')) } test("isBadNeighbor: whitespace is not bad neighbor") { assert(!PythonLexerUtils.isBadNeighbor(' ')) } test("isBadNeighbor: punctuation like comma is not bad neighbor") { assert(!PythonLexerUtils.isBadNeighbor(',')) } // -------- lineTail -------- test("lineTail: string without newline returns full string") { val text = "no-newline" assert(PythonLexerUtils.lineTail(text) == text) } test("lineTail: returns text after single newline") { val text = "first\nsecond" assert(PythonLexerUtils.lineTail(text) == "second") } test("lineTail: returns text after last newline") { val text = "a\nb\nc\nlast-line" assert(PythonLexerUtils.lineTail(text) == "last-line") } test("lineTail: works with trailing newline (returns empty)") { val text = "first\nsecond\n" assert(PythonLexerUtils.lineTail(text) == "") } // -------- hasUnclosedQuote -------- test("hasUnclosedQuote: empty string has no unclosed quote") { assert(!PythonLexerUtils.hasUnclosedQuote("")) } test("hasUnclosedQuote: balanced single quotes returns false") { assert(!PythonLexerUtils.hasUnclosedQuote("'a'")) } test("hasUnclosedQuote: balanced double quotes returns false") { assert(!PythonLexerUtils.hasUnclosedQuote("\"a\"")) } test("hasUnclosedQuote: unclosed single quote returns true") { assert(PythonLexerUtils.hasUnclosedQuote("'unclosed")) } test("hasUnclosedQuote: unclosed double quote returns true") { assert(PythonLexerUtils.hasUnclosedQuote("\"unclosed")) } test("hasUnclosedQuote: escaped single quote inside single quotes does not break balance") { val text = "'it\\'s ok'" assert(!PythonLexerUtils.hasUnclosedQuote(text)) } test("hasUnclosedQuote: escaped double quote inside double quotes does not break balance") { val text = "\"he said \\\"hi\\\"\"" assert(!PythonLexerUtils.hasUnclosedQuote(text)) } test("hasUnclosedQuote: mixed quotes with proper closing returns false") { val text = "'a' + \"b\"" assert(!PythonLexerUtils.hasUnclosedQuote(text)) } // -------- hasCommentOutsideQuotes -------- test("hasCommentOutsideQuotes: no hash means no comment") { assert(!PythonLexerUtils.hasCommentOutsideQuotes("print(1)")) } test("hasCommentOutsideQuotes: hash outside quotes is a comment") { assert(PythonLexerUtils.hasCommentOutsideQuotes("x = 1 # comment")) } test("hasCommentOutsideQuotes: hash inside single quotes is not a comment") { assert(!PythonLexerUtils.hasCommentOutsideQuotes("print('# not comment')")) } test("hasCommentOutsideQuotes: hash inside double quotes is not a comment") { assert(!PythonLexerUtils.hasCommentOutsideQuotes("print(\"# not comment\")")) } test("hasCommentOutsideQuotes: escaped quotes preserve quote state correctly") { val line = "print(\"\\\"# still in string\\\"\") # comment here" assert(PythonLexerUtils.hasCommentOutsideQuotes(line)) } test("hasCommentOutsideQuotes: multiple hashes only first outside quotes matters") { val line = "print('# in string') # real comment # more" assert(PythonLexerUtils.hasCommentOutsideQuotes(line)) } } ================================================ FILE: common/pybuilder/src/test/scala/org/apache/texera/amber/pybuilder/PythonTemplateBuilderApiSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.pybuilder import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.RenderMode.{Encode, Plain} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.{ EncodableStringRenderer, PyLiteralStringRenderer, fromInterpolated, wrapWithPythonDecoderExpr } import org.scalatest.funsuite.AnyFunSuite import java.nio.charset.StandardCharsets import java.util.Base64 /** * Covers the non-macro public surface of PythonTemplateBuilder that PythonTemplateBuilderSpec * exercises only incidentally: factories, renderer mode constants, render normalization, * concatenation operators, and require/throw preconditions. */ class PythonTemplateBuilderApiSpec extends AnyFunSuite { private def b64(s: String): String = Base64.getEncoder.encodeToString(s.getBytes(StandardCharsets.UTF_8)) // -------- wrapWithPythonDecoderExpr -------- test("wrapWithPythonDecoderExpr wraps text into a decode_python_template call") { assert(wrapWithPythonDecoderExpr("abc") == "self.decode_python_template('abc')") } test("wrapWithPythonDecoderExpr does not escape inner content (caller's responsibility)") { // The current contract simply interpolates the raw text. Pinning this so a future // escape-aware version trips this spec deliberately. assert(wrapWithPythonDecoderExpr("a'b") == "self.decode_python_template('a'b')") } // -------- RenderMode -------- test("RenderMode.Plain and RenderMode.Encode are distinct singletons") { assert(Plain != Encode) assert(Plain eq PythonTemplateBuilder.RenderMode.Plain) assert(Encode eq PythonTemplateBuilder.RenderMode.Encode) } // -------- EncodableStringRenderer -------- test("EncodableStringRenderer.render(Plain) returns the raw stringValue") { val r = EncodableStringRenderer("abc") assert(r.render(Plain) == "abc") assert(r.stringValue == "abc") } test("EncodableStringRenderer.render(Encode) wraps base64 with the python decoder expr") { val r = EncodableStringRenderer("abc") assert(r.render(Encode) == s"self.decode_python_template('${b64("abc")}')") } test("EncodableStringRenderer handles empty string in both modes") { val r = EncodableStringRenderer("") assert(r.render(Plain) == "") assert(r.render(Encode) == "self.decode_python_template('')") } test("EncodableStringRenderer uses UTF-8 base64 for non-ASCII content") { val raw = "你好" val r = EncodableStringRenderer(raw) assert(r.render(Encode) == s"self.decode_python_template('${b64(raw)}')") } // -------- PyLiteralStringRenderer -------- test("PyLiteralStringRenderer.render ignores mode and returns the raw stringValue") { val r = PyLiteralStringRenderer("print('x')") assert(r.render(Plain) == "print('x')") assert(r.render(Encode) == "print('x')") } // -------- PyStringTypes factories -------- test("PyStringTypes.EncodableStringFactory.apply returns the input string unchanged") { val out: String = PyStringTypes.EncodableStringFactory("hi") assert(out == "hi") } test("PyStringTypes.EncodableStringFactory.empty is the empty string") { val out: String = PyStringTypes.EncodableStringFactory.empty assert(out.isEmpty) } test("PyStringTypes.PyLiteralFactory.apply returns the input string unchanged") { assert(PyStringTypes.PyLiteralFactory("hi") == "hi") } test("PyStringTypes.PyLiteralFactory.empty is the empty string") { assert(PyStringTypes.PyLiteralFactory.empty.isEmpty) } // -------- fromInterpolated precondition -------- test("fromInterpolated requires parts.length == args.length + 1") { val thrown = intercept[IllegalArgumentException] { fromInterpolated(List("only-one-part"), List(EncodableStringRenderer("x"))) } assert(thrown.getMessage.contains("pyb interpolator mismatch")) assert(thrown.getMessage.contains("parts=1")) assert(thrown.getMessage.contains("args=1")) } test("fromInterpolated with zero args and one literal part renders that part") { val b = fromInterpolated(List("only"), Nil) assert(b.plain == "only") } test("fromInterpolated alternates text/value chunks in order") { val b = fromInterpolated( List("a-", "-b-", "-c"), List(PyLiteralStringRenderer("X"), PyLiteralStringRenderer("Y")) ) assert(b.plain == "a-X-b-Y-c") } // -------- PythonTemplateBuilder.+ and concatChunks -------- test("operator + merges adjacent literal-only builders into a single text chunk") { val left = fromInterpolated(List("hello "), Nil) val right = fromInterpolated(List("world"), Nil) val merged = left + right assert(merged.plain == "hello world") // Round-trip through encode mode to ensure no chunk fan-out side effects. assert(merged.encode == "hello world") } test("operator + preserves value chunks across the join boundary") { val left = fromInterpolated(List("pre-", "-mid"), List(EncodableStringRenderer("L"))) val right = fromInterpolated(List("-end"), Nil) val merged = left + right assert(merged.plain == "pre-L-mid-end") assert(merged.encode == s"pre-${"self.decode_python_template('" + b64("L") + "')"}-mid-end") } test("operator + with empty left builder returns content equivalent to right") { val left = fromInterpolated(List(""), Nil) val right = fromInterpolated(List("hi"), Nil) assert((left + right).plain == "hi") } test("operator + with empty right builder returns content equivalent to left") { val left = fromInterpolated(List("hi"), Nil) val right = fromInterpolated(List(""), Nil) assert((left + right).plain == "hi") } test("operator +(String) is unsupported and includes the offending string in the message") { val b = fromInterpolated(List("x"), Nil) val thrown = intercept[UnsupportedOperationException] { b + "oops" } assert(thrown.getMessage.contains("oops")) } // -------- render() line-ending normalization -------- test("render normalizes CRLF to LF") { val b = fromInterpolated(List("a\r\nb"), Nil) assert(b.plain == "a\nb") } test("render normalizes lone CR to LF") { val b = fromInterpolated(List("a\rb"), Nil) assert(b.plain == "a\nb") } test("render preserves existing LF unchanged") { val b = fromInterpolated(List("a\nb"), Nil) assert(b.plain == "a\nb") } test("render applies stripMargin (margin char '|' strips preceding whitespace per line)") { val b = fromInterpolated(List("first\n |second"), Nil) assert(b.plain == "first\nsecond") } // -------- containsEncodableString on edge inputs -------- test("containsEncodableString is false for a pure-text builder") { val b = fromInterpolated(List("just text"), Nil) assert(!b.containsEncodableString) } test("containsEncodableString is false for a builder holding only PyLiteralStringRenderer") { val b = fromInterpolated( List("", ""), List(PyLiteralStringRenderer("raw")) ) assert(!b.containsEncodableString) } test("containsEncodableString is true if any chunk is an EncodableStringRenderer") { val b = fromInterpolated( List("", "", ""), List(PyLiteralStringRenderer("a"), EncodableStringRenderer("b")) ) assert(b.containsEncodableString) } // -------- triple-quoted Python: pinning current (not-triple-quote-aware) behavior -------- // // PythonLexerUtils tracks single/double quote state one character at a time and does not // recognize Python triple-quoted strings as a single token. With six balanced quotes the // lexer happens to also report balanced, but the *intermediate* states matter: any time the // line tail ends with an odd count of `"`/`'`, hasUnclosedQuote returns true. // // These pin the current conservative behavior. If a future change makes the lexer aware of // triple-quoted strings, these specs should be revisited intentionally. test("hasUnclosedQuote: six matched double quotes are seen as balanced") { assert(!PythonLexerUtils.hasUnclosedQuote("\"\"\"abc\"\"\"")) } test("hasUnclosedQuote: three opening double quotes count as unclosed") { // A Python triple-quoted string opener `\"\"\"` is currently reported as 'inside string'. assert(PythonLexerUtils.hasUnclosedQuote("\"\"\"abc")) } test("hasUnclosedQuote: three opening single quotes count as unclosed") { assert(PythonLexerUtils.hasUnclosedQuote("'''abc")) } } ================================================ FILE: common/pybuilder/src/test/scala/org/apache/texera/amber/pybuilder/PythonTemplateBuilderSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.pybuilder import org.apache.texera.amber.pybuilder.PyStringTypes.{EncodableString, PythonLiteral} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.{ EncodableStringRenderer, PyLiteralStringRenderer, PythonTemplateBuilderStringContext } import org.scalatest.funsuite.AnyFunSuite import java.nio.charset.StandardCharsets import java.util.Base64 import scala.annotation.meta.field import scala.reflect.runtime.currentMirror import scala.tools.reflect.ToolBox class PythonTemplateBuilderSpec extends AnyFunSuite { // ---------- Helpers ---------- private def base64Of(text: String): String = Base64.getEncoder.encodeToString(text.getBytes(StandardCharsets.UTF_8)) private def decodeExpr(text: String): String = PythonTemplateBuilder.wrapWithPythonDecoderExpr(base64Of(text)) // Toolbox helpers: used to assert runtime exceptions without checking error strings. private lazy val tb: ToolBox[scala.reflect.runtime.universe.type] = currentMirror.mkToolBox() private def inPybuilderPkg(code: String): String = s"""package org.apache.texera.amber.pybuilder { | |$code | |}""".stripMargin private def assertToolboxDoesNotCompile(code: String): Unit = { intercept[Throwable] { // compile only (don’t run); macro expansion happens during compilation tb.compile(tb.parse(inPybuilderPkg(code))) } () } // Unicode escapes in *generated* Scala source: must be written as "\\uXXXX" in this test file. private def scalaUnicodeEscape(ch: Char): String = f"\\\\u${ch.toInt}%04X" // ======================================================================== // Rendering basics (plain vs encoded) // ======================================================================== test("plain renders empty text") { val builder = pyb"" assert(builder.plain == "") } test("plain renders literal text") { val builder = pyb"hello" assert(builder.plain == "hello") } test("encoded renders literal text (no UI args) same as plain") { val builder = pyb"hello" assert(builder.encode == "hello") } test("toString defaults to encoded") { val builder = pyb"hello" assert(builder.toString == builder.encode) } test("StringPyMk renders raw in both modes") { val pyFragment = PyLiteralStringRenderer("print('x')") assert(pyFragment.render(PythonTemplateBuilder.RenderMode.Plain) == "print('x')") assert(pyFragment.render(PythonTemplateBuilder.RenderMode.Encode) == "print('x')") } test("EncodableString renders raw in plain mode") { val uiText = EncodableStringRenderer("abc") assert(uiText.render(PythonTemplateBuilder.RenderMode.Plain) == "abc") } test("EncodableString renders B64.decode('') in encoded mode") { val rawText = "abc" val uiText = EncodableStringRenderer(rawText) assert(uiText.render(PythonTemplateBuilder.RenderMode.Encode) == decodeExpr(rawText)) } test("EncodableString base64 uses UTF-8 and handles unicode") { val rawText = "你好 👋" val uiText = EncodableStringRenderer(rawText) assert(uiText.render(PythonTemplateBuilder.RenderMode.Encode) == decodeExpr(rawText)) assert(uiText.render(PythonTemplateBuilder.RenderMode.Plain) == rawText) } test("pyb interpolator defaults to StringPyMk for normal values (toString)") { val value = 42 val builder = pyb"val=$value" assert(builder.plain == "val=42") assert(builder.encode == "val=42") } test("pyb supports multiple args") { val firstValue = 1 val secondValue = "two" val thirdValue = 3.0 val builder = pyb"a=$firstValue b=$secondValue c=$thirdValue" assert(builder.plain == "a=1 b=two c=3.0") } test("passing a PyString (EncodableString) is preserved (no re-wrapping)") { val rawText = "ui" val uiPyString: PythonTemplateBuilder.StringRenderer = EncodableStringRenderer(rawText) val builder = pyb"$uiPyString" assert(builder.plain == rawText) assert(builder.encode == decodeExpr(rawText)) } test("passing a PyString (StringPyMk) is preserved") { val rawPy: PythonTemplateBuilder.StringRenderer = PyLiteralStringRenderer("x + 1") val builder = pyb"$rawPy" assert(builder.plain == "x + 1") assert(builder.encode == "x + 1") } // ======================================================================== // Whitespace / multiline / normalization // ======================================================================== test("stripMargin is applied on render() output") { val builder = pyb"""|line1 |line2""" assert(builder.plain == "line1\nline2") } test("stripMargin works with interpolation too") { val value = 7 val builder = pyb"""|line1 $value |line2""" assert(builder.plain == "line1 7\nline2") } // ======================================================================== // Concatenation // ======================================================================== test("operator + concatenates builders") { val left = pyb"hello " val right = pyb"world" assert((left + right).plain == "hello world") } test("operator + preserves encoded behavior when mixing UI and raw") { val uiText = EncodableStringRenderer("X") val prefix = pyb"pre:" val middle = pyb"$uiText" val suffix = pyb":post" val combined = prefix + middle + suffix assert(combined.plain == "pre:X:post") assert(combined.encode == s"pre:${decodeExpr("X")}:post") } test("repeated concatenation still renders correctly") { val combined = pyb"a" + pyb"b" + pyb"c" assert(combined.plain == "abc") assert(combined.encode == "abc") } test("empty builder renders empty") { val builder = pyb"" assert(builder.plain.isEmpty) assert(builder.encode.isEmpty) } // ======================================================================== // Annotation / TYPE_USE behavior // ======================================================================== test("TYPE_USE alias EncodableString triggers UI encoding") { val uiText: EncodableString = "hello" val builder = pyb"$uiText" assert(builder.plain == "hello") assert(builder.encode == decodeExpr("hello")) } test("EncodableString helper apply triggers UI encoding") { val uiText: EncodableString = PyStringTypes.EncodableStringFactory("hey") val builder = pyb"$uiText" assert(builder.encode == decodeExpr("hey")) } test("TYPE_USE annotation on val type triggers UI encoding") { val uiText: String @EncodableStringAnnotation = "typeuse" val builder = pyb"$uiText" assert(builder.encode == decodeExpr("typeuse")) } test("@StringUI parameter triggers UI encoding") { def build(@EncodableStringAnnotation uiText: String): PythonTemplateBuilder = pyb"$uiText" val builder = build("param") assert(builder.encode == decodeExpr("param")) } test("@StringUI local val triggers UI encoding") { def build(): PythonTemplateBuilder = { @EncodableStringAnnotation val uiText: String = "local" pyb"$uiText" } val builder = build() assert(builder.encode == decodeExpr("local")) } test("@StringUI local val triggers UI encoding even when type is inferred") { def build(): PythonTemplateBuilder = { @EncodableStringAnnotation val uiText = "local-inferred" pyb"$uiText" } val builder = build() assert(builder.encode == decodeExpr("local-inferred")) } test("@StringUI lambda parameter triggers UI encoding") { val uiToBuilder: (String @EncodableStringAnnotation) => PythonTemplateBuilder = uiText => pyb"$uiText" val builder = uiToBuilder("lambda") assert(builder.encode == decodeExpr("lambda")) } test("@StringUI lambda param + map + mkString triggers UI encoding per element") { val rawItems = List("a", "b", "c") val joinedEncoded = rawItems .map((uiItem: String @EncodableStringAnnotation) => pyb"$uiItem") .mkString("[", ", ", "]") assert(joinedEncoded == s"[${rawItems.map(decodeExpr).mkString(", ")}]") } test("List[String @StringUI] element access preserves UI encoding") { val uiItems: List[String @EncodableStringAnnotation] = List("first", "second") val first = uiItems.head val builder = pyb"$first" assert(builder.encode == decodeExpr("first")) } test("Erasing List[String @StringUI] to List[String] drops UI encoding") { val uiItems: List[String @EncodableStringAnnotation] = List("erased") val erased: List[String] = uiItems.map((uiItem: String @EncodableStringAnnotation) => (uiItem: String)) val builder = pyb"${erased.head}" assert(builder.encode == "erased") } test("@(StringUI @field) on case class field triggers UI encoding via accessor/field") { final case class WithFieldAnnotation(@(EncodableStringAnnotation @field) uiText: String) val value = WithFieldAnnotation("field") val builder = pyb"${value.uiText}" assert(builder.encode == decodeExpr("field")) } test("@StringUI on case class param without @field does not trigger UI encoding via accessor") { final case class WithoutFieldAnnotation(@EncodableStringAnnotation uiText: String) val value = WithoutFieldAnnotation("param-only") val builder = pyb"${value.uiText}" assert(builder.encode == "param-only") } test("@StringUI method annotation triggers UI encoding") { object Holder { @EncodableStringAnnotation def uiText: String = "method" } val builder = pyb"${Holder.uiText}" assert(builder.encode == decodeExpr("method")) } test("@StringUI method annotation on def with parens triggers UI encoding") { object Holder { @EncodableStringAnnotation def uiText(): String = "method-parens" } val builder = pyb"${Holder.uiText()}" assert(builder.encode == decodeExpr("method-parens")) } test("unannotated String does not become UI (stays raw python)") { val rawText: String = "raw" val builder = pyb"$rawText" assert(builder.encode == "raw") } test("StringPy alias remains raw") { val rawText: PythonLiteral = "raw2" val builder = pyb"$rawText" assert(builder.encode == "raw2") } // ======================================================================== // Compile-time checks (direct UI args) // ======================================================================== test("UI with whitespace boundaries compiles") { assertCompiles(""" import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._ import org.apache.texera.amber.pybuilder.PyStringTypes._ object UiWhitespaceBoundariesOk { val ui: EncodableString = "x"; val b = pyb"foo $ui bar" } """) } test("UI next to comma is allowed (common in function args)") { assertCompiles(""" import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._ import org.apache.texera.amber.pybuilder.PyStringTypes._ object UiCommaOk { val ui: EncodableString = "x"; val b = pyb"f($ui, 1)" } """) } test("UI next to parentheses is allowed") { assertCompiles(""" import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._ import org.apache.texera.amber.pybuilder.PyStringTypes._ object UiParensOk { val ui: EncodableString = "x"; val b = pyb"($ui)" } """) } test("hash inside quotes does not count as a comment marker (UI allowed afterwards)") { assertCompiles(""" import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._ import org.apache.texera.amber.pybuilder.PyStringTypes._ object HashInQuotesOk { val ui: EncodableString = "x"; val b = pyb"print('#') $ui" } """) } test("UI glued to identifier on the left does not compile") { assertDoesNotCompile(""" import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._ import org.apache.texera.amber.pybuilder.PyStringTypes._ object UiGluedLeftBad { val ui: EncodableString = "x"; val b = pyb"foo$ui" } """) } test("UI glued to identifier on the right does not compile") { assertDoesNotCompile(""" import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._ import org.apache.texera.amber.pybuilder.PyStringTypes._ object UiGluedRightBad { val ui: EncodableString = "x"; val b = pyb"${ui}bar" } """) } test("UI glued to a quote on the right does not compile") { assertDoesNotCompile(""" import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._ import org.apache.texera.amber.pybuilder.PyStringTypes._ object UiGluedQuoteBad { val ui: EncodableString = "x"; val b = pyb"${ui}'" } """) } test("UI placed inside a quoted python string literal does not compile (single quotes)") { assertDoesNotCompile(""" import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._ import org.apache.texera.amber.pybuilder.PyStringTypes._ object UiInsideSingleQuotesBad { val ui: EncodableString = "x"; val b = pyb"print('${ui}')" } """) } test("UI placed inside a quoted python string literal does not compile (double quotes)") { assertDoesNotCompile(""" import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._ import org.apache.texera.amber.pybuilder.PyStringTypes._ object UiInsideDoubleQuotesBad { val ui: EncodableString = "x" val b = pyb"print(\\"${ui}\\")" } """) } test("UI placed after a python comment marker on same line does not compile") { assertDoesNotCompile(""" import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._ import org.apache.texera.amber.pybuilder.PyStringTypes._ object UiAfterCommentBad { val ui: EncodableString = "x"; val b = pyb"foo # ${ui}" } """) } test("UI placed after a python comment marker on same line does not compile (no whitespace)") { assertDoesNotCompile(""" import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._ import org.apache.texera.amber.pybuilder.PyStringTypes._ object UiAfterCommentNoSpaceBad { val ui: EncodableString = "x"; val b = pyb"foo #${ui}" } """) } test("comment marker on previous line does not affect next line (lineTail behavior)") { assertCompiles( "import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._\n" + "import org.apache.texera.amber.pybuilder.PyStringTypes._\n" + "object CommentPrevLineOk {\n" + " val ui: EncodableString = \"x\"\n" + " val b = pyb\"\"\"|# comment\n" + " |$ui\"\"\"\n" + "}\n" ) } test("PyString (EncodableString) glued to identifier on the left does not compile") { assertDoesNotCompile(""" import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._ import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.EncodableString object PyStringGluedLeftBad { val ui = EncodableString("x"); val b = pyb"foo${ui}" } """) } test("PyString (EncodableString) inside a quoted python string literal does not compile") { assertDoesNotCompile(""" import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._ import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.EncodableString object PyStringInsideQuotesBad { val ui = EncodableString("x"); val b = pyb"print('${ui}')" } """) } test("all isBadNeighbor characters reject direct UI adjacency at compile time (left + right)") { val candidates = (33 to 126).map(_.toChar) // printable ASCII, avoids whitespace val badChars = candidates.filter(PythonLexerUtils.isBadNeighbor) // This is intentionally exhaustive over the implementation-defined "bad neighbor" set. // We assert only compile success/failure, not the specific error message. badChars.zipWithIndex.foreach { case (ch, i) => val esc = scalaUnicodeEscape(ch) val leftAdj = s""" |import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._ |import org.apache.texera.amber.pybuilder.PyStringTypes._ |object UiBadLeft_$i { | val ui: EncodableString = "x" | val b = pyb\"\"\"pre$esc${'$'}{ui}post\"\"\" |} |""".stripMargin val rightAdj = s""" |import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._ |import org.apache.texera.amber.pybuilder.PyStringTypes._ |object UiBadRight_$i { | val ui: EncodableString = "x" | val b = pyb\"\"\"pre${'$'}{ui}$esc post\"\"\" |} |""".stripMargin assertToolboxDoesNotCompile(leftAdj) assertToolboxDoesNotCompile(rightAdj) } } // ======================================================================== // Interpolator semantics / evaluation // ======================================================================== test("interpolated args are evaluated once and not re-evaluated on render") { var evalCount = 0 def nextValue(): String = { evalCount += 1 "v" } val builder = pyb"${nextValue()}" assert(evalCount == 1) builder.plain assert(evalCount == 1) builder.encode assert(evalCount == 1) } // ======================================================================== // Nested PythonTemplateBuilder behavior (mode propagation + runtime UI checks) // ======================================================================== test("nested PythonTemplateBuilder with UI propagates mode (plain)") { val uiText = EncodableStringRenderer("Z") val inner = pyb"X=$uiText" val outer = pyb"pre $inner post" assert(outer.plain == "pre X=Z post") } test("nested PythonTemplateBuilder with UI propagates mode (encoded)") { val uiText = EncodableStringRenderer("Z") val inner = pyb"X=$uiText" val outer = pyb"pre $inner post" assert(outer.encode == s"pre X=${decodeExpr("Z")} post") } test( "nested PythonTemplateBuilder without UI can appear inside python quotes (no runtime checks)" ) { val inner = pyb"hello" val outer = pyb"print('$inner')" assert(outer.plain == "print('hello')") assert(outer.encode == "print('hello')") } test("containsUi detects UI chunks correctly") { val rawBuilder = pyb"raw" val uiBuilder = pyb"${EncodableStringRenderer("x")}" val combined = rawBuilder + uiBuilder assert(!rawBuilder.containsEncodableString) assert(uiBuilder.containsEncodableString) assert(combined.containsEncodableString) } test("nested PythonTemplateBuilder containing UI inside single quotes throws at runtime") { val inner = pyb"${EncodableStringRenderer("x")}" intercept[IllegalArgumentException] { pyb"print('$inner')" } } test("nested PythonTemplateBuilder containing UI inside double quotes throws at runtime") { val inner = pyb"${EncodableStringRenderer("x")}" intercept[IllegalArgumentException] { pyb"""print("$inner")""" } } test( "nested PythonTemplateBuilder containing UI after comment marker throws at runtime (with and without whitespace)" ) { val inner = pyb"${EncodableStringRenderer("x")}" intercept[IllegalArgumentException] { pyb"foo # $inner" } intercept[IllegalArgumentException] { pyb"foo #$inner" } } test("nested PythonTemplateBuilder containing UI glued to identifier/digit throws at runtime") { val inner = pyb"${EncodableStringRenderer("x")}" intercept[IllegalArgumentException] { pyb"foo$inner" } intercept[IllegalArgumentException] { pyb"${inner}bar" } intercept[IllegalArgumentException] { pyb"1$inner" } intercept[IllegalArgumentException] { pyb"${inner}2" } } test( "runtime guard does NOT throw when nested builder has no UI, even in unsafe boundary contexts" ) { val inner = pyb"hello" val outer1 = pyb"foo$inner" val outer2 = pyb"${inner}bar" val outer3 = pyb"print('$inner')" val outer4 = pyb"foo #$inner" assert(outer1.plain == "foohello") assert(outer2.plain == "hellobar") assert(outer3.plain == "print('hello')") assert(outer4.plain == "foo #hello") } test("nested PythonTemplateBuilder containing UI with safe whitespace boundaries is allowed") { val inner = pyb"${EncodableStringRenderer("x")}" val outer = pyb"foo $inner bar" assert(outer.plain == "foo x bar") assert(outer.encode == s"foo ${decodeExpr("x")} bar") } test("nested PythonTemplateBuilder containing UI next to punctuation is allowed") { val inner = pyb"${EncodableStringRenderer("x")}" val outer = pyb"f($inner, 1)" assert(outer.plain == "f(x, 1)") assert(outer.encode == s"f(${decodeExpr("x")}, 1)") } test("stripMargin works across nested builders") { val inner = pyb"""A |B""" val outer = pyb"""|start |$inner |end""" assert(outer.plain == "start\nA\nB\nend") } test("""format(): EncodableString arg after closing quote is allowed""") { val workflowParam = "wf" val portParam = PythonTemplateBuilder.EncodableStringRenderer("P") val builder = pyb""""$workflowParam".format($portParam)""" assert(builder.plain == "\"wf\".format(P)") assert(builder.encode.contains("self.decode_python_template(")) } test( "format(): nested PythonTemplateBuilder containing UI is allowed (no runtime false positive)" ) { val workflowParam = "wf" val portParam = pyb"int (${PythonTemplateBuilder.EncodableStringRenderer("\\.")})," val builder = pyb""""$workflowParam".format($portParam)""" assert(builder.plain.contains("format(int (\\.),")) assert(builder.encode.contains("self.decode_python_template(")) } test("still rejects nested UI builder inside Python quotes at runtime") { val portParam = pyb"${PythonTemplateBuilder.EncodableStringRenderer("P")}" intercept[IllegalArgumentException] { pyb"print('${portParam}')".plain } } } ================================================ FILE: common/workflow-core/build.sbt ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. ///////////////////////////////////////////////////////////////////////////// // Project Settings ///////////////////////////////////////////////////////////////////////////// name := "workflow-core" enablePlugins(JavaAppPackaging) // Enable semanticdb for Scalafix ThisBuild / semanticdbEnabled := true ThisBuild / semanticdbVersion := scalafixSemanticdb.revision // Manage dependency conflicts by always using the latest revision ThisBuild / conflictManager := ConflictManager.latestRevision // Restrict parallel execution of tests to avoid conflicts Global / concurrentRestrictions += Tags.limit(Tags.Test, 1) ///////////////////////////////////////////////////////////////////////////// // Compiler Options ///////////////////////////////////////////////////////////////////////////// // Scala compiler options Compile / scalacOptions ++= Seq( "-Xelide-below", "WARNING", // Turn on optimizations with "WARNING" as the threshold "-feature", // Check feature warnings "-deprecation", // Check deprecation warnings "-Ywarn-unused:imports" // Check for unused imports ) ///////////////////////////////////////////////////////////////////////////// // ScalaPB Configuration ///////////////////////////////////////////////////////////////////////////// // Exclude some proto files PB.generate / excludeFilter := "scalapb.proto" // Set the protoc version for ScalaPB ThisBuild / PB.protocVersion := "3.19.4" // ScalaPB code generation for .proto files Compile / PB.targets := Seq( scalapb.gen(singleLineToProtoString = true) -> (Compile / sourceManaged).value ) // Mark the ScalaPB-generated directory as a generated source root Compile / managedSourceDirectories += (Compile / sourceManaged).value // ScalaPB library dependencies libraryDependencies ++= Seq( "com.thesamet.scalapb" %% "scalapb-runtime" % scalapb.compiler.Version.scalapbVersion % "protobuf", "com.thesamet.scalapb" %% "scalapb-json4s" % "0.12.0" // For ScalaPB 0.11.x ) // Enable protobuf compilation in Test Test / PB.protoSources += PB.externalSourcePath.value ///////////////////////////////////////////////////////////////////////////// // Test-related Dependencies ///////////////////////////////////////////////////////////////////////////// val testcontainersVersion = "0.44.1" libraryDependencies ++= Seq( "org.scalamock" %% "scalamock" % "5.2.0" % Test, // ScalaMock "org.scalatest" %% "scalatest" % "3.2.15" % Test, // ScalaTest "junit" % "junit" % "4.13.2" % Test, // JUnit "com.novocode" % "junit-interface" % "0.11" % Test, // SBT interface for JUnit "com.dimafeng" %% "testcontainers-scala-scalatest" % testcontainersVersion % Test, // Testcontainers ScalaTest integration "com.dimafeng" %% "testcontainers-scala-minio" % testcontainersVersion % Test // MinIO Testcontainer Scala integration ) ///////////////////////////////////////////////////////////////////////////// // Jackson-related Dependencies ///////////////////////////////////////////////////////////////////////////// val jacksonVersion = "2.18.6" libraryDependencies ++= Seq( "javax.validation" % "validation-api" % "2.0.1.Final", "com.fasterxml.jackson.core" % "jackson-databind" % jacksonVersion, // Jackson Databind "com.fasterxml.jackson.module" % "jackson-module-kotlin" % jacksonVersion % Test, // Jackson Kotlin Module "com.fasterxml.jackson.datatype" % "jackson-datatype-jdk8" % jacksonVersion % Test, // Jackson JDK8 Datatypes "com.fasterxml.jackson.datatype" % "jackson-datatype-jsr310" % jacksonVersion % Test, // Jackson JSR310 "com.fasterxml.jackson.datatype" % "jackson-datatype-joda" % jacksonVersion % Test, // Jackson Joda "com.fasterxml.jackson.module" % "jackson-module-jsonSchema" % jacksonVersion, // JSON Schema Module "com.fasterxml.jackson.module" %% "jackson-module-scala" % jacksonVersion, // Scala Module "com.fasterxml.jackson.module" % "jackson-module-no-ctor-deser" % jacksonVersion // No Constructor Deserializer ) ///////////////////////////////////////////////////////////////////////////// // Arrow related val arrowVersion = "15.0.2" val nettyVersion = "4.1.96.Final" val arrowDependencies = Seq( // https://mvnrepository.com/artifact/org.apache.arrow/flight-grpc "org.apache.arrow" % "flight-grpc" % arrowVersion, // https://mvnrepository.com/artifact/org.apache.arrow/flight-core "org.apache.arrow" % "flight-core" % arrowVersion ) libraryDependencies ++= arrowDependencies // Netty dependency overrides to ensure compatibility with Arrow // Arrow 14.0.1 requires Netty 4.1.96.Final for proper memory allocation // The chunkSize field issue occurs when Netty versions are mismatched dependencyOverrides ++= Seq( "io.netty" % "netty-all" % nettyVersion, "io.netty" % "netty-buffer" % nettyVersion, "io.netty" % "netty-codec" % nettyVersion, "io.netty" % "netty-codec-http" % nettyVersion, "io.netty" % "netty-codec-http2" % nettyVersion, "io.netty" % "netty-codec-socks" % nettyVersion, "io.netty" % "netty-common" % nettyVersion, "io.netty" % "netty-handler" % nettyVersion, "io.netty" % "netty-handler-proxy" % nettyVersion, "io.netty" % "netty-resolver" % nettyVersion, "io.netty" % "netty-transport" % nettyVersion, "io.netty" % "netty-transport-classes-epoll" % nettyVersion, "io.netty" % "netty-transport-native-epoll" % nettyVersion, "io.netty" % "netty-transport-native-unix-common" % nettyVersion ) ///////////////////////////////////////////////////////////////////////////// // Iceberg-related Dependencies ///////////////////////////////////////////////////////////////////////////// val excludeJersey = ExclusionRule(organization = "com.sun.jersey") val excludeGlassfishJersey = ExclusionRule(organization = "org.glassfish.jersey") val excludeSlf4j = ExclusionRule(organization = "org.slf4j") val excludeJetty = ExclusionRule(organization = "org.eclipse.jetty") val excludeJsp = ExclusionRule(organization = "javax.servlet.jsp") val excludeXmlBind = ExclusionRule(organization = "javax.xml.bind") val excludeJackson = ExclusionRule(organization = "com.fasterxml.jackson.core") val excludeJacksonModule = ExclusionRule(organization = "com.fasterxml.jackson.module") libraryDependencies ++= Seq( "org.apache.iceberg" % "iceberg-api" % "1.7.1", "org.apache.iceberg" % "iceberg-parquet" % "1.7.1" excludeAll( excludeJackson, excludeJacksonModule ), "org.apache.iceberg" % "iceberg-core" % "1.7.1" excludeAll( excludeJackson, excludeJacksonModule ), "org.apache.iceberg" % "iceberg-data" % "1.7.1" excludeAll( excludeJackson, excludeJacksonModule ), "org.apache.iceberg" % "iceberg-aws" % "1.7.1" excludeAll( excludeJackson, excludeJacksonModule ), "org.apache.hadoop" % "hadoop-common" % "3.3.1" excludeAll( excludeXmlBind, excludeGlassfishJersey, excludeJersey, excludeSlf4j, excludeJetty, excludeJsp, excludeJackson, excludeJacksonModule ), "org.apache.hadoop" % "hadoop-mapreduce-client-core" % "3.3.1" excludeAll( excludeXmlBind, excludeGlassfishJersey, excludeJersey, excludeSlf4j, excludeJetty, excludeJsp, excludeJackson, excludeJacksonModule ), "org.postgresql" % "postgresql" % "42.7.10" ) ///////////////////////////////////////////////////////////////////////////// // Additional Dependencies ///////////////////////////////////////////////////////////////////////////// libraryDependencies ++= Seq( "com.github.sisyphsu" % "dateparser" % "1.0.11", // DateParser "com.google.guava" % "guava" % "31.1-jre", // Guava "org.ehcache" % "sizeof" % "0.4.3", // Ehcache SizeOf "org.jgrapht" % "jgrapht-core" % "1.4.0", // JGraphT Core "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5", // Scala Logging "org.eclipse.jgit" % "org.eclipse.jgit" % "5.13.0.202109080827-r", // jgit "org.apache.commons" % "commons-vfs2" % "2.9.0", // for FileResolver throw VFS-related exceptions "io.lakefs" % "sdk" % "1.51.0", // for lakeFS api calls "com.typesafe" % "config" % "1.4.6", // config reader "org.apache.commons" % "commons-jcs3-core" % "3.2", // Apache Commons JCS "software.amazon.awssdk" % "s3" % "2.29.51" excludeAll( ExclusionRule(organization = "io.netty") ), "software.amazon.awssdk" % "auth" % "2.29.51" excludeAll( ExclusionRule(organization = "io.netty") ), "software.amazon.awssdk" % "regions" % "2.29.51" excludeAll( ExclusionRule(organization = "io.netty") ), "software.amazon.awssdk" % "sts" % "2.29.51" excludeAll( ExclusionRule(organization = "io.netty") ), ) ================================================ FILE: common/workflow-core/src/main/protobuf/org/apache/texera/amber/core/executor.proto ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. syntax = "proto3"; package org.apache.texera.amber.core; import "org/apache/texera/amber/core/virtualidentity.proto"; import "scalapb/scalapb.proto"; option (scalapb.options) = { scope: FILE, preserve_unknown_fields: false no_default_values_in_constructor: false }; message OpExecWithCode { string code = 1; string language = 2; } message OpExecWithClassName { string className = 1; string descString = 2; } message OpExecSource { string storageKey = 1; WorkflowIdentity workflowIdentity = 2 [(scalapb.field).no_box = true]; } message OpExecInitInfo { oneof sealed_value { OpExecWithClassName opExecWithClassName = 1; OpExecWithCode opExecWithCode = 2; OpExecSource opExecSource = 3; } } ================================================ FILE: common/workflow-core/src/main/protobuf/org/apache/texera/amber/core/virtualidentity.proto ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. syntax = "proto3"; package org.apache.texera.amber.core; import "scalapb/scalapb.proto"; option (scalapb.options) = { scope: FILE, preserve_unknown_fields: false no_default_values_in_constructor: true }; message WorkflowIdentity { int64 id = 1; } message ExecutionIdentity { int64 id = 1; } message ActorVirtualIdentity { string name = 1; } message ChannelIdentity { ActorVirtualIdentity fromWorkerId = 1 [(scalapb.field).no_box = true]; ActorVirtualIdentity toWorkerId = 2 [(scalapb.field).no_box = true]; bool isControl = 3; } message OperatorIdentity { string id = 1; } message PhysicalOpIdentity{ OperatorIdentity logicalOpId = 1 [(scalapb.field).no_box = true]; string layerName = 2; } message EmbeddedControlMessageIdentity{ string id = 1; } ================================================ FILE: common/workflow-core/src/main/protobuf/org/apache/texera/amber/core/workflow.proto ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. syntax = "proto3"; package org.apache.texera.amber.core; import "org/apache/texera/amber/core/virtualidentity.proto"; import "scalapb/scalapb.proto"; option (scalapb.options) = { scope: FILE, preserve_unknown_fields: false, no_default_values_in_constructor: false }; message PortIdentity { int32 id = 1; bool internal = 2; } message GlobalPortIdentity{ PhysicalOpIdentity opId = 1 [(scalapb.field).no_box = true]; PortIdentity portId = 2 [(scalapb.field).no_box = true]; bool input = 3; } message InputPort { PortIdentity id = 1 [(scalapb.field).no_box = true]; string displayName = 2; bool disallowMultiLinks = 3; repeated PortIdentity dependencies = 4; } message OutputPort { enum OutputMode { // outputs complete result set snapshot for each update SET_SNAPSHOT = 0; // outputs incremental result set delta for each update SET_DELTA = 1; // outputs a single snapshot for the entire execution, // used explicitly to support visualization operators that may exceed the memory limit // TODO: remove this mode after we have a better solution for output size limit SINGLE_SNAPSHOT = 2; } PortIdentity id = 1 [(scalapb.field).no_box = true]; string displayName = 2; bool blocking = 3; OutputMode mode = 4; } message PhysicalLink { PhysicalOpIdentity fromOpId = 1 [(scalapb.field).no_box = true]; PortIdentity fromPortId = 2 [(scalapb.field).no_box = true]; PhysicalOpIdentity toOpId = 3 [(scalapb.field).no_box = true]; PortIdentity toPortId = 4 [(scalapb.field).no_box = true]; } ================================================ FILE: common/workflow-core/src/main/protobuf/org/apache/texera/amber/core/workflowruntimestate.proto ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. syntax = "proto3"; package org.apache.texera.amber.core; import "google/protobuf/timestamp.proto"; import "scalapb/scalapb.proto"; option (scalapb.options) = { scope: FILE, preserve_unknown_fields: false no_default_values_in_constructor: false }; enum FatalErrorType{ COMPILATION_ERROR = 0; EXECUTION_FAILURE = 1; } message WorkflowFatalError { FatalErrorType type = 1; google.protobuf.Timestamp timestamp = 2 [(scalapb.field).no_box = true]; string message = 3; string details = 4; string operatorId = 5; string workerId = 6; } ================================================ FILE: common/workflow-core/src/main/protobuf/scalapb/scalapb.proto ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. syntax = "proto2"; package scalapb; option java_package = "scalapb.options"; option (options) = { package_name: "scalapb.options" flat_package: true }; import "google/protobuf/descriptor.proto"; message ScalaPbOptions { // If set then it overrides the java_package and package. optional string package_name = 1; // If true, the compiler does not append the proto base file name // into the generated package name. If false (the default), the // generated scala package name is the package_name.basename where // basename is the proto file name without the .proto extension. optional bool flat_package = 2; // Adds the following imports at the top of the file (this is meant // to provide implicit TypeMappers) repeated string import = 3; // Text to add to the generated scala file. This can be used only // when single_file is true. repeated string preamble = 4; // If true, all messages and enums (but not services) will be written // to a single Scala file. optional bool single_file = 5; // By default, wrappers defined at // https://github.com/google/protobuf/blob/master/src/google/protobuf/wrappers.proto, // are mapped to an Option[T] where T is a primitive type. When this field // is set to true, we do not perform this transformation. optional bool no_primitive_wrappers = 7; // DEPRECATED. In ScalaPB <= 0.5.47, it was necessary to explicitly enable // primitive_wrappers. This field remains here for backwards compatibility, // but it has no effect on generated code. It is an error to set both // `primitive_wrappers` and `no_primitive_wrappers`. optional bool primitive_wrappers = 6; // Scala type to be used for repeated fields. If unspecified, // `scala.collection.Seq` will be used. optional string collection_type = 8; // If set to true, all generated messages in this file will preserve unknown // fields. optional bool preserve_unknown_fields = 9 [default = true]; // If defined, sets the name of the file-level object that would be generated. This // object extends `GeneratedFileObject` and contains descriptors, and list of message // and enum companions. optional string object_name = 10; // Whether to apply the options only to this file, or for the entire package (and its subpackages) enum OptionsScope { // Apply the options for this file only (default) FILE = 0; // Apply the options for the entire package and its subpackages. PACKAGE = 1; } // Experimental: scope to apply the given options. optional OptionsScope scope = 11; // If true, lenses will be generated. optional bool lenses = 12 [default = true]; // If true, then source-code info information will be included in the // generated code - normally the source code info is cleared out to reduce // code size. The source code info is useful for extracting source code // location from the descriptors as well as comments. optional bool retain_source_code_info = 13; // Scala type to be used for maps. If unspecified, // `scala.collection.immutable.Map` will be used. optional string map_type = 14; // If true, no default values will be generated in message constructors. optional bool no_default_values_in_constructor = 15; /* Naming convention for generated enum values */ enum EnumValueNaming { AS_IN_PROTO = 0; // Enum value names in Scala use the same name as in the proto CAMEL_CASE = 1; // Convert enum values to CamelCase in Scala. } optional EnumValueNaming enum_value_naming = 16; // Indicate if prefix (enum name + optional underscore) should be removed in scala code // Strip is applied before enum value naming changes. optional bool enum_strip_prefix = 17 [default = false]; // Scala type to use for bytes fields. optional string bytes_type = 21; // Enable java conversions for this file. optional bool java_conversions = 23; // AuxMessageOptions enables you to set message-level options through package-scoped options. // This is useful when you can't add a dependency on scalapb.proto from the proto file that // defines the message. message AuxMessageOptions { // The fully-qualified name of the message in the proto name space. optional string target = 1; // Options to apply to the message. If there are any options defined on the target message // they take precedence over the options. optional MessageOptions options = 2; } // AuxFieldOptions enables you to set field-level options through package-scoped options. // This is useful when you can't add a dependency on scalapb.proto from the proto file that // defines the field. message AuxFieldOptions { // The fully-qualified name of the field in the proto name space. optional string target = 1; // Options to apply to the field. If there are any options defined on the target message // they take precedence over the options. optional FieldOptions options = 2; } // AuxEnumOptions enables you to set enum-level options through package-scoped options. // This is useful when you can't add a dependency on scalapb.proto from the proto file that // defines the enum. message AuxEnumOptions { // The fully-qualified name of the enum in the proto name space. optional string target = 1; // Options to apply to the enum. If there are any options defined on the target enum // they take precedence over the options. optional EnumOptions options = 2; } // AuxEnumValueOptions enables you to set enum value level options through package-scoped // options. This is useful when you can't add a dependency on scalapb.proto from the proto // file that defines the enum. message AuxEnumValueOptions { // The fully-qualified name of the enum value in the proto name space. optional string target = 1; // Options to apply to the enum value. If there are any options defined on // the target enum value they take precedence over the options. optional EnumValueOptions options = 2; } // List of message options to apply to some messages. repeated AuxMessageOptions aux_message_options = 18; // List of message options to apply to some fields. repeated AuxFieldOptions aux_field_options = 19; // List of message options to apply to some enums. repeated AuxEnumOptions aux_enum_options = 20; // List of enum value options to apply to some enum values. repeated AuxEnumValueOptions aux_enum_value_options = 22; // List of preprocessors to apply. repeated string preprocessors = 24; repeated FieldTransformation field_transformations = 25; // Ignores all transformations for this file. This is meant to allow specific files to // opt out from transformations inherited through package-scoped options. optional bool ignore_all_transformations = 26; // If true, getters will be generated. optional bool getters = 27 [default = true]; // For use in tests only. Inhibit Java conversions even when when generator parameters // request for it. optional bool test_only_no_java_conversions = 999; extensions 1000 to max; } extend google.protobuf.FileOptions { // File-level optionals for ScalaPB. // Extension number officially assigned by protobuf-global-extension-registry@google.com optional ScalaPbOptions options = 1020; } message MessageOptions { // Additional classes and traits to mix in to the case class. repeated string extends = 1; // Additional classes and traits to mix in to the companion object. repeated string companion_extends = 2; // Custom annotations to add to the generated case class. repeated string annotations = 3; // All instances of this message will be converted to this type. An implicit TypeMapper // must be present. optional string type = 4; // Custom annotations to add to the companion object of the generated class. repeated string companion_annotations = 5; // Additional classes and traits to mix in to generated sealed_oneof base trait. repeated string sealed_oneof_extends = 6; // If true, when this message is used as an optional field, do not wrap it in an `Option`. // This is equivalent of setting `(field).no_box` to true on each field with the message type. optional bool no_box = 7; // Custom annotations to add to the generated `unknownFields` case class field. repeated string unknown_fields_annotations = 8; extensions 1000 to max; } extend google.protobuf.MessageOptions { // Message-level optionals for ScalaPB. // Extension number officially assigned by protobuf-global-extension-registry@google.com optional MessageOptions message = 1020; } // Represents a custom Collection type in Scala. This allows ScalaPB to integrate with // collection types that are different enough from the ones in the standard library. message Collection { // Type of the collection optional string type = 1; // Set to true if this collection type is not allowed to be empty, for example // cats.data.NonEmptyList. When true, ScalaPB will not generate `clearX` for the repeated // field and not provide a default argument in the constructor. optional bool non_empty = 2; // An Adapter is a Scala object available at runtime that provides certain static methods // that can operate on this collection type. optional string adapter = 3; } message FieldOptions { optional string type = 1; optional string scala_name = 2; // Can be specified only if this field is repeated. If unspecified, // it falls back to the file option named `collection_type`, which defaults // to `scala.collection.Seq`. optional string collection_type = 3; optional Collection collection = 8; // If the field is a map, you can specify custom Scala types for the key // or value. optional string key_type = 4; optional string value_type = 5; // Custom annotations to add to the field. repeated string annotations = 6; // Can be specified only if this field is a map. If unspecified, // it falls back to the file option named `map_type` which defaults to // `scala.collection.immutable.Map` optional string map_type = 7; // Do not box this value in Option[T]. If set, this overrides MessageOptions.no_box optional bool no_box = 30; // Like no_box it does not box a value in Option[T], but also fails parsing when a value // is not provided. This enables to emulate required fields in proto3. optional bool required = 31; extensions 1000 to max; } extend google.protobuf.FieldOptions { // Field-level optionals for ScalaPB. // Extension number officially assigned by protobuf-global-extension-registry@google.com optional FieldOptions field = 1020; } message EnumOptions { // Additional classes and traits to mix in to the base trait repeated string extends = 1; // Additional classes and traits to mix in to the companion object. repeated string companion_extends = 2; // All instances of this enum will be converted to this type. An implicit TypeMapper // must be present. optional string type = 3; // Custom annotations to add to the generated enum's base class. repeated string base_annotations = 4; // Custom annotations to add to the generated trait. repeated string recognized_annotations = 5; // Custom annotations to add to the generated Unrecognized case class. repeated string unrecognized_annotations = 6; extensions 1000 to max; } extend google.protobuf.EnumOptions { // Enum-level optionals for ScalaPB. // Extension number officially assigned by protobuf-global-extension-registry@google.com // // The field is called enum_options and not enum since enum is not allowed in Java. optional EnumOptions enum_options = 1020; } message EnumValueOptions { // Additional classes and traits to mix in to an individual enum value. repeated string extends = 1; // Name in Scala to use for this enum value. optional string scala_name = 2; // Custom annotations to add to the generated case object for this enum value. repeated string annotations = 3; extensions 1000 to max; } extend google.protobuf.EnumValueOptions { // Enum-level optionals for ScalaPB. // Extension number officially assigned by protobuf-global-extension-registry@google.com optional EnumValueOptions enum_value = 1020; } message OneofOptions { // Additional traits to mix in to a oneof. repeated string extends = 1; // Name in Scala to use for this oneof field. optional string scala_name = 2; extensions 1000 to max; } extend google.protobuf.OneofOptions { // Enum-level optionals for ScalaPB. // Extension number officially assigned by protobuf-global-extension-registry@google.com optional OneofOptions oneof = 1020; } enum MatchType { CONTAINS = 0; EXACT = 1; PRESENCE = 2; } message FieldTransformation { optional google.protobuf.FieldDescriptorProto when = 1; optional MatchType match_type = 2 [default = CONTAINS]; optional google.protobuf.FieldOptions set = 3; } message PreprocessorOutput { map options_by_file = 1; } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/WorkflowRuntimeException.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core import org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity class WorkflowRuntimeException( val message: String, val relatedWorkerId: Option[ActorVirtualIdentity] = None ) extends RuntimeException(message) with Serializable { def this(message: String, cause: Throwable, relatedWorkerId: Option[ActorVirtualIdentity]) = { this(message, relatedWorkerId) initCause(cause) } def this(cause: Throwable, relatedWorkerId: Option[ActorVirtualIdentity]) = { this(Option(cause).map(_.toString).orNull, cause, relatedWorkerId) } def this(cause: Throwable) = { this(Option(cause).map(_.toString).orNull, cause, None) } def this() = { this(null: String) } override def toString: String = message } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/executor/ExecFactory.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.executor object ExecFactory { def newExecFromJavaCode(code: String): OperatorExecutor = { JavaRuntimeCompilation .compileCode(code) .getDeclaredConstructor() .newInstance() .asInstanceOf[OperatorExecutor] } def newExecFromJavaClassName[K]( className: String, descString: String = "", idx: Int = 0, workerCount: Int = 1 ): OperatorExecutor = { val clazz = Class.forName(className).asInstanceOf[Class[K]] try { if (descString.isEmpty) { clazz.getDeclaredConstructor().newInstance().asInstanceOf[OperatorExecutor] } else { clazz .getDeclaredConstructor(classOf[String]) .newInstance(descString) .asInstanceOf[OperatorExecutor] } } catch { case e: NoSuchMethodException => if (descString.isEmpty) { clazz .getDeclaredConstructor(classOf[Int], classOf[Int]) .newInstance(idx, workerCount) .asInstanceOf[OperatorExecutor] } else { clazz .getDeclaredConstructor(classOf[String], classOf[Int], classOf[Int]) .newInstance(descString, idx, workerCount) .asInstanceOf[OperatorExecutor] } } } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/executor/JavaRuntimeCompilation.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.executor import java.io.ByteArrayOutputStream import java.net.URI import java.util import javax.tools._ object JavaRuntimeCompilation { val compiler: JavaCompiler = ToolProvider.getSystemJavaCompiler def compileCode(code: String): Class[_] = { val packageName = "org.apache.texera.amber.operators.udf.java" //to hide it from user we will append the package in the udf code. val codeToCompile = s"package $packageName;\n$code" val defaultClassName = s"$packageName.JavaUDFOpExec" val fileManager: CustomJavaFileManager = new CustomJavaFileManager( compiler.getStandardFileManager(null, null, null) ) // Diagnostic collector is to capture compilation diagnostics (errors, warnings, etc.) val diagnosticCollector = new DiagnosticCollector[JavaFileObject] /* Compiles the provided source code using the Java Compiler API, utilizing a custom file manager, Collecting compilation diagnostics, and storing the result in 'compilationResult'. */ val compilationResult = compiler .getTask( null, fileManager, diagnosticCollector, null, null, util.Arrays.asList(new StringJavaFileObject(defaultClassName, codeToCompile)) ) .call() // Checking if compilation was successful if (!compilationResult) { // Getting the compilation diagnostics (errors and warnings) val diagnostics = diagnosticCollector.getDiagnostics val errorMessageBuilder = new StringBuilder() diagnostics.forEach { diagnostic => errorMessageBuilder.append( s"Error at line ${diagnostic.getLineNumber}: ${diagnostic.getMessage(null)}\n" ) } throw new RuntimeException(errorMessageBuilder.toString()) } new CustomClassLoader().loadClass(defaultClassName, fileManager.getCompiledBytes) } private class CustomJavaFileManager(fileManager: JavaFileManager) extends ForwardingJavaFileManager[JavaFileManager](fileManager) { private val outputBuffer: ByteArrayOutputStream = new ByteArrayOutputStream() def getCompiledBytes: Array[Byte] = outputBuffer.toByteArray override def getJavaFileForOutput( location: JavaFileManager.Location, className: String, kind: JavaFileObject.Kind, sibling: FileObject ): JavaFileObject = { new SimpleJavaFileObject(URI.create(s"string:///$className${kind.extension}"), kind) { override def openOutputStream(): ByteArrayOutputStream = outputBuffer } } } private class StringJavaFileObject(className: String, code: String) extends SimpleJavaFileObject( URI.create( "string:///" + className.replace('.', '/') + JavaFileObject.Kind.SOURCE.extension ), JavaFileObject.Kind.SOURCE ) { override def getCharContent(ignoreEncodingErrors: Boolean): CharSequence = code } private class CustomClassLoader extends ClassLoader { def loadClass(name: String, classBytes: Array[Byte]): Class[_] = defineClass(name, classBytes, 0, classBytes.length) } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/executor/OperatorExecutor.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.executor import org.apache.texera.amber.core.state.State import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} import org.apache.texera.amber.core.workflow.PortIdentity trait OperatorExecutor { def open(): Unit = {} def produceStateOnStart(port: Int): Option[State] = None def processState(state: State, port: Int): Option[State] = Some(state) def processTupleMultiPort( tuple: Tuple, port: Int ): Iterator[(TupleLike, Option[PortIdentity])] = { processTuple(tuple, port).map(t => (t, None)) } def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] def produceStateOnFinish(port: Int): Option[State] = None def onFinishMultiPort(port: Int): Iterator[(TupleLike, Option[PortIdentity])] = { onFinish(port).map(t => (t, None)) } def onFinish(port: Int): Iterator[TupleLike] = Iterator.empty def close(): Unit = {} } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/executor/SourceOperatorExecutor.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.executor import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} import org.apache.texera.amber.core.workflow.PortIdentity trait SourceOperatorExecutor extends OperatorExecutor { override def open(): Unit = {} override def close(): Unit = {} override def processTupleMultiPort( tuple: Tuple, port: Int ): Iterator[(TupleLike, Option[PortIdentity])] = Iterator() override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = Iterator.empty def produceTuple(): Iterator[TupleLike] override def onFinishMultiPort(port: Int): Iterator[(TupleLike, Option[PortIdentity])] = { // We assume there is only one input port for source operators. The current assumption // makes produceTuple to be invoked on each input port finish. // We should move this to onFinishAllPorts later. produceTuple().map(t => (t, Option.empty)) } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/state/State.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.state import com.fasterxml.jackson.databind.JsonNode import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple} import org.apache.texera.amber.util.JSONUtils.objectMapper import java.util.Base64 import scala.jdk.CollectionConverters.IteratorHasAsScala final case class State(values: Map[String, Any]) { def toJson: String = objectMapper.writeValueAsString(State.toJsonValue(values)) def toTuple: Tuple = Tuple.builder(State.schema).addSequentially(Array(toJson)).build() } object State { private val Content = "content" private val BytesTypeMarker = "__texera_type__" private val BytesValue = "bytes" private val PayloadMarker = "payload" val schema: Schema = new Schema( new Attribute(Content, AttributeType.STRING) ) def fromJson(payload: String): State = State( objectMapper .readTree(payload) .fields() .asScala .map(entry => entry.getKey -> fromJsonValue(entry.getValue)) .toMap ) def fromTuple(row: Tuple): State = fromJson(row.getField[String](Content)) private def toJsonValue(value: Any): Any = value match { case null => null case bytes: Array[Byte] => Map(BytesTypeMarker -> BytesValue, PayloadMarker -> Base64.getEncoder.encodeToString(bytes)) case map: Map[?, ?] => map.iterator.map { case (k, v) => k -> toJsonValue(v) }.toMap case iterable: Iterable[_] => iterable.map(toJsonValue).toList case other => other } private def fromJsonValue(node: JsonNode): Any = { if (node == null || node.isNull) { null } else if (node.isObject) { val fields = node.fields().asScala.map(entry => entry.getKey -> entry.getValue).toMap fields.get(BytesTypeMarker) match { case Some(typeNode) if typeNode.isTextual && typeNode.asText() == BytesValue => Base64.getDecoder.decode(fields(PayloadMarker).asText()) case _ => fields.view.mapValues(fromJsonValue).toMap } } else if (node.isArray) { node.elements().asScala.map(fromJsonValue).toList } else if (node.isBoolean) { node.asBoolean() } else if (node.isIntegralNumber) { node.longValue() } else if (node.isFloatingPointNumber) { node.doubleValue() } else { node.asText() } } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/DocumentFactory.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage import org.apache.texera.amber.config.StorageConfig import org.apache.texera.amber.core.storage.FileResolver.DATASET_FILE_URI_SCHEME import org.apache.texera.amber.core.storage.VFSResourceType._ import org.apache.texera.amber.core.storage.VFSURIFactory.{VFS_FILE_URI_SCHEME, decodeURI} import org.apache.texera.amber.core.storage.model._ import org.apache.texera.amber.core.storage.result.iceberg.IcebergDocument import org.apache.texera.amber.core.tuple.{Schema, Tuple} import org.apache.texera.amber.util.IcebergUtil import org.apache.iceberg.data.Record import org.apache.iceberg.{Schema => IcebergSchema} import java.net.URI object DocumentFactory { val ICEBERG = "iceberg" private def sanitizeURIPath(uri: URI): String = uri.getPath.stripPrefix("/").replace("/", "_") /** * Open a document specified by the uri for read purposes only. * @param fileUri the uri of the document * @return ReadonlyVirtualDocument */ def openReadonlyDocument(fileUri: URI): ReadonlyVirtualDocument[_] = { fileUri.getScheme match { case DATASET_FILE_URI_SCHEME => new DatasetFileDocument(fileUri) case "file" => new ReadonlyLocalFileDocument(fileUri) case unsupportedScheme => throw new UnsupportedOperationException( s"Unsupported URI scheme: $unsupportedScheme for creating the ReadonlyDocument" ) } } /** * Create a document for storage specified by the uri. * This document is suitable for storing structural data, i.e. the schema is required to create such document. * @param uri the location of the document * @param schema the schema of the data stored in the document * @return the created document */ def createDocument(uri: URI, schema: Schema): VirtualDocument[_] = { uri.getScheme match { case VFS_FILE_URI_SCHEME => val (_, _, _, resourceType) = decodeURI(uri) val storageKey = sanitizeURIPath(uri) val namespace = resourceType match { case RESULT => StorageConfig.icebergTableResultNamespace case CONSOLE_MESSAGES => StorageConfig.icebergTableConsoleMessagesNamespace case RUNTIME_STATISTICS => StorageConfig.icebergTableRuntimeStatisticsNamespace case _ => throw new IllegalArgumentException(s"Resource type $resourceType is not supported") } val icebergSchema = IcebergUtil.toIcebergSchema(schema) IcebergUtil.createTable( IcebergCatalogInstance.getInstance(), namespace, storageKey, icebergSchema, overrideIfExists = true ) val serde: (IcebergSchema, Tuple) => Record = IcebergUtil.toGenericRecord val deserde: (IcebergSchema, Record) => Tuple = (schema, record) => IcebergUtil.fromRecord(record, IcebergUtil.fromIcebergSchema(schema)) new IcebergDocument[Tuple]( namespace, storageKey, icebergSchema, serde, deserde ) case unsupportedScheme => throw new UnsupportedOperationException( s"Unsupported URI scheme: $unsupportedScheme for creating the document" ) } } /** * Open a document specified by the uri. * If the document is storing structural data, the schema will also be returned * @param uri the uri of the document * @return the VirtualDocument, which is the handler of the data; the Schema, which is the schema of the data stored in the document */ def openDocument(uri: URI): (VirtualDocument[_], Option[Schema]) = { uri.getScheme match { case DATASET_FILE_URI_SCHEME => (new DatasetFileDocument(uri), None) case VFS_FILE_URI_SCHEME => val (_, _, _, resourceType) = decodeURI(uri) val storageKey = sanitizeURIPath(uri) val namespace = resourceType match { case RESULT => StorageConfig.icebergTableResultNamespace case CONSOLE_MESSAGES => StorageConfig.icebergTableConsoleMessagesNamespace case RUNTIME_STATISTICS => StorageConfig.icebergTableRuntimeStatisticsNamespace case _ => throw new IllegalArgumentException(s"Resource type $resourceType is not supported") } val table = IcebergUtil .loadTableMetadata( IcebergCatalogInstance.getInstance(), namespace, storageKey ) .getOrElse( throw new IllegalArgumentException("No storage is found for the given URI") ) val amberSchema = IcebergUtil.fromIcebergSchema(table.schema()) val serde: (IcebergSchema, Tuple) => Record = IcebergUtil.toGenericRecord val deserde: (IcebergSchema, Record) => Tuple = (schema, record) => IcebergUtil.fromRecord(record, IcebergUtil.fromIcebergSchema(schema)) ( new IcebergDocument[Tuple]( namespace, storageKey, table.schema(), serde, deserde ), Some(amberSchema) ) case unsupportedScheme => throw new UnsupportedOperationException( s"Unsupported URI scheme: $unsupportedScheme for opening the document" ) } } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/FileResolver.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage import org.apache.commons.vfs2.FileNotFoundException import org.apache.texera.dao.SqlServer import org.apache.texera.dao.SqlServer.withTransaction import org.apache.texera.dao.jooq.generated.tables.Dataset.DATASET import org.apache.texera.dao.jooq.generated.tables.DatasetVersion.DATASET_VERSION import org.apache.texera.dao.jooq.generated.tables.User.USER import org.apache.texera.dao.jooq.generated.tables.pojos.{Dataset, DatasetVersion} import java.net.{URI, URLEncoder} import java.nio.charset.StandardCharsets import java.nio.file.{Files, Paths} import scala.jdk.CollectionConverters.IteratorHasAsScala import scala.util.{Success, Try} /** * Unified object for resolving both VFS resources and local/dataset files. */ object FileResolver { val DATASET_FILE_URI_SCHEME = "dataset" /** * Resolves a given fileName to either a file on the local file system or a dataset file. * * @param fileName the name of the file to resolve. * @throws java.io.FileNotFoundException if the file cannot be resolved. * @return A URI pointing to the resolved file. */ def resolve(fileName: String): URI = { if (isFileResolved(fileName)) { return new URI(fileName) } val resolvers: Seq[String => URI] = Seq(localResolveFunc, datasetResolveFunc) // Try each resolver function in sequence resolvers .map(resolver => Try(resolver(fileName))) .collectFirst { case Success(output) => output } .getOrElse(throw new FileNotFoundException(fileName)) } /** * Attempts to resolve a local file path. * @throws java.io.FileNotFoundException if the local file does not exist * @param fileName the name of the file to check */ private def localResolveFunc(fileName: String): URI = { val filePath = Paths.get(fileName) if (!Files.exists(filePath)) { throw new FileNotFoundException(s"Local file $fileName does not exist") } filePath.toUri } /** * Parses a dataset file path and extracts its components. * Expected format: /ownerEmail/datasetName/versionName/fileRelativePath * * @param fileName The file path to parse * @return Some((ownerEmail, datasetName, versionName, fileRelativePath)) if valid, None otherwise */ private def parseDatasetFilePath( fileName: String ): Option[(String, String, String, Array[String])] = { val filePath = Paths.get(fileName) val pathSegments = (0 until filePath.getNameCount).map(filePath.getName(_).toString).toArray if (pathSegments.length < 4) { return None } val ownerEmail = pathSegments(0) val datasetName = pathSegments(1) val versionName = pathSegments(2) val fileRelativePathSegments = pathSegments.drop(3) Some((ownerEmail, datasetName, versionName, fileRelativePathSegments)) } /** * Attempts to resolve a given fileName to a URI. * * The fileName format should be: /ownerEmail/datasetName/versionName/fileRelativePath * e.g. /bob@texera.com/twitterDataset/v1/california/irvine/tw1.csv * The output dataset URI format is: {DATASET_FILE_URI_SCHEME}:///{repositoryName}/{versionHash}/fileRelativePath * e.g. {DATASET_FILE_URI_SCHEME}:///dataset-15/adeq233td/some/dir/file.txt * * @param fileName the name of the file to attempt resolving as a DatasetFileDocument * @return Either[String, DatasetFileDocument] - Right(document) if creation succeeds * @throws java.io.FileNotFoundException if the dataset file does not exist or cannot be created */ private def datasetResolveFunc(fileName: String): URI = { val (ownerEmail, datasetName, versionName, fileRelativePathSegments) = parseDatasetFilePath(fileName).getOrElse( throw new FileNotFoundException(s"Dataset file $fileName not found.") ) val fileRelativePath = Paths.get(fileRelativePathSegments.head, fileRelativePathSegments.tail: _*) // fetch the dataset and version from DB to get dataset ID and version hash val (dataset, datasetVersion) = withTransaction( SqlServer .getInstance() .createDSLContext() ) { ctx => // fetch the dataset from DB val dataset = ctx .select(DATASET.fields: _*) .from(DATASET) .leftJoin(USER) .on(USER.UID.eq(DATASET.OWNER_UID)) .where(USER.EMAIL.eq(ownerEmail)) .and(DATASET.NAME.eq(datasetName)) .fetchOneInto(classOf[Dataset]) // fetch the dataset version from DB val datasetVersion = ctx .selectFrom(DATASET_VERSION) .where(DATASET_VERSION.DID.eq(dataset.getDid)) .and(DATASET_VERSION.NAME.eq(versionName)) .fetchOneInto(classOf[DatasetVersion]) if (dataset == null || datasetVersion == null) { throw new FileNotFoundException(s"Dataset file $fileName not found.") } (dataset, datasetVersion) } // Convert each segment of fileRelativePath to an encoded String val encodedFileRelativePath = fileRelativePath .iterator() .asScala .map { segment => URLEncoder.encode(segment.toString, StandardCharsets.UTF_8) } .toArray // Prepend dataset name and versionHash to the encoded path segments val allPathSegments = Array( dataset.getRepositoryName, datasetVersion.getVersionHash ) ++ encodedFileRelativePath // Build the format /{repositoryName}/{versionHash}/{fileRelativePath}, both Linux and Windows use forward slash as the splitter val uriSplitter = "/" val encodedPath = uriSplitter + allPathSegments.mkString(uriSplitter) try { new URI(DATASET_FILE_URI_SCHEME, "", encodedPath, null) } catch { case e: Exception => throw new FileNotFoundException(s"Dataset file $fileName not found.") } } /** * Checks if a given file path has a valid scheme. * * @param filePath The file path to check. * @return `true` if the file path contains a valid scheme, `false` otherwise. */ def isFileResolved(filePath: String): Boolean = { try { val uri = new URI(filePath) uri.getScheme != null && uri.getScheme.nonEmpty } catch { case _: Exception => false // Invalid URI format } } /** * Parses a dataset file path to extract owner email and dataset name. * Expected format: /ownerEmail/datasetName/versionName/fileRelativePath * * @param path The file path from operator properties * @return Some((ownerEmail, datasetName)) if path is valid, None otherwise */ def parseDatasetOwnerAndName(path: String): Option[(String, String)] = { if (path == null) { return None } parseDatasetFilePath(path).map { case (ownerEmail, datasetName, _, _) => (ownerEmail, datasetName) } } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/IcebergCatalogInstance.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage import org.apache.texera.amber.config.StorageConfig import org.apache.texera.amber.util.IcebergUtil import org.apache.iceberg.catalog.Catalog /** * IcebergCatalogInstance is a singleton that manages the Iceberg catalog instance. * - Provides a single shared catalog for all Iceberg table-related operations in the Texera application. * - Lazily initializes the catalog on first access. * - Supports replacing the catalog instance primarily for testing or reconfiguration. */ object IcebergCatalogInstance { private var instance: Option[Catalog] = None /** * Retrieves the singleton Iceberg catalog instance. * - If the catalog is not initialized, it is lazily created using the configured properties. * * @return the Iceberg catalog instance. */ def getInstance(): Catalog = { instance match { case Some(catalog) => catalog case None => val catalog = StorageConfig.icebergCatalogType match { case "hadoop" => IcebergUtil.createHadoopCatalog( "texera_iceberg", StorageConfig.fileStorageDirectoryPath ) case "rest" => IcebergUtil.createRestCatalog( "texera_iceberg", StorageConfig.icebergRESTCatalogWarehouseName ) case "postgres" => IcebergUtil.createPostgresCatalog( "texera_iceberg", StorageConfig.fileStorageDirectoryPath ) case unsupported => throw new IllegalArgumentException(s"Unsupported catalog type: $unsupported") } instance = Some(catalog) catalog } } /** * Replaces the existing Iceberg catalog instance. * - This method is useful for testing or dynamically updating the catalog. * * @param catalog the new Iceberg catalog instance to replace the current one. */ def replaceInstance(catalog: Catalog): Unit = { instance = Some(catalog) } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/VFSURIFactory.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage import org.apache.texera.amber.core.virtualidentity.{ ExecutionIdentity, OperatorIdentity, WorkflowIdentity } import org.apache.texera.amber.core.workflow.GlobalPortIdentity import org.apache.texera.amber.util.serde.GlobalPortIdentitySerde import org.apache.texera.amber.util.serde.GlobalPortIdentitySerde.SerdeOps import java.net.URI object VFSResourceType extends Enumeration { val RESULT: Value = Value("result") val RUNTIME_STATISTICS: Value = Value("runtimeStatistics") val CONSOLE_MESSAGES: Value = Value("consoleMessages") } object VFSURIFactory { val VFS_FILE_URI_SCHEME = "vfs" /** * Parses a VFS URI and extracts its components * * @param uri The VFS URI to parse. * @return A `VFSUriComponents` object with the extracted data. * @throws java.lang.IllegalArgumentException if the URI is malformed. */ def decodeURI(uri: URI): ( WorkflowIdentity, ExecutionIdentity, Option[GlobalPortIdentity], VFSResourceType.Value ) = { if (uri.getScheme != VFS_FILE_URI_SCHEME) { throw new IllegalArgumentException(s"Invalid URI scheme: ${uri.getScheme}") } val segments = uri.getPath.stripPrefix("/").split("/").toList def extractValue(key: String): String = { val index = segments.indexOf(key) if (index == -1 || index + 1 >= segments.length) { throw new IllegalArgumentException(s"Missing value for key: $key in URI: $uri") } segments(index + 1) } val workflowId = WorkflowIdentity(extractValue("wid").toLong) val executionId = ExecutionIdentity(extractValue("eid").toLong) val globalPortIdOption = segments.indexOf("globalportid") match { case -1 => None case _ => Some(GlobalPortIdentitySerde.deserializeFromString(extractValue("globalportid"))) } val resourceTypeStr = segments.last.toLowerCase val resourceType = VFSResourceType.values .find(_.toString.toLowerCase == resourceTypeStr) .getOrElse(throw new IllegalArgumentException(s"Unknown resource type: $resourceTypeStr")) (workflowId, executionId, globalPortIdOption, resourceType) } /** * Create a URI pointing to a result storage */ def createResultURI( workflowId: WorkflowIdentity, executionId: ExecutionIdentity, globalPortId: GlobalPortIdentity ): URI = { val baseUri = s"$VFS_FILE_URI_SCHEME:///wid/${workflowId.id}/eid/${executionId.id}/globalportid/${globalPortId.serializeAsString}" new URI(s"$baseUri/${VFSResourceType.RESULT.toString.toLowerCase}") } /** * Create a URI pointing to runtime statistics */ def createRuntimeStatisticsURI( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): URI = { createNonResultVFSURI( VFSResourceType.RUNTIME_STATISTICS, workflowId, executionId ) } /** * Create a URI pointing to console messages */ def createConsoleMessagesURI( workflowId: WorkflowIdentity, executionId: ExecutionIdentity, operatorId: OperatorIdentity ): URI = { createNonResultVFSURI( VFSResourceType.CONSOLE_MESSAGES, workflowId, executionId, Some(operatorId) ) } /** * Internal helper to create URI pointing to a VFS resource for resource types other than `RESULT`. * The URI can be used by the DocumentFactory to create resource or open resource. * * @param resourceType The type of the VFS resource. * @param workflowId Workflow identifier. * @param executionId Execution identifier. * @param operatorId Operator identifier. * @return A VFS URI * @throws java.lang.IllegalArgumentException if `resourceType` is `RESULT`, if `operatorId` is provided for * `RUNTIME_STATISTICS`, or if `operatorId` is not provided for `CONSOLE_MESSAGES`. */ private def createNonResultVFSURI( resourceType: VFSResourceType.Value, workflowId: WorkflowIdentity, executionId: ExecutionIdentity, operatorId: Option[OperatorIdentity] = None ): URI = { if (resourceType == VFSResourceType.RESULT) { throw new IllegalArgumentException( "resourceType cannot be RESULT when using createOtherVFSURI." ) } if (resourceType == VFSResourceType.RUNTIME_STATISTICS && operatorId.isDefined) { throw new IllegalArgumentException( "Runtime statistics URI should not contain operatorId." ) } if (resourceType == VFSResourceType.CONSOLE_MESSAGES && operatorId.isEmpty) { throw new IllegalArgumentException( "Console messages URI should contain operatorId." ) } val baseUri = operatorId match { case Some(opId) => s"$VFS_FILE_URI_SCHEME:///wid/${workflowId.id}/eid/${executionId.id}/opid/${opId.id}" case None => s"$VFS_FILE_URI_SCHEME:///wid/${workflowId.id}/eid/${executionId.id}" } new URI(s"$baseUri/${resourceType.toString.toLowerCase}") } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/BufferedItemWriter.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage.model /** * BufferedItemWriter provides an interface for writing items to a buffer and performing I/O operations. * The items are buffered before being written to the underlying storage to optimize performance. * @tparam T the type of data items to be written. */ trait BufferedItemWriter[T] { /** * The size of the buffer. * @return the buffer size. */ val bufferSize: Int /** * Open the writer, initializing any necessary resources. * This method should be called before any write operations. */ def open(): Unit /** * Close the writer, flushing any remaining items in the buffer * to the underlying storage and releasing any held resources. */ def close(): Unit /** * Put one item into the buffer. If the buffer is full, it should be flushed to the underlying storage. * @param item the data item to be written. */ def putOne(item: T): Unit /** * Remove one item from the buffer. If the item is not found in the buffer, an appropriate action should be taken, * such as throwing an exception or ignoring the request. * @param item the data item to be removed. */ def removeOne(item: T): Unit } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/DatasetFileDocument.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage.model import com.typesafe.scalalogging.LazyLogging import org.apache.texera.amber.config.EnvironmentalVariable import org.apache.texera.amber.core.storage.model.DatasetFileDocument.{ fileServiceGetPresignURLEndpoint, userJwtToken } import org.apache.texera.amber.core.storage.util.LakeFSStorageClient import org.apache.texera.amber.core.storage.util.dataset.GitVersionControlLocalFileStorage import java.io.{File, FileOutputStream, InputStream} import java.net._ import java.nio.charset.StandardCharsets import java.nio.file.{Files, Path, Paths} import scala.jdk.CollectionConverters.IteratorHasAsScala object DatasetFileDocument { // Since requests need to be sent to the FileService in order to read the file, we store USER_JWT_TOKEN in the environment vars // This variable should be NON-EMPTY in the dynamic-computing-unit architecture, i.e. each user-created computing unit should store user's jwt token. // In the local development or other architectures, this token can be empty. lazy val userJwtToken: String = sys.env.getOrElse(EnvironmentalVariable.ENV_USER_JWT_TOKEN, "").trim // The endpoint of getting presigned url from the file service, also stored in the environment vars. lazy val fileServiceGetPresignURLEndpoint: String = sys.env .getOrElse( EnvironmentalVariable.ENV_FILE_SERVICE_GET_PRESIGNED_URL_ENDPOINT, "http://localhost:9092/api/dataset/presign-download" ) .trim } private[storage] class DatasetFileDocument(uri: URI) extends VirtualDocument[Nothing] with OnDataset with LazyLogging { // Utility function to parse and decode URI segments into individual components private def parseUri(uri: URI): (String, String, Path) = { val segments = Paths.get(uri.getPath).iterator().asScala.map(_.toString).toArray if (segments.length < 3) throw new IllegalArgumentException("URI format is incorrect") // parse uri to dataset components val repositoryName = segments(0) val datasetVersionHash = URLDecoder.decode(segments(1), StandardCharsets.UTF_8) val decodedRelativeSegments = segments.drop(2).map(part => URLDecoder.decode(part, StandardCharsets.UTF_8)) val fileRelativePath = Paths.get(decodedRelativeSegments.head, decodedRelativeSegments.tail: _*) (repositoryName, datasetVersionHash, fileRelativePath) } // Extract components from URI using the utility function private val (repositoryName, datasetVersionHash, fileRelativePath) = parseUri(uri) private var tempFile: Option[File] = None override def getURI: URI = uri override def asInputStream(): InputStream = { def fallbackToLakeFS(exception: Throwable): InputStream = { logger.warn(s"${exception.getMessage}. Falling back to LakeFS direct file fetch.", exception) val file = LakeFSStorageClient.getFileFromRepo( getRepositoryName(), getVersionHash(), getFileRelativePath() ) Files.newInputStream(file.toPath) } if (userJwtToken.isEmpty) { try { val presignUrl = LakeFSStorageClient.getFilePresignedUrl( getRepositoryName(), getVersionHash(), getFileRelativePath() ) new URL(presignUrl).openStream() } catch { case e: Exception => fallbackToLakeFS(e) } } else { val presignRequestUrl = s"$fileServiceGetPresignURLEndpoint?repositoryName=${getRepositoryName()}&commitHash=${getVersionHash()}&filePath=${URLEncoder .encode(getFileRelativePath(), StandardCharsets.UTF_8.name())}" val connection = new URL(presignRequestUrl).openConnection().asInstanceOf[HttpURLConnection] connection.setRequestMethod("GET") connection.setRequestProperty("Authorization", s"Bearer $userJwtToken") try { if (connection.getResponseCode != HttpURLConnection.HTTP_OK) { throw new RuntimeException( s"Failed to retrieve presigned URL: HTTP ${connection.getResponseCode}" ) } // Read response body as a string val responseBody = new String(connection.getInputStream.readAllBytes(), StandardCharsets.UTF_8) // Extract presigned URL from JSON response val presignedUrl = responseBody .split("\"presignedUrl\"\\s*:\\s*\"")(1) .split("\"")(0) new URL(presignedUrl).openStream() } catch { case e: Exception => fallbackToLakeFS(e) } finally { connection.disconnect() } } } override def asFile(): File = { tempFile match { case Some(file) => file case None => val tempFilePath = Files.createTempFile("versionedFile", ".tmp") val tempFileStream = new FileOutputStream(tempFilePath.toFile) val inputStream = asInputStream() val buffer = new Array[Byte](1024) // Create an iterator to repeatedly call inputStream.read, and direct buffered data to file Iterator .continually(inputStream.read(buffer)) .takeWhile(_ != -1) .foreach(tempFileStream.write(buffer, 0, _)) inputStream.close() tempFileStream.close() val file = tempFilePath.toFile tempFile = Some(file) file } } override def clear(): Unit = { // first remove the temporary file tempFile match { case Some(file) => Files.delete(file.toPath) case None => // Do nothing } lazy val datasetsRootPath = Path .of(sys.env.getOrElse("TEXERA_HOME", ".")) .resolve("amber") .resolve("user-resources") .resolve("datasets") def getDatasetPath(did: Integer): Path = { datasetsRootPath.resolve(did.toString) } // then remove the dataset file GitVersionControlLocalFileStorage.removeFileFromRepo( getDatasetPath(0), getDatasetPath(0).resolve(fileRelativePath) ) } override def getRepositoryName(): String = repositoryName override def getVersionHash(): String = datasetVersionHash override def getFileRelativePath(): String = fileRelativePath.toString } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/OnDataset.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage.model trait OnDataset { def getRepositoryName(): String def getVersionHash(): String def getFileRelativePath(): String } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/ReadonlyLocalFileDocument.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage.model import java.io.{File, FileInputStream, InputStream} import java.net.URI /** * ReadonlyLocalFileDocument provides a read-only abstraction over a local file. * The data type T is not required, as all iterator-related methods are unsupported */ private[storage] class ReadonlyLocalFileDocument(uri: URI) extends ReadonlyVirtualDocument[Nothing] { /** * Get the URI of the corresponding document. * @return the URI of the document */ override def getURI: URI = uri /** * Get the file as an input stream for read operations. * @return InputStream to read from the file */ override def asInputStream(): InputStream = new FileInputStream(new File(uri)) /** * Get the file as an input stream for read operations. * * @return File object based on the URI */ override def asFile(): File = new File(uri) override def getItem(i: Int): Nothing = throw new NotImplementedError("getItem is not supported for ReadonlyLocalFileDocument") override def get(): Iterator[Nothing] = throw new NotImplementedError("get is not supported for ReadonlyLocalFileDocument") override def getRange( from: Int, until: Int, columns: Option[Seq[String]] = None ): Iterator[Nothing] = throw new NotImplementedError("getRange is not supported for ReadonlyLocalFileDocument") override def getAfter(offset: Int): Iterator[Nothing] = throw new NotImplementedError("getAfter is not supported for ReadonlyLocalFileDocument") override def getCount: Long = throw new NotImplementedError("getCount is not supported for ReadonlyLocalFileDocument") } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/ReadonlyVirtualDocument.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage.model import java.io.{File, InputStream} import java.net.URI /** * ReadonlyVirtualDocument provides an abstraction for read operations over a single resource. * This trait can be implemented by resources that only need to support read-related functionality. * @tparam T the type of data that can use index to read. */ trait ReadonlyVirtualDocument[T] { /** * Get the URI of the corresponding document. * @return the URI of the document */ def getURI: URI /** * Find ith item and return. * @param i index starting from 0 * @return data item of type T */ def getItem(i: Int): T /** * Get an iterator that iterates over all indexed items. * @return an iterator that returns data items of type T */ def get(): Iterator[T] /** * Get an iterator of a sequence starting from index `from`, until index `until`. * @param from the starting index (inclusive) * @param until the ending index (exclusive) * @param columns optional sequence of column names to retrieve. If None, retrieves all columns. * @return an iterator that returns data items of type T */ def getRange(from: Int, until: Int, columns: Option[Seq[String]] = None): Iterator[T] /** * Get an iterator of all items after the specified index `offset`. * @param offset the starting index (exclusive) * @return an iterator that returns data items of type T */ def getAfter(offset: Int): Iterator[T] /** * Get the count of items in the document. * @return the count of items */ def getCount: Long /** * Read document as an input stream. * @return the input stream */ def asInputStream(): InputStream /** * Read or materialize document as an file */ def asFile(): File } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/VirtualCollection.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage.model import java.net.URI /** * VirtualCollection provides the abstraction of managing a collection of children VirtualDocument */ abstract class VirtualCollection { def getURI: URI /** * get children documents that are directly underneath the current collection * @return the children documents */ def getDocuments: List[VirtualDocument[_]] /** * get a child document with certain name under this collection and return * @param name the child document's name * @return the document */ def getDocument(name: String): VirtualDocument[_] /** * physically remove current collection from the system. All children documents underneath will be removed */ def remove(): Unit } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/VirtualDocument.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage.model import java.io.{File, InputStream} import java.net.URI /** * TODO: break this base definition into more self-contained pieces, including Writeonly, IteratorBased * VirtualDocument provides the abstraction of doing read/write/copy/delete operations over a single resource in Texera system. * Note that all methods have a default implementation. This is because one document implementation may not be able to reasonably support all methods. * e.g. for dataset file, supports for read/write using file stream are essential, whereas read & write using index are hard to support and are semantically meaningless * @tparam T the type of data that can use index to read and write. */ abstract class VirtualDocument[T] extends ReadonlyVirtualDocument[T] { /** * get the URI of corresponding document * @return the URI of the document */ def getURI: URI /** * find ith item and return * @param i index starting from 0 * @return data item of type T */ def getItem(i: Int): T = throw new NotImplementedError("getItem method is not implemented") /** * get an iterator that iterates all indexed items * @return an iterator that returns data items of type T */ def get(): Iterator[T] = throw new NotImplementedError("get method is not implemented") /** * get an iterator of a sequence starting from index `from`, until index `until` * @param from the starting index (inclusive) * @param until the ending index (exclusive) * @param columns the columns to be projected * @return an iterator that returns data items of type T */ def getRange(from: Int, until: Int, columns: Option[Seq[String]] = None): Iterator[T] = throw new NotImplementedError("getRange method is not implemented") /** * get an iterator of all items after the specified index `offset` * @param offset the starting index (exclusive) * @return an iterator that returns data items of type T */ def getAfter(offset: Int): Iterator[T] = throw new NotImplementedError("getAfter method is not implemented") /** * get the count of items in the document * @return the count of items */ def getCount: Long = throw new NotImplementedError("getCount method is not implemented") /** * set the ith item * @param i the index to set the item at * @param item the data item */ def setItem(i: Int, item: T): Unit = throw new NotImplementedError("setItem method is not implemented") /** * return a writer that buffers the items and performs the flush operation at close time * @param writerIdentifier the id of the writer, maybe required by some implementations * @return a buffered item writer */ def writer(writerIdentifier: String): BufferedItemWriter[T] = throw new NotImplementedError("write method is not implemented") /** * append one data item to the document * @param item the data item */ def append(item: T): Unit = throw new NotImplementedError("append method is not implemented") /** * append data items from the iterator to the document * @param items iterator for the data items */ def append(items: Iterator[T]): Unit = throw new NotImplementedError("append method is not implemented") /** * append the file content with an opened input stream * @param inputStream the data source input stream */ def appendStream(inputStream: InputStream): Unit = throw new NotImplementedError("appendStream method is not implemented") /** * convert document to an input stream * @return the input stream */ def asInputStream(): InputStream = throw new NotImplementedError("asInputStream method is not implemented") /** * convert document to a File * @return the file */ def asFile(): File = throw new NotImplementedError("asFile method is not implemented") /** * physically remove the current document */ def clear(): Unit /** * Retrieve table statistics if the document supports it. * Default implementation returns empty map. */ def getTableStatistics: Map[String, Map[String, Any]] = throw new NotImplementedError("getTableStatistics method is not implemented") def getTotalFileSize: Long = throw new NotImplementedError("getTotalFileSize method is not implemented") } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/result/ResultSchema.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage.result import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema} object ResultSchema { val runtimeStatisticsSchema: Schema = new Schema( new Attribute("operatorId", AttributeType.STRING), new Attribute("time", AttributeType.TIMESTAMP), new Attribute("inputTupleCnt", AttributeType.LONG), new Attribute("inputTupleSize", AttributeType.LONG), new Attribute("outputTupleCnt", AttributeType.LONG), new Attribute("outputTupleSize", AttributeType.LONG), new Attribute("dataProcessingTime", AttributeType.LONG), new Attribute("controlProcessingTime", AttributeType.LONG), new Attribute("idleTime", AttributeType.LONG), new Attribute("numWorkers", AttributeType.INTEGER), new Attribute("status", AttributeType.INTEGER) ) val consoleMessagesSchema: Schema = new Schema( new Attribute("message", AttributeType.STRING) ) } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/result/WorkflowResultStore.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage.result import org.apache.texera.amber.core.virtualidentity.OperatorIdentity case class OperatorResultMetadata(tupleCount: Int = 0, changeDetector: String = "") case class WorkflowResultStore( resultInfo: Map[OperatorIdentity, OperatorResultMetadata] = Map.empty ) ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/result/iceberg/IcebergDocument.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage.result.iceberg import org.apache.texera.amber.core.storage.IcebergCatalogInstance import org.apache.texera.amber.core.storage.model.{BufferedItemWriter, VirtualDocument} import org.apache.texera.amber.core.storage.util.StorageUtil.{withLock, withReadLock, withWriteLock} import org.apache.texera.amber.util.IcebergUtil import org.apache.commons.io.IOUtils import org.apache.iceberg.catalog.{Catalog, TableIdentifier} import org.apache.iceberg.data.Record import org.apache.iceberg.exceptions.NoSuchTableException import org.apache.iceberg.types.{Conversions, Types} import org.apache.iceberg.{FileScanTask, Table} import java.io.{ByteArrayInputStream, InputStream, PipedInputStream, PipedOutputStream} import java.net.URI import java.nio.ByteBuffer import java.time.format.DateTimeFormatter import java.time.{Instant, LocalDate, ZoneOffset} import java.util.concurrent.locks.{ReentrantLock, ReentrantReadWriteLock} import java.util.zip.{ZipEntry, ZipOutputStream} import scala.collection.mutable import scala.jdk.CollectionConverters._ import scala.util.Using object Constants { // Default buffer size for streaming Parquet files to outside val DEFAULT_BUFFER_SIZE: Int = 8192 } /** * IcebergDocument is used to read and write a set of T as an Iceberg table. * It provides iterator-based read methods and supports multiple writers to write to the same table. * * The table must exist when constructing the document * * @param tableNamespace namespace of the table. * @param tableName name of the table. * @param tableSchema schema of the table. * @param serde function to serialize T into an Iceberg Record. * @param deserde function to deserialize an Iceberg Record into T. * @tparam T type of the data items stored in the Iceberg table. */ private[storage] class IcebergDocument[T >: Null <: AnyRef]( val tableNamespace: String, val tableName: String, val tableSchema: org.apache.iceberg.Schema, val serde: (org.apache.iceberg.Schema, T) => Record, val deserde: (org.apache.iceberg.Schema, Record) => T ) extends VirtualDocument[T] with OnIceberg { private val lock = new ReentrantReadWriteLock() @transient lazy val catalog: Catalog = IcebergCatalogInstance.getInstance() /** * Returns the URI of the table location. * * @throws NoSuchTableException if the table does not exist. */ override def getURI: URI = { val table = IcebergUtil .loadTableMetadata(catalog, tableNamespace, tableName) .getOrElse( throw new NoSuchTableException(f"table ${tableNamespace}.${tableName} doesn't exist") ) URI.create(table.location()) } /** * Deletes the table and clears its contents. */ override def clear(): Unit = withWriteLock(lock) { val identifier = TableIdentifier.of(tableNamespace, tableName) if (catalog.tableExists(identifier)) { catalog.dropTable(identifier) } } /** * Get an iterator for reading records from the table. */ override def get(): Iterator[T] = getUsingFileSequenceOrder(0, None) /** * Get records within a specified range [from, until). */ override def getRange(from: Int, until: Int, columns: Option[Seq[String]] = None): Iterator[T] = { getUsingFileSequenceOrder(from, Some(until), columns) } /** * Get records starting after a specified offset. */ override def getAfter(offset: Int): Iterator[T] = { getUsingFileSequenceOrder(offset, None) } /** * Get the total count of records in the table. */ override def getCount: Long = { val table = IcebergUtil .loadTableMetadata(catalog, tableNamespace, tableName) .getOrElse( return 0 ) table.newScan().planFiles().iterator().asScala.map(f => f.file().recordCount()).sum } /** * Creates a BufferedItemWriter for writing data to the table. * * @param writerIdentifier The writer's ID. It should be unique within the same table, as each writer will use it as * the prefix of the files they append */ override def writer(writerIdentifier: String): BufferedItemWriter[T] = { new IcebergTableWriter[T]( writerIdentifier, catalog, tableNamespace, tableName, tableSchema, serde ) } /** * Util iterator to get T in certain range * * @param from start from which record inclusively, if 0 means start from the first * @param until end at which record exclusively, if None means read to the table's EOF * @param columns columns to be projected */ private def getUsingFileSequenceOrder( from: Int, until: Option[Int], columns: Option[Seq[String]] = None ): Iterator[T] = withReadLock(lock) { new Iterator[T] { private val iteLock = new ReentrantLock() // Load the table instance, initially the table instance may not exist private var table: Option[Table] = loadTableMetadata() // Last seen snapshot id(logically it's like a version number). While reading, new snapshots may be created private var lastSnapshotId: Option[Long] = None // Counter for how many records have been skipped private var numOfSkippedRecords = 0 // Counter for how many records have been returned private var numOfReturnedRecords = 0 // Total number of records to return private val totalRecordsToReturn = until.map(_ - from).getOrElse(Int.MaxValue) // Iterator for usable file scan tasks private var usableFileIterator: Iterator[FileScanTask] = seekToUsableFile() // Current record iterator for the active file private var currentRecordIterator: Iterator[Record] = Iterator.empty // Util function to load the table's metadata private def loadTableMetadata(): Option[Table] = { IcebergUtil.loadTableMetadata( catalog, tableNamespace, tableName ) } private def seekToUsableFile(): Iterator[FileScanTask] = withLock(iteLock) { if (numOfSkippedRecords > from) { throw new RuntimeException("seek operation should not be called") } // refresh the table's snapshots if (table.isEmpty) { table = loadTableMetadata() } table.foreach(_.refresh()) // Retrieve and sort the file scan tasks by file sequence number val fileScanTasksIterator: Iterator[FileScanTask] = table match { case Some(t) => val currentSnapshotId = Option(t.currentSnapshot()).map(_.snapshotId()) val fileScanTasks = (lastSnapshotId, currentSnapshotId) match { // Read from the start case (None, Some(_)) => val tasks = t.newScan().planFiles().iterator().asScala lastSnapshotId = currentSnapshotId tasks // Read incrementally from the last snapshot case (Some(lastId), Some(currId)) if lastId != currId => val tasks = t .newIncrementalAppendScan() .fromSnapshotExclusive(lastId) .toSnapshot(currId) .planFiles() .iterator() .asScala lastSnapshotId = currentSnapshotId tasks // No new data case (Some(lastId), Some(currId)) if lastId == currId => Iterator.empty // Default: No data yet case _ => Iterator.empty } fileScanTasks.toSeq.sortBy(_.file().fileSequenceNumber()).iterator case None => Iterator.empty } // Iterate through sorted FileScanTasks and update numOfSkippedRecords val usableTasks = fileScanTasksIterator.dropWhile { task => val recordCount = task.file().recordCount() if (numOfSkippedRecords + recordCount <= from) { numOfSkippedRecords += recordCount.toInt true } else { false } } usableTasks } override def hasNext: Boolean = { if (numOfReturnedRecords >= totalRecordsToReturn) { return false } // If the current file still has records, return immediately without touching the catalog. // Only when the current file is exhausted do we check for more files and possibly refresh. if (currentRecordIterator.hasNext) { return true } if (!usableFileIterator.hasNext) { usableFileIterator = seekToUsableFile() } while (!currentRecordIterator.hasNext && usableFileIterator.hasNext) { val nextFile = usableFileIterator.next() val schemaToUse = columns match { case Some(cols) => tableSchema.select(cols.asJava) case None => tableSchema } currentRecordIterator = IcebergUtil.readDataFileAsIterator( nextFile.file(), schemaToUse, table.get ) // Skip records within the file if necessary val recordsToSkipInFile = from - numOfSkippedRecords if (recordsToSkipInFile > 0) { currentRecordIterator = currentRecordIterator.drop(recordsToSkipInFile) numOfSkippedRecords += recordsToSkipInFile } } currentRecordIterator.hasNext } override def next(): T = { if (!hasNext) throw new NoSuchElementException("No more records available") val record = currentRecordIterator.next() numOfReturnedRecords += 1 val schemaToUse = columns match { case Some(cols) => tableSchema.select(cols.asJava) case None => tableSchema } deserde(schemaToUse, record) } } } /** * Provides methods for extracting metadata statistics for the results * * **Statistics Computed:** * - **Numeric fields (Int, Long, Double)**: Computes `min` and `max`. * - **Date fields (Timestamp)**: Computes `min` and `max` (converted to `LocalDate`). * - **All fields**: Computes `not_null_count` (number of non-null values). * * @return A map where each field name is mapped to a nested map containing its statistics. * @throws NoSuchTableException if the table does not exist in the catalog. */ override def getTableStatistics: Map[String, Map[String, Any]] = { val table = IcebergUtil .loadTableMetadata(catalog, tableNamespace, tableName) .getOrElse( throw new NoSuchTableException(f"table ${tableNamespace}.${tableName} doesn't exist") ) val schema = table.schema() // Extract field names, IDs, and types from the schema val fieldTypes = schema.columns().asScala.map(col => col.name() -> (col.fieldId(), col.`type`())).toMap val fieldStats = mutable.Map[String, mutable.Map[String, Any]]() val dateFormatter: DateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE // Initialize statistics for each field fieldTypes.foreach { case (field, (_, fieldType)) => val initialStats = mutable.Map[String, Any]( "not_null_count" -> 0L ) if ( fieldType == Types.IntegerType.get() || fieldType == Types.LongType .get() || fieldType == Types.DoubleType.get() ) { initialStats("min") = Double.MaxValue initialStats("max") = Double.MinValue } else if ( fieldType == Types.TimestampType.withoutZone() || fieldType == Types.TimestampType .withZone() ) { initialStats("min") = LocalDate.MAX.format(dateFormatter) initialStats("max") = LocalDate.MIN.format(dateFormatter) } fieldStats(field) = initialStats } // Scan table files and aggregate statistics table.newScan().includeColumnStats().planFiles().iterator().asScala.foreach { file => val fileStats = file.file() // Extract column-level statistics val lowerBounds = Option(fileStats.lowerBounds()).getOrElse(Map.empty[Integer, ByteBuffer].asJava) val upperBounds = Option(fileStats.upperBounds()).getOrElse(Map.empty[Integer, ByteBuffer].asJava) val nullCounts = Option(fileStats.nullValueCounts()).getOrElse(Map.empty[Integer, java.lang.Long].asJava) val nanCounts = Option(fileStats.nanValueCounts()).getOrElse(Map.empty[Integer, java.lang.Long].asJava) fieldTypes.foreach { case (field, (fieldId, fieldType)) => val lowerBound = Option(lowerBounds.get(fieldId)) val upperBound = Option(upperBounds.get(fieldId)) val nullCount: Long = Option(nullCounts.get(fieldId)).map(_.toLong).getOrElse(0L) val nanCount: Long = Option(nanCounts.get(fieldId)).map(_.toLong).getOrElse(0L) val fieldStat = fieldStats(field) // Process min/max values for numerical types if ( fieldType == Types.IntegerType.get() || fieldType == Types.LongType .get() || fieldType == Types.DoubleType.get() ) { lowerBound.foreach { buffer => val minValue = Conversions.fromByteBuffer(fieldType, buffer).asInstanceOf[Number].doubleValue() fieldStat("min") = Math.min(fieldStat("min").asInstanceOf[Double], minValue) } upperBound.foreach { buffer => val maxValue = Conversions.fromByteBuffer(fieldType, buffer).asInstanceOf[Number].doubleValue() fieldStat("max") = Math.max(fieldStat("max").asInstanceOf[Double], maxValue) } } // Process min/max values for timestamp types else if ( fieldType == Types.TimestampType.withoutZone() || fieldType == Types.TimestampType .withZone() ) { lowerBound.foreach { buffer => val epochMicros = Conversions .fromByteBuffer(Types.TimestampType.withoutZone(), buffer) .asInstanceOf[Long] val dateValue = Instant.ofEpochMilli(epochMicros / 1000).atZone(ZoneOffset.UTC).toLocalDate fieldStat("min") = if ( dateValue .isBefore(LocalDate.parse(fieldStat("min").asInstanceOf[String], dateFormatter)) ) dateValue.format(dateFormatter) else fieldStat("min") } upperBound.foreach { buffer => val epochMicros = Conversions .fromByteBuffer(Types.TimestampType.withoutZone(), buffer) .asInstanceOf[Long] val dateValue = Instant.ofEpochMilli(epochMicros / 1000).atZone(ZoneOffset.UTC).toLocalDate fieldStat("max") = if ( dateValue .isAfter(LocalDate.parse(fieldStat("max").asInstanceOf[String], dateFormatter)) ) dateValue.format(dateFormatter) else fieldStat("max") } } // Update non-null count fieldStat("not_null_count") = fieldStat("not_null_count").asInstanceOf[Long] + (fileStats.recordCount().toLong - nullCount - nanCount) } } fieldStats.map { case (field, stats) => field -> stats.toMap }.toMap } /** * Computes the total size of all data files in the Iceberg table. * * @return The total size of all data files in bytes. * @throws NoSuchTableException if the table does not exist. */ override def getTotalFileSize: Long = { val table = IcebergUtil .loadTableMetadata(catalog, tableNamespace, tableName) .getOrElse( throw new NoSuchTableException(f"table ${tableNamespace}.${tableName} doesn't exist") ) var filesSize: Long = 0L try { filesSize = table.currentSnapshot().summary().getOrDefault("total-files-size", "0").toLong } catch { case e: Exception => println(s"Failed to get total-files-size: ${e.getMessage}") } filesSize } private def getParquetFileStream(fileTask: FileScanTask): InputStream = { val file = fileTask.file() val table = catalog.loadTable(TableIdentifier.of(tableNamespace, tableName)) table.io().newInputFile(file.location()).newStream() } /** * Converts Iceberg Parquet files to ZIP and return as a piped stream. * Each file in the table is added as a separate entry in the ZIP archive. * * @return An InputStream that streams the contents of the Iceberg table as a ZIP archive. */ override def asInputStream(): InputStream = { val fileScanTasks: Seq[FileScanTask] = { val table = this.catalog.loadTable(TableIdentifier.of(this.tableNamespace, this.tableName)) table.refresh() table.newScan().planFiles().iterator().asScala.toSeq } if (fileScanTasks.isEmpty) { return new ByteArrayInputStream(Array.emptyByteArray) } val pipeIn = new PipedInputStream(Constants.DEFAULT_BUFFER_SIZE) val pipeOut = new PipedOutputStream(pipeIn) // Start processing in a separate thread to avoid blocking val processingThread = new Thread(() => { try { Using.resource(new ZipOutputStream(pipeOut)) { zipOut => for (i <- fileScanTasks.indices) { val fileTask = fileScanTasks(i) val entryName = s"part-${String.format("%05d", i)}.parquet" zipOut.putNextEntry(new ZipEntry(entryName)) Using.resource(getParquetFileStream(fileTask)) { fileInputStream => IOUtils.copy(fileInputStream, zipOut) } zipOut.closeEntry() } } } catch { case e: Exception => throw e } finally { pipeOut.close() } }) // Set the thread as a daemon so // 1- It will terminate if the request stop // 2- It handle lifecycle of cleaning up the pipe resources processingThread.setDaemon(true) processingThread.start() pipeIn } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/result/iceberg/IcebergTableWriter.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage.result.iceberg import org.apache.texera.amber.config.StorageConfig import org.apache.texera.amber.core.storage.model.BufferedItemWriter import org.apache.texera.amber.util.IcebergUtil import org.apache.iceberg.catalog.Catalog import org.apache.iceberg.data.Record import org.apache.iceberg.data.parquet.GenericParquetWriter import org.apache.iceberg.io.{DataWriter, OutputFile} import org.apache.iceberg.parquet.Parquet import org.apache.iceberg.{Schema, Table} import scala.collection.mutable.ArrayBuffer /** * IcebergTableWriter writes data to the given Iceberg table in an append-only way. * - Each time the buffer is flushed, a new data file is created with a unique name. * - The `writerIdentifier` is used to prefix the created files. * - Iceberg data files are immutable once created. So each flush will create a distinct file. * * **Thread Safety**: This writer is **NOT thread-safe**, so only one thread should call this writer. * * @param writerIdentifier a unique identifier used to prefix the created files. * @param catalog the Iceberg catalog to manage table metadata. * @param tableNamespace the namespace of the Iceberg table. * @param tableName the name of the Iceberg table. * @param tableSchema the schema of the Iceberg table. * @param serde a function to serialize `T` into an Iceberg `Record`. * @tparam T the type of the data items written to the table. */ private[storage] class IcebergTableWriter[T]( val writerIdentifier: String, val catalog: Catalog, val tableNamespace: String, val tableName: String, val tableSchema: Schema, val serde: (org.apache.iceberg.Schema, T) => Record ) extends BufferedItemWriter[T] { // Buffer to hold items before flushing to the table private val buffer = new ArrayBuffer[T]() // Incremental filename index, incremented each time a new buffer is flushed private var filenameIdx = 0 // Incremental record ID, incremented for each record private var recordId = 0 override val bufferSize: Int = StorageConfig.icebergTableCommitBatchSize // Load the Iceberg table private val table: Table = IcebergUtil .loadTableMetadata(catalog, tableNamespace, tableName) .get /** * Open the writer and clear the buffer. */ override def open(): Unit = { buffer.clear() } /** * Add a single item to the buffer. * - If the buffer size exceeds the configured limit, the buffer is flushed. * @param item the item to add to the buffer. */ override def putOne(item: T): Unit = { buffer.append(item) if (buffer.size >= bufferSize) { flushBuffer() } } /** * Remove a single item from the buffer. * @param item the item to remove from the buffer. */ override def removeOne(item: T): Unit = { buffer -= item } /** * Flush the current buffer to a new Iceberg data file. * - Creates a new data file using the writer identifier and an incremental filename index. * - Writes all buffered items to the new file and commits it to the Iceberg table. */ private def flushBuffer(): Unit = { if (buffer.nonEmpty) { // Create a unique file path using the writer's identifier and the filename index val location = table.location().stripSuffix("/") val filepathString = s"$location/${writerIdentifier}_$filenameIdx" // Increment the filename index by 1 filenameIdx += 1 val outputFile: OutputFile = table.io().newOutputFile(filepathString) // Create a Parquet data writer to write a new file val dataWriter: DataWriter[Record] = Parquet .writeData(outputFile) .forTable(table) .createWriterFunc(GenericParquetWriter.buildWriter) .overwrite() .build() // Write each buffered item to the data file try { buffer.foreach { item => dataWriter.write(serde(tableSchema, item)) } } finally { dataWriter.close() } // Commit the new file to the table val dataFile = dataWriter.toDataFile table.newAppend().appendFile(dataFile).commit() // Clear the item buffer buffer.clear() } } /** * Close the writer, ensuring any remaining buffered items are flushed. */ override def close(): Unit = { if (buffer.nonEmpty) { flushBuffer() } } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/result/iceberg/OnIceberg.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage.result.iceberg import org.apache.texera.amber.util.IcebergUtil import org.apache.iceberg.exceptions.NoSuchTableException trait OnIceberg { def catalog: org.apache.iceberg.catalog.Catalog def tableNamespace: String def tableName: String /** * Expire snapshots for the table. */ def expireSnapshots(): Unit = { val table = IcebergUtil .loadTableMetadata(catalog, tableNamespace, tableName) .getOrElse(throw new NoSuchTableException(s"table $tableNamespace.$tableName doesn't exist")) // Begin the snapshot expiration process: table .expireSnapshots() // Initiate snapshot expiration. .retainLast(1) // Retain only the most recent snapshot. .expireOlderThan( System.currentTimeMillis() ) // Expire all snapshots older than the current time. .cleanExpiredFiles(true) // Remove the files associated with expired snapshots. .commit() // Commit the changes to make expiration effective. } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/util/LakeFSStorageClient.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage.util import io.lakefs.clients.sdk._ import io.lakefs.clients.sdk.model.ResetCreation.TypeEnum import io.lakefs.clients.sdk.model._ import org.apache.texera.amber.config.StorageConfig import java.io.{File, FileOutputStream, InputStream} import java.net.URI import java.nio.file.Files import scala.jdk.CollectionConverters._ /** * LakeFSFileStorage provides high-level file storage operations using LakeFS, * similar to Git operations for version control and file management. */ object LakeFSStorageClient { // Maximum number of results per LakeFS API request (pagination page size) private val PageSize = 1000 private lazy val apiClient: ApiClient = { val client = new ApiClient() client.setApiKey(StorageConfig.lakefsPassword) client.setUsername(StorageConfig.lakefsUsername) client.setPassword(StorageConfig.lakefsPassword) client.setServers( List( new ServerConfiguration( StorageConfig.lakefsEndpoint, "LakeFS API server endpoint", new java.util.HashMap[String, ServerVariable]() ) ).asJava ) client } private lazy val repoApi: RepositoriesApi = new RepositoriesApi(apiClient) private lazy val objectsApi: ObjectsApi = new ObjectsApi(apiClient) private lazy val branchesApi: BranchesApi = new BranchesApi(apiClient) private lazy val commitsApi: CommitsApi = new CommitsApi(apiClient) private lazy val refsApi: RefsApi = new RefsApi(apiClient) private lazy val stagingApi: StagingApi = new StagingApi(apiClient) private lazy val experimentalApi: ExperimentalApi = new ExperimentalApi(apiClient) private lazy val healthCheckApi: HealthCheckApi = new HealthCheckApi(apiClient) private val storageNamespaceURI: String = s"${StorageConfig.lakefsBlockStorageType}://${StorageConfig.lakefsBucketName}" private val branchName: String = "main" def healthCheck(): Unit = { try { this.healthCheckApi.healthCheck().execute() } catch { case e: Exception => throw new RuntimeException(s"Failed to connect to lake fs server: ${e.getMessage}") } } /** * Initializes a new repository in LakeFS. * * @param repoName Name of the repository. */ def initRepo( repoName: String ): Repository = { // validate repoName, see https://docs.lakefs.io/latest/understand/model/#repository val repoNamePattern = "^[a-z0-9][a-z0-9-]{2,62}$".r if (!repoNamePattern.matches(repoName)) { throw new IllegalArgumentException( s"Invalid repository name: '$repoName'. " + "Repository names must be 3-63 characters long, " + "contain only lowercase letters, numbers, and hyphens, " + "and cannot start with a hyphen." ) } // create repository val storageNamespace = s"$storageNamespaceURI/$repoName" val repo = new RepositoryCreation() .name(repoName) .storageNamespace(storageNamespace) .defaultBranch(branchName) .sampleData(false) repoApi.createRepository(repo).execute() } /** * Writes a file to the repository (similar to Git add). * Converts the InputStream to a temporary file for upload. * * @param repoName Repository name. * @param filePath Path in the repository. * @param inputStream File content stream. */ def writeFileToRepo( repoName: String, filePath: String, inputStream: InputStream ): ObjectStats = { val tempFilePath = Files.createTempFile("lakefs-upload-", ".tmp") val tempFileStream = new FileOutputStream(tempFilePath.toFile) val buffer = new Array[Byte](8192) // Create an iterator to repeatedly call inputStream.read, and direct buffered data to file Iterator .continually(inputStream.read(buffer)) .takeWhile(_ != -1) .foreach(tempFileStream.write(buffer, 0, _)) inputStream.close() tempFileStream.close() // Upload the temporary file to LakeFS objectsApi.uploadObject(repoName, branchName, filePath).content(tempFilePath.toFile).execute() } /** * Retrieves a file from a specific repository and commit. * * @param repoName Repository name. * @param versionHash Commit hash of the version. * @param filePath Path to the file in the repository. * @return The file retrieved from LakeFS. */ def getFileFromRepo(repoName: String, versionHash: String, filePath: String): File = { objectsApi.getObject(repoName, versionHash, filePath).execute() } /** * Removes a file from the repository (similar to Git rm). * * @param repoName Repository name. * @param branch Branch name. * @param filePath Path in the repository to delete. */ def removeFileFromRepo(repoName: String, branch: String, filePath: String): Unit = { objectsApi.deleteObject(repoName, branch, filePath).execute() } /** * Executes operations and creates a commit (similar to a transactional commit). * * @param repoName Repository name. * @param commitMessage Commit message. * @param operations File operations to perform before committing. */ def withCreateVersion(repoName: String, commitMessage: String)( operations: => Unit ): Commit = { operations val commit = new CommitCreation() .message(commitMessage) commitsApi.commit(repoName, branchName, commit).execute() } /** * Retrieves file content from a specific commit and path. * * @param repoName Repository name. * @param commitHash Commit hash of the version. * @param filePath Path to the file in the repository. */ def retrieveFileContent(repoName: String, commitHash: String, filePath: String): File = { objectsApi.getObject(repoName, commitHash, filePath).execute() } /** * Retrieves file content from a specific commit and path. * * @param repoName Repository name. * @param commitHash Commit hash of the version. * @param filePath Path to the file in the repository. */ def getFilePresignedUrl(repoName: String, commitHash: String, filePath: String): String = { objectsApi.statObject(repoName, commitHash, filePath).presign(true).execute().getPhysicalAddress } /** * Initiates a presigned multipart upload for a file in LakeFS. * * @param repoName Repository name. * @param filePath File path within the repository. * @param numberOfParts Number of parts to upload. * @return Multipart upload information. */ def initiatePresignedMultipartUploads( repoName: String, filePath: String, numberOfParts: Int ): PresignMultipartUpload = { experimentalApi .createPresignMultipartUpload(repoName, branchName, filePath) .parts(numberOfParts) .execute() } /** * Completes a previously initiated multipart upload. * * @param repoName Repository name. * @param filePath File path within the repository. * @param uploadId Multipart upload ID. * @param partsList List of (part number, ETag) pairs. * @param physicalAddress Physical location of the file in storage. * @return Object metadata after completion. */ def completePresignedMultipartUploads( repoName: String, filePath: String, uploadId: String, partsList: List[(Int, String)], physicalAddress: String ): ObjectStats = { val completePresignMultipartUpload: CompletePresignMultipartUpload = new CompletePresignMultipartUpload() // Sort parts by part number in ascending order val sortedParts = partsList.sortBy(_._1) completePresignMultipartUpload.setParts( sortedParts .map(part => { val newUploadPart = new UploadPart newUploadPart.setPartNumber(part._1) newUploadPart.setEtag(part._2) newUploadPart }) .asJava ) completePresignMultipartUpload.setPhysicalAddress(physicalAddress) experimentalApi .completePresignMultipartUpload(repoName, branchName, uploadId, filePath) .completePresignMultipartUpload(completePresignMultipartUpload) .execute() } /** * Aborts a multipart upload operation for a given file. * * @param repoName Repository name. * @param filePath File path within the repository. * @param uploadId Multipart upload ID. * @param physicalAddress Physical address of the file. */ def abortPresignedMultipartUploads( repoName: String, filePath: String, uploadId: String, physicalAddress: String ): Unit = { val abortPresignMultipartUpload: AbortPresignMultipartUpload = new AbortPresignMultipartUpload abortPresignMultipartUpload.setPhysicalAddress(physicalAddress) experimentalApi .abortPresignMultipartUpload(repoName, branchName, uploadId, filePath) .abortPresignMultipartUpload(abortPresignMultipartUpload) .execute() } /** * Deletes an entire repository. * * @param repoName Name of the repository to delete. */ def deleteRepo(repoName: String): Unit = { repoApi.deleteRepository(repoName).execute() } private def retrieveVersionsOfRepository(repoName: String): List[Commit] = { refsApi .logCommits(repoName, branchName) .execute() .getResults .asScala .toList .sortBy(_.getCreationDate)(Ordering[java.lang.Long].reverse) // Sort in descending order } /** * Fetches all pages from a paginated LakeFS API call. * * @param fetch A function that takes a pagination cursor and returns (results, pagination). * @return All results across all pages. */ private def fetchAllPages[T]( fetch: String => (java.util.List[T], Pagination) ): List[T] = { val allResults = scala.collection.mutable.ListBuffer[T]() var hasMore = true var after = "" // Pagination cursor returned by LakeFS while (hasMore) { val (results, pagination) = fetch(after) allResults ++= results.asScala hasMore = pagination.getHasMore if (hasMore) after = pagination.getNextOffset } allResults.toList } def retrieveObjectsOfVersion(repoName: String, commitHash: String): List[ObjectStats] = { fetchAllPages[ObjectStats] { after => val request = objectsApi.listObjects(repoName, commitHash).amount(PageSize) if (after.nonEmpty) request.after(after) val response = request.execute() (response.getResults, response.getPagination) } } def retrieveRepositorySize(repoName: String, commitHash: String = ""): Long = { val versionHash: String = if (commitHash.isEmpty) { val versionList = retrieveVersionsOfRepository(repoName) if (versionList.isEmpty) { "" } else { versionList.head.getId } } else { commitHash } if (versionHash.isEmpty) { 0 } else { LakeFSStorageClient .retrieveObjectsOfVersion(repoName, versionHash) .map(_.getSizeBytes.longValue()) .sum } } /** * Retrieves a list of uncommitted (staged) objects in a repository branch. * * @param repoName Repository name. * @return List of uncommitted object stats. */ def retrieveUncommittedObjects(repoName: String): List[Diff] = { fetchAllPages[Diff] { after => val request = branchesApi.diffBranch(repoName, branchName).amount(PageSize) if (after.nonEmpty) request.after(after) val response = request.execute() (response.getResults, response.getPagination) } } def createCommit(repoName: String, branch: String, commitMessage: String): Commit = { val commit = new CommitCreation() .message(commitMessage) commitsApi.commit(repoName, branch, commit).execute() } def deleteObject(repoName: String, filePath: String): Unit = { objectsApi.deleteObject(repoName, branchName, filePath).execute() } def resetObjectUploadOrDeletion(repoName: String, filePath: String): Unit = { val resetCreation: ResetCreation = new ResetCreation resetCreation.setType(TypeEnum.OBJECT) resetCreation.setPath(filePath) branchesApi.resetBranch(repoName, branchName, resetCreation).execute() } /** * Parse a physical address URI of the form ":///" into (bucket, key). * * Expected examples: * - "s3://my-bucket/path/to/file.csv" * - "gs://my-bucket/some/prefix/data.json" * * @param address URI string in the form ":///" * @return (bucket, key) where key does not start with "/" * @throws java.lang.IllegalArgumentException * if the address is empty, not a valid URI, missing bucket/host, or missing key/path */ def parsePhysicalAddress(address: String): (String, String) = { val raw = Option(address).getOrElse("").trim if (raw.isEmpty) throw new IllegalArgumentException("Address is empty (expected ':///')") val uri = try new URI(raw) catch { case e: Exception => throw new IllegalArgumentException( s"Invalid address URI: '$raw' (expected ':///')", e ) } val bucket = Option(uri.getHost).getOrElse("").trim if (bucket.isEmpty) throw new IllegalArgumentException( s"Invalid address: missing host/bucket in '$raw' (expected ':///')" ) val key = Option(uri.getPath).getOrElse("").stripPrefix("/").trim if (key.isEmpty) throw new IllegalArgumentException( s"Invalid address: missing key/path in '$raw' (expected ':///')" ) (bucket, key) } /** * Get file size. * * @param repoName Repository name. * @param commitHash Commit hash of the version. * @param filePath Path to the file in the repository. * @return File size in bytes */ def getFileSize( repoName: String, commitHash: String, filePath: String ): Long = { objectsApi .statObject(repoName, commitHash, filePath) .execute() .getSizeBytes .longValue() } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/util/StorageUtil.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage.util import java.util.concurrent.locks.{Lock, ReadWriteLock} object StorageUtil { def withWriteLock[M](rwLock: ReadWriteLock)(block: => M): M = { rwLock.writeLock().lock() try block finally rwLock.writeLock().unlock() } def withReadLock[M](rwLock: ReadWriteLock)(block: => M): M = { rwLock.readLock().lock() try block finally rwLock.readLock().unlock() } def withLock[M](lock: Lock)(block: => M): M = { lock.lock() try block finally lock.unlock() } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/util/dataset/GitVersionControlLocalFileStorage.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage.util.dataset; import org.eclipse.jgit.api.errors.GitAPIException; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.Comparator; import java.util.Set; /** * Git-based implementation of the VersionControlFileStorageService, using local file storage. */ public class GitVersionControlLocalFileStorage { /** * Writes content from the InputStream to a file at the given path. And stage the file addition/modification to git * This function WILL create the missing parent directory along the path * This method is NOT THREAD SAFE, as it did the file I/O and the git add operation * @param filePath The path within the repository to write the file. * @param inputStream The InputStream from which to read the content. * @throws IOException If an I/O error occurs. */ public static void writeFileToRepo(Path repoPath, Path filePath, InputStream inputStream) throws IOException, GitAPIException { Files.createDirectories(filePath.getParent()); Files.copy(inputStream, filePath, StandardCopyOption.REPLACE_EXISTING); // stage the addition/modification JGitVersionControl.add(repoPath, filePath); } /** * Deletes a file at the given path. * If the file path is pointing to a directory, or if file does not exist, error will be thrown * This method is NOT THREAD SAFE, as it did the file I/O and the git rm operation * @param filePath The path of the file to delete. * @throws IOException If an I/O error occurs. */ public static void removeFileFromRepo(Path repoPath, Path filePath) throws IOException, GitAPIException { if (Files.isDirectory(filePath)) { throw new IllegalArgumentException("Provided path is a directory, not a file: " + filePath); } Files.delete(filePath); // stage the deletion JGitVersionControl.rm(repoPath, filePath); } /** * Deletes the entire repository specified by the path. * This method is NOT THREAD SAFE, as it did the file I/O recursively * @param directoryPath The path of the directory to delete. * @throws IOException If an I/O error occurs. */ public static void deleteRepo(Path directoryPath) throws IOException { Files.walk(directoryPath) .sorted(Comparator.reverseOrder()) .map(Path::toFile) .forEach(File::delete); } /** * Initializes a new repository for version control at the specified path. * This method is NOT THREAD SAFE * @param baseRepoPath Path to initialize the repository at. * @return The branch identifier * @throws IOException If an I/O error occurs. * @throws GitAPIException If the JGit operation is interrupted. */ public static String initRepo(Path baseRepoPath) throws IOException, GitAPIException { // Check if the directory exists, if not, create it if (Files.notExists(baseRepoPath)) { Files.createDirectories(baseRepoPath); } return JGitVersionControl.initRepo(baseRepoPath); } /** * Executes a group of file operations as a single versioned transaction. The version is bumped after the operations finish. * This method is NOT THREAD SAFE as it potentially does lots of file I/O along with git operations * @param baseRepoPath The repository path. * @param versionName The name or message associated with the version. * @param operations The file operations to be executed within this versioned transaction. * @throws IOException If an I/O error occurs. * @throws GitAPIException If a Git operation fails. */ public static String withCreateVersion(Path baseRepoPath, String versionName, Runnable operations) throws IOException, GitAPIException { // Execute the provided file operations operations.run(); // After successful execution, create a new version with the specified name return JGitVersionControl.commit(baseRepoPath, versionName); } /** * Retrieves the set of file nodes at the root level, identified by its commit hash. * This method is THREAD SAFE * @param baseRepoPath The repository path. * @param versionCommitHashVal The commit hash of the version. * @return A set of file nodes at the root level of the given repo at given version */ public static Set retrieveRootFileNodesOfVersion(Path baseRepoPath, String versionCommitHashVal) throws Exception { return JGitVersionControl.getRootFileNodeOfCommit(baseRepoPath, versionCommitHashVal); } /** * Retrieves the content of a specific file from a specific version identified by its commit hash. * Writes the file content to the provided OutputStream. * This method is THREAD SAFE * @param baseRepoPath The repository path. * @param commitHash The commit hash of the version from which the file content is retrieved. * @param filePath The path of the file within the repository. * @param outputStream The OutputStream to which the file content is written. * @throws IOException If an I/O error occurs. * @throws GitAPIException If the operation is interrupted. */ public static void retrieveFileContentOfVersion(Path baseRepoPath, String commitHash, Path filePath, OutputStream outputStream) throws IOException, GitAPIException { JGitVersionControl.readFileContentOfCommitAsOutputStream(baseRepoPath, commitHash, filePath, outputStream); } public static InputStream retrieveFileContentOfVersionAsInputStream(Path baseRepoPath, String commitHash, Path filePath) throws IOException { return JGitVersionControl.readFileContentOfCommitAsInputStream(baseRepoPath, commitHash, filePath); } /** * Creates a temporary file and writes the content of a specific version of a file, identified by its commit hash, into this temporary file. * This method is useful for retrieving and working with specific versions of a file from a Git repository in a temporary and isolated manner. * * The temporary file is created in the system's default temporary-file directory, with a prefix "versionedFile" and a ".tmp" suffix. * The created temporary file is marked for deletion on JVM exit, ensuring no leftover files during runtime, though it's the caller's responsibility to manage the file if it needs to persist longer. * * @param baseRepoPath The path to the repository where the file version is to be retrieved from. This should be a valid path to a local repository managed by Git. * @param commitHash The commit hash that identifies the specific version of the file to retrieve. This commit must exist in the repository's history. * @param filePath The path of the file within the repository, relative to the repository's root directory. This file should exist in the commit specified. * @return The {@link Path} to the created temporary file, which contains the content of the specified file version. This path is absolute, ensuring it can be accessed directly. * @throws IOException If an I/O error occurs during file operations, including issues with creating the temporary file or writing to it. * @throws GitAPIException If the operation to retrieve file content from the Git repository fails. This could be due to issues with accessing the repository, the commit hash, or the file path specified. */ public static Path writeVersionedFileToTempFile(Path baseRepoPath, String commitHash, Path filePath) throws IOException, GitAPIException { // Generate a temporary file Path tempFile = Files.createTempFile("versionedFile", ".tmp"); // Ensure the file gets deleted on JVM exit tempFile.toFile().deleteOnExit(); // Use the retrieveFileContentOfVersion method to write the file content into the temp file try (OutputStream outputStream = Files.newOutputStream(tempFile)) { retrieveFileContentOfVersion(baseRepoPath, commitHash, filePath, outputStream); } // Return the absolute path of the temporary file return tempFile.toAbsolutePath(); } /** * Check if there is any uncommitted change in the given repo * This method is THREAD SAFE * @param repoPath The repository path * @return True if there are uncommitted changes. * @throws GitAPIException * @throws IOException */ public static boolean hasUncommittedChanges(Path repoPath) throws GitAPIException, IOException { return JGitVersionControl.hasUncommittedChanges(repoPath); } /** * Recovers the repository to its latest version, discarding any uncommitted changes if any. * This method is NOT THREAD SAFE * @param baseRepoPath The repository path. * @throws IOException If an I/O error occurs. * @throws GitAPIException If the operation is interrupted. */ public static void discardUncommittedChanges(Path baseRepoPath) throws IOException, GitAPIException { JGitVersionControl.discardUncommittedChanges(baseRepoPath); } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/util/dataset/JGitVersionControl.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage.util.dataset; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.ResetCommand; import org.eclipse.jgit.api.Status; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.eclipse.jgit.treewalk.TreeWalk; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; public class JGitVersionControl { public static String initRepo(Path path) throws GitAPIException, IOException { File gitDir = path.resolve(".git").toFile(); if (gitDir.exists()) { throw new IOException("Repository already exists at " + path); } // try-with-resource make sure the resource is released try (Git git = Git.init().setDirectory(path.toFile()).call()) { // Retrieve the default branch name Ref head = git.getRepository().exactRef("HEAD"); if (head == null || head.getTarget() == null) { return null; } String refName = head.getTarget().getName(); // HEAD should be in the form of 'ref: refs/heads/defaultBranchName' if (!refName.startsWith("refs/heads/")) { return null; } return refName.substring("refs/heads/".length()); } } public static InputStream readFileContentOfCommitAsInputStream(Path repoPath, String commitHash, Path filePath) throws IOException { if (!filePath.startsWith(repoPath)) { throw new IllegalArgumentException("File path must be under the repository path."); } if (Files.isDirectory(filePath)) { throw new IllegalArgumentException("File path points to a directory, not a file."); } try (Repository repository = new FileRepositoryBuilder() .setGitDir(repoPath.resolve(".git").toFile()) .build(); RevWalk revWalk = new RevWalk(repository)) { RevCommit commit = revWalk.parseCommit(repository.resolve(commitHash)); TreeWalk treeWalk = TreeWalk.forPath(repository, repoPath.relativize(filePath).toString(), commit.getTree()); if (treeWalk == null) { throw new IOException("File not found in commit: " + filePath); } ObjectId objectId = treeWalk.getObjectId(0); ObjectLoader loader = repository.open(objectId); // Return the InputStream for caller to manage return loader.openStream(); } } public static void readFileContentOfCommitAsOutputStream(Path repoPath, String commitHash, Path filePath, OutputStream outputStream) throws IOException { if (!filePath.startsWith(repoPath)) { throw new IllegalArgumentException("File path must be under the repository path."); } if (Files.isDirectory(filePath)) { throw new IllegalArgumentException("File path points to a directory, not a file."); } try (Repository repository = new FileRepositoryBuilder() .setGitDir(repoPath.resolve(".git").toFile()) .build(); RevWalk revWalk = new RevWalk(repository)) { RevCommit commit = revWalk.parseCommit(repository.resolve(commitHash)); TreeWalk treeWalk = TreeWalk.forPath(repository, repoPath.relativize(filePath).toString(), commit.getTree()); if (treeWalk == null) { throw new IOException("File not found in commit: " + filePath); } ObjectId objectId = treeWalk.getObjectId(0); ObjectLoader loader = repository.open(objectId); loader.copyTo(outputStream); } } public static Set getRootFileNodeOfCommit(Path repoPath, String commitHash) throws Exception { Map pathToFileNodeMap = new HashMap<>(); Set rootNodes = new HashSet<>(); try (Repository repository = new FileRepositoryBuilder() .setGitDir(repoPath.resolve(".git").toFile()) .build(); RevWalk revWalk = new RevWalk(repository)) { ObjectId commitId = repository.resolve(commitHash); RevCommit commit = revWalk.parseCommit(commitId); try (TreeWalk treeWalk = new TreeWalk(repository)) { treeWalk.addTree(commit.getTree()); treeWalk.setRecursive(false); while (treeWalk.next()) { Path fullPath = repoPath.resolve(treeWalk.getPathString()); long size = 0; if (!treeWalk.isSubtree()) { // Get file size for non-directory entries ObjectId objectId = treeWalk.getObjectId(0); ObjectLoader loader = repository.open(objectId); size = loader.getSize(); } PhysicalFileNode currentNode = createOrGetNode(pathToFileNodeMap, repoPath, fullPath, size); if (treeWalk.isSubtree()) { treeWalk.enterSubtree(); } else { // For files, we've already added them. Just ensure parent linkage is correct. ensureParentChildLink(pathToFileNodeMap, repoPath, fullPath, currentNode); } // For directories, also ensure they are correctly linked if (currentNode.isDirectory()) { ensureParentChildLink(pathToFileNodeMap, repoPath, fullPath, currentNode); } } } } // Extract root nodes pathToFileNodeMap.values().forEach(node -> { if (node.getAbsolutePath().getParent().equals(repoPath)) { rootNodes.add(node); } }); return rootNodes; } private static PhysicalFileNode createOrGetNode(Map map, Path repoPath, Path path, long size) { return map.computeIfAbsent(path.toString(), k -> new PhysicalFileNode(repoPath, path, size)); } private static PhysicalFileNode createOrGetNode(Map map, Path repoPath, Path path) { return map.computeIfAbsent(path.toString(), k -> new PhysicalFileNode(repoPath, path, 0)); } private static void ensureParentChildLink(Map map, Path repoPath, Path childPath, PhysicalFileNode childNode) { Path parentPath = childPath.getParent(); if (parentPath != null && !parentPath.equals(repoPath)) { PhysicalFileNode parentNode = createOrGetNode(map, repoPath, parentPath); parentNode.addChildNode(childNode); } } public static void add(Path repoPath, Path filePath) throws IOException, GitAPIException { try (Git git = Git.open(repoPath.toFile())) { // Stage the file addition/modification git.add().addFilepattern(repoPath.relativize(filePath).toString()).call(); } } public static void rm(Path repoPath, Path filePath) throws IOException, GitAPIException { try (Git git = Git.open(repoPath.toFile())) { git.rm().addFilepattern(repoPath.relativize(filePath).toString()).call(); // Stages the file deletion } } // create a commit, and return the commit hash public static String commit(Path repoPath, String commitMessage) throws IOException, GitAPIException { FileRepositoryBuilder builder = new FileRepositoryBuilder(); try (Repository repository = builder.setGitDir(repoPath.resolve(".git").toFile()) .readEnvironment() // scan environment GIT_* variables .findGitDir() // scan up the file system tree .build()) { try (Git git = new Git(repository)) { // Commit the changes that have been staged RevCommit commit = git.commit().setMessage(commitMessage).call(); // Return the commit hash return commit.getId().getName(); } } } public static void discardUncommittedChanges(Path repoPath) throws IOException, GitAPIException { try (Repository repository = new FileRepositoryBuilder() .setGitDir(repoPath.resolve(".git").toFile()) .build(); Git git = new Git(repository)) { // Reset hard to discard changes in tracked files git.reset().setMode(ResetCommand.ResetType.HARD).call(); // Clean the working directory to remove untracked files git.clean().setCleanDirectories(true).call(); } } public static boolean hasUncommittedChanges(Path repoPath) throws IOException, GitAPIException { try (Repository repository = new FileRepositoryBuilder() .setGitDir(repoPath.resolve(".git").toFile()) .readEnvironment() .findGitDir() .build(); Git git = new Git(repository)) { Status status = git.status().call(); return !status.isClean(); } } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/util/dataset/PhysicalFileNode.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage.util.dataset; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; public class PhysicalFileNode { private final Path absoluteFilePath; private final Path relativeFilePath; private final Set children; private long size; public PhysicalFileNode(Path repoPath, Path path, long size) { this.absoluteFilePath = path; this.relativeFilePath = repoPath.relativize(this.absoluteFilePath); this.children = new HashSet<>(); this.size = size; } public boolean isFile() { return Files.isRegularFile(absoluteFilePath); } public boolean isDirectory() { return Files.isDirectory(absoluteFilePath); } public Path getAbsolutePath() { return absoluteFilePath; } public Path getRelativePath() { return relativeFilePath; } public void addChildNode(PhysicalFileNode child) { if (!child.getAbsolutePath().getParent().equals(this.absoluteFilePath)) { throw new IllegalArgumentException("Child node is not a direct subpath of the parent node"); } this.children.add(child); } public Set getChildren() { return children; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PhysicalFileNode physicalFileNode = (PhysicalFileNode) o; return Objects.equals(absoluteFilePath, physicalFileNode.absoluteFilePath) && Objects.equals(children, physicalFileNode.children); } @Override public int hashCode() { return Objects.hash(absoluteFilePath, children); } /** * Collects the relative paths of all file nodes from a given set of FileNode. * @param nodes The set of FileNode to collect file paths from. * @return A list of strings representing the relative paths of all file nodes. */ public static List getAllFileRelativePaths(Set nodes) { List filePaths = new ArrayList<>(); getAllFileRelativePathsHelper(nodes, filePaths); return filePaths; } /** * Helper method to recursively collect the relative paths of all file nodes. * @param nodes The current set of FileNode to collect file paths from. * @param filePaths The list to add the relative paths of the file nodes to. */ private static void getAllFileRelativePathsHelper(Set nodes, List filePaths) { for (PhysicalFileNode node : nodes) { if (node.isFile()) { filePaths.add(node.getRelativePath().toString()); } else if (node.isDirectory()) { getAllFileRelativePathsHelper(node.getChildren(), filePaths); } } } public long getSize() { return size; } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/tuple/Attribute.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.tuple; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import java.io.Serializable; import static com.google.common.base.Preconditions.checkNotNull; /** * An attribute describes the name and the type of a column. */ public class Attribute implements Serializable { private final String attributeName; private final AttributeType attributeType; @JsonCreator public Attribute( @JsonProperty(value = "attributeName", required = true) String attributeName, @JsonProperty(value = "attributeType", required = true) AttributeType attributeType ) { checkNotNull(attributeName); checkNotNull(attributeType); this.attributeName = attributeName; this.attributeType = attributeType; } @JsonProperty(value = "attributeName", required = true) @NotBlank(message = "Attribute name is required") public String getName() { return attributeName; } @JsonProperty(value = "attributeType", required = true) @NotNull(message = "Attribute type is required") public AttributeType getType() { return attributeType; } @Override public String toString() { return "Attribute[name=" + attributeName + ", type=" + attributeType + "]"; } @Override public boolean equals(Object toCompare) { if (this == toCompare) { return true; } if (toCompare == null) { return false; } if (this.getClass() != toCompare.getClass()) { return false; } Attribute that = (Attribute) toCompare; if (this.attributeName == null) { return that.attributeName == null; } if (this.attributeType == null) { return that.attributeType == null; } return this.attributeName.equalsIgnoreCase(that.attributeName) && this.attributeType.equals(that.attributeType); } @Override public int hashCode() { return this.attributeName.hashCode() + this.attributeType.toString().hashCode(); } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/tuple/AttributeType.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.tuple; import com.fasterxml.jackson.annotation.JsonValue; import java.io.Serializable; import java.sql.Timestamp; public enum AttributeType implements Serializable { /** * To add a new AttributeType, update the following files to handle the new type: * 1. AttributeTypeUtils * src/main/scala/org/apache/texera/workflow/common/tuple/schema/AttributeTypeUtils.scala * Provide parsing, inferring, and casting logic between other AttributeTypes. *

* 2. SQLSourceOpDesc * src/main/scala/org/apache/texera/workflow/operators/source/sql/SQLSourceOpDesc * Especially SQLSources will need to map the input schema to Texera.Schema. AttributeType * needs to be converted from original source types accordingly. *

* 3. FilterPredicate * src/main/scala/org/apache/texera/workflow/operators/filter/FilterPredicate.java * FilterPredicate takes in AttributeTypes and converts them into a comparable type, then do * the comparison. New AttributeTypes needs to be mapped to a comparable type there. *

* 4. SpecializedAverageOpDesc.getNumericalValue * src/main/scala/org/apache/texera/workflow/operators/aggregate/SpecializedAverageOpDesc.scala * New AttributeTypes might need to be converted into a numerical value in order to perform * aggregations. *

* 5. SchemaPropagationService.SchemaAttribute * src/app/workspace/service/dynamic-schema/schema-propagation/schema-propagation.service.ts * Declare the frontend SchemaAttribute for the new AttributeType. *

* 6. ArrowUtils (Java) * src/main/scala/org/apache/amber/engine/architecture/pythonworker/ArrowUtils.scala * Provide java-side conversion between ArrowType and AttributeType. *

* 7. ArrowUtils (Python) * src/main/python/core/util/arrow_utils.py * Provide python-side conversion between ArrowType and AttributeType. */ // A field that is indexed but not tokenized: the entire String // value is indexed as a single token STRING("string", String.class), INTEGER("integer", Integer.class), LONG("long", Long.class), DOUBLE("double", Double.class), BOOLEAN("boolean", Boolean.class), TIMESTAMP("timestamp", Timestamp.class), BINARY("binary", byte[].class), LARGE_BINARY("large_binary", LargeBinary.class), ANY("ANY", Object.class); private final String name; private final Class fieldClass; AttributeType(String name, Class fieldClass) { this.name = name; this.fieldClass = fieldClass; } @JsonValue public String getName() { if (this.name.equals(ANY.name)) { // exclude this enum type in JSON schema // JSON schema generator will ignore empty enum type return ""; } return this.name; } public Class getFieldClass() { return this.fieldClass; } public static AttributeType getAttributeType(Class fieldClass) { if (fieldClass.equals(String.class)) { return STRING; } else if (fieldClass.equals(Integer.class)) { return INTEGER; } else if (fieldClass.equals(Long.class)) { return LONG; } else if (fieldClass.equals(Double.class)) { return DOUBLE; } else if (fieldClass.equals(Boolean.class)) { return BOOLEAN; } else if (fieldClass.equals(Timestamp.class)) { return TIMESTAMP; } else if (fieldClass.equals(byte[].class)) { return BINARY; } else if (fieldClass.equals(LargeBinary.class)) { return LARGE_BINARY; } else { return ANY; } } @Override public String toString() { return this.getName(); } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/tuple/AttributeTypeUtils.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.tuple import com.github.sisyphsu.dateparser.DateParserUtils import java.sql.Timestamp import java.text.NumberFormat import java.util.Locale import scala.util.Try import scala.util.control.Exception.allCatch object AttributeTypeUtils extends Serializable { /** * This function checks whether the current attribute in the schema matches the selected attribute for casting. * If it matches, its type is changed to the specified result type. * If it doesn't match, the original type is retained. * The order of attributes in the schema is preserved. * * @param schema schema of data * @param attribute selected attribute * @param resultType casting type * @return a new schema with the modified attribute type */ def SchemaCasting( schema: Schema, attribute: String, resultType: AttributeType ): Schema = { val updatedAttributes = schema.getAttributes.map { attr => if (attr.getName == attribute) { resultType match { case AttributeType.STRING | AttributeType.INTEGER | AttributeType.DOUBLE | AttributeType.LONG | AttributeType.BOOLEAN | AttributeType.TIMESTAMP | AttributeType.BINARY => new Attribute(attribute, resultType) // Cast to the specified result type case AttributeType.ANY | _ => attr // Retain the original type for unsupported types } } else { attr // Retain attributes that don't match the target } } Schema(updatedAttributes) } /** * Casts the fields of a tuple to new types according to a list of type casting units, * producing a new tuple that conforms to the specified type changes. * Each type casting unit specifies the attribute name and the target type to cast to. * If an attribute name in the tuple does not have a corresponding type casting unit, * its value is included in the result tuple without type conversion. * * @param tuple The source tuple whose fields are to be casted. * @param targetTypes A mapping of attribute names to their target types, which specifies how to cast each field. * If an attribute is not present in the map, no casting is applied to it. * @return A new instance of TupleLike with fields casted to the target types * as specified by the typeCastingUnits. */ def tupleCasting( tuple: Tuple, targetTypes: Map[String, AttributeType] ): TupleLike = TupleLike( tuple.getSchema.getAttributes.map { attr => val targetType = targetTypes.getOrElse(attr.getName, attr.getType) parseField(tuple.getField(attr.getName), targetType, force = true) } ) def parseFields(fields: Array[Any], schema: Schema): Array[Any] = { parseFields(fields, schema.getAttributes.map(attr => attr.getType).toArray) } /** * parse Fields to corresponding Java objects base on the given Schema AttributeTypes * @param attributeTypes Schema AttributeTypeList * @param fields fields value * @return parsedFields in the target AttributeTypes */ @throws[AttributeTypeException] def parseFields( fields: Array[Any], attributeTypes: Array[AttributeType] ): Array[Any] = { fields.indices.map(i => parseField(fields(i), attributeTypes(i))).toArray } /** * parse Field to a corresponding Java object base on the given Schema AttributeType * @param field fields value * @param attributeType target AttributeType * @param force force to parse the field to the target type if possible * currently only support for comma-separated numbers * * @return parsedField in the target AttributeType */ @throws[AttributeTypeException] def parseField( field: Any, attributeType: AttributeType, force: Boolean = false ): Any = { if (field == null) return null attributeType match { case AttributeType.INTEGER => parseInteger(field, force) case AttributeType.LONG => parseLong(field, force) case AttributeType.DOUBLE => parseDouble(field) case AttributeType.BOOLEAN => parseBoolean(field) case AttributeType.TIMESTAMP => parseTimestamp(field) case AttributeType.STRING => field.toString case AttributeType.BINARY => field case AttributeType.LARGE_BINARY => new LargeBinary(field.toString) case AttributeType.ANY | _ => field } } @throws[AttributeTypeException] private def parseInteger(fieldValue: Any, force: Boolean = false): Integer = { val attempt: Try[Integer] = Try { fieldValue match { case str: String => if (force) { // Use US locale for comma-separated numbers NumberFormat.getNumberInstance(Locale.US).parse(str.trim).intValue() } else { str.trim.toInt } case int: Integer => int case long: java.lang.Long => long.toInt case double: java.lang.Double => double.toInt case boolean: java.lang.Boolean => if (boolean) 1 else 0 // Timestamp and Binary are considered to be illegal here. case _ => throw new IllegalArgumentException( s"Unsupported type for parsing to Integer: ${fieldValue.getClass.getName}" ) } } attempt.recover { case e: Exception => throw new AttributeTypeException( s"Failed to parse type ${fieldValue.getClass.getName} to Integer: ${fieldValue.toString}", e ) }.get } @throws[AttributeTypeException] private def parseLong(fieldValue: Any, force: Boolean = false): java.lang.Long = { val attempt: Try[Long] = Try { fieldValue match { case str: String => if (force) { // Use US locale for comma-separated numbers NumberFormat.getNumberInstance(Locale.US).parse(str.trim).longValue() } else { str.trim.toLong } case int: Integer => int.toLong case long: java.lang.Long => long case double: java.lang.Double => double.toLong case boolean: java.lang.Boolean => if (boolean) 1L else 0L case timestamp: Timestamp => timestamp.toInstant.toEpochMilli // Binary is considered to be illegal here. case _ => throw new IllegalArgumentException( s"Unsupported type for parsing to Long: ${fieldValue.getClass.getName}" ) } } attempt.recover { case e: Exception => throw new AttributeTypeException( s"Failed to parse type ${fieldValue.getClass.getName} to Long: ${fieldValue.toString}", e ) }.get } @throws[AttributeTypeException] def parseTimestamp(fieldValue: Any): Timestamp = { val attempt: Try[Timestamp] = Try { fieldValue match { case str: String => new Timestamp(DateParserUtils.parseDate(str.trim).getTime) case long: java.lang.Long => new Timestamp(long) case timestamp: Timestamp => timestamp case date: java.util.Date => new Timestamp(date.getTime) case localDateTime: java.time.LocalDateTime => Timestamp.valueOf(localDateTime) case instant: java.time.Instant => Timestamp.from(instant) case offsetDateTime: java.time.OffsetDateTime => Timestamp.from(offsetDateTime.toInstant) case zonedDateTime: java.time.ZonedDateTime => Timestamp.from(zonedDateTime.toInstant) case localDate: java.time.LocalDate => Timestamp.valueOf(localDate.atStartOfDay()) // Integer, Double, Boolean, Binary are considered to be illegal here. case _ => throw new AttributeTypeException( s"Unsupported type for parsing to Timestamp: ${fieldValue.getClass.getName}" ) } } attempt.recover { case e: Exception => throw new AttributeTypeException( s"Failed to parse type ${fieldValue.getClass.getName} to Timestamp: ${fieldValue.toString}", e ) }.get } @throws[AttributeTypeException] def parseDouble(fieldValue: Any): java.lang.Double = { val attempt: Try[Double] = Try { fieldValue match { case str: String => str.trim.toDouble case int: Integer => int.toDouble case long: java.lang.Long => long.toDouble case double: java.lang.Double => double case boolean: java.lang.Boolean => if (boolean) 1 else 0 // Timestamp and Binary are considered to be illegal here. case _ => throw new AttributeTypeException( s"Unsupported type for parsing to Double: ${fieldValue.getClass.getName}" ) } } attempt.recover { case e: Exception => throw new AttributeTypeException( s"Failed to parse type ${fieldValue.getClass.getName} to Double: ${fieldValue.toString}", e ) }.get } @throws[AttributeTypeException] private def parseBoolean(fieldValue: Any): java.lang.Boolean = { val attempt: Try[Boolean] = Try { fieldValue match { case str: String => (Try(str.trim.toBoolean) orElse Try(str.trim.toInt == 1)).get case int: Integer => int != 0 case long: java.lang.Long => long != 0 case double: java.lang.Double => double != 0 case boolean: java.lang.Boolean => boolean // Timestamp and Binary are considered to be illegal here. case _ => throw new AttributeTypeException( s"Unsupported type for parsing to Boolean: ${fieldValue.getClass.getName}" ) } } attempt.recover { case e: Exception => throw new AttributeTypeException( s"Failed to parse type ${fieldValue.getClass.getName} to Boolean: ${fieldValue.toString}", e ) }.get } /** * Infers field types of a given row of data. The given attributeTypes will be updated * through each iteration of row inference, to contain the most accurate inference. * @param attributeTypes AttributeTypes that being passed to each iteration. * @param fields data fields to be parsed * @return */ private def inferRow( attributeTypes: Array[AttributeType], fields: Array[Any] ): Unit = { for (i <- fields.indices) { attributeTypes.update(i, inferField(attributeTypes.apply(i), fields.apply(i))) } } /** * Infers field types of a given row of data. * @param fieldsIterator iterator of field arrays to be parsed. * each field array should have exact same order and length. * @return AttributeType array */ def inferSchemaFromRows(fieldsIterator: Iterator[Array[Any]]): Array[AttributeType] = { var attributeTypes: Array[AttributeType] = Array() for (fields <- fieldsIterator) { if (attributeTypes.isEmpty) { attributeTypes = Array.fill[AttributeType](fields.length)(AttributeType.INTEGER) } inferRow(attributeTypes, fields) } attributeTypes } /** * infer filed type with only data field * @param fieldValue data field to be parsed, original as String field * @return inferred AttributeType */ def inferField(fieldValue: Any): AttributeType = { tryParseInteger(fieldValue) } private def tryParseInteger(fieldValue: Any): AttributeType = { if (fieldValue == null) return AttributeType.INTEGER allCatch opt parseInteger(fieldValue) match { case Some(_) => AttributeType.INTEGER case None => tryParseLong(fieldValue) } } private def tryParseLong(fieldValue: Any): AttributeType = { if (fieldValue == null) return AttributeType.LONG allCatch opt parseLong(fieldValue) match { case Some(_) => AttributeType.LONG case None => tryParseTimestamp(fieldValue) } } private def tryParseTimestamp(fieldValue: Any): AttributeType = { if (fieldValue == null) return AttributeType.TIMESTAMP allCatch opt parseTimestamp(fieldValue) match { case Some(_) => AttributeType.TIMESTAMP case None => tryParseDouble(fieldValue) } } private def tryParseDouble(fieldValue: Any): AttributeType = { if (fieldValue == null) return AttributeType.DOUBLE allCatch opt parseDouble(fieldValue) match { case Some(_) => AttributeType.DOUBLE case None => tryParseBoolean(fieldValue) } } private def tryParseBoolean(fieldValue: Any): AttributeType = { if (fieldValue == null) return AttributeType.BOOLEAN allCatch opt parseBoolean(fieldValue) match { case Some(_) => AttributeType.BOOLEAN case None => tryParseString() } } private def tryParseString(): AttributeType = { AttributeType.STRING } /** * InferField when get both typeSofar and tuple string * @param attributeType typeSofar * @param fieldValue data field to be parsed, original as String field * @return inferred AttributeType */ def inferField(attributeType: AttributeType, fieldValue: Any): AttributeType = { attributeType match { case AttributeType.STRING => tryParseString() case AttributeType.BOOLEAN => tryParseBoolean(fieldValue) case AttributeType.DOUBLE => tryParseDouble(fieldValue) case AttributeType.LONG => tryParseLong(fieldValue) case AttributeType.INTEGER => tryParseInteger(fieldValue) case AttributeType.TIMESTAMP => tryParseTimestamp(fieldValue) case AttributeType.BINARY => tryParseString() case AttributeType.LARGE_BINARY => AttributeType.LARGE_BINARY // Large binaries are never inferred from data case _ => tryParseString() } } /** Three-way compare for the given attribute type. * Returns < 0 if left < right, > 0 if left > right, 0 if equal. * Null semantics: null < non-null (both null => 0). */ @throws[UnsupportedOperationException] def compare(left: Any, right: Any, attrType: AttributeType): Int = (left, right) match { case (null, null) => 0 case (null, _) => -1 case (_, null) => 1 case _ => attrType match { case AttributeType.INTEGER => java.lang.Integer.compare( left.asInstanceOf[Number].intValue(), right.asInstanceOf[Number].intValue() ) case AttributeType.LONG => java.lang.Long.compare( left.asInstanceOf[Number].longValue(), right.asInstanceOf[Number].longValue() ) case AttributeType.DOUBLE => java.lang.Double.compare( left.asInstanceOf[Number].doubleValue(), right.asInstanceOf[Number].doubleValue() ) // -Infinity < ... < -0.0 < +0.0 < ... < +Infinity < NaN case AttributeType.BOOLEAN => java.lang.Boolean.compare( left.asInstanceOf[Boolean], right.asInstanceOf[Boolean] ) case AttributeType.TIMESTAMP => java.lang.Long.compare( left.asInstanceOf[Timestamp].getTime, right.asInstanceOf[Timestamp].getTime ) case AttributeType.STRING => left.toString.compareTo(right.toString) case AttributeType.BINARY => java.util.Arrays.compareUnsigned( left.asInstanceOf[Array[Byte]], right.asInstanceOf[Array[Byte]] ) case _ => throw new UnsupportedOperationException( s"Unsupported attribute type for compare: $attrType" ) } } /** Type-aware addition (null is identity). */ @throws[UnsupportedOperationException] def add(left: Object, right: Object, attrType: AttributeType): Object = (left, right) match { case (null, null) => zeroValue(attrType) case (null, right) => right case (left, null) => left case (left, right) => attrType match { case AttributeType.INTEGER => java.lang.Integer.valueOf( left.asInstanceOf[Number].intValue() + right.asInstanceOf[Number].intValue() ) case AttributeType.LONG => java.lang.Long.valueOf( left.asInstanceOf[Number].longValue() + right.asInstanceOf[Number].longValue() ) case AttributeType.DOUBLE => java.lang.Double.valueOf( left.asInstanceOf[Number].doubleValue() + right.asInstanceOf[Number].doubleValue() ) case AttributeType.TIMESTAMP => new Timestamp( left.asInstanceOf[Timestamp].getTime + right.asInstanceOf[Timestamp].getTime ) case _ => throw new UnsupportedOperationException( s"Unsupported attribute type for addition: $attrType" ) } } /** Additive identity for supported numeric/timestamp types. * For BINARY an empty array is returned as an identity value. */ @throws[UnsupportedOperationException] def zeroValue(attrType: AttributeType): Object = attrType match { case AttributeType.INTEGER => java.lang.Integer.valueOf(0) case AttributeType.LONG => java.lang.Long.valueOf(0L) case AttributeType.DOUBLE => java.lang.Double.valueOf(0.0d) case AttributeType.TIMESTAMP => new Timestamp(0L) case AttributeType.BINARY => Array.emptyByteArray case _ => throw new UnsupportedOperationException( s"Unsupported attribute type for zero value: $attrType" ) } /** Returns the maximum possible value for a given attribute type. */ @throws[UnsupportedOperationException] def maxValue(attrType: AttributeType): Object = attrType match { case AttributeType.INTEGER => java.lang.Integer.valueOf(Integer.MAX_VALUE) case AttributeType.LONG => java.lang.Long.valueOf(java.lang.Long.MAX_VALUE) case AttributeType.DOUBLE => java.lang.Double.valueOf(java.lang.Double.MAX_VALUE) case AttributeType.TIMESTAMP => new Timestamp(java.lang.Long.MAX_VALUE) case _ => throw new UnsupportedOperationException( s"Unsupported attribute type for max value: $attrType" ) } /** Returns the minimum possible value for a given attribute type. * For BINARY under lexicographic order, the empty array is the global minimum. */ @throws[UnsupportedOperationException] def minValue(attrType: AttributeType): Object = attrType match { case AttributeType.INTEGER => java.lang.Integer.valueOf(Integer.MIN_VALUE) case AttributeType.LONG => java.lang.Long.valueOf(java.lang.Long.MIN_VALUE) case AttributeType.DOUBLE => java.lang.Double.valueOf(java.lang.Double.NEGATIVE_INFINITY) case AttributeType.TIMESTAMP => new Timestamp(0L) case AttributeType.BINARY => Array.emptyByteArray case _ => throw new UnsupportedOperationException( s"Unsupported attribute type for min value: $attrType" ) } class AttributeTypeException(msg: String, cause: Throwable = null) extends IllegalArgumentException(msg, cause) {} } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/tuple/LargeBinary.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.tuple; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonValue; import org.apache.texera.amber.core.executor.OperatorExecutor; import org.apache.texera.service.util.LargeBinaryManager; import java.net.URI; import java.util.Objects; /** * LargeBinary represents a reference to a large object stored in S3. * * Each LargeBinary is identified by an S3 URI (s3://bucket/path/to/object). * LargeBinaries are automatically tracked and cleaned up when the workflow execution completes. */ public class LargeBinary { private final String uri; /** * Creates a LargeBinary from an existing S3 URI. * Used primarily for deserialization from JSON. * * @param uri S3 URI in the format s3://bucket/path/to/object * @throws IllegalArgumentException if URI is null or doesn't start with "s3://" */ @JsonCreator public LargeBinary(@JsonProperty("uri") String uri) { if (uri == null) { throw new IllegalArgumentException("LargeBinary URI cannot be null"); } if (!uri.startsWith("s3://")) { throw new IllegalArgumentException( "LargeBinary URI must start with 's3://', got: " + uri ); } this.uri = uri; } /** * Creates a new LargeBinary for writing data. * Generates a unique S3 URI. * * Usage example: * * LargeBinary largeBinary = new LargeBinary(); * try (LargeBinaryOutputStream out = new LargeBinaryOutputStream(largeBinary)) { * out.write(data); * } * // largeBinary is now ready to be added to tuples * */ public LargeBinary() { this(LargeBinaryManager.create()); } @JsonValue public String getUri() { return uri; } public String getBucketName() { return URI.create(uri).getHost(); } public String getObjectKey() { String path = URI.create(uri).getPath(); return path.startsWith("/") ? path.substring(1) : path; } @Override public String toString() { return uri; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof LargeBinary)) return false; LargeBinary that = (LargeBinary) obj; return Objects.equals(uri, that.uri); } @Override public int hashCode() { return Objects.hash(uri); } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/tuple/Schema.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.tuple import com.fasterxml.jackson.annotation.{JsonCreator, JsonIgnore, JsonProperty} import com.google.common.base.Preconditions.checkNotNull import scala.collection.immutable.ListMap /** * Represents the schema of a tuple, consisting of a list of attributes. * The schema is immutable, and any modifications result in a new Schema instance. */ case class Schema @JsonCreator() ( @JsonProperty(value = "attributes", required = true) attributes: List[Attribute] = List() ) extends Serializable { checkNotNull(attributes) // Maps attribute names (case-insensitive) to their indices in the schema. private val attributeIndex: Map[String, Int] = attributes.view.map(_.getName.toLowerCase).zipWithIndex.toMap def this(attrs: Attribute*) = this(attrs.toList) /** * Returns the list of attributes in the schema. */ @JsonProperty(value = "attributes") def getAttributes: List[Attribute] = attributes /** * Returns a list of all attribute names in the schema. */ @JsonIgnore def getAttributeNames: List[String] = attributes.map(_.getName) /** * Returns the index of a specified attribute by name. * Throws an exception if the attribute is not found. */ def getIndex(attributeName: String): Int = { if (!containsAttribute(attributeName)) { throw new RuntimeException(s"$attributeName is not contained in the schema") } attributeIndex(attributeName.toLowerCase) } /** * Retrieves an attribute by its name. */ def getAttribute(attributeName: String): Attribute = attributes(getIndex(attributeName)) /** * Checks whether the schema contains an attribute with the specified name. */ @JsonIgnore def containsAttribute(attributeName: String): Boolean = attributeIndex.contains(attributeName.toLowerCase) override def hashCode(): Int = { val prime = 31 var result = 1 result = prime * result + (if (attributes == null) 0 else attributes.hashCode) result = prime * result + (if (attributeIndex == null) 0 else attributeIndex.hashCode) result } override def equals(obj: Any): Boolean = { obj match { case that: Schema => this.attributes == that.attributes case _ => false } } override def toString: String = s"Schema[${attributes.map(_.toString).mkString(", ")}]" /** * Creates a new Schema containing only the specified attributes. */ def getPartialSchema(attributeNames: List[String]): Schema = { Schema(attributeNames.map(name => getAttribute(name))) } /** * Converts the schema into a raw format where each attribute name * and attribute type are represented as strings. Useful for serialization across languages. */ def toRawSchema: Map[String, String] = attributes.foldLeft(ListMap[String, String]())((list, attr) => list + (attr.getName -> attr.getType.name()) ) /** * Creates a new Schema by adding multiple attributes to the current schema. * Throws an exception if any attribute name already exists in the schema. */ def add(attributesToAdd: Iterable[Attribute]): Schema = { val existingNames = this.getAttributeNames.map(_.toLowerCase).toSet val duplicateNames = attributesToAdd.map(_.getName.toLowerCase).toSet.intersect(existingNames) if (duplicateNames.nonEmpty) { throw new RuntimeException( s"Cannot add attributes with duplicate names: ${duplicateNames.mkString(", ")}" ) } val newAttributes = attributes ++ attributesToAdd Schema(newAttributes) } /** * Creates a new Schema by adding multiple attributes. * Accepts a variable number of `Attribute` arguments. * Throws an exception if any attribute name already exists in the schema. */ def add(attributes: Attribute*): Schema = { this.add(attributes) } /** * Creates a new Schema by adding a single attribute to the current schema. * Throws an exception if the attribute name already exists in the schema. */ def add(attribute: Attribute): Schema = { if (containsAttribute(attribute.getName)) { throw new RuntimeException( s"Attribute name '${attribute.getName}' already exists in the schema" ) } add(List(attribute)) } /** * Creates a new Schema by adding an attribute with the specified name and type. * Throws an exception if the attribute name already exists in the schema. */ def add(attributeName: String, attributeType: AttributeType): Schema = add(new Attribute(attributeName, attributeType)) /** * Creates a new Schema by merging it with another schema. * Throws an exception if there are duplicate attribute names. */ def add(schema: Schema): Schema = { add(schema.attributes) } /** * Creates a new Schema by removing attributes with the specified names. * Throws an exception if any of the specified attributes do not exist in the schema. */ def remove(attributeNames: Iterable[String]): Schema = { val attributesToRemove = attributeNames.map(_.toLowerCase).toSet // Check for non-existent attributes val nonExistentAttributes = attributesToRemove.diff(attributes.map(_.getName.toLowerCase).toSet) if (nonExistentAttributes.nonEmpty) { throw new IllegalArgumentException( s"Cannot remove non-existent attributes: ${nonExistentAttributes.mkString(", ")}" ) } val remainingAttributes = attributes.filterNot(attr => attributesToRemove.contains(attr.getName.toLowerCase)) Schema(remainingAttributes) } /** * Creates a new Schema by removing a single attribute with the specified name. */ def remove(attributeName: String): Schema = remove(List(attributeName)) } object Schema { /** * Creates a Schema instance from a raw map representation. * Each entry in the map contains an attribute name and its type as strings. */ def fromRawSchema(raw: Map[String, String]): Schema = { Schema(raw.map { case (name, attrType) => new Attribute(name, AttributeType.valueOf(attrType)) }.toList) } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/tuple/Tuple.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.tuple import com.fasterxml.jackson.annotation.{JsonCreator, JsonIgnore, JsonProperty} import com.google.common.base.Preconditions.checkNotNull import org.apache.texera.amber.core.tuple.Tuple.checkSchemaMatchesFields import org.ehcache.sizeof.SizeOf import java.util import scala.collection.mutable class TupleBuildingException(errorMessage: String) extends RuntimeException(errorMessage) {} /** * Represents a tuple in a data processing workflow, encapsulating a schema and corresponding field values. * * A Tuple is a fundamental data structure that holds an ordered collection of elements. Each element can be of any type. * The schema defines the structure of the Tuple, including the names and types of fields that the Tuple can hold. * * @constructor Create a new Tuple with a specified schema and field values. * @param schema The schema associated with this tuple, defining the structure and types of fields in the tuple. * @param fieldVals A list of values corresponding to the fields defined in the schema. Each value in this list * is mapped to a field in the schema, in the same order as the fields are defined. * * @throws java.lang.IllegalArgumentException if either schema or fieldVals is null, ensuring that every Tuple has a well-defined structure. */ case class Tuple @JsonCreator() ( @JsonProperty(value = "schema", required = true) schema: Schema, @JsonProperty(value = "fields", required = true) fieldVals: Array[Any] ) extends SeqTupleLike with Serializable { checkNotNull(schema) checkNotNull(fieldVals) checkSchemaMatchesFields(schema.getAttributes, fieldVals) override val inMemSize: Long = SizeOf.newInstance().deepSizeOf(this) @JsonIgnore def length: Int = fieldVals.length @JsonIgnore def getSchema: Schema = schema def getField[T](index: Int): T = { fieldVals(index).asInstanceOf[T] } def getField[T](attributeName: String): T = { if (!schema.containsAttribute(attributeName)) { throw new RuntimeException(s"$attributeName is not in the tuple") } getField(schema.getIndex(attributeName)) } def getField[T](attribute: Attribute): T = getField(attribute.getName) override def getFields: Array[Any] = fieldVals override def enforceSchema(schema: Schema): Tuple = { assert( getSchema == schema, s"output tuple schema does not match the expected schema! " + s"output schema: $getSchema, " + s"expected schema: $schema" ) this } override def hashCode: Int = util.Arrays.deepHashCode(getFields.map(_.asInstanceOf[AnyRef])) override def equals(obj: Any): Boolean = obj match { case that: Tuple => this.schema == that.schema && this.getFields.zip(that.getFields).forall { case (field1: Array[Byte], field2: Array[Byte]) => field1.sameElements(field2) // for Binary, use sameElements instead of == to compare case (field1, field2) => field1 == field2 } case _ => false } def getPartialTuple(attributeNames: List[String]): Tuple = { val partialSchema = schema.getPartialSchema(attributeNames) val builder = Tuple.Builder(partialSchema) val partialArray = attributeNames.map(getField[Any]).toArray builder.addSequentially(partialArray) builder.build() } override def toString: String = s"Tuple [schema=$schema, fields=${fieldVals.mkString("[", ", ", "]")}]" } object Tuple { /** * Validates that the provided attributes match the provided fields in type and order. * * @param attributes An iterable of Attributes to be validated against the fields. * @param fields An iterable of field values to be validated against the attributes. * @throws RuntimeException if the sizes of attributes and fields do not match, or if their types are incompatible. */ private def checkSchemaMatchesFields( attributes: Iterable[Attribute], fields: Iterable[Any] ): Unit = { val attributeList = attributes.toList val fieldList = fields.toList if (attributeList.size != fieldList.size) { throw new RuntimeException( s"Schema size (${attributeList.size}) and field size (${fieldList.size}) are different" ) } (attributeList zip fieldList).foreach { case (attribute, field) => checkAttributeMatchesField(attribute, field) } } /** * Validates that a single field matches its corresponding attribute in type. * * @param attribute The attribute to be matched. * @param field The field value to be checked. * @throws RuntimeException if the field's type does not match the attribute's defined type. */ private def checkAttributeMatchesField(attribute: Attribute, field: Any): Unit = { if ( field != null && attribute.getType != AttributeType.ANY && !field.getClass.equals( attribute.getType.getFieldClass ) ) { throw new RuntimeException( s"Attribute ${attribute.getName}'s type (${attribute.getType}) is different from field's type (${AttributeType .getAttributeType(field.getClass)})" ) } } /** * Creates a new Tuple builder for a specified schema. * * @param schema The schema for which the Tuple builder will create Tuples. * @return A new instance of Tuple.Builder configured with the specified schema. */ def builder(schema: Schema): Builder = { Tuple.Builder(schema) } /** * Builder class for constructing Tuple instances in a flexible and controlled manner. */ case class Builder(schema: Schema) { private val fieldNameMap = mutable.Map.empty[String, Any] def add(tuple: Tuple, isStrictSchemaMatch: Boolean = true): Builder = { require(tuple != null, "Tuple cannot be null") tuple.getFields.zipWithIndex.foreach { case (field, i) => val attribute = tuple.schema.getAttributes(i) if (!isStrictSchemaMatch && !schema.containsAttribute(attribute.getName)) { // Skip if not matching in non-strict mode } else { add(attribute, tuple.getFields(i)) } } this } def add(attribute: Attribute, field: Any): Builder = { require(attribute != null, "Attribute cannot be null") checkAttributeMatchesField(attribute, field) if (!schema.containsAttribute(attribute.getName)) { throw new TupleBuildingException( s"${attribute.getName} doesn't exist in the expected schema." ) } fieldNameMap.put(attribute.getName.toLowerCase, field) this } def add(attributeName: String, attributeType: AttributeType, field: Any): Builder = { require( attributeName != null && attributeType != null, "Attribute name and type cannot be null" ) this.add(new Attribute(attributeName, attributeType), field) this } def addSequentially(fields: Array[Any]): Builder = { require(fields != null, "Fields cannot be null") checkSchemaMatchesFields(schema.getAttributes, fields) schema.getAttributes.zip(fields).foreach { case (attribute, field) => this.add(attribute, field) } this } def build(): Tuple = { val missingAttributes = schema.getAttributes.filterNot(attr => fieldNameMap.contains(attr.getName.toLowerCase)) if (missingAttributes.nonEmpty) { throw new TupleBuildingException( s"Tuple does not have the same number of attributes as schema. Missing attributes are $missingAttributes" ) } val fields = schema.getAttributes.map(attr => fieldNameMap(attr.getName.toLowerCase)).toArray new Tuple(schema, fields) } } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/tuple/TupleLike.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.tuple import org.apache.texera.amber.core.workflow.PortIdentity import scala.jdk.CollectionConverters.CollectionHasAsScala sealed trait FieldArray { def getFields: Array[Any] } sealed trait TupleLike extends FieldArray { def inMemSize: Long = 0L } trait SchemaEnforceable { def enforceSchema(schema: Schema): Tuple } trait InternalMarker extends TupleLike { override def getFields: Array[Any] = Array.empty } final case class FinalizePort(portId: PortIdentity, input: Boolean) extends InternalMarker final case class FinalizeExecutor() extends InternalMarker trait SeqTupleLike extends TupleLike with SchemaEnforceable { override def inMemSize: Long = ??? /** * Constructs a Tuple object from a sequence of field values * according to the specified schema. It asserts that the number * of provided fields matches the schema's requirement, every * field must also satisfy the field type. * * @param schema Schema for Tuple construction. * @return Tuple constructed according to the schema. */ override def enforceSchema(schema: Schema): Tuple = { val attributes = schema.getAttributes val builder = Tuple.builder(schema) getFields.zipWithIndex.foreach { case (value, i) => builder.add(attributes(i), value) } builder.build() } } trait MapTupleLike extends SeqTupleLike with SchemaEnforceable { override def inMemSize: Long = ??? def fieldMappings: Map[String, Any] override def getFields: Array[Any] = fieldMappings.values.toArray /** * Constructs a `Tuple` based on the provided schema and `tupleLike` object. * * For each attribute in the schema, the function attempts to find a corresponding value * in the tuple-like object's field mappings. If a mapping is found, that value is used; * otherwise, `null` is used as the attribute value in the built tuple. * * @param schema The schema defining the attributes and their types for the tuple. * @return A new `Tuple` instance built according to the schema and the data provided * by the `tupleLike` object. */ override def enforceSchema(schema: Schema): Tuple = { val builder = Tuple.builder(schema) schema.getAttributes.foreach { attribute => val value = fieldMappings.getOrElse(attribute.getName, null) builder.add(attribute, value) } builder.build() } } object TupleLike { // Implicit evidence markers for different types trait NotAnIterable[A] // Provide a low-priority implicit evidence for all types that are not Iterable trait LowPriorityNotAnIterableImplicits { implicit def defaultNotAnIterable[A]: NotAnIterable[A] = new NotAnIterable[A] {} } // Object to hold the implicits object NotAnIterable extends LowPriorityNotAnIterableImplicits { // Prioritize this implicit for Strings, allowing them explicitly implicit object StringIsNotAnIterable extends NotAnIterable[String] // Ensure Iterable types do not have an implicit NotAnIterable available // This is a way to "exclude" Iterable types by not providing an implicit instance for them implicit def iterableIsNotAnIterable[C[_] <: Iterable[A], A]: NotAnIterable[C[A]] = throw new RuntimeException("Iterable types are not allowed") } def apply(mappings: Map[String, Any]): MapTupleLike = { new MapTupleLike { override val fieldMappings: Map[String, Any] = mappings } } def apply(mappings: Iterable[(String, Any)]): MapTupleLike = { new MapTupleLike { override val fieldMappings: Map[String, Any] = mappings.toMap } } def apply(mappings: (String, Any)*): MapTupleLike = { new MapTupleLike { override val fieldMappings: Map[String, Any] = mappings.toMap } } def apply(fieldList: java.util.List[Any]): SeqTupleLike = { new SeqTupleLike { override val getFields: Array[Any] = fieldList.asScala.toArray } } def apply[T: NotAnIterable](fieldSeq: T*)(implicit ev: NotAnIterable[_] = null): SeqTupleLike = { new SeqTupleLike { override val getFields: Array[Any] = fieldSeq.toArray } } def apply[T <: Any](fieldIter: Iterable[T]): SeqTupleLike = { new SeqTupleLike { override val getFields: Array[Any] = fieldIter.toArray } } def apply(array: Array[Any]): SeqTupleLike = { new SeqTupleLike { override val getFields: Array[Any] = array } } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/tuple/TupleUtils.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.tuple import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.ObjectNode import org.apache.texera.amber.core.tuple.AttributeTypeUtils.{inferSchemaFromRows, parseField} import org.apache.texera.amber.util.JSONUtils import org.apache.texera.amber.util.JSONUtils.{JSONToMap, objectMapper} import scala.collection.mutable.ArrayBuffer object TupleUtils { def tuple2json(schema: Schema, fieldVals: Array[Any]): ObjectNode = { val objectNode = JSONUtils.objectMapper.createObjectNode() schema.getAttributeNames.foreach { attrName => val valueNode = JSONUtils.objectMapper.convertValue(fieldVals(schema.getIndex(attrName)), classOf[JsonNode]) objectNode.set[ObjectNode](attrName, valueNode) } objectNode } def json2tuple(json: String): Tuple = { var fieldNames = Set[String]() val allFields: ArrayBuffer[Map[String, String]] = ArrayBuffer() val root: JsonNode = objectMapper.readTree(json) if (root.isObject) { val fields: Map[String, String] = JSONToMap(root) fieldNames = fieldNames.++(fields.keySet) allFields += fields } val sortedFieldNames = fieldNames.toList val attributeTypes = inferSchemaFromRows(allFields.iterator.map(fields => { val result = ArrayBuffer[Object]() for (fieldName <- sortedFieldNames) { if (fields.contains(fieldName)) { result += fields(fieldName) } else { result += null } } result.toArray })) val schema = Schema( sortedFieldNames.indices .map(i => new Attribute(sortedFieldNames(i), attributeTypes(i))) .toList ) try { val fields = scala.collection.mutable.ArrayBuffer.empty[Any] val data = JSONToMap(objectMapper.readTree(json)) for (fieldName <- schema.getAttributeNames) { if (data.contains(fieldName)) { fields += parseField(data(fieldName), schema.getAttribute(fieldName).getType) } else { fields += null } } Tuple.builder(schema).addSequentially(fields.toArray).build() } catch { case e: Exception => throw e } } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/workflow/ExecutionMode.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.workflow; public enum ExecutionMode { PIPELINED, MATERIALIZED; public static ExecutionMode fromString(String value) { return valueOf(value); } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/workflow/LocationPreference.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.workflow // LocationPreference defines where operators should run. sealed trait LocationPreference extends Serializable // PreferController: Run on the controller node. // Example: For scan operators reading files. object PreferController extends LocationPreference // RoundRobinPreference: Distribute across worker nodes, per operator. // Example: // - Operator A: Worker 1 -> Node 1, Worker 2 -> Node 2, Worker 3 -> Node 3 // - Operator B: Worker 1 -> Node 1, Worker 2 -> Node 2 object RoundRobinPreference extends LocationPreference ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/workflow/PartitionInfo.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.workflow import com.fasterxml.jackson.annotation.JsonSubTypes.Type import com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo} /** * The base interface of partition information in the compiler layer. */ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") @JsonSubTypes( Array( new Type(value = classOf[HashPartition], name = "hash"), new Type(value = classOf[RangePartition], name = "range"), new Type(value = classOf[SinglePartition], name = "single"), new Type(value = classOf[BroadcastPartition], name = "broadcast"), new Type(value = classOf[UnknownPartition], name = "none") ) ) sealed abstract class PartitionInfo { // whether this partition satisfies the other partition // in the default implementation, a partition only satisfies itself, // a partition also always satisfy unknown partition (indicating no partition requirement) def satisfies(other: PartitionInfo): Boolean = { this == other || other == UnknownPartition() } // after a stream with this partition merges with another stream with the other partition // returns the the result partition after the merge def merge(other: PartitionInfo): PartitionInfo = { // if merge with the same partition, the result is the same // if merge with a different partition, the result is unknown if (this == other) this else UnknownPartition() } } /** * Defines a partitioning strategy where an input stream is distributed across * multiple nodes based on a hash function applied to specified attribute names. * If the list of attribute names is empty, hashing is applied to all attributes. */ final case class HashPartition(hashAttributeNames: List[String] = List.empty) extends PartitionInfo object RangePartition { def apply(rangeAttributeNames: List[String], rangeMin: Long, rangeMax: Long): PartitionInfo = { if (rangeAttributeNames.nonEmpty) new RangePartition(rangeAttributeNames, rangeMin, rangeMax) else UnknownPartition() } } /** * Represents an input stream is partitioned on multiple nodes * and each node contains data fit in a specific range. * The data within each node is also sorted. */ final case class RangePartition(rangeAttributeNames: List[String], rangeMin: Long, rangeMax: Long) extends PartitionInfo { // if two streams of input with the same range partition are merged (without another sort), // we cannot ensure that the output stream follow the same sorting order. override def merge(other: PartitionInfo): PartitionInfo = { UnknownPartition() } } /** * Represent the input stream is not partitioned and all data are on a single node. */ final case class SinglePartition() extends PartitionInfo {} /** * Represents an input stream that is partitioned one-to-one between nodes, where each node processes a unique subset of the data. */ final case class OneToOnePartition() extends PartitionInfo {} /** * Represents the input stream needs to send to every node */ final case class BroadcastPartition() extends PartitionInfo {} /** * Represents there is no specific partitioning scheme of the input stream. */ final case class UnknownPartition() extends PartitionInfo {} ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/workflow/PhysicalOp.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.workflow import com.fasterxml.jackson.annotation.{JsonIgnore, JsonIgnoreProperties} import com.typesafe.scalalogging.LazyLogging import org.apache.texera.amber.core.executor.{OpExecInitInfo, OpExecWithCode} import org.apache.texera.amber.core.tuple.Schema import org.apache.texera.amber.core.virtualidentity.{ ExecutionIdentity, OperatorIdentity, PhysicalOpIdentity, WorkflowIdentity } import org.jgrapht.graph.{DefaultEdge, DirectedAcyclicGraph} import org.jgrapht.traverse.TopologicalOrderIterator import scala.collection.mutable.ArrayBuffer import scala.util.{Failure, Success, Try} case object SchemaPropagationFunc { private type JavaSchemaPropagationFunc = java.util.function.Function[Map[PortIdentity, Schema], Map[PortIdentity, Schema]] with java.io.Serializable def apply(javaFunc: JavaSchemaPropagationFunc): SchemaPropagationFunc = SchemaPropagationFunc(inputSchemas => javaFunc.apply(inputSchemas)) } case class SchemaPropagationFunc(func: Map[PortIdentity, Schema] => Map[PortIdentity, Schema]) class SchemaNotAvailableException(message: String) extends Exception(message) object PhysicalOp { /** all source operators should use sourcePhysicalOp to give the following configs: * 1) it initializes at the controller jvm. * 2) it only has 1 worker actor. * 3) it has no input ports. */ def sourcePhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity, logicalOpId: OperatorIdentity, opExecInitInfo: OpExecInitInfo ): PhysicalOp = sourcePhysicalOp( PhysicalOpIdentity(logicalOpId, "main"), workflowId, executionId, opExecInitInfo ) def sourcePhysicalOp( physicalOpId: PhysicalOpIdentity, workflowId: WorkflowIdentity, executionId: ExecutionIdentity, opExecInitInfo: OpExecInitInfo ): PhysicalOp = PhysicalOp( physicalOpId, workflowId, executionId, opExecInitInfo, parallelizable = false, locationPreference = Some(PreferController) ) def oneToOnePhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity, logicalOpId: OperatorIdentity, opExecInitInfo: OpExecInitInfo ): PhysicalOp = oneToOnePhysicalOp( PhysicalOpIdentity(logicalOpId, "main"), workflowId, executionId, opExecInitInfo ) def oneToOnePhysicalOp( physicalOpId: PhysicalOpIdentity, workflowId: WorkflowIdentity, executionId: ExecutionIdentity, opExecInitInfo: OpExecInitInfo ): PhysicalOp = PhysicalOp(physicalOpId, workflowId, executionId, opExecInitInfo) def manyToOnePhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity, logicalOpId: OperatorIdentity, opExecInitInfo: OpExecInitInfo ): PhysicalOp = manyToOnePhysicalOp( PhysicalOpIdentity(logicalOpId, "main"), workflowId, executionId, opExecInitInfo ) def manyToOnePhysicalOp( physicalOpId: PhysicalOpIdentity, workflowId: WorkflowIdentity, executionId: ExecutionIdentity, opExecInitInfo: OpExecInitInfo ): PhysicalOp = { PhysicalOp( physicalOpId, workflowId, executionId, opExecInitInfo, parallelizable = false, partitionRequirement = List(Option(SinglePartition())), derivePartition = _ => SinglePartition() ) } def localPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity, logicalOpId: OperatorIdentity, opExecInitInfo: OpExecInitInfo ): PhysicalOp = localPhysicalOp( PhysicalOpIdentity(logicalOpId, "main"), workflowId, executionId, opExecInitInfo ) def localPhysicalOp( physicalOpId: PhysicalOpIdentity, workflowId: WorkflowIdentity, executionId: ExecutionIdentity, opExecInitInfo: OpExecInitInfo ): PhysicalOp = { manyToOnePhysicalOp(physicalOpId, workflowId, executionId, opExecInitInfo) .withLocationPreference(Some(PreferController)) } } // In Scala case classes, @JsonIgnore on constructor parameters is not recognized by Jackson. // Use @JsonIgnoreProperties at the class level instead. @JsonIgnoreProperties( Array( "opExecInitInfo", // function type, ignore it "derivePartition", // function type, ignore it "inputPorts", // may contain very long stacktrace, ignore it "outputPorts", // same reason with above "propagateSchema", // function type, so ignore it "locationPreference", // ignore it for the deserialization "partitionRequirement" // ignore it for deserialization ) ) case class PhysicalOp( // the identifier of this PhysicalOp id: PhysicalOpIdentity, // the workflow id number workflowId: WorkflowIdentity, // the execution id number executionId: ExecutionIdentity, // information regarding initializing an operator executor instance opExecInitInfo: OpExecInitInfo, // preference of parallelism parallelizable: Boolean = true, // preference of worker placement locationPreference: Option[LocationPreference] = None, // requirement of partition policy (hash/range/single/none) on inputs partitionRequirement: List[Option[PartitionInfo]] = List(), // derive the output partition info given the input partitions // if not specified, by default the output partition is the same as input partition derivePartition: List[PartitionInfo] => PartitionInfo = inputParts => inputParts.head, // input/output ports of the physical operator // for operators with multiple input/output ports: must set these variables properly inputPorts: Map[PortIdentity, (InputPort, List[PhysicalLink], Either[Throwable, Schema])] = Map.empty, outputPorts: Map[PortIdentity, (OutputPort, List[PhysicalLink], Either[Throwable, Schema])] = Map.empty, // schema propagation function propagateSchema: SchemaPropagationFunc = SchemaPropagationFunc(schemas => schemas), isOneToManyOp: Boolean = false, // hint for number of workers suggestedWorkerNum: Option[Int] = None ) extends LazyLogging { // all the "dependee" links are also blocking lazy val dependeeInputs: List[PortIdentity] = inputPorts.values .flatMap({ case (port, _, _) => port.dependencies }) .toList .distinct /** * Helper functions related to compile-time operations */ @JsonIgnore def isSourceOperator: Boolean = { inputPorts.isEmpty } @JsonIgnore // this is needed to prevent the serialization issue def isPythonBased: Boolean = { opExecInitInfo match { case OpExecWithCode(_, language) => language == "python" || language == "r-tuple" || language == "r-table" case _ => false } } @JsonIgnore def getCode: String = { opExecInitInfo match { case OpExecWithCode(code, _) => code case _ => throw new IllegalAccessError("No code information in this physical operator") } } /** * creates a copy with the location preference information */ def withLocationPreference(preference: Option[LocationPreference]): PhysicalOp = { this.copy(locationPreference = preference) } /** * Creates a copy of the PhysicalOp with the specified input ports. Each input port is associated * with an empty list of links and a None schema, reflecting the absence of predefined connections * and schema information. * * @param inputs A list of InputPort instances to set as the new input ports. * @return A new instance of PhysicalOp with the input ports updated. */ def withInputPorts(inputs: List[InputPort]): PhysicalOp = { this.copy(inputPorts = inputs .map(input => input.id -> (input, List .empty[PhysicalLink], Left(new SchemaNotAvailableException("schema is not available"))) ) .toMap ) } /** * Creates a copy of the PhysicalOp with the specified output ports. Each output port is * initialized with an empty list of links and a None schema, indicating * the absence of outbound connections and schema details at this stage. * * @param outputs A list of OutputPort instances to set as the new output ports. * @return A new instance of PhysicalOp with the output ports updated. */ def withOutputPorts(outputs: List[OutputPort]): PhysicalOp = { this.copy(outputPorts = outputs .map(output => output.id -> (output, List .empty[PhysicalLink], Left(new SchemaNotAvailableException("schema is not available"))) ) .toMap ) } /** * creates a copy with suggested worker number. This is only to be used by Python UDF operators. */ def withSuggestedWorkerNum(workerNum: Int): PhysicalOp = { this.copy(suggestedWorkerNum = Some(workerNum)) } /** * creates a copy with the partition requirements */ def withPartitionRequirement(partitionRequirements: List[Option[PartitionInfo]]): PhysicalOp = { this.copy(partitionRequirement = partitionRequirements) } /** * creates a copy with the partition info derive function */ def withDerivePartition(derivePartition: List[PartitionInfo] => PartitionInfo): PhysicalOp = { this.copy(derivePartition = derivePartition) } /** * creates a copy with the parallelizable specified */ def withParallelizable(parallelizable: Boolean): PhysicalOp = this.copy(parallelizable = parallelizable) /** * creates a copy with the specified property that whether this operator is one-to-many */ def withIsOneToManyOp(isOneToManyOp: Boolean): PhysicalOp = this.copy(isOneToManyOp = isOneToManyOp) /** * Creates a copy of the PhysicalOp with the schema of a specified input port updated. * The schema can either be a successful schema definition or an error represented as a Throwable. * * @param portId The identity of the port to update. * @param schema The new schema, or error, to be associated with the port, encapsulated within an Either. * A Right value represents a successful schema, while a Left value represents an error (Throwable). * @return A new instance of PhysicalOp with the updated input port schema or error information. */ private def withInputSchema( portId: PortIdentity, schema: Either[Throwable, Schema] ): PhysicalOp = { this.copy(inputPorts = inputPorts.updatedWith(portId) { case Some((port, links, _)) => Some((port, links, schema)) case None => None }) } /** * Creates a copy of the PhysicalOp with the schema of a specified output port updated. * Similar to `withInputSchema`, the schema can either represent a successful schema definition * or an error, encapsulated as an Either type. * * @param portId The identity of the port to update. * @param schema The new schema, or error, to be associated with the port, encapsulated within an Either. * A Right value indicates a successful schema, while a Left value indicates an error (Throwable). * @return A new instance of PhysicalOp with the updated output port schema or error information. */ private def withOutputSchema( portId: PortIdentity, schema: Either[Throwable, Schema] ): PhysicalOp = { this.copy(outputPorts = outputPorts.updatedWith(portId) { case Some((port, links, _)) => Some((port, links, schema)) case None => None }) } /** * creates a copy with the schema propagation function. */ def withPropagateSchema(func: SchemaPropagationFunc): PhysicalOp = { this.copy(propagateSchema = func) } /** * creates a copy with an additional input link specified on an input port */ def addInputLink(link: PhysicalLink): PhysicalOp = { assert(link.toOpId == id) assert(inputPorts.contains(link.toPortId)) val (port, existingLinks, schema) = inputPorts(link.toPortId) val newLinks = existingLinks :+ link this.copy( inputPorts = inputPorts + (link.toPortId -> (port, newLinks, schema)) ) } /** * creates a copy with an additional output link specified on an output port */ def addOutputLink(link: PhysicalLink): PhysicalOp = { assert(link.fromOpId == id) assert(outputPorts.contains(link.fromPortId)) val (port, existingLinks, schema) = outputPorts(link.fromPortId) val newLinks = existingLinks :+ link this.copy( outputPorts = outputPorts + (link.fromPortId -> (port, newLinks, schema)) ) } /** * creates a copy with a removed input link */ def removeInputLink(linkToRemove: PhysicalLink): PhysicalOp = { val portId = linkToRemove.toPortId val (port, existingLinks, schema) = inputPorts(portId) this.copy( inputPorts = inputPorts + (portId -> (port, existingLinks.filter(link => link != linkToRemove), schema)) ) } /** * creates a copy with a removed output link */ def removeOutputLink(linkToRemove: PhysicalLink): PhysicalOp = { val portId = linkToRemove.fromPortId val (port, existingLinks, schema) = outputPorts(portId) this.copy( outputPorts = outputPorts + (portId -> (port, existingLinks.filter(link => link != linkToRemove), schema)) ) } /** * creates a copy with an input schema updated, and if all input schemas are available, propagate * the schema change to output schemas. * @param newInputSchema optionally provide a schema for an input port. */ def propagateSchema(newInputSchema: Option[(PortIdentity, Schema)] = None): PhysicalOp = { // Update the input schema if a new one is provided val updatedOp = newInputSchema.foldLeft(this) { (op, schemaEntry) => val (portId, schema) = schemaEntry op.inputPorts(portId)._3 match { case Left(_) => op.withInputSchema(portId, Right(schema)) case Right(existingSchema) if existingSchema != schema => throw new IllegalArgumentException( s"Conflict schemas received on port ${portId.id}, $existingSchema != $schema" ) case _ => op } } // Extract input schemas, checking if all are defined val inputSchemas = updatedOp.inputPorts.collect { case (portId, (_, _, Right(schema))) => portId -> schema } if (updatedOp.inputPorts.size == inputSchemas.size) { // All input schemas are available, propagate to output schema val schemaPropagationResult = Try(propagateSchema.func(inputSchemas)) schemaPropagationResult match { case Success(schemaMapping) => schemaMapping.foldLeft(updatedOp) { case (op, (portId, schema)) => op.withOutputSchema(portId, Right(schema)) } case Failure(exception) => // apply the exception to all output ports in case of failure updatedOp.outputPorts.keys.foldLeft(updatedOp) { (op, portId) => op.withOutputSchema(portId, Left(exception)) } } } else { // Not all input schemas are defined, return the updated operation without changes updatedOp } } /** * returns all output links. Optionally, if a specific portId is provided, returns the links connected to that portId. */ def getOutputLinks(portId: PortIdentity): List[PhysicalLink] = { outputPorts.values .flatMap(_._2) .filter(link => link.fromPortId == portId) .toList } /** * returns all input links. Optionally, if a specific portId is provided, returns the links connected to that portId. */ def getInputLinks(portIdOpt: Option[PortIdentity] = None): List[PhysicalLink] = { inputPorts.values .flatMap(_._2) .toList .filter(link => portIdOpt match { case Some(portId) => link.toPortId == portId case None => true } ) } /** * Tells whether the input port the link connects to is depended by another input . */ def isInputLinkDependee(link: PhysicalLink): Boolean = { dependeeInputs.contains(link.toPortId) } /** * Tells whether the output on this link is blocking i.e. the operator doesn't output anything till this link * outputs all its tuples. */ def isOutputLinkBlocking(link: PhysicalLink): Boolean = { this.outputPorts(link.fromPortId)._1.blocking } /** * Some operators process their inputs in a particular order. Eg: 2 phase hash join first * processes the build input, then the probe input. */ @JsonIgnore def getInputPortDependencyPairs: List[PortIdentity] = { val dependencyDag = { new DirectedAcyclicGraph[PortIdentity, DefaultEdge](classOf[DefaultEdge]) } inputPorts.values .map(_._1) .flatMap(port => port.dependencies.map(dependee => port.id -> dependee)) .foreach({ case (depender: PortIdentity, dependee: PortIdentity) => if (!dependencyDag.containsVertex(dependee)) { dependencyDag.addVertex(dependee) } if (!dependencyDag.containsVertex(depender)) { dependencyDag.addVertex(depender) } dependencyDag.addEdge(dependee, depender) }) val topologicalIterator = new TopologicalOrderIterator[PortIdentity, DefaultEdge](dependencyDag) val processingOrder = new ArrayBuffer[PortIdentity]() while (topologicalIterator.hasNext) { processingOrder.append(topologicalIterator.next()) } processingOrder.toList } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/workflow/PhysicalPlan.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.workflow import com.fasterxml.jackson.annotation.JsonIgnore import com.typesafe.scalalogging.LazyLogging import org.apache.texera.amber.core.tuple.Schema import org.apache.texera.amber.core.virtualidentity.{ ActorVirtualIdentity, OperatorIdentity, PhysicalOpIdentity } import org.apache.texera.amber.util.VirtualIdentityUtils import org.jgrapht.alg.connectivity.BiconnectivityInspector import org.jgrapht.alg.shortestpath.AllDirectedPaths import org.jgrapht.graph.DirectedAcyclicGraph import org.jgrapht.traverse.TopologicalOrderIterator import org.jgrapht.util.SupplierUtil import scala.jdk.CollectionConverters.{CollectionHasAsScala, IteratorHasAsScala} case class PhysicalPlan( operators: Set[PhysicalOp], links: Set[PhysicalLink] ) extends LazyLogging { @transient private lazy val operatorMap: Map[PhysicalOpIdentity, PhysicalOp] = operators.map(o => (o.id, o)).toMap // the dag will be re-computed again once it reaches the coordinator. @transient lazy val dag: DirectedAcyclicGraph[PhysicalOpIdentity, PhysicalLink] = { val jgraphtDag = new DirectedAcyclicGraph[PhysicalOpIdentity, PhysicalLink]( null, // vertexSupplier SupplierUtil.createSupplier(classOf[PhysicalLink]), // edgeSupplier false, // weighted true // allowMultipleEdges ) operatorMap.foreach(op => jgraphtDag.addVertex(op._1)) links.foreach(l => jgraphtDag.addEdge(l.fromOpId, l.toOpId, l)) jgraphtDag } @transient lazy val maxChains: Set[Set[PhysicalLink]] = this.getMaxChains @JsonIgnore def getSourceOperatorIds: Set[PhysicalOpIdentity] = operatorMap.keys.filter(op => dag.inDegreeOf(op) == 0).toSet def getPhysicalOpsOfLogicalOp(logicalOpId: OperatorIdentity): List[PhysicalOp] = { topologicalIterator() .filter(physicalOpId => physicalOpId.logicalOpId == logicalOpId) .map(physicalOpId => getOperator(physicalOpId)) .toList } def getOperator(physicalOpId: PhysicalOpIdentity): PhysicalOp = operatorMap(physicalOpId) /** * returns a sub-plan that contains the specified operators and the links connected within these operators */ def getSubPlan(subOperators: Set[PhysicalOpIdentity]): PhysicalPlan = { val newOps = operators.filter(op => subOperators.contains(op.id)) val newLinks = links.filter(link => subOperators.contains(link.fromOpId) && subOperators.contains(link.toOpId) ) PhysicalPlan(newOps, newLinks) } def getUpstreamPhysicalOpIds(physicalOpId: PhysicalOpIdentity): Set[PhysicalOpIdentity] = { dag.incomingEdgesOf(physicalOpId).asScala.map(e => dag.getEdgeSource(e)).toSet } def getUpstreamPhysicalLinks(physicalOpId: PhysicalOpIdentity): Set[PhysicalLink] = { links.filter(l => l.toOpId == physicalOpId) } def getDownstreamPhysicalLinks(physicalOpId: PhysicalOpIdentity): Set[PhysicalLink] = { links.filter(l => l.fromOpId == physicalOpId) } def topologicalIterator(): Iterator[PhysicalOpIdentity] = { new TopologicalOrderIterator(dag).asScala } /** * Computes the reverse topological layering of the DAG. * Each layer contains a set of operators with the same "distance" from the sinks. * This version correctly handles cases where multiple edges exist between nodes. */ lazy val layeredReversedTopologicalOrder: Seq[Set[PhysicalOpIdentity]] = { // Track the number of remaining outgoing edges for each node val remainingSuccessors = scala.collection.mutable.Map[PhysicalOpIdentity, Int]() dag.vertexSet().forEach { v => remainingSuccessors(v) = dag.outgoingEdgesOf(v).size() } // Initialize with sink nodes (those with zero outgoing edges) var currentLayer = remainingSuccessors.collect { case (v, 0) => v }.toSet currentLayer.foreach(remainingSuccessors.remove) // Accumulate layers from sink to source val layers = scala.collection.mutable.ArrayBuffer.empty[Set[PhysicalOpIdentity]] while (currentLayer.nonEmpty) { layers.append(currentLayer) val nextLayer = scala.collection.mutable.Set[PhysicalOpIdentity]() for (node <- currentLayer) { val incomingEdges = dag.incomingEdgesOf(node) incomingEdges.forEach { edge => val pred = dag.getEdgeSource(edge) if (remainingSuccessors.contains(pred)) { remainingSuccessors(pred) -= 1 if (remainingSuccessors(pred) == 0) { nextLayer += pred remainingSuccessors.remove(pred) } } } } currentLayer = nextLayer.toSet } layers.toSeq } def addOperator(physicalOp: PhysicalOp): PhysicalPlan = { this.copy(operators = Set(physicalOp) ++ operators) } def addLink(link: PhysicalLink): PhysicalPlan = { val formOp = operatorMap(link.fromOpId) val (_, _, outputSchema) = formOp.outputPorts(link.fromPortId) val newFromOp = formOp.addOutputLink(link) val newToOp = getOperator(link.toOpId) .addInputLink(link) .propagateSchema(outputSchema.toOption.map(schema => (link.toPortId, schema))) val newOperators = operatorMap + (link.fromOpId -> newFromOp) + (link.toOpId -> newToOp) this.copy(newOperators.values.toSet, links ++ Set(link)) } def removeLink( link: PhysicalLink ): PhysicalPlan = { val fromOpId = link.fromOpId val toOpId = link.toOpId val newOperators = operatorMap + (fromOpId -> getOperator(fromOpId).removeOutputLink(link)) + (toOpId -> getOperator(toOpId).removeInputLink(link)) this.copy(operators = newOperators.values.toSet, links.filter(l => l != link)) } def setOperator(physicalOp: PhysicalOp): PhysicalPlan = { this.copy(operators = (operatorMap + (physicalOp.id -> physicalOp)).values.toSet) } @JsonIgnore def getPhysicalOpByWorkerId(workerId: ActorVirtualIdentity): PhysicalOp = getOperator(VirtualIdentityUtils.getPhysicalOpId(workerId)) @JsonIgnore def getLinksBetween( from: PhysicalOpIdentity, to: PhysicalOpIdentity ): Set[PhysicalLink] = { links.filter(link => link.fromOpId == from && link.toOpId == to) } @JsonIgnore def getOutputPartitionInfo( link: PhysicalLink, upstreamPartitionInfo: PartitionInfo, opToWorkerNumberMapping: Map[PhysicalOpIdentity, Int] ): PartitionInfo = { val fromPhysicalOp = getOperator(link.fromOpId) val toPhysicalOp = getOperator(link.toOpId) // make sure this input is connected to this port assert( toPhysicalOp .getInputLinks(Some(link.toPortId)) .map(link => link.fromOpId) .contains(fromPhysicalOp.id) ) // partition requirement of this PhysicalOp on this input port val requiredPartitionInfo = toPhysicalOp.partitionRequirement .lift(link.toPortId.id) .flatten .getOrElse(UnknownPartition()) // the upstream partition info satisfies the requirement, and number of worker match if ( upstreamPartitionInfo.satisfies(requiredPartitionInfo) && opToWorkerNumberMapping.getOrElse( fromPhysicalOp.id, 0 ) == opToWorkerNumberMapping.getOrElse(toPhysicalOp.id, 0) ) { upstreamPartitionInfo } else { // we must re-distribute the input partitions requiredPartitionInfo } } @JsonIgnore def getBlockingAndDependeeLinks: Set[PhysicalLink] = { operators .flatMap { physicalOp => { getUpstreamPhysicalOpIds(physicalOp.id) .flatMap { upstreamPhysicalOpId => links .filter(link => link.fromOpId == upstreamPhysicalOpId && link.toOpId == physicalOp.id ) .filter(link => getOperator(physicalOp.id).isInputLinkDependee( link ) || getOperator(upstreamPhysicalOpId).isOutputLinkBlocking(link) ) } } } } @JsonIgnore def getDependeeLinks: Set[PhysicalLink] = { operators .flatMap { physicalOp => { getUpstreamPhysicalOpIds(physicalOp.id) .flatMap { upstreamPhysicalOpId => links .filter(link => link.fromOpId == upstreamPhysicalOpId && link.toOpId == physicalOp.id ) .filter(link => getOperator(physicalOp.id).isInputLinkDependee(link)) } } } } /** * create a DAG similar to the physical DAG but with all dependee links removed. */ @JsonIgnore // this is needed to prevent the serialization issue def getDependeeLinksRemovedDAG: PhysicalPlan = { this.copy(operators, links.diff(getDependeeLinks)) } /** * A link is a bridge if removal of that link would increase the number of (weakly) connected components in the DAG. * Assuming pipelining a link is more desirable than materializing it, and optimal physical plan always pipelines * a bridge. We can thus use bridges to optimize the process of searching for an optimal physical plan. * * @return All non-blocking links that are not bridges. */ @JsonIgnore def getNonBridgeNonBlockingLinks: Set[PhysicalLink] = { val bridges = new BiconnectivityInspector[PhysicalOpIdentity, PhysicalLink](this.dag).getBridges.asScala .map { edge => { val fromOpId = this.dag.getEdgeSource(edge) val toOpId = this.dag.getEdgeTarget(edge) links.find(l => l.fromOpId == fromOpId && l.toOpId == toOpId) } } .flatMap(_.toList) this.links.diff(getBlockingAndDependeeLinks).diff(bridges.toSet) } /** * A chain in a physical plan is a path such that each of its operators (except the first and the last operators) * is connected only to operators on the path. Assuming pipelining a link is more desirable than materializations, * and optimal physical plan has at most one link on each chain. We can thus use chains to optimize the process of * searching for an optimal physical plan. A maximal chain is a chain that is not a sub-path of any other chain. * A maximal chain can cover the optimizations of all its sub-chains, so finding only maximal chains is adequate for * optimization purposes. Note the definition of a chain has nothing to do with that of a connected component. * * @return All the maximal chains of this physical plan, where each chain is represented as a set of links. */ private def getMaxChains: Set[Set[PhysicalLink]] = { val dijkstra = new AllDirectedPaths[PhysicalOpIdentity, PhysicalLink](this.dag) val chains = this.dag .vertexSet() .asScala .flatMap { ancestor => { this.dag.getDescendants(ancestor).asScala.flatMap { descendant => { dijkstra .getAllPaths(ancestor, descendant, true, Integer.MAX_VALUE) .asScala .filter(path => path.getLength > 1 && path.getVertexList.asScala .filter(v => v != path.getStartVertex && v != path.getEndVertex) .forall(v => this.dag.inDegreeOf(v) == 1 && this.dag.outDegreeOf(v) == 1) ) .map(path => path.getEdgeList.asScala .map { edge => { val fromOpId = this.dag.getEdgeSource(edge) val toOpId = this.dag.getEdgeTarget(edge) links.find(l => l.fromOpId == fromOpId && l.toOpId == toOpId) } } .flatMap(_.toList) .toSet ) .toSet } } } } chains.filter(s1 => chains.forall(s2 => s1 == s2 || !s1.subsetOf(s2))).toSet } def propagateSchema(inputSchemas: Map[PortIdentity, Schema]): PhysicalPlan = { var physicalPlan = PhysicalPlan(operators = Set.empty, links = Set.empty) this .topologicalIterator() .map(this.getOperator) .foreach({ physicalOp => { val propagatedPhysicalOp = physicalOp.inputPorts.keys.foldLeft(physicalOp) { (op, inputPortId) => op.propagateSchema(inputSchemas.get(inputPortId).map(schema => (inputPortId, schema))) } // Add the operator to the physical plan physicalPlan = physicalPlan.addOperator(propagatedPhysicalOp.propagateSchema()) // Add internal links to the physical plan physicalPlan = getUpstreamPhysicalLinks(physicalOp.id).foldLeft(physicalPlan) { (plan, link) => plan.addLink(link) } } }) physicalPlan } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/workflow/WorkflowContext.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.workflow import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.WorkflowContext.{ DEFAULT_EXECUTION_ID, DEFAULT_WORKFLOW_ID, DEFAULT_WORKFLOW_SETTINGS } object WorkflowContext { val DEFAULT_EXECUTION_ID: ExecutionIdentity = ExecutionIdentity(1L) val DEFAULT_WORKFLOW_ID: WorkflowIdentity = WorkflowIdentity(1L) val DEFAULT_WORKFLOW_SETTINGS: WorkflowSettings = WorkflowSettings() } class WorkflowContext( var workflowId: WorkflowIdentity = DEFAULT_WORKFLOW_ID, var executionId: ExecutionIdentity = DEFAULT_EXECUTION_ID, var workflowSettings: WorkflowSettings = DEFAULT_WORKFLOW_SETTINGS ) ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/core/workflow/WorkflowSettings.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.workflow import org.apache.texera.config.GuiConfig case class WorkflowSettings( dataTransferBatchSize: Int = 400, executionMode: ExecutionMode = ExecutionMode.valueOf(GuiConfig.guiWorkflowWorkspaceDefaultExecutionMode), outputPortsNeedingStorage: Set[GlobalPortIdentity] = Set.empty ) ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/util/ArrowUtils.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.util import com.typesafe.scalalogging.LazyLogging import org.apache.texera.amber.core.tuple.AttributeTypeUtils.AttributeTypeException import org.apache.texera.amber.core.tuple._ import org.apache.arrow.memory.{BufferAllocator, RootAllocator} import org.apache.arrow.vector.types.FloatingPointPrecision import org.apache.arrow.vector.types.TimeUnit.MILLISECOND import org.apache.arrow.vector.types.pojo.ArrowType.PrimitiveType import org.apache.arrow.vector.types.pojo.{ArrowType, Field, FieldType} import org.apache.arrow.vector.{ BigIntVector, BitVector, FieldVector, Float8Vector, IntVector, TimeStampVector, VarBinaryVector, VarCharVector, VectorSchemaRoot } import java.nio.charset.StandardCharsets import java.util import scala.jdk.CollectionConverters.CollectionHasAsScala import scala.language.implicitConversions object ArrowUtils extends LazyLogging { // Create a single allocator for the entire utility private val allocator: BufferAllocator = new RootAllocator() implicit def bool2int(b: Boolean): Int = if (b) 1 else 0 /** * Reads a row of the given Arrow Vectors into a Texera.Tuple * e.g., * rowIndex IntVector BigIntVector BooleanVector * 0 1 100L true * * the row at rowIndex 0 can be converted into `Tuple[1, 100L, true]` * * @param rowIndex The row index of the target row to be converted in the Vectors. * @param vectorSchemaRoot The root of the Vectors that stores the Arrow Fields. It contains multiple Vectors. * @return */ def getTexeraTuple( rowIndex: Int, vectorSchemaRoot: VectorSchemaRoot ): Tuple = { val arrowSchema = vectorSchemaRoot.getSchema val schema = toTexeraSchema(arrowSchema) Tuple .builder(schema) .addSequentially( vectorSchemaRoot.getFieldVectors.asScala.zipWithIndex.map { case (fieldVector: FieldVector, index: Int) => val value: AnyRef = fieldVector.getObject(rowIndex) try { // Use the attribute type from the schema (which includes metadata) // instead of deriving it from the Arrow type val attributeType = schema.getAttributes(index).getType AttributeTypeUtils.parseField(value, attributeType) } catch { case e: Exception => logger.warn("Caught error during parsing Arrow value back to Texera value", e) null } }.toArray ) .build() } /** * Converts an Arrow Schema into Texera Schema. * Checks field metadata to detect LARGE_BINARY types. * * @param arrowSchema The Arrow Schema to be converted. * @return A Texera Schema. */ def toTexeraSchema(arrowSchema: org.apache.arrow.vector.types.pojo.Schema): Schema = Schema( arrowSchema.getFields.asScala.map { field => val isLargeBinary = Option(field.getMetadata) .exists(m => m.containsKey("texera_type") && m.get("texera_type") == "LARGE_BINARY") val attributeType = if (isLargeBinary) AttributeType.LARGE_BINARY else toAttributeType(field.getType) new Attribute(field.getName, attributeType) }.toList ) /** * Converts an ArrowType into an AttributeType. * * @param srcType the ArrowType to be converted. * @throws org.apache.texera.amber.core.tuple.AttributeTypeUtils.AttributeTypeException if the type cannot be converted. * @return An AttributeType. */ @throws[AttributeTypeException] def toAttributeType(srcType: ArrowType): AttributeType = { srcType match { case int: ArrowType.Int => int.getBitWidth match { case 16 | 32 => AttributeType.INTEGER case 64 | _ => AttributeType.LONG } case _: ArrowType.Bool => AttributeType.BOOLEAN case _: ArrowType.FloatingPoint => AttributeType.DOUBLE case _: ArrowType.Timestamp => AttributeType.TIMESTAMP case _: ArrowType.Utf8 => AttributeType.STRING case _: ArrowType.Binary => AttributeType.BINARY case _ => throw new AttributeTypeUtils.AttributeTypeException( "Unexpected value: " + srcType.getTypeID ) } } def appendTexeraTuple(tuple: Tuple, vectorSchemaRoot: VectorSchemaRoot): Unit = { val currentRowCount = vectorSchemaRoot.getRowCount val nextRowIndex = currentRowCount setTexeraTuple(tuple, nextRowIndex, vectorSchemaRoot) } /** * Writes a Texera.Tuple into a row of the Arrow Vectors. It will overwrite the data on the * target row of the Vectors. * * @param tuple A Texera.Tuple. * @param index The row index in the Vectors to be replaced. * @param vectorSchemaRoot The root of the Vectors that stores the Arrow Fields. It contains * multiple Vectors. */ def setTexeraTuple(tuple: Tuple, index: Int, vectorSchemaRoot: VectorSchemaRoot): Unit = { val arrowSchema = vectorSchemaRoot.getSchema val arrowFields = arrowSchema.getFields.asScala.toList for (i <- arrowFields.indices) { val vector: FieldVector = vectorSchemaRoot.getVector(i) val value = tuple.getField[AnyRef](i) val isNull = value == null arrowFields.apply(i).getFieldType.getType match { case _: ArrowType.Int => vector.getField.getFieldType.getType.asInstanceOf[ArrowType.Int].getBitWidth match { case 16 | 32 => vector .asInstanceOf[IntVector] .setSafe(index, !isNull, if (isNull) 0 else value.asInstanceOf[Int]) case 64 | _ => vector .asInstanceOf[BigIntVector] .setSafe(index, !isNull, if (isNull) 0 else value.asInstanceOf[Long]) } case _: ArrowType.Bool => vector .asInstanceOf[BitVector] .setSafe(index, !isNull, if (isNull) 0 else value.asInstanceOf[Boolean]) case _: ArrowType.FloatingPoint => vector .asInstanceOf[Float8Vector] .setSafe(index, !isNull, if (isNull) 0 else value.asInstanceOf[Double]) case _: ArrowType.Timestamp => vector .asInstanceOf[TimeStampVector] .setSafe( index, !isNull, if (isNull) 0L else AttributeTypeUtils .parseField(value, AttributeType.LONG) .asInstanceOf[Long] ) case _: ArrowType.Utf8 => if (isNull) vector.asInstanceOf[VarCharVector].setNull(index) else vector .asInstanceOf[VarCharVector] .setSafe(index, value.toString.getBytes(StandardCharsets.UTF_8)) case _: ArrowType.Binary | _: ArrowType.LargeBinary => if (isNull) vector.asInstanceOf[VarBinaryVector].setNull(index) else vector .asInstanceOf[VarBinaryVector] .setSafe(index, value.asInstanceOf[Array[Byte]]) } } vectorSchemaRoot.setRowCount(vectorSchemaRoot.getRowCount + 1) } /** * Converts an Amber schema into Arrow schema. * Stores AttributeType in field metadata to preserve LARGE_BINARY type information. * * @param schema The Texera Schema. * @return An Arrow Schema. */ def fromTexeraSchema(schema: Schema): org.apache.arrow.vector.types.pojo.Schema = { val arrowFields = schema.getAttributes.map { attribute => val metadata = if (attribute.getType == AttributeType.LARGE_BINARY) { val map = new util.HashMap[String, String]() map.put("texera_type", "LARGE_BINARY") map } else null new Field( attribute.getName, new FieldType(true, fromAttributeType(attribute.getType), null, metadata), null ) } new org.apache.arrow.vector.types.pojo.Schema(util.Arrays.asList(arrowFields: _*)) } /** * Converts an AttributeType into an ArrowType (PrimitiveType). * * @param srcType The AttributeType to be converted. * @throws org.apache.texera.amber.core.tuple.AttributeTypeUtils.AttributeTypeException if the type cannot be converted. * @return A PrimitiveType, a type of ArrowType, does not handle complex data. */ @throws[AttributeTypeException] def fromAttributeType(srcType: AttributeType): PrimitiveType = { srcType match { case AttributeType.INTEGER => new ArrowType.Int(32, true) case AttributeType.LONG => new ArrowType.Int(64, true) case AttributeType.DOUBLE => new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE) case AttributeType.BOOLEAN => ArrowType.Bool.INSTANCE case AttributeType.TIMESTAMP => new ArrowType.Timestamp(MILLISECOND, "UTC") case AttributeType.BINARY => new ArrowType.Binary case AttributeType.STRING | AttributeType.LARGE_BINARY | AttributeType.ANY => ArrowType.Utf8.INSTANCE case _ => throw new AttributeTypeUtils.AttributeTypeException("Unexpected value: " + srcType) } } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/util/IcebergUtil.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.util import org.apache.texera.amber.config.StorageConfig import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, LargeBinary, Schema, Tuple} import org.apache.hadoop.conf.Configuration import org.apache.iceberg.catalog.{Catalog, SupportsNamespaces, TableIdentifier} import org.apache.iceberg.data.parquet.GenericParquetReaders import org.apache.iceberg.data.{GenericRecord, Record} import org.apache.iceberg.aws.s3.S3FileIO import org.apache.iceberg.hadoop.{HadoopCatalog, HadoopFileIO} import org.apache.iceberg.io.{CloseableIterable, InputFile} import org.apache.iceberg.jdbc.JdbcCatalog import org.apache.iceberg.parquet.{Parquet, ParquetValueReader} import org.apache.iceberg.rest.RESTCatalog import org.apache.iceberg.types.Type.PrimitiveType import org.apache.iceberg.types.Types import org.apache.iceberg.{ CatalogProperties, DataFile, PartitionSpec, Table, TableProperties, Schema => IcebergSchema } import org.apache.iceberg.catalog.Namespace import org.apache.iceberg.exceptions.AlreadyExistsException import java.nio.ByteBuffer import java.nio.file.Path import java.sql.Timestamp import java.time.{LocalDateTime, ZoneId} import scala.jdk.CollectionConverters._ /** * Util functions to interact with Iceberg Tables */ object IcebergUtil { // Unique suffix for LARGE_BINARY field encoding private val LARGE_BINARY_FIELD_SUFFIX = "__texera_large_binary_ptr" /** * Creates and initializes a HadoopCatalog with the given parameters. * - Uses an empty Hadoop `Configuration`, meaning the local file system (or `file:/`) will be used by default * instead of HDFS. * - The `warehouse` parameter specifies the root directory for storing table data. * - Sets the file I/O implementation to `HadoopFileIO`. * * @param catalogName the name of the catalog. * @param warehouse the root path for the warehouse where the tables are stored. * @return the initialized HadoopCatalog instance. */ def createHadoopCatalog( catalogName: String, warehouse: Path ): HadoopCatalog = { val catalog = new HadoopCatalog() catalog.setConf(new Configuration) // Empty configuration, defaults to `file:/` catalog.initialize( catalogName, Map( "warehouse" -> warehouse.toString, CatalogProperties.FILE_IO_IMPL -> classOf[HadoopFileIO].getName ).asJava ) catalog } /** * Creates and initializes a RESTCatalog with the given parameters. * - Configures the catalog to interact with a REST endpoint. * - The `warehouse` parameter specifies the root directory for storing table data. * - Sets the file I/O implementation to `HadoopFileIO`. * - Authentication support is not implemented yet (see TODO). * * Note: The only tested REST catalog implementation is `tabulario/iceberg-rest` * (https://hub.docker.com/r/tabulario/iceberg-rest). * * TODO: Add authentication support, such as OAuth2, using `OAuth2Properties`. * * @param catalogName the name of the catalog. * @param warehouse the root path for the warehouse where the tables are stored. * @return the initialized RESTCatalog instance. */ def createRestCatalog( catalogName: String, warehouse: String ): RESTCatalog = { val catalog = new RESTCatalog() // Build base properties map var properties = Map( "warehouse" -> warehouse, CatalogProperties.URI -> StorageConfig.icebergRESTCatalogUri ) properties = properties ++ Map( CatalogProperties.FILE_IO_IMPL -> classOf[S3FileIO].getName, "s3.endpoint" -> StorageConfig.s3Endpoint, "s3.access-key-id" -> StorageConfig.s3Username, "s3.secret-access-key" -> StorageConfig.s3Password, "s3.region" -> StorageConfig.s3Region, "s3.path-style-access" -> "true" ) catalog.initialize(catalogName, properties.asJava) catalog } def createPostgresCatalog( catalogName: String, warehouse: Path ): JdbcCatalog = { // Occasionally the jdbc driver cannot be found during CI run. // Explicitly load the JDBC driver to avoid flaky CI failures. Class.forName("org.postgresql.Driver") val catalog = new JdbcCatalog() catalog.initialize( catalogName, Map( "warehouse" -> warehouse.toString.replace( ":", "" ), //warehouse path is C:/xxx/xxx in Windows, but PyArrow on the Python side cannot parse it. The acceptable format for PyArrow is C/xxx/xxx. CatalogProperties.FILE_IO_IMPL -> classOf[HadoopFileIO].getName, CatalogProperties.URI -> s"jdbc:postgresql://${StorageConfig.icebergPostgresCatalogUriWithoutScheme}", JdbcCatalog.PROPERTY_PREFIX + "user" -> StorageConfig.icebergPostgresCatalogUsername, JdbcCatalog.PROPERTY_PREFIX + "password" -> StorageConfig.icebergPostgresCatalogPassword ).asJava ) catalog } /** * Creates a new Iceberg table with the specified schema and properties. * - Drops the existing table if `overrideIfExists` is true and the table already exists. * - Creates an unpartitioned table with custom commit retry properties. * * @param catalog the Iceberg catalog to manage the table. * @param tableNamespace the namespace of the table. * @param tableName the name of the table. * @param tableSchema the schema of the table. * @param overrideIfExists whether to drop and recreate the table if it exists. * @return the created Iceberg table. */ def createTable( catalog: Catalog, tableNamespace: String, tableName: String, tableSchema: IcebergSchema, overrideIfExists: Boolean ): Table = { val tableProperties = Map( TableProperties.COMMIT_NUM_RETRIES -> StorageConfig.icebergTableCommitNumRetries.toString, TableProperties.COMMIT_MAX_RETRY_WAIT_MS -> StorageConfig.icebergTableCommitMaxRetryWaitMs.toString, TableProperties.COMMIT_MIN_RETRY_WAIT_MS -> StorageConfig.icebergTableCommitMinRetryWaitMs.toString ) val namespace = Namespace.of(tableNamespace) catalog match { case nsCatalog: SupportsNamespaces => try nsCatalog.createNamespace(namespace, Map.empty[String, String].asJava) catch { case _: AlreadyExistsException => () } case _ => throw new IllegalArgumentException( s"Catalog ${catalog.getClass.getName} does not support namespaces" ) } val identifier = TableIdentifier.of(tableNamespace, tableName) if (catalog.tableExists(identifier) && overrideIfExists) { catalog.dropTable(identifier) } catalog.createTable( identifier, tableSchema, PartitionSpec.unpartitioned, tableProperties.asJava ) } /** * Loads metadata for an existing Iceberg table. * - Returns `Some(Table)` if the table exists and is successfully loaded. * - Returns `None` if the table does not exist or cannot be loaded. * * @param catalog the Iceberg catalog to load the table from. * @param tableNamespace the namespace of the table. * @param tableName the name of the table. * @return an Option containing the table, or None if not found. */ def loadTableMetadata( catalog: Catalog, tableNamespace: String, tableName: String ): Option[Table] = { val identifier = TableIdentifier.of(tableNamespace, tableName) try { Some(catalog.loadTable(identifier)) } catch { case _: Exception => None } } /** * Converts a custom Amber `Schema` to an Iceberg `Schema`. * Field names are encoded to preserve LARGE_BINARY type information. * * @param amberSchema The custom Amber Schema. * @return An Iceberg Schema. */ def toIcebergSchema(amberSchema: Schema): IcebergSchema = { val icebergFields = amberSchema.getAttributes.zipWithIndex.map { case (attribute, index) => val encodedName = encodeLargeBinaryFieldName(attribute.getName, attribute.getType) val icebergType = toIcebergType(attribute.getType) Types.NestedField.optional(index + 1, encodedName, icebergType) } new IcebergSchema(icebergFields.asJava) } /** * Converts a custom Amber `AttributeType` to an Iceberg `Type`. * Note: LARGE_BINARY is stored as StringType; field name encoding is used to distinguish it. * * @param attributeType The custom Amber AttributeType. * @return The corresponding Iceberg Type. */ def toIcebergType(attributeType: AttributeType): PrimitiveType = { attributeType match { case AttributeType.STRING => Types.StringType.get() case AttributeType.INTEGER => Types.IntegerType.get() case AttributeType.LONG => Types.LongType.get() case AttributeType.DOUBLE => Types.DoubleType.get() case AttributeType.BOOLEAN => Types.BooleanType.get() case AttributeType.TIMESTAMP => Types.TimestampType.withoutZone() case AttributeType.BINARY => Types.BinaryType.get() case AttributeType.LARGE_BINARY => Types.StringType.get() // Store LargeBinary URI as string case AttributeType.ANY => throw new IllegalArgumentException("ANY type is not supported in Iceberg") } } /** * Converts a custom Amber `Tuple` to an Iceberg `GenericRecord`, handling `null` values. * * @param tuple The custom Amber Tuple. * @return An Iceberg GenericRecord. */ def toGenericRecord(icebergSchema: IcebergSchema, tuple: Tuple): Record = { val record = GenericRecord.create(icebergSchema) tuple.schema.getAttributes.zipWithIndex.foreach { case (attribute, index) => val fieldName = encodeLargeBinaryFieldName(attribute.getName, attribute.getType) val value = tuple.getField[AnyRef](index) match { case null => null case ts: Timestamp => ts.toInstant.atZone(ZoneId.systemDefault()).toLocalDateTime case bytes: Array[Byte] => ByteBuffer.wrap(bytes) case largeBinaryPtr: LargeBinary => largeBinaryPtr.getUri case other => other } record.setField(fieldName, value) } record } /** * Converts an Iceberg `Record` to an Amber `Tuple` * * @param record The Iceberg Record. * @param amberSchema The corresponding Amber Schema. * @return An Amber Tuple. */ def fromRecord(record: Record, amberSchema: Schema): Tuple = { val fieldValues = amberSchema.getAttributes.map { attribute => val fieldName = encodeLargeBinaryFieldName(attribute.getName, attribute.getType) val rawValue = record.getField(fieldName) rawValue match { case null => null case ldt: LocalDateTime => Timestamp.valueOf(ldt) case buffer: ByteBuffer => val bytes = new Array[Byte](buffer.remaining()) buffer.get(bytes) bytes case uri: String if attribute.getType == AttributeType.LARGE_BINARY => new LargeBinary(uri) case other => other } } Tuple(amberSchema, fieldValues.toArray) } /** * Encodes a field name for LARGE_BINARY types by adding a unique system suffix. * This ensures LARGE_BINARY fields can be identified when reading from Iceberg. * * @param fieldName The original field name * @param attributeType The attribute type * @return The encoded field name with a unique suffix for LARGE_BINARY types */ private def encodeLargeBinaryFieldName( fieldName: String, attributeType: AttributeType ): String = { if (attributeType == AttributeType.LARGE_BINARY) { s"${fieldName}${LARGE_BINARY_FIELD_SUFFIX}" } else { fieldName } } /** * Decodes a field name by removing the unique system suffix if present. * This restores the original user-defined field name. * * @param fieldName The encoded field name * @return The original field name with system suffix removed */ private def decodeLargeBinaryFieldName(fieldName: String): String = { if (isLargeBinaryField(fieldName)) { fieldName.substring(0, fieldName.length - LARGE_BINARY_FIELD_SUFFIX.length) } else { fieldName } } /** * Checks if a field name indicates a LARGE_BINARY type by examining the unique suffix. * * @param fieldName The field name to check * @return true if the field represents a LARGE_BINARY type, false otherwise */ private def isLargeBinaryField(fieldName: String): Boolean = { fieldName.endsWith(LARGE_BINARY_FIELD_SUFFIX) } /** * Converts an Iceberg `Schema` to an Amber `Schema`. * Field names are decoded to restore original names and detect LARGE_BINARY types. * * @param icebergSchema The Iceberg Schema. * @return The corresponding Amber Schema. */ def fromIcebergSchema(icebergSchema: IcebergSchema): Schema = { val attributes = icebergSchema .columns() .asScala .map { field => val fieldName = field.name() val attributeType = fromIcebergType(field.`type`().asPrimitiveType(), fieldName) val originalName = decodeLargeBinaryFieldName(fieldName) new Attribute(originalName, attributeType) } .toList Schema(attributes) } /** * Converts an Iceberg `Type` to an Amber `AttributeType`. * * @param icebergType The Iceberg Type. * @param fieldName The field name (used to detect LARGE_BINARY by suffix). * @return The corresponding Amber AttributeType. */ def fromIcebergType( icebergType: PrimitiveType, fieldName: String = "" ): AttributeType = { icebergType match { case _: Types.StringType => if (isLargeBinaryField(fieldName)) AttributeType.LARGE_BINARY else AttributeType.STRING case _: Types.IntegerType => AttributeType.INTEGER case _: Types.LongType => AttributeType.LONG case _: Types.DoubleType => AttributeType.DOUBLE case _: Types.BooleanType => AttributeType.BOOLEAN case _: Types.TimestampType => AttributeType.TIMESTAMP case _: Types.BinaryType => AttributeType.BINARY case _ => throw new IllegalArgumentException(s"Unsupported Iceberg type: $icebergType") } } /** * Util function to create a Record iterator over the given DataFile in Iceberg * @param dataFile the data file * @param schema the schema of the table * @param table the iceberg table * @return an iterator over the records in the data file */ def readDataFileAsIterator( dataFile: DataFile, schema: IcebergSchema, table: Table ): Iterator[Record] = { val inputFile: InputFile = table.io().newInputFile(dataFile) val readerFunc : java.util.function.Function[org.apache.parquet.schema.MessageType, ParquetValueReader[ _ ]] = (messageType: org.apache.parquet.schema.MessageType) => GenericParquetReaders.buildReader(schema, messageType) val closeableIterable: CloseableIterable[Record] = Parquet .read(inputFile) .project(schema) .createReaderFunc(readerFunc) .build() closeableIterable.iterator().asScala } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/util/JSONUtils.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.util import com.fasterxml.jackson.annotation.JsonInclude.Include import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper} import com.fasterxml.jackson.module.noctordeser.NoCtorDeserModule import com.fasterxml.jackson.module.scala.DefaultScalaModule import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.util.serde.{PortIdentityKeyDeserializer, PortIdentityKeySerializer} import java.text.SimpleDateFormat import scala.jdk.CollectionConverters.IteratorHasAsScala object JSONUtils { /** * A singleton object for configuring the Jackson `ObjectMapper` to handle JSON serialization and deserialization * in Scala. This custom `ObjectMapper` is tailored for Scala, ensuring compatibility with Scala types * and specific serialization/deserialization settings. * * - Registers the `DefaultScalaModule` to ensure proper handling of Scala-specific types (e.g., `Option`, `Seq`). * - Registers the `NoCtorDeserModule` to handle deserialization of Scala classes that lack a default constructor, * which is common in case classes. * - Registers the `SimpleModule` with pairs of serializer & deserializer to ensure proper handling of serializing * and deserializing the PhysicalPlan * - Sets the serialization inclusion rules to exclude `null` and `absent` values: * - `Include.NON_NULL`: Excludes fields with `null` values from the serialized JSON. * - `Include.NON_ABSENT`: Excludes fields with `Option.empty` (or equivalent absent values) from serialization. * - Configures the date format for JSON serialization and deserialization: * - The format is set to `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'`, which follows the ISO-8601 standard for representing date and time, * commonly used in JSON APIs, including millisecond precision and the UTC 'Z' suffix. * * This `ObjectMapper` provides a consistent way to serialize and deserialize JSON while adhering to Scala conventions * and handling common patterns like `Option` and case classes. */ final val objectMapper = new ObjectMapper() .registerModule(DefaultScalaModule) .registerModule(new NoCtorDeserModule()) .registerModule( new SimpleModule() .addKeySerializer(classOf[PortIdentity], new PortIdentityKeySerializer()) .addKeyDeserializer(classOf[PortIdentity], new PortIdentityKeyDeserializer()) ) .setSerializationInclusion(Include.NON_NULL) .setSerializationInclusion(Include.NON_ABSENT) .setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")) /** * this method helps convert JSON into a key-value Map. By default it will only * take the first level attributes of the JSON object, and ignore nested objects * and arrays. For example: * input JSON {"A" : "a", "B": 1, "C": 2.3, "D" :{"some":"object"}, "E": ["1", "2"]} * will be converted to Map[String, String]{"A" : "a", "B": "1", "C": "2.3"}. * * If flatten mode is enabled, then the nested objects and arrays will be converted * to map recursively. The key will be the `parentName[index].childName`. For example: * input JSON {"A" : "a", "B": 1, "C": 2.3, "D" :{"some":"object"}, "E": ["X", "Y"]} * will be converted to Map[String, String]{"A" : "a", "B": "1", "C": "2.3", * "D.some":"object", "E1":"X", "E2":"Y"}. * * @param node the JSONNode to convert. * @param flatten a boolean to toggle flatten mode. * @param parentName the parent's name to pass into children's naming conversion. * @return a Map[String, String] of all the key value pairs from the given JSONNode. */ def JSONToMap( node: JsonNode, flatten: Boolean = false, parentName: String = "" ): Map[String, String] = { var result = Map[String, String]() if (node.isObject) { for (key <- node.fieldNames().asScala) { val child: JsonNode = node.get(key) val absoluteKey = (if (parentName.nonEmpty) parentName + "." else "") + key if (flatten && (child.isObject || child.isArray)) { result = result ++ JSONToMap(child, flatten, absoluteKey) } else if (child.isValueNode) { result = result + (absoluteKey -> child.asText()) } else { // do nothing } } } else if (node.isArray) { for ((child, i) <- node.elements().asScala.zipWithIndex) { result = result ++ JSONToMap(child, flatten, parentName + (i + 1)) } } result } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/util/VirtualIdentityUtils.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.util import org.apache.texera.amber.core.virtualidentity.{ ActorVirtualIdentity, OperatorIdentity, PhysicalOpIdentity, WorkflowIdentity } import scala.util.matching.Regex object VirtualIdentityUtils { private val workerNamePattern: Regex = raw"Worker:WF(\d+)-(.+)-(\w+)-(\d+)".r private val operatorUUIDPattern: Regex = raw"(\w+)-(.+)-(\w+)".r private val MATERIALIZATION_READER_ACTOR_PREFIX: String = "MATERIALIZATION_READER_" def createWorkerIdentity( workflowId: WorkflowIdentity, operator: String, layerName: String, workerId: Int ): ActorVirtualIdentity = { ActorVirtualIdentity( s"Worker:WF${workflowId.id}-$operator-$layerName-$workerId" ) } def createWorkerIdentity( workflowId: WorkflowIdentity, physicalOpId: PhysicalOpIdentity, workerId: Int ): ActorVirtualIdentity = { createWorkerIdentity( workflowId, physicalOpId.logicalOpId.id, physicalOpId.layerName, workerId ) } def getPhysicalOpId(workerId: ActorVirtualIdentity): PhysicalOpIdentity = { workerId.name match { case workerNamePattern(_, operator, layerName, _) => PhysicalOpIdentity(OperatorIdentity(operator), layerName) case other => // for special actorId such as SELF, CONTROLLER PhysicalOpIdentity(OperatorIdentity("__DummyOperator"), "__DummyLayer") } } def getWorkerIndex(workerId: ActorVirtualIdentity): Int = { workerId.name match { case workerNamePattern(_, _, _, idx) => idx.toInt } } def toShorterString(workerId: ActorVirtualIdentity): String = { workerId.name match { case workerNamePattern(workflowId, operatorName, layerName, workerIndex) => val shorterName = if (operatorName.length > 6) { operatorName match { case operatorUUIDPattern(op, _, postfix) => op + "-" + postfix.takeRight(6) case _ => operatorName.takeRight(6) } } else { operatorName } s"WF$workflowId-$shorterName-$layerName-$workerIndex" case _ => workerId.name } } /** * An input port materialization reader thread mimics the behavior of an upstream worker. * Each thread has a virtual actor id. This method creates such a virtual actor id. * @param storageURIStr The materialization location to read from. * @param toWorkerActorId The worker actor that the thread belongs to. * @return */ def getFromActorIdForInputPortStorage( storageURIStr: String, toWorkerActorId: ActorVirtualIdentity ): ActorVirtualIdentity = { ActorVirtualIdentity(MATERIALIZATION_READER_ACTOR_PREFIX + storageURIStr + toWorkerActorId.name) } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/util/serde/GlobalPortIdentitySerde.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.util.serde import org.apache.texera.amber.core.virtualidentity.{OperatorIdentity, PhysicalOpIdentity} import org.apache.texera.amber.core.workflow.{GlobalPortIdentity, PortIdentity} /** * Serialize and deserializes a GlobalPortIdentity object to a string using a custom, human-readable format * to ensure it works with both URI and file path and does not incldue underscore "_" so that it does not * interfere with our own VFS URI parsing. */ object GlobalPortIdentitySerde { implicit class SerdeOps(globalPortId: GlobalPortIdentity) { /** * Serializes a GlobalPortIdentity object into a string using our custom, human-readable format * that works with both URI and file path and does not incldue underscore "_" so that it does not * interfere with our own VFS URI parsing. * * @return A serialized string representation of globalPortId */ def serializeAsString: String = { val logicalOpId = globalPortId.opId.logicalOpId.id val layerName = globalPortId.opId.layerName val portId = globalPortId.portId.id val isInternal = globalPortId.portId.internal val isInput = globalPortId.input s"(logicalOpId=$logicalOpId,layerName=$layerName,portId=$portId,isInternal=$isInternal,isInput=$isInput)" } } /** * Deserializes a string as a GlobalPortIdentity object. Must use our custom format: * `(logicalOpId=,layerName=,portId=,isInternal=,isInput=)` * @param serializedGlobalPortId A serialized string foramt of a GlobalPortIdentity * @return A desrialized GlobalPortIdentity, or IllegalArgumentException if the format is not correct. */ def deserializeFromString(serializedGlobalPortId: String): GlobalPortIdentity = { val pattern = """\(logicalOpId=([^,]+),layerName=([^,]+),portId=([^,]+),isInternal=([^,]+),isInput=([^)]+)\)""".r serializedGlobalPortId match { case pattern(logicalOpId, layerName, portIdStr, isInternalStr, isInputStr) => val portIdInt = portIdStr.toInt val isInternal = isInternalStr.toBoolean val isInput = isInputStr.toBoolean val physicalOpId = PhysicalOpIdentity( logicalOpId = OperatorIdentity(logicalOpId), layerName = layerName ) val portId = PortIdentity(id = portIdInt, internal = isInternal) GlobalPortIdentity(opId = physicalOpId, portId = portId, input = isInput) case _ => throw new IllegalArgumentException( s"Invalid GlobalPortIdentity format: $serializedGlobalPortId" ) } } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/util/serde/PortIdentityKeyDeserializer.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.util.serde import com.fasterxml.jackson.databind.{DeserializationContext, KeyDeserializer} import org.apache.texera.amber.core.workflow.PortIdentity class PortIdentityKeyDeserializer extends KeyDeserializer { override def deserializeKey(key: String, ctxt: DeserializationContext): PortIdentity = { // Deserialize the string back to PortIdentity val parts = key.split("_") PortIdentity(parts(0).toInt, parts(1).toBoolean) } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/amber/util/serde/PortIdentityKeySerializer.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.util.serde import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.{JsonSerializer, SerializerProvider} import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.util.serde.PortIdentityKeySerializer.portIdToString case object PortIdentityKeySerializer { def portIdToString(portId: PortIdentity): String = { s"${portId.id}_${portId.internal}" } } class PortIdentityKeySerializer extends JsonSerializer[PortIdentity] { override def serialize( key: PortIdentity, gen: JsonGenerator, serializers: SerializerProvider ): Unit = { // Serialize PortIdentity as a string "id_internal" gen.writeFieldName(portIdToString(key)) } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/service/util/LargeBinaryInputStream.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.util import org.apache.texera.amber.core.tuple.LargeBinary import java.io.InputStream /** * InputStream for reading LargeBinary data from S3. * * The underlying S3 download is lazily initialized on first read. * The stream will fail if the S3 object doesn't exist when read is attempted. * * Usage: * {{{ * val largeBinary: LargeBinary = ... * try (val in = new LargeBinaryInputStream(largeBinary)) { * val bytes = in.readAllBytes() * } * }}} */ class LargeBinaryInputStream(largeBinary: LargeBinary) extends InputStream { require(largeBinary != null, "LargeBinary cannot be null") // Lazy initialization - downloads only when first read() is called private lazy val underlying: InputStream = S3StorageClient.downloadObject(largeBinary.getBucketName, largeBinary.getObjectKey) @volatile private var closed = false override def read(): Int = whenOpen(underlying.read()) override def read(b: Array[Byte], off: Int, len: Int): Int = whenOpen(underlying.read(b, off, len)) override def readAllBytes(): Array[Byte] = whenOpen(underlying.readAllBytes()) override def readNBytes(n: Int): Array[Byte] = whenOpen(underlying.readNBytes(n)) override def skip(n: Long): Long = whenOpen(underlying.skip(n)) override def available(): Int = whenOpen(underlying.available()) override def close(): Unit = { if (!closed) { closed = true if (underlying != null) { // Only close if initialized underlying.close() } } } override def markSupported(): Boolean = whenOpen(underlying.markSupported()) override def mark(readlimit: Int): Unit = whenOpen(underlying.mark(readlimit)) override def reset(): Unit = whenOpen(underlying.reset()) private def whenOpen[T](f: => T): T = { if (closed) throw new java.io.IOException("Stream is closed") f } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/service/util/LargeBinaryManager.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.util import com.typesafe.scalalogging.LazyLogging import java.util.UUID /** * Manages the lifecycle of LargeBinaries stored in S3. * * Handles creation and deletion of large objects that exceed * normal tuple size limits. */ object LargeBinaryManager extends LazyLogging { val DEFAULT_BUCKET: String = "texera-large-binaries" /** * Creates a new LargeBinary reference. * The actual data upload happens separately via LargeBinaryOutputStream. * * @return S3 URI string for the new LargeBinary (format: s3://bucket/key) */ def create(): String = { val objectKey = s"objects/${System.currentTimeMillis()}/${UUID.randomUUID()}" val uri = s"s3://$DEFAULT_BUCKET/$objectKey" uri } /** * Deletes all large binaries from the bucket. * * @throws java.lang.Exception if the deletion fails * @return Unit */ def deleteAllObjects(): Unit = { try { S3StorageClient.deleteDirectory(DEFAULT_BUCKET, "objects") logger.info(s"Successfully deleted all large binaries from bucket: $DEFAULT_BUCKET") } catch { case e: Exception => logger.warn(s"Failed to delete large binaries from bucket: $DEFAULT_BUCKET", e) } } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/service/util/LargeBinaryOutputStream.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.util import com.typesafe.scalalogging.LazyLogging import org.apache.texera.amber.core.tuple.LargeBinary import java.io.{IOException, OutputStream, PipedInputStream, PipedOutputStream} import java.util.concurrent.atomic.AtomicReference import scala.concurrent.{Await, ExecutionContext, Future} import scala.concurrent.duration.Duration /** * OutputStream for streaming LargeBinary data to S3. * * Data is uploaded in the background using multipart upload as you write. * Call close() to complete the upload and ensure all data is persisted. * * Usage: * {{{ * val largeBinary = new LargeBinary() * try (val out = new LargeBinaryOutputStream(largeBinary)) { * out.write(myBytes) * } * // largeBinary is now ready to use * }}} * * Note: Not thread-safe. Do not access from multiple threads concurrently. * * @param largeBinary The LargeBinary reference to write to */ class LargeBinaryOutputStream(largeBinary: LargeBinary) extends OutputStream with LazyLogging { private val PIPE_BUFFER_SIZE = 64 * 1024 // 64KB require(largeBinary != null, "LargeBinary cannot be null") private val bucketName: String = largeBinary.getBucketName private val objectKey: String = largeBinary.getObjectKey private implicit val ec: ExecutionContext = ExecutionContext.global // Pipe: we write to pipedOut, and S3 reads from pipedIn private val pipedIn = new PipedInputStream(PIPE_BUFFER_SIZE) private val pipedOut = new PipedOutputStream(pipedIn) @volatile private var closed = false private val uploadException = new AtomicReference[Option[Throwable]](None) // Start background upload immediately private val uploadFuture: Future[Unit] = Future { try { S3StorageClient.createBucketIfNotExist(bucketName) S3StorageClient.uploadObject(bucketName, objectKey, pipedIn) logger.debug(s"Upload completed: ${largeBinary.getUri}") } catch { case e: Exception => uploadException.set(Some(e)) logger.error(s"Upload failed: ${largeBinary.getUri}", e) } finally { pipedIn.close() } } override def write(b: Int): Unit = whenOpen(pipedOut.write(b)) override def write(b: Array[Byte], off: Int, len: Int): Unit = whenOpen(pipedOut.write(b, off, len)) override def flush(): Unit = { if (!closed) pipedOut.flush() } /** * Closes the stream and completes the S3 upload. * Blocks until upload is complete. Throws IOException if upload failed. */ override def close(): Unit = { if (closed) return closed = true try { pipedOut.close() Await.result(uploadFuture, Duration.Inf) checkUploadSuccess() } catch { case e: IOException => throw e case e: Exception => S3StorageClient.deleteObject(bucketName, objectKey) throw new IOException(s"Failed to complete upload: ${e.getMessage}", e) } } private def whenOpen[T](f: => T): T = { if (closed) throw new IOException("Stream is closed") checkUploadSuccess() f } private def checkUploadSuccess(): Unit = { uploadException.get().foreach { ex => throw new IOException("Background upload failed", ex) } } } ================================================ FILE: common/workflow-core/src/main/scala/org/apache/texera/service/util/S3StorageClient.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.util import org.apache.texera.amber.config.StorageConfig import software.amazon.awssdk.auth.credentials.{AwsBasicCredentials, StaticCredentialsProvider} import software.amazon.awssdk.regions.Region import software.amazon.awssdk.services.s3.model._ import software.amazon.awssdk.services.s3.{S3Client, S3Configuration} import software.amazon.awssdk.core.sync.RequestBody import java.io.InputStream import java.security.MessageDigest import scala.jdk.CollectionConverters._ /** * S3Storage provides an abstraction for S3-compatible storage (e.g., MinIO). * - Uses credentials and endpoint from StorageConfig. * - Supports object upload, download, listing, and deletion. */ object S3StorageClient { val MINIMUM_NUM_OF_MULTIPART_S3_PART: Long = 5L * 1024 * 1024 // 5 MiB val MAXIMUM_NUM_OF_MULTIPART_S3_PARTS = 10_000 //Keep on sync with LakeFS https://github.com/treeverse/lakeFS/pull/10180 val PHYSICAL_ADDRESS_EXPIRATION_TIME_HRS = 24 // Initialize MinIO-compatible S3 Client private lazy val s3Client: S3Client = { val credentials = AwsBasicCredentials.create(StorageConfig.s3Username, StorageConfig.s3Password) S3Client .builder() .credentialsProvider(StaticCredentialsProvider.create(credentials)) .region(Region.of(StorageConfig.s3Region)) .endpointOverride(java.net.URI.create(StorageConfig.s3Endpoint)) // MinIO URL .serviceConfiguration( S3Configuration.builder().pathStyleAccessEnabled(true).build() ) .build() } /** * Checks if a directory (prefix) exists within an S3 bucket. * * @param bucketName The bucket name. * @param directoryPrefix The directory (prefix) to check (must end with `/`). * @return True if the directory contains at least one object, False otherwise. */ def directoryExists(bucketName: String, directoryPrefix: String): Boolean = { // Ensure the prefix ends with `/` to correctly match directories val normalizedPrefix = if (directoryPrefix.endsWith("/")) directoryPrefix else directoryPrefix + "/" val listRequest = ListObjectsV2Request .builder() .bucket(bucketName) .prefix(normalizedPrefix) .maxKeys(1) // Only check if at least one object exists .build() val listResponse = s3Client.listObjectsV2(listRequest) !listResponse.contents().isEmpty // If contents exist, directory exists } /** * Creates an S3 bucket if it does not already exist. * * @param bucketName The name of the bucket to create. */ def createBucketIfNotExist(bucketName: String): Unit = { try { // Check if the bucket already exists s3Client.headBucket(HeadBucketRequest.builder().bucket(bucketName).build()) } catch { case _: NoSuchBucketException | _: S3Exception => // If the bucket does not exist, create it val createBucketRequest = CreateBucketRequest.builder().bucket(bucketName).build() s3Client.createBucket(createBucketRequest) println(s"Bucket '$bucketName' created successfully.") } } /** * Deletes a directory (all objects under a given prefix) from a bucket. * * @param bucketName Target S3/MinIO bucket. * @param directoryPrefix The directory to delete (must end with `/`). */ def deleteDirectory(bucketName: String, directoryPrefix: String): Unit = { // Ensure the directory prefix ends with `/` to avoid accidental deletions val prefix = if (directoryPrefix.endsWith("/")) directoryPrefix else directoryPrefix + "/" // List objects under the given prefix val listRequest = ListObjectsV2Request .builder() .bucket(bucketName) .prefix(prefix) .build() val listResponse = s3Client.listObjectsV2(listRequest) // Extract object keys val objectKeys = listResponse.contents().asScala.map(_.key()) if (objectKeys.nonEmpty) { val objectsToDelete = objectKeys.map(key => ObjectIdentifier.builder().key(key).build()).asJava val deleteRequest = Delete .builder() .objects(objectsToDelete) .build() // Compute MD5 checksum for MinIO if required val md5Hash = MessageDigest .getInstance("MD5") .digest(deleteRequest.toString.getBytes("UTF-8")) // Convert object keys to S3 DeleteObjectsRequest format val deleteObjectsRequest = DeleteObjectsRequest .builder() .bucket(bucketName) .delete(deleteRequest) .build() // Perform batch deletion s3Client.deleteObjects(deleteObjectsRequest) } } /** * Uploads an object to S3 using multipart upload. * Handles streams of any size without loading into memory. */ def uploadObject(bucketName: String, objectKey: String, inputStream: InputStream): String = { val buffer = new Array[Byte](MINIMUM_NUM_OF_MULTIPART_S3_PART.toInt) // Helper to read a full buffer from input stream def readChunk(): Int = { var offset = 0 var read = 0 while ( offset < buffer.length && { read = inputStream.read(buffer, offset, buffer.length - offset); read > 0 } ) { offset += read } offset } // Read first chunk to check if stream is empty val firstChunkSize = readChunk() if (firstChunkSize == 0) { return s3Client .putObject( PutObjectRequest.builder().bucket(bucketName).key(objectKey).build(), RequestBody.fromBytes(Array.empty[Byte]) ) .eTag() } val uploadId = s3Client .createMultipartUpload( CreateMultipartUploadRequest.builder().bucket(bucketName).key(objectKey).build() ) .uploadId() var uploadSuccess = false try { // Upload all parts using an iterator val allParts = Iterator .iterate((1, firstChunkSize)) { case (partNum, _) => (partNum + 1, readChunk()) } .takeWhile { case (_, size) => size > 0 } .map { case (partNumber, chunkSize) => val eTag = s3Client .uploadPart( UploadPartRequest .builder() .bucket(bucketName) .key(objectKey) .uploadId(uploadId) .partNumber(partNumber) .build(), RequestBody.fromBytes(buffer.take(chunkSize)) ) .eTag() CompletedPart.builder().partNumber(partNumber).eTag(eTag).build() } .toList val result = s3Client .completeMultipartUpload( CompleteMultipartUploadRequest .builder() .bucket(bucketName) .key(objectKey) .uploadId(uploadId) .multipartUpload(CompletedMultipartUpload.builder().parts(allParts.asJava).build()) .build() ) .eTag() uploadSuccess = true result } finally { if (!uploadSuccess) { try { s3Client.abortMultipartUpload( AbortMultipartUploadRequest .builder() .bucket(bucketName) .key(objectKey) .uploadId(uploadId) .build() ) } catch { case _: Exception => } } } } /** * Downloads an object from S3 as an InputStream. * * @param bucketName The S3 bucket name. * @param objectKey The object key (path) in S3. * @return An InputStream containing the object data. */ def downloadObject(bucketName: String, objectKey: String): InputStream = { s3Client.getObject( GetObjectRequest.builder().bucket(bucketName).key(objectKey).build() ) } /** * Deletes a single object from S3. * * @param bucketName The S3 bucket name. * @param objectKey The object key (path) in S3. */ def deleteObject(bucketName: String, objectKey: String): Unit = { s3Client.deleteObject( DeleteObjectRequest.builder().bucket(bucketName).key(objectKey).build() ) } /** * Uploads a single part for an in-progress S3 multipart upload. * * This method wraps the AWS SDK v2 {@code UploadPart} API: * it builds an [[software.amazon.awssdk.services.s3.model.UploadPartRequest]] * and streams the part payload via a [[software.amazon.awssdk.core.sync.RequestBody]]. * * Payload handling: * - If {@code contentLength} is provided, the payload is streamed directly from {@code inputStream} * using {@code RequestBody.fromInputStream(inputStream, len)}. * - If {@code contentLength} is {@code None}, the entire {@code inputStream} is read into memory * ({@code readAllBytes}) and uploaded using {@code RequestBody.fromBytes(bytes)}. * This is convenient but can be memory-expensive for large parts; prefer providing a known length. * * Notes: * - {@code partNumber} must be in the valid S3 range (typically 1..10,000). * - The caller is responsible for closing {@code inputStream}. * - This method is synchronous and will block the calling thread until the upload completes. * * @param bucket S3 bucket name. * @param key Object key (path) being uploaded. * @param uploadId Multipart upload identifier returned by CreateMultipartUpload. * @param partNumber 1-based part number for this upload. * @param inputStream Stream containing the bytes for this part. * @param contentLength Optional size (in bytes) of this part; provide it to avoid buffering in memory. * @return The [[software.amazon.awssdk.services.s3.model.UploadPartResponse]], * including the part ETag used for completing the multipart upload. */ def uploadPartWithRequest( bucket: String, key: String, uploadId: String, partNumber: Int, inputStream: InputStream, contentLength: Option[Long] ): UploadPartResponse = { val requestBody: RequestBody = contentLength match { case Some(len) => RequestBody.fromInputStream(inputStream, len) case None => val bytes = inputStream.readAllBytes() RequestBody.fromBytes(bytes) } val req = UploadPartRequest .builder() .bucket(bucket) .key(key) .uploadId(uploadId) .partNumber(partNumber) .build() s3Client.uploadPart(req, requestBody) } } ================================================ FILE: common/workflow-core/src/test/resources/country_sales_small.csv ================================================ Region,Country,Item Type,Sales Channel,Order Priority,Order Date,Order ID,Ship Date,Units Sold,Unit Price,Unit Cost,Total Revenue,Total Cost,Total Profit Australia and Oceania,Tuvalu,Baby Food,Offline,H,5/28/2010,669165933,6/27/2010,9925,255.28,159.42,2533654.00,1582243.50,951410.50 Central America and the Caribbean,Grenada,Cereal,Online,C,8/22/2012,963881480,9/15/2012,2804,205.70,117.11,576782.80,328376.44,248406.36 Europe,Russia,Office Supplies,Offline,L,5/2/2014,341417157,5/8/2014,1779,651.21,524.96,1158502.59,933903.84,224598.75 Sub-Saharan Africa,Sao Tome and Principe,Fruits,Online,C,6/20/2014,514321792,7/5/2014,8102,9.33,6.92,75591.66,56065.84,19525.82 Sub-Saharan Africa,Rwanda,Office Supplies,Offline,L,2/1/2013,115456712,2/6/2013,5062,651.21,524.96,3296425.02,2657347.52,639077.50 Australia and Oceania,Solomon Islands,Baby Food,Online,C,2/4/2015,547995746,2/21/2015,2974,255.28,159.42,759202.72,474115.08,285087.64 Sub-Saharan Africa,Angola,Household,Offline,M,4/23/2011,135425221,4/27/2011,4187,668.27,502.54,2798046.49,2104134.98,693911.51 Sub-Saharan Africa,Burkina Faso,Vegetables,Online,H,7/17/2012,871543967,7/27/2012,8082,154.06,90.93,1245112.92,734896.26,510216.66 Sub-Saharan Africa,Republic of the Congo,Personal Care,Offline,M,7/14/2015,770463311,8/25/2015,6070,81.73,56.67,496101.10,343986.90,152114.20 Sub-Saharan Africa,Senegal,Cereal,Online,H,4/18/2014,616607081,5/30/2014,6593,205.70,117.11,1356180.10,772106.23,584073.87 Asia,Kyrgyzstan,Vegetables,Online,H,6/24/2011,814711606,7/12/2011,124,154.06,90.93,19103.44,11275.32,7828.12 Sub-Saharan Africa,Cape Verde,Clothes,Offline,H,8/2/2014,939825713,8/19/2014,4168,109.28,35.84,455479.04,149381.12,306097.92 Asia,Bangladesh,Clothes,Online,L,1/13/2017,187310731,3/1/2017,8263,109.28,35.84,902980.64,296145.92,606834.72 Central America and the Caribbean,Honduras,Household,Offline,H,2/8/2017,522840487,2/13/2017,8974,668.27,502.54,5997054.98,4509793.96,1487261.02 Asia,Mongolia,Personal Care,Offline,C,2/19/2014,832401311,2/23/2014,4901,81.73,56.67,400558.73,277739.67,122819.06 Europe,Bulgaria,Clothes,Online,M,4/23/2012,972292029,6/3/2012,1673,109.28,35.84,182825.44,59960.32,122865.12 Asia,Sri Lanka,Cosmetics,Offline,M,11/19/2016,419123971,12/18/2016,6952,437.20,263.33,3039414.40,1830670.16,1208744.24 Sub-Saharan Africa,Cameroon,Beverages,Offline,C,4/1/2015,519820964,4/18/2015,5430,47.45,31.79,257653.50,172619.70,85033.80 Asia,Turkmenistan,Household,Offline,L,12/30/2010,441619336,1/20/2011,3830,668.27,502.54,2559474.10,1924728.20,634745.90 Australia and Oceania,East Timor,Meat,Online,L,7/31/2012,322067916,9/11/2012,5908,421.89,364.69,2492526.12,2154588.52,337937.60 Europe,Norway,Baby Food,Online,L,5/14/2014,819028031,6/28/2014,7450,255.28,159.42,1901836.00,1187679.00,714157.00 Europe,Portugal,Baby Food,Online,H,7/31/2015,860673511,9/3/2015,1273,255.28,159.42,324971.44,202941.66,122029.78 Central America and the Caribbean,Honduras,Snacks,Online,L,6/30/2016,795490682,7/26/2016,2225,152.58,97.44,339490.50,216804.00,122686.50 Australia and Oceania,New Zealand,Fruits,Online,H,9/8/2014,142278373,10/4/2014,2187,9.33,6.92,20404.71,15134.04,5270.67 Europe,Moldova ,Personal Care,Online,L,5/7/2016,740147912,5/10/2016,5070,81.73,56.67,414371.10,287316.90,127054.20 Europe,France,Cosmetics,Online,H,5/22/2017,898523128,6/5/2017,1815,437.20,263.33,793518.00,477943.95,315574.05 Australia and Oceania,Kiribati,Fruits,Online,M,10/13/2014,347140347,11/10/2014,5398,9.33,6.92,50363.34,37354.16,13009.18 Sub-Saharan Africa,Mali,Fruits,Online,L,5/7/2010,686048400,5/10/2010,5822,9.33,6.92,54319.26,40288.24,14031.02 Europe,Norway,Beverages,Offline,C,7/18/2014,435608613,7/30/2014,5124,47.45,31.79,243133.80,162891.96,80241.84 Sub-Saharan Africa,The Gambia,Household,Offline,L,5/26/2012,886494815,6/9/2012,2370,668.27,502.54,1583799.90,1191019.80,392780.10 Europe,Switzerland,Cosmetics,Offline,M,9/17/2012,249693334,10/20/2012,8661,437.20,263.33,3786589.20,2280701.13,1505888.07 Sub-Saharan Africa,South Sudan,Personal Care,Offline,C,12/29/2013,406502997,1/28/2014,2125,81.73,56.67,173676.25,120423.75,53252.50 Australia and Oceania,Australia,Office Supplies,Online,C,10/27/2015,158535134,11/25/2015,2924,651.21,524.96,1904138.04,1534983.04,369155.00 Asia,Myanmar,Household,Offline,H,1/16/2015,177713572,3/1/2015,8250,668.27,502.54,5513227.50,4145955.00,1367272.50 Sub-Saharan Africa,Djibouti,Snacks,Online,M,2/25/2017,756274640,2/25/2017,7327,152.58,97.44,1117953.66,713942.88,404010.78 Central America and the Caribbean,Costa Rica,Personal Care,Offline,L,5/8/2017,456767165,5/21/2017,6409,81.73,56.67,523807.57,363198.03,160609.54 Middle East and North Africa,Syria,Fruits,Online,L,11/22/2011,162052476,12/3/2011,3784,9.33,6.92,35304.72,26185.28,9119.44 Sub-Saharan Africa,The Gambia,Meat,Online,M,1/14/2017,825304400,1/23/2017,4767,421.89,364.69,2011149.63,1738477.23,272672.40 Asia,Brunei,Office Supplies,Online,L,4/1/2012,320009267,5/8/2012,6708,651.21,524.96,4368316.68,3521431.68,846885.00 Europe,Bulgaria,Office Supplies,Online,M,2/16/2012,189965903,2/28/2012,3987,651.21,524.96,2596374.27,2093015.52,503358.75 Sub-Saharan Africa,Niger,Personal Care,Online,H,3/11/2017,699285638,3/28/2017,3015,81.73,56.67,246415.95,170860.05,75555.90 Middle East and North Africa,Azerbaijan,Cosmetics,Online,M,2/6/2010,382392299,2/25/2010,7234,437.20,263.33,3162704.80,1904929.22,1257775.58 Sub-Saharan Africa,The Gambia,Cereal,Offline,H,6/7/2012,994022214,6/8/2012,2117,205.70,117.11,435466.90,247921.87,187545.03 Europe,Slovakia,Vegetables,Online,H,10/6/2012,759224212,11/10/2012,171,154.06,90.93,26344.26,15549.03,10795.23 Asia,Myanmar,Clothes,Online,H,11/14/2015,223359620,11/18/2015,5930,109.28,35.84,648030.40,212531.20,435499.20 Sub-Saharan Africa,Comoros,Cereal,Offline,H,3/29/2016,902102267,4/29/2016,962,205.70,117.11,197883.40,112659.82,85223.58 Europe,Iceland,Cosmetics,Online,C,12/31/2016,331438481,12/31/2016,8867,437.20,263.33,3876652.40,2334947.11,1541705.29 Europe,Switzerland,Personal Care,Online,M,12/23/2010,617667090,1/31/2011,273,81.73,56.67,22312.29,15470.91,6841.38 Europe,Macedonia,Clothes,Offline,C,10/14/2014,787399423,11/14/2014,7842,109.28,35.84,856973.76,281057.28,575916.48 Sub-Saharan Africa,Mauritania,Office Supplies,Offline,C,1/11/2012,837559306,1/13/2012,1266,651.21,524.96,824431.86,664599.36,159832.50 Europe,Albania,Clothes,Online,C,2/2/2010,385383069,3/18/2010,2269,109.28,35.84,247956.32,81320.96,166635.36 Sub-Saharan Africa,Lesotho,Fruits,Online,L,8/18/2013,918419539,9/18/2013,9606,9.33,6.92,89623.98,66473.52,23150.46 Middle East and North Africa,Saudi Arabia,Cereal,Online,M,3/25/2013,844530045,3/28/2013,4063,205.70,117.11,835759.10,475817.93,359941.17 Sub-Saharan Africa,Sierra Leone,Office Supplies,Offline,M,11/26/2011,441888415,1/7/2012,3457,651.21,524.96,2251232.97,1814786.72,436446.25 Sub-Saharan Africa,Sao Tome and Principe,Fruits,Offline,H,9/17/2013,508980977,10/24/2013,7637,9.33,6.92,71253.21,52848.04,18405.17 Sub-Saharan Africa,Cote d'Ivoire,Clothes,Online,C,6/8/2012,114606559,6/27/2012,3482,109.28,35.84,380512.96,124794.88,255718.08 Australia and Oceania,Fiji,Clothes,Offline,C,6/30/2010,647876489,8/1/2010,9905,109.28,35.84,1082418.40,354995.20,727423.20 Europe,Austria,Cosmetics,Offline,H,2/23/2015,868214595,3/2/2015,2847,437.20,263.33,1244708.40,749700.51,495007.89 Europe,United Kingdom,Household,Online,L,1/5/2012,955357205,2/14/2012,282,668.27,502.54,188452.14,141716.28,46735.86 Sub-Saharan Africa,Djibouti,Cosmetics,Offline,H,4/7/2014,259353148,4/19/2014,7215,437.20,263.33,3154398.00,1899925.95,1254472.05 Australia and Oceania,Australia,Cereal,Offline,H,6/9/2013,450563752,7/2/2013,682,205.70,117.11,140287.40,79869.02,60418.38 Europe,San Marino,Baby Food,Online,L,6/26/2013,569662845,7/1/2013,4750,255.28,159.42,1212580.00,757245.00,455335.00 Sub-Saharan Africa,Cameroon,Office Supplies,Online,M,11/7/2011,177636754,11/15/2011,5518,651.21,524.96,3593376.78,2896729.28,696647.50 Middle East and North Africa,Libya,Clothes,Offline,H,10/30/2010,705784308,11/17/2010,6116,109.28,35.84,668356.48,219197.44,449159.04 Central America and the Caribbean,Haiti,Cosmetics,Offline,H,10/13/2013,505716836,11/16/2013,1705,437.20,263.33,745426.00,448977.65,296448.35 Sub-Saharan Africa,Rwanda,Cosmetics,Offline,H,10/11/2013,699358165,11/25/2013,4477,437.20,263.33,1957344.40,1178928.41,778415.99 Sub-Saharan Africa,Gabon,Personal Care,Offline,L,7/8/2012,228944623,7/9/2012,8656,81.73,56.67,707454.88,490535.52,216919.36 Central America and the Caribbean,Belize,Clothes,Offline,M,7/25/2016,807025039,9/7/2016,5498,109.28,35.84,600821.44,197048.32,403773.12 Europe,Lithuania,Office Supplies,Offline,H,10/24/2010,166460740,11/17/2010,8287,651.21,524.96,5396577.27,4350343.52,1046233.75 Sub-Saharan Africa,Madagascar,Clothes,Offline,L,4/25/2015,610425555,5/28/2015,7342,109.28,35.84,802333.76,263137.28,539196.48 Asia,Turkmenistan,Office Supplies,Online,M,4/23/2013,462405812,5/20/2013,5010,651.21,524.96,3262562.10,2630049.60,632512.50 Middle East and North Africa,Libya,Fruits,Online,L,8/14/2015,816200339,9/30/2015,673,9.33,6.92,6279.09,4657.16,1621.93 Sub-Saharan Africa,Democratic Republic of the Congo,Beverages,Online,C,5/26/2011,585920464,7/15/2011,5741,47.45,31.79,272410.45,182506.39,89904.06 Sub-Saharan Africa,Djibouti,Cereal,Online,H,5/20/2017,555990016,6/17/2017,8656,205.70,117.11,1780539.20,1013704.16,766835.04 Middle East and North Africa,Pakistan,Cosmetics,Offline,L,7/5/2013,231145322,8/16/2013,9892,437.20,263.33,4324782.40,2604860.36,1719922.04 North America,Mexico,Household,Offline,C,11/6/2014,986435210,12/12/2014,6954,668.27,502.54,4647149.58,3494663.16,1152486.42 Australia and Oceania,Federated States of Micronesia,Beverages,Online,C,10/28/2014,217221009,11/15/2014,9379,47.45,31.79,445033.55,298158.41,146875.14 Asia,Laos,Vegetables,Offline,C,9/15/2011,789176547,10/23/2011,3732,154.06,90.93,574951.92,339350.76,235601.16 Europe,Monaco,Baby Food,Offline,H,5/29/2012,688288152,6/2/2012,8614,255.28,159.42,2198981.92,1373243.88,825738.04 Australia and Oceania,Samoa ,Cosmetics,Online,H,7/20/2013,670854651,8/7/2013,9654,437.20,263.33,4220728.80,2542187.82,1678540.98 Europe,Spain,Household,Offline,L,10/21/2012,213487374,11/30/2012,4513,668.27,502.54,3015902.51,2267963.02,747939.49 Middle East and North Africa,Lebanon,Clothes,Online,L,9/18/2012,663110148,10/8/2012,7884,109.28,35.84,861563.52,282562.56,579000.96 Middle East and North Africa,Iran,Cosmetics,Online,H,11/15/2016,286959302,12/8/2016,6489,437.20,263.33,2836990.80,1708748.37,1128242.43 Sub-Saharan Africa,Zambia,Snacks,Online,L,1/4/2011,122583663,1/5/2011,4085,152.58,97.44,623289.30,398042.40,225246.90 Sub-Saharan Africa,Kenya,Vegetables,Online,L,3/18/2012,827844560,4/7/2012,6457,154.06,90.93,994765.42,587135.01,407630.41 North America,Mexico,Personal Care,Offline,L,2/17/2012,430915820,3/20/2012,6422,81.73,56.67,524870.06,363934.74,160935.32 Sub-Saharan Africa,Sao Tome and Principe,Beverages,Offline,C,1/16/2011,180283772,1/21/2011,8829,47.45,31.79,418936.05,280673.91,138262.14 Sub-Saharan Africa,The Gambia,Baby Food,Offline,M,2/3/2014,494747245,3/20/2014,5559,255.28,159.42,1419101.52,886215.78,532885.74 Middle East and North Africa,Kuwait,Fruits,Online,M,4/30/2012,513417565,5/18/2012,522,9.33,6.92,4870.26,3612.24,1258.02 Europe,Slovenia,Beverages,Offline,C,10/23/2016,345718562,11/25/2016,4660,47.45,31.79,221117.00,148141.40,72975.60 Sub-Saharan Africa,Sierra Leone,Office Supplies,Offline,H,12/6/2016,621386563,12/14/2016,948,651.21,524.96,617347.08,497662.08,119685.00 Australia and Oceania,Australia,Beverages,Offline,H,7/7/2014,240470397,7/11/2014,9389,47.45,31.79,445508.05,298476.31,147031.74 Middle East and North Africa,Azerbaijan,Office Supplies,Online,M,6/13/2012,423331391,7/24/2012,2021,651.21,524.96,1316095.41,1060944.16,255151.25 Europe,Romania,Cosmetics,Online,H,11/26/2010,660643374,12/25/2010,7910,437.20,263.33,3458252.00,2082940.30,1375311.70 Central America and the Caribbean,Nicaragua,Beverages,Offline,C,2/8/2011,963392674,3/21/2011,8156,47.45,31.79,387002.20,259279.24,127722.96 Sub-Saharan Africa,Mali,Clothes,Online,M,7/26/2011,512878119,9/3/2011,888,109.28,35.84,97040.64,31825.92,65214.72 Asia,Malaysia,Fruits,Offline,L,11/11/2011,810711038,12/28/2011,6267,9.33,6.92,58471.11,43367.64,15103.47 Sub-Saharan Africa,Sierra Leone,Vegetables,Offline,C,6/1/2016,728815257,6/29/2016,1485,154.06,90.93,228779.10,135031.05,93748.05 North America,Mexico,Personal Care,Offline,M,7/30/2015,559427106,8/8/2015,5767,81.73,56.67,471336.91,326815.89,144521.02 Sub-Saharan Africa,Mozambique,Household,Offline,L,2/10/2012,665095412,2/15/2012,5367,668.27,502.54,3586605.09,2697132.18,889472.91 ================================================ FILE: common/workflow-core/src/test/resources/datasets/1/directory/a.csv ================================================ a,b,c 1,2,3 ================================================ FILE: common/workflow-core/src/test/resources/datasets/1/random_data.csv ================================================ Column1,Column2,Column3,Column4,Column5,Column6 EvrcV,26,0.06067039007379471,Yes,k,C YnHds,27,0.9387539870726667,No,E,C rksVm,6,0.9344931557793014,Yes,b,D eqzNn,97,0.7037098509059294,No,O,C zmuVi,20,0.8147046178705271,Yes,q,C upHfB,55,0.4712155316388439,No,s,C mwyje,49,0.6525030237720966,No,j,C OCAnU,55,0.47413059111627787,No,F,B ugEGD,14,0.07025010405580678,Yes,h,A YWiyx,44,0.42460389666717013,Yes,S,C fjRUf,13,0.7412344837693339,Yes,S,A AuuJw,10,0.4593863012758286,No,I,C UtHwJ,57,0.26180762634806287,Yes,S,C xPYSY,28,0.667158130221986,No,l,B mOTGN,22,0.9910612329469569,Yes,q,A aKcJS,33,0.4892585888456522,No,k,D LCWPs,52,0.009099149863733391,No,M,A CXDlO,54,0.4130046073677608,Yes,J,C Jbpaj,1,0.012118952480451806,No,K,D spdai,61,0.9534430864332027,Yes,y,C ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/amber/core/WorkflowRuntimeExceptionSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core import org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers class WorkflowRuntimeExceptionSpec extends AnyFlatSpec with Matchers { private val worker = ActorVirtualIdentity("Worker:WF1-myOp-main-0") "WorkflowRuntimeException(message)" should "carry the message and default to no related worker" in { val ex = new WorkflowRuntimeException("boom") ex.message shouldBe "boom" ex.relatedWorkerId shouldBe None ex.getCause shouldBe null } "WorkflowRuntimeException(message, cause, workerId)" should "preserve message, attach cause, and record the worker" in { val cause = new IllegalStateException("inner") val ex = new WorkflowRuntimeException("outer", cause, Some(worker)) ex.message shouldBe "outer" ex.getCause should be theSameInstanceAs cause ex.relatedWorkerId shouldBe Some(worker) } "WorkflowRuntimeException(cause, workerId)" should "derive the message from cause.toString" in { val cause = new IllegalArgumentException("bad arg") val ex = new WorkflowRuntimeException(cause, Some(worker)) ex.message shouldBe cause.toString ex.getCause should be theSameInstanceAs cause ex.relatedWorkerId shouldBe Some(worker) } "WorkflowRuntimeException(cause)" should "derive the message and leave the worker unset" in { val cause = new RuntimeException("inner") val ex = new WorkflowRuntimeException(cause) ex.message shouldBe cause.toString ex.getCause should be theSameInstanceAs cause ex.relatedWorkerId shouldBe None } it should "fall back to a null message when the cause is null" in { // Pin: `Option(cause).map(_.toString).orNull` returns null for a null // cause, which then propagates into RuntimeException(null) — the parent // exception accepts that and reports getMessage as null. val ex = new WorkflowRuntimeException(null: Throwable) ex.message shouldBe null ex.getCause shouldBe null } "WorkflowRuntimeException()" should "produce a message-less exception with no cause and no worker" in { val ex = new WorkflowRuntimeException() ex.message shouldBe null ex.relatedWorkerId shouldBe None ex.getCause shouldBe null } "toString" should "return the raw message field rather than the JVM default" in { // The override returns `message` (or null) — not RuntimeException's // default `: ` format. val ex = new WorkflowRuntimeException("oops") ex.toString shouldBe "oops" } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/amber/core/executor/CoreExecutorReflectionSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.executor import org.apache.texera.amber.core.state.State import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple, TupleLike} import org.scalatest.flatspec.AnyFlatSpec class CoreExecutorReflectionSpec extends AnyFlatSpec { // --------------------------------------------------------------------------- // OperatorExecutor trait defaults // --------------------------------------------------------------------------- private val schema: Schema = Schema().add(new Attribute("v", AttributeType.INTEGER)) private def tuple(v: Int): Tuple = Tuple.builder(schema).add(schema.getAttribute("v"), Integer.valueOf(v)).build() /** Minimal concrete subclass — only `processTuple` is abstract. */ private class IdentityExec extends OperatorExecutor { override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = Iterator.single(tuple) } "OperatorExecutor.open" should "default to a no-op" in { val exec = new IdentityExec exec.open() // should not throw succeed } "OperatorExecutor.close" should "default to a no-op" in { val exec = new IdentityExec exec.close() succeed } "OperatorExecutor.produceStateOnStart" should "default to None for any port" in { val exec = new IdentityExec assert(exec.produceStateOnStart(0).isEmpty) assert(exec.produceStateOnStart(7).isEmpty) } "OperatorExecutor.processState" should "default to passing the state through unchanged" in { val exec = new IdentityExec val state = State(Map[String, Any]("k" -> 1)) assert(exec.processState(state, 0).contains(state)) } "OperatorExecutor.processTupleMultiPort" should "default to delegating to processTuple with no port routing" in { val exec = new IdentityExec val out = exec.processTupleMultiPort(tuple(1), 0).toList assert(out.size == 1) assert(out.head._1.asInstanceOf[Tuple] == tuple(1)) assert(out.head._2.isEmpty) } "OperatorExecutor.produceStateOnFinish" should "default to None for any port" in { val exec = new IdentityExec assert(exec.produceStateOnFinish(0).isEmpty) } "OperatorExecutor.onFinish" should "default to an empty iterator" in { val exec = new IdentityExec assert(exec.onFinish(0).isEmpty) } "OperatorExecutor.onFinishMultiPort" should "default to delegating to onFinish with no port routing" in { val exec = new IdentityExec assert(exec.onFinishMultiPort(0).isEmpty) } // --------------------------------------------------------------------------- // SourceOperatorExecutor trait defaults // --------------------------------------------------------------------------- private class CountingSource extends SourceOperatorExecutor { override def produceTuple(): Iterator[TupleLike] = List(tuple(1), tuple(2), tuple(3)).iterator } "SourceOperatorExecutor.processTuple" should "always return an empty iterator" in { val exec = new CountingSource assert(exec.processTuple(tuple(99), 0).isEmpty) } "SourceOperatorExecutor.processTupleMultiPort" should "always return an empty iterator" in { val exec = new CountingSource assert(exec.processTupleMultiPort(tuple(99), 0).isEmpty) } "SourceOperatorExecutor.onFinishMultiPort" should "delegate to produceTuple with no port routing" in { val exec = new CountingSource val out = exec.onFinishMultiPort(0).toList assert(out.size == 3) assert(out.map(_._1.asInstanceOf[Tuple]) == List(tuple(1), tuple(2), tuple(3))) assert(out.forall(_._2.isEmpty)) } // --------------------------------------------------------------------------- // ExecFactory.newExecFromJavaClassName // --------------------------------------------------------------------------- "ExecFactory.newExecFromJavaClassName" should "instantiate a no-arg constructor when no descString is given" in { val exec = ExecFactory.newExecFromJavaClassName( classOf[CoreExecutorReflectionSpec.NoArgExec].getName ) assert(exec.isInstanceOf[CoreExecutorReflectionSpec.NoArgExec]) } it should "instantiate a (String) constructor when descString is provided" in { val exec = ExecFactory.newExecFromJavaClassName( classOf[CoreExecutorReflectionSpec.StringArgExec].getName, descString = "hello" ) val typed = exec.asInstanceOf[CoreExecutorReflectionSpec.StringArgExec] assert(typed.desc == "hello") } it should "fall back to (Int, Int) constructor for parallelizable executors with no descString" in { val exec = ExecFactory.newExecFromJavaClassName( classOf[CoreExecutorReflectionSpec.IdxCountExec].getName, idx = 3, workerCount = 7 ) val typed = exec.asInstanceOf[CoreExecutorReflectionSpec.IdxCountExec] assert(typed.idx == 3) assert(typed.workerCount == 7) } it should "fall back to (String, Int, Int) constructor when descString is given" in { val exec = ExecFactory.newExecFromJavaClassName( classOf[CoreExecutorReflectionSpec.StringIdxCountExec].getName, descString = "hi", idx = 1, workerCount = 4 ) val typed = exec.asInstanceOf[CoreExecutorReflectionSpec.StringIdxCountExec] assert(typed.desc == "hi") assert(typed.idx == 1) assert(typed.workerCount == 4) } it should "raise ClassNotFoundException for unknown class names" in { assertThrows[ClassNotFoundException] { ExecFactory.newExecFromJavaClassName("does.not.exist.AtAll") } } // --------------------------------------------------------------------------- // JavaRuntimeCompilation.compileCode // // A success-path test that compiles a real OperatorExecutor subclass from a // string is intentionally omitted: `compiler.getTask(...)` is invoked with // null compilation options, which means the system javac picks up its own // (test) classpath rather than the project classpath. Under sbt test that // does not include workflow-core itself, so the compile fails with // "package org.apache.texera... does not exist" — a deployment-environment // artifact rather than a contract violation. We exercise just the diagnostic // path here. // --------------------------------------------------------------------------- "JavaRuntimeCompilation.compileCode" should "compile a self-contained Java class with no external deps" in { val src = """public class JavaUDFOpExec { | public int compute() { return 42; } |}""".stripMargin val cls = JavaRuntimeCompilation.compileCode(src) assert(cls.getName == "org.apache.texera.amber.operators.udf.java.JavaUDFOpExec") val instance = cls.getDeclaredConstructor().newInstance() val result = cls.getMethod("compute").invoke(instance).asInstanceOf[Integer] assert(result == 42) } it should "raise RuntimeException with diagnostics when the source has syntax errors" in { val ex = intercept[RuntimeException] { JavaRuntimeCompilation.compileCode("public class Garbage { not valid java }") } assert(ex.getMessage.contains("Error at line")) } } private object CoreExecutorReflectionSpec { // Public so reflection inside ExecFactory can reach the no-arg constructor. class NoArgExec extends OperatorExecutor { override def processTuple( tuple: org.apache.texera.amber.core.tuple.Tuple, port: Int ): Iterator[org.apache.texera.amber.core.tuple.TupleLike] = Iterator.empty } class StringArgExec(val desc: String) extends OperatorExecutor { override def processTuple( tuple: org.apache.texera.amber.core.tuple.Tuple, port: Int ): Iterator[org.apache.texera.amber.core.tuple.TupleLike] = Iterator.empty } class IdxCountExec(val idx: Int, val workerCount: Int) extends OperatorExecutor { override def processTuple( tuple: org.apache.texera.amber.core.tuple.Tuple, port: Int ): Iterator[org.apache.texera.amber.core.tuple.TupleLike] = Iterator.empty } class StringIdxCountExec(val desc: String, val idx: Int, val workerCount: Int) extends OperatorExecutor { override def processTuple( tuple: org.apache.texera.amber.core.tuple.Tuple, port: Int ): Iterator[org.apache.texera.amber.core.tuple.TupleLike] = Iterator.empty } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/amber/core/state/StateSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.state import org.scalatest.flatspec.AnyFlatSpec class StateSpec extends AnyFlatSpec { "State" should "json-round-trip an empty state" in { val original = State(Map.empty) assert(State.fromJson(original.toJson) == original) } it should "json-round-trip primitive values" in { val original = State( Map( "string" -> "hello", "long" -> 42L, "double" -> 3.14, "bool_true" -> true, "bool_false" -> false ) ) val decoded = State.fromJson(original.toJson) assert(decoded.values("string") == "hello") assert(decoded.values("long") == 42L) assert(decoded.values("double") == 3.14) assert(decoded.values("bool_true") == true) assert(decoded.values("bool_false") == false) } it should "drop null entries during JSON serialization" in { // The shared `objectMapper` is configured with `Include.NON_NULL`, so // null values are stripped before they hit the wire. Document the // behavior here so callers know they cannot transport an explicit null // through a State -- Python's serializer keeps nulls but Scala does not. val original = State(Map("present" -> "value", "absent" -> null)) val decoded = State.fromJson(original.toJson) assert(decoded.values.keySet == Set("present")) assert(decoded.values("present") == "value") } it should "json-round-trip byte arrays via the bytes type marker" in { val payload = Array[Byte](0, 1, 2, -1) val original = State(Map("payload" -> payload)) val decoded = State.fromJson(original.toJson) val decodedBytes = decoded.values("payload").asInstanceOf[Array[Byte]] assert(decodedBytes.sameElements(payload)) } it should "json-round-trip nested maps" in { val original = State(Map("outer" -> Map("inner" -> Map("value" -> 1L)))) val decoded = State.fromJson(original.toJson) assert(decoded == original) } it should "json-round-trip lists of mixed values" in { val original = State(Map("items" -> List(1L, "two", 3.0, true, null))) val decoded = State.fromJson(original.toJson) assert(decoded == original) } it should "json-round-trip byte arrays nested inside lists and maps" in { val original = State( Map( "blobs" -> List(Array[Byte](1, 2), Array[Byte](3, 4)), "nested" -> Map("sub_blob" -> Array[Byte](5, 6)) ) ) val decoded = State.fromJson(original.toJson) val blobs = decoded.values("blobs").asInstanceOf[List[Array[Byte]]] assert(blobs.head.sameElements(Array[Byte](1, 2))) assert(blobs(1).sameElements(Array[Byte](3, 4))) val subBlob = decoded.values .apply("nested") .asInstanceOf[Map[String, Any]]("sub_blob") .asInstanceOf[Array[Byte]] assert(subBlob.sameElements(Array[Byte](5, 6))) } it should "tuple-round-trip" in { val original = State( Map( "loop_counter" -> 3L, "label" -> "outer", "blob" -> Array[Byte](1, 2) ) ) val decoded = State.fromTuple(original.toTuple) assert(decoded.values("loop_counter") == 3L) assert(decoded.values("label") == "outer") assert( decoded.values("blob").asInstanceOf[Array[Byte]].sameElements(Array[Byte](1, 2)) ) } it should "produce a tuple whose payload is the JSON serialization" in { val tuple = State(Map("x" -> 1L)).toTuple assert(tuple.getSchema == State.schema) assert(tuple.getField[String]("content") == """{"x":1}""") } it should "decode a payload encoded by the Python serializer" in { // Wire-format compatibility check: the bytes-marker keys and the // single-row "content" column must match what core/models/state.py // emits, otherwise cross-language transport breaks. val pythonEmitted = """{"i":2,"blob":{"__texera_type__":"bytes","payload":"AQID"}}""" val decoded = State.fromJson(pythonEmitted) assert(decoded.values("i") == 2L) assert( decoded.values("blob").asInstanceOf[Array[Byte]].sameElements(Array[Byte](1, 2, 3)) ) } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/VFSURIFactorySpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage import org.apache.texera.amber.core.virtualidentity.{ ExecutionIdentity, OperatorIdentity, PhysicalOpIdentity, WorkflowIdentity } import org.apache.texera.amber.core.workflow.{GlobalPortIdentity, PortIdentity} import org.scalatest.flatspec.AnyFlatSpec import java.net.URI class VFSURIFactorySpec extends AnyFlatSpec { private val workflowId = WorkflowIdentity(7L) private val executionId = ExecutionIdentity(11L) private val operatorId = OperatorIdentity("opA") private val portId = GlobalPortIdentity( PhysicalOpIdentity(operatorId, "main"), PortIdentity(0), input = true ) "VFSURIFactory.createResultURI" should "include workflow, execution, port, and the result resource type" in { val uri = VFSURIFactory.createResultURI(workflowId, executionId, portId) assert(uri.getScheme == VFSURIFactory.VFS_FILE_URI_SCHEME) val path = uri.getPath assert(path.contains("/wid/7")) assert(path.contains("/eid/11")) assert(path.contains("/globalportid/")) assert(path.endsWith("/result")) } it should "round-trip through decodeURI" in { val uri = VFSURIFactory.createResultURI(workflowId, executionId, portId) val (wid, eid, globalPortIdOpt, resourceType) = VFSURIFactory.decodeURI(uri) assert(wid == workflowId) assert(eid == executionId) assert(globalPortIdOpt.contains(portId)) assert(resourceType == VFSResourceType.RESULT) } "VFSURIFactory.createRuntimeStatisticsURI" should "produce a runtimeStatistics URI without an opid segment" in { val uri = VFSURIFactory.createRuntimeStatisticsURI(workflowId, executionId) val path = uri.getPath assert(path.endsWith("/runtimestatistics")) assert(!path.contains("/opid/")) val (wid, eid, globalPortIdOpt, resourceType) = VFSURIFactory.decodeURI(uri) assert(wid == workflowId) assert(eid == executionId) assert(globalPortIdOpt.isEmpty) assert(resourceType == VFSResourceType.RUNTIME_STATISTICS) } "VFSURIFactory.createConsoleMessagesURI" should "embed the operator id and the consoleMessages resource type" in { val uri = VFSURIFactory.createConsoleMessagesURI(workflowId, executionId, operatorId) val path = uri.getPath assert(path.contains(s"/opid/${operatorId.id}")) assert(path.endsWith("/consolemessages")) // The current `decodeURI` does not extract the operator id (it has no // "opid" branch), so we only round-trip wid/eid/resourceType here. val (wid, eid, globalPortIdOpt, resourceType) = VFSURIFactory.decodeURI(uri) assert(wid == workflowId) assert(eid == executionId) assert(globalPortIdOpt.isEmpty) assert(resourceType == VFSResourceType.CONSOLE_MESSAGES) } "VFSURIFactory.decodeURI" should "reject URIs with a non-vfs scheme" in { assertThrows[IllegalArgumentException] { VFSURIFactory.decodeURI(new URI("http:///wid/1/eid/1/result")) } } it should "reject URIs missing required segments" in { assertThrows[IllegalArgumentException] { VFSURIFactory.decodeURI(new URI("vfs:///wid/1/result")) } } it should "reject URIs whose final segment is not a known resource type" in { assertThrows[IllegalArgumentException] { VFSURIFactory.decodeURI(new URI("vfs:///wid/1/eid/2/notarealresource")) } } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/model/VirtualDocumentSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage.model import org.scalatest.BeforeAndAfterEach import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.time.SpanSugar.convertIntToGrainOfTime import java.util.UUID import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.{Await, Future} /** * A trait for testing VirtualDocument implementations. * Provides common read/write test cases and hooks for subclasses to customize. * @tparam T the type of data that the VirtualDocument handles. */ trait VirtualDocumentSpec[T] extends AnyFlatSpec with BeforeAndAfterEach { /** * Constructs the VirtualDocument instance to be tested. * Subclasses should override this to provide their specific implementation. */ def getDocument: VirtualDocument[T] // VirtualDocument instance for each test var document: VirtualDocument[T] = _ override def beforeEach(): Unit = { document = getDocument } "VirtualDocument" should "write and read items successfully" in { val items = generateSampleItems() // Get writer and write items val writer = document.writer(UUID.randomUUID().toString) writer.open() items.foreach(writer.putOne) writer.close() // Read items back val retrievedItems = document.get().toList assert(retrievedItems.toSet == items.toSet) } "VirtualDocument" should "read items while writer is writing new data" in { val allItems = generateSampleItems() // Split the items into two batches val (batch1, batch2) = allItems.splitAt(allItems.length / 2) // Create a reader before any data is written val reader = document.get() assert(!reader.hasNext, "Reader should initially have no data.") // Write the first batch val writer = document.writer(UUID.randomUUID().toString) writer.open() batch1.foreach(writer.putOne) writer.close() // The reader should detect and read the first batch val retrievedBatch1 = reader.take(batch1.length).toList assert(retrievedBatch1.toSet == batch1.toSet, "Reader should read the first batch correctly.") // Write the second batch val writer2 = document.writer(UUID.randomUUID().toString) writer2.open() batch2.foreach(writer2.putOne) writer2.close() // The reader should detect and read the second batch val retrievedBatch2 = reader.toList assert(retrievedBatch2.toSet == batch2.toSet, "Reader should read the second batch correctly.") } it should "clear the document" in { val items = generateSampleItems() // Write items val writer = document.writer(UUID.randomUUID().toString) writer.open() items.foreach(writer.putOne) writer.close() // Ensure items are written assert(document.get().nonEmpty, "The document should contain items before clearing.") // Clear the document document.clear() // Check if the document is cleared assert(document.get().isEmpty, "The document should have no items after clearing.") } it should "handle empty reads gracefully" in { val retrievedItems = document.get().toList assert(retrievedItems.isEmpty, "Reading from an empty document should return an empty list.") } it should "handle concurrent writes and read all items correctly" in { val allItems = generateSampleItems() val numWriters = 10 // Calculate the batch size and the remainder val batchSize = allItems.length / numWriters val remainder = allItems.length % numWriters // Create writer's batches val itemBatches = (0 until numWriters).map { i => val start = i * batchSize + Math.min(i, remainder) val end = start + batchSize + (if (i < remainder) 1 else 0) allItems.slice(start, end) }.toList assert( itemBatches.length == numWriters, s"Expected $numWriters batches but got ${itemBatches.length}" ) // Perform concurrent writes val writeFutures = itemBatches.map { batch => Future { val writer = document.writer(UUID.randomUUID().toString) writer.open() batch.foreach(writer.putOne) writer.close() } } // Wait for all writers to complete Await.result(Future.sequence(writeFutures), 30.seconds) // Read all items back val retrievedItems = document.get().toList // Verify that the retrieved items match the original items assert( retrievedItems.toSet == allItems.toSet, "All items should be read correctly after concurrent writes." ) } it should "allow a reader to read data while a writer is writing items incrementally" in { val allItems = generateSampleItems() val numBatches = 5 val batchSize = Math.max(1, allItems.length / numBatches) // Ensure batchSize is at least 1 // Split items into batches val itemBatches = allItems.grouped(batchSize).toList // Flag to indicate when writing is done @volatile var writingComplete = false // Start the writer in a Future to write batches with delays val writerFuture = Future { val writer = document.writer(UUID.randomUUID().toString) writer.open() try { itemBatches.foreach { batch => batch.foreach(writer.putOne) Thread.sleep(500) // Simulate delay between batches } } finally { writer.close() writingComplete = true } } // Start the reader in another Future val readerFuture = Future { val reader = document.get() val retrievedItems = scala.collection.mutable.ListBuffer[T]() // Keep checking for new data until writing is complete and no more items are available while (!writingComplete || reader.hasNext) { if (reader.hasNext) { retrievedItems += reader.next() } else { Thread.sleep(200) // Wait before retrying to avoid busy-waiting } } retrievedItems.toList } // Wait for both writer and reader to complete val retrievedItems = Await.result(readerFuture, 30.seconds) Await.result(writerFuture, 30.seconds) // Verify that the retrieved items match the original items assert( retrievedItems.toSet == allItems.toSet, "All items should be read correctly while writing is happening concurrently." ) } it should "read all items using ranges correctly" in { val allItems = generateSampleItems() // Write items val writer = document.writer(UUID.randomUUID().toString) writer.open() allItems.foreach(writer.putOne) writer.close() // Read all items using ranges val batchSize = 1500 val ranges = allItems.indices.grouped(batchSize).toList val retrievedItems = ranges.flatMap { range => document.getRange(range.head, range.lastOption.getOrElse(range.head) + 1).toList } assert(retrievedItems.size == allItems.size) // Verify that the retrieved items match the original items assert( retrievedItems.toSet == allItems.toSet, "All items should be retrieved correctly using ranges." ) } it should "retrieve items correctly using getAfter" in { val allItems = generateSampleItems() // Write items val writer = document.writer(UUID.randomUUID().toString) writer.open() allItems.foreach(writer.putOne) writer.close() // Test getAfter for various offsets val offsets = List(0, allItems.length / 2, allItems.length - 1) offsets.foreach { offset => val expectedItems = if (offset < allItems.length) { allItems.slice(offset, allItems.length) } else { List.empty[T] } val retrievedItems = document.getAfter(offset).toList assert( retrievedItems == expectedItems, s"getAfter($offset) did not return the expected items. Expected: $expectedItems, Got: $retrievedItems" ) } // Test getAfter for an offset beyond the range val invalidOffset = allItems.length val retrievedItems = document.getAfter(invalidOffset).toList assert( retrievedItems.isEmpty, s"getAfter($invalidOffset) should return an empty list, but got: $retrievedItems" ) } it should "get the count of records correctly" in { val allItems = generateSampleItems() // Write items val writer = document.writer(UUID.randomUUID().toString) writer.open() allItems.foreach(writer.putOne) writer.close() assert( allItems.length == document.getCount, "getCount should return the same number with allItems" ) } /** * Generates a sample list of items for testing. * Subclasses should override this to provide their specific sample items. * @return a list of sample items of type T. */ def generateSampleItems(): List[T] } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/util/StorageUtilSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.storage.util import org.scalatest.flatspec.AnyFlatSpec import java.util.concurrent.locks.{ReentrantLock, ReentrantReadWriteLock} class StorageUtilSpec extends AnyFlatSpec { "StorageUtil.withWriteLock" should "execute the block and return its value" in { val rwLock = new ReentrantReadWriteLock() val result = StorageUtil.withWriteLock(rwLock)("written") assert(result == "written") assert(!rwLock.writeLock().isHeldByCurrentThread) } it should "release the write lock even when the block throws" in { val rwLock = new ReentrantReadWriteLock() val ex = intercept[RuntimeException] { StorageUtil.withWriteLock(rwLock)(throw new RuntimeException("boom")) } assert(ex.getMessage == "boom") assert(!rwLock.isWriteLocked) } "StorageUtil.withReadLock" should "execute the block and release the read lock" in { val rwLock = new ReentrantReadWriteLock() val result = StorageUtil.withReadLock(rwLock)(42) assert(result == 42) assert(rwLock.getReadLockCount == 0) } it should "release the read lock even when the block throws" in { val rwLock = new ReentrantReadWriteLock() intercept[IllegalStateException] { StorageUtil.withReadLock(rwLock)(throw new IllegalStateException("boom")) } assert(rwLock.getReadLockCount == 0) } "StorageUtil.withLock" should "execute the block and release the lock" in { val lock = new ReentrantLock() val result = StorageUtil.withLock(lock)("done") assert(result == "done") assert(!lock.isHeldByCurrentThread) } it should "release the lock even when the block throws" in { val lock = new ReentrantLock() intercept[ArithmeticException] { StorageUtil.withLock(lock)(throw new ArithmeticException("boom")) } assert(!lock.isHeldByCurrentThread) assert(!lock.isLocked) } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/amber/core/tuple/AttributeTypeUtilsSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.tuple import org.apache.texera.amber.core.tuple.AttributeType._ import org.apache.texera.amber.core.tuple.AttributeTypeUtils.{ AttributeTypeException, add, compare, inferField, inferSchemaFromRows, maxValue, minValue, parseField, zeroValue } import org.scalatest.funsuite.AnyFunSuite import java.sql.Timestamp import java.time.{Instant, LocalDate, LocalDateTime, OffsetDateTime, ZoneId, ZonedDateTime} class AttributeTypeUtilsSpec extends AnyFunSuite { // Unit Test for Infer Schema test("type should get inferred correctly individually") { assert(inferField(" 1 \n\n") == INTEGER) assert(inferField(" 1.1\t") == DOUBLE) assert(inferField("1,111.1 ") == STRING) assert(inferField("k2068-10-29T18:43:15.000Z") == STRING) assert(inferField(" 12321321312321312312321321 ") == DOUBLE) assert(inferField(" 123,123,123,123,123,123,123.11") == STRING) assert(inferField(" 00\t") == INTEGER) assert(inferField("\t-.2 ") == DOUBLE) assert(inferField("\n False ") == BOOLEAN) assert(inferField("07/10/96 4:5 PM, PDT") == TIMESTAMP) assert(inferField("02/2/2020") == TIMESTAMP) assert(inferField("\n\n02/2/23 ") == TIMESTAMP) assert(inferField(" 2023年8月7日 ") == TIMESTAMP) assert( inferField("2020-12-31T23:25:59.999Z") == TIMESTAMP ) // ISO format with milliseconds and UTC assert(inferField("2020-12-31T11:59:59+01:00") == TIMESTAMP) // ISO format with timezone offset assert( inferField("2020-12-31T11:59:59") == TIMESTAMP ) // ISO format without milliseconds and timezone assert( inferField("31/12/2020 23:59:59") == TIMESTAMP ) // European datetime format with slash separators assert( inferField("12/31/2020 11:59:59") == TIMESTAMP ) // US datetime format with slash separators assert(inferField("2020-12-31") == TIMESTAMP) // Common date format assert(inferField("31-Dec-2020") == TIMESTAMP) // Date format with three-letter month assert( inferField("Wednesday, 31-Dec-20 23:59:59 GMT") == TIMESTAMP ) // Verbose format with day and timezone assert( inferField("1 Jan 2020 05:30:00 GMT") == TIMESTAMP ) // Another verbose format with timezone assert(inferField("15-Aug-2020 20:20:20") == TIMESTAMP) // Day-Month-Year format with time assert(inferField("2020年12月31日 23:59") == TIMESTAMP) // East Asian date format with time assert(inferField("2020/12/31 23:59") == TIMESTAMP) // Alternate slash format with time } test("types should get inferred correctly with one row") { val row: Array[Any] = Array("string", "1", "2020-01-02T00:05:56.000Z", "1.3", "213214124124124", "true") val rows: Iterator[Array[Any]] = Iterator(row) val attributeTypes = inferSchemaFromRows(rows) assert(attributeTypes(0) == STRING) assert(attributeTypes(1) == INTEGER) assert(attributeTypes(2) == TIMESTAMP) assert(attributeTypes(3) == DOUBLE) assert(attributeTypes(4) == LONG) assert(attributeTypes(5) == BOOLEAN) } test("types should get inferred correctly with multiple rows") { val rows: Iterator[Array[Any]] = Iterator( Array("string", "1 ", "2020-01-02T00:05:56.000Z", "1.3 ", "9223372036854775807", "true"), Array("1932-09-06", "0 ", "1932-09-06T03:47:19Z", "9223.23", "-1", "false "), Array("", "-1", "1979-08-12T10:18:49Z", "-.11", "-9223372036854775808 ", "0"), Array("123,456,789", " -0", " 2023-6-7 8:9:38", " -9.32", "0", "1"), Array("92233720368547758072", "2147483647", "2023-06-27T08:09:38Z", ".1", "1", " TRUE"), Array("\n", "-2147483648", "2068-10-29T18:43:15.000Z ", " 100.00 ", "03685477", "FALSE") ) val attributeTypes = inferSchemaFromRows(rows) assert(attributeTypes(0) == STRING) assert(attributeTypes(1) == INTEGER) assert(attributeTypes(2) == TIMESTAMP) assert(attributeTypes(3) == DOUBLE) assert(attributeTypes(4) == LONG) assert(attributeTypes(5) == BOOLEAN) } test("parseField correctly parses to INTEGER") { assert(parseField("123", AttributeType.INTEGER) == 123) assert(parseField("1,234", AttributeType.INTEGER, force = true) == 1234) assert(parseField(456, AttributeType.INTEGER) == 456) assert(parseField(123.45, AttributeType.INTEGER) == 123) assert(parseField(true, AttributeType.INTEGER) == 1) assert(parseField(false, AttributeType.INTEGER) == 0) assertThrows[AttributeTypeException] { parseField("invalid", AttributeType.INTEGER) } assertThrows[AttributeTypeException] { parseField("1,234", AttributeType.INTEGER) } } test("parseField correctly parses to LONG") { assert(parseField("1234567890", AttributeType.LONG) == 1234567890L) assert(parseField("1,234,567", AttributeType.LONG, force = true) == 1234567L) assert(parseField(12345L, AttributeType.LONG) == 12345L) assert(parseField(123.45, AttributeType.LONG) == 123L) assert(parseField(true, AttributeType.LONG) == 1L) assertThrows[AttributeTypeException] { parseField("invalid", AttributeType.LONG) } assertThrows[AttributeTypeException] { parseField("1,234,567", AttributeType.LONG) } } test("parseField correctly parses to DOUBLE") { assert(parseField("123.45", AttributeType.DOUBLE) == 123.45) assert(parseField(12345, AttributeType.DOUBLE) == 12345.0) assert(parseField(12345L, AttributeType.DOUBLE) == 12345.0) assert(parseField(true, AttributeType.DOUBLE) == 1.0) assertThrows[AttributeTypeException] { parseField("invalid", AttributeType.DOUBLE) } } test("parseField correctly parses to BOOLEAN") { assert(parseField("true", AttributeType.BOOLEAN) == true) assert(parseField("True", AttributeType.BOOLEAN) == true) assert(parseField("TRUE", AttributeType.BOOLEAN) == true) assert(parseField("false", AttributeType.BOOLEAN) == false) assert(parseField("False", AttributeType.BOOLEAN) == false) assert(parseField("FALSE", AttributeType.BOOLEAN) == false) assert(parseField("1", AttributeType.BOOLEAN) == true) assert(parseField("0", AttributeType.BOOLEAN) == false) assert(parseField(1, AttributeType.BOOLEAN) == true) assert(parseField(0, AttributeType.BOOLEAN) == false) assertThrows[AttributeTypeException] { parseField("invalid", AttributeType.BOOLEAN) } } test("parseField correctly parses to TIMESTAMP") { val timestamp = parseField("2023-11-13T10:15:30", AttributeType.TIMESTAMP).asInstanceOf[java.sql.Timestamp] assert(timestamp.toString == "2023-11-13 10:15:30.0") assert( parseField(1699820130000L, AttributeType.TIMESTAMP) .asInstanceOf[java.sql.Timestamp] .getTime == 1699820130000L ) val localDateTime = LocalDateTime.of(2023, 11, 13, 10, 15, 30) val timestampFromLocalDateTime = parseField(localDateTime, AttributeType.TIMESTAMP).asInstanceOf[Timestamp] assert(timestampFromLocalDateTime == Timestamp.valueOf(localDateTime)) val instant = Instant.parse("2023-11-13T10:15:30Z") val timestampFromInstant = parseField(instant, AttributeType.TIMESTAMP).asInstanceOf[Timestamp] assert(timestampFromInstant == Timestamp.from(instant)) val offsetDateTime = OffsetDateTime.parse("2023-11-13T12:15:30+02:00") val timestampFromOffsetDateTime = parseField(offsetDateTime, AttributeType.TIMESTAMP).asInstanceOf[Timestamp] assert(timestampFromOffsetDateTime == Timestamp.from(offsetDateTime.toInstant)) val zonedDateTime = ZonedDateTime.of(2023, 11, 13, 2, 15, 30, 0, ZoneId.of("America/Los_Angeles")) val timestampFromZonedDateTime = parseField(zonedDateTime, AttributeType.TIMESTAMP).asInstanceOf[Timestamp] assert(timestampFromZonedDateTime == Timestamp.from(zonedDateTime.toInstant)) val localDate = LocalDate.of(2023, 11, 13) val timestampFromLocalDate = parseField(localDate, AttributeType.TIMESTAMP).asInstanceOf[Timestamp] assert(timestampFromLocalDate == Timestamp.valueOf(localDate.atStartOfDay())) val utilDate = new java.util.Date(1699820130000L) val timestampFromDate = parseField(utilDate, AttributeType.TIMESTAMP).asInstanceOf[Timestamp] assert(timestampFromDate.getTime == 1699820130000L) assertThrows[AttributeTypeException] { parseField("invalid", AttributeType.TIMESTAMP) } assertThrows[AttributeTypeException] { parseField(123.45, AttributeType.TIMESTAMP) } } test("parseField correctly parses to STRING") { assert(parseField(123, AttributeType.STRING) == "123") assert(parseField(123.45, AttributeType.STRING) == "123.45") assert(parseField(true, AttributeType.STRING) == "true") } test("parseField returns original value for BINARY and ANY") { val binaryData = Array[Byte](1, 2, 3) assert(parseField(binaryData, AttributeType.BINARY) == binaryData) assert(parseField("anything", AttributeType.ANY) == "anything") } test("parseField correctly parses to LARGE_BINARY") { // Valid S3 URI strings are converted to LargeBinary val pointer1 = parseField("s3://bucket/path/to/object", AttributeType.LARGE_BINARY) .asInstanceOf[LargeBinary] assert(pointer1.getUri == "s3://bucket/path/to/object") assert(pointer1.getBucketName == "bucket") assert(pointer1.getObjectKey == "path/to/object") // Null input returns null assert(parseField(null, AttributeType.LARGE_BINARY) == null) } test("LARGE_BINARY type is preserved but never inferred from data") { // LARGE_BINARY remains LARGE_BINARY when passed as typeSoFar assert(inferField(AttributeType.LARGE_BINARY, "any-value") == AttributeType.LARGE_BINARY) assert(inferField(AttributeType.LARGE_BINARY, null) == AttributeType.LARGE_BINARY) // String data is inferred as STRING, never LARGE_BINARY assert(inferField("s3://bucket/path") == AttributeType.STRING) } test("compare correctly handles null values for different attribute types") { assert(compare(null, null, INTEGER) == 0) assert(compare(null, 10, INTEGER) < 0) assert(compare(10, null, INTEGER) > 0) } test("compare correctly orders numeric, boolean, timestamp, string and binary values") { assert(compare(1, 2, INTEGER) < 0) assert(compare(2, 1, INTEGER) > 0) assert(compare(5, 5, INTEGER) == 0) assert(compare(false, true, BOOLEAN) < 0) assert(compare(true, false, BOOLEAN) > 0) assert(compare(true, true, BOOLEAN) == 0) val earlierTimestamp = new java.sql.Timestamp(1000L) val laterTimestamp = new java.sql.Timestamp(2000L) assert(compare(earlierTimestamp, laterTimestamp, TIMESTAMP) < 0) assert(compare(laterTimestamp, earlierTimestamp, TIMESTAMP) > 0) assert(compare("apple", "banana", STRING) < 0) assert(compare("banana", "apple", STRING) > 0) assert(compare("same", "same", STRING) == 0) val firstBytes = Array[Byte](0, 1, 2) val secondBytes = Array[Byte](0, 2, 0) assert(compare(firstBytes, secondBytes, BINARY) < 0) } test("add correctly handles null values as identity for numeric types") { val integerZeroFromAdd = add(null, null, INTEGER).asInstanceOf[Int] assert(integerZeroFromAdd == 0) val rightOnlyResult = add(null, java.lang.Integer.valueOf(5), INTEGER).asInstanceOf[Int] assert(rightOnlyResult == 5) val leftOnlyResult = add(java.lang.Integer.valueOf(7), null, INTEGER).asInstanceOf[Int] assert(leftOnlyResult == 7) } test("add correctly adds integer, long, double and timestamp values") { val integerSum = add(java.lang.Integer.valueOf(3), java.lang.Integer.valueOf(4), INTEGER) .asInstanceOf[Int] assert(integerSum == 7) val longSum = add(java.lang.Long.valueOf(10L), java.lang.Long.valueOf(5L), LONG) .asInstanceOf[Long] assert(longSum == 15L) val doubleSum = add(java.lang.Double.valueOf(1.5), java.lang.Double.valueOf(2.5), DOUBLE) .asInstanceOf[Double] assert(doubleSum == 4.0) val firstTimestamp = new java.sql.Timestamp(1000L) val secondTimestamp = new java.sql.Timestamp(2500L) val timestampSum = add(firstTimestamp, secondTimestamp, TIMESTAMP).asInstanceOf[java.sql.Timestamp] assert(timestampSum.getTime == 3500L) } test("zeroValue returns correct numeric and timestamp identity values") { val integerZero = zeroValue(INTEGER).asInstanceOf[Int] val longZero = zeroValue(LONG).asInstanceOf[Long] val doubleZero = zeroValue(DOUBLE).asInstanceOf[Double] val timestampZero = zeroValue(TIMESTAMP).asInstanceOf[java.sql.Timestamp] assert(integerZero == 0) assert(longZero == 0L) assert(doubleZero == 0.0d) assert(timestampZero.getTime == 0L) } test("zeroValue returns empty binary array and fails for unsupported types") { val binaryZero = zeroValue(BINARY).asInstanceOf[Array[Byte]] assert(binaryZero.isEmpty) assertThrows[UnsupportedOperationException] { zeroValue(STRING) } } test("maxValue returns correct maximum numeric bounds") { val integerMax = maxValue(INTEGER).asInstanceOf[Int] val longMax = maxValue(LONG).asInstanceOf[Long] val doubleMax = maxValue(DOUBLE).asInstanceOf[Double] assert(integerMax == Int.MaxValue) assert(longMax == Long.MaxValue) assert(doubleMax == Double.MaxValue) } test("maxValue returns maximum timestamp and fails for unsupported types") { val timestampMax = maxValue(TIMESTAMP).asInstanceOf[java.sql.Timestamp] assert(timestampMax.getTime == Long.MaxValue) assertThrows[UnsupportedOperationException] { maxValue(BOOLEAN) } } test("minValue returns correct minimum numeric bounds") { val integerMin = minValue(INTEGER).asInstanceOf[Int] val longMin = minValue(LONG).asInstanceOf[Long] val doubleMin = minValue(DOUBLE).asInstanceOf[Double] assert(integerMin == Int.MinValue) assert(longMin == Long.MinValue) assert(doubleMin == java.lang.Double.NEGATIVE_INFINITY) } test("minValue returns timestamp epoch and empty binary array, and fails for unsupported types") { val timestampMin = minValue(TIMESTAMP).asInstanceOf[java.sql.Timestamp] val binaryMin = minValue(BINARY).asInstanceOf[Array[Byte]] assert(timestampMin.getTime == 0L) assert(binaryMin.isEmpty) assertThrows[UnsupportedOperationException] { minValue(STRING) } } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/amber/core/tuple/InternalMarkerSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.tuple import org.apache.texera.amber.core.workflow.PortIdentity import org.scalatest.flatspec.AnyFlatSpec class InternalMarkerSpec extends AnyFlatSpec { "FinalizePort" should "carry the configured portId and direction" in { val marker = FinalizePort(PortIdentity(3), input = false) assert(marker.portId == PortIdentity(3)) assert(!marker.input) } it should "be a TupleLike with empty getFields and zero inMemSize" in { val marker = FinalizePort(PortIdentity(0), input = true) assert(marker.isInstanceOf[TupleLike]) assert(marker.getFields.isEmpty) assert(marker.inMemSize == 0L) } it should "respect case-class equality on the constructor arguments" in { assert( FinalizePort(PortIdentity(0), input = true) == FinalizePort(PortIdentity(0), input = true) ) assert( FinalizePort(PortIdentity(0), input = true) != FinalizePort(PortIdentity(0), input = false) ) assert( FinalizePort(PortIdentity(0), input = true) != FinalizePort(PortIdentity(1), input = true) ) } "FinalizeExecutor" should "be a TupleLike with empty getFields and zero inMemSize" in { val marker = FinalizeExecutor() assert(marker.isInstanceOf[TupleLike]) assert(marker.getFields.isEmpty) assert(marker.inMemSize == 0L) } it should "be equal to another FinalizeExecutor instance (no-field case class)" in { assert(FinalizeExecutor() == FinalizeExecutor()) } "InternalMarker" should "be distinguishable via pattern matching" in { val markers: List[TupleLike] = List( FinalizePort(PortIdentity(0), input = true), FinalizeExecutor() ) val classified = markers.map { case FinalizePort(_, _) => "port" case FinalizeExecutor() => "executor" case other: InternalMarker => s"other-marker:$other" case other => s"non-marker:$other" } assert(classified == List("port", "executor")) } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/amber/core/tuple/SchemaSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.tuple import org.scalatest.flatspec.AnyFlatSpec class SchemaSpec extends AnyFlatSpec { "Schema" should "create an empty schema" in { val schema = Schema() assert(schema.getAttributes.isEmpty) assert(schema.getAttributeNames.isEmpty) } it should "create a schema with attributes of all types" in { val schema = Schema( List( new Attribute("stringAttr", AttributeType.STRING), new Attribute("integerAttr", AttributeType.INTEGER), new Attribute("longAttr", AttributeType.LONG), new Attribute("doubleAttr", AttributeType.DOUBLE), new Attribute("booleanAttr", AttributeType.BOOLEAN), new Attribute("timestampAttr", AttributeType.TIMESTAMP), new Attribute("binaryAttr", AttributeType.BINARY) ) ) assert( schema.getAttributes == List( new Attribute("stringAttr", AttributeType.STRING), new Attribute("integerAttr", AttributeType.INTEGER), new Attribute("longAttr", AttributeType.LONG), new Attribute("doubleAttr", AttributeType.DOUBLE), new Attribute("booleanAttr", AttributeType.BOOLEAN), new Attribute("timestampAttr", AttributeType.TIMESTAMP), new Attribute("binaryAttr", AttributeType.BINARY) ) ) assert( schema.getAttributeNames == List( "stringAttr", "integerAttr", "longAttr", "doubleAttr", "booleanAttr", "timestampAttr", "binaryAttr" ) ) } it should "add a single attribute using add(Attribute)" in { val schema = Schema() val updatedSchema = schema.add(new Attribute("id", AttributeType.INTEGER)) assert(updatedSchema.getAttributes == List(new Attribute("id", AttributeType.INTEGER))) } it should "add multiple attributes using add(Attribute*)" in { val schema = Schema() val updatedSchema = schema.add( new Attribute("stringAttr", AttributeType.STRING), new Attribute("integerAttr", AttributeType.INTEGER), new Attribute("longAttr", AttributeType.LONG) ) assert( updatedSchema.getAttributes == List( new Attribute("stringAttr", AttributeType.STRING), new Attribute("integerAttr", AttributeType.INTEGER), new Attribute("longAttr", AttributeType.LONG) ) ) } it should "add attributes from another schema using add(Schema)" in { val schema1 = Schema(List(new Attribute("id", AttributeType.INTEGER))) val schema2 = Schema(List(new Attribute("name", AttributeType.STRING))) val mergedSchema = schema1.add(schema2) assert( mergedSchema.getAttributes == List( new Attribute("id", AttributeType.INTEGER), new Attribute("name", AttributeType.STRING) ) ) } it should "add an attribute with name and type using add(String, AttributeType)" in { val schema = Schema() val updatedSchema = schema.add("id", AttributeType.INTEGER) assert(updatedSchema.getAttributes == List(new Attribute("id", AttributeType.INTEGER))) } it should "remove an existing attribute" in { val schema = Schema( List( new Attribute("id", AttributeType.INTEGER), new Attribute("name", AttributeType.STRING) ) ) val updatedSchema = schema.remove("id") assert(updatedSchema.getAttributes == List(new Attribute("name", AttributeType.STRING))) } it should "throw an exception when removing a non-existent attribute" in { val schema = Schema( List(new Attribute("id", AttributeType.INTEGER)) ) val exception = intercept[IllegalArgumentException] { schema.remove("name") } assert(exception.getMessage == "Cannot remove non-existent attributes: name") } it should "retrieve an attribute by name" in { val schema = Schema( List( new Attribute("id", AttributeType.INTEGER), new Attribute("name", AttributeType.STRING) ) ) val attribute = schema.getAttribute("id") assert(attribute == new Attribute("id", AttributeType.INTEGER)) } it should "throw an exception when retrieving a non-existent attribute" in { val schema = Schema(List(new Attribute("id", AttributeType.INTEGER))) val exception = intercept[RuntimeException] { schema.getAttribute("name") } assert(exception.getMessage == "name is not contained in the schema") } it should "return a partial schema for attributes of all types" in { val schema = Schema( List( new Attribute("stringAttr", AttributeType.STRING), new Attribute("integerAttr", AttributeType.INTEGER), new Attribute("booleanAttr", AttributeType.BOOLEAN), new Attribute("doubleAttr", AttributeType.DOUBLE) ) ) val partialSchema = schema.getPartialSchema(List("stringAttr", "booleanAttr")) assert( partialSchema.getAttributes == List( new Attribute("stringAttr", AttributeType.STRING), new Attribute("booleanAttr", AttributeType.BOOLEAN) ) ) } it should "convert to raw schema and back for attributes of all types" in { val schema = Schema( List( new Attribute("stringAttr", AttributeType.STRING), new Attribute("integerAttr", AttributeType.INTEGER), new Attribute("longAttr", AttributeType.LONG), new Attribute("doubleAttr", AttributeType.DOUBLE), new Attribute("booleanAttr", AttributeType.BOOLEAN), new Attribute("timestampAttr", AttributeType.TIMESTAMP), new Attribute("binaryAttr", AttributeType.BINARY) ) ) val rawSchema = schema.toRawSchema assert( rawSchema == Map( "stringAttr" -> "STRING", "integerAttr" -> "INTEGER", "longAttr" -> "LONG", "doubleAttr" -> "DOUBLE", "booleanAttr" -> "BOOLEAN", "timestampAttr" -> "TIMESTAMP", "binaryAttr" -> "BINARY" ) ) val reconstructedSchema = Schema.fromRawSchema(rawSchema) assert(reconstructedSchema == schema) } it should "check if attributes exist in schema" in { val schema = Schema( List( new Attribute("stringAttr", AttributeType.STRING), new Attribute("integerAttr", AttributeType.INTEGER) ) ) assert(schema.containsAttribute("stringAttr")) assert(!schema.containsAttribute("nonExistentAttr")) } it should "return the index of an attribute by name" in { val schema = Schema( List( new Attribute("id", AttributeType.INTEGER), new Attribute("name", AttributeType.STRING) ) ) assert(schema.getIndex("id") == 0) assert(schema.getIndex("name") == 1) } it should "throw an exception when getting the index of a non-existent attribute" in { val schema = Schema(List(new Attribute("id", AttributeType.INTEGER))) val exception = intercept[RuntimeException] { schema.getIndex("name") } assert(exception.getMessage == "name is not contained in the schema") } it should "compare schemas for equality" in { val schema1 = Schema( List( new Attribute("id", AttributeType.INTEGER), new Attribute("name", AttributeType.STRING) ) ) val schema2 = Schema( List( new Attribute("id", AttributeType.INTEGER), new Attribute("name", AttributeType.STRING) ) ) val schema3 = Schema( List( new Attribute("id", AttributeType.INTEGER) ) ) assert(schema1 == schema2) assert(schema1 != schema3) } it should "return a proper string representation" in { val schema = Schema( List( new Attribute("id", AttributeType.INTEGER), new Attribute("name", AttributeType.STRING) ) ) assert( schema.toString == "Schema[Attribute[name=id, type=integer], Attribute[name=name, type=string]]" ) } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/amber/core/tuple/TupleSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.tuple import org.apache.texera.amber.core.tuple.TupleUtils.{json2tuple, tuple2json} import org.scalatest.flatspec.AnyFlatSpec import java.sql.Timestamp class TupleSpec extends AnyFlatSpec { val stringAttribute = new Attribute("col-string", AttributeType.STRING) val integerAttribute = new Attribute("col-int", AttributeType.INTEGER) val boolAttribute = new Attribute("col-bool", AttributeType.BOOLEAN) val longAttribute = new Attribute("col-long", AttributeType.LONG) val doubleAttribute = new Attribute("col-double", AttributeType.DOUBLE) val timestampAttribute = new Attribute("col-timestamp", AttributeType.TIMESTAMP) val binaryAttribute = new Attribute("col-binary", AttributeType.BINARY) val capitalizedStringAttribute = new Attribute("COL-string", AttributeType.STRING) it should "create a tuple with capitalized attributeName" in { val schema = Schema().add(capitalizedStringAttribute) val tuple = Tuple.builder(schema).add(capitalizedStringAttribute, "string-value").build() assert(tuple.getField("COL-string").asInstanceOf[String] == "string-value") } it should "create a tuple with capitalized attributeName, using addSequentially" in { val schema = Schema().add(capitalizedStringAttribute) val tuple = Tuple.builder(schema).addSequentially(Array("string-value")).build() assert(tuple.getField("COL-string").asInstanceOf[String] == "string-value") } it should "create a tuple using new builder, based on another tuple using old builder" in { val schema = Schema().add(stringAttribute) val inputTuple = Tuple.builder(schema).addSequentially(Array("string-value")).build() val newTuple = Tuple.builder(inputTuple.getSchema).add(inputTuple).build() assert(newTuple.length == inputTuple.length) } it should "fail when unknown attribute is added to tuple" in { val schema = Schema().add(stringAttribute) assertThrows[TupleBuildingException] { Tuple.builder(schema).add(integerAttribute, 1) } } it should "fail when tuple does not conform to complete schema" in { val schema = Schema().add(stringAttribute).add(integerAttribute) assertThrows[TupleBuildingException] { Tuple.builder(schema).add(integerAttribute, 1).build() } } it should "fail when entire tuple passed in has extra attributes" in { val inputSchema = Schema().add(stringAttribute).add(integerAttribute).add(boolAttribute) val inputTuple = Tuple .builder(inputSchema) .add(integerAttribute, 1) .add(stringAttribute, "string-attr") .add(boolAttribute, true) .build() val outputSchema = Schema().add(stringAttribute).add(integerAttribute) assertThrows[TupleBuildingException] { Tuple.builder(outputSchema).add(inputTuple).build() } } it should "not fail when entire tuple passed in has extra attributes and strictSchemaMatch is false" in { val inputSchema = Schema().add(stringAttribute).add(integerAttribute).add(boolAttribute) val inputTuple = Tuple .builder(inputSchema) .add(integerAttribute, 1) .add(stringAttribute, "string-attr") .add(boolAttribute, true) .build() val outputSchema = Schema().add(stringAttribute).add(integerAttribute) val outputTuple = Tuple.builder(outputSchema).add(inputTuple, false).build() // This is the important test. Input tuple has 3 attributes but output tuple has only 2 // It's because of isStrictSchemaMatch=false assert(outputTuple.length == 2); } it should "produce identical strings" in { val inputSchema = Schema().add(stringAttribute).add(integerAttribute).add(boolAttribute) val inputTuple = Tuple .builder(inputSchema) .add(integerAttribute, 1) .add(stringAttribute, "string-attr") .add(boolAttribute, true) .build() val line = tuple2json(inputTuple.schema, inputTuple.fieldVals).toString val newTuple = json2tuple(line) assert(line == tuple2json(newTuple.schema, newTuple.fieldVals).toString) } it should "calculate hash" in { val inputSchema = Schema() .add(integerAttribute) .add(stringAttribute) .add(boolAttribute) .add(longAttribute) .add(doubleAttribute) .add(timestampAttribute) .add(binaryAttribute) val inputTuple = Tuple .builder(inputSchema) .add(integerAttribute, 922323) .add(stringAttribute, "string-attr") .add(boolAttribute, true) .add(longAttribute, 1123213213213L) .add(doubleAttribute, 214214.9969346) .add(timestampAttribute, new Timestamp(100000000L)) .add(binaryAttribute, Array[Byte](104, 101, 108, 108, 111)) .build() assert(inputTuple.hashCode() == -1335416166) val inputTuple2 = Tuple .builder(inputSchema) .add(integerAttribute, 0) .add(stringAttribute, "") .add(boolAttribute, false) .add(longAttribute, 0L) .add(doubleAttribute, 0.0) .add(timestampAttribute, new Timestamp(0L)) .add(binaryAttribute, Array[Byte]()) .build() assert(inputTuple2.hashCode() == -1409761483) val inputTuple3 = Tuple .builder(inputSchema) .add(integerAttribute, null) .add(stringAttribute, null) .add(boolAttribute, null) .add(longAttribute, null) .add(doubleAttribute, null) .add(timestampAttribute, null) .add(binaryAttribute, null) .build() assert(inputTuple3.hashCode() == 1742810335) val inputTuple4 = Tuple .builder(inputSchema) .add(integerAttribute, -3245763) .add(stringAttribute, "\n\r\napple") .add(boolAttribute, true) .add(longAttribute, -8965536434247L) .add(doubleAttribute, 1 / 3.0d) .add(timestampAttribute, new Timestamp(-1990)) .add(binaryAttribute, null) .build() assert(inputTuple4.hashCode() == -592643630) val inputTuple5 = Tuple .builder(inputSchema) .add(integerAttribute, Int.MaxValue) .add(stringAttribute, new String()) .add(boolAttribute, true) .add(longAttribute, Long.MaxValue) .add(doubleAttribute, 7 / 17.0d) .add(timestampAttribute, new Timestamp(1234567890L)) .add(binaryAttribute, Array.fill[Byte](4097)('o')) .build() assert(inputTuple5.hashCode() == -2099556631) } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/amber/core/tuple/TupleUtilsSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.tuple import org.scalatest.flatspec.AnyFlatSpec import scala.jdk.CollectionConverters._ class TupleUtilsSpec extends AnyFlatSpec { // --- tuple2json ------------------------------------------------------------ "TupleUtils.tuple2json" should "emit one JSON field per schema attribute, in the schema's declared order" in { val schema = new Schema( new Attribute("id", AttributeType.INTEGER), new Attribute("name", AttributeType.STRING) ) val node = TupleUtils.tuple2json(schema, Array[Any](Int.box(7), "alice")) // Field iteration order on Jackson ObjectNode follows insertion order, // which mirrors the schema's getAttributeNames order. assert(node.fieldNames().asScala.toList == List("id", "name")) assert(node.get("id").asInt() == 7) assert(node.get("name").asText() == "alice") } it should "emit JSON null for null field values" in { val schema = new Schema(new Attribute("v", AttributeType.STRING)) val node = TupleUtils.tuple2json(schema, Array[Any](null)) assert(node.get("v").isNull) } it should "respect schema.getIndex when fieldVals is laid out positionally" in { // Re-ordering the schema must change which slot of fieldVals each // attribute pulls from, because tuple2json indexes fieldVals via // schema.getIndex(attrName). val schema = new Schema( new Attribute("b", AttributeType.STRING), new Attribute("a", AttributeType.STRING) ) val node = TupleUtils.tuple2json(schema, Array[Any]("first", "second")) assert(node.get("b").asText() == "first") assert(node.get("a").asText() == "second") } it should "produce an empty object for an empty schema" in { val node = TupleUtils.tuple2json(new Schema(), Array.empty[Any]) assert(node.size() == 0) } // --- json2tuple ------------------------------------------------------------ "TupleUtils.json2tuple" should "infer a schema from a flat JSON object's keys and types" in { val tuple = TupleUtils.json2tuple("""{"name": "bob", "age": 30}""") val names = tuple.getSchema.getAttributeNames.toSet assert(names == Set("name", "age")) assert(tuple.getField[Any]("name") == "bob") // age is parsed via inferSchemaFromRows; the inferred type for "30" is // a numeric type — assert we can read the field rather than locking in // the precise inferred AttributeType. assert(tuple.getField[Any]("age").toString == "30") } it should "round-trip a schema-and-values through tuple2json → json2tuple" in { val schema = new Schema( new Attribute("city", AttributeType.STRING), new Attribute("score", AttributeType.INTEGER) ) val original = TupleUtils.tuple2json(schema, Array[Any]("Irvine", Int.box(42))).toString val parsed = TupleUtils.json2tuple(original) val reSerialized = TupleUtils.tuple2json(parsed.getSchema, parsed.getFields.toArray.asInstanceOf[Array[Any]]) // The exact column order isn't part of the json2tuple contract (it builds // schemaFieldNames from a Set), so compare by JSON-tree equality. val mapper = org.apache.texera.amber.util.JSONUtils.objectMapper assert(mapper.readTree(reSerialized.toString) == mapper.readTree(original)) } it should "drop non-object roots (e.g. a JSON array) into an empty tuple" in { // The implementation only collects fields when the root `isObject`. A // non-object root leaves `fieldNames` empty, so the result is a tuple // over an empty schema with no fields — observed contract is no-throw, // empty result. val tuple = TupleUtils.json2tuple("""[1, 2, 3]""") assert(tuple.getSchema.getAttributes.isEmpty) assert(tuple.getFields.isEmpty) } it should "throw when given malformed JSON" in { intercept[Exception] { TupleUtils.json2tuple("{ this is not json }") } } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/amber/core/workflow/PartitionInfoSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.workflow import com.fasterxml.jackson.annotation.JsonSubTypes import org.scalatest.flatspec.AnyFlatSpec class PartitionInfoSpec extends AnyFlatSpec { // The full set of "named" partition kinds we care about cross-checking. // Two HashPartitions with different attribute lists count as a "different // partition" too, so we include both shapes. private val hashA: PartitionInfo = HashPartition(List("a")) private val hashB: PartitionInfo = HashPartition(List("b")) private val rangeA: PartitionInfo = new RangePartition(List("a"), 0L, 10L) private val single: PartitionInfo = SinglePartition() private val broadcast: PartitionInfo = BroadcastPartition() private val oneToOne: PartitionInfo = OneToOnePartition() private val unknown: PartitionInfo = UnknownPartition() // Five "primary" partition kinds (excluding the duplicate Hash and the // catch-all Unknown — both handled separately) used for the cross product. private val primaryKinds: List[(String, PartitionInfo)] = List( "HashPartition" -> hashA, "RangePartition" -> rangeA, "SinglePartition" -> single, "BroadcastPartition" -> broadcast, "OneToOnePartition" -> oneToOne ) "PartitionInfo.satisfies" should "hold reflexively (each partition satisfies itself)" in { primaryKinds.foreach { case (name, p) => assert(p.satisfies(p), s"$name should satisfy itself") } // UnknownPartition reflexively satisfies itself too. assert(unknown.satisfies(unknown)) // HashPartition with the same attribute list satisfies itself even // across distinct instances. assert(HashPartition(List("a")).satisfies(HashPartition(List("a")))) } it should "fail across the full 5x5 cross-product of distinct primary kinds" in { // For every pair of distinct primary partition kinds, satisfies must be // false. This covers the full 5x5 = 25 cell matrix; the diagonal is // covered by the reflexivity test above. for { (lname, lhs) <- primaryKinds (rname, rhs) <- primaryKinds if lhs != rhs } { assert(!lhs.satisfies(rhs), s"$lname must not satisfy $rname") } } it should "hold for any primary partition against UnknownPartition" in { primaryKinds.foreach { case (name, p) => assert(p.satisfies(unknown), s"$name should satisfy UnknownPartition") } // And UnknownPartition satisfies itself. assert(unknown.satisfies(unknown)) } it should "fail when UnknownPartition is on the LHS against any primary kind" in { primaryKinds.foreach { case (name, p) => assert(!unknown.satisfies(p), s"UnknownPartition must not satisfy $name") } } it should "fail for HashPartition with different attribute lists (and otherwise-equal shape)" in { assert(!hashA.satisfies(hashB)) assert(!hashB.satisfies(hashA)) // But both still satisfy UnknownPartition. assert(hashA.satisfies(unknown)) assert(hashB.satisfies(unknown)) } "PartitionInfo.merge" should "preserve the partition when merged with itself across every kind" in { primaryKinds.foreach { case (name, p) => // RangePartition has its own override that always returns // UnknownPartition (covered separately below); skip it here. if (!p.isInstanceOf[RangePartition]) { assert(p.merge(p) == p, s"$name should merge with itself to itself") } } // UnknownPartition merges with itself to itself. assert(unknown.merge(unknown) == unknown) // HashPartition with same attributes merges to itself. assert(HashPartition(List("a")).merge(HashPartition(List("a"))) == HashPartition(List("a"))) } it should "fall back to UnknownPartition for the full 5x5 cross-product of distinct primary kinds" in { // Every distinct-pair merge produces UnknownPartition. for { (lname, lhs) <- primaryKinds (rname, rhs) <- primaryKinds if lhs != rhs } { assert( lhs.merge(rhs) == unknown, s"$lname.merge($rname) must be UnknownPartition" ) } } it should "fall back to UnknownPartition when either side is UnknownPartition (excluding self-merge)" in { primaryKinds.foreach { case (name, p) => assert(p.merge(unknown) == unknown, s"$name.merge(Unknown) must be Unknown") assert(unknown.merge(p) == unknown, s"Unknown.merge($name) must be Unknown") } } it should "always return UnknownPartition for RangePartition merges, including with itself" in { val r = new RangePartition(List("a"), 0L, 10L) assert(r.merge(r) == unknown, "RangePartition self-merge is overridden to Unknown") primaryKinds.foreach { case (name, p) => assert(r.merge(p) == unknown, s"RangePartition.merge($name) must be Unknown") } } it should "treat HashPartitions with different attribute lists as distinct (merge → Unknown)" in { assert(hashA.merge(hashB) == unknown) assert(hashB.merge(hashA) == unknown) } "RangePartition.apply" should "return an UnknownPartition when no range attributes are provided" in { assert(RangePartition(List.empty, 0L, 10L) == UnknownPartition()) } it should "return a RangePartition when at least one range attribute is provided" in { val result = RangePartition(List("a"), 0L, 10L) assert(result.isInstanceOf[RangePartition]) val rp = result.asInstanceOf[RangePartition] assert(rp.rangeAttributeNames == List("a")) assert(rp.rangeMin == 0L) assert(rp.rangeMax == 10L) } // --------------------------------------------------------------------------- // HashPartition default attribute list // --------------------------------------------------------------------------- "HashPartition()" should "default to an empty hash attribute list" in { assert(HashPartition().hashAttributeNames.isEmpty) } // --------------------------------------------------------------------------- // JsonSubTypes registration // --------------------------------------------------------------------------- "PartitionInfo @JsonSubTypes" should "list the current registration set (omits OneToOnePartition)" in { // Pin: the @JsonSubTypes annotation on PartitionInfo currently registers // HashPartition, RangePartition, SinglePartition, BroadcastPartition, // and UnknownPartition — but NOT OneToOnePartition. The "all" claim is // documented separately in the pendingUntilFixed test below so this // spec only documents the present-day set. val annotation = classOf[PartitionInfo].getAnnotation(classOf[JsonSubTypes]) val registered = annotation.value().toList.map(_.value().getSimpleName).toSet assert( registered == Set( "HashPartition", "RangePartition", "SinglePartition", "BroadcastPartition", "UnknownPartition" ) ) } it should "eventually register every concrete PartitionInfo subclass (pendingUntilFixed)" in pendingUntilFixed { // Intended contract: every concrete PartitionInfo subtype must be // reachable through the polymorphic dispatch on `type`, otherwise // Jackson cannot deserialize the missing payload (today: OneToOne- // Partition). Asserting `contains "OneToOnePartition"` here flips // this test from Pending to a real pass once the bug is fixed — // pendingUntilFixed inverts that and turns the now-passing // assertion into a failure so the fix has to delete the marker // deliberately. val annotation = classOf[PartitionInfo].getAnnotation(classOf[JsonSubTypes]) val registered = annotation.value().toList.map(_.value().getSimpleName).toSet assert(registered.contains("OneToOnePartition")) } // --------------------------------------------------------------------------- // case-class equality // --------------------------------------------------------------------------- "PartitionInfo case classes" should "use structural equality (case-class semantics)" in { assert(HashPartition(List("k")) == HashPartition(List("k"))) assert(HashPartition(List("k")) != HashPartition(List("other"))) assert(SinglePartition() == SinglePartition()) assert(UnknownPartition() == UnknownPartition()) } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/amber/core/workflow/WorkflowContextSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.workflow import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers class WorkflowContextSpec extends AnyFlatSpec with Matchers { "WorkflowContext companion" should "expose pinned defaults" in { // These constants seed every default-constructed WorkflowContext, and the // engine's bootstrap path relies on the exact 1L identifiers for both // workflow and execution. Pinning so a renumber is reviewed deliberately. WorkflowContext.DEFAULT_WORKFLOW_ID shouldBe WorkflowIdentity(1L) WorkflowContext.DEFAULT_EXECUTION_ID shouldBe ExecutionIdentity(1L) WorkflowContext.DEFAULT_WORKFLOW_SETTINGS shouldBe WorkflowSettings() } "default WorkflowContext" should "use the companion-object defaults" in { val ctx = new WorkflowContext() ctx.workflowId shouldBe WorkflowContext.DEFAULT_WORKFLOW_ID ctx.executionId shouldBe WorkflowContext.DEFAULT_EXECUTION_ID ctx.workflowSettings shouldBe WorkflowContext.DEFAULT_WORKFLOW_SETTINGS } "WorkflowContext fields" should "be reassignable through their var accessors" in { val ctx = new WorkflowContext() ctx.workflowId = WorkflowIdentity(42L) ctx.executionId = ExecutionIdentity(7L) ctx.workflowId shouldBe WorkflowIdentity(42L) ctx.executionId shouldBe ExecutionIdentity(7L) } "WorkflowContext constructor" should "accept overridden defaults at construction time" in { val ctx = new WorkflowContext( workflowId = WorkflowIdentity(99L), executionId = ExecutionIdentity(123L) ) ctx.workflowId shouldBe WorkflowIdentity(99L) ctx.executionId shouldBe ExecutionIdentity(123L) // Settings argument was not overridden, so the companion default holds. ctx.workflowSettings shouldBe WorkflowContext.DEFAULT_WORKFLOW_SETTINGS } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/amber/core/workflow/WorkflowCoreTypesSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.core.workflow import org.apache.texera.amber.core.executor.OpExecInitInfo import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema} import org.apache.texera.amber.core.virtualidentity.{ ExecutionIdentity, OperatorIdentity, PhysicalOpIdentity, WorkflowIdentity } import org.scalatest.flatspec.AnyFlatSpec class WorkflowCoreTypesSpec extends AnyFlatSpec { // --------------------------------------------------------------------------- // LocationPreference // --------------------------------------------------------------------------- "LocationPreference" should "have PreferController and RoundRobinPreference as singleton subtypes" in { val a: LocationPreference = PreferController val b: LocationPreference = RoundRobinPreference assert(a eq PreferController) assert(b eq RoundRobinPreference) assert(a != b) } it should "be Serializable on every subtype" in { val all: Seq[LocationPreference] = Seq(PreferController, RoundRobinPreference) all.foreach(p => assert(p.isInstanceOf[Serializable])) } // --------------------------------------------------------------------------- // WorkflowSettings // --------------------------------------------------------------------------- "WorkflowSettings" should "default dataTransferBatchSize to 400 and outputPortsNeedingStorage to empty" in { val s = WorkflowSettings() assert(s.dataTransferBatchSize == 400) assert(s.outputPortsNeedingStorage.isEmpty) } it should "carry custom values constructed via case-class apply" in { val portId = GlobalPortIdentity( PhysicalOpIdentity(OperatorIdentity("op"), "main"), PortIdentity(0), input = false ) val s = WorkflowSettings( dataTransferBatchSize = 50, outputPortsNeedingStorage = Set(portId) ) assert(s.dataTransferBatchSize == 50) assert(s.outputPortsNeedingStorage == Set(portId)) } // --------------------------------------------------------------------------- // WorkflowContext // --------------------------------------------------------------------------- "WorkflowContext" should "default to DEFAULT_WORKFLOW_ID, DEFAULT_EXECUTION_ID, and DEFAULT_WORKFLOW_SETTINGS" in { val ctx = new WorkflowContext() assert(ctx.workflowId == WorkflowContext.DEFAULT_WORKFLOW_ID) assert(ctx.executionId == WorkflowContext.DEFAULT_EXECUTION_ID) assert(ctx.workflowSettings == WorkflowContext.DEFAULT_WORKFLOW_SETTINGS) } it should "expose the documented default constants" in { assert(WorkflowContext.DEFAULT_WORKFLOW_ID == WorkflowIdentity(1L)) assert(WorkflowContext.DEFAULT_EXECUTION_ID == ExecutionIdentity(1L)) } it should "allow workflowId / executionId / workflowSettings to be reassigned" in { val ctx = new WorkflowContext() ctx.workflowId = WorkflowIdentity(7L) ctx.executionId = ExecutionIdentity(11L) val custom = WorkflowSettings(dataTransferBatchSize = 1) ctx.workflowSettings = custom assert(ctx.workflowId == WorkflowIdentity(7L)) assert(ctx.executionId == ExecutionIdentity(11L)) assert(ctx.workflowSettings eq custom) } // --------------------------------------------------------------------------- // PhysicalOp helpers // --------------------------------------------------------------------------- private val workflowId = WorkflowIdentity(0L) private val executionId = ExecutionIdentity(0L) private def opId(name: String): PhysicalOpIdentity = PhysicalOpIdentity(OperatorIdentity(name), "main") private val intSchema: Schema = Schema().add(new Attribute("v", AttributeType.INTEGER)) private def newPhysicalOp(name: String, parallelizable: Boolean = true): PhysicalOp = PhysicalOp .oneToOnePhysicalOp( opId(name), workflowId, executionId, OpExecInitInfo.Empty ) .copy(parallelizable = parallelizable) "PhysicalOp.isSourceOperator" should "be true when there are no input ports" in { val op = newPhysicalOp("a") assert(op.inputPorts.isEmpty) assert(op.isSourceOperator) } it should "be false once an input port is added" in { val op = newPhysicalOp("a").withInputPorts(List(InputPort(PortIdentity(0)))) assert(!op.isSourceOperator) } "PhysicalOp.withLocationPreference" should "store the location preference" in { val op = newPhysicalOp("a").withLocationPreference(Some(PreferController)) assert(op.locationPreference.contains(PreferController)) } "PhysicalOp.withParallelizable" should "set the parallelizable flag and round-trip through copy" in { val op = newPhysicalOp("a", parallelizable = true) val flipped = op.withParallelizable(false) assert(!flipped.parallelizable) assert(op.parallelizable, "the original instance is immutable") } "PhysicalOp.withSuggestedWorkerNum" should "set the suggested worker count" in { val op = newPhysicalOp("a").withSuggestedWorkerNum(7) assert(op.suggestedWorkerNum.contains(7)) } "PhysicalOp.addInputLink" should "append the link to the matching input port" in { val op = newPhysicalOp("a").withInputPorts(List(InputPort(PortIdentity(0)))) val link = PhysicalLink(opId("up"), PortIdentity(0), opId("a"), PortIdentity(0)) val updated = op.addInputLink(link) assert(updated.getInputLinks(Some(PortIdentity(0))) == List(link)) assert(updated.getInputLinks() == List(link)) } it should "fail the assertion when the link does not target this op id" in { val op = newPhysicalOp("a").withInputPorts(List(InputPort(PortIdentity(0)))) val mismatched = PhysicalLink(opId("up"), PortIdentity(0), opId("other"), PortIdentity(0)) assertThrows[AssertionError] { op.addInputLink(mismatched) } } it should "fail the assertion when the target port is not declared" in { val op = newPhysicalOp("a").withInputPorts(List(InputPort(PortIdentity(0)))) val unknownPort = PhysicalLink(opId("up"), PortIdentity(0), opId("a"), PortIdentity(99)) assertThrows[AssertionError] { op.addInputLink(unknownPort) } } "PhysicalOp.addOutputLink" should "append the link to the matching output port" in { val op = newPhysicalOp("a").withOutputPorts(List(OutputPort(PortIdentity(0)))) val link = PhysicalLink(opId("a"), PortIdentity(0), opId("dn"), PortIdentity(0)) val updated = op.addOutputLink(link) assert(updated.getOutputLinks(PortIdentity(0)) == List(link)) } "PhysicalOp.removeInputLink" should "drop the matching link, leaving others intact" in { val op = newPhysicalOp("a").withInputPorts(List(InputPort(PortIdentity(0)))) val l1 = PhysicalLink(opId("u1"), PortIdentity(0), opId("a"), PortIdentity(0)) val l2 = PhysicalLink(opId("u2"), PortIdentity(0), opId("a"), PortIdentity(0)) val updated = op.addInputLink(l1).addInputLink(l2).removeInputLink(l1) assert(updated.getInputLinks() == List(l2)) } "PhysicalOp.removeOutputLink" should "drop the matching link, leaving others intact" in { val op = newPhysicalOp("a").withOutputPorts(List(OutputPort(PortIdentity(0)))) val l1 = PhysicalLink(opId("a"), PortIdentity(0), opId("d1"), PortIdentity(0)) val l2 = PhysicalLink(opId("a"), PortIdentity(0), opId("d2"), PortIdentity(0)) val updated = op.addOutputLink(l1).addOutputLink(l2).removeOutputLink(l1) assert(updated.getOutputLinks(PortIdentity(0)) == List(l2)) } "PhysicalOp.propagateSchema" should "fill in output schemas once every input schema is known" in { val out = OutputPort(PortIdentity(0)) val in = InputPort(PortIdentity(0)) val op = newPhysicalOp("a") .withInputPorts(List(in)) .withOutputPorts(List(out)) .withPropagateSchema(SchemaPropagationFunc(inputs => Map(out.id -> inputs(in.id)))) val updated = op.propagateSchema(Some((in.id, intSchema))) val outSchema = updated.outputPorts(out.id)._3 assert(outSchema.toOption.contains(intSchema)) } it should "raise IllegalArgumentException when a conflicting schema arrives on an already-known port" in { val out = OutputPort(PortIdentity(0)) val in = InputPort(PortIdentity(0)) val op = newPhysicalOp("a") .withInputPorts(List(in)) .withOutputPorts(List(out)) .withPropagateSchema(SchemaPropagationFunc(inputs => Map(out.id -> inputs(in.id)))) .propagateSchema(Some((in.id, intSchema))) val different = Schema().add(new Attribute("w", AttributeType.STRING)) assertThrows[IllegalArgumentException] { op.propagateSchema(Some((in.id, different))) } } it should "leave output schemas as a Left when the propagation function throws" in { val out = OutputPort(PortIdentity(0)) val in = InputPort(PortIdentity(0)) val op = newPhysicalOp("a") .withInputPorts(List(in)) .withOutputPorts(List(out)) .withPropagateSchema(SchemaPropagationFunc(_ => throw new RuntimeException("boom"))) val updated = op.propagateSchema(Some((in.id, intSchema))) assert(updated.outputPorts(out.id)._3.isLeft) } "PhysicalOp.isOutputLinkBlocking" should "reflect the configured blocking flag on the source port" in { val opBlocking = newPhysicalOp("a").withOutputPorts(List(OutputPort(PortIdentity(0), blocking = true))) val opOpen = newPhysicalOp("b").withOutputPorts(List(OutputPort(PortIdentity(0), blocking = false))) // Each link's `fromOpId` is set to the operator under test, so the test // remains correct if `isOutputLinkBlocking` is later tightened to // validate `fromOpId == this.id`. val blockingLink = PhysicalLink(opId("a"), PortIdentity(0), opId("downstream"), PortIdentity(0)) val openLink = PhysicalLink(opId("b"), PortIdentity(0), opId("downstream"), PortIdentity(0)) assert(opBlocking.isOutputLinkBlocking(blockingLink)) assert(!opOpen.isOutputLinkBlocking(openLink)) } // --------------------------------------------------------------------------- // PhysicalPlan // --------------------------------------------------------------------------- private def physicalOp(name: String): PhysicalOp = newPhysicalOp(name) .withInputPorts(List(InputPort(PortIdentity(0)))) .withOutputPorts(List(OutputPort(PortIdentity(0)))) private def link(from: String, to: String): PhysicalLink = PhysicalLink(opId(from), PortIdentity(0), opId(to), PortIdentity(0)) "PhysicalPlan.getOperator" should "look up by physical id" in { val a = physicalOp("a") val b = physicalOp("b") val plan = PhysicalPlan(Set(a, b), Set.empty) assert(plan.getOperator(a.id) == a) } "PhysicalPlan.getSourceOperatorIds" should "return operators with no incoming links in the DAG" in { val a = physicalOp("a") val b = physicalOp("b") val c = physicalOp("c") val plan = PhysicalPlan(Set(a, b, c), Set(link("a", "b"), link("b", "c"))) assert(plan.getSourceOperatorIds == Set(a.id)) } it should "return all operators when there are no links" in { val a = physicalOp("a") val b = physicalOp("b") val plan = PhysicalPlan(Set(a, b), Set.empty) assert(plan.getSourceOperatorIds == Set(a.id, b.id)) } "PhysicalPlan.topologicalIterator" should "produce a topological ordering across the DAG" in { val a = physicalOp("a") val b = physicalOp("b") val c = physicalOp("c") val plan = PhysicalPlan(Set(a, b, c), Set(link("a", "b"), link("b", "c"))) assert(plan.topologicalIterator().toList == List(a.id, b.id, c.id)) } "PhysicalPlan.getUpstreamPhysicalOpIds" should "return direct predecessors only" in { val a = physicalOp("a") val b = physicalOp("b") val c = physicalOp("c") val plan = PhysicalPlan(Set(a, b, c), Set(link("a", "b"), link("a", "c"), link("b", "c"))) assert(plan.getUpstreamPhysicalOpIds(c.id) == Set(a.id, b.id)) assert(plan.getUpstreamPhysicalOpIds(a.id).isEmpty) } "PhysicalPlan.getUpstreamPhysicalLinks" should "return only links targeting the operator" in { val a = physicalOp("a") val b = physicalOp("b") val c = physicalOp("c") val l1 = link("a", "c") val l2 = link("b", "c") val plan = PhysicalPlan(Set(a, b, c), Set(l1, l2)) assert(plan.getUpstreamPhysicalLinks(c.id) == Set(l1, l2)) assert(plan.getUpstreamPhysicalLinks(a.id).isEmpty) } "PhysicalPlan.getDownstreamPhysicalLinks" should "return only links sourcing from the operator" in { val a = physicalOp("a") val b = physicalOp("b") val c = physicalOp("c") val l1 = link("a", "b") val l2 = link("a", "c") val plan = PhysicalPlan(Set(a, b, c), Set(l1, l2)) assert(plan.getDownstreamPhysicalLinks(a.id) == Set(l1, l2)) assert(plan.getDownstreamPhysicalLinks(c.id).isEmpty) } "PhysicalPlan.getSubPlan" should "include only the requested operators and the links between them" in { val a = physicalOp("a") val b = physicalOp("b") val c = physicalOp("c") val plan = PhysicalPlan(Set(a, b, c), Set(link("a", "b"), link("b", "c"), link("a", "c"))) val sub = plan.getSubPlan(Set(a.id, b.id)) assert(sub.operators.map(_.id) == Set(a.id, b.id)) assert(sub.links == Set(link("a", "b"))) } "PhysicalPlan.getPhysicalOpsOfLogicalOp" should "return every physical op sharing a logical id, in topological order" in { val a = physicalOp("a") val b = physicalOp("b") val plan = PhysicalPlan(Set(a, b), Set(link("a", "b"))) val onlyB = plan.getPhysicalOpsOfLogicalOp(OperatorIdentity("b")) assert(onlyB.map(_.id) == List(b.id)) } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/amber/storage/FileResolverSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.storage import org.apache.texera.amber.core.storage.FileResolver import org.apache.commons.vfs2.FileNotFoundException import org.apache.texera.dao.MockTexeraDB import org.apache.texera.dao.jooq.generated.enums.UserRoleEnum import org.apache.texera.dao.jooq.generated.tables.daos.{DatasetDao, DatasetVersionDao, UserDao} import org.apache.texera.dao.jooq.generated.tables.pojos.{Dataset, DatasetVersion, User} import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach} import java.nio.file.Paths class FileResolverSpec extends AnyFlatSpec with BeforeAndAfterAll with BeforeAndAfterEach with MockTexeraDB { private val testUser: User = { val user = new User user.setUid(Integer.valueOf(1)) user.setName("test_user") user.setRole(UserRoleEnum.ADMIN) user.setPassword("123") user.setEmail("test_user@test.com") user } private val testDataset: Dataset = { val dataset = new Dataset dataset.setDid(Integer.valueOf(1)) dataset.setName("test_dataset") dataset.setRepositoryName("test_dataset") dataset.setDescription("dataset for test") dataset.setIsPublic(true) dataset.setOwnerUid(Integer.valueOf(1)) dataset } private val testDatasetVersion1: DatasetVersion = { val datasetVersion = new DatasetVersion datasetVersion.setDid(Integer.valueOf(1)) datasetVersion.setName("v1") datasetVersion.setDvid(Integer.valueOf(1)) datasetVersion.setCreatorUid(Integer.valueOf(1)) datasetVersion.setVersionHash("97fd4c2a755b69b7c66d322eab40b7e5c2ad5d10") datasetVersion } private val testDatasetVersion2: DatasetVersion = { val datasetVersion = new DatasetVersion datasetVersion.setDid(Integer.valueOf(1)) datasetVersion.setName("v2") datasetVersion.setDvid(Integer.valueOf(2)) datasetVersion.setCreatorUid(Integer.valueOf(1)) datasetVersion.setVersionHash("37966c92cb3a8bee1f9d8e21937aa8faa5e48513") datasetVersion } private val localCsvFilePath = "common/workflow-core/src/test/resources/country_sales_small.csv" private val datasetACsvFilePath = "/test_user@test.com/test_dataset/v2/directory/a.csv" private val dataset1TxtFilePath = "/test_user@test.com/test_dataset/v1/1.txt" override protected def beforeAll(): Unit = { initializeDBAndReplaceDSLContext() // add test user val userDao = new UserDao(getDSLContext.configuration()) userDao.insert(testUser) // add test dataset val datasetDao = new DatasetDao(getDSLContext.configuration()) datasetDao.insert(testDataset) // add test dataset versions val datasetVersionDao = new DatasetVersionDao(getDSLContext.configuration()) datasetVersionDao.insert(testDatasetVersion1) datasetVersionDao.insert(testDatasetVersion2) } "FileResolver" should "resolve local file correctly" in { val localUri = FileResolver.resolve(localCsvFilePath) assert(localUri == Paths.get(localCsvFilePath).toUri) } "FileResolver" should "resolve dataset file correctly" in { val datasetACsvUri = FileResolver.resolve(datasetACsvFilePath) val dataset1TxtUri = FileResolver.resolve(dataset1TxtFilePath) assert( datasetACsvUri.toString == f"${FileResolver.DATASET_FILE_URI_SCHEME}:///${testDataset.getRepositoryName}/${testDatasetVersion2.getVersionHash}/directory/a.csv" ) assert( dataset1TxtUri.toString == f"${FileResolver.DATASET_FILE_URI_SCHEME}:///${testDataset.getRepositoryName}/${testDatasetVersion1.getVersionHash}/1.txt" ) } "FileResolver" should "throw not found exception" in { assertThrows[FileNotFoundException] { FileResolver.resolve("some/random/path") } } override protected def afterAll(): Unit = { shutdownDB() } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/amber/storage/result/iceberg/IcebergDocumentConsoleMessagesSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.storage.result.iceberg import org.apache.texera.amber.core.storage.model._ import org.apache.texera.amber.core.storage.result.ResultSchema import org.apache.texera.amber.core.storage.{DocumentFactory, VFSURIFactory} import org.apache.texera.amber.core.tuple.{Schema, Tuple} import org.apache.texera.amber.core.virtualidentity._ import org.scalatest.BeforeAndAfterAll import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import java.net.URI import scala.util.Using.Releasable import scala.util.{Try, Using} class IcebergDocumentConsoleMessagesSpec extends AnyFlatSpec with Matchers with VirtualDocumentSpec[Tuple] with BeforeAndAfterAll { private val amberSchema: Schema = ResultSchema.consoleMessagesSchema var uri: URI = _ override def beforeEach(): Unit = { uri = VFSURIFactory.createConsoleMessagesURI( WorkflowIdentity(0), ExecutionIdentity(0), OperatorIdentity("test_operator") ) DocumentFactory.createDocument(uri, amberSchema) super.beforeEach() } override def afterAll(): Unit = { super.afterAll() } override def generateSampleItems(): List[Tuple] = List( new Tuple(amberSchema, Array("First console message")), new Tuple(amberSchema, Array("Second console message")), new Tuple(amberSchema, Array("Third console message")) ) implicit val bufferedItemWriterReleasable: Releasable[BufferedItemWriter[Tuple]] = (resource: BufferedItemWriter[Tuple]) => resource.close() "IcebergDocument" should "write and read console messages successfully" in { Using.resource(document.writer("console_messages_test")) { writer => writer.open() generateSampleItems().foreach(writer.putOne) writer.close() } val retrievedMessages = Try(document.get().toList.collect { case t: Tuple => t }).getOrElse(Nil) retrievedMessages should contain theSameElementsAs generateSampleItems() } override def getDocument: VirtualDocument[Tuple] = { DocumentFactory.openDocument(uri)._1 match { case doc: VirtualDocument[_] => doc.asInstanceOf[VirtualDocument[Tuple]] case _ => fail("Failed to open document as VirtualDocument[Tuple]") } } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/amber/storage/result/iceberg/IcebergDocumentSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.storage.result.iceberg import org.apache.texera.amber.config.StorageConfig import org.apache.texera.amber.core.storage.model.{VirtualDocument, VirtualDocumentSpec} import org.apache.texera.amber.core.storage.{DocumentFactory, IcebergCatalogInstance, VFSURIFactory} import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple} import org.apache.iceberg.Table import org.apache.texera.amber.core.virtualidentity.{ ExecutionIdentity, OperatorIdentity, PhysicalOpIdentity, WorkflowIdentity } import org.apache.texera.amber.core.workflow.{GlobalPortIdentity, PortIdentity} import org.apache.texera.amber.util.IcebergUtil import org.apache.iceberg.catalog.Catalog import org.apache.iceberg.data.Record import org.apache.iceberg.{Schema => IcebergSchema} import org.scalatest.BeforeAndAfterAll import java.lang.reflect.{InvocationHandler, Method, Proxy} import java.net.URI import java.sql.Timestamp import java.util.UUID import java.util.concurrent.atomic.AtomicInteger class IcebergDocumentSpec extends VirtualDocumentSpec[Tuple] with BeforeAndAfterAll { var amberSchema: Schema = _ var icebergSchema: IcebergSchema = _ var serde: (IcebergSchema, Tuple) => Record = _ var deserde: (IcebergSchema, Record) => Tuple = _ var catalog: Catalog = _ val tableNamespace = "test_namespace" var uri: URI = _ override def beforeAll(): Unit = { super.beforeAll() // Initialize Amber Schema with all possible attribute types amberSchema = Schema( List( new Attribute("col-string", AttributeType.STRING), new Attribute("col-int", AttributeType.INTEGER), new Attribute("col-bool", AttributeType.BOOLEAN), new Attribute("col-long", AttributeType.LONG), new Attribute("col-double", AttributeType.DOUBLE), new Attribute("col-timestamp", AttributeType.TIMESTAMP), new Attribute("col-binary", AttributeType.BINARY) ) ) // Initialize Iceberg Schema icebergSchema = IcebergUtil.toIcebergSchema(amberSchema) // Initialize serialization and deserialization functions serde = IcebergUtil.toGenericRecord deserde = (schema, record) => IcebergUtil.fromRecord(record, amberSchema) } override def beforeEach(): Unit = { // Generate a unique table name for each test uri = VFSURIFactory.createResultURI( WorkflowIdentity(0), ExecutionIdentity(0), GlobalPortIdentity( PhysicalOpIdentity( logicalOpId = OperatorIdentity(s"test_table_${UUID.randomUUID().toString.replace("-", "")}"), layerName = "main" ), PortIdentity() ) ) DocumentFactory.createDocument(uri, amberSchema) super.beforeEach() } override def afterAll(): Unit = { super.afterAll() } override def getDocument: VirtualDocument[Tuple] = { DocumentFactory.openDocument(uri)._1.asInstanceOf[VirtualDocument[Tuple]] } it should "not trigger excessive catalog seeks when reading the last file (lazy file advancement)" in { val batchSize = StorageConfig.icebergTableCommitBatchSize val items = generateSampleItems().take(batchSize * 2) val (batch1, batch2) = items.splitAt(batchSize) // Write two separate batches to produce two committed data files. // This also initialises `document.catalog` (lazy val) with the real catalog, which // is why we open a fresh reader document below after injecting the spy. val writer1 = document.writer(UUID.randomUUID().toString) writer1.open(); batch1.foreach(writer1.putOne); writer1.close() val writer2 = document.writer(UUID.randomUUID().toString) writer2.open(); batch2.foreach(writer2.putOne); writer2.close() val refreshCount = new AtomicInteger(0) val realCatalog = IcebergCatalogInstance.getInstance() IcebergCatalogInstance.replaceInstance(catalogWithRefreshSpy(realCatalog, refreshCount)) // Open a fresh reader: its `catalog` lazy val hasn't been initialised yet, so it // will pick up the spy catalog on first access inside seekToUsableFile. val readerDoc = getDocument try { val retrieved = readerDoc.get().toList assert( retrieved.toSet == items.toSet, "All records from both files should be read correctly" ) // With lazy file advancement seekToUsableFile() (and therefore table.refresh()) is called: // once on iterator creation, once when the last file is exhausted → 2 total. // Without the fix it would be called once per hasNext() on the last file → O(batchSize). assert( refreshCount.get() <= 4, s"table.refresh() should be called at most 4 times (lazy advancement), but was ${refreshCount.get()}" ) } finally { IcebergCatalogInstance.replaceInstance(realCatalog) } } /** Returns a dynamic proxy for `realTable` that increments `counter` on every `refresh()` call. */ private def tableWithRefreshSpy(realTable: Table, counter: AtomicInteger): Table = Proxy .newProxyInstance( classOf[Table].getClassLoader, Array(classOf[Table]), new InvocationHandler { override def invoke(proxy: Object, method: Method, args: Array[Object]): Object = { if (method.getName == "refresh") counter.incrementAndGet() if (args == null) method.invoke(realTable) else method.invoke(realTable, args: _*) } } ) .asInstanceOf[Table] /** Returns a dynamic proxy for `realCatalog` that wraps every loaded `Table` with a refresh spy. */ private def catalogWithRefreshSpy(realCatalog: Catalog, counter: AtomicInteger): Catalog = Proxy .newProxyInstance( classOf[Catalog].getClassLoader, Array(classOf[Catalog]), new InvocationHandler { override def invoke(proxy: Object, method: Method, args: Array[Object]): Object = { val result = if (args == null) method.invoke(realCatalog) else method.invoke(realCatalog, args: _*) if (method.getName == "loadTable" && result != null) tableWithRefreshSpy(result.asInstanceOf[Table], counter) else result } } ) .asInstanceOf[Catalog] override def generateSampleItems(): List[Tuple] = { val baseTuples = List( Tuple .builder(amberSchema) .add("col-string", AttributeType.STRING, "Hello World") .add("col-int", AttributeType.INTEGER, 42) .add("col-bool", AttributeType.BOOLEAN, true) .add("col-long", AttributeType.LONG, 12345678901234L) .add("col-double", AttributeType.DOUBLE, 3.14159) .add("col-timestamp", AttributeType.TIMESTAMP, new Timestamp(System.currentTimeMillis())) .add("col-binary", AttributeType.BINARY, Array[Byte](0, 1, 2, 3, 4, 5, 6, 7)) .build(), Tuple .builder(amberSchema) .add("col-string", AttributeType.STRING, "") .add("col-int", AttributeType.INTEGER, -1) .add("col-bool", AttributeType.BOOLEAN, false) .add("col-long", AttributeType.LONG, -98765432109876L) .add("col-double", AttributeType.DOUBLE, -0.001) .add("col-timestamp", AttributeType.TIMESTAMP, new Timestamp(0L)) .add("col-binary", AttributeType.BINARY, Array[Byte](127, -128, 0, 64)) .build(), Tuple .builder(amberSchema) .add("col-string", AttributeType.STRING, "Special Characters: \n\t\r") .add("col-int", AttributeType.INTEGER, Int.MaxValue) .add("col-bool", AttributeType.BOOLEAN, true) .add("col-long", AttributeType.LONG, Long.MaxValue) .add("col-double", AttributeType.DOUBLE, Double.MaxValue) .add("col-timestamp", AttributeType.TIMESTAMP, new Timestamp(1234567890L)) .add("col-binary", AttributeType.BINARY, Array[Byte](1, 2, 3, 4, 5)) .build() ) def generateRandomBinary(size: Int): Array[Byte] = { val array = new Array[Byte](size) scala.util.Random.nextBytes(array) array } val additionalTuples = (1 to 20000).map { i => Tuple .builder(amberSchema) .add("col-string", AttributeType.STRING, if (i % 7 == 0) null else s"Generated String $i") .add("col-int", AttributeType.INTEGER, if (i % 5 == 0) null else i) .add("col-bool", AttributeType.BOOLEAN, if (i % 6 == 0) null else i % 2 == 0) .add("col-long", AttributeType.LONG, if (i % 4 == 0) null else i.toLong * 1000000L) .add("col-double", AttributeType.DOUBLE, if (i % 3 == 0) null else i * 0.12345) .add( "col-timestamp", AttributeType.TIMESTAMP, if (i % 8 == 0) null else new Timestamp(System.currentTimeMillis() + i * 1000L) ) .add("col-binary", AttributeType.BINARY, if (i % 9 == 0) null else generateRandomBinary(10)) .build() } baseTuples ++ additionalTuples } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/amber/storage/result/iceberg/IcebergTableStatsSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.storage.result.iceberg import org.apache.texera.amber.core.storage.model.VirtualDocument import org.apache.texera.amber.core.storage.{DocumentFactory, VFSURIFactory} import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple} import org.apache.texera.amber.core.virtualidentity.{ ExecutionIdentity, OperatorIdentity, PhysicalOpIdentity, WorkflowIdentity } import org.apache.texera.amber.core.workflow.{GlobalPortIdentity, PortIdentity} import org.apache.texera.amber.util.IcebergUtil import org.apache.iceberg.catalog.Catalog import org.apache.iceberg.data.Record import org.apache.iceberg.{Schema => IcebergSchema} import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.{BeforeAndAfterAll, Suite} import java.net.URI import java.sql.Timestamp import java.time.format.DateTimeFormatter import java.time.{LocalDate, ZoneId} import java.util.UUID class IcebergTableStatsSpec extends AnyFlatSpec with BeforeAndAfterAll with Suite { var amberSchema: Schema = _ var icebergSchema: IcebergSchema = _ var serde: (IcebergSchema, Tuple) => Record = _ var deserde: (IcebergSchema, Record) => Tuple = _ var catalog: Catalog = _ val tableNamespace = "test_namespace" var uri: URI = VFSURIFactory.createResultURI( WorkflowIdentity(0), ExecutionIdentity(0), GlobalPortIdentity( PhysicalOpIdentity( logicalOpId = OperatorIdentity(s"test_table_${UUID.randomUUID().toString.replace("-", "")}"), layerName = "main" ), PortIdentity() ) ) override def beforeAll(): Unit = { super.beforeAll() // Initialize Amber Schema with all possible attribute types amberSchema = Schema( List( new Attribute("col-string", AttributeType.STRING), new Attribute("col-int", AttributeType.INTEGER), new Attribute("col-bool", AttributeType.BOOLEAN), new Attribute("col-long", AttributeType.LONG), new Attribute("col-double", AttributeType.DOUBLE), new Attribute("col-timestamp", AttributeType.TIMESTAMP), new Attribute("col-binary", AttributeType.BINARY) ) ) // Initialize Iceberg Schema icebergSchema = IcebergUtil.toIcebergSchema(amberSchema) // Initialize serialization and deserialization functions serde = IcebergUtil.toGenericRecord deserde = (schema, record) => IcebergUtil.fromRecord(record, amberSchema) } behavior of "TableStatistics" it should "get correct statistics after inserting three tuples" in { DocumentFactory.createDocument(uri, amberSchema) val document = DocumentFactory.openDocument(uri)._1.asInstanceOf[VirtualDocument[Tuple]] val tuples = generateSampleItems( List( ( "first", Some(42), Some(true), Some(100L), Some(3.14), Some(10000L), Some(Array[Byte](1, 2, 3)) ), ( "second", Some(50), Some(false), Some(200L), Some(2.71), Some(20000L), Some(Array[Byte](4, 5, 6)) ), ( "third", Some(30), Some(true), Some(150L), Some(1.41), Some(15000L), Some(Array[Byte](7, 8, 9)) ) ) ) // Get writer and write items val writer = document.writer(UUID.randomUUID().toString) writer.open() tuples.foreach(writer.putOne) writer.close() val stats = document.getTableStatistics assert(!stats("col-string").contains("min")) assert(!stats("col-string").contains("max")) assert(stats("col-int")("min") == 30) assert(stats("col-int")("max") == 50) assert(stats("col-long")("min") == 100L) assert(stats("col-long")("max") == 200L) assert(stats("col-double")("min") == 1.41) assert(stats("col-double")("max") == 3.14) val formatter = DateTimeFormatter.ISO_LOCAL_DATE val actualMin = LocalDate.parse(stats("col-timestamp")("min").asInstanceOf[String], formatter) val actualMax = LocalDate.parse(stats("col-timestamp")("max").asInstanceOf[String], formatter) val expectedMin = new Timestamp(10000L).toInstant.atZone(ZoneId.systemDefault()).toLocalDate val expectedMax = new Timestamp(20000L).toInstant.atZone(ZoneId.systemDefault()).toLocalDate assert(actualMin == expectedMin) assert(actualMax == expectedMax) assert(stats("col-bool")("not_null_count") == 3L) } it should "get updated statistics after adding new tuples" in { val newTuples = generateSampleItems( List( ( "new-first", Some(99), Some(false), Some(300L), Some(4.56), Some(30000L), Some(Array[Byte](10, 11, 12)) ), ( "new-second", Some(77), Some(true), Some(400L), Some(5.67), Some(40000L), Some(Array[Byte](13, 14, 15)) ) ) ) val document = DocumentFactory.openDocument(uri)._1.asInstanceOf[VirtualDocument[Tuple]] // Get writer and write items val writer = document.writer(UUID.randomUUID().toString) writer.open() newTuples.foreach(writer.putOne) writer.close() val stats = document.getTableStatistics assert(stats("col-int")("min") == 30) assert(stats("col-int")("max") == 99) assert(stats("col-long")("min") == 100L) assert(stats("col-long")("max") == 400L) assert(stats("col-double")("min") == 1.41) assert(stats("col-double")("max") == 5.67) val formatter = DateTimeFormatter.ISO_LOCAL_DATE val actualMin = LocalDate.parse(stats("col-timestamp")("min").asInstanceOf[String], formatter) val actualMax = LocalDate.parse(stats("col-timestamp")("max").asInstanceOf[String], formatter) val expectedMin = new Timestamp(10000L).toInstant.atZone(ZoneId.systemDefault()).toLocalDate val expectedMax = new Timestamp(40000L).toInstant.atZone(ZoneId.systemDefault()).toLocalDate assert(actualMin == expectedMin) assert(actualMax == expectedMax) assert(stats("col-bool")("not_null_count") == 5L) } it should "correctly count non-null values in the presence of null values" in { val tuplesWithNulls = generateSampleItems( List( ("first", Some(42), None, Some(100L), Some(3.14), Some(10000L), Some(Array[Byte](1, 2, 3))), ( "second", None, Some(false), Some(200L), Some(2.71), Some(20000L), Some(Array[Byte](4, 5, 6)) ), ("third", Some(30), Some(true), None, Some(1.41), Some(15000L), None) ) ) val document = DocumentFactory.openDocument(uri)._1.asInstanceOf[VirtualDocument[Tuple]] val writer = document.writer(UUID.randomUUID().toString) writer.open() tuplesWithNulls.foreach(writer.putOne) writer.close() val stats = document.getTableStatistics assert(stats("col-string")("not_null_count") == 8L) assert(stats("col-int")("not_null_count") == 7L) assert(stats("col-bool")("not_null_count") == 7L) assert(stats("col-long")("not_null_count") == 7L) assert(stats("col-double")("not_null_count") == 8L) assert(stats("col-timestamp")("not_null_count") == 8L) assert(stats("col-binary")("not_null_count") == 7L) } def generateSampleItems( values: List[ ( String, Option[Int], Option[Boolean], Option[Long], Option[Double], Option[Long], Option[Array[Byte]] ) ] ): List[Tuple] = { values.map { case (strVal, intVal, boolVal, longVal, doubleVal, timestampVal, binaryVal) => Tuple .builder(amberSchema) .add("col-string", AttributeType.STRING, strVal) .add("col-int", AttributeType.INTEGER, intVal.orNull) .add("col-bool", AttributeType.BOOLEAN, boolVal.orNull) .add("col-long", AttributeType.LONG, longVal.orNull) .add("col-double", AttributeType.DOUBLE, doubleVal.orNull) .add("col-timestamp", AttributeType.TIMESTAMP, timestampVal.map(new Timestamp(_)).orNull) .add("col-binary", AttributeType.BINARY, binaryVal.orNull) .build() } } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/amber/util/ArrowUtilsSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.util import org.apache.arrow.vector.types.{FloatingPointPrecision, TimeUnit} import org.apache.arrow.vector.types.pojo.{ArrowType, Field, FieldType} import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema} import org.apache.texera.amber.core.tuple.AttributeTypeUtils.AttributeTypeException import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import java.util import scala.jdk.CollectionConverters.CollectionHasAsScala class ArrowUtilsSpec extends AnyFlatSpec with Matchers { // ----- toAttributeType ----- "toAttributeType" should "map Int(16) to INTEGER" in { ArrowUtils.toAttributeType(new ArrowType.Int(16, true)) shouldBe AttributeType.INTEGER } it should "map Int(32) to INTEGER" in { ArrowUtils.toAttributeType(new ArrowType.Int(32, true)) shouldBe AttributeType.INTEGER } it should "map Int(64) to LONG" in { ArrowUtils.toAttributeType(new ArrowType.Int(64, true)) shouldBe AttributeType.LONG } it should "map non-standard Int bit-widths to LONG (current behavior)" in { // Pin: the source code's match is `case 16 | 32 => INTEGER` then // `case 64 | _ => LONG`. The trailing `_` makes the second arm a // catch-all, so Int(8), Int(128) and any other width all surface as // LONG. A future fix that distinguishes those widths will deliberately // break this spec. ArrowUtils.toAttributeType(new ArrowType.Int(8, true)) shouldBe AttributeType.LONG ArrowUtils.toAttributeType(new ArrowType.Int(128, true)) shouldBe AttributeType.LONG } it should "map Bool to BOOLEAN" in { ArrowUtils.toAttributeType(ArrowType.Bool.INSTANCE) shouldBe AttributeType.BOOLEAN } it should "map FloatingPoint to DOUBLE" in { ArrowUtils.toAttributeType( new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE) ) shouldBe AttributeType.DOUBLE } it should "map Timestamp to TIMESTAMP" in { ArrowUtils.toAttributeType( new ArrowType.Timestamp(TimeUnit.MILLISECOND, "UTC") ) shouldBe AttributeType.TIMESTAMP } it should "map Utf8 to STRING" in { ArrowUtils.toAttributeType(ArrowType.Utf8.INSTANCE) shouldBe AttributeType.STRING } it should "map Binary to BINARY" in { ArrowUtils.toAttributeType(new ArrowType.Binary) shouldBe AttributeType.BINARY } it should "throw AttributeTypeException for unsupported Arrow types" in { // ArrowType.Null is a real Arrow type that this method doesn't handle. assertThrows[AttributeTypeException] { ArrowUtils.toAttributeType(ArrowType.Null.INSTANCE) } } // ----- fromAttributeType ----- "fromAttributeType" should "map INTEGER to Int(32, signed)" in { val arrow = ArrowUtils.fromAttributeType(AttributeType.INTEGER) arrow shouldBe new ArrowType.Int(32, true) } it should "map LONG to Int(64, signed)" in { val arrow = ArrowUtils.fromAttributeType(AttributeType.LONG) arrow shouldBe new ArrowType.Int(64, true) } it should "map DOUBLE to FloatingPoint(DOUBLE)" in { val arrow = ArrowUtils.fromAttributeType(AttributeType.DOUBLE) arrow shouldBe new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE) } it should "map BOOLEAN to Bool.INSTANCE" in { ArrowUtils.fromAttributeType(AttributeType.BOOLEAN) shouldBe ArrowType.Bool.INSTANCE } it should "map TIMESTAMP to Timestamp(MILLISECOND, UTC)" in { val arrow = ArrowUtils.fromAttributeType(AttributeType.TIMESTAMP) arrow shouldBe new ArrowType.Timestamp(TimeUnit.MILLISECOND, "UTC") } it should "map BINARY to ArrowType.Binary" in { ArrowUtils.fromAttributeType(AttributeType.BINARY) shouldBe new ArrowType.Binary } it should "map STRING, LARGE_BINARY, and ANY all to Utf8.INSTANCE" in { // Pin: STRING / LARGE_BINARY / ANY collapse onto the same Arrow type // (Utf8). LARGE_BINARY is recovered via field metadata, ANY loses its // distinction entirely. Documenting the collision so a future change // that splits them apart will surface here. ArrowUtils.fromAttributeType(AttributeType.STRING) shouldBe ArrowType.Utf8.INSTANCE ArrowUtils.fromAttributeType( AttributeType.LARGE_BINARY ) shouldBe ArrowType.Utf8.INSTANCE ArrowUtils.fromAttributeType(AttributeType.ANY) shouldBe ArrowType.Utf8.INSTANCE } // ----- bool2int implicit ----- "bool2int implicit" should "map true to 1 and false to 0" in { import ArrowUtils.bool2int val one: Int = true val zero: Int = false one shouldBe 1 zero shouldBe 0 } // ----- toTexeraSchema ----- private def arrowField( name: String, t: ArrowType, metadata: util.Map[String, String] = null ): Field = new Field(name, new FieldType(true, t, null, metadata), null) "toTexeraSchema" should "produce an empty Texera Schema for an empty Arrow schema" in { val arrow = new org.apache.arrow.vector.types.pojo.Schema(util.Arrays.asList[Field]()) ArrowUtils.toTexeraSchema(arrow).getAttributes shouldBe empty } it should "translate each Arrow field to a Texera Attribute by primitive type" in { val arrow = new org.apache.arrow.vector.types.pojo.Schema( util.Arrays.asList( arrowField("a", new ArrowType.Int(32, true)), arrowField("b", new ArrowType.Int(64, true)), arrowField("c", ArrowType.Bool.INSTANCE), arrowField("d", ArrowType.Utf8.INSTANCE) ) ) val schema = ArrowUtils.toTexeraSchema(arrow) val attrs = schema.getAttributes.toList attrs.map(_.getName) shouldBe List("a", "b", "c", "d") attrs.map(_.getType) shouldBe List( AttributeType.INTEGER, AttributeType.LONG, AttributeType.BOOLEAN, AttributeType.STRING ) } it should "promote Utf8 fields to LARGE_BINARY when texera_type metadata says so" in { val md = new util.HashMap[String, String]() md.put("texera_type", "LARGE_BINARY") val arrow = new org.apache.arrow.vector.types.pojo.Schema( util.Arrays.asList( arrowField("blob", ArrowType.Utf8.INSTANCE, md), arrowField("plain", ArrowType.Utf8.INSTANCE) // no metadata ) ) val schema = ArrowUtils.toTexeraSchema(arrow) val attrs = schema.getAttributes.toList attrs.map(_.getName) shouldBe List("blob", "plain") attrs.map(_.getType) shouldBe List(AttributeType.LARGE_BINARY, AttributeType.STRING) } // ----- fromTexeraSchema ----- "fromTexeraSchema" should "translate each Texera Attribute to an Arrow field with primitive types" in { val schema = Schema( List( new Attribute("i", AttributeType.INTEGER), new Attribute("l", AttributeType.LONG), new Attribute("d", AttributeType.DOUBLE), new Attribute("b", AttributeType.BOOLEAN) ) ) val arrow = ArrowUtils.fromTexeraSchema(schema) val fields = arrow.getFields.asScala.toList fields.map(_.getName) shouldBe List("i", "l", "d", "b") fields.map(_.getFieldType.getType) shouldBe List( new ArrowType.Int(32, true), new ArrowType.Int(64, true), new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE), ArrowType.Bool.INSTANCE ) } it should "attach texera_type=LARGE_BINARY metadata to LARGE_BINARY fields and only those" in { val schema = Schema( List( new Attribute("blob", AttributeType.LARGE_BINARY), new Attribute("name", AttributeType.STRING) ) ) val arrow = ArrowUtils.fromTexeraSchema(schema) val fields = arrow.getFields.asScala.toList val blob = fields.find(_.getName == "blob").get val name = fields.find(_.getName == "name").get blob.getMetadata.get("texera_type") shouldBe "LARGE_BINARY" // STRING fields do not get the texera_type metadata. Option(name.getMetadata).map(_.containsKey("texera_type")).getOrElse(false) shouldBe false } // ----- round-trip ----- "schema round-trip" should "preserve primitive AttributeTypes through fromTexeraSchema and back" in { val original = Schema( List( new Attribute("i", AttributeType.INTEGER), new Attribute("l", AttributeType.LONG), new Attribute("d", AttributeType.DOUBLE), new Attribute("b", AttributeType.BOOLEAN), new Attribute("t", AttributeType.TIMESTAMP), new Attribute("s", AttributeType.STRING), new Attribute("y", AttributeType.BINARY) ) ) val recovered = ArrowUtils.toTexeraSchema(ArrowUtils.fromTexeraSchema(original)) recovered.getAttributes.toList.map(a => (a.getName, a.getType)) shouldBe original.getAttributes.toList.map(a => (a.getName, a.getType)) } it should "preserve LARGE_BINARY through the metadata-based path" in { val original = Schema( List( new Attribute("blob", AttributeType.LARGE_BINARY), new Attribute("name", AttributeType.STRING) ) ) val recovered = ArrowUtils.toTexeraSchema(ArrowUtils.fromTexeraSchema(original)) recovered.getAttributes.toList.map(a => (a.getName, a.getType)) shouldBe List( ("blob", AttributeType.LARGE_BINARY), ("name", AttributeType.STRING) ) } it should "lose the ANY distinction (round-trips as STRING)" in { // Pin: ANY fromAttributeType produces Utf8 with no metadata. toAttributeType // then can only see Utf8, so the recovered type is STRING. Documenting this // information loss so a future fix that round-trips ANY can break the spec. val original = Schema(List(new Attribute("v", AttributeType.ANY))) val recovered = ArrowUtils.toTexeraSchema(ArrowUtils.fromTexeraSchema(original)) recovered.getAttributes.toList.map(a => (a.getName, a.getType)) shouldBe List( ("v", AttributeType.STRING) ) } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/amber/util/IcebergUtilSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.util import org.apache.texera.amber.core.tuple.{AttributeType, LargeBinary, Schema, Tuple} import org.apache.texera.amber.util.IcebergUtil.toIcebergSchema import org.apache.iceberg.data.GenericRecord import org.apache.iceberg.types.Types import org.apache.iceberg.{Schema => IcebergSchema} import org.scalatest.flatspec.AnyFlatSpec import java.nio.ByteBuffer import java.sql.Timestamp import java.time.{LocalDateTime, ZoneId} import scala.jdk.CollectionConverters._ class IcebergUtilSpec extends AnyFlatSpec { val texeraSchema: Schema = Schema() .add("test-1", AttributeType.INTEGER) .add("test-2", AttributeType.LONG) .add("test-3", AttributeType.BOOLEAN) .add("test-4", AttributeType.DOUBLE) .add("test-5", AttributeType.TIMESTAMP) .add("test-6", AttributeType.STRING) .add("test-7", AttributeType.BINARY) val icebergSchema: IcebergSchema = new IcebergSchema( List( Types.NestedField.optional(1, "test-1", Types.IntegerType.get()), Types.NestedField.optional(2, "test-2", Types.LongType.get()), Types.NestedField.optional(3, "test-3", Types.BooleanType.get()), Types.NestedField.optional(4, "test-4", Types.DoubleType.get()), Types.NestedField.optional(5, "test-5", Types.TimestampType.withoutZone()), Types.NestedField.optional(6, "test-6", Types.StringType.get()), Types.NestedField.optional(7, "test-7", Types.BinaryType.get()) ).asJava ) behavior of "IcebergUtil" it should "convert from AttributeType to Iceberg Type correctly" in { assert(IcebergUtil.toIcebergType(AttributeType.INTEGER) == Types.IntegerType.get()) assert(IcebergUtil.toIcebergType(AttributeType.LONG) == Types.LongType.get()) assert(IcebergUtil.toIcebergType(AttributeType.BOOLEAN) == Types.BooleanType.get()) assert(IcebergUtil.toIcebergType(AttributeType.DOUBLE) == Types.DoubleType.get()) assert(IcebergUtil.toIcebergType(AttributeType.TIMESTAMP) == Types.TimestampType.withoutZone()) assert(IcebergUtil.toIcebergType(AttributeType.STRING) == Types.StringType.get()) assert(IcebergUtil.toIcebergType(AttributeType.BINARY) == Types.BinaryType.get()) } it should "convert from Iceberg Type to AttributeType correctly" in { assert(IcebergUtil.fromIcebergType(Types.IntegerType.get()) == AttributeType.INTEGER) assert(IcebergUtil.fromIcebergType(Types.LongType.get()) == AttributeType.LONG) assert(IcebergUtil.fromIcebergType(Types.BooleanType.get()) == AttributeType.BOOLEAN) assert(IcebergUtil.fromIcebergType(Types.DoubleType.get()) == AttributeType.DOUBLE) assert( IcebergUtil.fromIcebergType(Types.TimestampType.withoutZone()) == AttributeType.TIMESTAMP ) assert(IcebergUtil.fromIcebergType(Types.StringType.get()) == AttributeType.STRING) assert(IcebergUtil.fromIcebergType(Types.BinaryType.get()) == AttributeType.BINARY) } it should "convert from Texera Schema to Iceberg Schema correctly" in { assert(IcebergUtil.toIcebergSchema(texeraSchema).sameSchema(icebergSchema)) } it should "convert from Iceberg Schema to Texera Schema correctly" in { assert(IcebergUtil.fromIcebergSchema(icebergSchema) == texeraSchema) } it should "convert Texera Tuple to Iceberg GenericRecord correctly" in { val tuple = Tuple .builder(texeraSchema) .addSequentially( Array( Int.box(42), Long.box(123456789L), Boolean.box(true), Double.box(3.14), new Timestamp(10000L), "hello world", Array[Byte](1, 2, 3, 4) ) ) .build() val record = IcebergUtil.toGenericRecord(toIcebergSchema(tuple.schema), tuple) assert(record.getField("test-1") == 42) assert(record.getField("test-2") == 123456789L) assert(record.getField("test-3") == true) assert(record.getField("test-4") == 3.14) assert(record.getField("test-5") == new Timestamp(10000L).toLocalDateTime) assert(record.getField("test-6") == "hello world") assert(record.getField("test-7") == ByteBuffer.wrap(Array[Byte](1, 2, 3, 4))) val tupleFromRecord = IcebergUtil.fromRecord(record, texeraSchema) assert(tupleFromRecord == tuple) } it should "convert Texera Tuple with null values to Iceberg GenericRecord correctly" in { val tuple = Tuple .builder(texeraSchema) .addSequentially( Array( Int.box(42), // Non-null null, // Null Long Boolean.box(true), // Non-null null, // Null Double null, // Null Timestamp "hello world", // Non-null String null // Null Binary ) ) .build() val record = IcebergUtil.toGenericRecord(toIcebergSchema(tuple.schema), tuple) assert(record.getField("test-1") == 42) assert(record.getField("test-2") == null) assert(record.getField("test-3") == true) assert(record.getField("test-4") == null) assert(record.getField("test-5") == null) assert(record.getField("test-6") == "hello world") assert(record.getField("test-7") == null) val tupleFromRecord = IcebergUtil.fromRecord(record, texeraSchema) assert(tupleFromRecord == tuple) } it should "convert a fully null Texera Tuple to Iceberg GenericRecord correctly" in { val tuple = Tuple .builder(texeraSchema) .addSequentially( Array( null, // Null Integer null, // Null Long null, // Null Boolean null, // Null Double null, // Null Timestamp null, // Null String null // Null Binary ) ) .build() val record = IcebergUtil.toGenericRecord(toIcebergSchema(tuple.schema), tuple) assert(record.getField("test-1") == null) assert(record.getField("test-2") == null) assert(record.getField("test-3") == null) assert(record.getField("test-4") == null) assert(record.getField("test-5") == null) assert(record.getField("test-6") == null) assert(record.getField("test-7") == null) val tupleFromRecord = IcebergUtil.fromRecord(record, texeraSchema) assert(tupleFromRecord == tuple) } it should "convert Iceberg GenericRecord to Texera Tuple correctly" in { val record = GenericRecord.create(icebergSchema) record.setField("test-1", 42) record.setField("test-2", 123456789L) record.setField("test-3", true) record.setField("test-4", 3.14) record.setField( "test-5", LocalDateTime.ofInstant(new Timestamp(10000L).toInstant, ZoneId.systemDefault()) ) record.setField("test-6", "hello world") record.setField("test-7", ByteBuffer.wrap(Array[Byte](1, 2, 3, 4))) val tuple = IcebergUtil.fromRecord(record, texeraSchema) assert(tuple.getField[Integer]("test-1") == 42) assert(tuple.getField[Long]("test-2") == 123456789L) assert(tuple.getField[Boolean]("test-3") == true) assert(tuple.getField[Double]("test-4") == 3.14) assert(tuple.getField[Timestamp]("test-5") == new Timestamp(10000L)) assert(tuple.getField[String]("test-6") == "hello world") assert(tuple.getField[Array[Byte]]("test-7") sameElements Array[Byte](1, 2, 3, 4)) } // LARGE_BINARY type tests it should "convert LARGE_BINARY type correctly between Texera and Iceberg" in { // LARGE_BINARY stored as StringType with field name suffix assert(IcebergUtil.toIcebergType(AttributeType.LARGE_BINARY) == Types.StringType.get()) assert(IcebergUtil.fromIcebergType(Types.StringType.get(), "field") == AttributeType.STRING) assert( IcebergUtil.fromIcebergType( Types.StringType.get(), "field__texera_large_binary_ptr" ) == AttributeType.LARGE_BINARY ) } it should "convert schemas with LARGE_BINARY fields correctly" in { val texeraSchema = Schema() .add("id", AttributeType.INTEGER) .add("large_data", AttributeType.LARGE_BINARY) val icebergSchema = IcebergUtil.toIcebergSchema(texeraSchema) // LARGE_BINARY field gets encoded name with suffix assert(icebergSchema.findField("large_data__texera_large_binary_ptr") != null) assert( icebergSchema.findField("large_data__texera_large_binary_ptr").`type`() == Types.StringType .get() ) // Round-trip preserves schema val roundTripSchema = IcebergUtil.fromIcebergSchema(icebergSchema) assert(roundTripSchema.getAttribute("large_data").getType == AttributeType.LARGE_BINARY) } it should "convert tuples with LARGE_BINARY to records and back correctly" in { val schema = Schema() .add("id", AttributeType.INTEGER) .add("large_data", AttributeType.LARGE_BINARY) val tuple = Tuple .builder(schema) .addSequentially(Array(Int.box(42), new LargeBinary("s3://bucket/object/key.data"))) .build() val record = IcebergUtil.toGenericRecord(toIcebergSchema(schema), tuple) // LARGE_BINARY stored as URI string with encoded field name assert(record.getField("id") == 42) assert(record.getField("large_data__texera_large_binary_ptr") == "s3://bucket/object/key.data") // Round-trip preserves data val roundTripTuple = IcebergUtil.fromRecord(record, schema) assert(roundTripTuple == tuple) // LargeBinary properties are accessible val largeBinary = roundTripTuple.getField[LargeBinary]("large_data") assert(largeBinary.getUri == "s3://bucket/object/key.data") assert(largeBinary.getBucketName == "bucket") assert(largeBinary.getObjectKey == "object/key.data") } it should "handle null LARGE_BINARY values correctly" in { val schema = Schema().add("data", AttributeType.LARGE_BINARY) val tupleWithNull = Tuple.builder(schema).addSequentially(Array(null)).build() val record = IcebergUtil.toGenericRecord(toIcebergSchema(schema), tupleWithNull) assert(record.getField("data__texera_large_binary_ptr") == null) assert(IcebergUtil.fromRecord(record, schema) == tupleWithNull) } it should "handle multiple LARGE_BINARY fields and mixed types correctly" in { val schema = Schema() .add("int_field", AttributeType.INTEGER) .add("large_binary_1", AttributeType.LARGE_BINARY) .add("string_field", AttributeType.STRING) .add("large_binary_2", AttributeType.LARGE_BINARY) val tuple = Tuple .builder(schema) .addSequentially( Array( Int.box(123), new LargeBinary("s3://bucket1/file1.dat"), "normal string", null // null LARGE_BINARY ) ) .build() val record = IcebergUtil.toGenericRecord(toIcebergSchema(schema), tuple) assert(record.getField("int_field") == 123) assert(record.getField("large_binary_1__texera_large_binary_ptr") == "s3://bucket1/file1.dat") assert(record.getField("string_field") == "normal string") assert(record.getField("large_binary_2__texera_large_binary_ptr") == null) assert(IcebergUtil.fromRecord(record, schema) == tuple) } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/amber/util/JSONUtilsSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.util import com.fasterxml.jackson.databind.JsonNode import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers class JSONUtilsSpec extends AnyFlatSpec with Matchers { private def parse(json: String): JsonNode = JSONUtils.objectMapper.readTree(json) // ----- non-flatten mode (default) ----- "JSONToMap" should "return an empty map for an empty object" in { JSONUtils.JSONToMap(parse("{}")) shouldBe Map.empty[String, String] } it should "return only first-level primitives by default" in { val node = parse("""{"a":"x","b":1,"c":2.5,"d":true}""") JSONUtils.JSONToMap(node) shouldBe Map( "a" -> "x", "b" -> "1", "c" -> "2.5", "d" -> "true" ) } it should "render JSON null as the literal string \"null\" for top-level fields" in { val node = parse("""{"a":null}""") JSONUtils.JSONToMap(node) shouldBe Map("a" -> "null") } it should "skip nested objects when flatten=false" in { val node = parse("""{"a":"x","nested":{"k":"v"}}""") JSONUtils.JSONToMap(node, flatten = false) shouldBe Map("a" -> "x") } it should "skip nested arrays when flatten=false" in { val node = parse("""{"a":"x","arr":[1,2,3]}""") JSONUtils.JSONToMap(node, flatten = false) shouldBe Map("a" -> "x") } // ----- flatten mode ----- it should "flatten a nested object with parent.child keys when flatten=true" in { val node = parse("""{"a":"x","nested":{"k":"v","deep":{"z":"y"}}}""") JSONUtils.JSONToMap(node, flatten = true) shouldBe Map( "a" -> "x", "nested.k" -> "v", "nested.deep.z" -> "y" ) } it should "flatten an array of objects with parent.field keys when flatten=true" in { // The recursion uses 1-based indexing: first array element gets "1", second "2". val node = parse("""{"items":[{"id":"a"},{"id":"b","extra":"e"}]}""") JSONUtils.JSONToMap(node, flatten = true) shouldBe Map( "items1.id" -> "a", "items2.id" -> "b", "items2.extra" -> "e" ) } it should "drop array-of-primitive elements when flatten=true (current behavior)" in { // Pin: the docstring claims `{"E":["X","Y"]}` flattens to // `{"E1":"X","E2":"Y"}`, but the implementation only emits an entry when // the recursive call is iterating an *object* node. Recursing into a // value node returns an empty map, so primitives inside an array are // silently dropped. Document this divergence so a future fix that // brings the code into line with the docstring will deliberately // break this spec and force the contract to be reviewed together. val node = parse("""{"a":"x","arr":["X","Y"]}""") JSONUtils.JSONToMap(node, flatten = true) shouldBe Map("a" -> "x") } it should "respect an explicit parentName for keying" in { val node = parse("""{"k":"v"}""") JSONUtils.JSONToMap(node, flatten = false, parentName = "outer") shouldBe Map( "outer.k" -> "v" ) } it should "return an empty map for a top-level value node" in { // Only object nodes contribute entries directly; a bare value at the top // level (e.g. raw JSON `42`) has no parent key to attach to. JSONUtils.JSONToMap(parse("42")) shouldBe Map.empty[String, String] JSONUtils.JSONToMap(parse("\"x\"")) shouldBe Map.empty[String, String] JSONUtils.JSONToMap(parse("null")) shouldBe Map.empty[String, String] } it should "return an empty map for a top-level array even when flatten=true" in { // A top-level array is iterated with parentName="" so children become // "1", "2", ...; primitives inside still produce no entries (same root // cause as the array-of-primitives case above), and a top-level array // therefore yields nothing for primitive content. JSONUtils.JSONToMap(parse("[1,2,3]"), flatten = true) shouldBe Map.empty[String, String] } it should "key a top-level array of objects with the bare 1-based index" in { val node = parse("""[{"id":"a"},{"id":"b"}]""") JSONUtils.JSONToMap(node, flatten = true) shouldBe Map( "1.id" -> "a", "2.id" -> "b" ) } // ----- objectMapper configuration ----- "objectMapper" should "exclude null and absent fields from serialized output" in { case class Box(present: String, opt: Option[String]) val box = Box("kept", None) // Parse back into a JsonNode so the assertion is structural rather than // substring-based: pretty-printing changes or a "kept" value that happens // to contain "opt" would otherwise produce false positives/negatives. val root = parse(JSONUtils.objectMapper.writeValueAsString(box)) root.get("present").asText() shouldBe "kept" root.has("opt") shouldBe false } it should "serialize Scala collections via DefaultScalaModule" in { // Without DefaultScalaModule registration, Scala Seqs / Maps fall through // to Jackson's bean serialization and emit reflection-leaking output. Pin // the working contract so a future ObjectMapper rewire that drops the // module catches the regression here. Use structural assertions on a // parsed tree so whitespace / pretty-printing changes don't flake. val payload = Map("xs" -> Seq("a", "b"), "n" -> Seq.empty[String]) val root = parse(JSONUtils.objectMapper.writeValueAsString(payload)) val xs = root.get("xs") xs.isArray shouldBe true xs.size() shouldBe 2 xs.get(0).asText() shouldBe "a" xs.get(1).asText() shouldBe "b" val n = root.get("n") n.isArray shouldBe true n.size() shouldBe 0 } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/amber/util/VirtualIdentityUtilsSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.util import org.apache.texera.amber.core.virtualidentity.{ ActorVirtualIdentity, OperatorIdentity, PhysicalOpIdentity, WorkflowIdentity } import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers class VirtualIdentityUtilsSpec extends AnyFlatSpec with Matchers { // ----- createWorkerIdentity ----- "createWorkerIdentity (raw fields)" should "format Worker:WF---" in { val actor = VirtualIdentityUtils.createWorkerIdentity( WorkflowIdentity(7), operator = "myOp", layerName = "main", workerId = 3 ) actor.name shouldBe "Worker:WF7-myOp-main-3" } "createWorkerIdentity (PhysicalOpIdentity overload)" should "delegate to the same encoded format" in { val physicalOpId = PhysicalOpIdentity(OperatorIdentity("myOp"), "main") val actor = VirtualIdentityUtils.createWorkerIdentity( WorkflowIdentity(7), physicalOpId, workerId = 3 ) actor.name shouldBe "Worker:WF7-myOp-main-3" } // ----- getPhysicalOpId ----- "getPhysicalOpId" should "extract operator id and layer name from a worker actor name" in { val actor = ActorVirtualIdentity("Worker:WF7-myOp-main-3") val opId = VirtualIdentityUtils.getPhysicalOpId(actor) opId.logicalOpId.id shouldBe "myOp" opId.layerName shouldBe "main" } it should "fall back to __DummyOperator/__DummyLayer for non-worker actor names" in { val controller = ActorVirtualIdentity("CONTROLLER") val opId = VirtualIdentityUtils.getPhysicalOpId(controller) opId.logicalOpId.id shouldBe "__DummyOperator" opId.layerName shouldBe "__DummyLayer" } it should "tolerate operator names that contain hyphens by greedy backtracking" in { // The operator capture group is `.+` which backtracks to leave the trailing // `-(\w+)-(\d+)` slots populated. A multi-hyphen operator name must still // round-trip without losing characters from the operator itself. val actor = ActorVirtualIdentity("Worker:WF1-multi-part-op-main-0") val opId = VirtualIdentityUtils.getPhysicalOpId(actor) opId.logicalOpId.id shouldBe "multi-part-op" opId.layerName shouldBe "main" } it should "misparse layer names that contain hyphens (current behavior)" in { // The layer capture group is `(\w+)`, which does not allow `-`. When the // real layer name contains hyphens (e.g. "1st-physical-op", as seen in // amber WorkerSpec), the greedy operator group eats most of the layer: // operator becomes "myOp-1st-physical" and layer becomes "op". This pins // the current bug so a future fix that broadens `workerNamePattern` to // accept hyphenated layers will surface here and force this spec to be // updated alongside the implementation. val actor = ActorVirtualIdentity("Worker:WF1-myOp-1st-physical-op-3") val opId = VirtualIdentityUtils.getPhysicalOpId(actor) opId.logicalOpId.id shouldBe "myOp-1st-physical" opId.layerName shouldBe "op" } // ----- getWorkerIndex ----- "getWorkerIndex" should "return the trailing numeric workerId from a worker actor name" in { val actor = ActorVirtualIdentity("Worker:WF7-myOp-main-42") VirtualIdentityUtils.getWorkerIndex(actor) shouldBe 42 } it should "throw MatchError on non-worker actor names (current behavior)" in { // getWorkerIndex pattern-matches on workerNamePattern with no fallback, // so passing a special ActorVirtualIdentity like CONTROLLER or SELF // yields scala.MatchError. Pinning this behavior here means a future // change that adds a fallback (or a different exception) breaks this // spec on purpose so the new contract is reviewed. val controller = ActorVirtualIdentity("CONTROLLER") assertThrows[scala.MatchError] { VirtualIdentityUtils.getWorkerIndex(controller) } } // ----- toShorterString ----- "toShorterString" should "keep operator names <= 6 chars unchanged" in { val actor = ActorVirtualIdentity("Worker:WF1-myOp-main-0") VirtualIdentityUtils.toShorterString(actor) shouldBe "WF1-myOp-main-0" } it should "keep operator names of exactly 6 chars unchanged (boundary case)" in { // Pin the off-by-one boundary: the implementation uses `length > 6`, so a // six-character operator name must still pass through untouched. A // regression to `>= 6` would shorten "sixSix" and fail this spec. val actor = ActorVirtualIdentity("Worker:WF1-sixSix-main-0") VirtualIdentityUtils.toShorterString(actor) shouldBe "WF1-sixSix-main-0" } it should "shorten UUID-style operator names to op + last 6 chars of the postfix" in { // The operatorUUIDPattern is `(\w+)-(.+)-(\w+)`; the regex is greedy on the // middle segment, so `op` is the first \w+, and the trailing \w+ is the // postfix that gets `takeRight(6)`-ed. val actor = ActorVirtualIdentity("Worker:WF1-Filter-uuid12-abcdefghij-main-0") val shorter = VirtualIdentityUtils.toShorterString(actor) // postfix = "abcdefghij"; takeRight(6) = "efghij". shorter shouldBe "WF1-Filter-efghij-main-0" } it should "fall back to takeRight(6) when long operator name does not match the UUID pattern" in { // `nohyphens` is one \w+ token with no hyphens, so the UUID pattern can't // match (it requires at least two `-`s) and we hit the takeRight(6) branch. val actor = ActorVirtualIdentity("Worker:WF1-nohyphens-main-0") val shorter = VirtualIdentityUtils.toShorterString(actor) // takeRight(6) of "nohyphens" = "yphens" shorter shouldBe "WF1-yphens-main-0" } it should "return the actor name unchanged when it does not match the worker pattern" in { val controller = ActorVirtualIdentity("CONTROLLER") VirtualIdentityUtils.toShorterString(controller) shouldBe "CONTROLLER" } // ----- getFromActorIdForInputPortStorage ----- "getFromActorIdForInputPortStorage" should "prefix MATERIALIZATION_READER_ to the storage URI plus actor name" in { val toWorker = ActorVirtualIdentity("Worker:WF1-myOp-main-0") val virtualReader = VirtualIdentityUtils.getFromActorIdForInputPortStorage( "iceberg:/warehouse/x", toWorker ) virtualReader.name shouldBe "MATERIALIZATION_READER_iceberg:/warehouse/xWorker:WF1-myOp-main-0" } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/service/util/LargeBinaryInputStreamSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.util import org.apache.texera.amber.core.tuple.LargeBinary import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach} import org.scalatest.funsuite.AnyFunSuite import java.io.{ByteArrayInputStream, IOException} import scala.util.Random class LargeBinaryInputStreamSpec extends AnyFunSuite with S3StorageTestBase with BeforeAndAfterAll with BeforeAndAfterEach { private val testBucketName = "test-large-binary-input-stream" override def beforeAll(): Unit = { super.beforeAll() S3StorageClient.createBucketIfNotExist(testBucketName) } override def afterAll(): Unit = { try { S3StorageClient.deleteDirectory(testBucketName, "") } catch { case _: Exception => // Ignore cleanup errors } super.afterAll() } // Helper methods private def createTestObject(key: String, data: Array[Byte]): LargeBinary = { S3StorageClient.uploadObject(testBucketName, key, new ByteArrayInputStream(data)) new LargeBinary(s"s3://$testBucketName/$key") } private def createTestObject(key: String, data: String): LargeBinary = createTestObject(key, data.getBytes) private def generateRandomData(size: Int): Array[Byte] = Array.fill[Byte](size)((Random.nextInt(256) - 128).toByte) private def withStream[T](largeBinary: LargeBinary)(f: LargeBinaryInputStream => T): T = { val stream = new LargeBinaryInputStream(largeBinary) try { f(stream) } finally { stream.close() } } private def assertThrowsIOExceptionWhenClosed(operation: LargeBinaryInputStream => Unit): Unit = { val largeBinary = createTestObject(s"test/closed-${Random.nextInt()}.txt", "data") val stream = new LargeBinaryInputStream(largeBinary) stream.close() val exception = intercept[IOException](operation(stream)) assert(exception.getMessage.contains("Stream is closed")) } // Constructor Tests test("constructor should reject null LargeBinary") { val exception = intercept[IllegalArgumentException] { new LargeBinaryInputStream(null) } assert(exception.getMessage.contains("LargeBinary cannot be null")) } test("constructor should accept valid LargeBinary") { val largeBinary = createTestObject("test/valid.txt", "test data") withStream(largeBinary) { _ => } } // read() Tests test("read() should read single bytes correctly") { val largeBinary = createTestObject("test/single-byte.txt", "Hello") withStream(largeBinary) { stream => assert(stream.read() == 'H'.toByte) assert(stream.read() == 'e'.toByte) assert(stream.read() == 'l'.toByte) assert(stream.read() == 'l'.toByte) assert(stream.read() == 'o'.toByte) assert(stream.read() == -1) // EOF } } test("read() should return -1 for empty object") { val largeBinary = createTestObject("test/empty.txt", "") withStream(largeBinary) { stream => assert(stream.read() == -1) } } // read(byte[], int, int) Tests test("read(byte[], int, int) should read data into buffer") { val testData = "Hello, World!" val largeBinary = createTestObject("test/buffer-read.txt", testData) withStream(largeBinary) { stream => val buffer = new Array[Byte](testData.length) val bytesRead = stream.read(buffer, 0, buffer.length) assert(bytesRead == testData.length) assert(new String(buffer) == testData) } } test("read(byte[], int, int) should handle partial reads and offsets") { val testData = "Hello, World!" val largeBinary = createTestObject("test/partial.txt", testData) withStream(largeBinary) { stream => // Test partial read val buffer1 = new Array[Byte](5) assert(stream.read(buffer1, 0, 5) == 5) assert(new String(buffer1) == "Hello") } // Test offset withStream(largeBinary) { stream => val buffer2 = new Array[Byte](20) assert(stream.read(buffer2, 5, 10) == 10) assert(new String(buffer2, 5, 10) == "Hello, Wor") } } test("read(byte[], int, int) should return -1 at EOF") { val largeBinary = createTestObject("test/eof.txt", "test") withStream(largeBinary) { stream => val buffer = new Array[Byte](10) stream.read(buffer, 0, 10) assert(stream.read(buffer, 0, 10) == -1) } } // readAllBytes() Tests test("readAllBytes() should read entire object") { val testData = "Hello, World! This is a test." val largeBinary = createTestObject("test/read-all.txt", testData) withStream(largeBinary) { stream => assert(new String(stream.readAllBytes()) == testData) } } test("readAllBytes() should handle large objects") { val largeData = generateRandomData(1024 * 1024) // 1MB val largeBinary = createTestObject("test/large.bin", largeData) withStream(largeBinary) { stream => val bytes = stream.readAllBytes() assert(bytes.length == largeData.length) assert(bytes.sameElements(largeData)) } } test("readAllBytes() should return empty array for empty object") { val largeBinary = createTestObject("test/empty-all.txt", "") withStream(largeBinary) { stream => assert(stream.readAllBytes().length == 0) } } // readNBytes() Tests test("readNBytes() should read exactly N bytes") { val testData = "Hello, World! This is a test." val largeBinary = createTestObject("test/read-n.txt", testData) withStream(largeBinary) { stream => val bytes = stream.readNBytes(5) assert(bytes.length == 5) assert(new String(bytes) == "Hello") } } test("readNBytes() should handle EOF and zero") { val largeBinary = createTestObject("test/read-n-eof.txt", "Hello") withStream(largeBinary) { stream => // Request more than available val bytes = stream.readNBytes(100) assert(bytes.length == 5) assert(new String(bytes) == "Hello") } // Test n=0 withStream(largeBinary) { stream => assert(stream.readNBytes(0).length == 0) } } // skip() Tests test("skip() should skip bytes correctly") { val largeBinary = createTestObject("test/skip.txt", "Hello, World!") withStream(largeBinary) { stream => assert(stream.skip(7) == 7) assert(stream.read() == 'W'.toByte) } } test("skip() should handle EOF and zero") { val largeBinary = createTestObject("test/skip-eof.txt", "Hello") withStream(largeBinary) { stream => assert(stream.skip(100) == 5) assert(stream.read() == -1) } // Test n=0 withStream(largeBinary) { stream => assert(stream.skip(0) == 0) } } // available() Tests test("available() should return non-negative value") { val largeBinary = createTestObject("test/available.txt", "Hello, World!") withStream(largeBinary) { stream => assert(stream.available() >= 0) } } // close() Tests test("close() should be idempotent") { val largeBinary = createTestObject("test/close-idempotent.txt", "data") val stream = new LargeBinaryInputStream(largeBinary) stream.close() stream.close() // Should not throw stream.close() // Should not throw } test("close() should prevent further operations") { val largeBinary = createTestObject("test/close-prevents.txt", "data") val stream = new LargeBinaryInputStream(largeBinary) stream.close() intercept[IOException] { stream.read() } intercept[IOException] { stream.readAllBytes() } intercept[IOException] { stream.readNBytes(10) } intercept[IOException] { stream.skip(10) } intercept[IOException] { stream.available() } } test("close() should work without reading (lazy initialization)") { val largeBinary = createTestObject("test/close-lazy.txt", "data") val stream = new LargeBinaryInputStream(largeBinary) stream.close() // Should not throw } // Closed stream tests - consolidated test("operations should throw IOException when stream is closed") { assertThrowsIOExceptionWhenClosed(_.read()) assertThrowsIOExceptionWhenClosed(_.read(new Array[Byte](10), 0, 10)) assertThrowsIOExceptionWhenClosed(_.readAllBytes()) assertThrowsIOExceptionWhenClosed(_.readNBytes(10)) assertThrowsIOExceptionWhenClosed(_.skip(10)) assertThrowsIOExceptionWhenClosed(_.available()) assertThrowsIOExceptionWhenClosed(_.mark(100)) assertThrowsIOExceptionWhenClosed(_.reset()) } // mark/reset Tests test("markSupported() should delegate to underlying stream") { val largeBinary = createTestObject("test/mark.txt", "data") withStream(largeBinary) { stream => val supported = stream.markSupported() assert(!supported || supported) // Just verify it's callable } } test("mark() and reset() should delegate to underlying stream") { val largeBinary = createTestObject("test/mark-reset.txt", "data") withStream(largeBinary) { stream => if (stream.markSupported()) { stream.mark(100) stream.read() stream.reset() } // If not supported, methods should still be callable } } // Lazy initialization Tests test("lazy initialization should not download until first read") { val largeBinary = createTestObject("test/lazy-init.txt", "data") val stream = new LargeBinaryInputStream(largeBinary) // Creating the stream should not trigger download // Reading should trigger download try { assert(stream.read() == 'd'.toByte) } finally { stream.close() } } // Integration Tests test("should handle chunked reading of large objects") { val largeData = generateRandomData(10 * 1024) // 10KB val largeBinary = createTestObject("test/chunked.bin", largeData) withStream(largeBinary) { stream => val buffer = new Array[Byte](1024) val output = new java.io.ByteArrayOutputStream() var bytesRead = 0 while ({ bytesRead = stream.read(buffer, 0, buffer.length) bytesRead != -1 }) { output.write(buffer, 0, bytesRead) } val result = output.toByteArray assert(result.length == largeData.length) assert(result.sameElements(largeData)) } } test("should handle multiple streams reading same object") { val testData = "Shared data" val largeBinary = createTestObject("test/shared.txt", testData) val stream1 = new LargeBinaryInputStream(largeBinary) val stream2 = new LargeBinaryInputStream(largeBinary) try { assert(new String(stream1.readAllBytes()) == testData) assert(new String(stream2.readAllBytes()) == testData) } finally { stream1.close() stream2.close() } } test("should preserve binary data integrity") { val binaryData = Array[Byte](0, 1, 2, 127, -128, -1, 50, 100) val largeBinary = createTestObject("test/binary.bin", binaryData) withStream(largeBinary) { stream => assert(stream.readAllBytes().sameElements(binaryData)) } } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/service/util/LargeBinaryManagerSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.util import org.apache.texera.amber.core.tuple.LargeBinary import org.scalatest.funsuite.AnyFunSuite class LargeBinaryManagerSpec extends AnyFunSuite with S3StorageTestBase { /** Creates a large binary from string data and returns it. */ private def createLargeBinary(data: String): LargeBinary = { val largeBinary = new LargeBinary() val out = new LargeBinaryOutputStream(largeBinary) try { out.write(data.getBytes) } finally { out.close() } largeBinary } /** Verifies standard bucket name. */ private def assertStandardBucket(pointer: LargeBinary): Unit = { assert(pointer.getBucketName == "texera-large-binaries") assert(pointer.getUri.startsWith("s3://texera-large-binaries/")) } // ======================================== // LargeBinaryInputStream Tests (Standard Java InputStream) // ======================================== test("LargeBinaryInputStream should read all bytes from stream") { val data = "Hello, World! This is a test." val largeBinary = createLargeBinary(data) val stream = new LargeBinaryInputStream(largeBinary) assert(stream.readAllBytes().sameElements(data.getBytes)) stream.close() LargeBinaryManager.deleteAllObjects() } test("LargeBinaryInputStream should read exact number of bytes") { val largeBinary = createLargeBinary("0123456789ABCDEF") val stream = new LargeBinaryInputStream(largeBinary) val result = stream.readNBytes(10) assert(result.length == 10) assert(result.sameElements("0123456789".getBytes)) stream.close() LargeBinaryManager.deleteAllObjects() } test("LargeBinaryInputStream should handle reading more bytes than available") { val data = "Short" val largeBinary = createLargeBinary(data) val stream = new LargeBinaryInputStream(largeBinary) val result = stream.readNBytes(100) assert(result.length == data.length) assert(result.sameElements(data.getBytes)) stream.close() LargeBinaryManager.deleteAllObjects() } test("LargeBinaryInputStream should support standard single-byte read") { val largeBinary = createLargeBinary("ABC") val stream = new LargeBinaryInputStream(largeBinary) assert(stream.read() == 65) // 'A' assert(stream.read() == 66) // 'B' assert(stream.read() == 67) // 'C' assert(stream.read() == -1) // EOF stream.close() LargeBinaryManager.deleteAllObjects() } test("LargeBinaryInputStream should return -1 at EOF") { val largeBinary = createLargeBinary("EOF") val stream = new LargeBinaryInputStream(largeBinary) stream.readAllBytes() // Read all data assert(stream.read() == -1) stream.close() LargeBinaryManager.deleteAllObjects() } test("LargeBinaryInputStream should throw exception when reading from closed stream") { val largeBinary = createLargeBinary("test") val stream = new LargeBinaryInputStream(largeBinary) stream.close() assertThrows[java.io.IOException](stream.read()) assertThrows[java.io.IOException](stream.readAllBytes()) LargeBinaryManager.deleteAllObjects() } test("LargeBinaryInputStream should handle multiple close calls") { val largeBinary = createLargeBinary("test") val stream = new LargeBinaryInputStream(largeBinary) stream.close() stream.close() // Should not throw LargeBinaryManager.deleteAllObjects() } test("LargeBinaryInputStream should read large data correctly") { val largeData = Array.fill[Byte](20000)((scala.util.Random.nextInt(256) - 128).toByte) val largeBinary = new LargeBinary() val out = new LargeBinaryOutputStream(largeBinary) try { out.write(largeData) } finally { out.close() } val stream = new LargeBinaryInputStream(largeBinary) val result = stream.readAllBytes() assert(result.sameElements(largeData)) stream.close() LargeBinaryManager.deleteAllObjects() } // ======================================== // LargeBinaryManager Tests // ======================================== test("LargeBinaryManager should create a large binary") { val pointer = createLargeBinary("Test large binary data") assertStandardBucket(pointer) } test("LargeBinaryInputStream should open and read a large binary") { val data = "Hello from large binary!" val pointer = createLargeBinary(data) val stream = new LargeBinaryInputStream(pointer) val readData = stream.readAllBytes() stream.close() assert(readData.sameElements(data.getBytes)) } test("LargeBinaryInputStream should fail to open non-existent large binary") { val fakeLargeBinary = new LargeBinary("s3://texera-large-binaries/nonexistent/file") val stream = new LargeBinaryInputStream(fakeLargeBinary) try { intercept[Exception] { stream.read() } } finally { try { stream.close() } catch { case _: Exception => } } } test("LargeBinaryManager should delete all large binaries") { val pointer1 = new LargeBinary() val out1 = new LargeBinaryOutputStream(pointer1) try { out1.write("Object 1".getBytes) } finally { out1.close() } val pointer2 = new LargeBinary() val out2 = new LargeBinaryOutputStream(pointer2) try { out2.write("Object 2".getBytes) } finally { out2.close() } LargeBinaryManager.deleteAllObjects() } test("LargeBinaryManager should handle delete with no objects gracefully") { LargeBinaryManager.deleteAllObjects() // Should not throw exception } test("LargeBinaryManager should delete all objects") { val pointer1 = createLargeBinary("Test data") val pointer2 = createLargeBinary("Test data") LargeBinaryManager.deleteAllObjects() } test("LargeBinaryManager should create bucket if it doesn't exist") { val pointer = createLargeBinary("Test bucket creation") assertStandardBucket(pointer) LargeBinaryManager.deleteAllObjects() } test("LargeBinaryManager should handle large objects correctly") { val largeData = Array.fill[Byte](6 * 1024 * 1024)((scala.util.Random.nextInt(256) - 128).toByte) val pointer = new LargeBinary() val out = new LargeBinaryOutputStream(pointer) try { out.write(largeData) } finally { out.close() } val stream = new LargeBinaryInputStream(pointer) val readData = stream.readAllBytes() stream.close() assert(readData.sameElements(largeData)) LargeBinaryManager.deleteAllObjects() } test("LargeBinaryManager should generate unique URIs for different objects") { val testData = "Unique URI test".getBytes val pointer1 = new LargeBinary() val out1 = new LargeBinaryOutputStream(pointer1) try { out1.write(testData) } finally { out1.close() } val pointer2 = new LargeBinary() val out2 = new LargeBinaryOutputStream(pointer2) try { out2.write(testData) } finally { out2.close() } assert(pointer1.getUri != pointer2.getUri) assert(pointer1.getObjectKey != pointer2.getObjectKey) LargeBinaryManager.deleteAllObjects() } test("LargeBinaryInputStream should handle multiple reads from the same large binary") { val data = "Multiple reads test data" val pointer = createLargeBinary(data) val stream1 = new LargeBinaryInputStream(pointer) val readData1 = stream1.readAllBytes() stream1.close() val stream2 = new LargeBinaryInputStream(pointer) val readData2 = stream2.readAllBytes() stream2.close() assert(readData1.sameElements(data.getBytes)) assert(readData2.sameElements(data.getBytes)) LargeBinaryManager.deleteAllObjects() } test("LargeBinaryManager should properly parse bucket name and object key from large binary") { val largeBinary = createLargeBinary("URI parsing test") assertStandardBucket(largeBinary) assert(largeBinary.getObjectKey.nonEmpty) assert(!largeBinary.getObjectKey.startsWith("/")) LargeBinaryManager.deleteAllObjects() } // ======================================== // Object-Oriented API Tests // ======================================== test("LargeBinary with LargeBinaryOutputStream should create a large binary") { val data = "Test data for LargeBinary with LargeBinaryOutputStream" val largeBinary = new LargeBinary() val out = new LargeBinaryOutputStream(largeBinary) try { out.write(data.getBytes) } finally { out.close() } assertStandardBucket(largeBinary) LargeBinaryManager.deleteAllObjects() } test("LargeBinaryInputStream constructor should read large binary contents") { val data = "Test data for LargeBinaryInputStream constructor" val largeBinary = createLargeBinary(data) val stream = new LargeBinaryInputStream(largeBinary) val readData = stream.readAllBytes() stream.close() assert(readData.sameElements(data.getBytes)) LargeBinaryManager.deleteAllObjects() } test("LargeBinaryOutputStream and LargeBinaryInputStream should work together end-to-end") { val data = "End-to-end test data" // Create using streaming API val largeBinary = new LargeBinary() val out = new LargeBinaryOutputStream(largeBinary) try { out.write(data.getBytes) } finally { out.close() } // Read using standard constructor val stream = new LargeBinaryInputStream(largeBinary) val readData = stream.readAllBytes() stream.close() assert(readData.sameElements(data.getBytes)) LargeBinaryManager.deleteAllObjects() } // ======================================== // LargeBinaryOutputStream Tests (New Symmetric API) // ======================================== test("LargeBinaryOutputStream should write and upload data to S3") { val data = "Test data for LargeBinaryOutputStream" val largeBinary = new LargeBinary() val outStream = new LargeBinaryOutputStream(largeBinary) outStream.write(data.getBytes) outStream.close() assertStandardBucket(largeBinary) // Verify data can be read back val inStream = new LargeBinaryInputStream(largeBinary) val readData = inStream.readAllBytes() inStream.close() assert(readData.sameElements(data.getBytes)) LargeBinaryManager.deleteAllObjects() } test("LargeBinaryOutputStream should create large binary") { val data = "Database registration test" val largeBinary = new LargeBinary() val outStream = new LargeBinaryOutputStream(largeBinary) outStream.write(data.getBytes) outStream.close() assertStandardBucket(largeBinary) LargeBinaryManager.deleteAllObjects() } test("LargeBinaryOutputStream should handle large data correctly") { val largeData = Array.fill[Byte](8 * 1024 * 1024)((scala.util.Random.nextInt(256) - 128).toByte) val largeBinary = new LargeBinary() val outStream = new LargeBinaryOutputStream(largeBinary) outStream.write(largeData) outStream.close() // Verify data integrity val inStream = new LargeBinaryInputStream(largeBinary) val readData = inStream.readAllBytes() inStream.close() assert(readData.sameElements(largeData)) LargeBinaryManager.deleteAllObjects() } test("LargeBinaryOutputStream should handle multiple writes") { val largeBinary = new LargeBinary() val outStream = new LargeBinaryOutputStream(largeBinary) outStream.write("Hello ".getBytes) outStream.write("World".getBytes) outStream.write("!".getBytes) outStream.close() val inStream = new LargeBinaryInputStream(largeBinary) val readData = inStream.readAllBytes() inStream.close() assert(readData.sameElements("Hello World!".getBytes)) LargeBinaryManager.deleteAllObjects() } test("LargeBinaryOutputStream should throw exception when writing to closed stream") { val largeBinary = new LargeBinary() val outStream = new LargeBinaryOutputStream(largeBinary) outStream.write("test".getBytes) outStream.close() assertThrows[java.io.IOException](outStream.write("more".getBytes)) LargeBinaryManager.deleteAllObjects() } test("LargeBinaryOutputStream should handle close() being called multiple times") { val largeBinary = new LargeBinary() val outStream = new LargeBinaryOutputStream(largeBinary) outStream.write("test".getBytes) outStream.close() outStream.close() // Should not throw LargeBinaryManager.deleteAllObjects() } test("New LargeBinary() constructor should create unique URIs") { val largeBinary1 = new LargeBinary() val largeBinary2 = new LargeBinary() assert(largeBinary1.getUri != largeBinary2.getUri) assert(largeBinary1.getObjectKey != largeBinary2.getObjectKey) LargeBinaryManager.deleteAllObjects() } test("LargeBinary() and LargeBinaryOutputStream API should be symmetric with input") { val data = "Symmetric API test" // Write using new symmetric API val largeBinary = new LargeBinary() val outStream = new LargeBinaryOutputStream(largeBinary) outStream.write(data.getBytes) outStream.close() // Read using symmetric API val inStream = new LargeBinaryInputStream(largeBinary) val readData = inStream.readAllBytes() inStream.close() assert(readData.sameElements(data.getBytes)) LargeBinaryManager.deleteAllObjects() } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/service/util/LargeBinaryOutputStreamSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.util import org.apache.texera.amber.core.tuple.LargeBinary import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach} import org.scalatest.funsuite.AnyFunSuite import java.io.IOException import scala.util.Random class LargeBinaryOutputStreamSpec extends AnyFunSuite with S3StorageTestBase with BeforeAndAfterAll with BeforeAndAfterEach { private val testBucketName = "test-large-binary-output-stream" override def beforeAll(): Unit = { super.beforeAll() S3StorageClient.createBucketIfNotExist(testBucketName) } override def afterAll(): Unit = { try { S3StorageClient.deleteDirectory(testBucketName, "") } catch { case _: Exception => // Ignore cleanup errors } super.afterAll() } // Helper methods private def createLargeBinary(key: String): LargeBinary = new LargeBinary(s"s3://$testBucketName/$key") private def generateRandomData(size: Int): Array[Byte] = Array.fill[Byte](size)((Random.nextInt(256) - 128).toByte) private def withStream[T](largeBinary: LargeBinary)(f: LargeBinaryOutputStream => T): T = { val stream = new LargeBinaryOutputStream(largeBinary) try f(stream) finally stream.close() } private def readBack(largeBinary: LargeBinary): Array[Byte] = { val inputStream = new LargeBinaryInputStream(largeBinary) try inputStream.readAllBytes() finally inputStream.close() } private def writeAndVerify(key: String, data: Array[Byte]): Unit = { val largeBinary = createLargeBinary(key) withStream(largeBinary)(_.write(data, 0, data.length)) assert(readBack(largeBinary).sameElements(data)) } // === Constructor Tests === test("should reject null LargeBinary") { val exception = intercept[IllegalArgumentException](new LargeBinaryOutputStream(null)) assert(exception.getMessage.contains("LargeBinary cannot be null")) } // === Basic Write Tests === test("should write single bytes correctly") { val largeBinary = createLargeBinary("test/single-bytes.txt") withStream(largeBinary) { stream => "Hello".foreach(c => stream.write(c.toByte)) } assert(new String(readBack(largeBinary)) == "Hello") } test("should write byte arrays correctly") { val testData = "Hello, World!".getBytes writeAndVerify("test/array-write.txt", testData) } test("should handle partial writes with offset and length") { val testData = "Hello, World!".getBytes val largeBinary = createLargeBinary("test/partial-write.txt") withStream(largeBinary) { stream => stream.write(testData, 0, 5) // "Hello" stream.write(testData, 7, 5) // "World" } assert(new String(readBack(largeBinary)) == "HelloWorld") } test("should handle multiple consecutive writes") { val largeBinary = createLargeBinary("test/multiple-writes.txt") withStream(largeBinary) { stream => stream.write("Hello".getBytes) stream.write(", ".getBytes) stream.write("World!".getBytes) } assert(new String(readBack(largeBinary)) == "Hello, World!") } // === Stream Lifecycle Tests === test("flush should not throw") { val largeBinary = createLargeBinary("test/flush.txt") withStream(largeBinary) { stream => stream.write("test".getBytes) stream.flush() stream.write(" data".getBytes) } assert(new String(readBack(largeBinary)) == "test data") } test("close should be idempotent") { val largeBinary = createLargeBinary("test/close-idempotent.txt") val stream = new LargeBinaryOutputStream(largeBinary) stream.write("data".getBytes) stream.close() stream.close() // Should not throw stream.flush() // Should not throw after close assert(new String(readBack(largeBinary)) == "data") } test("close should handle empty stream") { val largeBinary = createLargeBinary("test/empty-stream.txt") val stream = new LargeBinaryOutputStream(largeBinary) stream.close() assert(readBack(largeBinary).length == 0) } // === Error Handling === test("write operations should throw IOException when stream is closed") { val largeBinary = createLargeBinary("test/closed-stream.txt") val stream = new LargeBinaryOutputStream(largeBinary) stream.close() val ex1 = intercept[IOException](stream.write('A'.toByte)) assert(ex1.getMessage.contains("Stream is closed")) val ex2 = intercept[IOException](stream.write("test".getBytes)) assert(ex2.getMessage.contains("Stream is closed")) } // === Large Data Tests === test("should handle large data (1MB)") { val largeData = generateRandomData(1024 * 1024) writeAndVerify("test/large-1mb.bin", largeData) } test("should handle very large data (10MB)") { val veryLargeData = generateRandomData(10 * 1024 * 1024) writeAndVerify("test/large-10mb.bin", veryLargeData) } test("should handle chunked writes") { val totalSize = 1024 * 1024 // 1MB val chunkSize = 8 * 1024 // 8KB val data = generateRandomData(totalSize) val largeBinary = createLargeBinary("test/chunked.bin") withStream(largeBinary) { stream => data.grouped(chunkSize).foreach(chunk => stream.write(chunk)) } assert(readBack(largeBinary).sameElements(data)) } // === Binary Data Tests === test("should preserve all byte values (0-255)") { val allBytes = (0 until 256).map(_.toByte).toArray writeAndVerify("test/all-bytes.bin", allBytes) } // === Integration Tests === test("should handle concurrent writes to different objects") { val streams = (1 to 3).map { i => val obj = createLargeBinary(s"test/concurrent-$i.txt") val stream = new LargeBinaryOutputStream(obj) (obj, stream, s"Data $i") } try { streams.foreach { case (_, stream, data) => stream.write(data.getBytes) } } finally { streams.foreach(_._2.close()) } streams.foreach { case (obj, _, expected) => assert(new String(readBack(obj)) == expected) } } test("should overwrite existing object") { val largeBinary = createLargeBinary("test/overwrite.txt") withStream(largeBinary)(_.write("original data".getBytes)) withStream(largeBinary)(_.write("new data".getBytes)) assert(new String(readBack(largeBinary)) == "new data") } test("should handle mixed write operations") { val largeBinary = createLargeBinary("test/mixed-writes.txt") withStream(largeBinary) { stream => stream.write('A'.toByte) stream.write(" test ".getBytes) stream.write('B'.toByte) val data = "Hello, World!".getBytes stream.write(data, 7, 6) // "World!" } assert(new String(readBack(largeBinary)) == "A test BWorld!") } // === Edge Cases === test("should create bucket automatically") { val newBucketName = s"new-bucket-${Random.nextInt(10000)}" val largeBinary = new LargeBinary(s"s3://$newBucketName/test/auto-create.txt") try { withStream(largeBinary)(_.write("test".getBytes)) assert(new String(readBack(largeBinary)) == "test") } finally { try S3StorageClient.deleteDirectory(newBucketName, "") catch { case _: Exception => /* ignore */ } } } test("should handle rapid open/close cycles") { (1 to 10).foreach { i => withStream(createLargeBinary(s"test/rapid-$i.txt"))(_.write(s"data-$i".getBytes)) } (1 to 10).foreach { i => val result = readBack(createLargeBinary(s"test/rapid-$i.txt")) assert(new String(result) == s"data-$i") } } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/service/util/S3StorageClientSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.util import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach} import org.scalatest.funsuite.AnyFunSuite import java.io.ByteArrayInputStream import scala.util.Random class S3StorageClientSpec extends AnyFunSuite with S3StorageTestBase with BeforeAndAfterAll with BeforeAndAfterEach { private val testBucketName = "test-s3-storage-client" override def beforeAll(): Unit = { super.beforeAll() S3StorageClient.createBucketIfNotExist(testBucketName) } override def afterAll(): Unit = { // Clean up test bucket try { S3StorageClient.deleteDirectory(testBucketName, "") } catch { case _: Exception => // Ignore cleanup errors } super.afterAll() } // Helper methods private def createInputStream(data: String): ByteArrayInputStream = { new ByteArrayInputStream(data.getBytes) } private def createInputStream(data: Array[Byte]): ByteArrayInputStream = { new ByteArrayInputStream(data) } private def readInputStream(inputStream: java.io.InputStream): Array[Byte] = { val buffer = new Array[Byte](8192) val outputStream = new java.io.ByteArrayOutputStream() var bytesRead = 0 while ({ bytesRead = inputStream.read(buffer); bytesRead != -1 }) { outputStream.write(buffer, 0, bytesRead) } outputStream.toByteArray } // ======================================== // uploadObject Tests // ======================================== test("uploadObject should upload a small object successfully") { val testData = "Hello, World! This is a small test object." val objectKey = "test/small-object.txt" val eTag = S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(testData)) assert(eTag != null) assert(eTag.nonEmpty) // Clean up S3StorageClient.deleteObject(testBucketName, objectKey) } test("uploadObject should upload an empty object") { val objectKey = "test/empty-object.txt" val eTag = S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream("")) assert(eTag != null) // Clean up S3StorageClient.deleteObject(testBucketName, objectKey) } test("uploadObject should upload a large object using multipart upload") { // Create data larger than MINIMUM_NUM_OF_MULTIPART_S3_PART (5MB) val largeData = Array.fill[Byte](6 * 1024 * 1024)((Random.nextInt(256) - 128).toByte) val objectKey = "test/large-object.bin" val eTag = S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(largeData)) assert(eTag != null) assert(eTag.nonEmpty) // Verify the uploaded content val downloadedStream = S3StorageClient.downloadObject(testBucketName, objectKey) val downloadedData = readInputStream(downloadedStream) downloadedStream.close() assert(downloadedData.length == largeData.length) assert(downloadedData.sameElements(largeData)) // Clean up S3StorageClient.deleteObject(testBucketName, objectKey) } test("uploadObject should handle objects with special characters in key") { val testData = "Testing special characters" val objectKey = "test/special-chars/file with spaces & symbols!@#.txt" val eTag = S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(testData)) assert(eTag != null) // Clean up S3StorageClient.deleteObject(testBucketName, objectKey) } test("uploadObject should overwrite existing object") { val objectKey = "test/overwrite-test.txt" val data1 = "Original data" val data2 = "Updated data" S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(data1)) val eTag2 = S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(data2)) assert(eTag2 != null) val downloadedStream = S3StorageClient.downloadObject(testBucketName, objectKey) val downloadedData = new String(readInputStream(downloadedStream)) downloadedStream.close() assert(downloadedData == data2) // Clean up S3StorageClient.deleteObject(testBucketName, objectKey) } // ======================================== // downloadObject Tests // ======================================== test("downloadObject should download an object successfully") { val testData = "This is test data for download." val objectKey = "test/download-test.txt" S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(testData)) val inputStream = S3StorageClient.downloadObject(testBucketName, objectKey) val downloadedData = new String(readInputStream(inputStream)) inputStream.close() assert(downloadedData == testData) // Clean up S3StorageClient.deleteObject(testBucketName, objectKey) } test("downloadObject should download large objects correctly") { val largeData = Array.fill[Byte](10 * 1024 * 1024)((Random.nextInt(256) - 128).toByte) val objectKey = "test/large-download-test.bin" S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(largeData)) val inputStream = S3StorageClient.downloadObject(testBucketName, objectKey) val downloadedData = readInputStream(inputStream) inputStream.close() assert(downloadedData.length == largeData.length) assert(downloadedData.sameElements(largeData)) // Clean up S3StorageClient.deleteObject(testBucketName, objectKey) } test("downloadObject should download empty objects") { val objectKey = "test/empty-download-test.txt" S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream("")) val inputStream = S3StorageClient.downloadObject(testBucketName, objectKey) val downloadedData = readInputStream(inputStream) inputStream.close() assert(downloadedData.isEmpty) // Clean up S3StorageClient.deleteObject(testBucketName, objectKey) } test("downloadObject should throw exception for non-existent object") { val nonExistentKey = "test/non-existent-object.txt" assertThrows[Exception] { S3StorageClient.downloadObject(testBucketName, nonExistentKey) } } test("downloadObject should handle binary data correctly") { val binaryData = Array[Byte](0, 1, 2, 127, -128, -1, 64, 32, 16, 8, 4, 2, 1) val objectKey = "test/binary-data.bin" S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(binaryData)) val inputStream = S3StorageClient.downloadObject(testBucketName, objectKey) val downloadedData = readInputStream(inputStream) inputStream.close() assert(downloadedData.sameElements(binaryData)) // Clean up S3StorageClient.deleteObject(testBucketName, objectKey) } // ======================================== // deleteObject Tests // ======================================== test("deleteObject should delete an existing object") { val objectKey = "test/delete-test.txt" S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream("delete me")) S3StorageClient.deleteObject(testBucketName, objectKey) // Verify deletion by attempting to download assertThrows[Exception] { S3StorageClient.downloadObject(testBucketName, objectKey) } } test("deleteObject should not throw exception for non-existent object") { val nonExistentKey = "test/already-deleted.txt" // Should not throw exception S3StorageClient.deleteObject(testBucketName, nonExistentKey) } test("deleteObject should delete large objects") { val largeData = Array.fill[Byte](7 * 1024 * 1024)((Random.nextInt(256) - 128).toByte) val objectKey = "test/large-delete-test.bin" S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(largeData)) S3StorageClient.deleteObject(testBucketName, objectKey) // Verify deletion by attempting to download assertThrows[Exception] { S3StorageClient.downloadObject(testBucketName, objectKey) } } test("deleteObject should handle multiple deletions of the same object") { val objectKey = "test/multi-delete-test.txt" S3StorageClient.uploadObject( testBucketName, objectKey, createInputStream("delete multiple times") ) S3StorageClient.deleteObject(testBucketName, objectKey) // Second delete should not throw exception S3StorageClient.deleteObject(testBucketName, objectKey) } // ======================================== // Integration Tests (combining methods) // ======================================== test("upload, download, and delete workflow should work correctly") { val testData = "Complete workflow test data" val objectKey = "test/workflow-test.txt" // Upload val eTag = S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(testData)) assert(eTag != null) // Download val inputStream = S3StorageClient.downloadObject(testBucketName, objectKey) val downloadedData = new String(readInputStream(inputStream)) inputStream.close() assert(downloadedData == testData) // Delete S3StorageClient.deleteObject(testBucketName, objectKey) } test("multiple objects can be managed independently") { val objects = Map( "test/object1.txt" -> "Data for object 1", "test/object2.txt" -> "Data for object 2", "test/object3.txt" -> "Data for object 3" ) // Upload all objects objects.foreach { case (key, data) => S3StorageClient.uploadObject(testBucketName, key, createInputStream(data)) } // Delete one object S3StorageClient.deleteObject(testBucketName, "test/object2.txt") // Clean up remaining objects S3StorageClient.deleteObject(testBucketName, "test/object1.txt") S3StorageClient.deleteObject(testBucketName, "test/object3.txt") } test("objects with nested paths should be handled correctly") { val objectKey = "test/deeply/nested/path/to/object.txt" val testData = "Nested path test" S3StorageClient.uploadObject(testBucketName, objectKey, createInputStream(testData)) val inputStream = S3StorageClient.downloadObject(testBucketName, objectKey) val downloadedData = new String(readInputStream(inputStream)) inputStream.close() assert(downloadedData == testData) S3StorageClient.deleteObject(testBucketName, objectKey) } } ================================================ FILE: common/workflow-core/src/test/scala/org/apache/texera/service/util/S3StorageTestBase.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.util import com.dimafeng.testcontainers.MinIOContainer import org.apache.texera.amber.config.StorageConfig import org.scalatest.{BeforeAndAfterAll, Suite} import org.testcontainers.utility.DockerImageName /** * Base trait for tests requiring S3 storage (MinIO). * Provides access to a single shared MinIO container across all test suites. * * Usage: Mix this trait into any test suite that needs S3 storage. */ trait S3StorageTestBase extends BeforeAndAfterAll { this: Suite => override def beforeAll(): Unit = { super.beforeAll() // Trigger lazy initialization of shared container S3StorageTestBase.ensureContainerStarted() } } object S3StorageTestBase { private lazy val container: MinIOContainer = { val c = MinIOContainer( dockerImageName = DockerImageName.parse("minio/minio:RELEASE.2025-02-28T09-55-16Z"), userName = "texera_minio", password = "password" ) c.start() val endpoint = s"http://${c.host}:${c.mappedPort(9000)}" StorageConfig.s3Endpoint = endpoint println(s"[S3Storage] Started shared MinIO at $endpoint") sys.addShutdownHook { println("[S3Storage] Stopping shared MinIO...") c.stop() } c } /** Ensures the container is started (triggers lazy initialization). */ def ensureContainerStarted(): Unit = { container // Access lazy val to trigger initialization () } } ================================================ FILE: common/workflow-operator/build.sbt ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. import scala.collection.Seq ///////////////////////////////////////////////////////////////////////////// // Project Settings ///////////////////////////////////////////////////////////////////////////// name := "workflow-operator" enablePlugins(JavaAppPackaging) // Enable semanticdb for Scalafix ThisBuild / semanticdbEnabled := true ThisBuild / semanticdbVersion := scalafixSemanticdb.revision // Manage dependency conflicts by always using the latest revision ThisBuild / conflictManager := ConflictManager.latestRevision // Restrict parallel execution of tests to avoid conflicts Global / concurrentRestrictions += Tags.limit(Tags.Test, 1) ///////////////////////////////////////////////////////////////////////////// // Compiler Options ///////////////////////////////////////////////////////////////////////////// // Scala compiler options Compile / scalacOptions ++= Seq( "-Xelide-below", "WARNING", // Turn on optimizations with "WARNING" as the threshold "-feature", // Check feature warnings "-deprecation", // Check deprecation warnings "-Ywarn-unused:imports" // Check for unused imports ) ///////////////////////////////////////////////////////////////////////////// // Test-related Dependencies ///////////////////////////////////////////////////////////////////////////// libraryDependencies ++= Seq( "org.scalamock" %% "scalamock" % "5.2.0" % Test, // ScalaMock "org.scalatest" %% "scalatest" % "3.2.15" % Test, // ScalaTest "junit" % "junit" % "4.13.2" % Test, // JUnit "com.novocode" % "junit-interface" % "0.11" % Test // SBT interface for JUnit ) ///////////////////////////////////////////////////////////////////////////// // Jackson-related Dependencies ///////////////////////////////////////////////////////////////////////////// val jacksonVersion = "2.18.6" libraryDependencies ++= Seq( "com.fasterxml.jackson.core" % "jackson-databind" % jacksonVersion, // Jackson Databind "com.fasterxml.jackson.core" % "jackson-annotations" % jacksonVersion, // Jackson Annotation "com.fasterxml.jackson.module" %% "jackson-module-scala" % jacksonVersion, // Scala Module ) // Lucene related, used by the keyword-search operators val luceneVersion = "8.7.0" libraryDependencies ++= Seq( "org.apache.lucene" % "lucene-core" % luceneVersion, "org.apache.lucene" % "lucene-queryparser" % luceneVersion, "org.apache.lucene" % "lucene-queries" % luceneVersion, "org.apache.lucene" % "lucene-memory" % luceneVersion ) // kjetland libraryDependencies ++= Seq( "javax.validation" % "validation-api" % "2.0.1.Final", "org.slf4j" % "slf4j-api" % "1.7.26", "io.github.classgraph" % "classgraph" % "4.8.157", "ch.qos.logback" % "logback-classic" % "1.2.3" % "test", "com.github.java-json-tools" % "json-schema-validator" % "2.2.14" % "test", "com.fasterxml.jackson.module" % "jackson-module-kotlin" % jacksonVersion % "test", "com.fasterxml.jackson.datatype" % "jackson-datatype-jdk8" % jacksonVersion % "test", "com.fasterxml.jackson.datatype" % "jackson-datatype-jsr310" % jacksonVersion % "test", "joda-time" % "joda-time" % "2.12.5" % "test", "com.fasterxml.jackson.datatype" % "jackson-datatype-joda" % jacksonVersion % "test", "com.fasterxml.jackson.module" % "jackson-module-jsonSchema" % jacksonVersion, "com.fasterxml.jackson.module" %% "jackson-module-scala" % jacksonVersion, // https://mvnrepository.com/artifact/com.fasterxml.jackson.module/jackson-module-no-ctor-deser "com.fasterxml.jackson.module" % "jackson-module-no-ctor-deser" % jacksonVersion, ) ///////////////////////////////////////////////////////////////////////////// // Additional Dependencies ///////////////////////////////////////////////////////////////////////////// libraryDependencies ++= Seq( "com.thesamet.scalapb" %% "scalapb-json4s" % "0.12.0", "com.github.tototoshi" %% "scala-csv" % "1.3.10", // csv parser "com.konghq" % "unirest-java" % "3.14.2", "commons-io" % "commons-io" % "2.15.1", "org.apache.commons" % "commons-compress" % "1.27.1", "org.tukaani" % "xz" % "1.9", "com.univocity" % "univocity-parsers" % "2.9.1", "org.apache.lucene" % "lucene-analyzers-common" % "8.11.4" ) libraryDependencies += "io.github.classgraph" % "classgraph" % "4.8.184" % Test ================================================ FILE: common/workflow-operator/project/build.properties ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. sbt.version=1.12.9 ================================================ FILE: common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaDraft.java ================================================ /* * Copyright (c) 2016 Kjell Tore Eliassen (mbknor) * Licensed under the MIT License. * * This file is derived from mbknor-jackson-jsonschema. * Source: https://github.com/mbknor/mbknor-jackson-jsonschema */ package com.kjetland.jackson.jsonSchema; public enum JsonSchemaDraft { DRAFT_04("http://json-schema.org/draft-04/schema#"), DRAFT_06("http://json-schema.org/draft-06/schema#"), DRAFT_07("http://json-schema.org/draft-07/schema#"), DRAFT_2019_09("http://json-schema.org/draft/2019-09/schema#"); final String url; JsonSchemaDraft(String url) { this.url = url; } } ================================================ FILE: common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala ================================================ /* * Copyright (c) 2016 Kjell Tore Eliassen (mbknor) * Licensed under the MIT License. * * This file is derived from mbknor-jackson-jsonschema. * Source: https://github.com/mbknor/mbknor-jackson-jsonschema */ package com.kjetland.jackson.jsonSchema import com.fasterxml.jackson.annotation._ import com.fasterxml.jackson.core.JsonParser.NumberType import com.fasterxml.jackson.databind._ import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.introspect.{AnnotatedClass, AnnotatedClassResolver} import com.fasterxml.jackson.databind.jsonFormatVisitors._ import com.fasterxml.jackson.databind.jsontype.impl.MinimalClassNameIdResolver import com.fasterxml.jackson.databind.node.{ArrayNode, JsonNodeFactory, ObjectNode} import com.fasterxml.jackson.databind.util.ClassUtil import com.kjetland.jackson.jsonSchema.annotations._ import io.github.classgraph.{ClassGraph, ScanResult} import org.slf4j.LoggerFactory import java.lang.annotation.Annotation import java.util import java.util.function.Supplier import java.util.{Optional, List => JList} import javax.validation.constraints._ import javax.validation.groups.Default import scala.jdk.CollectionConverters.{CollectionHasAsScala, MapHasAsScala} object JsonSchemaGenerator {} object JsonSchemaConfig { val vanillaJsonSchemaDraft4: JsonSchemaConfig = JsonSchemaConfig( autoGenerateTitleForProperties = false, defaultArrayFormat = None, useOneOfForOption = false, useOneOfForNullables = false, useNullableForOption = false, useNullableForNullables = false, usePropertyOrdering = false, hidePolymorphismTypeProperty = false, disableWarnings = false, useMinLengthForNotNull = false, useTypeIdForDefinitionName = false, customType2FormatMapping = Map(), useMultipleEditorSelectViaProperty = false, uniqueItemClasses = Set(), classTypeReMapping = Map(), jsonSuppliers = Map() ) /** * Use this configuration if using the JsonSchema to generate HTML5 GUI, eg. by using https://github.com/jdorn/json-editor * * autoGenerateTitleForProperties - If property is named "someName", we will add {"title": "Some Name"} * defaultArrayFormat - this will result in a better gui than te default one. */ val html5EnabledSchema: JsonSchemaConfig = JsonSchemaConfig( autoGenerateTitleForProperties = true, defaultArrayFormat = Some("table"), useOneOfForOption = true, useOneOfForNullables = false, useNullableForOption = false, useNullableForNullables = false, usePropertyOrdering = true, hidePolymorphismTypeProperty = true, disableWarnings = false, useMinLengthForNotNull = true, useTypeIdForDefinitionName = false, customType2FormatMapping = Map[String, String]( // Java7 dates "java.time.LocalDateTime" -> "datetime-local", "java.time.OffsetDateTime" -> "datetime", "java.time.LocalDate" -> "date", // Joda-dates "org.joda.time.LocalDate" -> "date" ), useMultipleEditorSelectViaProperty = true, uniqueItemClasses = Set( classOf[scala.collection.immutable.Set[_]], classOf[scala.collection.mutable.Set[_]], classOf[java.util.Set[_]] ), classTypeReMapping = Map(), jsonSuppliers = Map() ) /** * This configuration is exactly like the vanilla JSON schema generator, except that "nullables" have been turned on: * `useOneOfForOption` and `useOneForNullables` have both been set to `true`. With this configuration you can either * use `Optional` or `Option`, or a standard nullable Java type and get back a schema that allows nulls. * * If you need to mix nullable and non-nullable types, you may override the nullability of the type by either setting * a `NotNull` annotation on the given property, or setting the `required` attribute of the `JsonProperty` annotation. */ val nullableJsonSchemaDraft4 = JsonSchemaConfig( autoGenerateTitleForProperties = false, defaultArrayFormat = None, useOneOfForOption = true, useOneOfForNullables = true, useNullableForOption = false, useNullableForNullables = false, usePropertyOrdering = false, hidePolymorphismTypeProperty = false, disableWarnings = false, useMinLengthForNotNull = false, useTypeIdForDefinitionName = false, customType2FormatMapping = Map(), useMultipleEditorSelectViaProperty = false, uniqueItemClasses = Set(), classTypeReMapping = Map(), jsonSuppliers = Map() ) // Java-API def create( autoGenerateTitleForProperties: Boolean, defaultArrayFormat: Optional[String], useOneOfForOption: Boolean, useOneOfForNullables: Boolean, useNullableForOption: Boolean, useNullableForNullables: Boolean, usePropertyOrdering: Boolean, hidePolymorphismTypeProperty: Boolean, disableWarnings: Boolean, useMinLengthForNotNull: Boolean, useTypeIdForDefinitionName: Boolean, customType2FormatMapping: java.util.Map[String, String], useMultipleEditorSelectViaProperty: Boolean, uniqueItemClasses: java.util.Set[Class[_]], classTypeReMapping: java.util.Map[Class[_], Class[_]], jsonSuppliers: java.util.Map[String, Supplier[JsonNode]], subclassesResolver: SubclassesResolver, failOnUnknownProperties: Boolean, javaxValidationGroups: java.util.List[Class[_]] ): JsonSchemaConfig = { JsonSchemaConfig( autoGenerateTitleForProperties, Option(defaultArrayFormat.orElse(null)), useOneOfForOption, useOneOfForNullables, useNullableForOption, useNullableForNullables, usePropertyOrdering, hidePolymorphismTypeProperty, disableWarnings, useMinLengthForNotNull, useTypeIdForDefinitionName, customType2FormatMapping.asScala.toMap, useMultipleEditorSelectViaProperty, uniqueItemClasses.asScala.toSet, classTypeReMapping.asScala.toMap, jsonSuppliers.asScala.toMap, Option(subclassesResolver).getOrElse(new SubclassesResolverImpl()), failOnUnknownProperties, if (javaxValidationGroups == null) Array[Class[_]]() else { javaxValidationGroups.toArray.asInstanceOf[Array[Class[_]]] } ) } } trait SubclassesResolver { def getSubclasses(clazz: Class[_]): List[Class[_]] } case class SubclassesResolverImpl( classGraph: Option[ClassGraph] = None, packagesToScan: List[String] = List(), classesToScan: List[String] = List() ) extends SubclassesResolver { def this() = this(None, List(), List()) def withClassGraph(classGraph: ClassGraph): SubclassesResolverImpl = { this.copy(classGraph = Option(classGraph)) } // Scala API def withPackagesToScan(packagesToScan: List[String]): SubclassesResolverImpl = { this.copy(packagesToScan = packagesToScan) } // Java API def withPackagesToScan(packagesToScan: JList[String]): SubclassesResolverImpl = { this.copy(packagesToScan = packagesToScan.asScala.toList) } // Scala API def withClassesToScan(classesToScan: List[String]): SubclassesResolverImpl = { this.copy(classesToScan = classesToScan) } // Java API def withClassesToScan(classesToScan: JList[String]): SubclassesResolverImpl = { this.copy(classesToScan = classesToScan.asScala.toList) } lazy val reflection: ScanResult = { var classGraphConfigured: Boolean = false if (classGraph.isDefined) { classGraphConfigured = true } val _classGraph: ClassGraph = classGraph.getOrElse(new ClassGraph()) if (packagesToScan.nonEmpty) { classGraphConfigured = true _classGraph.acceptClasses(packagesToScan: _*) } if (classesToScan.nonEmpty) { classGraphConfigured = true _classGraph.acceptClasses(classesToScan: _*) } if (!classGraphConfigured) { LoggerFactory .getLogger(this.getClass) .warn( s"Performance-warning. Since SubclassesResolver is not configured," + s" it scans the entire classpath. " + s"https://github.com/mbknor/mbknor-jackson-jsonSchema#subclass-resolving-using-reflection" ) } _classGraph.enableClassInfo().scan() } override def getSubclasses(clazz: Class[_]): List[Class[_]] = { if (clazz.isInterface) reflection.getClassesImplementing(clazz.getName).loadClasses().asScala.toList else reflection.getSubclasses(clazz.getName).loadClasses().asScala.toList } } case class JsonSchemaConfig( autoGenerateTitleForProperties: Boolean, defaultArrayFormat: Option[String], useOneOfForOption: Boolean, useOneOfForNullables: Boolean, useNullableForOption: Boolean, useNullableForNullables: Boolean, usePropertyOrdering: Boolean, hidePolymorphismTypeProperty: Boolean, disableWarnings: Boolean, useMinLengthForNotNull: Boolean, useTypeIdForDefinitionName: Boolean, customType2FormatMapping: Map[String, String], useMultipleEditorSelectViaProperty: Boolean, // https://github.com/jdorn/json-editor/issues/709 uniqueItemClasses: Set[ Class[_] ], // If rendering array and type is instanceOf class in this set, then we add 'uniqueItems": true' to schema - See // https://github.com/jdorn/json-editor for more info classTypeReMapping: Map[Class[_], Class[ _ ]], // Can be used to prevent rendering using polymorphism for specific classes. jsonSuppliers: Map[String, Supplier[ JsonNode ]], // Suppliers in this map can be accessed using @JsonSchemaInject(jsonSupplierViaLookup = "lookupKey") subclassesResolver: SubclassesResolver = new SubclassesResolverImpl(), // Using default impl that scans entire classpath failOnUnknownProperties: Boolean = true, javaxValidationGroups: Array[Class[_]] = Array(), // Used to match against different validation-groups (javax.validation.constraints) jsonSchemaDraft: JsonSchemaDraft = JsonSchemaDraft.DRAFT_04 ) { def withFailOnUnknownProperties(failOnUnknownProperties: Boolean): JsonSchemaConfig = { this.copy(failOnUnknownProperties = failOnUnknownProperties) } def withSubclassesResolver(subclassesResolver: SubclassesResolver): JsonSchemaConfig = { this.copy(subclassesResolver = subclassesResolver) } def withJavaxValidationGroups(javaxValidationGroups: Array[Class[_]]): JsonSchemaConfig = { this.copy(javaxValidationGroups = javaxValidationGroups) } def withJsonSchemaDraft(jsonSchemaDraft: JsonSchemaDraft): JsonSchemaConfig = { this.copy(jsonSchemaDraft = jsonSchemaDraft) } } /** * Json Schema Generator * * @param rootObjectMapper pre-configured ObjectMapper * @param debug Default = false - set to true if generator should log some debug info while generating the schema * @param config default = vanillaJsonSchemaDraft4. Please use html5EnabledSchema if generating HTML5 GUI, e.g. using https://github.com/jdorn/json-editor */ class JsonSchemaGenerator( val rootObjectMapper: ObjectMapper, debug: Boolean = false, config: JsonSchemaConfig = JsonSchemaConfig.vanillaJsonSchemaDraft4 ) { val javaxValidationGroups = config.javaxValidationGroups // Java API def this(rootObjectMapper: ObjectMapper) = this(rootObjectMapper, false, JsonSchemaConfig.vanillaJsonSchemaDraft4) // Java API def this(rootObjectMapper: ObjectMapper, config: JsonSchemaConfig) = this(rootObjectMapper, false, config) val log = LoggerFactory.getLogger(getClass) val dateFormatMapping = Map[String, String]( // Java7 dates "java.time.LocalDateTime" -> "datetime-local", "java.time.OffsetDateTime" -> "datetime", "java.time.LocalDate" -> "date", // Joda-dates "org.joda.time.LocalDate" -> "date" ) trait MySerializerProvider { var provider: SerializerProvider = null def getProvider: SerializerProvider = provider def setProvider(provider: SerializerProvider): Unit = this.provider = provider } trait EnumSupport { val _node: ObjectNode def enumTypes(enums: util.Set[String]): Unit = { // l(s"JsonStringFormatVisitor-enum.enumTypes: ${enums}") val enumValuesNode = JsonNodeFactory.instance.arrayNode() _node.set("enum", enumValuesNode) enums.asScala.foreach { enumValue => if (enumValue.nonEmpty) { enumValuesNode.add(enumValue) } } } } private def setFormat(node: ObjectNode, format: String): Unit = { node.put("format", format) } // Verifies that the annotation is applicable based on the config.javaxValidationGroups private def annotationIsApplicable(annotation: Annotation): Boolean = { def extractGroupsFromAnnotation(annotation: Annotation): Array[Class[_]] = { // Annotations cannot implement interface, so we have to check each and every // javax-annotation... To prevent bugs with missing groups-extract-impl when new // validation-annotations are added, I've decided to do it using reflection val annotationClass = annotation.annotationType() if (annotationClass.getPackage.getName().startsWith("javax.validation.constraints")) { val groupsMethod = try { annotationClass.getMethod("groups") } catch { case e: NoSuchMethodException => null } if (groupsMethod != null) { groupsMethod.invoke(annotation).asInstanceOf[Array[Class[_]]] } else { Array() } } else { annotation match { case x: JsonSchemaInject => x.javaxValidationGroups() case _ => Array() } } } val javaxDefaultGroup = classOf[Default] val groupsOnAnnotation: Array[Class[_]] = extractGroupsFromAnnotation(annotation) (javaxValidationGroups, groupsOnAnnotation) match { case (Array(), Array()) => true case (Array(), l) => l.contains(javaxDefaultGroup) // Use it if groupsOnAnnotation contains Default case (l, Array()) => l.contains(javaxDefaultGroup) // Use it if javaxValidationGroups contains Default case (a, b) => a.exists(c => b.contains(c)) // One of a must be included in b } } // Tries to retrieve a annotation and validates that it is applicable private def selectAnnotation[T <: Annotation]( property: BeanProperty, annotationClass: Class[T] ): Option[T] = { Option(property.getAnnotation(annotationClass)) .filter(annotationIsApplicable(_)) } // Tries to retrieve a annotation and validates that it is applicable private def selectAnnotation[T <: Annotation]( annotatedClass: AnnotatedClass, annotationClass: Class[T] ): Option[T] = { Option(annotatedClass.getAnnotation(annotationClass)) .filter(annotationIsApplicable(_)) } case class DefinitionInfo( ref: Option[String], jsonObjectFormatVisitor: Option[JsonObjectFormatVisitor] ) // Class that manages creating new definitions or getting $refs to existing definitions class DefinitionsHandler() { private var class2Ref = Map[JavaType, String]() private val definitionsNode = JsonNodeFactory.instance.objectNode() case class WorkInProgress(typeInProgress: JavaType, nodeInProgress: ObjectNode) // Used when 'combining' multiple invocations to getOrCreateDefinition when processing polymorphism. private var workInProgress: Option[WorkInProgress] = None private var workInProgressStack = List[Option[WorkInProgress]]() def pushWorkInProgress(): Unit = { workInProgressStack = workInProgress :: workInProgressStack workInProgress = None } def popworkInProgress(): Unit = { workInProgress = workInProgressStack.head workInProgressStack = workInProgressStack.tail } def extractTypeName(_type: JavaType): String = { // use JsonTypeName annotation if present val annotation = _type.getRawClass.getDeclaredAnnotation(classOf[JsonTypeName]) Option(annotation) .flatMap(a => Option(a.value())) .filter(_.nonEmpty) .getOrElse(_type.getRawClass.getSimpleName) } def getDefinitionName(_type: JavaType): String = { val baseName = if (config.useTypeIdForDefinitionName) _type.getRawClass.getTypeName else extractTypeName(_type) if (_type.hasGenericTypes) { val containedTypes = Range(0, _type.containedTypeCount()).map(_type.containedType) val typeNames = containedTypes.map(getDefinitionName).mkString(",") s"$baseName($typeNames)" } else { baseName } } // Either creates new definitions or return $ref to existing one def getOrCreateDefinition( _type: JavaType )(objectDefinitionBuilder: (ObjectNode) => Option[JsonObjectFormatVisitor]): DefinitionInfo = { class2Ref.get(_type) match { case Some(ref) => workInProgress match { case None => DefinitionInfo(Some(ref), None) case Some(w) => // this is a recursive polymorphism call if (_type != w.typeInProgress) throw new Exception(s"Wrong type - working on ${w.typeInProgress} - got ${_type}") DefinitionInfo(None, objectDefinitionBuilder(w.nodeInProgress)) } case None => // new one - must build it var retryCount = 0 val definitionName = getDefinitionName(_type) var shortRef = definitionName var longRef = "#/definitions/" + definitionName while (class2Ref.values.toList.contains(longRef)) { retryCount = retryCount + 1 shortRef = definitionName + "_" + retryCount longRef = "#/definitions/" + definitionName + "_" + retryCount } class2Ref = class2Ref + (_type -> longRef) // create definition val node = JsonNodeFactory.instance.objectNode() // When processing polymorphism, we might get multiple recursive calls to getOrCreateDefinition - this is a wau to combine them workInProgress = Some(WorkInProgress(_type, node)) definitionsNode.set(shortRef, node) val jsonObjectFormatVisitor = objectDefinitionBuilder.apply(node) workInProgress = None DefinitionInfo(Some(longRef), jsonObjectFormatVisitor) } } def getFinalDefinitionsNode(): Option[ObjectNode] = { if (class2Ref.isEmpty) None else Some(definitionsNode) } } class MyJsonFormatVisitorWrapper( objectMapper: ObjectMapper, level: Int = 0, val node: ObjectNode = JsonNodeFactory.instance.objectNode(), val definitionsHandler: DefinitionsHandler, currentProperty: Option[ BeanProperty ] // This property may represent the BeanProperty when we're directly processing beneath the property ) extends JsonFormatVisitorWrapper with MySerializerProvider { def l(s: => String): Unit = { if (!debug) return var indent = "" for (_ <- 0 until level) { indent = indent + " " } println(indent + s) } def createChild( childNode: ObjectNode, currentProperty: Option[BeanProperty] ): MyJsonFormatVisitorWrapper = { new MyJsonFormatVisitorWrapper( objectMapper, level + 1, node = childNode, definitionsHandler = definitionsHandler, currentProperty = currentProperty ) } def extractDefaultValue(p: BeanProperty): Option[String] = { // Prefer default-value from @JsonProperty selectAnnotation(p, classOf[JsonProperty]) .flatMap { jsonProp => val defaultValue = jsonProp.defaultValue(); // Since it is default set to "", we should only use it if it is nonEmpty if (defaultValue.nonEmpty) { Some(defaultValue) } else None } .orElse { // Then, look for @JsonSchemaDefault selectAnnotation(p, classOf[JsonSchemaDefault]).map { defaultValue => defaultValue.value() } } } override def expectStringFormat(_type: JavaType) = { l(s"expectStringFormat - _type: ${_type}") node.put("type", "string") // Check if we should include minLength and/or maxLength case class MinAndMaxLength(minLength: Option[Int], maxLength: Option[Int]) // If we have 'currentProperty', then check for annotations and insert stuff into schema. currentProperty.flatMap { p => // Look for @NotBlank selectAnnotation(p, classOf[NotBlank]).map { _ => // Need to write this pattern first in case we should override it with more specific @Pattern node.put("pattern", "^.*\\S+.*$") } // Look for @Pattern selectAnnotation(p, classOf[Pattern]).map { pattern => node.put("pattern", pattern.regexp()) } // Look for @Pattern.List selectAnnotation(p, classOf[Pattern.List]).map { patterns => { val regex = patterns.value().map(_.regexp).foldLeft("^")(_ + "(?=" + _ + ")").concat(".*$") node.put("pattern", regex) } } extractDefaultValue(p).map { value => node.put("default", value) } // Look for @JsonSchemaExamples selectAnnotation(p, classOf[JsonSchemaExamples]).map { exampleValues => val examples: ArrayNode = JsonNodeFactory.instance.arrayNode() exampleValues.value().map { exampleValue => examples.add(exampleValue) } node.set("examples", examples) () } // Look for @Email selectAnnotation(p, classOf[Email]).map { _ => node.put("format", "email") } // Look for a @Size annotation, which should have a set of min/max properties. val minAndMaxLength: Option[MinAndMaxLength] = selectAnnotation(p, classOf[Size]) .map { size => (size.min(), size.max()) match { case (0, max) => MinAndMaxLength(None, Some(max)) case (min, Integer.MAX_VALUE) => MinAndMaxLength(Some(min), None) case (min, max) => MinAndMaxLength(Some(min), Some(max)) } } // Look for other annotations that don't have an explicit size, but we can infer the need to set a size for. .orElse { // If we're annotated with @NotNull, check to see if our config requires a size property to be generated. if ( config.useMinLengthForNotNull && (selectAnnotation(p, classOf[NotNull]).isDefined) ) { Option(MinAndMaxLength(Some(1), None)) } // Other javax.validation annotations that require a length. else if ( selectAnnotation(p, classOf[NotBlank]).isDefined || selectAnnotation( p, classOf[NotEmpty] ).isDefined ) { Option(MinAndMaxLength(Some(1), None)) } // No length required. else { None } } // Apply size-data if found minAndMaxLength.map { minAndMax: MinAndMaxLength => minAndMax.minLength.map(length => node.put("minLength", length)) minAndMax.maxLength.map(length => node.put("maxLength", length)) } } new JsonStringFormatVisitor with EnumSupport { val _node = node override def format(format: JsonValueFormat): Unit = { setFormat(node, format.toString) } } } override def expectArrayFormat(_type: JavaType) = { l(s"expectArrayFormat - _type: ${_type}") node.put("type", "array") if (config.uniqueItemClasses.exists(c => _type.getRawClass.isAssignableFrom(c))) { // Adding '"uniqueItems": true' to be used with https://github.com/jdorn/json-editor node.put("uniqueItems", true) setFormat(node, "checkbox") } else { // Try to set default format config.defaultArrayFormat.foreach { format => setFormat(node, format) } } currentProperty.map { p => // Look for @Size selectAnnotation(p, classOf[Size]).map { size => node.put("minItems", size.min()) node.put("maxItems", size.max()) } // Look for @NotEmpty selectAnnotation(p, classOf[NotEmpty]).map { notEmpty => node.put("minItems", 1) } } val itemsNode = JsonNodeFactory.instance.objectNode() node.set("items", itemsNode) // We get improved result while processing scala-collections by getting elementType this way // instead of using the one which we receive in JsonArrayFormatVisitor.itemsFormat // This approach also works for Java val preferredElementType: JavaType = _type.getContentType new JsonArrayFormatVisitor with MySerializerProvider { override def itemsFormat(handler: JsonFormatVisitable, _elementType: JavaType): Unit = { l( s"expectArrayFormat - handler: $handler - elementType: ${_elementType} - preferredElementType: $preferredElementType" ) objectMapper.acceptJsonFormatVisitor( tryToReMapType(preferredElementType), createChild(itemsNode, currentProperty = None) ) } override def itemsFormat(format: JsonFormatTypes): Unit = { l(s"itemsFormat - format: $format") itemsNode.put("type", format.value()) } } } override def expectNumberFormat(_type: JavaType) = { l("expectNumberFormat") node.put("type", "number") // Look for @Min, @Max, @DecimalMin, @DecimalMax => minimum, maximum currentProperty.map { p => selectAnnotation(p, classOf[Min]).map { min => node.put("minimum", min.value()) } selectAnnotation(p, classOf[Max]).map { max => node.put("maximum", max.value()) } selectAnnotation(p, classOf[DecimalMin]).map { decimalMin => node.put("minimum", decimalMin.value().toDouble) } selectAnnotation(p, classOf[DecimalMax]).map { decimalMax => node.put("maximum", decimalMax.value().toDouble) } extractDefaultValue(p).map { value => node.put("default", value.toInt) } // Look for @JsonSchemaExamples Option(p.getAnnotation(classOf[JsonSchemaExamples])).map { exampleValues => val examples: ArrayNode = JsonNodeFactory.instance.arrayNode() exampleValues.value().map { exampleValue => examples.add(exampleValue) } node.set("examples", examples) } } new JsonNumberFormatVisitor with EnumSupport { val _node = node override def numberType(_type: NumberType): Unit = l(s"JsonNumberFormatVisitor.numberType: ${_type}") override def format(format: JsonValueFormat): Unit = { setFormat(node, format.toString) } } } override def expectAnyFormat(_type: JavaType) = { if (!config.disableWarnings) { log.warn( s"Not able to generate jsonSchema-info for type: ${_type} - probably using custom serializer which does not override acceptJsonFormatVisitor" ) } new JsonAnyFormatVisitor {} } override def expectIntegerFormat(_type: JavaType) = { l("expectIntegerFormat") node.put("type", "integer") // Look for @Min, @Max => minimum, maximum currentProperty.map { p => selectAnnotation(p, classOf[Min]).map { min => node.put("minimum", min.value()) } selectAnnotation(p, classOf[Max]).map { max => node.put("maximum", max.value()) } extractDefaultValue(p).map { value => node.put("default", value.toInt) } // Look for @JsonSchemaExamples selectAnnotation(p, classOf[JsonSchemaExamples]).map { exampleValues => val examples: ArrayNode = JsonNodeFactory.instance.arrayNode() exampleValues.value().map { exampleValue => examples.add(exampleValue) } node.set("examples", examples) () } } new JsonIntegerFormatVisitor with EnumSupport { val _node = node override def numberType(_type: NumberType): Unit = l(s"JsonIntegerFormatVisitor.numberType: ${_type}") override def format(format: JsonValueFormat): Unit = { setFormat(node, format.toString) } } } override def expectNullFormat(_type: JavaType) = { l(s"expectNullFormat - _type: ${_type}") node.put("type", "null") new JsonNullFormatVisitor {} } override def expectBooleanFormat(_type: JavaType) = { l("expectBooleanFormat") node.put("type", "boolean") currentProperty.map { p => extractDefaultValue(p).map { value => node.put("default", value.toBoolean) } } new JsonBooleanFormatVisitor with EnumSupport { val _node = node override def format(format: JsonValueFormat): Unit = { setFormat(node, format.toString) } } } override def expectMapFormat(_type: JavaType) = { l(s"expectMapFormat - _type: ${_type}") // There is no way to specify map in jsonSchema, // So we're going to treat it as type=object with additionalProperties = true, // so that it can hold whatever the map can hold node.put("type", "object") val additionalPropsObject = JsonNodeFactory.instance.objectNode() node.set("additionalProperties", additionalPropsObject) // If we're annotated with @NotEmpty, make sure we add a minItems of 1 to our schema here. currentProperty.map { p => Option(p.getAnnotation(classOf[NotEmpty])).map { notEmpty => node.put("minProperties", 1) } } definitionsHandler.pushWorkInProgress() val childVisitor = createChild(additionalPropsObject, None) objectMapper.acceptJsonFormatVisitor(tryToReMapType(_type.getContentType), childVisitor) definitionsHandler.popworkInProgress() new JsonMapFormatVisitor with MySerializerProvider { override def keyFormat(handler: JsonFormatVisitable, keyType: JavaType): Unit = { l(s"JsonMapFormatVisitor.keyFormat handler: $handler - keyType: $keyType") } override def valueFormat(handler: JsonFormatVisitable, valueType: JavaType): Unit = { l(s"JsonMapFormatVisitor.valueFormat handler: $handler - valueType: $valueType") } } } private def getRequiredArrayNode(objectNode: ObjectNode): ArrayNode = { Option(objectNode.get("required")).map(_.asInstanceOf[ArrayNode]).getOrElse { val rn = JsonNodeFactory.instance.arrayNode() objectNode.set("required", rn) rn } } private def getOptionsNode(objectNode: ObjectNode): ObjectNode = { getOrCreateObjectChild(objectNode, "options") } private def getOrCreateObjectChild(parentObjectNode: ObjectNode, name: String): ObjectNode = { Option(parentObjectNode.get(name)).map(_.asInstanceOf[ObjectNode]).getOrElse { val o = JsonNodeFactory.instance.objectNode() parentObjectNode.set(name, o) o } } case class PolymorphismInfo(typePropertyName: String, subTypeName: String) private def extractPolymorphismInfo(_type: JavaType): Option[PolymorphismInfo] = { val maybeBaseType = ClassUtil.findSuperTypes(_type, null, false).asScala.find { cl => cl.getRawClass.isAnnotationPresent(classOf[JsonTypeInfo]) } orElse Option(_type.getSuperClass) maybeBaseType.flatMap { baseType => val serializerOrNull = objectMapper.getSerializerFactory .createTypeSerializer(objectMapper.getSerializationConfig, baseType) Option(serializerOrNull).map { serializer => serializer.getTypeInclusion match { case JsonTypeInfo.As.PROPERTY | JsonTypeInfo.As.EXISTING_PROPERTY => val idResolver = serializer.getTypeIdResolver val id = idResolver match { // use custom implementation instead, because default implementation needs instance and we don't have one case _: MinimalClassNameIdResolver => extractMinimalClassnameId(baseType, _type) case _ => idResolver.idFromValueAndType(null, _type.getRawClass) } PolymorphismInfo(serializer.getPropertyName, id) case x => throw new Exception( s"We do not support polymorphism using jsonTypeInfo.include() = $x" ) } } } } private def extractSubTypes(_type: JavaType): List[Class[_]] = { val ac = AnnotatedClassResolver.resolve( objectMapper.getDeserializationConfig, _type, objectMapper.getDeserializationConfig ) Option(ac.getAnnotation(classOf[JsonTypeInfo])) .map { jsonTypeInfo: JsonTypeInfo => jsonTypeInfo.use() match { case JsonTypeInfo.Id.NAME => // First we try to resolve types via manually finding annotations (if success, it will preserve the order), if not we fallback to use collectAndResolveSubtypesByClass() val subTypes: List[Class[_]] = Option(_type.getRawClass.getDeclaredAnnotation(classOf[JsonSubTypes])) .map { ann: JsonSubTypes => // We found it via @JsonSubTypes-annotation ann .value() .map { t: JsonSubTypes.Type => t.value() } .toList } .getOrElse { // We did not find it via @JsonSubTypes-annotation (Probably since it is using mixin's) => Must fallback to using collectAndResolveSubtypesByClass val resolvedSubTypes = objectMapper.getSubtypeResolver .collectAndResolveSubtypesByClass(objectMapper.getDeserializationConfig, ac) .asScala .toList resolvedSubTypes .map(_.getType) .filter(c => _type.getRawClass.isAssignableFrom(c) && _type.getRawClass != c) } subTypes case _ => // Just find all subclasses config.subclassesResolver.getSubclasses(_type.getRawClass) } } .getOrElse(List()) } def tryToReMapType(originalClass: Class[_]): Class[_] = { config.classTypeReMapping .get(originalClass) .map { mappedToClass: Class[_] => l(s"Class $originalClass is remapped to $mappedToClass") mappedToClass } .getOrElse(originalClass) } private def tryToReMapType(originalType: JavaType): JavaType = { val _type: JavaType = config.classTypeReMapping .get(originalType.getRawClass) .map { mappedToClass: Class[_] => l(s"Class ${originalType.getRawClass} is remapped to $mappedToClass") val mappedToJavaType: JavaType = objectMapper.getTypeFactory.constructType(mappedToClass) mappedToJavaType } .getOrElse(originalType) _type } // Returns the value of merge private def injectFromJsonSchemaInject( a: JsonSchemaInject, thisObjectNode: ObjectNode ): Boolean = { // Must parse json val injectJsonNode = objectMapper.readTree(a.json()) Option(a.jsonSupplier()) .flatMap(cls => Option(cls.getDeclaredConstructor().newInstance().get())) .foreach(json => merge(injectJsonNode, json)) if (a.jsonSupplierViaLookup().nonEmpty) { val json = config.jsonSuppliers .getOrElse( a.jsonSupplierViaLookup(), throw new Exception( s"@JsonSchemaInject(jsonSupplierLookup='${a.jsonSupplierViaLookup()}') does not exist in config.jsonSupplierLookup-map" ) ) .get() merge(injectJsonNode, json) } a.strings().foreach(v => injectJsonNode.visit(v.path(), (o, n) => o.put(n, v.value()))) a.ints().foreach(v => injectJsonNode.visit(v.path(), (o, n) => o.put(n, v.value()))) a.bools().foreach(v => injectJsonNode.visit(v.path(), (o, n) => o.put(n, v.value()))) val mergeInjectedJson: Boolean = a.merge() if (!mergeInjectedJson) { // Since we're not merging, we must remove all content of thisObjectNode before injecting. // We cannot just "replace" it with injectJsonNode, since thisObjectNode already have been added to its parent thisObjectNode.removeAll() } merge(thisObjectNode, injectJsonNode) // return mergeInjectedJson } override def expectObjectFormat(_type: JavaType) = { val subTypes: List[Class[_]] = extractSubTypes(_type) // Check if we have subtypes if (subTypes.nonEmpty) { // We have subtypes //l(s"polymorphism - subTypes: $subTypes") val anyOfArrayNode = JsonNodeFactory.instance.arrayNode() node.set("oneOf", anyOfArrayNode) subTypes.foreach { subType: Class[_] => l(s"polymorphism - subType: $subType") val definitionInfo: DefinitionInfo = definitionsHandler.getOrCreateDefinition(objectMapper.constructType(subType)) { objectNode => val childVisitor = createChild(objectNode, currentProperty = None) objectMapper.acceptJsonFormatVisitor(tryToReMapType(subType), childVisitor) None } val thisOneOfNode = JsonNodeFactory.instance.objectNode() thisOneOfNode.put("$ref", definitionInfo.ref.get) // If class is annotated with JsonSchemaTitle, we should add it Option(subType.getDeclaredAnnotation(classOf[JsonSchemaTitle])).map(_.value()).foreach { title => thisOneOfNode.put("title", title) } anyOfArrayNode.add(thisOneOfNode) } null // Returning null to stop jackson from visiting this object since we have done it manually } else { // We do not have subtypes val objectBuilder: ObjectNode => Option[JsonObjectFormatVisitor] = { thisObjectNode: ObjectNode => thisObjectNode.put("type", "object") thisObjectNode.put("additionalProperties", !config.failOnUnknownProperties) // If class is annotated with JsonSchemaFormat, we should add it val ac = AnnotatedClassResolver.resolve( objectMapper.getDeserializationConfig, _type, objectMapper.getDeserializationConfig ) resolvePropertyFormat(_type, objectMapper).foreach { format => setFormat(thisObjectNode, format) } // If class is annotated with JsonSchemaDescription, we should add it Option(ac.getAnnotations.get(classOf[JsonSchemaDescription])) .map(_.value()) .orElse(Option(ac.getAnnotations.get(classOf[JsonPropertyDescription])).map(_.value)) .foreach { description: String => thisObjectNode.put("description", description) } // If class is annotated with JsonSchemaTitle, we should add it Option(ac.getAnnotations.get(classOf[JsonSchemaTitle])).map(_.value()).foreach { title => thisObjectNode.put("title", title) } // If class is annotated with JsonSchemaOptions, we should add it Option(ac.getAnnotations.get(classOf[JsonSchemaOptions])).map(_.items()).foreach { items => val optionsNode = getOptionsNode(thisObjectNode) items.foreach { item => optionsNode.put(item.name, item.value) } } // Optionally add JsonSchemaInject to top-level val renderProps: Boolean = selectAnnotation(ac, classOf[JsonSchemaInject]) .map { a => val merged = injectFromJsonSchemaInject(a, thisObjectNode) merged == true // Continue to render props since we merged injection } .getOrElse(true) // nothing injected => of course we should render props if (renderProps) { val propertiesNode = getOrCreateObjectChild(thisObjectNode, "properties") extractPolymorphismInfo(_type).map { case pi: PolymorphismInfo => // This class is a child in a polymorphism config.. // Set the title = subTypeName thisObjectNode.put("title", pi.subTypeName) // must inject the 'type'-param and value as enum with only one possible value // This is done to make sure the json generated from the schema using this oneOf // contains the correct "type info" val enumValuesNode = JsonNodeFactory.instance.arrayNode() enumValuesNode.add(pi.subTypeName) val enumObjectNode = getOrCreateObjectChild(propertiesNode, pi.typePropertyName) enumObjectNode.put("type", "string") enumObjectNode.set("enum", enumValuesNode) enumObjectNode.put("default", pi.subTypeName) if (config.hidePolymorphismTypeProperty) { // Make sure the editor hides this polymorphism-specific property val optionsNode = JsonNodeFactory.instance.objectNode() enumObjectNode.set("options", optionsNode) optionsNode.put("hidden", true) } getRequiredArrayNode(thisObjectNode).add(pi.typePropertyName) if (config.useMultipleEditorSelectViaProperty) { // https://github.com/jdorn/json-editor/issues/709 // Generate info to help generated editor to select correct oneOf-type // when populating the gui/schema with existing data val objectOptionsNode = getOrCreateObjectChild(thisObjectNode, "options") val multipleEditorSelectViaPropertyNode = getOrCreateObjectChild( objectOptionsNode, "multiple_editor_select_via_property" ) multipleEditorSelectViaPropertyNode.put("property", pi.typePropertyName) multipleEditorSelectViaPropertyNode.put("value", pi.subTypeName) () } } Some(new JsonObjectFormatVisitor with MySerializerProvider { // Used when rendering schema using propertyOrdering as specified here: // https://github.com/jdorn/json-editor#property-ordering var nextPropertyOrderIndex = 1 def myPropertyHandler( propertyName: String, propertyType: JavaType, prop: Option[BeanProperty], jsonPropertyRequired: Boolean ): Unit = { l(s"JsonObjectFormatVisitor - ${propertyName}: ${propertyType}") if (propertiesNode.get(propertyName) != null) { if (!config.disableWarnings) { log.warn( s"Ignoring property '$propertyName' in $propertyType since it has already been added, probably as type-property using polymorphism" ) } return } // Need to check for Option/Optional-special-case before we know what node to use here. case class PropertyNode(main: ObjectNode, meta: ObjectNode) // Check if we should set this property as required. Primitive types MUST have a value, as does anything // with a @JsonProperty that has "required" set to true. Lastly, various javax.validation annotations also // make this required. val requiredProperty: Boolean = if ( propertyType.getRawClass.isPrimitive || jsonPropertyRequired || validationAnnotationRequired( prop ) ) { true } else { false } val thisPropertyNode: PropertyNode = { val thisPropertyNode = JsonNodeFactory.instance.objectNode() propertiesNode.set(propertyName, thisPropertyNode) if (config.usePropertyOrdering) { thisPropertyNode.put("propertyOrder", nextPropertyOrderIndex) nextPropertyOrderIndex = nextPropertyOrderIndex + 1 } // Figure out if the type is considered optional by either Java or Scala. val optionalType: Boolean = classOf[Option[_]].isAssignableFrom(propertyType.getRawClass) || classOf[Optional[_]].isAssignableFrom(propertyType.getRawClass) // If the property is not required, and our configuration allows it, let's go ahead and mark the type as nullable. if ( !requiredProperty && ((config.useOneOfForOption && optionalType) || (config.useOneOfForNullables && !optionalType)) ) { // We support this type being null, insert a oneOf consisting of a sentinel "null" and the real type. val oneOfArray = JsonNodeFactory.instance.arrayNode() thisPropertyNode.set("oneOf", oneOfArray) // Create our sentinel "null" value for the case no value is provided. val oneOfNull = JsonNodeFactory.instance.objectNode() oneOfNull.put("type", "null") oneOfNull.put("title", "Not included") oneOfArray.add(oneOfNull) // If our nullable/optional type has a value, it'll be this. val oneOfReal = JsonNodeFactory.instance.objectNode() oneOfArray.add(oneOfReal) // Return oneOfReal which, from now on, will be used as the node representing this property PropertyNode(oneOfReal, thisPropertyNode) } else if ( !requiredProperty && ((config.useNullableForOption && optionalType) || (config.useNullableForNullables && !optionalType)) ) { // add {nullable: true} in the json schema following OpenAPI and AJV specification // see https://ajv.js.org/json-schema.html#openapi-support thisPropertyNode.put("nullable", true) PropertyNode(thisPropertyNode, thisPropertyNode) } else { // Our type must not be null: primitives, @NotNull annotations, @JsonProperty annotations marked required etc. PropertyNode(thisPropertyNode, thisPropertyNode) } } // Continue processing this property val childVisitor = createChild(thisPropertyNode.main, currentProperty = prop) // Push current work in progress since we're about to start working on a new class definitionsHandler.pushWorkInProgress() if ( (classOf[Option[_]] .isAssignableFrom(propertyType.getRawClass) || classOf[Optional[_]] .isAssignableFrom(propertyType.getRawClass)) && propertyType .containedTypeCount() >= 1 ) { // Property is scala Option or Java Optional. // // Due to Java's Type Erasure, the type behind Option is lost. // To workaround this, we use the same workaround as jackson-scala-module described here: // https://github.com/FasterXML/jackson-module-scala/wiki/FAQ#deserializing-optionint-and-other-primitive-challenges val optionType: JavaType = resolveType(propertyType, prop, objectMapper) objectMapper.acceptJsonFormatVisitor(tryToReMapType(optionType), childVisitor) } else { objectMapper.acceptJsonFormatVisitor(tryToReMapType(propertyType), childVisitor) } // Pop back the work we were working on.. definitionsHandler.popworkInProgress() prop.flatMap(resolvePropertyFormat(_)).foreach { format => setFormat(thisPropertyNode.main, format) } // Optionally add description prop .flatMap { p: BeanProperty => Option(p.getAnnotation(classOf[JsonSchemaDescription])) .map(_.value()) .orElse( Option(p.getAnnotation(classOf[JsonPropertyDescription])).map(_.value()) ) } .map { description => thisPropertyNode.meta.put("description", description) } // If this property is required, add it to our array of required properties. if (requiredProperty) { getRequiredArrayNode(thisObjectNode).add(propertyName) } // Optionally add title prop .flatMap { p: BeanProperty => Option(p.getAnnotation(classOf[JsonSchemaTitle])) } .map(_.value()) .orElse { if (config.autoGenerateTitleForProperties) { // We should generate 'pretty-name' based on propertyName Some(generateTitleFromPropertyName(propertyName)) } else None } .map { title => thisPropertyNode.meta.put("title", title) } // Optionally add options prop .flatMap { p: BeanProperty => Option(p.getAnnotation(classOf[JsonSchemaOptions])) } .map(_.items()) .foreach { items => val optionsNode = getOptionsNode(thisPropertyNode.meta) items.foreach { item => optionsNode.put(item.name, item.value) } } // Optionally add JsonSchemaInject prop .flatMap { p: BeanProperty => selectAnnotation(p, classOf[JsonSchemaInject]) match { case Some(a) => Some(a) case None => // Try to look at the class itself -- Looks like this is the only way to find it if the type is Enum Option(p.getType.getRawClass.getAnnotation(classOf[JsonSchemaInject])) .filter(annotationIsApplicable(_)) } } .foreach { a => injectFromJsonSchemaInject(a, thisPropertyNode.meta) } } override def optionalProperty(prop: BeanProperty): Unit = { l(s"JsonObjectFormatVisitor.optionalProperty: prop:${prop}") myPropertyHandler( prop.getName, prop.getType, Some(prop), jsonPropertyRequired = false ) } override def optionalProperty( name: String, handler: JsonFormatVisitable, propertyTypeHint: JavaType ): Unit = { l( s"JsonObjectFormatVisitor.optionalProperty: name:${name} handler:${handler} propertyTypeHint:${propertyTypeHint}" ) myPropertyHandler(name, propertyTypeHint, None, jsonPropertyRequired = false) } override def property(prop: BeanProperty): Unit = { l(s"JsonObjectFormatVisitor.property: prop:${prop}") myPropertyHandler( prop.getName, prop.getType, Some(prop), jsonPropertyRequired = true ) } override def property( name: String, handler: JsonFormatVisitable, propertyTypeHint: JavaType ): Unit = { l( s"JsonObjectFormatVisitor.property: name:${name} handler:${handler} propertyTypeHint:${propertyTypeHint}" ) myPropertyHandler(name, propertyTypeHint, None, jsonPropertyRequired = true) } // Checks to see if a javax.validation field that makes our field required is present. private def validationAnnotationRequired(prop: Option[BeanProperty]): Boolean = { prop.exists(p => selectAnnotation(p, classOf[NotNull]).isDefined || selectAnnotation( p, classOf[NotBlank] ).isDefined || selectAnnotation(p, classOf[NotEmpty]).isDefined ) } }) } else None } if (level == 0) { // This is the first level - we must not use definitions objectBuilder(node).orNull } else { val definitionInfo: DefinitionInfo = definitionsHandler.getOrCreateDefinition(_type)(objectBuilder) definitionInfo.ref.foreach { r => // Must add ref to def at "this location" node.put("$ref", r) } definitionInfo.jsonObjectFormatVisitor.orNull } } } } private def extractMinimalClassnameId(baseType: JavaType, child: JavaType) = { // code taken straight from Jackson's MinimalClassNameIdResolver val base = baseType.getRawClass.getName val ix = base.lastIndexOf('.') val _basePackagePrefix = if (ix < 0) { // can this ever occur? "." } else { base.substring(0, ix + 1) } val n = child.getRawClass.getName if (n.startsWith(_basePackagePrefix)) { // note: we will leave the leading dot in there n.substring(_basePackagePrefix.length - 1) } else { n } } private def merge(mainNode: JsonNode, updateNode: JsonNode): Unit = { val fieldNames = updateNode.fieldNames() while (fieldNames.hasNext) { val fieldName = fieldNames.next() val jsonNode = mainNode.get(fieldName) // if field exists and is an embedded object if (jsonNode != null && jsonNode.isObject) { merge(jsonNode, updateNode.get(fieldName)) } else { mainNode match { case node: ObjectNode => // Overwrite field val value = updateNode.get(fieldName) node.set(fieldName, value) () case _ => } } } } def generateTitleFromPropertyName(propertyName: String): String = { if (propertyName.isEmpty) return propertyName // Insert spaces at camelCase/PascalCase boundaries and letter-to-non-letter transitions. val builder = new StringBuilder for (i <- propertyName.indices) { val c = propertyName(i) if (i > 0) { val prev = propertyName(i - 1) val isCurrentUpper = c.isUpper val isPrevUpper = prev.isUpper val isPrevLetter = prev.isLetter val isCurrentLetter = c.isLetter val nextIsLower = i + 1 < propertyName.length && propertyName(i + 1).isLower // Space before uppercase that follows a non-uppercase char (e.g., "camelCase" or "123Value") // Space before uppercase in an acronym run when next char is lowercase (e.g., "XMLParser") // Space before a non-letter that follows a letter (e.g., "test123") if ( (isCurrentUpper && !isPrevUpper) || (isCurrentUpper && isPrevUpper && nextIsLower) || (!isCurrentLetter && isPrevLetter) ) { builder.append(' ') } } builder.append(c) } val s = builder.toString() // Make the first letter uppercase s.substring(0, 1).toUpperCase() + s.substring(1) } def resolvePropertyFormat(_type: JavaType, objectMapper: ObjectMapper): Option[String] = { val ac = AnnotatedClassResolver.resolve( objectMapper.getDeserializationConfig, _type, objectMapper.getDeserializationConfig ) resolvePropertyFormat( Option(ac.getAnnotation(classOf[JsonSchemaFormat])), _type.getRawClass.getName ) } def resolvePropertyFormat(prop: BeanProperty): Option[String] = { // Prefer format specified in annotation resolvePropertyFormat( Option(prop.getAnnotation(classOf[JsonSchemaFormat])), prop.getType.getRawClass.getName ) } def resolvePropertyFormat( jsonSchemaFormatAnnotation: Option[JsonSchemaFormat], rawClassName: String ): Option[String] = { // Prefer format specified in annotation jsonSchemaFormatAnnotation .map { jsonSchemaFormat => jsonSchemaFormat.value() } .orElse { config.customType2FormatMapping.get(rawClassName) } } def resolveType( propertyType: JavaType, prop: Option[BeanProperty], objectMapper: ObjectMapper ): JavaType = { val containedType = propertyType.containedType(0) if (containedType.getRawClass == classOf[Object]) { // try to resolve it via @JsonDeserialize as described here: https://github.com/FasterXML/jackson-module-scala/wiki/FAQ#deserializing-optionint-and-other-primitive-challenges prop .flatMap { p: BeanProperty => Option(p.getAnnotation(classOf[JsonDeserialize])) } .flatMap { jsonDeserialize: JsonDeserialize => Option(jsonDeserialize.contentAs()).map { clazz => objectMapper.getTypeFactory.constructType(clazz) } } .getOrElse({ if (!config.disableWarnings) { log.warn( s"$prop - Contained type is java.lang.Object and we're unable to extract its Type using fallback-approach looking for @JsonDeserialize" ) } containedType }) } else { // use containedType as is containedType } } def generateJsonSchema[T <: Any](clazz: Class[T]): JsonNode = generateJsonSchema(clazz, None, None) def generateJsonSchema[T <: Any](javaType: JavaType): JsonNode = generateJsonSchema(javaType, None, None) // Java-API def generateJsonSchema[T <: Any](clazz: Class[T], title: String, description: String): JsonNode = generateJsonSchema(clazz, Option(title), Option(description)) // Java-API def generateJsonSchema[T <: Any]( javaType: JavaType, title: String, description: String ): JsonNode = generateJsonSchema(javaType, Option(title), Option(description)) def generateJsonSchema[T <: Any]( clazz: Class[T], title: Option[String], description: Option[String] ): JsonNode = { def tryToReMapType(originalClass: Class[_]): Class[_] = { config.classTypeReMapping .get(originalClass) .map { mappedToClass: Class[_] => if (debug) { println(s"Class $originalClass is remapped to $mappedToClass") } mappedToClass } .getOrElse(originalClass) } val clazzToUse = tryToReMapType(clazz) val javaType = rootObjectMapper.constructType(clazzToUse) generateJsonSchema(javaType, title, description) } def generateJsonSchema[T <: Any]( javaType: JavaType, title: Option[String], description: Option[String] ): JsonNode = { val rootNode = JsonNodeFactory.instance.objectNode() // Specify that this is a v4 json schema rootNode.put("$schema", config.jsonSchemaDraft.url) //rootNode.put("id", "http://my.site/myschema#") // Add schema title title .orElse { Some(generateTitleFromPropertyName(javaType.getRawClass.getSimpleName)) } .flatMap { title => // Skip it if specified to empty string if (title.isEmpty) None else Some(title) } .map { title => rootNode.put("title", title) // If root class is annotated with @JsonSchemaTitle, it will later override this title } // Maybe set schema description description.map { d => rootNode.put("description", d) // If root class is annotated with @JsonSchemaDescription, it will later override this description } val definitionsHandler = new DefinitionsHandler val rootVisitor = new MyJsonFormatVisitorWrapper( rootObjectMapper, node = rootNode, definitionsHandler = definitionsHandler, currentProperty = None ) rootObjectMapper.acceptJsonFormatVisitor(javaType, rootVisitor) definitionsHandler.getFinalDefinitionsNode().foreach { definitionsNode => rootNode.set("definitions", definitionsNode) () } rootNode } implicit class JsonNodeExtension(o: JsonNode) { def visit(path: String, f: (ObjectNode, String) => Unit) = { var p = o val split = path.split('/') for (name <- split.dropRight(1)) { p = Option(p.get(name)).getOrElse(p.asInstanceOf[ObjectNode].putObject(name)) } f(p.asInstanceOf[ObjectNode], split.last) } } } ================================================ FILE: common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaArrayWithUniqueItems.java ================================================ /* * Copyright (c) 2016 Kjell Tore Eliassen (mbknor) * Licensed under the MIT License. * * This file is derived from mbknor-jackson-jsonschema. * Source: https://github.com/mbknor/mbknor-jackson-jsonschema */ package com.kjetland.jackson.jsonSchema.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({METHOD, FIELD, PARAMETER, TYPE}) @Retention(RUNTIME) public @interface JsonSchemaArrayWithUniqueItems { String value(); } ================================================ FILE: common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaBool.java ================================================ /* * Copyright (c) 2016 Kjell Tore Eliassen (mbknor) * Licensed under the MIT License. * * This file is derived from mbknor-jackson-jsonschema. * Source: https://github.com/mbknor/mbknor-jackson-jsonschema */ package com.kjetland.jackson.jsonSchema.annotations; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; /** * Injects custom values to the schema generated for fields or getters. * * @author bbyk */ @Target({METHOD, FIELD, TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface JsonSchemaBool { /** * @return a slash separated path to the value in the schema */ String path(); /** * @return a boolean value to place in the schema */ boolean value(); } ================================================ FILE: common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDefault.java ================================================ /* * Copyright (c) 2016 Kjell Tore Eliassen (mbknor) * Licensed under the MIT License. * * This file is derived from mbknor-jackson-jsonschema. * Source: https://github.com/mbknor/mbknor-jackson-jsonschema */ package com.kjetland.jackson.jsonSchema.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({METHOD, FIELD, PARAMETER, TYPE}) @Retention(RUNTIME) public @interface JsonSchemaDefault { String value(); } ================================================ FILE: common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDescription.java ================================================ /* * Copyright (c) 2016 Kjell Tore Eliassen (mbknor) * Licensed under the MIT License. * * This file is derived from mbknor-jackson-jsonschema. * Source: https://github.com/mbknor/mbknor-jackson-jsonschema */ package com.kjetland.jackson.jsonSchema.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * Same as com.fasterxml.jackson.annotation.JsonPropertyDescription */ @Target({METHOD, FIELD, PARAMETER, TYPE}) @Retention(RUNTIME) public @interface JsonSchemaDescription { String value(); } ================================================ FILE: common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaExamples.java ================================================ /* * Copyright (c) 2016 Kjell Tore Eliassen (mbknor) * Licensed under the MIT License. * * This file is derived from mbknor-jackson-jsonschema. * Source: https://github.com/mbknor/mbknor-jackson-jsonschema */ package com.kjetland.jackson.jsonSchema.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({METHOD, FIELD, PARAMETER, TYPE}) @Retention(RUNTIME) public @interface JsonSchemaExamples { String[] value(); } ================================================ FILE: common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaFormat.java ================================================ /* * Copyright (c) 2016 Kjell Tore Eliassen (mbknor) * Licensed under the MIT License. * * This file is derived from mbknor-jackson-jsonschema. * Source: https://github.com/mbknor/mbknor-jackson-jsonschema */ package com.kjetland.jackson.jsonSchema.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({METHOD, FIELD, PARAMETER, TYPE}) @Retention(RUNTIME) public @interface JsonSchemaFormat { String value(); } ================================================ FILE: common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInject.java ================================================ /* * Copyright (c) 2016 Kjell Tore Eliassen (mbknor) * Licensed under the MIT License. * * This file is derived from mbknor-jackson-jsonschema. * Source: https://github.com/mbknor/mbknor-jackson-jsonschema */ package com.kjetland.jackson.jsonSchema.annotations; import com.fasterxml.jackson.databind.JsonNode; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.util.function.Supplier; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * Use this annotation to inject json into the generated jsonSchema. */ @Target({METHOD, FIELD, PARAMETER, TYPE}) @Retention(RUNTIME) public @interface JsonSchemaInject { /** * @return a raw json that will be merged on top of the generated jsonSchema */ String json() default "{}"; /** * @return a class for supplier of a raw json. The json gets applied after {@link JsonSchemaInject#json()}. */ Class> jsonSupplier() default None.class; /** * @return a key to lookup a jsonSupplier via lookupMap defined in JsonSchemaConfig */ String jsonSupplierViaLookup() default ""; /** * @return a collection of key/value pairs to merge on top of the generated jsonSchema and applied after {@link JsonSchemaInject#jsonSupplier()} */ JsonSchemaString[] strings() default {}; /** * @return a collection of key/value pairs to merge on top of the generated jsonSchema and applied after {@link #jsonSupplier() */ JsonSchemaInt[] ints() default {}; /** * @return a collection of key/value pairs to merge on top of the generated jsonSchema and applied after {@link #jsonSupplier() */ JsonSchemaBool[] bools() default {}; /** * If merge is true (the default), the injected json will be injected into the generated jsonSchema-node. If merge = false, then * we skips the generated jsonSchema-node and use the entire injected one instead. * * @return whether we should merge or replaceWith the injected json */ boolean merge() default true; // This can be used in the same way as 'groups' in javax.validation.constraints, e.g @NotNull Class[] javaxValidationGroups() default {}; class None implements Supplier { @Override public JsonNode get() { return null; } } } ================================================ FILE: common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInt.java ================================================ /* * Copyright (c) 2016 Kjell Tore Eliassen (mbknor) * Licensed under the MIT License. * * This file is derived from mbknor-jackson-jsonschema. * Source: https://github.com/mbknor/mbknor-jackson-jsonschema */ package com.kjetland.jackson.jsonSchema.annotations; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; /** * Injects custom values to the schema generated for fields or getters. * * @author bbyk */ @Target({METHOD, FIELD, TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface JsonSchemaInt { /** * @return a slash separated path to the value in the schema */ String path(); /** * @return an int value to place in the schema */ int value(); } ================================================ FILE: common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaOptions.java ================================================ /* * Copyright (c) 2016 Kjell Tore Eliassen (mbknor) * Licensed under the MIT License. * * This file is derived from mbknor-jackson-jsonschema. * Source: https://github.com/mbknor/mbknor-jackson-jsonschema */ package com.kjetland.jackson.jsonSchema.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({METHOD, FIELD, PARAMETER, TYPE}) @Retention(RUNTIME) public @interface JsonSchemaOptions { public Item[] items(); public @interface Item { public String name(); public String value(); } } ================================================ FILE: common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaString.java ================================================ /* * Copyright (c) 2016 Kjell Tore Eliassen (mbknor) * Licensed under the MIT License. * * This file is derived from mbknor-jackson-jsonschema. * Source: https://github.com/mbknor/mbknor-jackson-jsonschema */ package com.kjetland.jackson.jsonSchema.annotations; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; /** * Injects custom values to the schema generated for fields or getters. * * @author bbyk */ @Target({METHOD, FIELD, TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface JsonSchemaString { /** * @return a slash separated path to the value in the schema */ String path(); /** * @return a string value to place in the schema */ String value(); } ================================================ FILE: common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaTitle.java ================================================ /* * Copyright (c) 2016 Kjell Tore Eliassen (mbknor) * Licensed under the MIT License. * * This file is derived from mbknor-jackson-jsonschema. * Source: https://github.com/mbknor/mbknor-jackson-jsonschema */ package com.kjetland.jackson.jsonSchema.annotations; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({METHOD, FIELD, PARAMETER, TYPE}) @Retention(RUNTIME) public @interface JsonSchemaTitle { String value(); } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/DummyProperties.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator import com.fasterxml.jackson.annotation.JsonProperty import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle class DummyProperties { @JsonProperty @JsonSchemaTitle("Dummy Property") var dummyProperty: String = "" @JsonProperty @JsonSchemaTitle("Dummy Value") var dummyValue: String = "" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/LogicalOp.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator import com.fasterxml.jackson.annotation.JsonSubTypes.Type import com.fasterxml.jackson.annotation._ import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.tuple.Schema import org.apache.texera.amber.core.virtualidentity.{ ExecutionIdentity, OperatorIdentity, WorkflowIdentity } import org.apache.texera.amber.core.workflow.WorkflowContext.{ DEFAULT_EXECUTION_ID, DEFAULT_WORKFLOW_ID } import org.apache.texera.amber.core.workflow.{PhysicalOp, PhysicalPlan, PortIdentity} import org.apache.texera.amber.operator.aggregate.AggregateOpDesc import org.apache.texera.amber.operator.cartesianProduct.CartesianProductOpDesc import org.apache.texera.amber.operator.dictionary.DictionaryMatcherOpDesc import org.apache.texera.amber.operator.difference.DifferenceOpDesc import org.apache.texera.amber.operator.distinct.DistinctOpDesc import org.apache.texera.amber.operator.dummy.DummyOpDesc import org.apache.texera.amber.operator.filter.SpecializedFilterOpDesc import org.apache.texera.amber.operator.hashJoin.HashJoinOpDesc import org.apache.texera.amber.operator.huggingFace.{ HuggingFaceIrisLogisticRegressionOpDesc, HuggingFaceSentimentAnalysisOpDesc, HuggingFaceSpamSMSDetectionOpDesc, HuggingFaceTextSummarizationOpDesc } import org.apache.texera.amber.operator.ifStatement.IfOpDesc import org.apache.texera.amber.operator.intersect.IntersectOpDesc import org.apache.texera.amber.operator.intervalJoin.IntervalJoinOpDesc import org.apache.texera.amber.operator.keywordSearch.KeywordSearchOpDesc import org.apache.texera.amber.operator.limit.LimitOpDesc import org.apache.texera.amber.operator.machineLearning.Scorer.MachineLearningScorerOpDesc import org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.KNNTrainer.{ SklearnAdvancedKNNClassifierTrainerOpDesc, SklearnAdvancedKNNRegressorTrainerOpDesc } import org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.SVCTrainer.SklearnAdvancedSVCTrainerOpDesc import org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.SVRTrainer.SklearnAdvancedSVRTrainerOpDesc import org.apache.texera.amber.operator.metadata.{OPVersion, OperatorInfo, PropertyNameConstants} import org.apache.texera.amber.operator.projection.ProjectionOpDesc import org.apache.texera.amber.operator.randomksampling.RandomKSamplingOpDesc import org.apache.texera.amber.operator.regex.RegexOpDesc import org.apache.texera.amber.operator.reservoirsampling.ReservoirSamplingOpDesc import org.apache.texera.amber.operator.sklearn._ import org.apache.texera.amber.operator.sklearn.training._ import org.apache.texera.amber.operator.sleep.SleepOpDesc import org.apache.texera.amber.operator.sort.{SortOpDesc, StableMergeSortOpDesc} import org.apache.texera.amber.operator.sortPartitions.SortPartitionsOpDesc import org.apache.texera.amber.operator.source.apis.reddit.RedditSearchSourceOpDesc import org.apache.texera.amber.operator.source.apis.twitter.v2.{ TwitterFullArchiveSearchSourceOpDesc, TwitterSearchSourceOpDesc } import org.apache.texera.amber.operator.source.dataset.FileListerSourceOpDesc import org.apache.texera.amber.operator.source.fetcher.URLFetcherOpDesc import org.apache.texera.amber.operator.source.scan.arrow.ArrowSourceOpDesc import org.apache.texera.amber.operator.source.scan.csv.CSVScanSourceOpDesc import org.apache.texera.amber.operator.source.scan.csvOld.CSVOldScanSourceOpDesc import org.apache.texera.amber.operator.source.scan.json.JSONLScanSourceOpDesc import org.apache.texera.amber.operator.source.scan.text.TextInputSourceOpDesc import org.apache.texera.amber.operator.source.sql.asterixdb.AsterixDBSourceOpDesc import org.apache.texera.amber.operator.source.sql.mysql.MySQLSourceOpDesc import org.apache.texera.amber.operator.source.sql.postgresql.PostgreSQLSourceOpDesc import org.apache.texera.amber.operator.split.SplitOpDesc import org.apache.texera.amber.operator.substringSearch.SubstringSearchOpDesc import org.apache.texera.amber.operator.symmetricDifference.SymmetricDifferenceOpDesc import org.apache.texera.amber.operator.typecasting.TypeCastingOpDesc import org.apache.texera.amber.operator.udf.java.JavaUDFOpDesc import org.apache.texera.amber.operator.udf.python._ import org.apache.texera.amber.operator.udf.python.source.PythonUDFSourceOpDescV2 import org.apache.texera.amber.operator.udf.r.{RUDFOpDesc, RUDFSourceOpDesc} import org.apache.texera.amber.operator.union.UnionOpDesc import org.apache.texera.amber.operator.unneststring.UnnestStringOpDesc import org.apache.texera.amber.operator.visualization.DotPlot.DotPlotOpDesc import org.apache.texera.amber.operator.visualization.IcicleChart.IcicleChartOpDesc import org.apache.texera.amber.operator.visualization.ImageViz.ImageVisualizerOpDesc import org.apache.texera.amber.operator.visualization.ScatterMatrixChart.ScatterMatrixChartOpDesc import org.apache.texera.amber.operator.visualization.barChart.BarChartOpDesc import org.apache.texera.amber.operator.visualization.boxViolinPlot.BoxViolinPlotOpDesc import org.apache.texera.amber.operator.visualization.bubbleChart.BubbleChartOpDesc import org.apache.texera.amber.operator.visualization.bulletChart.BulletChartOpDesc import org.apache.texera.amber.operator.visualization.candlestickChart.CandlestickChartOpDesc import org.apache.texera.amber.operator.visualization.choroplethMap.ChoroplethMapOpDesc import org.apache.texera.amber.operator.visualization.continuousErrorBands.ContinuousErrorBandsOpDesc import org.apache.texera.amber.operator.visualization.contourPlot.ContourPlotOpDesc import org.apache.texera.amber.operator.visualization.dendrogram.DendrogramOpDesc import org.apache.texera.amber.operator.visualization.dumbbellPlot.DumbbellPlotOpDesc import org.apache.texera.amber.operator.visualization.ecdfPlot.ECDFPlotOpDesc import org.apache.texera.amber.operator.visualization.figureFactoryTable.FigureFactoryTableOpDesc import org.apache.texera.amber.operator.visualization.filledAreaPlot.FilledAreaPlotOpDesc import org.apache.texera.amber.operator.visualization.funnelPlot.FunnelPlotOpDesc import org.apache.texera.amber.operator.visualization.ganttChart.GanttChartOpDesc import org.apache.texera.amber.operator.visualization.gaugeChart.GaugeChartOpDesc import org.apache.texera.amber.operator.visualization.heatMap.HeatMapOpDesc import org.apache.texera.amber.operator.visualization.hierarchychart.HierarchyChartOpDesc import org.apache.texera.amber.operator.visualization.histogram.HistogramChartOpDesc import org.apache.texera.amber.operator.visualization.histogram2d.Histogram2DOpDesc import org.apache.texera.amber.operator.visualization.htmlviz.HtmlVizOpDesc import org.apache.texera.amber.operator.visualization.lineChart.LineChartOpDesc import org.apache.texera.amber.operator.visualization.nestedTable.NestedTableOpDesc import org.apache.texera.amber.operator.visualization.networkGraph.NetworkGraphOpDesc import org.apache.texera.amber.operator.visualization.pieChart.PieChartOpDesc import org.apache.texera.amber.operator.visualization.quiverPlot.QuiverPlotOpDesc import org.apache.texera.amber.operator.visualization.radarPlot.RadarPlotOpDesc import org.apache.texera.amber.operator.visualization.radarChart.RadarChartOpDesc import org.apache.texera.amber.operator.visualization.rangeSlider.RangeSliderOpDesc import org.apache.texera.amber.operator.visualization.sankeyDiagram.SankeyDiagramOpDesc import org.apache.texera.amber.operator.visualization.scatter3DChart.Scatter3dChartOpDesc import org.apache.texera.amber.operator.visualization.scatterplot.ScatterplotOpDesc import org.apache.texera.amber.operator.visualization.tablesChart.TablesPlotOpDesc import org.apache.texera.amber.operator.visualization.ternaryContour.TernaryContourOpDesc import org.apache.texera.amber.operator.visualization.ternaryPlot.TernaryPlotOpDesc import org.apache.texera.amber.operator.visualization.parallelCoordinatesPlot.ParallelCoordinatesPlotOpDesc import org.apache.texera.amber.operator.visualization.polarChart.PolarChartOpDesc import org.apache.texera.amber.operator.visualization.timeSeriesplot.TimeSeriesOpDesc import org.apache.texera.amber.operator.visualization.treeplot.TreePlotOpDesc import org.apache.texera.amber.operator.visualization.urlviz.UrlVizOpDesc import org.apache.texera.amber.operator.visualization.volcanoPlot.VolcanoPlotOpDesc import org.apache.texera.amber.operator.visualization.waterfallChart.WaterfallChartOpDesc import org.apache.texera.amber.operator.visualization.windRoseChart.WindRoseChartOpDesc import org.apache.texera.amber.operator.visualization.wordCloud.WordCloudOpDesc import org.apache.commons.lang3.builder.{EqualsBuilder, HashCodeBuilder, ToStringBuilder} import org.apache.texera.amber.operator.sklearn.testing.SklearnTestingOpDesc import org.apache.texera.amber.operator.source.scan.file.{FileScanOpDesc, FileScanSourceOpDesc} import org.apache.texera.amber.operator.visualization.stripChart.StripChartOpDesc import org.apache.texera.amber.operator.visualization.carpetPlot.CarpetPlotOpDesc import java.util.UUID import scala.util.Try trait StateTransferFunc extends ((OperatorExecutor, OperatorExecutor) => Unit) with java.io.Serializable @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "operatorType" ) @JsonSubTypes( Array( new Type(value = classOf[IfOpDesc], name = "If"), new Type(value = classOf[SankeyDiagramOpDesc], name = "SankeyDiagram"), new Type(value = classOf[IcicleChartOpDesc], name = "IcicleChart"), new Type(value = classOf[FileListerSourceOpDesc], name = "FileLister"), new Type(value = classOf[CSVScanSourceOpDesc], name = "CSVFileScan"), // disabled the ParallelCSVScanSourceOpDesc so that it does not confuse user. it can be re-enabled when doing experiments. // new Type(value = classOf[ParallelCSVScanSourceOpDesc], name = "ParallelCSVFileScan"), new Type(value = classOf[JSONLScanSourceOpDesc], name = "JSONLFileScan"), new Type(value = classOf[FileScanSourceOpDesc], name = "FileScan"), new Type(value = classOf[FileScanOpDesc], name = "FileScanOp"), new Type(value = classOf[TextInputSourceOpDesc], name = "TextInput"), new Type( value = classOf[TwitterFullArchiveSearchSourceOpDesc], name = "TwitterFullArchiveSearch" ), new Type( value = classOf[TwitterSearchSourceOpDesc], name = "TwitterSearch" ), new Type(value = classOf[ChoroplethMapOpDesc], name = "ChoroplethMap"), new Type(value = classOf[TimeSeriesOpDesc], name = "TimeSeriesPlot"), new Type(value = classOf[CandlestickChartOpDesc], name = "CandlestickChart"), new Type(value = classOf[SplitOpDesc], name = "Split"), new Type(value = classOf[ContourPlotOpDesc], name = "ContourPlot"), new Type(value = classOf[ECDFPlotOpDesc], name = "ECDFPlot"), new Type(value = classOf[RegexOpDesc], name = "Regex"), new Type(value = classOf[SpecializedFilterOpDesc], name = "Filter"), new Type(value = classOf[ProjectionOpDesc], name = "Projection"), new Type(value = classOf[StripChartOpDesc], name = "StripChart"), new Type(value = classOf[UnionOpDesc], name = "Union"), new Type(value = classOf[KeywordSearchOpDesc], name = "KeywordSearch"), new Type(value = classOf[SubstringSearchOpDesc], name = "SubstringSearch"), new Type(value = classOf[AggregateOpDesc], name = "Aggregate"), new Type(value = classOf[LineChartOpDesc], name = "LineChart"), new Type(value = classOf[WaterfallChartOpDesc], name = "WaterfallChart"), new Type(value = classOf[WindRoseChartOpDesc], name = "WindRoseChart"), new Type(value = classOf[BarChartOpDesc], name = "BarChart"), new Type(value = classOf[PolarChartOpDesc], name = "PolarChart"), new Type(value = classOf[RangeSliderOpDesc], name = "RangeSlider"), new Type(value = classOf[PieChartOpDesc], name = "PieChart"), new Type(value = classOf[QuiverPlotOpDesc], name = "QuiverPlot"), new Type(value = classOf[RadarPlotOpDesc], name = "RadarPlot"), new Type(value = classOf[RadarChartOpDesc], name = "RadarChart"), new Type(value = classOf[ParallelCoordinatesPlotOpDesc], name = "ParallelCoordinatesPlot"), new Type(value = classOf[WordCloudOpDesc], name = "WordCloud"), new Type(value = classOf[HtmlVizOpDesc], name = "HTMLVisualizer"), new Type(value = classOf[UrlVizOpDesc], name = "URLVisualizer"), new Type(value = classOf[ScatterplotOpDesc], name = "Scatterplot"), new Type(value = classOf[PythonUDFOpDescV2], name = "PythonUDFV2"), new Type(value = classOf[PythonUDFSourceOpDescV2], name = "PythonUDFSourceV2"), new Type(value = classOf[DualInputPortsPythonUDFOpDescV2], name = "DualInputPortsPythonUDFV2"), new Type(value = classOf[MySQLSourceOpDesc], name = "MySQLSource"), new Type(value = classOf[PostgreSQLSourceOpDesc], name = "PostgreSQLSource"), new Type(value = classOf[AsterixDBSourceOpDesc], name = "AsterixDBSource"), new Type(value = classOf[TypeCastingOpDesc], name = "TypeCasting"), new Type(value = classOf[LimitOpDesc], name = "Limit"), new Type(value = classOf[SleepOpDesc], name = "Sleep"), new Type(value = classOf[RandomKSamplingOpDesc], name = "RandomKSampling"), new Type(value = classOf[ReservoirSamplingOpDesc], name = "ReservoirSampling"), new Type(value = classOf[HashJoinOpDesc[String]], name = "HashJoin"), new Type(value = classOf[DistinctOpDesc], name = "Distinct"), new Type(value = classOf[IntersectOpDesc], name = "Intersect"), new Type(value = classOf[SymmetricDifferenceOpDesc], name = "SymmetricDifference"), new Type(value = classOf[DifferenceOpDesc], name = "Difference"), new Type(value = classOf[IntervalJoinOpDesc], name = "IntervalJoin"), new Type(value = classOf[UnnestStringOpDesc], name = "UnnestString"), new Type(value = classOf[DictionaryMatcherOpDesc], name = "DictionaryMatcher"), new Type(value = classOf[SortPartitionsOpDesc], name = "SortPartitions"), new Type(value = classOf[CSVOldScanSourceOpDesc], name = "CSVOldFileScan"), new Type(value = classOf[RedditSearchSourceOpDesc], name = "RedditSearch"), new Type(value = classOf[PythonLambdaFunctionOpDesc], name = "PythonLambdaFunction"), new Type(value = classOf[PythonTableReducerOpDesc], name = "PythonTableReducer"), new Type(value = classOf[URLFetcherOpDesc], name = "URLFetcher"), new Type(value = classOf[VolcanoPlotOpDesc], name = "VolcanoPlot"), new Type(value = classOf[CartesianProductOpDesc], name = "CartesianProduct"), new Type(value = classOf[FilledAreaPlotOpDesc], name = "FilledAreaPlot"), new Type(value = classOf[CarpetPlotOpDesc], name = "CarpetPlot"), new Type(value = classOf[DotPlotOpDesc], name = "DotPlot"), new Type(value = classOf[TreePlotOpDesc], name = "TreePlot"), new Type(value = classOf[BubbleChartOpDesc], name = "BubbleChart"), new Type(value = classOf[BulletChartOpDesc], name = "BulletChart"), new Type(value = classOf[GanttChartOpDesc], name = "GanttChart"), new Type(value = classOf[GaugeChartOpDesc], name = "GaugeChart"), new Type(value = classOf[ImageVisualizerOpDesc], name = "ImageVisualizer"), new Type(value = classOf[HierarchyChartOpDesc], name = "HierarchyChart"), new Type(value = classOf[DumbbellPlotOpDesc], name = "DumbbellPlot"), new Type(value = classOf[DummyOpDesc], name = "Dummy"), new Type(value = classOf[BoxViolinPlotOpDesc], name = "BoxViolinPlot"), new Type(value = classOf[NetworkGraphOpDesc], name = "NetworkGraph"), new Type(value = classOf[HistogramChartOpDesc], name = "Histogram"), new Type(value = classOf[Histogram2DOpDesc], name = "Histogram2D"), new Type(value = classOf[ScatterMatrixChartOpDesc], name = "ScatterMatrixChart"), new Type(value = classOf[HeatMapOpDesc], name = "HeatMap"), new Type(value = classOf[Scatter3dChartOpDesc], name = "Scatter3DChart"), new Type(value = classOf[FunnelPlotOpDesc], name = "FunnelPlot"), new Type(value = classOf[TablesPlotOpDesc], name = "TablesPlot"), new Type(value = classOf[ContinuousErrorBandsOpDesc], name = "ContinuousErrorBands"), new Type(value = classOf[FigureFactoryTableOpDesc], name = "FigureFactoryTable"), new Type(value = classOf[TernaryContourOpDesc], name = "TernaryContour"), new Type(value = classOf[TernaryPlotOpDesc], name = "TernaryPlot"), new Type(value = classOf[DendrogramOpDesc], name = "Dendrogram"), new Type(value = classOf[NestedTableOpDesc], name = "NestedTable"), new Type(value = classOf[JavaUDFOpDesc], name = "JavaUDF"), new Type(value = classOf[RUDFOpDesc], name = "RUDF"), new Type(value = classOf[RUDFSourceOpDesc], name = "RUDFSource"), new Type(value = classOf[ArrowSourceOpDesc], name = "ArrowSource"), new Type(value = classOf[MachineLearningScorerOpDesc], name = "Scorer"), new Type(value = classOf[SortOpDesc], name = "Sort"), new Type(value = classOf[StableMergeSortOpDesc], name = "StableMergeSort"), new Type(value = classOf[SklearnLogisticRegressionOpDesc], name = "SklearnLogisticRegression"), new Type( value = classOf[SklearnLogisticRegressionCVOpDesc], name = "SklearnLogisticRegressionCV" ), new Type(value = classOf[SklearnTrainingRidgeOpDesc], name = "SklearnTrainingRidge"), new Type(value = classOf[SklearnTrainingRidgeCVOpDesc], name = "SklearnTrainingRidgeCV"), new Type(value = classOf[SklearnTrainingSDGOpDesc], name = "SklearnTrainingSDG"), new Type( value = classOf[SklearnTrainingPassiveAggressiveOpDesc], name = "SklearnTrainingPassiveAggressive" ), new Type(value = classOf[SklearnTrainingPerceptronOpDesc], name = "SklearnTrainingPerceptron"), new Type(value = classOf[SklearnTrainingKNNOpDesc], name = "SklearnTrainingKNN"), new Type( value = classOf[SklearnTrainingNearestCentroidOpDesc], name = "SklearnTrainingNearestCentroid" ), new Type(value = classOf[SklearnTrainingSVMOpDesc], name = "SklearnTrainingSVM"), new Type(value = classOf[SklearnTrainingLinearSVMOpDesc], name = "SklearnTrainingLinearSVM"), new Type( value = classOf[SklearnTrainingDecisionTreeOpDesc], name = "SklearnTrainingDecisionTree" ), new Type(value = classOf[SklearnTrainingExtraTreeOpDesc], name = "SklearnTrainingExtraTree"), new Type( value = classOf[SklearnTrainingMultiLayerPerceptronOpDesc], name = "SklearnTrainingMultiLayerPerceptron" ), new Type( value = classOf[SklearnTrainingProbabilityCalibrationOpDesc], name = "SklearnTrainingProbabilityCalibration" ), new Type( value = classOf[SklearnTrainingRandomForestOpDesc], name = "SklearnTrainingRandomForest" ), new Type(value = classOf[SklearnTrainingBaggingOpDesc], name = "SklearnTrainingBagging"), new Type( value = classOf[SklearnTrainingGradientBoostingOpDesc], name = "SklearnTrainingGradientBoosting" ), new Type( value = classOf[SklearnTrainingAdaptiveBoostingOpDesc], name = "SklearnTrainingAdaptiveBoosting" ), new Type(value = classOf[SklearnTrainingExtraTreesOpDesc], name = "SklearnTrainingExtraTrees"), new Type( value = classOf[SklearnTrainingGaussianNaiveBayesOpDesc], name = "SklearnTrainingGaussianNaiveBayes" ), new Type( value = classOf[SklearnTrainingMultinomialNaiveBayesOpDesc], name = "SklearnTrainingMultinomialNaiveBayes" ), new Type( value = classOf[SklearnTrainingComplementNaiveBayesOpDesc], name = "SklearnTrainingComplementNaiveBayes" ), new Type( value = classOf[SklearnTrainingBernoulliNaiveBayesOpDesc], name = "SklearnTrainingBernoulliNaiveBayes" ), new Type( value = classOf[SklearnTrainingDummyClassifierOpDesc], name = "SklearnTrainingDummyClassifier" ), new Type( value = classOf[SklearnTrainingLinearRegressionOpDesc], name = "SklearnTrainingLinearRegression" ), new Type( value = classOf[SklearnTrainingLogisticRegressionOpDesc], name = "SklearnTrainingLogisticRegression" ), new Type( value = classOf[SklearnTrainingLogisticRegressionCVOpDesc], name = "SklearnTrainingLogisticRegressionCV" ), new Type(value = classOf[SklearnLogisticRegressionOpDesc], name = "SklearnLogisticRegression"), new Type( value = classOf[SklearnLogisticRegressionCVOpDesc], name = "SklearnLogisticRegressionCV" ), new Type(value = classOf[SklearnRidgeOpDesc], name = "SklearnRidge"), new Type(value = classOf[SklearnRidgeCVOpDesc], name = "SklearnRidgeCV"), new Type(value = classOf[SklearnSDGOpDesc], name = "SklearnSDG"), new Type(value = classOf[SklearnPassiveAggressiveOpDesc], name = "SklearnPassiveAggressive"), new Type(value = classOf[SklearnPerceptronOpDesc], name = "SklearnPerceptron"), new Type(value = classOf[SklearnKNNOpDesc], name = "SklearnKNN"), new Type(value = classOf[SklearnNearestCentroidOpDesc], name = "SklearnNearestCentroid"), new Type(value = classOf[SklearnSVMOpDesc], name = "SklearnSVM"), new Type(value = classOf[SklearnLinearSVMOpDesc], name = "SklearnLinearSVM"), new Type(value = classOf[SklearnLinearRegressionOpDesc], name = "SklearnLinearRegression"), new Type(value = classOf[SklearnDecisionTreeOpDesc], name = "SklearnDecisionTree"), new Type(value = classOf[SklearnExtraTreeOpDesc], name = "SklearnExtraTree"), new Type( value = classOf[SklearnMultiLayerPerceptronOpDesc], name = "SklearnMultiLayerPerceptron" ), new Type( value = classOf[SklearnProbabilityCalibrationOpDesc], name = "SklearnProbabilityCalibration" ), new Type(value = classOf[SklearnRandomForestOpDesc], name = "SklearnRandomForest"), new Type(value = classOf[SklearnBaggingOpDesc], name = "SklearnBagging"), new Type(value = classOf[SklearnGradientBoostingOpDesc], name = "SklearnGradientBoosting"), new Type(value = classOf[SklearnAdaptiveBoostingOpDesc], name = "SklearnAdaptiveBoosting"), new Type(value = classOf[SklearnExtraTreesOpDesc], name = "SklearnExtraTrees"), new Type(value = classOf[SklearnGaussianNaiveBayesOpDesc], name = "SklearnGaussianNaiveBayes"), new Type( value = classOf[SklearnMultinomialNaiveBayesOpDesc], name = "SklearnMultinomialNaiveBayes" ), new Type( value = classOf[SklearnComplementNaiveBayesOpDesc], name = "SklearnComplementNaiveBayes" ), new Type( value = classOf[SklearnBernoulliNaiveBayesOpDesc], name = "SklearnBernoulliNaiveBayes" ), new Type(value = classOf[SklearnDummyClassifierOpDesc], name = "SklearnDummyClassifier"), new Type(value = classOf[SklearnPredictionOpDesc], name = "SklearnPrediction"), new Type( value = classOf[HuggingFaceSentimentAnalysisOpDesc], name = "HuggingFaceSentimentAnalysis" ), new Type( value = classOf[HuggingFaceTextSummarizationOpDesc], name = "HuggingFaceTextSummarization" ), new Type( value = classOf[HuggingFaceSpamSMSDetectionOpDesc], name = "HuggingFaceSpamSMSDetection" ), new Type( value = classOf[HuggingFaceIrisLogisticRegressionOpDesc], name = "HuggingFaceIrisLogisticRegression" ), new Type( value = classOf[SklearnAdvancedKNNClassifierTrainerOpDesc], name = "KNNClassifierTrainer" ), new Type( value = classOf[SklearnAdvancedKNNRegressorTrainerOpDesc], name = "KNNRegressorTrainer" ), new Type( value = classOf[SklearnAdvancedSVCTrainerOpDesc], name = "SVCTrainer" ), new Type( value = classOf[SklearnAdvancedSVRTrainerOpDesc], name = "SVRTrainer" ), new Type(value = classOf[SklearnTestingOpDesc], name = "SklearnTesting") ) ) abstract class LogicalOp extends PortDescriptor with Serializable { @JsonProperty(PropertyNameConstants.OPERATOR_ID) private var operatorId: String = getClass.getSimpleName + "-" + UUID.randomUUID.toString @JsonProperty(PropertyNameConstants.OPERATOR_VERSION) var operatorVersion: String = getOperatorVersion def operatorIdentifier: OperatorIdentity = OperatorIdentity(operatorId) def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = ??? // a logical operator corresponds multiple physical operators (a small DAG) def getPhysicalPlan( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalPlan = { PhysicalPlan( operators = Set(getPhysicalOp(workflowId, executionId)), links = Set.empty ) } def operatorInfo: OperatorInfo private def getOperatorVersion: String = { val path = "amber/src/main/scala/" val operatorPath = path + this.getClass.getPackage.getName.replace(".", "/") OPVersion.getVersion(this.getClass.getSimpleName, operatorPath) } override def hashCode: Int = HashCodeBuilder.reflectionHashCode(this) override def equals(that: Any): Boolean = EqualsBuilder.reflectionEquals(this, that, "context") override def toString: String = ToStringBuilder.reflectionToString(this) def setOperatorId(id: String): Unit = { operatorId = id } def runtimeReconfiguration( workflowId: WorkflowIdentity, executionId: ExecutionIdentity, oldOpDesc: LogicalOp, newOpDesc: LogicalOp ): Try[(PhysicalOp, Option[StateTransferFunc])] = { throw new UnsupportedOperationException( "operator " + getClass.getSimpleName + " does not support reconfiguration" ) } @JsonProperty @JsonSchemaTitle("Dummy Property List") @JsonPropertyDescription("Add dummy property if needed") var dummyPropertyList: List[DummyProperties] = List() /** * Propagates the schema from external input ports to external output ports. * This method is primarily used to derive the output schemas for logical operators. * * @param inputSchemas A map containing the schemas of the external input ports. * @return A map of external output port identities to their corresponding schemas. */ def getExternalOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { this .getPhysicalPlan(DEFAULT_WORKFLOW_ID, DEFAULT_EXECUTION_ID) .propagateSchema(inputSchemas) .operators .flatMap { operator => operator.outputPorts.values .filterNot { case (port, _, _) => port.id.internal } // Exclude internal ports .map { case (port, _, schemaEither) => schemaEither match { case Left(error) => throw error case Right(schema) => port.id -> schema // Map external port ID to its schema } } } .toMap } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/PortDescriptor.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator import com.fasterxml.jackson.annotation.{JsonIgnoreProperties, JsonProperty} import org.apache.texera.amber.core.workflow.PartitionInfo @JsonIgnoreProperties( Array("allowMultiInputs") ) // TODO: temporary backward compatibility for workflows persisted before PR #4379. case class PortDescription( portID: String, displayName: String, disallowMultiInputs: Boolean, isDynamicPort: Boolean, partitionRequirement: PartitionInfo, dependencies: List[Int] = List.empty ) trait PortDescriptor { @JsonProperty(required = false) var inputPorts: List[PortDescription] = null @JsonProperty(required = false) var outputPorts: List[PortDescription] = null } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/PythonOperatorDescriptor.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator import org.apache.texera.amber.core.executor.OpExecWithCode import org.apache.texera.amber.core.tuple.Schema import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{PhysicalOp, PortIdentity, SchemaPropagationFunc} trait PythonOperatorDescriptor extends LogicalOp { private def generatePythonCodeForRaisingException(ex: Throwable): String = { s"#EXCEPTION DURING CODE GENERATION: ${ex.getMessage}" } override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { val pythonCode = try { generatePythonCode() } catch { case ex: Throwable => // instead of throwing error directly, we embed the error in the code // this can let upper-level compiler catch the error without interrupting the schema propagation generatePythonCodeForRaisingException(ex) } val physicalOp = if (asSource()) { PhysicalOp.sourcePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithCode(pythonCode, "python") ) } else { PhysicalOp.oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithCode(pythonCode, "python") ) } physicalOp .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withParallelizable(parallelizable()) .withPropagateSchema(SchemaPropagationFunc(inputSchemas => getOutputSchemas(inputSchemas))) } def parallelizable(): Boolean = false def asSource(): Boolean = false /** * This method is to be implemented to generate the actual Python source code * based on operators predicates. * * @return a String representation of the executable Python source code. */ def generatePythonCode(): String def getOutputSchemas(inputSchemas: Map[PortIdentity, Schema]): Map[PortIdentity, Schema] } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/SpecialPhysicalOpFactory.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator import org.apache.texera.amber.core.executor.OpExecSource import org.apache.texera.amber.core.storage.VFSURIFactory import org.apache.texera.amber.core.tuple.Schema import org.apache.texera.amber.core.virtualidentity.{ ExecutionIdentity, PhysicalOpIdentity, WorkflowIdentity } import org.apache.texera.amber.core.workflow._ import java.net.URI object SpecialPhysicalOpFactory { def newSourcePhysicalOp( workflowIdentity: WorkflowIdentity, executionIdentity: ExecutionIdentity, uri: URI, downstreamOperator: PhysicalOpIdentity, downstreamPort: PortIdentity, schema: Schema ): PhysicalOp = { val (_, _, globalPortIdOption, _) = VFSURIFactory.decodeURI(uri) val globalPortId = globalPortIdOption.get val outputPort = OutputPort() PhysicalOp .sourcePhysicalOp( PhysicalOpIdentity( globalPortId.opId.logicalOpId, s"${globalPortId.opId.layerName}_source_${globalPortId.portId.id}_${downstreamOperator.logicalOpId.id .replace('-', '_')}_${downstreamPort.id}" ), workflowIdentity, executionIdentity, OpExecSource(uri.toString, workflowIdentity) ) .withInputPorts(List.empty) .withOutputPorts(List(outputPort)) .withPropagateSchema( SchemaPropagationFunc(_ => Map(outputPort.id -> schema)) ) .propagateSchema() } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/TestOperators.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator import org.apache.texera.amber.core.storage.FileResolver import org.apache.texera.amber.core.tuple.{Attribute, AttributeType} import org.apache.texera.amber.operator.aggregate.{ AggregateOpDesc, AggregationFunction, AggregationOperation } import org.apache.texera.amber.operator.hashJoin.HashJoinOpDesc import org.apache.texera.amber.operator.keywordSearch.KeywordSearchOpDesc import org.apache.texera.amber.operator.source.scan.csv.CSVScanSourceOpDesc import org.apache.texera.amber.operator.source.scan.json.JSONLScanSourceOpDesc import org.apache.texera.amber.operator.source.sql.asterixdb.AsterixDBSourceOpDesc import org.apache.texera.amber.operator.source.sql.mysql.MySQLSourceOpDesc import org.apache.texera.amber.operator.udf.python.PythonUDFOpDescV2 import org.apache.texera.amber.operator.udf.python.source.PythonUDFSourceOpDescV2 import java.nio.file.Path object TestOperators { val parentDir = Path .of(sys.env.getOrElse("TEXERA_HOME", ".")) .resolve("common") .resolve("workflow-operator") .toRealPath() .toString val CountrySalesSmallCsvPath = s"$parentDir/src/test/resources/country_sales_small.csv" val CountrySalesMediumCsvPath = s"$parentDir/src/test/resources/country_sales_medium.csv" val CountrySalesHeaderlessSmallCsvPath = s"$parentDir/src/test/resources/country_sales_headerless_small.csv" val CountrySalesSmallMultiLineCsvPath = s"$parentDir/src/test/resources/country_sales_small_multi_line.csv" val CountrySalesSmallMultiLineCustomDelimiterCsvPath = s"$parentDir/src/test/resources/country_sales_headerless_small_multi_line_custom_delimiter.csv" val smallJsonLPath = s"$parentDir/src/test/resources/100.jsonl" val mediumJsonLPath = s"$parentDir/src/test/resources/1000.jsonl" val TestTextFilePath: String = s"$parentDir/src/test/resources/line_numbers.txt" val TestCRLFTextFilePath: String = s"$parentDir/src/test/resources/line_numbers_crlf.txt" val TestNumbersFilePath: String = s"$parentDir/src/test/resources/numbers.txt" def headerlessSmallCsvScanOpDesc(): CSVScanSourceOpDesc = { getCsvScanOpDesc(CountrySalesHeaderlessSmallCsvPath, header = false) } def headerlessSmallMultiLineDataCsvScanOpDesc(): CSVScanSourceOpDesc = { getCsvScanOpDesc( CountrySalesHeaderlessSmallCsvPath, header = false, multiLine = true ) } def smallCsvScanOpDesc(): CSVScanSourceOpDesc = { getCsvScanOpDesc(CountrySalesSmallCsvPath, header = true) } def smallJSONLScanOpDesc(): JSONLScanSourceOpDesc = { getJSONLScanOpDesc(smallJsonLPath) } def mediumFlattenJSONLScanOpDesc(): JSONLScanSourceOpDesc = { getJSONLScanOpDesc(mediumJsonLPath, flatten = true) } def getCsvScanOpDesc( fileName: String, header: Boolean, multiLine: Boolean = false ): CSVScanSourceOpDesc = { val csvHeaderlessOp = new CSVScanSourceOpDesc() csvHeaderlessOp.fileName = Some(fileName) csvHeaderlessOp.customDelimiter = Some(",") csvHeaderlessOp.hasHeader = header csvHeaderlessOp.setResolvedFileName(FileResolver.resolve(fileName)) csvHeaderlessOp } def getJSONLScanOpDesc(fileName: String, flatten: Boolean = false): JSONLScanSourceOpDesc = { val jsonlOp = new JSONLScanSourceOpDesc jsonlOp.fileName = Some(fileName) jsonlOp.flatten = flatten jsonlOp.setResolvedFileName(FileResolver.resolve(fileName)) jsonlOp } def joinOpDesc(buildAttrName: String, probeAttrName: String): HashJoinOpDesc[String] = { val joinOp = new HashJoinOpDesc[String]() joinOp.buildAttributeName = buildAttrName joinOp.probeAttributeName = probeAttrName joinOp } def mediumCsvScanOpDesc(): CSVScanSourceOpDesc = { getCsvScanOpDesc(CountrySalesMediumCsvPath, header = true) } def keywordSearchOpDesc(attribute: String, keywordToSearch: String): KeywordSearchOpDesc = { val keywordSearchOp = new KeywordSearchOpDesc() keywordSearchOp.attribute = attribute keywordSearchOp.keyword = keywordToSearch keywordSearchOp } def aggregateAndGroupByDesc( attributeToAggregate: String, aggFunction: AggregationFunction, groupByAttributes: List[String] ): AggregateOpDesc = { val aggOp = new AggregateOpDesc() val aggFunc = new AggregationOperation() aggFunc.aggFunction = aggFunction aggFunc.attribute = attributeToAggregate aggFunc.resultAttribute = "aggregate-result" aggOp.aggregations = List(aggFunc) aggOp.groupByKeys = groupByAttributes aggOp } def inMemoryMySQLSourceOpDesc( host: String, port: String, database: String, table: String, username: String, password: String ): MySQLSourceOpDesc = { val inMemoryMySQLSourceOpDesc = new MySQLSourceOpDesc() inMemoryMySQLSourceOpDesc.host = host inMemoryMySQLSourceOpDesc.port = port inMemoryMySQLSourceOpDesc.database = database inMemoryMySQLSourceOpDesc.table = table inMemoryMySQLSourceOpDesc.username = username inMemoryMySQLSourceOpDesc.password = password inMemoryMySQLSourceOpDesc.limit = Some(1000) inMemoryMySQLSourceOpDesc } // TODO: use mock data to perform the test, remove dependency on the real AsterixDB def asterixDBSourceOpDesc(): AsterixDBSourceOpDesc = { val asterixDBOp = new AsterixDBSourceOpDesc() asterixDBOp.host = "ipubmed4.ics.uci.edu" // AsterixDB at version 0.9.5 asterixDBOp.port = "default" asterixDBOp.database = "twitter" asterixDBOp.table = "ds_tweet" asterixDBOp.limit = Some(1000) asterixDBOp } def pythonOpDesc(): PythonUDFOpDescV2 = { val udf = new PythonUDFOpDescV2() udf.workers = 1 udf.retainInputColumns = true udf.code = """ |from pytexera import * | |class ProcessTupleOperator(UDFOperatorV2): | @overrides | def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]: | yield tuple_ |""".stripMargin udf } def pythonSourceOpDesc(numTuple: Int): PythonUDFSourceOpDescV2 = { val udf = new PythonUDFSourceOpDescV2() udf.workers = 1 udf.columns = List(new Attribute("field_1", AttributeType.STRING)) udf.code = s""" |from pytexera import * | |class UDFSourceOperator(UDFSourceOperator): | @overrides | def produce(self) -> Iterator[Union[TupleLike, TableLike, None]]: | for i in range($numTuple): | yield {'field_1': str(i) } |""".stripMargin udf } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/aggregate/AggregateOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.aggregate import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.tuple.Schema import org.apache.texera.amber.core.virtualidentity.{ ExecutionIdentity, PhysicalOpIdentity, WorkflowIdentity } import org.apache.texera.amber.core.workflow._ import org.apache.texera.amber.operator.LogicalOp import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeNameList import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.util.JSONUtils.objectMapper import javax.validation.constraints.{NotNull, Size} class AggregateOpDesc extends LogicalOp { @JsonProperty(value = "aggregations", required = true) @JsonPropertyDescription("multiple aggregation functions") @NotNull(message = "aggregation cannot be null") @Size(min = 1, message = "aggregations cannot be empty") var aggregations: List[AggregationOperation] = List() @JsonProperty("groupByKeys") @JsonSchemaTitle("Group By Keys") @JsonPropertyDescription("group by columns") @AutofillAttributeNameList var groupByKeys: List[String] = List() override def getPhysicalPlan( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalPlan = { if (groupByKeys == null) groupByKeys = List() // TODO: this is supposed to be blocking but due to limitations of materialization naming on the logical operator // we are keeping it not annotated as blocking. val inputPort = InputPort(PortIdentity()) val outputPort = OutputPort(PortIdentity(internal = true)) val partialDesc = objectMapper.writeValueAsString(this) val localAggregations = List(aggregations: _*) val partialPhysicalOp = PhysicalOp .oneToOnePhysicalOp( PhysicalOpIdentity(operatorIdentifier, "localAgg"), workflowId, executionId, OpExecWithClassName( "org.apache.texera.amber.operator.aggregate.AggregateOpExec", partialDesc ) ) .withIsOneToManyOp(true) .withInputPorts(List(inputPort)) .withOutputPorts(List(outputPort)) .withPropagateSchema( SchemaPropagationFunc(inputSchemas => { val inputSchema = inputSchemas(operatorInfo.inputPorts.head.id) val outputSchema = Schema( groupByKeys.map(key => inputSchema.getAttribute(key)) ++ localAggregations.map(agg => agg.getAggregationAttribute(inputSchema.getAttribute(agg.attribute).getType) ) ) Map(PortIdentity(internal = true) -> outputSchema) }) ) val finalInputPort = InputPort(PortIdentity(0, internal = true)) val finalOutputPort = OutputPort(PortIdentity(0), blocking = true) // change aggregations to final aggregations = aggregations.map(aggr => aggr.getFinal) val finalDesc = objectMapper.writeValueAsString(this) val finalPhysicalOp = PhysicalOp .oneToOnePhysicalOp( PhysicalOpIdentity(operatorIdentifier, "globalAgg"), workflowId, executionId, OpExecWithClassName("org.apache.texera.amber.operator.aggregate.AggregateOpExec", finalDesc) ) .withParallelizable(false) .withIsOneToManyOp(true) .withInputPorts(List(finalInputPort)) .withOutputPorts(List(finalOutputPort)) .withPropagateSchema( SchemaPropagationFunc(inputSchemas => Map(operatorInfo.outputPorts.head.id -> inputSchemas(finalInputPort.id)) ) ) .withPartitionRequirement(List(Option(HashPartition(groupByKeys)))) .withDerivePartition(_ => HashPartition(groupByKeys)) var plan = PhysicalPlan( operators = Set(partialPhysicalOp, finalPhysicalOp), links = Set( PhysicalLink(partialPhysicalOp.id, outputPort.id, finalPhysicalOp.id, finalInputPort.id) ) ) plan.operators.foreach(op => plan = plan.setOperator(op.withIsOneToManyOp(true))) plan } override def operatorInfo: OperatorInfo = OperatorInfo( "Aggregate", "Calculate different types of aggregation values", OperatorGroupConstants.AGGREGATE_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort()) ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/aggregate/AggregateOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.aggregate import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} import org.apache.texera.amber.util.JSONUtils.objectMapper import scala.collection.mutable /** * AggregateOpExec performs aggregation operations on input tuples, optionally grouping them by specified keys. */ class AggregateOpExec(descString: String) extends OperatorExecutor { private val desc: AggregateOpDesc = objectMapper.readValue(descString, classOf[AggregateOpDesc]) private var keyedPartialAggregates: mutable.HashMap[List[Object], List[Object]] = _ private var distributedAggregations: List[DistributedAggregation[Object]] = _ override def open(): Unit = { keyedPartialAggregates = new mutable.HashMap[List[Object], List[Object]]() distributedAggregations = null } override def close(): Unit = { keyedPartialAggregates.clear() distributedAggregations = null } override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = { // Initialize distributedAggregations if it's not yet initialized if (distributedAggregations == null) { distributedAggregations = desc.aggregations.map(agg => agg.getAggFunc(tuple.getSchema.getAttribute(agg.attribute).getType) ) } // Construct the group key val key = desc.groupByKeys.map(tuple.getField[Object]) // Get or initialize the partial aggregate for the key val partialAggregates = keyedPartialAggregates.getOrElseUpdate(key, distributedAggregations.map(_.init())) // Update the partial aggregates with the current tuple val updatedAggregates = (distributedAggregations zip partialAggregates).map { case (aggregation, partial) => aggregation.iterate(partial, tuple) } keyedPartialAggregates(key) = updatedAggregates Iterator.empty } override def onFinish(port: Int): Iterator[TupleLike] = { // Finalize aggregation for all keys and produce the result keyedPartialAggregates.iterator.map { case (key, partials) => val finalAggregates = partials.zipWithIndex.map { case (partial, index) => distributedAggregations(index).finalAgg(partial) } TupleLike(key ++ finalAggregates) } } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/aggregate/AggregationFunction.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.aggregate; import com.fasterxml.jackson.annotation.JsonValue; public enum AggregationFunction { SUM("sum"), COUNT("count"), AVERAGE("average"), MIN("min"), MAX("max"), CONCAT("concat"); private final String name; AggregationFunction(String name) { this.name = name; } // use the name string instead of enum string in JSON @JsonValue public String getName() { return this.name; } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/aggregate/AggregationOperation.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.aggregate import com.fasterxml.jackson.annotation.{JsonIgnore, JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, AttributeTypeUtils, Tuple} import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import javax.validation.constraints.NotNull case class AveragePartialObj(sum: Double, count: Double) extends Serializable {} @JsonSchemaInject(json = """ { "attributeTypeRules": { "attribute": { "allOf": [ { "if": { "aggFunction": { "valEnum": ["sum", "average", "min", "max"] } }, "then": { "enum": ["integer", "long", "double", "timestamp"] } }, { "if": { "aggFunction": { "valEnum": ["concat"] } }, "then": { "enum": ["string"] } } ] } } } """) class AggregationOperation { @JsonProperty(required = true) @JsonSchemaTitle("Aggregate Func") @JsonPropertyDescription("sum, count, average, min, max, or concat") var aggFunction: AggregationFunction = _ @JsonProperty(value = "attribute", required = true) @JsonPropertyDescription("column to calculate average value") @AutofillAttributeName var attribute: String = _ @JsonProperty(value = "result attribute", required = true) @JsonPropertyDescription("column name of average result") @NotNull(message = "result attribute is required") var resultAttribute: String = _ @JsonIgnore def getAggregationAttribute(attrType: AttributeType): Attribute = { val resultAttrType = this.aggFunction match { case AggregationFunction.SUM => attrType case AggregationFunction.COUNT => AttributeType.INTEGER case AggregationFunction.AVERAGE => AttributeType.DOUBLE case AggregationFunction.MIN => attrType case AggregationFunction.MAX => attrType case AggregationFunction.CONCAT => AttributeType.STRING case _ => throw new RuntimeException("Unknown aggregation function: " + this.aggFunction) } new Attribute(resultAttribute, resultAttrType) } @JsonIgnore def getAggFunc(attrType: AttributeType): DistributedAggregation[Object] = { val aggFunc = aggFunction match { case AggregationFunction.AVERAGE => averageAgg() case AggregationFunction.COUNT => countAgg() case AggregationFunction.MAX => maxAgg(attrType) case AggregationFunction.MIN => minAgg(attrType) case AggregationFunction.SUM => sumAgg(attrType) case AggregationFunction.CONCAT => concatAgg() case _ => throw new UnsupportedOperationException("Unknown aggregation function: " + aggFunction) } aggFunc.asInstanceOf[DistributedAggregation[Object]] } @JsonIgnore def getFinal: AggregationOperation = { val newAggFunc = aggFunction match { case AggregationFunction.COUNT => AggregationFunction.SUM case a: AggregationFunction => a } val res = new AggregationOperation() res.aggFunction = newAggFunc res.resultAttribute = resultAttribute res.attribute = resultAttribute res } private def sumAgg(attributeType: AttributeType): DistributedAggregation[Object] = { if ( attributeType != AttributeType.INTEGER && attributeType != AttributeType.DOUBLE && attributeType != AttributeType.LONG && attributeType != AttributeType.TIMESTAMP ) { throw new UnsupportedOperationException( "Unsupported attribute type for sum aggregation: " + attributeType ) } new DistributedAggregation[Object]( () => AttributeTypeUtils.zeroValue(attributeType), (partial, tuple) => { val value = tuple.getField[Object](attribute) AttributeTypeUtils.add(partial, value, attributeType) }, (partial1, partial2) => AttributeTypeUtils.add(partial1, partial2, attributeType), partial => partial ) } private def countAgg(): DistributedAggregation[Integer] = { new DistributedAggregation[Integer]( () => 0, (partial, tuple) => { val inc = if (attribute == null) 1 else if (tuple.getField(attribute) != null) 1 else 0 partial + inc }, (partial1, partial2) => partial1 + partial2, partial => partial ) } private def concatAgg(): DistributedAggregation[String] = { new DistributedAggregation[String]( () => "", (partial, tuple) => { if (partial == "") { if (tuple.getField(attribute) != null) tuple.getField(attribute).toString else "" } else { partial + "," + (if (tuple.getField(attribute) != null) tuple.getField(attribute).toString else "") } }, (partial1, partial2) => { if (partial1 != "" && partial2 != "") { partial1 + "," + partial2 } else { partial1 + partial2 } }, partial => partial ) } private def minAgg(attributeType: AttributeType): DistributedAggregation[Object] = { if ( attributeType != AttributeType.INTEGER && attributeType != AttributeType.DOUBLE && attributeType != AttributeType.LONG && attributeType != AttributeType.TIMESTAMP ) { throw new UnsupportedOperationException( "Unsupported attribute type for min aggregation: " + attributeType ) } new DistributedAggregation[Object]( () => AttributeTypeUtils.maxValue(attributeType), (partial, tuple) => { val value = tuple.getField[Object](attribute) val comp = AttributeTypeUtils.compare(value, partial, attributeType) if (value != null && comp < 0) value else partial }, (partial1, partial2) => if (AttributeTypeUtils.compare(partial1, partial2, attributeType) < 0) partial1 else partial2, partial => if (partial == AttributeTypeUtils.maxValue(attributeType)) null else partial ) } private def maxAgg(attributeType: AttributeType): DistributedAggregation[Object] = { if ( attributeType != AttributeType.INTEGER && attributeType != AttributeType.DOUBLE && attributeType != AttributeType.LONG && attributeType != AttributeType.TIMESTAMP ) { throw new UnsupportedOperationException( "Unsupported attribute type for max aggregation: " + attributeType ) } new DistributedAggregation[Object]( () => AttributeTypeUtils.minValue(attributeType), (partial, tuple) => { val value = tuple.getField[Object](attribute) val comp = AttributeTypeUtils.compare(value, partial, attributeType) if (value != null && comp > 0) value else partial }, (partial1, partial2) => if (AttributeTypeUtils.compare(partial1, partial2, attributeType) > 0) partial1 else partial2, partial => if (partial == AttributeTypeUtils.maxValue(attributeType)) null else partial ) } private def getNumericalValue(tuple: Tuple): Option[Double] = { val value: Object = tuple.getField(attribute) if (value == null) return None if (tuple.getSchema.getAttribute(attribute).getType == AttributeType.TIMESTAMP) Option(AttributeTypeUtils.parseTimestamp(value.toString).getTime.toDouble) else Option(value.toString.toDouble) } private def averageAgg(): DistributedAggregation[AveragePartialObj] = { new DistributedAggregation[AveragePartialObj]( () => AveragePartialObj(0, 0), (partial, tuple) => { val value = getNumericalValue(tuple) AveragePartialObj( partial.sum + (if (value.isDefined) value.get else 0), partial.count + (if (value.isDefined) 1 else 0) ) }, (partial1, partial2) => AveragePartialObj(partial1.sum + partial2.sum, partial1.count + partial2.count), partial => { val ret: java.lang.Double = if (partial.count == 0d) null else partial.sum / partial.count ret } ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/aggregate/DistributedAggregation.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.aggregate import org.apache.texera.amber.core.tuple.Tuple /** * This class defines the necessary functions required by a distributed aggregation. * * Explanation using "average" as an example: * To compute the average of data on multiple nodes, * each node first computes the sum and count of its local data (partial result), * then the partial results are sent to one node, where it adds up the sum and count of all nodes, * finally, average is computed by calculating sum/count. * * Corresponding to the functions: * init: initializes partial result: partial = (sum=0, count=0) * iterate: accumulates each input data: sum += inputValue, count += 1 * merge: adds up all partial results: sum += partialSum, count += partialCount * finalAgg: calculates final result: average = sum / count * * These function definitions are from * "Distributed Aggregation for Data-Parallel Computing: Interfaces and Implementations" * https://www.sigops.org/s/conferences/sosp/2009/papers/yu-sosp09.pdf */ case class DistributedAggregation[P <: AnyRef]( // () => PartialObject init: () => P, // PartialObject + Tuple => PartialObject iterate: (P, Tuple) => P, // PartialObject + PartialObject => PartialObject merge: (P, P) => P, // PartialObject => Tuple with one column, later be combined into FinalObject finalAgg: (P) => Object ) ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/cartesianProduct/CartesianProductOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.cartesianProduct import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.tuple.{Attribute, Schema} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow._ import org.apache.texera.amber.operator.LogicalOp import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} class CartesianProductOpDesc extends LogicalOp { override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.cartesianProduct.CartesianProductOpExec" ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPropagateSchema( SchemaPropagationFunc(inputSchemas => { // Combines the left and right input schemas into a single output schema. // // - The output schema includes all attributes from the left schema first, followed by // attributes from the right schema. // - Duplicate attribute names are resolved by appending an increasing suffix (e.g., `#@1`, `#@2`). // - Attributes from the left schema retain their original names in the output schema. // // Example: // Left schema: (dup, dup#@1, dup#@2) // Right schema: (r1, r2, dup) // Output schema: (dup, dup#@1, dup#@2, r1, r2, dup#@3) // // In this example, the last attribute from the right schema (`dup`) is renamed to `dup#@3` // to avoid conflicts. var outputSchema = Schema() val leftSchema = inputSchemas(operatorInfo.inputPorts.head.id) val rightSchema = inputSchemas(operatorInfo.inputPorts.last.id) val leftAttributeNames = leftSchema.getAttributeNames val rightAttributeNames = rightSchema.getAttributeNames outputSchema = outputSchema.add(leftSchema) rightSchema.getAttributes.foreach(attr => { var newName = attr.getName while ( leftAttributeNames.contains(newName) || rightAttributeNames .filterNot(attrName => attrName == attr.getName) .contains(newName) ) { newName = s"$newName#@1" } if (newName == attr.getName) { // non-duplicate attribute, add to builder as is outputSchema = outputSchema.add(attr) } else { // renamed the duplicate attribute, construct new Attribute outputSchema = outputSchema.add(new Attribute(newName, attr.getType)) } }) Map(operatorInfo.outputPorts.head.id -> outputSchema) }) ) // TODO : refactor to parallelize this operator for better performance and scalability: // can consider hash partition on larger input, broadcast smaller table to each partition .withParallelizable(false) } override def operatorInfo: OperatorInfo = OperatorInfo( "Cartesian Product", "Append fields together to get the cartesian product of two inputs", OperatorGroupConstants.JOIN_GROUP, inputPorts = List( InputPort(PortIdentity(), displayName = "left"), InputPort(PortIdentity(1), displayName = "right", dependencies = List(PortIdentity())) ), outputPorts = List(OutputPort()) ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/cartesianProduct/CartesianProductOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.cartesianProduct import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} import org.apache.texera.amber.operator.hashJoin.JoinUtils import scala.collection.mutable.ArrayBuffer /** * Executes a Cartesian Product operation between tuples from two input streams. */ class CartesianProductOpExec extends OperatorExecutor { private var leftTuples: ArrayBuffer[Tuple] = _ override def open(): Unit = { leftTuples = ArrayBuffer[Tuple]() } override def close(): Unit = { leftTuples.clear() } /** * Processes incoming tuples from either the left or right input stream. * Tuples from the left stream are collected until the stream is exhausted. * Then, for each tuple from the right stream, it generates a Cartesian product * with every tuple collected from the left stream. * * @param tuple Either a Tuple from one of the streams or an InputExhausted signal. * @param port The port number indicating which stream the tuple is from (0 for left, 1 for right). * @return An Iterator of TupleLike objects representing the Cartesian product. */ override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = { if (port == 0) { leftTuples += tuple Iterator.empty } else { leftTuples.map(leftTuple => JoinUtils.joinTuples(leftTuple, tuple)).iterator } } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/dictionary/DictionaryMatcherOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.dictionary import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.tuple.{Attribute, AttributeType} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{ InputPort, OutputPort, PhysicalOp, SchemaPropagationFunc } import org.apache.texera.amber.operator.map.MapOpDesc import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.util.JSONUtils.objectMapper /** * Dictionary matcher operator matches a tuple if the specified column is in the given dictionary. * It outputs an extra column to label the tuple if it is matched or not * This is the description of the operator */ class DictionaryMatcherOpDesc extends MapOpDesc { @JsonProperty(value = "Dictionary", required = true) @JsonPropertyDescription("dictionary values separated by a comma") var dictionary: String = _ @JsonProperty(value = "Attribute", required = true) @JsonPropertyDescription("column name to match") @AutofillAttributeName var attribute: String = _ @JsonProperty(value = "result attribute", required = true, defaultValue = "matched") @JsonPropertyDescription("column name of the matching result") var resultAttribute: String = _ @JsonProperty(value = "Matching type", required = true) var matchingType: MatchingType = _ override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.dictionary.DictionaryMatcherOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPropagateSchema( SchemaPropagationFunc(inputSchemas => { if (resultAttribute == null || resultAttribute.trim.isEmpty) return null Map( operatorInfo.outputPorts.head.id -> inputSchemas.values.head .add(new Attribute(resultAttribute, AttributeType.BOOLEAN)) ) }) ) } override def operatorInfo: OperatorInfo = OperatorInfo( "Dictionary matcher", "Matches tuples if they appear in a given dictionary", OperatorGroupConstants.SEARCH_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort()), supportReconfiguration = true ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/dictionary/DictionaryMatcherOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.dictionary import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} import org.apache.texera.amber.operator.map.MapOpExec import org.apache.texera.amber.util.JSONUtils.objectMapper import org.apache.lucene.analysis.Analyzer import org.apache.lucene.analysis.en.EnglishAnalyzer import org.apache.lucene.analysis.tokenattributes.CharTermAttribute import java.io.StringReader import scala.collection.mutable import scala.collection.mutable.ListBuffer class DictionaryMatcherOpExec(descString: String) extends MapOpExec { private val desc: DictionaryMatcherOpDesc = objectMapper.readValue(descString, classOf[DictionaryMatcherOpDesc]) // this is needed for the matching types Phrase and Conjunction var tokenizedDictionaryEntries: ListBuffer[mutable.Set[String]] = _ // this is needed for the simple Scan matching type var dictionaryEntries: List[String] = _ var luceneAnalyzer: Analyzer = _ /** An unmodifiable set containing some common URL words that are not usually useful * for searching. */ private final val URL_STOP_WORDS_SET = List[String]( "http", "https", "org", "net", "com", "store", "www", "html" ) /** * first prepare the dictionary by splitting the values using a comma delimiter then tokenize the split values */ override def open(): Unit = { // create the dictionary by splitting the values first dictionaryEntries = desc.dictionary.split(",").toList.map(_.toLowerCase) if (desc.matchingType == MatchingType.CONJUNCTION_INDEXBASED) { // then tokenize each entry this.luceneAnalyzer = new EnglishAnalyzer tokenizedDictionaryEntries = ListBuffer[mutable.Set[String]]() tokenizeDictionary() } } override def close(): Unit = { if (tokenizedDictionaryEntries != null) { tokenizedDictionaryEntries.clear() } dictionaryEntries = null luceneAnalyzer = null } /** * use LuceneAnalyzer to tokenize the dictionary */ private def tokenizeDictionary(): Unit = { for (text <- dictionaryEntries) { tokenizedDictionaryEntries += tokenize(text) } } /** * Determines whether a given tuple matches any dictionary entry based on defined matching criteria. * The tuple's specified field is converted to a lowercase string for comparison. * * @param tuple The tuple whose field is to be checked against dictionary entries. * @return true if the tuple matches a dictionary entry according to the matching criteria; false otherwise. */ private def isTupleInDictionary(tuple: Tuple): Boolean = { val text = tuple.getField(desc.attribute).asInstanceOf[String].toLowerCase // Return false if the text is empty, as it cannot match any dictionary entry if (text.isEmpty) return false desc.matchingType match { case MatchingType.SCANBASED => // Directly check if the dictionary contains the text dictionaryEntries.contains(text) case MatchingType.SUBSTRING => // Check if any dictionary entry contains the text as a substring dictionaryEntries.exists(entry => entry.contains(text)) case MatchingType.CONJUNCTION_INDEXBASED => // Tokenize the text and check if any tokenized dictionary entry is a subset of the tokenized text val tokenizedText = tokenize(text) tokenizedText.nonEmpty && tokenizedDictionaryEntries.exists(entry => entry.subsetOf(tokenizedText) ) } } /** * Tokenizes a given text into a set of unique tokens, excluding stopwords. * * @param text The input text to tokenize. * @return A mutable set of tokens derived from the input text, excluding stopwords. */ private def tokenize(text: String): mutable.Set[String] = { val tokens = mutable.Set[String]() val tokenStream = luceneAnalyzer.tokenStream("", new StringReader(text)) try { val charTermAttribute = tokenStream.addAttribute(classOf[CharTermAttribute]) tokenStream.reset() while (tokenStream.incrementToken()) { val term = charTermAttribute.toString.toLowerCase if ( !EnglishAnalyzer.ENGLISH_STOP_WORDS_SET.contains(term) && !URL_STOP_WORDS_SET .contains(term) ) { tokens += term } } } finally { tokenStream.close() // Ensure the token stream is always closed } tokens } /** * Labels an input tuple as matched if it is present in the dictionary. * * @param tuple The input tuple to be checked against the dictionary. * @return A TupleLike object containing the original fields of the tuple and a boolean indicating the match status. */ private def labelTupleIfMatched(tuple: Tuple): TupleLike = { val isMatched = Option(tuple.getField[Any](desc.attribute)).exists(_ => isTupleInDictionary(tuple)) TupleLike(tuple.getFields ++ Seq(isMatched)) } setMapFunc(labelTupleIfMatched) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/dictionary/MatchingType.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.dictionary; import com.fasterxml.jackson.annotation.JsonValue; /** * MatchingType: the type of matching to perform.
* Currently we have 3 types of matching:
*

* SCANBASED:
* Performs simple exact matching of the query. Matching is * case insensitive.
*

* SUBSTRING:
* Performs simple substring matching of the query. Matching is * case insensitive.
*

* CONJUNCTION_INDEXBASED:
* Performs search of conjunction of query tokens. The query is tokenized * into keywords, with each token treated as a separate keyword. The order * of tokens doesn't matter in the source tuple.
*

* For example:
* query "book appointment with the doctor"
* matches: "book appointment"
* also matches: "an appointment for a book"
*
* * @author Zuozhi Wang */ public enum MatchingType { SCANBASED("Scan"), SUBSTRING("Substring"), CONJUNCTION_INDEXBASED("Conjunction"); private final String name; private MatchingType(String name) { this.name = name; } // use the name string instead of enum string in JSON @JsonValue public String getName() { return this.name; } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/difference/DifferenceOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.difference import com.google.common.base.Preconditions import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow._ import org.apache.texera.amber.operator.LogicalOp import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} class DifferenceOpDesc extends LogicalOp { override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName("org.apache.texera.amber.operator.difference.DifferenceOpExec") ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPartitionRequirement(List(Option(HashPartition()), Option(HashPartition()))) .withDerivePartition(_ => HashPartition()) .withPropagateSchema(SchemaPropagationFunc(inputSchemas => { Preconditions.checkArgument(inputSchemas.values.toSet.size == 1) val outputSchema = inputSchemas.values.head operatorInfo.outputPorts.map(port => port.id -> outputSchema).toMap })) } override def operatorInfo: OperatorInfo = OperatorInfo( "Difference", "find the set difference of two inputs", OperatorGroupConstants.SET_GROUP, inputPorts = List( InputPort(PortIdentity(), displayName = "left"), InputPort(PortIdentity(1), displayName = "right") ), outputPorts = List(OutputPort(blocking = true)) ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/difference/DifferenceOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.difference import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} import scala.collection.mutable class DifferenceOpExec extends OperatorExecutor { private var leftHashSet: mutable.HashSet[Tuple] = _ private var rightHashSet: mutable.HashSet[Tuple] = _ private var exhaustedCounter: Int = _ override def open(): Unit = { leftHashSet = new mutable.HashSet() rightHashSet = new mutable.HashSet() exhaustedCounter = 0 } override def close(): Unit = { leftHashSet.clear() rightHashSet.clear() } override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = { if (port == 1) { // right input rightHashSet.add(tuple) } else { // left input leftHashSet.add(tuple) } Iterator() } override def onFinish(port: Int): Iterator[TupleLike] = { exhaustedCounter += 1 if (2 == exhaustedCounter) { leftHashSet.diff(rightHashSet).iterator } else { Iterator() } } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/distinct/DistinctOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.distinct import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{HashPartition, InputPort, OutputPort, PhysicalOp} import org.apache.texera.amber.operator.LogicalOp import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} class DistinctOpDesc extends LogicalOp { override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName("org.apache.texera.amber.operator.distinct.DistinctOpExec") ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPartitionRequirement(List(Option(HashPartition()))) .withDerivePartition(_ => HashPartition()) } override def operatorInfo: OperatorInfo = OperatorInfo( "Distinct", "Remove duplicate tuples", OperatorGroupConstants.CLEANING_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort(blocking = true)) ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/distinct/DistinctOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.distinct import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} import scala.collection.mutable /** * An executor for the distinct operation that filters out duplicate tuples. * It uses a `LinkedHashSet` to preserve the input order while removing duplicates. */ class DistinctOpExec extends OperatorExecutor { private var seenTuples: mutable.LinkedHashSet[Tuple] = _ override def open(): Unit = { seenTuples = mutable.LinkedHashSet() } override def close(): Unit = { seenTuples.clear() } override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = { seenTuples.add(tuple) Iterator.empty } override def onFinish(port: Int): Iterator[TupleLike] = { seenTuples.iterator } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/dummy/DummyOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.dummy import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity} import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.operator.{LogicalOp, PortDescription, PortDescriptor} class DummyOpDesc extends LogicalOp with PortDescriptor { @JsonProperty @JsonSchemaTitle("Description") @JsonPropertyDescription("The description of this dummy operator") var dummyOperator: String = "" override def operatorInfo: OperatorInfo = { val inputPortInfo = if (inputPorts != null) { inputPorts.zipWithIndex.map { case (portDesc: PortDescription, idx) => InputPort( PortIdentity(idx), displayName = portDesc.displayName, disallowMultiLinks = portDesc.disallowMultiInputs, dependencies = portDesc.dependencies.map(idx => PortIdentity(idx)) ) } } else { List(InputPort()) } val outputPortInfo = if (outputPorts != null) { outputPorts.zipWithIndex.map { case (portDesc, idx) => OutputPort(PortIdentity(idx), displayName = portDesc.displayName) } } else { List(OutputPort()) } OperatorInfo( "Dummy", "A dummy operator used as a placeholder.", OperatorGroupConstants.UTILITY_GROUP, inputPortInfo, outputPortInfo, dynamicInputPorts = true, dynamicOutputPorts = true, supportReconfiguration = true, allowPortCustomization = true ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/filter/ComparisonType.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.filter; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; public enum ComparisonType { EQUAL_TO("="), GREATER_THAN(">"), GREATER_THAN_OR_EQUAL_TO(">="), LESS_THAN("<"), LESS_THAN_OR_EQUAL_TO("<="), NOT_EQUAL_TO("!="), IS_NULL("is null"), IS_NOT_NULL("is not null"); private final String name; private ComparisonType(String name) { this.name = name; } // use the name string instead of enum string in JSON @JsonValue public String getName() { return this.name; } // Handle custom deserialization for enum @JsonCreator public static ComparisonType fromString(String value) { for (ComparisonType type : ComparisonType.values()) { if (type.name.equalsIgnoreCase(value)) { return type; } } throw new IllegalArgumentException("Unknown comparison type: " + value); } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/filter/FilterOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.filter import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.PhysicalOp import org.apache.texera.amber.operator.{LogicalOp, StateTransferFunc} import scala.util.{Success, Try} abstract class FilterOpDesc extends LogicalOp { override def runtimeReconfiguration( workflowId: WorkflowIdentity, executionId: ExecutionIdentity, oldOpDesc: LogicalOp, newOpDesc: LogicalOp ): Try[(PhysicalOp, Option[StateTransferFunc])] = { Success(newOpDesc.getPhysicalOp(workflowId, executionId), None) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/filter/FilterOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.filter import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} abstract class FilterOpExec extends OperatorExecutor with Serializable { var filterFunc: Tuple => Boolean = _ def setFilterFunc(func: Tuple => Boolean): Unit = filterFunc = func override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = if (filterFunc(tuple)) Iterator.single(tuple) else Iterator.empty } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/filter/FilterPredicate.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.filter; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaString; import org.apache.texera.amber.core.tuple.AttributeType; import org.apache.texera.amber.core.tuple.AttributeTypeUtils; import org.apache.texera.amber.core.tuple.Tuple; import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName; import org.apache.texera.amber.operator.metadata.annotations.HideAnnotation; import java.sql.Timestamp; import java.util.Objects; public class FilterPredicate { @JsonProperty(value = "attribute", required = true) @AutofillAttributeName public String attribute; @JsonProperty(value = "condition", required = true) public ComparisonType condition; @JsonSchemaInject(strings = { @JsonSchemaString(path = HideAnnotation.hideTarget, value = "condition"), @JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.regex), @JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = "is null|is not null") }) @JsonProperty(value = "value") public String value; @JsonCreator public FilterPredicate( @JsonProperty("attribute") String attribute, @JsonProperty("condition") ComparisonType condition, @JsonProperty("value") String value ) { this.attribute = attribute; this.condition = condition; this.value = value; } private static > boolean evaluateFilter(T tupleValue, T userSuppliedValue, ComparisonType comparisonType) { int compareResult = tupleValue.compareTo(userSuppliedValue); switch (comparisonType) { case EQUAL_TO: return compareResult == 0; case GREATER_THAN: return compareResult > 0; case GREATER_THAN_OR_EQUAL_TO: return compareResult >= 0; case LESS_THAN: return compareResult < 0; case LESS_THAN_OR_EQUAL_TO: return compareResult <= 0; case NOT_EQUAL_TO: return compareResult != 0; default: throw new RuntimeException( "Unable to do comparison: unknown comparison type: " + comparisonType); } } @JsonIgnore public boolean evaluate(Tuple tuple) { boolean isFieldNull = tuple.getField(attribute) == null; if (condition == org.apache.texera.amber.operator.filter.ComparisonType.IS_NULL) { return isFieldNull; } else if (condition == org.apache.texera.amber.operator.filter.ComparisonType.IS_NOT_NULL) { return !isFieldNull; } else if (isFieldNull) { return false; } AttributeType type = tuple.getSchema().getAttribute(this.attribute).getType(); switch (type) { case STRING: case ANY: return evaluateFilterString(tuple); case BOOLEAN: return evaluateFilterBoolean(tuple); case LONG: return evaluateFilterLong(tuple); case INTEGER: return evaluateFilterInt(tuple); case DOUBLE: return evaluateFilterDouble(tuple); case TIMESTAMP: return evaluateFilterTimestamp(tuple); default: throw new RuntimeException("unsupported attribute type: " + type); } } private boolean evaluateFilterBoolean(Tuple inputTuple) { Boolean tupleValue = inputTuple.getField(attribute); return evaluateFilter(tupleValue.toString().toLowerCase(), value.trim().toLowerCase(), condition); } private boolean evaluateFilterDouble(Tuple inputTuple) { Double tupleValue = inputTuple.getField(attribute); Double compareToValue = Double.parseDouble(value); return evaluateFilter(tupleValue, compareToValue, condition); } private boolean evaluateFilterInt(Tuple inputTuple) { Integer tupleValueInt = inputTuple.getField(attribute); Double tupleValueDouble = tupleValueInt == null ? null : (double) tupleValueInt; Double compareToValue = Double.parseDouble(value); return evaluateFilter(tupleValueDouble, compareToValue, condition); } private boolean evaluateFilterLong(Tuple inputTuple) { Long tupleValue = inputTuple.getField(attribute); Long compareToValue = Long.valueOf(value.trim()); return evaluateFilter(tupleValue, compareToValue, condition); } private boolean evaluateFilterString(Tuple inputTuple) { String tupleValue = inputTuple.getField(attribute).toString(); try { Double tupleValueDouble = tupleValue == null ? null : Double.parseDouble(tupleValue.trim()); Double compareToValueDouble = Double.parseDouble(value); return evaluateFilter(tupleValueDouble, compareToValueDouble, condition); } catch (NumberFormatException e) { return evaluateFilter(tupleValue, value, condition); } } private boolean evaluateFilterTimestamp(Tuple inputTuple) { Timestamp ts = inputTuple.getField(attribute); Long tupleValue = ts.getTime(); Long compareToValue = AttributeTypeUtils.parseTimestamp(value.trim()).getTime(); return evaluateFilter(tupleValue, compareToValue, condition); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } FilterPredicate that = (FilterPredicate) o; return Objects.equals(attribute, that.attribute) && condition == that.condition && Objects.equals(value, that.value); } @Override public int hashCode() { return Objects.hash(attribute, condition, value); } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/filter/SpecializedFilterOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.filter import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PhysicalOp} import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.util.JSONUtils.objectMapper class SpecializedFilterOpDesc extends FilterOpDesc { @JsonProperty(value = "predicates", required = true) @JsonPropertyDescription("multiple predicates in OR") var predicates: List[FilterPredicate] = List.empty override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.filter.SpecializedFilterOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) } override def operatorInfo: OperatorInfo = { OperatorInfo( "Filter", "Performs a filter operation using OR between multiple predicates", OperatorGroupConstants.CLEANING_GROUP, List(InputPort()), List(OutputPort()), supportReconfiguration = true ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/filter/SpecializedFilterOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.filter import org.apache.texera.amber.core.tuple.Tuple import org.apache.texera.amber.util.JSONUtils.objectMapper class SpecializedFilterOpExec(descString: String) extends FilterOpExec { private val desc: SpecializedFilterOpDesc = objectMapper.readValue(descString, classOf[SpecializedFilterOpDesc]) setFilterFunc((tuple: Tuple) => desc.predicates.exists(_.evaluate(tuple))) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/flatmap/FlatMapOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.flatmap import org.apache.texera.amber.operator.LogicalOp abstract class FlatMapOpDesc extends LogicalOp {} ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/flatmap/FlatMapOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.flatmap import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} /** * Executes a flatMap() operation. * This operation takes a single input Tuple, flattens it, applies a mapping function to each element, * and produces an output Tuple for each element. */ class FlatMapOpExec extends OperatorExecutor with Serializable { var flatMapFunc: Tuple => Iterator[TupleLike] = _ def setFlatMapFunc(func: Tuple => Iterator[TupleLike]): Unit = flatMapFunc = func override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = flatMapFunc(tuple) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/hashJoin/HashJoinBuildOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.hashJoin import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} import org.apache.texera.amber.util.JSONUtils.objectMapper import scala.collection.mutable import scala.collection.mutable.ListBuffer class HashJoinBuildOpExec[K](descString: String) extends OperatorExecutor { private val desc: HashJoinOpDesc[K] = objectMapper.readValue(descString, classOf[HashJoinOpDesc[K]]) var buildTableHashMap: mutable.HashMap[K, ListBuffer[Tuple]] = _ override def open(): Unit = { buildTableHashMap = new mutable.HashMap[K, mutable.ListBuffer[Tuple]]() } override def close(): Unit = { buildTableHashMap.clear() } override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = { val key = tuple.getField(desc.buildAttributeName).asInstanceOf[K] buildTableHashMap.getOrElseUpdate(key, new ListBuffer[Tuple]()) += tuple Iterator() } override def onFinish(port: Int): Iterator[TupleLike] = { buildTableHashMap.iterator.flatMap { case (k, v) => v.map(t => TupleLike(List(k) ++ t.getFields)) } } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/hashJoin/HashJoinOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.hashJoin import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.tuple.{Attribute, Schema} import org.apache.texera.amber.core.virtualidentity.{ ExecutionIdentity, PhysicalOpIdentity, WorkflowIdentity } import org.apache.texera.amber.core.workflow._ import org.apache.texera.amber.operator.LogicalOp import org.apache.texera.amber.operator.hashJoin.HashJoinOpDesc.HASH_JOIN_INTERNAL_KEY_NAME import org.apache.texera.amber.operator.metadata.annotations.{ AutofillAttributeName, AutofillAttributeNameOnPort1 } import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.util.JSONUtils.objectMapper object HashJoinOpDesc { val HASH_JOIN_INTERNAL_KEY_NAME = "__internal__hashtable__key__" } @JsonSchemaInject(json = """ { "attributeTypeRules": { "buildAttributeName": { "const": { "$data": "probeAttributeName" } } } } """) class HashJoinOpDesc[K] extends LogicalOp { @JsonProperty(required = true) @JsonSchemaTitle("Left Input Attribute") @JsonPropertyDescription("attribute to be joined on the Left Input") @AutofillAttributeName var buildAttributeName: String = _ @JsonProperty(required = true) @JsonSchemaTitle("Right Input Attribute") @JsonPropertyDescription("attribute to be joined on the Right Input") @AutofillAttributeNameOnPort1 var probeAttributeName: String = _ @JsonProperty(required = true, defaultValue = "inner") @JsonSchemaTitle("Join Type") @JsonPropertyDescription("select the join type to execute") var joinType: JoinType = JoinType.INNER override def getPhysicalPlan( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalPlan = { val buildInputPort = operatorInfo.inputPorts.head val buildOutputPort = OutputPort(PortIdentity(0, internal = true), blocking = true) val buildPhysicalOp = PhysicalOp .oneToOnePhysicalOp( PhysicalOpIdentity(operatorIdentifier, "build"), workflowId, executionId, OpExecWithClassName( "org.apache.texera.amber.operator.hashJoin.HashJoinBuildOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(List(buildInputPort)) .withOutputPorts(List(buildOutputPort)) .withPartitionRequirement(List(Option(HashPartition(List(buildAttributeName))))) .withPropagateSchema( SchemaPropagationFunc(inputSchemas => Map( PortIdentity(internal = true) -> Schema( List( new Attribute( HASH_JOIN_INTERNAL_KEY_NAME, // Because we need to materialize the outputs of build, we cannot use ANY type. inputSchemas(operatorInfo.inputPorts.head.id) .getAttribute(buildAttributeName) .getType ) ) ).add(inputSchemas(operatorInfo.inputPorts.head.id)) ) ) ) .withParallelizable(true) val probeBuildInputPort = InputPort(PortIdentity(0, internal = true)) val probeDataInputPort = InputPort(operatorInfo.inputPorts(1).id, dependencies = List(probeBuildInputPort.id)) val probeOutputPort = OutputPort(PortIdentity(0)) val probePhysicalOp = PhysicalOp .oneToOnePhysicalOp( PhysicalOpIdentity(operatorIdentifier, "probe"), workflowId, executionId, OpExecWithClassName( "org.apache.texera.amber.operator.hashJoin.HashJoinProbeOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts( List( probeBuildInputPort, probeDataInputPort ) ) .withOutputPorts(List(probeOutputPort)) .withPartitionRequirement( List( // Cannot use OneToOnePartition because it does not work with InputPortMaterializationReaderThreads. Option(HashPartition(List(buildAttributeName))), Option(HashPartition(List(probeAttributeName))) ) ) .withDerivePartition(_ => HashPartition(List(probeAttributeName))) .withParallelizable(true) .withPropagateSchema( SchemaPropagationFunc(inputSchemas => { val buildSchema = inputSchemas(PortIdentity(internal = true)) val probeSchema = inputSchemas(PortIdentity(1)) // Start with the attributes from the build schema, excluding the hash join internal key val leftAttributes = buildSchema.getAttributes.filterNot(_.getName == HASH_JOIN_INTERNAL_KEY_NAME) val leftAttributeNames = leftAttributes.map(_.getName).toSet // Filter and rename attributes from the probe schema to avoid conflicts val rightAttributes = probeSchema.getAttributes .filterNot(_.getName == probeAttributeName) .map { attr => var newName = attr.getName while (leftAttributeNames.contains(newName)) { val suffixIndex = """#@(\d+)$""".r .findFirstMatchIn(newName) .map(_.group(1).toInt + 1) .getOrElse(1) newName = s"${attr.getName}#@$suffixIndex" } new Attribute(newName, attr.getType) } // Combine left and right attributes into a new schema val outputSchema = Schema(leftAttributes ++ rightAttributes) Map(PortIdentity() -> outputSchema) }) ) PhysicalPlan( operators = Set(buildPhysicalOp, probePhysicalOp), links = Set( PhysicalLink( buildPhysicalOp.id, buildOutputPort.id, probePhysicalOp.id, probeBuildInputPort.id ) ) ) } override def operatorInfo: OperatorInfo = OperatorInfo( "Hash Join", "join two inputs", OperatorGroupConstants.JOIN_GROUP, inputPorts = List( InputPort(PortIdentity(0), displayName = "left"), InputPort(PortIdentity(1), displayName = "right", dependencies = List(PortIdentity(0))) ), outputPorts = List(OutputPort()) ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/hashJoin/HashJoinProbeOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.hashJoin import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} import org.apache.texera.amber.operator.hashJoin.HashJoinOpDesc.HASH_JOIN_INTERNAL_KEY_NAME import org.apache.texera.amber.util.JSONUtils.objectMapper import scala.collection.mutable import scala.collection.mutable.ListBuffer object JoinUtils { def joinTuples( leftTuple: Tuple, rightTuple: Tuple, skipAttributeName: Option[String] = None ): TupleLike = { val leftAttributeNames = leftTuple.getSchema.getAttributeNames val rightAttributeNames = rightTuple.getSchema.getAttributeNames.filterNot(name => skipAttributeName.isDefined && name == skipAttributeName.get ) // Create a Map from leftTuple's fields val leftTupleFields: Map[String, Any] = leftAttributeNames .map(name => name -> leftTuple.getField(name)) .toMap // Create a Map from rightTuple's fields, renaming conflicts val rightTupleFields = rightAttributeNames .map { name => var newName = name while ( leftAttributeNames.contains(newName) || rightAttributeNames .filter(attrName => name != attrName) .contains(newName) ) { newName = s"$newName#@1" } newName -> rightTuple.getField[Any](name) } TupleLike(leftTupleFields ++ rightTupleFields) } } class HashJoinProbeOpExec[K]( descString: String ) extends OperatorExecutor { private val desc: HashJoinOpDesc[K] = objectMapper.readValue(descString, classOf[HashJoinOpDesc[K]]) var buildTableHashMap: mutable.HashMap[K, (ListBuffer[Tuple], Boolean)] = _ override def open(): Unit = { buildTableHashMap = new mutable.HashMap[K, (mutable.ListBuffer[Tuple], Boolean)]() } override def close(): Unit = { buildTableHashMap.clear() } override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = if (port == 0) { // Load build hash map val key = tuple.getField[K](HASH_JOIN_INTERNAL_KEY_NAME) buildTableHashMap.getOrElseUpdate(key, (new ListBuffer[Tuple](), false))._1 += tuple .getPartialTuple( tuple.getSchema.getAttributeNames.filterNot(n => n == HASH_JOIN_INTERNAL_KEY_NAME) ) Iterator.empty } else { // Probe phase val key = tuple.getField(desc.probeAttributeName).asInstanceOf[K] val (matchedTuples, joined) = buildTableHashMap.getOrElse(key, (new ListBuffer[Tuple](), false)) if (matchedTuples.nonEmpty) { // Join match found buildTableHashMap.put(key, (matchedTuples, true)) performJoin(tuple, matchedTuples) } else if (desc.joinType == JoinType.RIGHT_OUTER || desc.joinType == JoinType.FULL_OUTER) { // Handle right and full outer joins without a match performRightAntiJoin(tuple) } else { // No match found Iterator.empty } } override def onFinish(port: Int): Iterator[TupleLike] = { if ( port == 1 && (desc.joinType == JoinType.LEFT_OUTER || desc.joinType == JoinType.FULL_OUTER) ) { // Handle left and full outer joins after input is exhausted performLeftAntiJoin } else { Iterator.empty } } private def performLeftAntiJoin: Iterator[TupleLike] = { buildTableHashMap.valuesIterator .collect { case (tuples: ListBuffer[Tuple], joined: Boolean) if !joined => tuples } .flatMap { tuples => tuples.map { tuple => TupleLike( tuple.getSchema.getAttributeNames .map(attributeName => attributeName -> tuple.getField(attributeName)): _* ) } } } private def performJoin( probeTuple: Tuple, matchedTuples: ListBuffer[Tuple] ): Iterator[TupleLike] = { matchedTuples.iterator.map { buildTuple => JoinUtils.joinTuples( buildTuple, probeTuple, skipAttributeName = Some(desc.probeAttributeName) ) } } private def performRightAntiJoin(tuple: Tuple): Iterator[TupleLike] = Iterator( TupleLike( tuple.getSchema.getAttributeNames .map(attributeName => attributeName -> tuple.getField(attributeName)): _* ) ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/hashJoin/JoinType.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.hashJoin; import com.fasterxml.jackson.annotation.JsonValue; public enum JoinType { INNER("inner"), LEFT_OUTER("left outer"), RIGHT_OUTER("right outer"), FULL_OUTER("full outer"); private final String value; JoinType(String value) { this.value = value; } @JsonValue public String getJoinType() { return this.value; } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/HuggingFaceIrisLogisticRegressionOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.huggingFace import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity} import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} class HuggingFaceIrisLogisticRegressionOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "petalLengthCmAttribute", required = true) @JsonPropertyDescription("attribute in your dataset corresponding to PetalLengthCm") @AutofillAttributeName var petalLengthCmAttribute: EncodableString = _ @JsonProperty(value = "petalWidthCmAttribute", required = true) @JsonPropertyDescription("attribute in your dataset corresponding to PetalWidthCm") @AutofillAttributeName var petalWidthCmAttribute: EncodableString = _ @JsonProperty( value = "prediction class name", required = true, defaultValue = "Species_prediction" ) @JsonPropertyDescription("output attribute name for the predicted class of species") var predictionClassName: EncodableString = _ @JsonProperty( value = "prediction probability name", required = true, defaultValue = "Species_probability" ) @JsonPropertyDescription( "output attribute name for the prediction's probability of being a Iris-setosa" ) var predictionProbabilityName: EncodableString = _ /** * Python code to apply a pre-trained liner regression model on the Iris dataset. * For more info about the model, see https://huggingface.co/sadhaklal/logistic-regression-iris. * * @return a String representation of the executable Python source code. */ override def generatePythonCode(): String = { pyb"""from pytexera import * |import numpy as np |import torch |import torch.nn as nn |from huggingface_hub import PyTorchModelHubMixin | |class ProcessTupleOperator(UDFOperatorV2): | def open(self): | class LinearModel(nn.Module, PyTorchModelHubMixin): | def __init__(self): | super().__init__() | self.fc = nn.Linear(2, 1) | | def forward(self, x): | out = self.fc(x) | return out | | self.model = LinearModel.from_pretrained("sadhaklal/logistic-regression-iris") | self.model.eval() | | @overrides | def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]: | training_features_means = [3.72666667, 1.17619048] | training_features_stds = [1.72528903, 0.73788937] | length = tuple_[$petalLengthCmAttribute] | width = tuple_[$petalWidthCmAttribute] | features = np.array([[length, width]]) | features = ((features - training_features_means) / training_features_stds) | features = torch.from_numpy(features).float() | with torch.no_grad(): | logits = self.model(features) | proba = torch.sigmoid(logits.squeeze()) | preds = (proba > 0.5).long() | tuple_[$predictionProbabilityName] = float(proba) | tuple_[$predictionClassName] = "Iris-setosa" if preds == 1 else "Not Iris-setosa" | yield tuple_""".encode } override def operatorInfo: OperatorInfo = OperatorInfo( "Hugging Face Iris Logistic Regression", "Predict whether an iris is an Iris-setosa using a pre-trained logistic regression model", OperatorGroupConstants.HUGGINGFACE_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort()) ) override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { if ( predictionClassName == null || predictionClassName.trim.isEmpty || predictionProbabilityName == null || predictionProbabilityName.trim.isEmpty ) throw new RuntimeException("Result attribute name should not be empty") Map( operatorInfo.outputPorts.head.id -> inputSchemas(operatorInfo.inputPorts.head.id) .add(predictionClassName, AttributeType.STRING) .add(predictionProbabilityName, AttributeType.DOUBLE) ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/HuggingFaceSentimentAnalysisOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.huggingFace import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity} import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext class HuggingFaceSentimentAnalysisOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "attribute", required = true) @JsonPropertyDescription("column to perform sentiment analysis on") @AutofillAttributeName var attribute: EncodableString = _ @JsonProperty( value = "Positive result attribute", required = true, defaultValue = "huggingface_sentiment_positive" ) @JsonPropertyDescription("column name of the sentiment analysis result (positive)") var resultAttributePositive: EncodableString = _ @JsonProperty( value = "Neutral result attribute", required = true, defaultValue = "huggingface_sentiment_neutral" ) @JsonPropertyDescription("column name of the sentiment analysis result (neutral)") var resultAttributeNeutral: EncodableString = _ @JsonProperty( value = "Negative result attribute", required = true, defaultValue = "huggingface_sentiment_negative" ) @JsonPropertyDescription("column name of the sentiment analysis result (negative)") var resultAttributeNegative: EncodableString = _ override def generatePythonCode(): String = { pyb"""from pytexera import * |from transformers import pipeline |from transformers import AutoModelForSequenceClassification |from transformers import TFAutoModelForSequenceClassification |from transformers import AutoTokenizer, AutoConfig |import numpy as np |from scipy.special import softmax | |class ProcessTupleOperator(UDFOperatorV2): | | def open(self): | model_name = "cardiffnlp/twitter-roberta-base-sentiment-latest" | self.tokenizer = AutoTokenizer.from_pretrained(model_name) | self.config = AutoConfig.from_pretrained(model_name) | self.model = AutoModelForSequenceClassification.from_pretrained(model_name) | | @overrides | def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]: | encoded_input = self.tokenizer(tuple_[$attribute], return_tensors='pt') | output = self.model(**encoded_input) | scores = softmax(output[0][0].detach().numpy()) | ranking = np.argsort(scores)[::-1] | labels = {"positive": $resultAttributePositive, "neutral": $resultAttributeNeutral, "negative": $resultAttributeNegative} | for i in range(scores.shape[0]): | label = labels[self.config.id2label[ranking[i]]] | score = scores[ranking[i]] | tuple_[label] = np.round(float(score), 4) | yield tuple_""".encode } override def operatorInfo: OperatorInfo = OperatorInfo( "Hugging Face Sentiment Analysis", "Analyzing Sentiments with a Twitter-Based Model from Hugging Face", OperatorGroupConstants.HUGGINGFACE_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort()), supportReconfiguration = true ) override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { if ( resultAttributePositive == null || resultAttributePositive.trim.isEmpty || resultAttributeNeutral == null || resultAttributeNeutral.trim.isEmpty || resultAttributeNegative == null || resultAttributeNegative.trim.isEmpty ) return null Map( operatorInfo.outputPorts.head.id -> inputSchemas(operatorInfo.inputPorts.head.id) .add(resultAttributePositive, AttributeType.DOUBLE) .add(resultAttributeNeutral, AttributeType.DOUBLE) .add(resultAttributeNegative, AttributeType.DOUBLE) ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/HuggingFaceSpamSMSDetectionOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.huggingFace import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity} import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext class HuggingFaceSpamSMSDetectionOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "attribute", required = true) @JsonPropertyDescription("column to perform spam detection on") @AutofillAttributeName var attribute: EncodableString = _ @JsonProperty( value = "Spam result attribute", required = true, defaultValue = "is_spam" ) @JsonPropertyDescription("column name of whether spam or not") var resultAttributeSpam: EncodableString = _ @JsonProperty( value = "Score result attribute", required = true, defaultValue = "score" ) @JsonPropertyDescription("column name of Probability for classification") var resultAttributeProbability: EncodableString = _ override def generatePythonCode(): String = { pyb"""from transformers import pipeline |from pytexera import * | |class ProcessTupleOperator(UDFOperatorV2): | | def open(self): | self.pipeline = pipeline("text-classification", model="mrm8488/bert-tiny-finetuned-sms-spam-detection") | | @overrides | def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]: | result = self.pipeline(tuple_[$attribute])[0] | tuple_[$resultAttributeSpam] = (result["label"] == "LABEL_1") | tuple_[$resultAttributeProbability] = result["score"] | yield tuple_""".encode } override def operatorInfo: OperatorInfo = OperatorInfo( "Hugging Face Spam Detection", "Spam Detection by SMS Spam Detection Model from Hugging Face", OperatorGroupConstants.HUGGINGFACE_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort()) ) override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { Map( operatorInfo.outputPorts.head.id -> inputSchemas.values.head .add(resultAttributeSpam, AttributeType.BOOLEAN) .add(resultAttributeProbability, AttributeType.DOUBLE) ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/HuggingFaceTextSummarizationOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.huggingFace import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity} import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext class HuggingFaceTextSummarizationOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "attribute", required = true) @JsonPropertyDescription("attribute to perform text summarization on") @AutofillAttributeName var attribute: EncodableString = _ @JsonProperty( value = "Result attribute name", required = false, defaultValue = "summary" ) @JsonPropertyDescription("attribute name of the text summary result") var resultAttribute: EncodableString = _ override def generatePythonCode(): String = { pyb""" |from transformers import BertTokenizerFast, EncoderDecoderModel |import torch |from pytexera import * | |class ProcessTupleOperator(UDFOperatorV2): | | def open(self): | model_name = "mrm8488/bert-mini2bert-mini-finetuned-cnn_daily_mail-summarization" | self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') | self.tokenizer = BertTokenizerFast.from_pretrained(model_name) | self.model = EncoderDecoderModel.from_pretrained(model_name).to(self.device) | | @overrides | def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]: | text = tuple_[$attribute] | | inputs = self.tokenizer([text], padding="max_length", truncation=True, max_length=512, return_tensors="pt") | input_ids = inputs.input_ids.to(self.device) | attention_mask = inputs.attention_mask.to(self.device) | | output = self.model.generate(input_ids, attention_mask=attention_mask) | summary = self.tokenizer.decode(output[0], skip_special_tokens=True) | tuple_[$resultAttribute] = summary | yield tuple_""".encode } override def operatorInfo: OperatorInfo = OperatorInfo( "Hugging Face Text Summarization", "Summarize the given text content with a mini2bert pre-trained model from Hugging Face", OperatorGroupConstants.HUGGINGFACE_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort()) ) override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { if (resultAttribute == null || resultAttribute.trim.isEmpty) throw new RuntimeException("Result attribute name should be given") Map( operatorInfo.outputPorts.head.id -> inputSchemas.values.head .add(resultAttribute, AttributeType.STRING) ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/ifStatement/IfOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.ifStatement import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow._ import org.apache.texera.amber.operator.LogicalOp import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.util.JSONUtils.objectMapper class IfOpDesc extends LogicalOp { @JsonProperty(required = true) @JsonSchemaTitle("Condition State") @JsonPropertyDescription("name of the state variable to evaluate") var conditionName: String = _ override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.ifStatement.IfOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withParallelizable(false) .withPropagateSchema( SchemaPropagationFunc(inputSchemas => operatorInfo.outputPorts .map(_.id) .map(id => id -> inputSchemas(operatorInfo.inputPorts.last.id)) .toMap ) ) } override def operatorInfo: OperatorInfo = OperatorInfo( "If", "If", OperatorGroupConstants.CONTROL_GROUP, inputPorts = List( InputPort(PortIdentity(), "Condition"), InputPort(PortIdentity(1), dependencies = List(PortIdentity())) ), outputPorts = List(OutputPort(PortIdentity(), "False"), OutputPort(PortIdentity(1), "True")) ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/ifStatement/IfOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.ifStatement import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.state.State import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.util.JSONUtils.objectMapper class IfOpExec(descString: String) extends OperatorExecutor { private val desc: IfOpDesc = objectMapper.readValue(descString, classOf[IfOpDesc]) private var outputPort: PortIdentity = PortIdentity(1) // by default, it should be the true port. //This function can handle one or more states. //The state can have mutiple key-value pairs. Keys are not identified by conditionName will be ignored. //It can accept any value that can be converted to a boolean. For example, Int 1 will be converted to true. override def processState(state: State, port: Int): Option[State] = { outputPort = if (state.values(desc.conditionName).asInstanceOf[Boolean]) PortIdentity(1) else PortIdentity() Some(state) } override def processTupleMultiPort( tuple: Tuple, port: Int ): Iterator[(TupleLike, Option[PortIdentity])] = Iterator((tuple, Some(outputPort))) override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = ??? } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/intersect/IntersectOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.intersect import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow._ import org.apache.texera.amber.operator.LogicalOp import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} class IntersectOpDesc extends LogicalOp { override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName("org.apache.texera.amber.operator.intersect.IntersectOpExec") ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPartitionRequirement(List(Option(HashPartition()), Option(HashPartition()))) .withDerivePartition(_ => HashPartition()) } override def operatorInfo: OperatorInfo = OperatorInfo( "Intersect", "Take the intersect of two inputs", OperatorGroupConstants.SET_GROUP, inputPorts = List(InputPort(PortIdentity()), InputPort(PortIdentity(1))), outputPorts = List(OutputPort(blocking = true)) ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/intersect/IntersectOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.intersect import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} import scala.collection.mutable class IntersectOpExec extends OperatorExecutor { private var leftSet: mutable.HashSet[Tuple] = _ private var rightSet: mutable.HashSet[Tuple] = _ private var exhaustedCounter: Int = _ override def open(): Unit = { leftSet = new mutable.HashSet[Tuple]() rightSet = new mutable.HashSet[Tuple]() exhaustedCounter = 0 } override def close(): Unit = { leftSet.clear() rightSet.clear() } override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = { // add the tuple to corresponding set if (port == 0) leftSet += tuple else rightSet += tuple Iterator() } override def onFinish(port: Int): Iterator[TupleLike] = { exhaustedCounter += 1 if (exhaustedCounter == 2) { // both streams are exhausted, take the intersect and return the results leftSet.intersect(rightSet).iterator } else { // only one of the stream is exhausted, continue accepting tuples Iterator() } } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/intervalJoin/IntervalJoinOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.intervalJoin import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.tuple.{Attribute, Schema} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow._ import org.apache.texera.amber.operator.LogicalOp import org.apache.texera.amber.operator.metadata.annotations.{ AutofillAttributeName, AutofillAttributeNameOnPort1 } import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.util.JSONUtils.objectMapper /** This Operator have two assumptions: * 1. The tuples in both inputs come in ascending order * 2. The left input join key takes as points, join condition is: left key in the range of (right key, right key + constant) */ @JsonSchemaInject(json = """ { "attributeTypeRules": { "leftAttributeName": { "enum": ["integer", "long", "double", "timestamp"] }, "rightAttributeName": { "const": { "$data": "leftAttributeName" } } } } """) class IntervalJoinOpDesc extends LogicalOp { @JsonProperty(required = true) @JsonSchemaTitle("Left Input attr") @JsonPropertyDescription("Choose one attribute in the left table") @AutofillAttributeName var leftAttributeName: String = _ @JsonProperty(required = true) @JsonSchemaTitle("Right Input attr") @JsonPropertyDescription("Choose one attribute in the right table") @AutofillAttributeNameOnPort1 var rightAttributeName: String = _ @JsonProperty(required = true, defaultValue = "10") @JsonSchemaTitle("Interval Constant") @JsonPropertyDescription("left attri in (right, right + constant)") var constant: Long = 10 @JsonProperty(required = true, defaultValue = "true") @JsonSchemaTitle("Include Left Bound") @JsonPropertyDescription("Include condition left attri = right attri") var includeLeftBound: Boolean = true @JsonProperty(required = true, defaultValue = "true") @JsonSchemaTitle("Include Right Bound") @JsonPropertyDescription("Include condition left attri = right attri") var includeRightBound: Boolean = true @JsonDeserialize(contentAs = classOf[TimeIntervalType]) @JsonProperty(defaultValue = "day", required = false) @JsonSchemaTitle("Time interval type") @JsonPropertyDescription("Year, Month, Day, Hour, Minute or Second") var timeIntervalType: Option[TimeIntervalType] = _ override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { val partitionRequirement = List( Option(HashPartition(List(leftAttributeName))), Option(HashPartition(List(rightAttributeName))) ) PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.intervalJoin.IntervalJoinOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPropagateSchema( SchemaPropagationFunc(inputSchemas => { val leftTableSchema: Schema = inputSchemas(operatorInfo.inputPorts.head.id) val rightTableSchema: Schema = inputSchemas(operatorInfo.inputPorts.last.id) // Start with the left table schema val outputSchema = rightTableSchema.getAttributes.foldLeft(leftTableSchema) { (currentSchema, attr) => if (currentSchema.containsAttribute(attr.getName)) { // Add the attribute with a suffix to avoid conflicts currentSchema.add(new Attribute(s"${attr.getName}#@1", attr.getType)) } else { // Add the attribute as is currentSchema.add(attr) } } Map(operatorInfo.outputPorts.head.id -> outputSchema) }) ) .withPartitionRequirement(partitionRequirement) } override def operatorInfo: OperatorInfo = OperatorInfo( "Interval Join", "Join two inputs with left table join key in the range of [right table join key, right table join key + constant value]", OperatorGroupConstants.JOIN_GROUP, inputPorts = List( InputPort(PortIdentity(), displayName = "left table"), InputPort( PortIdentity(1), displayName = "right table", dependencies = List(PortIdentity(0)) ) ), outputPorts = List(OutputPort()) ) def this( leftTableAttributeName: String, rightTableAttributeName: String, constant: Long, includeLeftBound: Boolean, includeRightBound: Boolean, timeIntervalType: TimeIntervalType ) = { this() // Calling primary constructor, and it is first line this.leftAttributeName = leftTableAttributeName this.rightAttributeName = rightTableAttributeName this.constant = constant this.includeLeftBound = includeLeftBound this.includeRightBound = includeRightBound this.timeIntervalType = Some(timeIntervalType) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/intervalJoin/IntervalJoinOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.intervalJoin import org.apache.texera.amber.core.WorkflowRuntimeException import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.tuple.{AttributeType, Tuple, TupleLike} import org.apache.texera.amber.operator.hashJoin.JoinUtils import org.apache.texera.amber.util.JSONUtils.objectMapper import java.sql.Timestamp import scala.collection.mutable.ListBuffer /** This Operator have two assumptions: * 1. The tuples in both inputs come in ascending order * 2. The left input join key takes as points, join condition is: left key in the range of (right key, right key + constant) */ class IntervalJoinOpExec(descString: String) extends OperatorExecutor { private val desc: IntervalJoinOpDesc = objectMapper.readValue(descString, classOf[IntervalJoinOpDesc]) private var leftTable: ListBuffer[Tuple] = _ private var rightTable: ListBuffer[Tuple] = _ override def open(): Unit = { leftTable = new ListBuffer[Tuple]() rightTable = new ListBuffer[Tuple]() } override def close(): Unit = { leftTable.clear() rightTable.clear() } override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = { if (port == 0) { leftTable += tuple if (rightTable.nonEmpty) { removeTooSmallTupleInRightCache(leftTable.head) rightTable .filter(rightTableTuple => { intervalCompare( tuple.getField(desc.leftAttributeName), rightTableTuple.getField(desc.rightAttributeName), rightTableTuple.getSchema .getAttribute(desc.rightAttributeName) .getType ) == 0 }) .map(rightTuple => JoinUtils.joinTuples(tuple, rightTuple)) .iterator } else { Iterator() } } else { rightTable += tuple if (leftTable.nonEmpty) { removeTooSmallTupleInLeftCache(rightTable.head) leftTable .filter(leftTableTuple => { intervalCompare( leftTableTuple.getField(desc.leftAttributeName), tuple.getField(desc.rightAttributeName), leftTableTuple.getSchema .getAttribute(desc.leftAttributeName) .getType ) == 0 }) .map(leftTuple => JoinUtils.joinTuples(leftTuple, tuple)) .iterator } else { Iterator() } } } //if right table has tuple smaller than smallest tuple in left table, delete it private def removeTooSmallTupleInRightCache(leftTableSmallestTuple: Tuple): Unit = { while (rightTable.nonEmpty) { if ( intervalCompare( leftTableSmallestTuple.getField(desc.leftAttributeName), rightTable.head.getField(desc.rightAttributeName), leftTableSmallestTuple.getSchema .getAttribute(desc.leftAttributeName) .getType ) > 0 ) { rightTable.remove(0) } else { return } } } //if left table has tuple smaller than smallest tuple in right table, delete it private def removeTooSmallTupleInLeftCache(rightTableSmallestTuple: Tuple): Unit = { while (leftTable.nonEmpty) { if ( intervalCompare( leftTable.head.getField(desc.leftAttributeName), rightTableSmallestTuple.getField(desc.rightAttributeName), rightTableSmallestTuple.getSchema .getAttribute(desc.rightAttributeName) .getType ) < 0 ) { leftTable.remove(0) } else { return } } } private def processNumValue[T]( pointValue: T, leftBoundValue: T, rightBoundValue: T )(implicit ev$1: T => Ordered[T]): Int = { if (desc.includeLeftBound && desc.includeRightBound) { if (pointValue >= leftBoundValue && pointValue <= rightBoundValue) 0 else if (pointValue < leftBoundValue) -1 else 1 } else if (desc.includeLeftBound && !desc.includeRightBound) { if (pointValue >= leftBoundValue && pointValue < rightBoundValue) 0 else if (pointValue < leftBoundValue) -1 else 1 } else if (!desc.includeLeftBound && desc.includeRightBound) { if (pointValue > leftBoundValue && pointValue <= rightBoundValue) 0 else if (pointValue <= leftBoundValue) -1 else 1 } else { if (pointValue > leftBoundValue && pointValue < rightBoundValue) 0 else if (pointValue <= leftBoundValue) -1 else 1 } } private def intervalCompare[K]( point: K, leftBound: K, dataType: AttributeType ): Int = { var result: Int = -2 if (dataType == AttributeType.LONG) { val pointValue: Long = point.asInstanceOf[Long] val leftBoundValue: Long = leftBound.asInstanceOf[Long] val constantValue: Long = desc.constant val rightBoundValue: Long = leftBoundValue + constantValue result = processNumValue[Long]( pointValue, leftBoundValue, rightBoundValue ) } else if (dataType == AttributeType.DOUBLE) { val pointValue: Double = point.asInstanceOf[Double] val leftBoundValue: Double = leftBound.asInstanceOf[Double] val constantValue: Double = desc.constant.asInstanceOf[Double] val rightBoundValue: Double = leftBoundValue + constantValue result = processNumValue[Double]( pointValue, leftBoundValue, rightBoundValue ) } else if (dataType == AttributeType.INTEGER) { val pointValue: Int = point.asInstanceOf[Int] val leftBoundValue: Int = leftBound.asInstanceOf[Int] val constantValue: Int = desc.constant.asInstanceOf[Int] val rightBoundValue: Int = leftBoundValue + constantValue result = processNumValue[Int]( pointValue, leftBoundValue, rightBoundValue ) } else if (dataType == AttributeType.TIMESTAMP) { val pointValue: Timestamp = point.asInstanceOf[Timestamp] val leftBoundValue: Timestamp = leftBound.asInstanceOf[Timestamp] val rightBoundValue: Timestamp = desc.timeIntervalType match { case Some(TimeIntervalType.YEAR) => Timestamp.valueOf(leftBoundValue.toLocalDateTime.plusYears(desc.constant)) case Some(TimeIntervalType.MONTH) => Timestamp.valueOf(leftBoundValue.toLocalDateTime.plusMonths(desc.constant)) case Some(TimeIntervalType.DAY) => Timestamp.valueOf(leftBoundValue.toLocalDateTime.plusDays(desc.constant)) case Some(TimeIntervalType.HOUR) => Timestamp.valueOf(leftBoundValue.toLocalDateTime.plusHours(desc.constant)) case Some(TimeIntervalType.MINUTE) => Timestamp.valueOf(leftBoundValue.toLocalDateTime.plusMinutes(desc.constant)) case Some(TimeIntervalType.SECOND) => Timestamp.valueOf(leftBoundValue.toLocalDateTime.plusSeconds(desc.constant)) case None => Timestamp.valueOf(leftBoundValue.toLocalDateTime.plusDays(desc.constant)) } result = processNumValue( pointValue.getTime, leftBoundValue.getTime, rightBoundValue.getTime ) } else { throw new WorkflowRuntimeException(s"The data type can not support comparison: $dataType") } result } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/intervalJoin/TimeIntervalType.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.intervalJoin; import com.fasterxml.jackson.annotation.JsonValue; import java.io.Serializable; public enum TimeIntervalType implements Serializable { YEAR("year"), MONTH("month"), DAY("day"), HOUR("hour"), MINUTE("minute"), SECOND("second"); private final String name; TimeIntervalType(String name) { this.name = name; } @JsonValue public String getName() { return this.name; } @Override public String toString() { return this.getName(); } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/keywordSearch/KeywordSearchOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.keywordSearch import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PhysicalOp} import org.apache.texera.amber.operator.filter.FilterOpDesc import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.util.JSONUtils.objectMapper class KeywordSearchOpDesc extends FilterOpDesc { @JsonProperty(required = true) @JsonSchemaTitle("attribute") @JsonPropertyDescription("column to search keyword on") @AutofillAttributeName var attribute: String = _ @JsonProperty(required = true) @JsonSchemaTitle("keywords") @JsonPropertyDescription("keywords") @JsonSchemaInject(json = """{"minLength": 1}""") var keyword: String = _ override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.keywordSearch.KeywordSearchOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) } override def operatorInfo: OperatorInfo = OperatorInfo( userFriendlyName = "Keyword Search", operatorDescription = "Search for keyword(s) in a string column", operatorGroupName = OperatorGroupConstants.SEARCH_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort()), supportReconfiguration = true ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/keywordSearch/KeywordSearchOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.keywordSearch import org.apache.texera.amber.core.tuple.Tuple import org.apache.texera.amber.operator.filter.FilterOpExec import org.apache.texera.amber.util.JSONUtils.objectMapper import org.apache.lucene.analysis.standard.StandardAnalyzer import org.apache.lucene.index.memory.MemoryIndex import org.apache.lucene.queryparser.classic.QueryParser import org.apache.lucene.search.Query class KeywordSearchOpExec(descString: String) extends FilterOpExec { private val desc: KeywordSearchOpDesc = objectMapper.readValue(descString, classOf[KeywordSearchOpDesc]) // We chose StandardAnalyzer because it provides more comprehensive tokenization, retaining numeric tokens and handling a broader range of characters. // This ensures that search functionality can include standalone numbers (e.g., "3") and complex queries while offering robust performance for most use cases. @transient private lazy val analyzer = new StandardAnalyzer() @transient lazy val query: Query = new QueryParser(desc.attribute, analyzer).parse(desc.keyword) @transient private lazy val memoryIndex: MemoryIndex = new MemoryIndex() this.setFilterFunc(findKeyword) private def findKeyword(tuple: Tuple): Boolean = { Option[Any](tuple.getField(desc.attribute)).map(_.toString).exists { fieldValue => memoryIndex.addField(desc.attribute, fieldValue, analyzer) val isMatch = memoryIndex.search(query) > 0.0f memoryIndex.reset() isMatch } } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/limit/LimitOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.limit import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PhysicalOp} import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.operator.{LogicalOp, StateTransferFunc} import org.apache.texera.amber.util.JSONUtils.objectMapper import scala.util.{Success, Try} class LimitOpDesc extends LogicalOp { @JsonProperty(required = true) @JsonSchemaTitle("Limit") @JsonPropertyDescription("the max number of output rows") var limit: Int = _ override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.limit.LimitOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withParallelizable(false) } override def operatorInfo: OperatorInfo = OperatorInfo( "Limit", "Limit the number of output rows", OperatorGroupConstants.CLEANING_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort()), supportReconfiguration = true ) override def runtimeReconfiguration( workflowId: WorkflowIdentity, executionId: ExecutionIdentity, oldLogicalOp: LogicalOp, newLogicalOp: LogicalOp ): Try[(PhysicalOp, Option[StateTransferFunc])] = { val newPhysicalOp = newLogicalOp.getPhysicalOp(workflowId, executionId) val stateTransferFunc: StateTransferFunc = (oldOp, newOp) => { val oldLimitOp = oldOp.asInstanceOf[LimitOpExec] val newLimitOp = newOp.asInstanceOf[LimitOpExec] newLimitOp.count = oldLimitOp.count } Success(newPhysicalOp, Some(stateTransferFunc)) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/limit/LimitOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.limit import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} import org.apache.texera.amber.util.JSONUtils.objectMapper class LimitOpExec(descString: String) extends OperatorExecutor { private val desc: LimitOpDesc = objectMapper.readValue(descString, classOf[LimitOpDesc]) var count: Int = _ override def open(): Unit = { count = 0 } override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = { if (count < desc.limit) { count += 1 Iterator(tuple) } else { Iterator() } } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/Scorer/MachineLearningScorerOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.machineLearning.Scorer import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{ JsonSchemaInject, JsonSchemaString, JsonSchemaTitle } import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity} import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.{AutofillAttributeName, HideAnnotation} import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} class MachineLearningScorerOpDesc extends PythonOperatorDescriptor { @JsonProperty(required = true, defaultValue = "false") @JsonSchemaTitle("Regression") @JsonPropertyDescription( "Choose to solve a regression task" ) var isRegression: Boolean = false @JsonProperty(required = true) @JsonSchemaTitle("Actual Value") @JsonPropertyDescription("Specify the label attribute") @AutofillAttributeName var actualValueColumn: EncodableString = "" @JsonProperty(required = true) @JsonSchemaTitle("Predicted Value") @JsonPropertyDescription("Specify the attribute generated by the model") @AutofillAttributeName var predictValueColumn: EncodableString = "" @JsonProperty(required = false, value = "classificationFlag") @JsonSchemaTitle("Scorer Functions") @JsonPropertyDescription("Select classification tasks metrics") @JsonSchemaInject( strings = Array( new JsonSchemaString(path = HideAnnotation.hideTarget, value = "isRegression"), new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.equals), new JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = "true") ) ) var classificationMetrics: List[classificationMetricsFnc] = List() @JsonProperty(required = false, value = "regressionFlag") @JsonSchemaTitle("Scorer Functions") @JsonPropertyDescription("Select regression tasks metrics") @JsonSchemaInject( strings = Array( new JsonSchemaString(path = HideAnnotation.hideTarget, value = "isRegression"), new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.equals), new JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = "false") ) ) var regressionMetrics: List[regressionMetricsFnc] = List() override def operatorInfo: OperatorInfo = OperatorInfo( "Machine Learning Scorer", "Scorer for machine learning models", OperatorGroupConstants.MACHINE_LEARNING_GENERAL_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort()) ) override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val metrics = if (isRegression) { regressionMetrics.map(_.getName()) } else { classificationMetrics.map(_.getName()) } val baseSchema = if (!isRegression) { Schema(List(new Attribute("Class", AttributeType.STRING))) } else { Schema(List()) } val outputSchema = metrics.foldLeft(baseSchema) { (currentSchema, metricName) => currentSchema.add(new Attribute(metricName, AttributeType.DOUBLE)) } Map(operatorInfo.outputPorts.head.id -> outputSchema) } // private def getClassificationScorerName(scorer: classificationMetricsFnc): String = { // // Directly return the name of the scorer using the getName() method // scorer.getName() // } // private def getRegressionScorerName(scorer: regressionMetricsFnc): String = { // // Directly return the name of the scorer using the getName() method // scorer.getName() // } private def getMetricName(metric: Any): EncodableString = metric match { case m: regressionMetricsFnc => m.getName() case m: classificationMetricsFnc => m.getName() case _ => throw new IllegalArgumentException("Unknown metric type") } private def getSelectedMetrics(): EncodableString = { // Return a string of metrics using the getEachScorerName() method val metric = if (isRegression) regressionMetrics else classificationMetrics metric.map(metric => getMetricName(metric)).mkString("'", "','", "'") } override def generatePythonCode(): String = { val isRegressionStr = if (isRegression) "True" else "False" val finalcode = pyb""" |from pytexera import * |import pandas as pd |from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, mean_squared_error, root_mean_squared_error, mean_absolute_error, r2_score | |def classification_metrics(y_true, y_pred, metric_list, labels): | if 'Accuracy' in metric_list: | labels.insert(0, 'Overall') | result = {metric: [None] * len(labels) for metric in metric_list} | metrics_func = {'Precision Score': precision_score, 'Recall Score': recall_score, 'F1 Score': f1_score} | | for metric in metric_list: | prediction = None | if metric == 'Accuracy': | result['Accuracy'][0] = round(accuracy_score(y_true, y_pred), 4) | else: | for i, label in enumerate(labels): | if label != 'Overall': | prediction = metrics_func[metric](y_true, y_pred, average=None, labels=[label]) | result[metric][i] = round(prediction[0], 4) | | # if the label is not a string, convert it to string | labels = ['class_' + str(label) if type(label) != str else label for label in labels] | result['Class'] = labels | result_df = pd.DataFrame(result) | | return result_df | | |def regression_metrics(y_true, y_pred, metric_list): | result = dict() | metrics_func = {'MSE': mean_squared_error, 'RMSE': root_mean_squared_error, 'MAE': mean_absolute_error, 'R2': r2_score} | for metric in metric_list: | result[metric] = metrics_func[metric](y_true, y_pred) | | result_df = pd.DataFrame(result, index=[0]) | | return result_df | |class ProcessTableOperator(UDFTableOperator): | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | y_true = table[$actualValueColumn] | y_pred = table[$predictValueColumn] | | metric_list = [${getSelectedMetrics()}] | | if $isRegressionStr: | result = regression_metrics(y_true, y_pred, metric_list) | else: | # calculate the number of unique labels | labels = list(set(y_true)) | result = classification_metrics(y_true, y_pred, metric_list, labels) | | yield result |""" finalcode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/Scorer/classificationMetricsFnc.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.machineLearning.Scorer; import com.fasterxml.jackson.annotation.JsonValue; public enum classificationMetricsFnc { accuracy("Accuracy"), precisionScore("Precision Score"), recallScore("Recall Score"), f1Score("F1 Score"), ; private final String name; classificationMetricsFnc(String name) { this.name = name; } @JsonValue public String getName() { return this.name; } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/Scorer/regressionMetricsFnc.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.machineLearning.Scorer; import com.fasterxml.jackson.annotation.JsonValue; public enum regressionMetricsFnc { mse("MSE"), rmse("RMSE"), mae("MAE"), r2("R2"), ; private final String name; regressionMetricsFnc(String name) { this.name = name; } @JsonValue public String getName() { return this.name; } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/sklearnAdvanced/KNNTrainer/SklearnAdvancedKNNClassifierTrainerOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.KNNTrainer import org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.base.SklearnMLOperatorDescriptor class SklearnAdvancedKNNClassifierTrainerOpDesc extends SklearnMLOperatorDescriptor[SklearnAdvancedKNNParameters] { override def getImportStatements: String = { "from sklearn.neighbors import KNeighborsClassifier" } override def getOperatorInfo: String = { "KNN Classifier" } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/sklearnAdvanced/KNNTrainer/SklearnAdvancedKNNParameters.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.KNNTrainer; import org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.base.ParamClass; public enum SklearnAdvancedKNNParameters implements ParamClass { n_neighbors("n_neighbors", "int"), p("p", "int"), weights("weights", "str"), algorithm("algorithm", "str"), leaf_size("leaf_size", "int"), metric("metric", "int"), metric_params("metric_params", "str"); private final String name; private final String type; SklearnAdvancedKNNParameters(String name, String type) { this.name = name; this.type = type; } public String getType() { return this.type; } public String getName() { return this.name; } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/sklearnAdvanced/KNNTrainer/SklearnAdvancedKNNRegressorTrainerOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.KNNTrainer import org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.base.SklearnMLOperatorDescriptor class SklearnAdvancedKNNRegressorTrainerOpDesc extends SklearnMLOperatorDescriptor[SklearnAdvancedKNNParameters] { override def getImportStatements: String = { "from sklearn.neighbors import KNeighborsRegressor" } override def getOperatorInfo: String = { "KNN Regressor" } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/sklearnAdvanced/SVCTrainer/SklearnAdvancedSVCParameters.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.SVCTrainer; import org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.base.ParamClass; public enum SklearnAdvancedSVCParameters implements ParamClass { C("C", "float"), kernel("kernel", "str"), gamma("gamma", "float"), degree("degree", "int"), coef0("coef0", "float"), tol("tol", "float"), probability("probability", "(lambda value: value.lower() == \"true\")"); private final String name; private final String type; SklearnAdvancedSVCParameters(String name, String type) { this.name = name; this.type = type; } public String getType() { return this.type; } public String getName() { return this.name; } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/sklearnAdvanced/SVCTrainer/SklearnAdvancedSVCTrainerOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.SVCTrainer import org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.base.SklearnMLOperatorDescriptor class SklearnAdvancedSVCTrainerOpDesc extends SklearnMLOperatorDescriptor[SklearnAdvancedSVCParameters] { override def getImportStatements: String = { "from sklearn.svm import SVC" } override def getOperatorInfo: String = { "SVM Classifier" } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/sklearnAdvanced/SVRTrainer/SklearnAdvancedSVRParameters.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.SVRTrainer; import org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.base.ParamClass; public enum SklearnAdvancedSVRParameters implements ParamClass { C("C", "float"), kernel("kernel", "str"), gamma("gamma", "float"), degree("degree", "int"), coef0("coef0", "float"), tol("tol", "float"), probability("shrinking", "(lambda value: value.lower() == \"true\")"), verbose("verbose", "(lambda value: value.lower() == \"true\")"), epsilon("epsilon", "float"), cache_size("cache_size", "int"), max_iter("max_iter", "int"); private final String name; private final String type; SklearnAdvancedSVRParameters(String name, String type) { this.name = name; this.type = type; } public String getType() { return this.type; } public String getName() { return this.name; } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/sklearnAdvanced/SVRTrainer/SklearnAdvancedSVRTrainerOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.SVRTrainer import org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.base.SklearnMLOperatorDescriptor class SklearnAdvancedSVRTrainerOpDesc extends SklearnMLOperatorDescriptor[SklearnAdvancedSVRParameters] { override def getImportStatements: String = { "from sklearn.svm import SVR" } override def getOperatorInfo: String = { "SVM Regressor" } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/sklearnAdvanced/base/HyperParameters.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.base import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations._ import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.operator.metadata.annotations.{ CommonOpDescAnnotation, HideAnnotation } class HyperParameters[T] { @JsonProperty(required = true) @JsonSchemaTitle("Parameter") @JsonPropertyDescription("Choose the name of the parameter") var parameter: T = _ @JsonSchemaInject( strings = Array( new JsonSchemaString( path = CommonOpDescAnnotation.autofill, value = CommonOpDescAnnotation.attributeName ), new JsonSchemaString(path = HideAnnotation.hideTarget, value = "parametersSource"), new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.`equals`), new JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = "false") ), ints = Array( new JsonSchemaInt(path = CommonOpDescAnnotation.autofillAttributeOnPort, value = 1) ) ) @JsonProperty(value = "attribute") var attribute: EncodableString = _ @JsonSchemaInject( strings = Array( new JsonSchemaString(path = HideAnnotation.hideTarget, value = "parametersSource"), new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.`equals`), new JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = "true") ), bools = Array(new JsonSchemaBool(path = HideAnnotation.hideOnNull, value = true)) ) @JsonProperty(value = "value") var value: EncodableString = _ @JsonProperty(defaultValue = "false") @JsonSchemaTitle("Workflow") @JsonPropertyDescription("Parameter from workflow") var parametersSource: Boolean = false } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/sklearnAdvanced/base/SklearnAdvancedBaseDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.machineLearning.sklearnAdvanced.base import com.fasterxml.jackson.annotation.{JsonIgnore, JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity} import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.{ AutofillAttributeName, AutofillAttributeNameList } import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder trait ParamClass { def getName: String def getType: String } abstract class SklearnMLOperatorDescriptor[T <: ParamClass] extends PythonOperatorDescriptor { @JsonIgnore def getImportStatements: String @JsonIgnore def getOperatorInfo: String @JsonProperty(required = true) @JsonSchemaTitle("Parameter Setting") var paraList: List[HyperParameters[T]] = List() @JsonProperty(required = true) @JsonSchemaTitle("Ground Truth Attribute Column") @JsonPropertyDescription("Ground truth attribute column") @AutofillAttributeName var groundTruthAttribute: EncodableString = "" @JsonProperty(value = "Selected Features", required = true) @JsonSchemaTitle("Selected Features") @JsonPropertyDescription("Features used to train the model") @AutofillAttributeNameList var selectedFeatures: List[EncodableString] = _ private def getLoopTimes(paraList: List[HyperParameters[T]]): PythonTemplateBuilder = { for (ele <- paraList) { if (ele.parametersSource) { return pyb"""table[${ele.attribute}].values.shape[0]""" } } pyb"1" } def getParameter(paraList: List[HyperParameters[T]]): List[PythonTemplateBuilder] = { var workflowParam = s""; var portParam = pyb""; var paramString = pyb"" for (ele <- paraList) { if (ele.parametersSource) { workflowParam = s"$workflowParam${ele.parameter.getName} = {}," portParam = portParam + pyb"${ele.parameter.getType}(table[${ele.attribute}].values[i])," paramString = pyb"$paramString${ele.parameter.getName} = ${ele.parameter.getType}(table[${ele.attribute}].values[i])," } else { workflowParam = s"$workflowParam${ele.parameter.getName} = {}," portParam = pyb"$portParam${ele.parameter.getType} (${ele.value})," paramString = pyb"$paramString${ele.parameter.getName} = ${ele.parameter.getType} (${ele.value})," } } List(pyb""""$workflowParam".format($portParam)""", paramString) } override def generatePythonCode(): String = { val listFeatures = selectedFeatures.map(feature => pyb"""$feature""").mkString(",") val trainingName = getImportStatements.split(" ").last val stringList = getParameter(paraList) val trainingParam = stringList(1) val paramString = stringList(0) val finalCode = pyb""" |from pytexera import * | |import pandas as pd |${getImportStatements} | |class ProcessTableOperator(UDFTableOperator): | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | model_list = [] | para_list = [] | features = [$listFeatures] | | if port == 0: | self.dataset = table | | if port == 1 : | y_train = self.dataset[$groundTruthAttribute] | X_train = self.dataset[features] | loop_times = ${getLoopTimes(paraList)} | | for i in range(loop_times): | model = ${trainingName}(${trainingParam}) | model.fit(X_train, y_train) | model_list.append(model) | para_str = ${paramString} | para_list.append(para_str) | | data = dict() | data["Model"]= model_list | data["Parameters"] =para_list | | df = pd.DataFrame(data) | yield df | |""" finalCode.encode } override def operatorInfo: OperatorInfo = { val name = getOperatorInfo OperatorInfo( name, "Sklearn " + name + " Operator", OperatorGroupConstants.ADVANCED_SKLEARN_GROUP, inputPorts = List( InputPort( PortIdentity(0), displayName = "training" ), InputPort( PortIdentity(1), displayName = "parameter", dependencies = List(PortIdentity(0)) ) ), outputPorts = List(OutputPort()) ) } override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema( List( new Attribute("Model", AttributeType.BINARY), new Attribute("Parameters", AttributeType.STRING) ) ) Map(operatorInfo.outputPorts.head.id -> outputSchema) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/map/MapOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.map import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.PhysicalOp import org.apache.texera.amber.operator.{LogicalOp, StateTransferFunc} import scala.util.{Success, Try} abstract class MapOpDesc extends LogicalOp { override def runtimeReconfiguration( workflowId: WorkflowIdentity, executionId: ExecutionIdentity, oldOpDesc: LogicalOp, newOpDesc: LogicalOp ): Try[(PhysicalOp, Option[StateTransferFunc])] = { Success(newOpDesc.getPhysicalOp(workflowId, executionId), None) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/map/MapOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.map import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} /** * Common operator executor of a map() function * A map() function transforms one input tuple to exactly one output tuple. */ abstract class MapOpExec extends OperatorExecutor with Serializable { private var mapFunc: Tuple => TupleLike = _ /** * Provides the flatMap function of this executor, it should be called in the constructor * If the operator executor is implemented in Java, it should be called with: * setMapFunc((Function1 & Serializable) func) */ def setMapFunc(func: Tuple => TupleLike): Unit = mapFunc = func override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = Iterator(mapFunc(tuple)) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/OPVersion.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.metadata; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; public class OPVersion { private static Git git = null; private static Map opMap = new HashMap<>(); static { try { git = Git.open(new File(Path.of(System.getenv().getOrDefault("TEXERA_HOME", ".")).toString())); } catch (IOException e) { e.printStackTrace(); } } public static String getVersion(String operatorName, String operatorPath) { if(!opMap.containsKey(operatorName)) { try { String version = git.log().addPath(operatorPath).setMaxCount(1).call().iterator().next().getName(); opMap.put(operatorName, version); } catch (GitAPIException e) { e.printStackTrace(); } catch (NullPointerException e) { opMap.put(operatorName, "N/A"); } } return opMap.get(operatorName); } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/OperatorGroupConstants.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.metadata object OperatorGroupConstants { final val INPUT_GROUP = "Data Input" final val DATABASE_GROUP = "Database Connector" final val SEARCH_GROUP = "Search" final val CLEANING_GROUP = "Data Cleaning" final val JOIN_GROUP = "Join" final val SET_GROUP = "Set" final val AGGREGATE_GROUP = "Aggregate" final val SORT_GROUP = "Sort" final val UTILITY_GROUP = "Utilities" final val API_GROUP = "External API" final val VISUALIZATION_GROUP = "Visualization" final val VISUALIZATION_BASIC_GROUP = "Basic" final val VISUALIZATION_STATISTICAL_GROUP = "Statistical" final val VISUALIZATION_SCIENTIFIC_GROUP = "Scientific" final val VISUALIZATION_FINANCIAL_GROUP = "Financial" final val VISUALIZATION_MEDIA_GROUP = "Media" final val VISUALIZATION_ADVANCED_GROUP = "Advanced" final val MACHINE_LEARNING_GROUP = "Machine Learning" final val ADVANCED_SKLEARN_GROUP = "Advanced Sklearn" final val HUGGINGFACE_GROUP = "Hugging Face" final val SKLEARN_GROUP = "Sklearn" final val SKLEARN_TRAINING_GROUP = "Sklearn Training" final val UDF_GROUP = "User-defined Functions" final val PYTHON_GROUP = "Python" final val JAVA_GROUP = "Java" final val R_GROUP = "R" final val MACHINE_LEARNING_GENERAL_GROUP = "Machine Learning General" final val CONTROL_GROUP = "Control Block" /** * The order of the groups to show up in the frontend operator panel. * The order numbers are relative. */ final val OperatorGroupOrderList: List[GroupInfo] = List( GroupInfo(INPUT_GROUP), GroupInfo(DATABASE_GROUP), GroupInfo(SEARCH_GROUP), GroupInfo( CLEANING_GROUP, List( GroupInfo(JOIN_GROUP), GroupInfo(SET_GROUP), GroupInfo(AGGREGATE_GROUP), GroupInfo(SORT_GROUP) ) ), GroupInfo( MACHINE_LEARNING_GROUP, List( GroupInfo(SKLEARN_GROUP, List(GroupInfo(SKLEARN_TRAINING_GROUP))), GroupInfo(ADVANCED_SKLEARN_GROUP), GroupInfo(HUGGINGFACE_GROUP), GroupInfo(MACHINE_LEARNING_GENERAL_GROUP) ) ), GroupInfo(UTILITY_GROUP), GroupInfo(API_GROUP), GroupInfo(UDF_GROUP, List(GroupInfo(PYTHON_GROUP), GroupInfo(JAVA_GROUP), GroupInfo(R_GROUP))), GroupInfo( VISUALIZATION_GROUP, List( GroupInfo(VISUALIZATION_BASIC_GROUP), GroupInfo(VISUALIZATION_STATISTICAL_GROUP), GroupInfo(VISUALIZATION_SCIENTIFIC_GROUP), GroupInfo(VISUALIZATION_FINANCIAL_GROUP), GroupInfo(VISUALIZATION_MEDIA_GROUP), GroupInfo(VISUALIZATION_ADVANCED_GROUP) ) ), GroupInfo(CONTROL_GROUP) ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/OperatorMetadataGenerator.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.metadata import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.introspect.AnnotatedClassResolver import com.fasterxml.jackson.databind.jsontype.NamedType import com.fasterxml.jackson.databind.node.{ArrayNode, ObjectNode} import com.kjetland.jackson.jsonSchema.JsonSchemaConfig.html5EnabledSchema import com.kjetland.jackson.jsonSchema.{JsonSchemaConfig, JsonSchemaDraft, JsonSchemaGenerator} import org.apache.texera.amber.core.workflow.OutputPort.OutputMode import org.apache.texera.amber.core.workflow.{InputPort, OutputPort} import org.apache.texera.amber.operator.LogicalOp import org.apache.texera.amber.operator.source.scan.csv.CSVScanSourceOpDesc import org.apache.texera.amber.util.JSONUtils.objectMapper import java.util import scala.jdk.CollectionConverters.{IteratorHasAsScala, ListHasAsScala} case class OperatorInfo( userFriendlyName: String, operatorDescription: String, operatorGroupName: String, inputPorts: List[InputPort], outputPorts: List[OutputPort], dynamicInputPorts: Boolean = false, dynamicOutputPorts: Boolean = false, supportReconfiguration: Boolean = false, allowPortCustomization: Boolean = false ) object OperatorInfo { def forVisualization( userFriendlyName: String, operatorDescription: String, operatorGroupName: String ): OperatorInfo = OperatorInfo( userFriendlyName, operatorDescription, operatorGroupName, inputPorts = List(InputPort(disallowMultiLinks = true)), outputPorts = List(OutputPort(mode = OutputMode.SINGLE_SNAPSHOT)) ) } case class OperatorMetadata( operatorType: String, jsonSchema: JsonNode, additionalMetadata: OperatorInfo, operatorVersion: String ) case class GroupInfo( groupName: String, children: List[GroupInfo] = null ) case class AllOperatorMetadata( operators: List[OperatorMetadata], groups: List[GroupInfo] ) /** * Generates the metadata of a Texera Operator Descriptor for the frontend. * The type definitions correspond to "workspace/types/operator-schema.interface.ts" in frontend. */ object OperatorMetadataGenerator { val texeraSchemaGeneratorConfig: JsonSchemaConfig = html5EnabledSchema.copy( useOneOfForOption = false, useOneOfForNullables = false, useNullableForOption = true, useNullableForNullables = true, defaultArrayFormat = None, jsonSchemaDraft = JsonSchemaDraft.DRAFT_07 ) val jsonSchemaGenerator = new JsonSchemaGenerator(objectMapper, texeraSchemaGeneratorConfig) // a map from a Texera Operator Descriptor's class to its operatorType string value val operatorTypeMap: Map[Class[_ <: LogicalOp], String] = { // find all the operator type declarations in PredicateBase annotation val types = objectMapper.getSubtypeResolver.collectAndResolveSubtypesByClass( objectMapper.getDeserializationConfig, AnnotatedClassResolver.resolveWithoutSuperTypes( objectMapper.getDeserializationConfig, objectMapper.constructType(classOf[LogicalOp]).getRawClass ) ) new util.ArrayList[NamedType](types).asScala .filter(t => t.getType != null && t.getName != null) .map(t => (t.getType.asInstanceOf[Class[_ <: LogicalOp]], t.getName)) .toMap } val allOperatorMetadata: AllOperatorMetadata = generateAllOperatorMetadata() def main(args: Array[String]): Unit = { // run this if you want to check the json schema generated for an operator descriptor // replace the argument with the class of your operator descriptor println(generateOperatorJsonSchema(classOf[CSVScanSourceOpDesc]).toPrettyString) } def generateOperatorJsonSchema(opDescClass: Class[_ <: LogicalOp]): JsonNode = { val jsonSchema = jsonSchemaGenerator.generateJsonSchema(opDescClass).asInstanceOf[ObjectNode] // remove operatorID from json schema jsonSchema.get("properties").asInstanceOf[ObjectNode].remove("operatorID") // remove operatorId from json schema jsonSchema.get("properties").asInstanceOf[ObjectNode].remove("operatorId") // remove operatorType from json schema jsonSchema.get("properties").asInstanceOf[ObjectNode].remove("operatorType") // remove operatorVersion from json schema jsonSchema.get("properties").asInstanceOf[ObjectNode].remove("operatorVersion") // remove inputPorts/outputPorts from json schema jsonSchema.get("properties").asInstanceOf[ObjectNode].remove("inputPorts") jsonSchema.get("properties").asInstanceOf[ObjectNode].remove("outputPorts") // remove operatorType from required list val operatorTypeIndex = jsonSchema .get("required") .asInstanceOf[ArrayNode] .elements() .asScala .indexWhere(p => p.asText().equals("operatorType")) jsonSchema.get("required").asInstanceOf[ArrayNode].remove(operatorTypeIndex) // remove "title" for the operator - frontend uses userFriendlyName to show operator title jsonSchema.remove("title") jsonSchema } def generateAllOperatorMetadata(): AllOperatorMetadata = { AllOperatorMetadata( operatorTypeMap.keys.map(generateOperatorMetadata).toList, OperatorGroupConstants.OperatorGroupOrderList ) } def generateOperatorMetadata(opDescClass: Class[_ <: LogicalOp]): OperatorMetadata = { if (!operatorTypeMap.contains(opDescClass)) throw new RuntimeException( "Texera Operator Descriptor class " + opDescClass.toString + " is not registered in TexeraOperatorDescription class" ) // find the operatorType of the predicate class val operatorType = operatorTypeMap(opDescClass) // generate json schema for operator properties val jsonSchema = generateOperatorJsonSchema(opDescClass) // generate texera operator info val texeraOperatorInfo = opDescClass.getConstructor().newInstance().operatorInfo OperatorMetadata( operatorType, jsonSchema, texeraOperatorInfo, opDescClass.getConstructor().newInstance().operatorVersion ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/PropertyNameConstants.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.metadata /** * PropertyNameConstants defines the key names * in the JSON representation of each operator. * * @author Zuozhi Wang */ object PropertyNameConstants { // logical plan property names final val OPERATOR_ID = "operatorID" final val OPERATOR_TYPE = "operatorType" final val ORIGIN_OPERATOR_ID = "origin" final val DESTINATION_OPERATOR_ID = "destination" final val OPERATOR_LIST = "operators" final val OPERATOR_LINK_LIST = "links" final val OPERATOR_VERSION = "operatorVersion" // common operator property names final val ATTRIBUTE_NAMES = "attributes" final val ATTRIBUTE_NAME = "attribute" final val RESULT_ATTRIBUTE_NAME = "resultAttribute" final val SPAN_LIST_NAME = "spanListName" final val TABLE_NAME = "tableName" // physical plan property names final val WORKFLOW_ID = "workflowID" final val EXECUTION_ID = "executionID" final val PARALLELIZABLE = "parallelizable" final val LOCATION_PREFERENCE = "locationPreference" final val PARTITION_REQUIREMENT = "partitionRequirement" // derivePartition is a function type that cannot be serialized final val INPUT_PORTS = "inputPorts" final val OUTPUT_PORTS = "outputPorts" // propagateSchema is a function type that cannot be serialized final val IS_ONE_TO_MANY_OP = "isOneToManyOp" final val SUGGESTED_WORKER_NUM = "suggestedWorkerNum" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/annotations/AutofillAttributeName.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.metadata.annotations; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInt; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaString; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD}) @JacksonAnnotationsInside @JsonSchemaInject( strings = @JsonSchemaString(path = CommonOpDescAnnotation.autofill, value = CommonOpDescAnnotation.attributeName), ints = @JsonSchemaInt(path = CommonOpDescAnnotation.autofillAttributeOnPort, value = 0)) public @interface AutofillAttributeName { } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/annotations/AutofillAttributeNameLambda.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.metadata.annotations; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInt; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaString; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD}) @JacksonAnnotationsInside @JsonSchemaInject( strings = { @JsonSchemaString(path = CommonOpDescAnnotation.autofill, value = CommonOpDescAnnotation.attributeName), @JsonSchemaString(path = HideAnnotation.hideTarget, value = "attributeName"), @JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.regex), @JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = "Add New Column"), @JsonSchemaString(path = "additionalEnumValue", value = "Add New Column"), }, ints = @JsonSchemaInt(path = CommonOpDescAnnotation.autofillAttributeOnPort, value = 0)) public @interface AutofillAttributeNameLambda { } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/annotations/AutofillAttributeNameList.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.metadata.annotations; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInt; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaString; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD}) @JacksonAnnotationsInside @JsonSchemaInject( strings = @JsonSchemaString(path = CommonOpDescAnnotation.autofill, value = CommonOpDescAnnotation.attributeNameList), ints = @JsonSchemaInt(path = CommonOpDescAnnotation.autofillAttributeOnPort, value = 0)) public @interface AutofillAttributeNameList { } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/annotations/AutofillAttributeNameOnPort1.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.metadata.annotations; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInt; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaString; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD}) @JacksonAnnotationsInside @JsonSchemaInject( strings = @JsonSchemaString(path = CommonOpDescAnnotation.autofill, value = CommonOpDescAnnotation.attributeName), ints = @JsonSchemaInt(path = CommonOpDescAnnotation.autofillAttributeOnPort, value = 1)) public @interface AutofillAttributeNameOnPort1 { } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/annotations/BatchByColumn.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.metadata.annotations; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaString; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD}) @JacksonAnnotationsInside @JsonSchemaInject(strings = @JsonSchemaString(path = "dependOn", value = "batchByColumn")) public @interface BatchByColumn { } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/annotations/CommonOpDescAnnotation.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.metadata.annotations; public class CommonOpDescAnnotation { // JSON schema key public final static String autofill = "autofill"; // allowed JSON schema values for the key autoCompleteType public final static String attributeName = "attributeName"; public final static String attributeNameList = "attributeNameList"; // JSON schema key to indicate which port public final static String autofillAttributeOnPort = "autofillAttributeOnPort"; } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/annotations/EnablePresets.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.metadata.annotations; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaBool; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD}) @JacksonAnnotationsInside @JsonSchemaInject( bools = @JsonSchemaBool(path = "enable-presets", value = true)) public @interface EnablePresets { String path = "enable-presets"; boolean value = true; } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/annotations/HideAnnotation.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.metadata.annotations; /* For every hide annotation, you specify on a formly field three things: the target (field name of dependent comparison), the hide type (specified by Type), and the expected value (specified as a string). This information is passed to the frontend and hidden. Here's how you specify an example hide, hiding someOtherFieldName when someFieldName == 3: @JsonSchemaInject(strings = { @JsonSchemaString(path = HideAnnotation.hideTarget, value = "someFieldName"), @JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.equals), @JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = "3") }) public Integer someOtherFieldName; public Integer someFieldName; */ public class HideAnnotation { public final static String hideTarget = "hideTarget"; public final static String hideType = "hideType"; public final static String hideExpectedValue = "hideExpectedValue"; public final static String hideOnNull = "hideOnNull"; /* The types of matching on which a hide occurs. Evaluated at runtime by javascript. */ public static class Type { /* String equality operator is applied to assert hideTarget == hideExpectedValue. */ public final static String equals = "equals"; /* Regex matching is applied to assert regex for hideExpectedValue matches hideTarget */ public final static String regex = "regex"; } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/metadata/annotations/UIWidget.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.metadata.annotations; public class UIWidget { public static final String UIWidgetTextArea = "{ \"widget\": {\n \"formlyConfig\": {\n \"type\": \"textarea\",\n \"templateOptions\": {\n \"autosize\": true,\n \"autosizeMinRows\": 3\n }\n }\n }\n }"; public static final String UIWidgetPassword = "{ \"widget\": {\n \"formlyConfig\": {\n \"templateOptions\": {\n \"type\": \"password\"\n }\n }\n }\n }"; } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/projection/AttributeUnit.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.projection; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle; import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName; import org.jooq.tools.StringUtils; import java.util.Objects; public class AttributeUnit{ @JsonProperty(required = true) @JsonSchemaTitle("Attribute") @JsonPropertyDescription("Attribute name in the schema") @AutofillAttributeName private String originalAttribute; @JsonProperty @JsonSchemaTitle("Alias") @JsonPropertyDescription("Renamed attribute name") private String alias; // TODO: explore the reason why this JsonCreator annotation is required @JsonCreator public AttributeUnit( @JsonProperty("originalAttribute") String attributeName, @JsonProperty("alias") String alias) { this.originalAttribute = attributeName; this.alias = alias; } String getOriginalAttribute(){ return originalAttribute; } String getAlias(){ if(StringUtils.isBlank(alias)){ return originalAttribute; } return alias; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; AttributeUnit that = (AttributeUnit) o; return Objects.equals(originalAttribute, that.originalAttribute) && Objects.equals(alias, that.alias); } @Override public int hashCode() { return Objects.hash(originalAttribute, alias); } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/projection/ProjectionOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.projection import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.tuple.Schema import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.PhysicalOp.oneToOnePhysicalOp import org.apache.texera.amber.core.workflow._ import org.apache.texera.amber.operator.map.MapOpDesc import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.util.JSONUtils.objectMapper class ProjectionOpDesc extends MapOpDesc { @JsonProperty(required = true, defaultValue = "false") @JsonSchemaTitle("Drop Option") @JsonPropertyDescription("check to drop the selected attributes") var isDrop: Boolean = false var attributes: List[AttributeUnit] = List() override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.projection.ProjectionOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withDerivePartition(derivePartition()) .withPropagateSchema(SchemaPropagationFunc(inputSchemas => { require(attributes.nonEmpty, "Attributes must not be empty") val inputSchema = inputSchemas.values.head val outputSchema = if (!isDrop) { attributes.foldLeft(Schema()) { (schema, attribute) => val originalType = inputSchema.getAttribute(attribute.getOriginalAttribute).getType schema.add(attribute.getAlias, originalType) } } else { attributes.foldLeft(inputSchema) { (schema, attribute) => schema.remove(attribute.getOriginalAttribute) } } Map(operatorInfo.outputPorts.head.id -> outputSchema) })) } def derivePartition()(partition: List[PartitionInfo]): PartitionInfo = { val inputPartitionInfo = partition.head val outputPartitionInfo = inputPartitionInfo match { case HashPartition(hashAttributeNames) => if (hashAttributeNames.nonEmpty) HashPartition(hashAttributeNames) else UnknownPartition() case RangePartition(rangeAttributeNames, min, max) => if (rangeAttributeNames.nonEmpty) RangePartition(rangeAttributeNames, min, max) else UnknownPartition() case _ => inputPartitionInfo } outputPartitionInfo } override def operatorInfo: OperatorInfo = { OperatorInfo( "Projection", "Keeps or drops the column", OperatorGroupConstants.CLEANING_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort()) ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/projection/ProjectionOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.projection import com.google.common.base.Preconditions import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} import org.apache.texera.amber.operator.map.MapOpExec import org.apache.texera.amber.util.JSONUtils.objectMapper import scala.collection.mutable class ProjectionOpExec( descString: String ) extends MapOpExec { val desc: ProjectionOpDesc = objectMapper.readValue(descString, classOf[ProjectionOpDesc]) setMapFunc(project) def project(tuple: Tuple): TupleLike = { Preconditions.checkArgument(desc.attributes.nonEmpty) var selectedUnits: List[AttributeUnit] = List() val fields = mutable.LinkedHashMap[String, Any]() if (desc.isDrop) { val allAttribute = tuple.schema.getAttributeNames val selectedAttributes = desc.attributes.map(_.getOriginalAttribute) val keepAttributes = allAttribute.diff(selectedAttributes) keepAttributes.foreach { attribute => val newList = List( new AttributeUnit(attribute, attribute) ) selectedUnits = selectedUnits ::: newList } } else { selectedUnits = desc.attributes } selectedUnits.foreach { attributeUnit => val alias = attributeUnit.getAlias if (fields.contains(alias)) { throw new RuntimeException("have duplicated attribute name/alias") } fields(alias) = tuple.getField[Any](attributeUnit.getOriginalAttribute) } TupleLike(fields.toSeq: _*) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/randomksampling/RandomKSamplingOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.randomksampling import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PhysicalOp} import org.apache.texera.amber.operator.filter.FilterOpDesc import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.util.JSONUtils.objectMapper class RandomKSamplingOpDesc extends FilterOpDesc { @JsonProperty(value = "random k sample percentage", required = true) @JsonPropertyDescription("random k sampling with given percentage") var percentage: Int = _ override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.randomksampling.RandomKSamplingOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) } override def operatorInfo: OperatorInfo = OperatorInfo( userFriendlyName = "Random K Sampling", operatorDescription = "random sampling with given percentage", operatorGroupName = OperatorGroupConstants.UTILITY_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort()), supportReconfiguration = true ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/randomksampling/RandomKSamplingOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.randomksampling import org.apache.texera.amber.operator.filter.FilterOpExec import org.apache.texera.amber.util.JSONUtils.objectMapper import scala.util.Random class RandomKSamplingOpExec(descString: String, idx: Int, workerCount: Int) extends FilterOpExec { private val desc: RandomKSamplingOpDesc = objectMapper.readValue(descString, classOf[RandomKSamplingOpDesc]) val rand: Random = new Random(workerCount) setFilterFunc(_ => (desc.percentage / 100.0) >= rand.nextDouble()) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/regex/RegexOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.regex import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PhysicalOp} import org.apache.texera.amber.operator.filter.FilterOpDesc import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.util.JSONUtils.objectMapper class RegexOpDesc extends FilterOpDesc { @JsonProperty(value = "attribute", required = true) @JsonPropertyDescription("column to search regex on") @AutofillAttributeName var attribute: String = _ @JsonProperty(value = "regex", required = true) @JsonPropertyDescription("regular expression") var regex: String = _ @JsonProperty(required = false, defaultValue = "false") @JsonSchemaTitle("Case Insensitive") @JsonPropertyDescription("regex match is case sensitive") var caseInsensitive: Boolean = _ override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.regex.RegexOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) } override def operatorInfo: OperatorInfo = OperatorInfo( userFriendlyName = "Regular Expression", operatorDescription = "Search a regular expression in a string column", operatorGroupName = OperatorGroupConstants.SEARCH_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort()), supportReconfiguration = true ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/regex/RegexOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.regex import org.apache.texera.amber.core.tuple.Tuple import org.apache.texera.amber.operator.filter.FilterOpExec import org.apache.texera.amber.util.JSONUtils.objectMapper import java.util.regex.Pattern class RegexOpExec(descString: String) extends FilterOpExec { private val desc: RegexOpDesc = objectMapper.readValue(descString, classOf[RegexOpDesc]) lazy val pattern: Pattern = Pattern.compile(desc.regex, if (desc.caseInsensitive) Pattern.CASE_INSENSITIVE else 0) this.setFilterFunc(this.matchRegex) private def matchRegex(tuple: Tuple): Boolean = Option[Any](tuple.getField(desc.attribute).toString) .map(_.toString) .exists(value => pattern.matcher(value).find) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/reservoirsampling/ReservoirSamplingOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.reservoirsampling import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PhysicalOp} import org.apache.texera.amber.operator.LogicalOp import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.util.JSONUtils.objectMapper class ReservoirSamplingOpDesc extends LogicalOp { @JsonProperty(value = "number of item sampled in reservoir sampling", required = true) @JsonPropertyDescription("reservoir sampling with k items being kept randomly") var k: Int = _ override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.reservoirsampling.ReservoirSamplingOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) } override def operatorInfo: OperatorInfo = { OperatorInfo( userFriendlyName = "Reservoir Sampling", operatorDescription = "Reservoir Sampling with k items being kept randomly", operatorGroupName = OperatorGroupConstants.UTILITY_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort()) ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/reservoirsampling/ReservoirSamplingOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.reservoirsampling import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} import org.apache.texera.amber.operator.util.OperatorDescriptorUtils.equallyPartitionGoal import org.apache.texera.amber.util.JSONUtils.objectMapper import scala.util.Random class ReservoirSamplingOpExec(descString: String, idx: Int, workerCount: Int) extends OperatorExecutor { private val desc: ReservoirSamplingOpDesc = objectMapper.readValue(descString, classOf[ReservoirSamplingOpDesc]) private val count: Int = equallyPartitionGoal(desc.k, workerCount)(idx) private var n: Int = _ private var reservoir: Array[Tuple] = _ private val rand: Random = new Random(workerCount) override def open(): Unit = { n = 0 reservoir = Array.ofDim(count) } override def close(): Unit = { reservoir = null } override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = { if (n < count) { reservoir(n) = tuple } else { val i = rand.nextInt(n) if (i < count) { reservoir(i) = tuple } } n += 1 Iterator() } override def onFinish(port: Int): Iterator[TupleLike] = reservoir.iterator } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sink/ProgressiveUtils.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sink import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple} object ProgressiveUtils { // boolean attribute to indicate insertion / retraction // true indicates insertion (+) // false indicates retraction (-) val insertRetractFlagAttr = new Attribute("__internal_is_insertion", AttributeType.BOOLEAN) def addInsertionFlag(tuple: Tuple, outputSchema: Schema): Tuple = { assert(!tuple.getSchema.containsAttribute(insertRetractFlagAttr.getName)) Tuple.builder(outputSchema).add(insertRetractFlagAttr, true).add(tuple).build() } def addRetractionFlag(tuple: Tuple, outputSchema: Schema): Tuple = { assert(!tuple.getSchema.containsAttribute(insertRetractFlagAttr.getName)) Tuple.builder(outputSchema).add(insertRetractFlagAttr, false).add(tuple).build() } def isInsertion(tuple: Tuple): Boolean = { if (tuple.getSchema.containsAttribute(insertRetractFlagAttr.getName)) { tuple.getField[Boolean](insertRetractFlagAttr.getName) } else { true } } def getTupleFlagAndValue( tuple: Tuple ): (Boolean, Tuple) = { ( isInsertion(tuple), { val originalSchema = tuple.getSchema val schema = originalSchema.getPartialSchema( originalSchema.getAttributeNames.filterNot(_ == insertRetractFlagAttr.getName) ) Tuple.builder(schema).add(tuple, isStrictSchemaMatch = false).build() } ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnAdaptiveBoostingOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnAdaptiveBoostingOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.ensemble import AdaBoostClassifier" override def getUserFriendlyModelName = "Adaptive Boosting" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnBaggingOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnBaggingOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.ensemble import BaggingClassifier" override def getUserFriendlyModelName = "Bagging" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnBernoulliNaiveBayesOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnBernoulliNaiveBayesOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.naive_bayes import BernoulliNB" override def getUserFriendlyModelName = "Bernoulli Naive Bayes" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnClassifierOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn import com.fasterxml.jackson.annotation.{JsonIgnore, JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{ JsonSchemaInject, JsonSchemaInt, JsonSchemaString, JsonSchemaTitle } import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity} import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.{ AutofillAttributeName, CommonOpDescAnnotation, HideAnnotation } import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} abstract class SklearnClassifierOpDesc extends PythonOperatorDescriptor { @JsonSchemaTitle("Target Attribute") @JsonPropertyDescription("Attribute in your dataset corresponding to target.") @JsonProperty(required = true) @AutofillAttributeName var target: EncodableString = _ @JsonSchemaTitle("Count Vectorizer") @JsonPropertyDescription("Convert a collection of text documents to a matrix of token counts.") @JsonProperty(defaultValue = "false") var countVectorizer: Boolean = false @JsonSchemaTitle("Text Attribute") @JsonPropertyDescription("Attribute in your dataset with text to vectorize.") @JsonSchemaInject( strings = Array( new JsonSchemaString( path = CommonOpDescAnnotation.autofill, value = CommonOpDescAnnotation.attributeName ), new JsonSchemaString(path = HideAnnotation.hideTarget, value = "countVectorizer"), new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.equals), new JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = "false") ), ints = Array( new JsonSchemaInt(path = CommonOpDescAnnotation.autofillAttributeOnPort, value = 0) ) ) var text: EncodableString = _ @JsonSchemaTitle("Tfidf Transformer") @JsonPropertyDescription("Transform a count matrix to a normalized tf or tf-idf representation.") @JsonProperty(defaultValue = "false") @JsonSchemaInject( strings = Array( new JsonSchemaString(path = HideAnnotation.hideTarget, value = "countVectorizer"), new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.equals), new JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = "false") ) ) val tfidfTransformer: Boolean = false @JsonIgnore def getImportStatements = "" @JsonIgnore def getUserFriendlyModelName = "" override def generatePythonCode(): String = pyb"""$getImportStatements |from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score |from sklearn.pipeline import make_pipeline |from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer |import numpy as np |from pytexera import * |class ProcessTableOperator(UDFTableOperator): | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | Y = table[$target] | X = table.drop($target, axis=1) | X = ${if (countVectorizer) pyb"X[$text]" else "X"} | if port == 0: | self.model = make_pipeline(${if (countVectorizer) "CountVectorizer()," else ""} ${if (tfidfTransformer) "TfidfTransformer()," else ""} ${getImportStatements .split(" ") .last}()).fit(X, Y) | else: | predictions = self.model.predict(X) | print("Overall Accuracy:", round(accuracy_score(Y, predictions), 4)) | f1s = f1_score(Y, predictions, average=None) | precisions = precision_score(Y, predictions, average=None) | recalls = recall_score(Y, predictions, average=None) | for i, class_name in enumerate(np.unique(Y)): | print("Class", repr(class_name), " - F1:", round(f1s[i], 4), ", Precision:", round(precisions[i], 4), ", Recall:", round(recalls[i], 4)) | yield {"model_name" : "$getUserFriendlyModelName", "model" : self.model}""".encode override def operatorInfo: OperatorInfo = OperatorInfo( getUserFriendlyModelName, "Sklearn " + getUserFriendlyModelName + " Operator", OperatorGroupConstants.SKLEARN_GROUP, inputPorts = List( InputPort(PortIdentity(), "training"), InputPort(PortIdentity(1), "testing", dependencies = List(PortIdentity())) ), outputPorts = List(OutputPort(blocking = true)) ) override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { Map( operatorInfo.outputPorts.head.id -> Schema() .add("model_name", AttributeType.STRING) .add("model", AttributeType.BINARY) ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnComplementNaiveBayesOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnComplementNaiveBayesOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.naive_bayes import ComplementNB" override def getUserFriendlyModelName = "Complement Naive Bayes" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnDecisionTreeOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnDecisionTreeOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.tree import DecisionTreeClassifier" override def getUserFriendlyModelName = "Decision Tree" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnDummyClassifierOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnDummyClassifierOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.dummy import DummyClassifier" override def getUserFriendlyModelName = "Dummy Classifier" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnExtraTreeOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnExtraTreeOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.tree import ExtraTreeClassifier" override def getUserFriendlyModelName = "Extra Tree" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnExtraTreesOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnExtraTreesOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.ensemble import ExtraTreesClassifier" override def getUserFriendlyModelName = "Extra Trees" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnGaussianNaiveBayesOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnGaussianNaiveBayesOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.naive_bayes import GaussianNB" override def getUserFriendlyModelName = "Gaussian Naive Bayes" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnGradientBoostingOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnGradientBoostingOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.ensemble import GradientBoostingClassifier" override def getUserFriendlyModelName = "Gradient Boosting" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnKNNOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnKNNOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.neighbors import KNeighborsClassifier" override def getUserFriendlyModelName = "K-nearest Neighbors" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnLinearRegressionOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity} import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} class SklearnLinearRegressionOpDesc extends PythonOperatorDescriptor { @JsonSchemaTitle("Target Attribute") @JsonPropertyDescription("Attribute in your dataset corresponding to target.") @JsonProperty(required = true) @AutofillAttributeName var target: EncodableString = _ @JsonSchemaTitle("Degree") @JsonPropertyDescription("Degree of polynomial function") @JsonProperty(required = true) val degree: Int = 1 override def generatePythonCode(): String = pyb""" |from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score, mean_absolute_error, r2_score |from sklearn.pipeline import make_pipeline |from sklearn.linear_model import LinearRegression |from sklearn.preprocessing import PolynomialFeatures |import numpy as np |from pytexera import * |class ProcessTableOperator(UDFTableOperator): | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | Y = table[$target] | X = table.drop($target, axis=1) | if port == 0: | pipeline = make_pipeline( | PolynomialFeatures(degree=$degree), | LinearRegression() | ) | self.model = pipeline.fit(X, Y) | else: | predictions = self.model.predict(X) | mae = round(mean_absolute_error(Y, predictions), 4) | r2 = round(r2_score(Y, predictions), 4) | print("MAE:", mae, ", R2:", r2) | yield {"model_name" : "LinearRegression", "model" : self.model}""".encode override def operatorInfo: OperatorInfo = OperatorInfo( "Linear Regression", "Sklearn Linear Regression Operator", OperatorGroupConstants.SKLEARN_GROUP, inputPorts = List( InputPort(PortIdentity(), "training"), InputPort(PortIdentity(1), "testing", dependencies = List(PortIdentity())) ), outputPorts = List(OutputPort(blocking = true)) ) override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { Map( operatorInfo.outputPorts.head.id -> Schema() .add("model_name", AttributeType.STRING) .add("model", AttributeType.BINARY) ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnLinearSVMOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnLinearSVMOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.svm import LinearSVC" override def getUserFriendlyModelName = "Linear Support Vector Machine" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnLogisticRegressionCVOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnLogisticRegressionCVOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.linear_model import LogisticRegressionCV" override def getUserFriendlyModelName = "Logistic Regression Cross Validation" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnLogisticRegressionOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnLogisticRegressionOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.linear_model import LogisticRegression" override def getUserFriendlyModelName = "Logistic Regression" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnMultiLayerPerceptronOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnMultiLayerPerceptronOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.neural_network import MLPClassifier" override def getUserFriendlyModelName = "Multi-layer Perceptron" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnMultinomialNaiveBayesOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnMultinomialNaiveBayesOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.naive_bayes import MultinomialNB" override def getUserFriendlyModelName = "Multinomial Naive Bayes" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnNearestCentroidOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnNearestCentroidOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.neighbors import NearestCentroid" override def getUserFriendlyModelName = "Nearest Centroid" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnPassiveAggressiveOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnPassiveAggressiveOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.linear_model import PassiveAggressiveClassifier" override def getUserFriendlyModelName = "Passive Aggressive" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnPerceptronOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnPerceptronOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.linear_model import Perceptron" override def getUserFriendlyModelName = "Linear Perceptron" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnPredictionOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity} import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.{ AutofillAttributeName, AutofillAttributeNameOnPort1 } import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} class SklearnPredictionOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "Model Attribute", required = true, defaultValue = "model") @JsonPropertyDescription("attribute corresponding to ML model") @AutofillAttributeName var model: EncodableString = _ @JsonProperty(value = "Output Attribute Name", required = true, defaultValue = "prediction") @JsonPropertyDescription("attribute name of the prediction result") var resultAttribute: EncodableString = _ @JsonProperty( value = "Ground Truth Attribute Name to Ignore", required = false, defaultValue = "" ) @JsonPropertyDescription("attribute name of the ground truth") @AutofillAttributeNameOnPort1 var groundTruthAttribute: EncodableString = "" override def generatePythonCode(): String = pyb"""from pytexera import * |from sklearn.pipeline import Pipeline |class ProcessTupleOperator(UDFOperatorV2): | @overrides | def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]: | if port == 0: | self.model = tuple_[$model] | else: | input_features = tuple_ | if $groundTruthAttribute != "": | input_features = input_features.get_partial_tuple([col for col in tuple_.get_field_names() if col != $groundTruthAttribute]) | tuple_[$resultAttribute] = type(tuple_[$groundTruthAttribute])(self.model.predict(Table.from_tuple_likes([input_features]))[0]) | else: | tuple_[$resultAttribute] = str(self.model.predict(Table.from_tuple_likes([input_features]))[0]) | yield tuple_""".encode override def operatorInfo: OperatorInfo = OperatorInfo( "Sklearn Prediction", "Sklearn Prediction Operator", OperatorGroupConstants.SKLEARN_GROUP, inputPorts = List( InputPort(PortIdentity(), "model"), InputPort(PortIdentity(1), dependencies = List(PortIdentity())) ), outputPorts = List(OutputPort()) ) override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { var resultType = AttributeType.STRING val inputSchema = inputSchemas(operatorInfo.inputPorts(1).id) if (groundTruthAttribute != "") { resultType = inputSchema.attributes.find(attr => attr.getName == groundTruthAttribute).get.getType } Map( operatorInfo.outputPorts.head.id -> inputSchema .add(resultAttribute, resultType) ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnProbabilityCalibrationOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnProbabilityCalibrationOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.calibration import CalibratedClassifierCV" override def getUserFriendlyModelName = "Probability Calibration" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnRandomForestOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnRandomForestOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.ensemble import RandomForestClassifier" override def getUserFriendlyModelName = "Random Forest" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnRidgeCVOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnRidgeCVOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.linear_model import RidgeClassifierCV" override def getUserFriendlyModelName = "Ridge Regression Cross Validation" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnRidgeOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnRidgeOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.linear_model import RidgeClassifier" override def getUserFriendlyModelName = "Ridge Regression" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnSDGOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnSDGOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.linear_model import SGDClassifier" override def getUserFriendlyModelName = "Stochastic Gradient Descent" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/SklearnSVMOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn class SklearnSVMOpDesc extends SklearnClassifierOpDesc { override def getImportStatements = "from sklearn.svm import SVC" override def getUserFriendlyModelName = "Support Vector Machine" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/testing/SklearnTestingOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.testing import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity} import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.{ AutofillAttributeName, AutofillAttributeNameOnPort1 } import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext class SklearnTestingOpDesc extends PythonOperatorDescriptor { @JsonProperty(required = true, defaultValue = "false") @JsonSchemaTitle("Regression") @JsonPropertyDescription( "Choose to solve a regression task" ) var isRegression: Boolean = false @JsonSchemaTitle("Model Attribute") @JsonProperty(required = true, defaultValue = "model") @JsonPropertyDescription("Attribute corresponding to ML model") @AutofillAttributeName var model: EncodableString = _ @JsonSchemaTitle("Target Attribute") @JsonPropertyDescription("Attribute in your dataset corresponding to target.") @JsonProperty(required = true) @AutofillAttributeNameOnPort1 var target: EncodableString = _ override def generatePythonCode(): String = { val isRegressionStr = if (isRegression) "True" else "False" pyb"""from pytexera import * |from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score, root_mean_squared_error, mean_absolute_error, r2_score |class ProcessTupleOperator(UDFOperatorV2): | @overrides | def open(self) -> None: | self.data = [] | @overrides | def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]: | if port == 1: | self.data.append(tuple_) | else: | model = tuple_[$model] | table = Table(self.data) | Y = table[$target] | X = table.drop($target, axis=1) | predictions = model.predict(X.squeeze()) | if $isRegressionStr: | tuple_["R2"] = r2_score(Y, predictions) | tuple_["RMSE"] = root_mean_squared_error(Y, predictions) | tuple_["MAE"] = mean_absolute_error(Y, predictions) | else: | tuple_["accuracy"] = round(accuracy_score(Y, predictions), 4) | tuple_["f1"] = f1_score(Y, predictions, average="weighted") | tuple_["precision"] = precision_score(Y, predictions, average="weighted") | tuple_["recall"] = recall_score(Y, predictions, average="weighted") | yield tuple_""".encode } override def operatorInfo: OperatorInfo = OperatorInfo( "Sklearn Testing", "It will generate scorers for Sklearn model", OperatorGroupConstants.SKLEARN_GROUP, inputPorts = List( InputPort( PortIdentity(), "model", dependencies = List(PortIdentity(1)) ), InputPort(PortIdentity(1), "data") ), outputPorts = List(OutputPort()) ) override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = Map( operatorInfo.outputPorts.head.id -> (if (!isRegression) Seq("accuracy", "f1", "precision", "recall") else Seq("R2", "RMSE", "MAE")) .foldLeft(inputSchemas(operatorInfo.inputPorts.head.id))( _.add(_, AttributeType.DOUBLE) ) ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingAdaptiveBoostingOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingAdaptiveBoostingOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.ensemble import AdaBoostClassifier" override def getUserFriendlyModelName = "Training: Adaptive Boosting" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingBaggingOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingBaggingOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.ensemble import BaggingClassifier" override def getUserFriendlyModelName = "Training: Bagging" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingBernoulliNaiveBayesOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingBernoulliNaiveBayesOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.naive_bayes import BernoulliNB" override def getUserFriendlyModelName = "Training: Bernoulli Naive Bayes" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingComplementNaiveBayesOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingComplementNaiveBayesOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.naive_bayes import ComplementNB" override def getUserFriendlyModelName = "Training: Complement Naive Bayes" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingDecisionTreeOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingDecisionTreeOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.tree import DecisionTreeClassifier" override def getUserFriendlyModelName = "Training: Decision Tree" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingDummyClassifierOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingDummyClassifierOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.dummy import DummyClassifier" override def getUserFriendlyModelName = "Training: Dummy Classifier" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingExtraTreeOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingExtraTreeOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.tree import ExtraTreeClassifier" override def getUserFriendlyModelName = "Training: Extra Tree" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingExtraTreesOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingExtraTreesOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.ensemble import ExtraTreesClassifier" override def getUserFriendlyModelName = "Training: Extra Trees" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingGaussianNaiveBayesOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingGaussianNaiveBayesOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.naive_bayes import GaussianNB" override def getUserFriendlyModelName = "Training: Gaussian Naive Bayes" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingGradientBoostingOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingGradientBoostingOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.ensemble import GradientBoostingClassifier" override def getUserFriendlyModelName = "Training: Gradient Boosting" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingKNNOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingKNNOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.neighbors import KNeighborsClassifier" override def getUserFriendlyModelName = "Training: K-nearest Neighbors" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingLinearRegressionOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingLinearRegressionOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.linear_model import LinearRegression" override def getUserFriendlyModelName = "Training: Linear Regression" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingLinearSVMOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingLinearSVMOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.svm import LinearSVC" override def getUserFriendlyModelName = "Training: Linear Support Vector Machine" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingLogisticRegressionCVOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingLogisticRegressionCVOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.linear_model import LogisticRegressionCV" override def getUserFriendlyModelName = "Training: Logistic Regression Cross Validation" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingLogisticRegressionOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingLogisticRegressionOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.linear_model import LogisticRegression" override def getUserFriendlyModelName = "Training: Logistic Regression" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingMultiLayerPerceptronOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingMultiLayerPerceptronOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.neural_network import MLPClassifier" override def getUserFriendlyModelName = "Training: Multi-layer Perceptron" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingMultinomialNaiveBayesOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingMultinomialNaiveBayesOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.naive_bayes import MultinomialNB" override def getUserFriendlyModelName = "Training: Multinomial Naive Bayes" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingNearestCentroidOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingNearestCentroidOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.neighbors import NearestCentroid" override def getUserFriendlyModelName = "Training: Nearest Centroid" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training import com.fasterxml.jackson.annotation.{JsonIgnore, JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{ JsonSchemaInject, JsonSchemaInt, JsonSchemaString, JsonSchemaTitle } import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity} import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.{ AutofillAttributeName, CommonOpDescAnnotation, HideAnnotation } import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} class SklearnTrainingOpDesc extends PythonOperatorDescriptor { @JsonSchemaTitle("Target Attribute") @JsonPropertyDescription("Attribute in your dataset corresponding to target.") @JsonProperty(required = true) @AutofillAttributeName var target: EncodableString = _ @JsonSchemaTitle("Count Vectorizer") @JsonPropertyDescription("Convert a collection of text documents to a matrix of token counts.") @JsonProperty(defaultValue = "false") var countVectorizer: Boolean = false @JsonSchemaTitle("Text Attribute") @JsonPropertyDescription("Attribute in your dataset with text to vectorize.") @JsonSchemaInject( strings = Array( new JsonSchemaString( path = CommonOpDescAnnotation.autofill, value = CommonOpDescAnnotation.attributeName ), new JsonSchemaString(path = HideAnnotation.hideTarget, value = "countVectorizer"), new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.equals), new JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = "false") ), ints = Array( new JsonSchemaInt(path = CommonOpDescAnnotation.autofillAttributeOnPort, value = 0) ) ) var text: EncodableString = _ @JsonSchemaTitle("Tfidf Transformer") @JsonPropertyDescription("Transform a count matrix to a normalized tf or tf-idf representation.") @JsonProperty(defaultValue = "false") @JsonSchemaInject( strings = Array( new JsonSchemaString(path = HideAnnotation.hideTarget, value = "countVectorizer"), new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.equals), new JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = "false") ) ) var tfidfTransformer: Boolean = false @JsonIgnore def getImportStatements = "from sklearn.ensemble import RandomForestClassifier" @JsonIgnore def getUserFriendlyModelName = "RandomForest Training" override def generatePythonCode(): String = pyb"""$getImportStatements |from sklearn.pipeline import make_pipeline |from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer |import numpy as np |from pytexera import * |class ProcessTableOperator(UDFTableOperator): | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | Y = table[$target] | X = table.drop($target, axis=1) | X = ${if (countVectorizer) pyb"X[$text]" else "X"} | model = make_pipeline(${if (countVectorizer) "CountVectorizer()," else ""} ${if ( tfidfTransformer ) "TfidfTransformer()," else ""} ${getImportStatements.split(" ").last}()).fit(X, Y) | yield {"model_name" : "$getUserFriendlyModelName", "model" : model} | | """.encode override def operatorInfo: OperatorInfo = OperatorInfo( getUserFriendlyModelName, "Sklearn " + getUserFriendlyModelName + " Operator", OperatorGroupConstants.SKLEARN_TRAINING_GROUP, inputPorts = List(InputPort(PortIdentity(), "training")), outputPorts = List(OutputPort(blocking = true)) ) override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { Map( operatorInfo.outputPorts.head.id -> Schema() .add("model_name", AttributeType.STRING) .add("model", AttributeType.BINARY) ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingPassiveAggressiveOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingPassiveAggressiveOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.linear_model import PassiveAggressiveClassifier" override def getUserFriendlyModelName = "Training: Passive Aggressive" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingPerceptronOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingPerceptronOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.linear_model import Perceptron" override def getUserFriendlyModelName = "Training: Linear Perceptron" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingProbabilityCalibrationOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingProbabilityCalibrationOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.calibration import CalibratedClassifierCV" override def getUserFriendlyModelName = "Training: Probability Calibration" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingRandomForestOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingRandomForestOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.ensemble import RandomForestClassifier" override def getUserFriendlyModelName = "Training: Random Forest" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingRidgeCVOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingRidgeCVOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.linear_model import RidgeClassifierCV" override def getUserFriendlyModelName = "Training: Ridge Regression Cross Validation" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingRidgeOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingRidgeOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.linear_model import RidgeClassifier" override def getUserFriendlyModelName = "Training: Ridge Regression" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingSDGOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingSDGOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.linear_model import SGDClassifier" override def getUserFriendlyModelName = "Training: Stochastic Gradient Descent" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sklearn/training/SklearnTrainingSVMOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn.training class SklearnTrainingSVMOpDesc extends SklearnTrainingOpDesc { override def getImportStatements = "from sklearn.svm import SVC" override def getUserFriendlyModelName = "Training: Support Vector Machine" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sleep/SleepOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sleep import com.fasterxml.jackson.annotation.JsonProperty import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PhysicalOp} import org.apache.texera.amber.operator.LogicalOp import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.util.JSONUtils.objectMapper class SleepOpDesc extends LogicalOp { @JsonProperty(required = true) @JsonSchemaTitle("Sleep Time (seconds)") var sleepTime: Int = _ override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.sleep.SleepOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withParallelizable(false) .withSuggestedWorkerNum(1) } override def operatorInfo: OperatorInfo = OperatorInfo( "Sleep", "Sleep n seconds between each tuple", OperatorGroupConstants.CONTROL_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort()) ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sleep/SleepOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sleep import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} import org.apache.texera.amber.util.JSONUtils.objectMapper class SleepOpExec(descString: String) extends OperatorExecutor { private val desc: SleepOpDesc = objectMapper.readValue(descString, classOf[SleepOpDesc]) override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = { Thread.sleep(1000 * desc.sleepTime) Iterator(tuple) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sort/SortCriteriaUnit.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sort import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName class SortCriteriaUnit { @JsonProperty(value = "attribute", required = true) @JsonPropertyDescription("Attribute name to sort by") @AutofillAttributeName var attributeName: EncodableString = _ @JsonProperty(value = "sortPreference", required = true) @JsonPropertyDescription("Sort preference (ASC or DESC)") var sortPreference: SortPreference = _ } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sort/SortOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sort import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import org.apache.texera.amber.core.tuple.Schema import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity} import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} class SortOpDesc extends PythonOperatorDescriptor { @JsonProperty(required = true) @JsonPropertyDescription("column to perform sorting on") var attributes: List[SortCriteriaUnit] = _ override def generatePythonCode(): String = { val attributeName = "[" + attributes .map { criteria => pyb"""${criteria.attributeName}""" } .mkString(", ") + "]" val sortOrders: String = "[" + attributes .map { criteria => criteria.sortPreference match { case SortPreference.ASC => "True" case SortPreference.DESC => "False" } } .mkString(", ") + "]" pyb"""from pytexera import * |import pandas as pd |from datetime import datetime | |class ProcessTableOperator(UDFTableOperator): | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | sort_columns = $attributeName | ascending_orders = $sortOrders | | sorted_df = table.sort_values(by=sort_columns, ascending=ascending_orders) | yield sorted_df""".encode } def getOutputSchemas(inputSchemas: Map[PortIdentity, Schema]): Map[PortIdentity, Schema] = { Map(operatorInfo.outputPorts.head.id -> inputSchemas.values.head) } override def operatorInfo: OperatorInfo = OperatorInfo( "Sort", "Sort based on the columns and sorting methods", OperatorGroupConstants.SORT_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort(blocking = true)) ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sort/SortPreference.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sort; public enum SortPreference { ASC, DESC } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sort/StableMergeSortOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sort import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PhysicalOp} import org.apache.texera.amber.operator.LogicalOp import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.util.JSONUtils.objectMapper import scala.collection.mutable.ListBuffer /** * This operator performs a stable, per-partition sort using an incremental * stack of sorted buckets and pairwise stable merges. The sort keys define * the lexicographic order and per-key direction (ASC/DESC). */ //TODO(#3922): disallowing sorting on binary type class StableMergeSortOpDesc extends LogicalOp { @JsonProperty(value = "keys", required = true) @JsonSchemaTitle("Sort Keys") @JsonPropertyDescription("List of attributes to sort by with ordering preferences") var keys: ListBuffer[SortCriteriaUnit] = _ override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .manyToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.sort.StableMergeSortOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) } override def operatorInfo: OperatorInfo = OperatorInfo( "Stable Merge Sort", "Stable per-partition sort with multi-key ordering (incremental stack of sorted buckets)", OperatorGroupConstants.SORT_GROUP, List(InputPort()), List(OutputPort(blocking = true)) ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sort/StableMergeSortOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sort import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.tuple.{AttributeType, Schema, Tuple, TupleLike} import org.apache.texera.amber.util.JSONUtils.objectMapper import scala.collection.mutable.ArrayBuffer /** * Stable in-memory merge sort for a single input partition. * * Strategy: * - Buffer incoming tuples as size-1 sorted buckets. * - Maintain a stack of buckets where adjacent buckets never share the same length. * - On each push, perform "binary-carry" merges while the top two buckets have equal sizes. * - At finish, collapse the stack left-to-right. Merging is stable (left wins on ties). * * Null policy: * - Nulls are always ordered last, regardless of ascending/descending per key. */ class StableMergeSortOpExec(descString: String) extends OperatorExecutor { private val desc: StableMergeSortOpDesc = objectMapper.readValue(descString, classOf[StableMergeSortOpDesc]) private var inputSchema: Schema = _ /** Sort key resolved against the schema (index, data type, and direction). */ private case class CompiledSortKey( index: Int, attributeType: AttributeType, descending: Boolean ) /** Lexicographic sort keys compiled once on first tuple. */ private var compiledSortKeys: Array[CompiledSortKey] = _ /** Stack of sorted buckets. Invariant: no two adjacent buckets have equal lengths. */ private var sortedBuckets: ArrayBuffer[ArrayBuffer[Tuple]] = _ /** Exposed for testing: current bucket sizes from bottom to top of the stack. */ private[sort] def debugBucketSizes: List[Int] = if (sortedBuckets == null) Nil else sortedBuckets.filter(_ != null).map(_.size).toList /** Initialize internal state. */ override def open(): Unit = { sortedBuckets = ArrayBuffer.empty[ArrayBuffer[Tuple]] } /** Release internal buffers. */ override def close(): Unit = { if (sortedBuckets != null) sortedBuckets.clear() } /** * Ingest a tuple. Defers emission until onFinish. * * Schema compilation happens on the first tuple. * Each tuple forms a size-1 sorted bucket that is pushed and possibly merged. */ override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = { if (inputSchema == null) { inputSchema = tuple.getSchema compiledSortKeys = compileSortKeys(inputSchema) } val sizeOneBucket = ArrayBuffer[Tuple](tuple) pushBucketAndCombine(sizeOneBucket) Iterator.empty } /** * Emit all sorted tuples by collapsing the bucket stack left-to-right. * Stability is preserved because merge prefers the left bucket on equality. */ override def onFinish(port: Int): Iterator[TupleLike] = { if (sortedBuckets.isEmpty) return Iterator.empty var accumulator = sortedBuckets(0) var bucketIdx = 1 while (bucketIdx < sortedBuckets.length) { accumulator = mergeSortedBuckets(accumulator, sortedBuckets(bucketIdx)) bucketIdx += 1 } sortedBuckets.clear() sortedBuckets.append(accumulator) accumulator.iterator } /** * Resolve logical sort keys to schema indices and attribute types. * Outputs an array of compiled sort keys used by [[compareBySortKeys]]. */ private def compileSortKeys(schema: Schema): Array[CompiledSortKey] = { desc.keys.map { sortCriteria: SortCriteriaUnit => val name = sortCriteria.attributeName val index = schema.getIndex(name) val dataType = schema.getAttribute(name).getType val isDescending = sortCriteria.sortPreference == SortPreference.DESC CompiledSortKey(index, dataType, isDescending) }.toArray } /** * Push an already-sorted bucket and perform "binary-carry" merges while the * top two buckets have equal sizes. * * Scope: * - Internal helper. Called by [[processTuple]] for size-1 buckets; * * Expected output: * - Updates the internal stack so that no two adjacent buckets have equal sizes. * * Limitations / possible issues: * - The given bucket must already be sorted by [[compareBySortKeys]]. * - Stability relies on left-before-right merge order; do not reorder parameters. * * Complexity: * - Amortized O(1) per push; total O(n log n) over n tuples. */ private[sort] def pushBucketAndCombine(newBucket: ArrayBuffer[Tuple]): Unit = { sortedBuckets.append(newBucket) // Merge while top two buckets are equal-sized; left-before-right preserves stability. while ( sortedBuckets.length >= 2 && sortedBuckets(sortedBuckets.length - 1).size == sortedBuckets(sortedBuckets.length - 2).size ) { val right = sortedBuckets.remove(sortedBuckets.length - 1) // newer val left = sortedBuckets.remove(sortedBuckets.length - 1) // older val merged = mergeSortedBuckets(left, right) sortedBuckets.append(merged) } } /** * Stable two-way merge of two buckets already sorted by [[compareBySortKeys]]. * * Scope: * - Internal helper used during incremental carries and final collapse. * * Expected output: * - A new bucket with all elements of both inputs, globally sorted. * * Limitations / possible issues: * - Both inputs must be sorted with the same key config; behavior is undefined otherwise. * - Stability guarantee: if keys are equal, the element from the left bucket is emitted first. * * Complexity: * - O(left.size + right.size) */ private[sort] def mergeSortedBuckets( leftBucket: ArrayBuffer[Tuple], rightBucket: ArrayBuffer[Tuple] ): ArrayBuffer[Tuple] = { val outMerged = new ArrayBuffer[Tuple](leftBucket.size + rightBucket.size) var leftIndex = 0 var rightIndex = 0 while (leftIndex < leftBucket.size && rightIndex < rightBucket.size) { if (compareBySortKeys(leftBucket(leftIndex), rightBucket(rightIndex)) <= 0) { outMerged += leftBucket(leftIndex); leftIndex += 1 } else { outMerged += rightBucket(rightIndex); rightIndex += 1 } } while (leftIndex < leftBucket.size) { outMerged += leftBucket(leftIndex); leftIndex += 1 } while (rightIndex < rightBucket.size) { outMerged += rightBucket(rightIndex); rightIndex += 1 } outMerged } /** * Lexicographic comparison of two tuples using the compiled sort keys. * * Semantics: * - Nulls are always ordered last, regardless of sort direction. * - For non-null values, comparison is type-aware (see [[compareTypedNonNullValues]]). * - If a key compares equal, evaluation proceeds to the next key. * - Descending reverses the sign of the base comparison. * * Limitations / possible issues: * - Requires [[compiledSortKeys]] to be initialized; called after the first tuple. * - For unsupported types, [[compareTypedNonNullValues]] throws IllegalStateException. */ private def compareBySortKeys(left: Tuple, right: Tuple): Int = { var keyIndex = 0 while (keyIndex < compiledSortKeys.length) { val currentKey = compiledSortKeys(keyIndex) val leftValue = left.getField[Any](currentKey.index) val rightValue = right.getField[Any](currentKey.index) // Null policy: ALWAYS last, regardless of ASC/DESC if (leftValue == null || rightValue == null) { if (leftValue == null && rightValue == null) { keyIndex += 1 } else { return if (leftValue == null) 1 else -1 } } else { val base = compareTypedNonNullValues(leftValue, rightValue, currentKey.attributeType) if (base != 0) return if (currentKey.descending) -base else base keyIndex += 1 } } 0 } /** * Compare two non-null values using their attribute type. * * Type semantics: * - INTEGER, LONG: numeric ascending via Java primitive compares. * - DOUBLE: java.lang.Double.compare (orders -Inf < ... < +Inf < NaN). * - BOOLEAN: false < true. * - TIMESTAMP: java.sql.Timestamp#compareTo. * - STRING: String#compareTo (UTF-16, lexicographic). * - BINARY: unsigned lexicographic order over byte arrays: * - Compare byte-by-byte treating each as 0..255 (mask 0xff). * - The first differing byte decides the order. * - If all compared bytes are equal, the shorter array sorts first. * - Example: [] < [0x00] < [0x00,0x00] < [0x00,0x01] < [0x7F] < [0x80] < [0xFF]. */ private def compareTypedNonNullValues( leftValue: Any, rightValue: Any, attrType: AttributeType ): Int = attrType match { case AttributeType.INTEGER => java.lang.Integer.compare( leftValue.asInstanceOf[Number].intValue(), rightValue.asInstanceOf[Number].intValue() ) case AttributeType.LONG => java.lang.Long.compare( leftValue.asInstanceOf[Number].longValue(), rightValue.asInstanceOf[Number].longValue() ) case AttributeType.DOUBLE => java.lang.Double.compare( leftValue.asInstanceOf[Number].doubleValue(), rightValue.asInstanceOf[Number].doubleValue() ) case AttributeType.BOOLEAN => java.lang.Boolean.compare(leftValue.asInstanceOf[Boolean], rightValue.asInstanceOf[Boolean]) case AttributeType.TIMESTAMP => leftValue .asInstanceOf[java.sql.Timestamp] .compareTo(rightValue.asInstanceOf[java.sql.Timestamp]) case AttributeType.STRING => leftValue.asInstanceOf[String].compareTo(rightValue.asInstanceOf[String]) case AttributeType.BINARY => java.util.Arrays.compareUnsigned( leftValue.asInstanceOf[Array[Byte]], rightValue.asInstanceOf[Array[Byte]] ) case other => throw new IllegalStateException(s"Unsupported attribute type $other in StableMergeSort") } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sortPartitions/SortPartitionsOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sortPartitions import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PhysicalOp, RangePartition} import org.apache.texera.amber.operator.LogicalOp import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.util.JSONUtils.objectMapper @JsonSchemaInject(json = """ { "attributeTypeRules": { "sortAttributeName":{ "enum": ["integer", "long", "double"] } } } """) class SortPartitionsOpDesc extends LogicalOp { @JsonProperty(required = true) @JsonSchemaTitle("Attribute") @JsonPropertyDescription("Attribute to sort (must be numerical).") @AutofillAttributeName var sortAttributeName: String = _ @JsonProperty(required = true) @JsonSchemaTitle("Attribute Domain Min") @JsonPropertyDescription("Minimum value of the domain of the attribute.") var domainMin: Long = _ @JsonProperty(required = true) @JsonSchemaTitle("Attribute Domain Max") @JsonPropertyDescription("Maximum value of the domain of the attribute.") var domainMax: Long = _ override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.sortPartitions.SortPartitionsOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPartitionRequirement( List(Option(RangePartition(List(sortAttributeName), domainMin, domainMax))) ) override def operatorInfo: OperatorInfo = OperatorInfo( "Sort Partitions", "Sort Partitions", OperatorGroupConstants.SORT_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort(blocking = true)) ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/sortPartitions/SortPartitionsOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sortPartitions import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.tuple.{AttributeTypeUtils, Tuple, TupleLike} import org.apache.texera.amber.util.JSONUtils.objectMapper import scala.collection.mutable.ArrayBuffer class SortPartitionsOpExec(descString: String) extends OperatorExecutor { private val desc: SortPartitionsOpDesc = objectMapper.readValue(descString, classOf[SortPartitionsOpDesc]) private var unorderedTuples: ArrayBuffer[Tuple] = _ override def open(): Unit = { unorderedTuples = new ArrayBuffer[Tuple]() } override def close(): Unit = { unorderedTuples.clear() } private def sortTuples(): Iterator[TupleLike] = unorderedTuples.sortWith(compareTuples).iterator override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = { unorderedTuples.append(tuple) Iterator() } override def onFinish(port: Int): Iterator[TupleLike] = sortTuples() private def compareTuples(tuple1: Tuple, tuple2: Tuple): Boolean = AttributeTypeUtils.compare( tuple1.getField[Any](tuple1.getSchema.getIndex(desc.sortAttributeName)), tuple2.getField[Any](tuple2.getSchema.getIndex(desc.sortAttributeName)), tuple1.getSchema.getAttribute(desc.sortAttributeName).getType ) < 0 } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/BufferedBlockReader.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source; import com.google.common.primitives.Ints; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashSet; import java.util.List; public class BufferedBlockReader { private InputStream input; private long blockSize; private long currentPos; private int cursor; private int bufferSize = 0; private byte[] buffer = new byte[4096]; //4k buffer private ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); private List fields = new ArrayList<>(); private HashSet keptFields = null; private char delimiter; public BufferedBlockReader(InputStream input, long blockSize, char delimiter, int[] kept) { this.input = input; this.blockSize = blockSize; this.delimiter = delimiter; if (kept != null) { this.keptFields = new HashSet<>(Ints.asList(kept)); } } public String[] readLine() throws IOException { outputStream.reset(); fields.clear(); int index = 0; while (true) { if (cursor >= bufferSize) { fillBuffer(); if (bufferSize == -1) { if (outputStream.size() > 0) { fields.add(outputStream.toString()); } return fields.isEmpty() ? null : fields.toArray(new String[0]); } } int start = cursor; while (cursor < bufferSize) { if (buffer[cursor] == delimiter) { addField(start, index); outputStream.reset(); start = cursor + 1; index++; } else if (buffer[cursor] == '\r' || buffer[cursor] == '\n') { // If line ended with '\r\n', all the fields will be outputted when buffer[cursor] == '\r' // And then the cursor will move to '\n' and output Tuple(null) in next readLine() call // The behavior above is the same for either // 1. the current buffer keeps '\r\n' // 2. '\n' comes from the next fillBuffer() call addField(start, index); cursor++; return fields.toArray(new String[0]); } cursor++; } outputStream.write(buffer, start, bufferSize - start); currentPos += bufferSize - start; } } private void fillBuffer() throws IOException { bufferSize = input.read(buffer); cursor = 0; } private void addField(int start, int fieldIndex) { if (keptFields == null || keptFields.contains(fieldIndex)) { if (cursor - start > 0) { outputStream.write(buffer, start, cursor - start); fields.add(outputStream.toString()); } else if (outputStream.size() > 0) { fields.add(outputStream.toString()); } else { fields.add(null); } } currentPos += cursor - start + 1; } public boolean hasNext() throws IOException { return currentPos <= blockSize && bufferSize != -1; } public void close() throws IOException { input.close(); } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/PythonSourceOperatorDescriptor.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source import org.apache.texera.amber.operator.PythonOperatorDescriptor abstract class PythonSourceOperatorDescriptor extends SourceOperatorDescriptor with PythonOperatorDescriptor {} ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/SourceOperatorDescriptor.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source import org.apache.texera.amber.core.tuple.Schema import org.apache.texera.amber.operator.LogicalOp abstract class SourceOperatorDescriptor extends LogicalOp { def sourceSchema(): Schema } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/apis/reddit/RedditSearchSourceOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.apis.reddit import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.{OutputPort, PortIdentity} import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.operator.source.PythonSourceOperatorDescriptor class RedditSearchSourceOpDesc extends PythonSourceOperatorDescriptor { @JsonProperty(required = true) @JsonSchemaTitle("Client Id") @JsonPropertyDescription("Client id that uses to access Reddit API") var clientId: EncodableString = _ @JsonProperty(required = true) @JsonSchemaTitle("Client Secret") @JsonPropertyDescription("Client secret that uses to access Reddit API") var clientSecret: EncodableString = _ @JsonProperty(required = true) @JsonSchemaTitle("Query") @JsonPropertyDescription("Search query") var query: EncodableString = _ @JsonProperty(required = true, defaultValue = "100") @JsonSchemaTitle("Limit") @JsonPropertyDescription("Up to 1000") var limit: Integer = 100 @JsonProperty(required = true, defaultValue = "none") @JsonSchemaTitle("Sorting") @JsonPropertyDescription("The sorting method, hot, new, etc.") var sorting: RedditSourceOperatorFunction = _ override def generatePythonCode(): String = { val clientIdReal: EncodableString = this.clientId.replace("\n", "").trim val clientSecretReal: EncodableString = this.clientSecret.replace("\n", "").trim val queryReal: EncodableString = this.query.replace("\n", "").trim pyb"""from pytexera import * |import praw |from datetime import datetime | |class ProcessTupleOperator(UDFSourceOperator): | client_id = $clientIdReal | client_secret = $clientSecretReal | limit = $limit | query = $queryReal | sorting = '${sorting.getName}' | | @overrides | def produce(self) -> Iterator[Union[TupleLike, TableLike, None]]: | redditInstance = praw.Reddit( | client_id=self.client_id, | client_secret=self.client_secret, | user_agent='chrome:reddit 0.0.0 (by /u/)' | ) | | if len(self.client_id) == 0: | raise ValueError('Client Id cannot be None.') | | if len(self.client_secret) == 0: | raise ValueError('Client Secret cannot be None.') | | if len(self.query) == 0: | raise ValueError('Query cannot be None.') | | if self.limit <= 0 or self.limit > 1000: | raise ValueError('Limit should be larger than 0 and no more than 1000.') | if self.sorting == 'none': | submissions = redditInstance.subreddit('all').search(query=self.query, limit=self.limit) | else: | submissions = redditInstance.subreddit('all').search(query=self.query, limit=self.limit, sort=self.sorting) | for submission in submissions: | author = submission.author | subreddit = str(submission.subreddit.display_name) | edited = None | if type(submission.edited) != type(True): | edited = datetime.fromtimestamp(submission.edited) | tuple_submission = Tuple({ | 'id': submission.id, | 'name': submission.name, | 'title': submission.title, | 'created_utc': datetime.fromtimestamp(submission.created_utc), | 'edited': edited, | 'is_self': submission.is_self, | 'selftext': submission.selftext, | 'over_18': submission.over_18, | 'is_original_content': submission.is_original_content, | 'locked': submission.locked, | 'score': submission.score, | 'upvote_ratio': submission.upvote_ratio, | 'num_comments': submission.num_comments, | 'permalink': submission.permalink, | 'url': submission.url, | 'author_name': author.name, | 'subreddit': subreddit | }) | yield tuple_submission""".encode } override def operatorInfo: OperatorInfo = OperatorInfo( "Reddit Search", "Search for recent posts with python-wrapped Reddit API, PRAW", OperatorGroupConstants.API_GROUP, inputPorts = List.empty, outputPorts = List(OutputPort()) ) override def asSource() = true override def sourceSchema(): Schema = Schema() .add("id", AttributeType.STRING) .add("name", AttributeType.STRING) .add("title", AttributeType.STRING) .add("created_utc", AttributeType.TIMESTAMP) .add("edited", AttributeType.TIMESTAMP) .add("is_self", AttributeType.BOOLEAN) .add("selftext", AttributeType.STRING) .add("over_18", AttributeType.BOOLEAN) .add("is_original_content", AttributeType.BOOLEAN) .add("locked", AttributeType.BOOLEAN) .add("score", AttributeType.INTEGER) .add("upvote_ratio", AttributeType.DOUBLE) .add("num_comments", AttributeType.INTEGER) .add("permalink", AttributeType.STRING) .add("url", AttributeType.STRING) .add("author_name", AttributeType.STRING) .add("subreddit", AttributeType.STRING) def getOutputSchemas(inputSchemas: Map[PortIdentity, Schema]): Map[PortIdentity, Schema] = { Map(operatorInfo.outputPorts.head.id -> sourceSchema()) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/apis/reddit/RedditSourceOperatorFunction.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.apis.reddit; import com.fasterxml.jackson.annotation.JsonValue; public enum RedditSourceOperatorFunction { None("none"), Controversial("controversial"), Gilded("gilded"), Hot("hot"), New("new"), Rising("rising"), Top("top"); private final String name; RedditSourceOperatorFunction(String name) { this.name = name; } // use the name string instead of enum string in JSON @JsonValue public String getName() { return this.name; } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/apis/twitter/TwitterSourceOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.apis.twitter import com.fasterxml.jackson.annotation.{JsonIgnore, JsonProperty} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaDescription, JsonSchemaTitle} import org.apache.texera.amber.core.workflow.OutputPort import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.operator.source.SourceOperatorDescriptor @deprecated("Twitter source operator is no longer executable.", "1.1.0-incubating") abstract class TwitterSourceOpDesc extends SourceOperatorDescriptor { @JsonIgnore val APIName: Option[String] = None @JsonProperty(required = true) @JsonSchemaTitle("API Key") var apiKey: String = _ @JsonProperty(required = true) @JsonSchemaTitle("API Secret Key") var apiSecretKey: String = _ @JsonProperty(required = true, defaultValue = "false") @JsonSchemaTitle("Stop Upon Rate Limit") @JsonSchemaDescription("Stop when hitting rate limit?") var stopWhenRateLimited: Boolean = false override def operatorInfo: OperatorInfo = { OperatorInfo( userFriendlyName = s"Twitter ${APIName.get} API", operatorDescription = s"Retrieve data from Twitter ${APIName.get} API", OperatorGroupConstants.API_GROUP, inputPorts = List.empty, outputPorts = List(OutputPort()) ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/apis/twitter/TwitterSourceOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.apis.twitter import org.apache.texera.amber.core.executor.SourceOperatorExecutor @deprecated("Twitter source operator is no longer executable.", "1.1.0-incubating") abstract class TwitterSourceOpExec( descString: String ) extends SourceOperatorExecutor { override def open(): Unit = throw new UnsupportedOperationException( "Twitter source operator is no longer executable in Apache Texera." ) override def close(): Unit = {} } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/apis/twitter/v2/TwitterFullArchiveSearchSourceOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.apis.twitter.v2 import com.fasterxml.jackson.annotation.{JsonIgnore, JsonProperty} import com.kjetland.jackson.jsonSchema.annotations.{ JsonSchemaDescription, JsonSchemaInject, JsonSchemaTitle } import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{PhysicalOp, SchemaPropagationFunc} import org.apache.texera.amber.operator.metadata.annotations.UIWidget import org.apache.texera.amber.operator.source.apis.twitter.TwitterSourceOpDesc import org.apache.texera.amber.util.JSONUtils.objectMapper @deprecated("Twitter source operator is no longer executable.", "1.1.0-incubating") class TwitterFullArchiveSearchSourceOpDesc extends TwitterSourceOpDesc { @JsonIgnore override val APIName: Option[String] = Some("Full Archive Search") @JsonProperty(required = true) @JsonSchemaTitle("Search Query") @JsonSchemaDescription("Up to 1024 characters (Limited By Twitter)") @JsonSchemaInject(json = UIWidget.UIWidgetTextArea) var searchQuery: String = _ @JsonProperty(required = true, defaultValue = "2021-04-01T00:00:00Z") @JsonSchemaTitle("From Datetime") @JsonSchemaDescription("ISO 8601 format") var fromDateTime: String = _ @JsonProperty(required = true, defaultValue = "2021-05-01T00:00:00Z") @JsonSchemaTitle("To Datetime") @JsonSchemaDescription("ISO 8601 format") var toDateTime: String = _ @JsonProperty(required = true, defaultValue = "100") @JsonSchemaTitle("Limit") @JsonSchemaDescription("Maximum number of tweets to retrieve") var limit: Int = _ override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = // TODO: use multiple workers PhysicalOp .sourcePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.source.apis.twitter.v2.TwitterFullArchiveSearchSourceOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPropagateSchema( SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema())) ) override def sourceSchema(): Schema = { // twitter schema is hard coded for now. V2 API has changed many fields of the Tweet object. // we are also currently depending on redouane59/twittered client library to parse tweet fields. Schema() .add("id", AttributeType.STRING) .add("text", AttributeType.STRING) .add("created_at", AttributeType.TIMESTAMP) .add("lang", AttributeType.STRING) .add("tweet_type", AttributeType.STRING) .add("place_id", AttributeType.STRING) .add("place_coordinate", AttributeType.STRING) .add("in_reply_to_status_id", AttributeType.STRING) .add("in_reply_to_user_id", AttributeType.STRING) .add("like_count", AttributeType.LONG) .add("quote_count", AttributeType.LONG) .add("reply_count", AttributeType.LONG) .add("retweet_count", AttributeType.LONG) .add("hashtags", AttributeType.STRING) .add("symbols", AttributeType.STRING) .add("urls", AttributeType.STRING) .add("mentions", AttributeType.STRING) .add("user_id", AttributeType.STRING) .add("user_created_at", AttributeType.TIMESTAMP) .add("user_name", AttributeType.STRING) .add("user_display_name", AttributeType.STRING) .add("user_lang", AttributeType.STRING) .add("user_description", AttributeType.STRING) .add("user_followers_count", AttributeType.LONG) .add("user_following_count", AttributeType.LONG) .add("user_tweet_count", AttributeType.LONG) .add("user_listed_count", AttributeType.LONG) .add("user_location", AttributeType.STRING) .add("user_url", AttributeType.STRING) .add("user_profile_image_url", AttributeType.STRING) .add("user_pinned_tweet_id", AttributeType.STRING) .add("user_protected", AttributeType.BOOLEAN) .add("user_verified", AttributeType.BOOLEAN) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/apis/twitter/v2/TwitterFullArchiveSearchSourceOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.apis.twitter.v2 import org.apache.texera.amber.core.tuple.TupleLike import org.apache.texera.amber.operator.source.apis.twitter.TwitterSourceOpExec @deprecated("Twitter source operator is no longer executable.", "1.1.0-incubating") class TwitterFullArchiveSearchSourceOpExec( descString: String ) extends TwitterSourceOpExec(descString) { override def produceTuple(): Iterator[TupleLike] = Iterator.empty } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/apis/twitter/v2/TwitterSearchSourceOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.apis.twitter.v2 import com.fasterxml.jackson.annotation.{JsonIgnore, JsonProperty} import com.kjetland.jackson.jsonSchema.annotations.{ JsonSchemaDescription, JsonSchemaInject, JsonSchemaTitle } import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{PhysicalOp, SchemaPropagationFunc} import org.apache.texera.amber.operator.metadata.annotations.UIWidget import org.apache.texera.amber.operator.source.apis.twitter.TwitterSourceOpDesc import org.apache.texera.amber.util.JSONUtils.objectMapper @deprecated("Twitter source operator is no longer executable.", "1.1.0-incubating") class TwitterSearchSourceOpDesc extends TwitterSourceOpDesc { @JsonIgnore override val APIName: Option[String] = Some("Search") @JsonProperty(required = true) @JsonSchemaTitle("Search Query") @JsonSchemaDescription("Up to 1024 characters (Limited by Twitter)") @JsonSchemaInject(json = UIWidget.UIWidgetTextArea) var searchQuery: String = _ @JsonProperty(required = true, defaultValue = "100") @JsonSchemaTitle("Limit") @JsonSchemaDescription("Maximum number of tweets to retrieve") var limit: Int = _ override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = // TODO: use multiple workers PhysicalOp .sourcePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.source.apis.twitter.v2.TwitterSearchSourceOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPropagateSchema( SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema())) ) override def sourceSchema(): Schema = { // twitter schema is hard coded for now. V2 API has changed many fields of the Tweet object. // we are also currently depending on redouane59/twittered client library to parse tweet fields. Schema() .add("id", AttributeType.STRING) .add("text", AttributeType.STRING) .add("created_at", AttributeType.TIMESTAMP) .add("lang", AttributeType.STRING) .add("tweet_type", AttributeType.STRING) .add("place_id", AttributeType.STRING) .add("place_coordinate", AttributeType.STRING) .add("in_reply_to_status_id", AttributeType.STRING) .add("in_reply_to_user_id", AttributeType.STRING) .add("like_count", AttributeType.LONG) .add("quote_count", AttributeType.LONG) .add("reply_count", AttributeType.LONG) .add("retweet_count", AttributeType.LONG) .add("hashtags", AttributeType.STRING) .add("symbols", AttributeType.STRING) .add("urls", AttributeType.STRING) .add("mentions", AttributeType.STRING) .add("user_id", AttributeType.STRING) .add("user_created_at", AttributeType.TIMESTAMP) .add("user_name", AttributeType.STRING) .add("user_display_name", AttributeType.STRING) .add("user_lang", AttributeType.STRING) .add("user_description", AttributeType.STRING) .add("user_followers_count", AttributeType.LONG) .add("user_following_count", AttributeType.LONG) .add("user_tweet_count", AttributeType.LONG) .add("user_listed_count", AttributeType.LONG) .add("user_location", AttributeType.STRING) .add("user_url", AttributeType.STRING) .add("user_profile_image_url", AttributeType.STRING) .add("user_pinned_tweet_id", AttributeType.STRING) .add("user_protected", AttributeType.BOOLEAN) .add("user_verified", AttributeType.BOOLEAN) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/apis/twitter/v2/TwitterSearchSourceOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.apis.twitter.v2 import org.apache.texera.amber.core.tuple.TupleLike import org.apache.texera.amber.operator.source.apis.twitter.TwitterSourceOpExec @deprecated("Twitter source operator is no longer executable.", "1.1.0-incubating") class TwitterSearchSourceOpExec( descString: String ) extends TwitterSourceOpExec(descString) { override def produceTuple(): Iterator[TupleLike] = Iterator.empty } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/cache/CacheSourceOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.cache import com.typesafe.scalalogging.LazyLogging import org.apache.texera.amber.core.executor.SourceOperatorExecutor import org.apache.texera.amber.core.storage.model.VirtualDocument import org.apache.texera.amber.core.storage.{DocumentFactory, VFSResourceType, VFSURIFactory} import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} import java.net.URI class CacheSourceOpExec(storageUri: URI) extends SourceOperatorExecutor with LazyLogging { val (_, _, _, resourceType) = VFSURIFactory.decodeURI(storageUri) if (resourceType != VFSResourceType.RESULT) { throw new RuntimeException("The storage URI must point to a result storage") } private val storage = DocumentFactory.openDocument(storageUri)._1.asInstanceOf[VirtualDocument[Tuple]] override def produceTuple(): Iterator[TupleLike] = storage.get() } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/FileListerSourceOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.dataset import com.fasterxml.jackson.annotation.JsonProperty import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{OutputPort, PhysicalOp, SchemaPropagationFunc} import org.apache.texera.amber.operator.LogicalOp import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.util.JSONUtils.objectMapper class FileListerSourceOpDesc extends LogicalOp { @JsonProperty(required = true) @JsonSchemaTitle("Dataset") var datasetVersionPath: String = _ override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = PhysicalOp .sourcePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.source.dataset.FileListerSourceOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPropagateSchema( SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> Schema().add("filename", AttributeType.STRING)) ) ) override def operatorInfo: OperatorInfo = OperatorInfo( userFriendlyName = "File Lister", operatorDescription = "Select a dataset version and output one filename tuple per file", operatorGroupName = OperatorGroupConstants.INPUT_GROUP, inputPorts = List.empty, outputPorts = List(OutputPort()) ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/FileListerSourceOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.dataset import org.apache.texera.amber.core.executor.SourceOperatorExecutor import org.apache.texera.amber.core.storage.util.LakeFSStorageClient import org.apache.texera.amber.core.tuple.TupleLike import org.apache.texera.amber.util.JSONUtils.objectMapper import org.apache.texera.dao.SqlServer import org.apache.texera.dao.jooq.generated.tables.Dataset.DATASET import org.apache.texera.dao.jooq.generated.tables.DatasetVersion.DATASET_VERSION import org.apache.texera.dao.jooq.generated.tables.User.USER class FileListerSourceOpExec private[dataset] (descString: String) extends SourceOperatorExecutor { private val desc: FileListerSourceOpDesc = objectMapper.readValue(descString, classOf[FileListerSourceOpDesc]) override def produceTuple(): Iterator[TupleLike] = { val Seq(_, ownerEmail, datasetName, versionName, _*) = desc.datasetVersionPath.split("/").toSeq val (repositoryName, versionHash) = SqlServer .getInstance() .createDSLContext() .select(DATASET.REPOSITORY_NAME, DATASET_VERSION.VERSION_HASH) .from(DATASET) .join(USER) .on(USER.UID.eq(DATASET.OWNER_UID)) .join(DATASET_VERSION) .on(DATASET_VERSION.DID.eq(DATASET.DID)) .where(USER.EMAIL.eq(ownerEmail)) .and(DATASET.NAME.eq(datasetName)) .and(DATASET_VERSION.NAME.eq(versionName)) .fetchOne(r => (r.value1(), r.value2())) LakeFSStorageClient .retrieveObjectsOfVersion(repositoryName, versionHash) .map(obj => TupleLike("filename" -> s"${desc.datasetVersionPath}/${obj.getPath}")) .iterator } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/fetcher/DecodingMethod.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.fetcher; import com.fasterxml.jackson.annotation.JsonValue; public enum DecodingMethod { UTF_8("UTF-8"), RAW_BYTES("RAW BYTES"); private final String name; private DecodingMethod(String name) { this.name = name; } // use the name string instead of enum string in JSON @JsonValue public String getName() { return this.name; } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/fetcher/RandomUserAgent.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.fetcher; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; public class RandomUserAgent { private static Map uaMap = new HashMap(); private static Map freqMap = new HashMap(); static { freqMap.put("Internet Explorer", 11.8); freqMap.put("Firefox", 28.2); freqMap.put("Chrome", 52.9); freqMap.put("Safari", 3.9); freqMap.put("Opera", 1.8); uaMap.put("Internet Explorer", new String[]{ "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0", "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)", "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)", "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)", "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/4.0; InfoPath.2; SV1; .NET CLR 2.0.50727; WOW64)", "Mozilla/5.0 (compatible; MSIE 10.0; Macintosh; Intel Mac OS X 10_7_3; Trident/6.0)", "Mozilla/4.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)", "Mozilla/1.22 (compatible; MSIE 10.0; Windows 3.1)", "Mozilla/5.0 (Windows; U; MSIE 9.0; WIndows NT 9.0; en-US))", "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 7.1; Trident/5.0)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; Media Center PC 6.0; InfoPath.3; MS-RTC LM 8; Zune 4.7)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; Media Center PC 6.0; InfoPath.3; MS-RTC LM 8; Zune 4.7", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Zune 4.0; InfoPath.3; MS-RTC LM 8; .NET4.0C; .NET4.0E)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; chromeframe/12.0.742.112)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 2.0.50727; SLCC2; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Zune 4.0; Tablet PC 2.0; InfoPath.3; .NET4.0C; .NET4.0E)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; yie8)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.2; .NET CLR 1.1.4322; .NET4.0C; Tablet PC 2.0)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; FunWebProducts)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; chromeframe/13.0.782.215)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; chromeframe/11.0.696.57)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0) chromeframe/10.0.648.205", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/4.0; GTB7.4; InfoPath.1; SV1; .NET CLR 2.8.52393; WOW64; en-US)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/5.0; chromeframe/11.0.696.57)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/4.0; GTB7.4; InfoPath.3; SV1; .NET CLR 3.1.76908; WOW64; en-US)", "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; GTB7.4; InfoPath.2; SV1; .NET CLR 3.3.69573; WOW64; en-US)", "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)", "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; InfoPath.1; SV1; .NET CLR 3.8.36217; WOW64; en-US)", "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; .NET CLR 2.7.58687; SLCC2; Media Center PC 5.0; Zune 3.4; Tablet PC 3.6; InfoPath.3)", "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; Media Center PC 4.0; SLCC1; .NET CLR 3.0.04320)", "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 1.1.4322)", "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727)", "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; SLCC1; .NET CLR 1.1.4322)", "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.0; Trident/4.0; InfoPath.1; SV1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 3.0.04506.30)", "Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 5.0; Trident/4.0; FBSMTWB; .NET CLR 2.0.34861; .NET CLR 3.0.3746.3218; .NET CLR 3.5.33652; msn OptimizedIE8;ENUS)", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.2; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0)", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; Media Center PC 6.0; InfoPath.2; MS-RTC LM 8)", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; Media Center PC 6.0; InfoPath.2; MS-RTC LM 8", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; Media Center PC 6.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C)", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; InfoPath.3; .NET4.0C; .NET4.0E; .NET CLR 3.5.30729; .NET CLR 3.0.30729; MS-RTC LM 8)", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; InfoPath.2)", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Zune 3.0)", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; msn OptimizedIE8;ZHCN)", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; MS-RTC LM 8; InfoPath.3; .NET4.0C; .NET4.0E) chromeframe/8.0.552.224", "Mozilla/4.0(compatible; MSIE 7.0b; Windows NT 6.0)", "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 6.0)", "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)", "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; Media Center PC 3.0; .NET CLR 1.0.3705; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.1)", "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; FDM; .NET CLR 1.1.4322)", "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322; InfoPath.1; .NET CLR 2.0.50727)", "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322; InfoPath.1)", "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322; Alexa Toolbar; .NET CLR 2.0.50727)", "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322; Alexa Toolbar)", "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.40607)", "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322)", "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.0.3705; Media Center PC 3.1; Alexa Toolbar; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", "Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)", "Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; el-GR)", "Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 5.2)", "Mozilla/5.0 (MSIE 7.0; Macintosh; U; SunOS; X11; gu; SV1; InfoPath.2; .NET CLR 3.0.04506.30; .NET CLR 3.0.04506.648)", "Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 6.0; WOW64; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; c .NET CLR 3.0.04506; .NET CLR 3.5.30707; InfoPath.1; el-GR)", "Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; c .NET CLR 3.0.04506; .NET CLR 3.5.30707; InfoPath.1; el-GR)", "Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 6.0; fr-FR)", "Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 6.0; en-US)", "Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 5.2; WOW64; .NET CLR 2.0.50727)", "Mozilla/5.0 (compatible; MSIE 7.0; Windows 98; SpamBlockerUtility 6.3.91; SpamBlockerUtility 6.2.91; .NET CLR 4.1.89;GB)", "Mozilla/4.79 [en] (compatible; MSIE 7.0; Windows NT 5.0; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 1.1.4322; .NET CLR 3.0.04506.30; .NET CLR 3.0.04506.648)", "Mozilla/4.0 (Windows; MSIE 7.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)", "Mozilla/4.0 (Mozilla/4.0; MSIE 7.0; Windows NT 5.1; FDM; SV1; .NET CLR 3.0.04506.30)", "Mozilla/4.0 (Mozilla/4.0; MSIE 7.0; Windows NT 5.1; FDM; SV1)", "Mozilla/4.0 (compatible;MSIE 7.0;Windows NT 6.0)", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.2; Win64; x64; Trident/6.0; .NET4.0E; .NET4.0C)", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; SLCC2; .NET CLR 2.0.50727; InfoPath.3; .NET4.0C; .NET4.0E; .NET CLR 3.5.30729; .NET CLR 3.0.30729; MS-RTC LM 8)", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; MS-RTC LM 8; .NET4.0C; .NET4.0E; InfoPath.3)", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/6.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E)", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; chromeframe/12.0.742.100)", "Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", "Mozilla/4.0 (compatible; MSIE 6.1; Windows XP)", "Mozilla/4.0 (compatible; MSIE 6.01; Windows NT 6.0)", "Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.1; DigExt)", "Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.1)", "Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.0; YComp 5.0.2.6)", "Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.0; YComp 5.0.0.0) (Compatible; ; ; Trident/4.0)", "Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.0; YComp 5.0.0.0)", "Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.0; .NET CLR 1.1.4322)", "Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.0)", "Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 4.0; .NET CLR 1.0.2914)", "Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 4.0)", "Mozilla/4.0 (compatible; MSIE 6.0b; Windows 98; YComp 5.0.0.0)", "Mozilla/4.0 (compatible; MSIE 6.0b; Windows 98; Win 9x 4.90)", "Mozilla/4.0 (compatible; MSIE 6.0b; Windows 98)", "Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.1)", "Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.0; .NET CLR 1.0.3705)", "Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 4.0)", "Mozilla/5.0 (Windows; U; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)", "Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)", "Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4325)", "Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1)", "Mozilla/45.0 (compatible; MSIE 6.0; Windows NT 5.1)", "Mozilla/4.08 (compatible; MSIE 6.0; Windows NT 5.1)", "Mozilla/4.01 (compatible; MSIE 6.0; Windows NT 5.1)", "Mozilla/4.0 (X11; MSIE 6.0; i686; .NET CLR 1.1.4322; .NET CLR 2.0.50727; FDM)", "Mozilla/4.0 (Windows; MSIE 6.0; Windows NT 6.0)", "Mozilla/4.0 (Windows; MSIE 6.0; Windows NT 5.2)", "Mozilla/4.0 (Windows; MSIE 6.0; Windows NT 5.0)", "Mozilla/4.0 (Windows; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)", "Mozilla/4.0 (MSIE 6.0; Windows NT 5.1)", "Mozilla/4.0 (MSIE 6.0; Windows NT 5.0)", "Mozilla/4.0 (compatible;MSIE 6.0;Windows 98;Q312461)", "Mozilla/4.0 (Compatible; Windows NT 5.1; MSIE 6.0) (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", "Mozilla/4.0 (compatible; U; MSIE 6.0; Windows NT 5.1) (Compatible; ; ; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)", "Mozilla/4.0 (compatible; U; MSIE 6.0; Windows NT 5.1)", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; InfoPath.3; Tablet PC 2.0)", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; GTB6.5; QQDownload 534; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; SLCC2; .NET CLR 2.0.50727; Media Center PC 6.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729)", "Mozilla/4.0 (compatible; MSIE 5.5b1; Mac_PowerPC)", "Mozilla/4.0 (compatible; MSIE 5.50; Windows NT; SiteKiosk 4.9; SiteCoach 1.0)", "Mozilla/4.0 (compatible; MSIE 5.50; Windows NT; SiteKiosk 4.8; SiteCoach 1.0)", "Mozilla/4.0 (compatible; MSIE 5.50; Windows NT; SiteKiosk 4.8)", "Mozilla/4.0 (compatible; MSIE 5.50; Windows 98; SiteKiosk 4.8)", "Mozilla/4.0 (compatible; MSIE 5.50; Windows 95; SiteKiosk 4.8)", "Mozilla/4.0 (compatible;MSIE 5.5; Windows 98)", "Mozilla/4.0 (compatible; MSIE 6.0; MSIE 5.5; Windows NT 5.1)", "Mozilla/4.0 (compatible; MSIE 5.5;)", "Mozilla/4.0 (Compatible; MSIE 5.5; Windows NT5.0; Q312461; SV1; .NET CLR 1.1.4322; InfoPath.2)", "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT5)", "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)", "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 6.1; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)", "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 6.1; chromeframe/12.0.742.100; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C)", "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30618)", "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.5)", "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.2; .NET CLR 1.1.4322; InfoPath.2; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022; FDM)", "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.2; .NET CLR 1.1.4322) (Compatible; ; ; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)", "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.2; .NET CLR 1.1.4322)", "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)", "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)", "Mozilla/4.0 (compatible; MSIE 5.23; Mac_PowerPC)", "Mozilla/4.0 (compatible; MSIE 5.22; Mac_PowerPC)", "Mozilla/4.0 (compatible; MSIE 5.21; Mac_PowerPC)", "Mozilla/4.0 (compatible; MSIE 5.2; Mac_PowerPC)", "Mozilla/4.0 (compatible; MSIE 5.2; Mac_PowerPC)", "Mozilla/4.0 (compatible; MSIE 5.17; Mac_PowerPC)", "Mozilla/4.0 (compatible; MSIE 5.17; Mac_PowerPC Mac OS; en)", "Mozilla/4.0 (compatible; MSIE 5.16; Mac_PowerPC)", "Mozilla/4.0 (compatible; MSIE 5.16; Mac_PowerPC)", "Mozilla/4.0 (compatible; MSIE 5.15; Mac_PowerPC)", "Mozilla/4.0 (compatible; MSIE 5.15; Mac_PowerPC)", "Mozilla/4.0 (compatible; MSIE 5.14; Mac_PowerPC)", "Mozilla/4.0 (compatible; MSIE 5.13; Mac_PowerPC)", "Mozilla/4.0 (compatible; MSIE 5.12; Mac_PowerPC)", "Mozilla/4.0 (compatible; MSIE 5.12; Mac_PowerPC)", "Mozilla/4.0 (compatible; MSIE 5.05; Windows NT 4.0)", "Mozilla/4.0 (compatible; MSIE 5.05; Windows NT 3.51)", "Mozilla/4.0 (compatible; MSIE 5.05; Windows 98; .NET CLR 1.1.4322)", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT; YComp 5.0.0.0)", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT; Hotbar 4.1.8.0)", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT; DigExt)", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT; .NET CLR 1.0.3705)", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT)", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; YComp 5.0.2.6; MSIECrawler)", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; YComp 5.0.2.6; Hotbar 4.2.8.0)", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; YComp 5.0.2.6; Hotbar 3.0)", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; YComp 5.0.2.6)", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; YComp 5.0.2.4)", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; YComp 5.0.0.0; Hotbar 4.1.8.0)", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; YComp 5.0.0.0)", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; Wanadoo 5.6)", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; Wanadoo 5.3; Wanadoo 5.5)", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; Wanadoo 5.1)", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; SV1; .NET CLR 1.1.4322; .NET CLR 1.0.3705; .NET CLR 2.0.50727)", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; SV1)", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; Q312461; T312461)", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; Q312461)", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; MSIECrawler)", "Mozilla/4.0 (compatible; MSIE 5.0b1; Mac_PowerPC)", "Mozilla/4.0 (compatible; MSIE 5.00; Windows 98)", "Mozilla/4.0(compatible; MSIE 5.0; Windows 98; DigExt)", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT;)", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt; YComp 5.0.2.6)", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt; YComp 5.0.2.5)", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt; YComp 5.0.0.0)", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt; Hotbar 4.1.8.0)", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt; Hotbar 3.0)", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt; .NET CLR 1.0.3705)", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT)", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 6.0; Trident/4.0; InfoPath.1; SV1; .NET CLR 3.0.04506.648; .NET4.0C; .NET4.0E)", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 5.9; .NET CLR 1.1.4322)", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 5.2; .NET CLR 1.1.4322)", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 5.0)", "Mozilla/4.0 (compatible; MSIE 5.0; Windows 98;)", "Mozilla/4.0 (compatible; MSIE 5.0; Windows 98; YComp 5.0.2.4)", "Mozilla/4.0 (compatible; MSIE 5.0; Windows 98; Hotbar 3.0)", "Mozilla/4.0 (compatible; MSIE 5.0; Windows 98; DigExt; YComp 5.0.2.6; yplus 1.0)", "Mozilla/4.0 (compatible; MSIE 5.0; Windows 98; DigExt; YComp 5.0.2.6)", "Mozilla/4.0 (compatible; MSIE 4.5; Windows NT 5.1; .NET CLR 2.0.40607)", "Mozilla/4.0 (compatible; MSIE 4.5; Windows 98; )", "Mozilla/4.0 (compatible; MSIE 4.5; Mac_PowerPC)", "Mozilla/4.0 (compatible; MSIE 4.5; Mac_PowerPC)", "Mozilla/4.0 PPC (compatible; MSIE 4.01; Windows CE; PPC; 240x320; Sprint:PPC-6700; PPC; 240x320)", "Mozilla/4.0 (compatible; MSIE 4.01; Windows NT)", "Mozilla/4.0 (compatible; MSIE 4.01; Windows NT 5.0)", "Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; Sprint;PPC-i830; PPC; 240x320)", "Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; Sprint; SCH-i830; PPC; 240x320)", "Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; Sprint:SPH-ip830w; PPC; 240x320)", "Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; Sprint:SPH-ip320; Smartphone; 176x220)", "Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; Sprint:SCH-i830; PPC; 240x320)", "Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; Sprint:SCH-i320; Smartphone; 176x220)", "Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; Sprint:PPC-i830; PPC; 240x320)", "Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; Smartphone; 176x220)", "Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; PPC; 240x320; Sprint:PPC-6700; PPC; 240x320)", "Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; PPC; 240x320; PPC)", "Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; PPC)", "Mozilla/4.0 (compatible; MSIE 4.01; Windows CE)", "Mozilla/4.0 (compatible; MSIE 4.01; Windows 98; Hotbar 3.0)", "Mozilla/4.0 (compatible; MSIE 4.01; Windows 98; DigExt)", "Mozilla/4.0 (compatible; MSIE 4.01; Windows 98)", "Mozilla/4.0 (compatible; MSIE 4.01; Windows 95)", "Mozilla/4.0 (compatible; MSIE 4.01; Mac_PowerPC)", "Mozilla/4.0 WebTV/2.6 (compatible; MSIE 4.0)", "Mozilla/4.0 (compatible; MSIE 4.0; Windows NT)", "Mozilla/4.0 (compatible; MSIE 4.0; Windows 98 )", "Mozilla/4.0 (compatible; MSIE 4.0; Windows 95; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", "Mozilla/4.0 (compatible; MSIE 4.0; Windows 95)", "Mozilla/4.0 (Compatible; MSIE 4.0)", "Mozilla/2.0 (compatible; MSIE 4.0; Windows 98)", "Mozilla/2.0 (compatible; MSIE 3.03; Windows 3.1)", "Mozilla/2.0 (compatible; MSIE 3.02; Windows 3.1)", "Mozilla/2.0 (compatible; MSIE 3.01; Windows 95)", "Mozilla/2.0 (compatible; MSIE 3.01; Windows 95)", "Mozilla/2.0 (compatible; MSIE 3.0B; Windows NT)", "Mozilla/3.0 (compatible; MSIE 3.0; Windows NT 5.0)", "Mozilla/2.0 (compatible; MSIE 3.0; Windows 95)", "Mozilla/2.0 (compatible; MSIE 3.0; Windows 3.1)", "Mozilla/4.0 (compatible; MSIE 2.0; Windows NT 5.0; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0)", "Mozilla/1.22 (compatible; MSIE 2.0; Windows 95)", "Mozilla/1.22 (compatible; MSIE 2.0; Windows 3.1)" }); uaMap.put("Firefox", new String[]{ "Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20120101 Firefox/29.0", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/29.0", "Mozilla/5.0 (X11; OpenBSD amd64; rv:28.0) Gecko/20100101 Firefox/28.0", "Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0", "Mozilla/5.0 (Windows NT 6.1; rv:27.3) Gecko/20130101 Firefox/27.3", "Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:27.0) Gecko/20121011 Firefox/27.0", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/25.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:25.0) Gecko/20100101 Firefox/25.0", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:24.0) Gecko/20100101 Firefox/24.0", "Mozilla/5.0 (Windows NT 6.0; WOW64; rv:24.0) Gecko/20100101 Firefox/24.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20100101 Firefox/24.0", "Mozilla/5.0 (Windows NT 6.2; rv:22.0) Gecko/20130405 Firefox/23.0", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:23.0) Gecko/20130406 Firefox/23.0", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:23.0) Gecko/20131011 Firefox/23.0", "Mozilla/5.0 (Windows NT 6.2; rv:22.0) Gecko/20130405 Firefox/22.0", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:22.0) Gecko/20130328 Firefox/22.0", "Mozilla/5.0 (Windows NT 6.1; rv:22.0) Gecko/20130405 Firefox/22.0", "Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/21.0.1", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/21.0.1", "Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:21.0.0) Gecko/20121011 Firefox/21.0.0", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:21.0) Gecko/20130331 Firefox/21.0", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:21.0) Gecko/20100101 Firefox/21.0", "Mozilla/5.0 (X11; Linux i686; rv:21.0) Gecko/20100101 Firefox/21.0", "Mozilla/5.0 (Windows NT 6.2; rv:21.0) Gecko/20130326 Firefox/21.0", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20130401 Firefox/21.0", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20130331 Firefox/21.0", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20130330 Firefox/21.0", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0", "Mozilla/5.0 (Windows NT 6.1; rv:21.0) Gecko/20130401 Firefox/21.0", "Mozilla/5.0 (Windows NT 6.1; rv:21.0) Gecko/20130328 Firefox/21.0", "Mozilla/5.0 (Windows NT 6.1; rv:21.0) Gecko/20100101 Firefox/21.0", "Mozilla/5.0 (Windows NT 5.1; rv:21.0) Gecko/20130401 Firefox/21.0", "Mozilla/5.0 (Windows NT 5.1; rv:21.0) Gecko/20130331 Firefox/21.0", "Mozilla/5.0 (Windows NT 5.1; rv:21.0) Gecko/20100101 Firefox/21.0", "Mozilla/5.0 (Windows NT 5.0; rv:21.0) Gecko/20100101 Firefox/21.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0", "Mozilla/5.0 (Windows NT 6.2; Win64; x64;) Gecko/20100101 Firefox/20.0", "Mozilla/5.0 (Windows x86; rv:19.0) Gecko/20100101 Firefox/19.0", "Mozilla/5.0 (Windows NT 6.1; rv:6.0) Gecko/20100101 Firefox/19.0", "Mozilla/5.0 (Windows NT 6.1; rv:14.0) Gecko/20100101 Firefox/18.0.1", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:18.0) Gecko/20100101 Firefox/18.0", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:17.0) Gecko/20100101 Firefox/17.0.6", "Mozilla/5.0 (X11; Ubuntu; Linux armv7l; rv:17.0) Gecko/20100101 Firefox/17.0", "Mozilla/6.0 (Windows NT 6.2; WOW64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1", "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1", "Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1", "Mozilla/5.0 (X11; NetBSD amd64; rv:16.0) Gecko/20121102 Firefox/16.0", "Mozilla/5.0 (Windows NT 6.1; rv:15.0) Gecko/20120716 Firefox/15.0a2", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.16) Gecko/20120427 Firefox/15.0a1", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:15.0) Gecko/20120427 Firefox/15.0a1", "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:15.0) Gecko/20120910144328 Firefox/15.0.2", "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:15.0) Gecko/20100101 Firefox/15.0.1", "Mozilla/5.0 (Windows; U; Windows NT 5.1; rv:15.0) Gecko/20121011 Firefox/15.0.1", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:14.0) Gecko/20120405 Firefox/14.0a1", "Mozilla/5.0 (Windows NT 6.1; rv:14.0) Gecko/20120405 Firefox/14.0a1", "Mozilla/5.0 (Windows NT 5.1; rv:14.0) Gecko/20120405 Firefox/14.0a1", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1", "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:14.0) Gecko/20100101 Firefox/14.0.1", "Mozilla/5.0 (Windows; U; Windows NT 6.1; WOW64; en-US; rv:2.0.4) Gecko/20120718 AskTbAVR-IDW/3.12.5.17700 Firefox/14.0.1", "Mozilla/5.0 (Windows NT 6.1; rv:12.0) Gecko/20120403211507 Firefox/14.0.1", "Mozilla/5.0 (Windows NT 6.1; rv:12.0) Gecko/ 20120405 Firefox/14.0.1", "Mozilla/5.0 (Windows NT 6.0; rv:14.0) Gecko/20100101 Firefox/14.0.1", "Mozilla/5.0 (Windows NT 5.1; rv:15.0) Gecko/20100101 Firefox/13.0.1", "Mozilla/5.0 (Windows NT 6.1; rv:12.0) Gecko/20120403211507 Firefox/12.0", "Mozilla/5.0 (Windows NT 6.1; de;rv:12.0) Gecko/20120403211507 Firefox/12.0", "Mozilla/5.0 (Windows NT 5.1; rv:12.0) Gecko/20120403211507 Firefox/12.0", "Mozilla/5.0 (compatible; Windows; U; Windows NT 6.2; WOW64; en-US; rv:12.0) Gecko/20120403211507 Firefox/12.0", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.16) Gecko/20120421 Gecko Firefox/11.0", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.16) Gecko/20120421 Firefox/11.0", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:11.0) Gecko Firefox/11.0", "Mozilla/5.0 (Windows NT 6.1; U;WOW64; de;rv:11.0) Gecko Firefox/11.0", "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0", "Mozilla/6.0 (Macintosh; I; Intel Mac OS X 11_7_9; de-LI; rv:1.9b4) Gecko/2012010317 Firefox/10.0a4", "Mozilla/5.0 (Macintosh; I; Intel Mac OS X 11_7_9; de-LI; rv:1.9b4) Gecko/2012010317 Firefox/10.0a4", "Mozilla/5.0 (X11; Mageia; Linux x86_64; rv:10.0.9) Gecko/20100101 Firefox/10.0.9", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:9.0a2) Gecko/20111101 Firefox/9.0a2", "Mozilla/5.0 (Windows NT 6.2; rv:9.0.1) Gecko/20100101 Firefox/9.0.1", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:9.0) Gecko/20100101 Firefox/9.0", "Mozilla/5.0 (Windows NT 5.1; rv:8.0; en_us) Gecko/20100101 Firefox/8.0", "Mozilla/5.0 (Windows NT 6.1; rv:6.0) Gecko/20100101 Firefox/7.0", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0a2) Gecko/20110613 Firefox/6.0a2", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0a2) Gecko/20110612 Firefox/6.0a2", "Mozilla/5.0 (X11; Linux i686; rv:6.0) Gecko/20100101 Firefox/6.0", "Mozilla/5.0 (Windows NT 6.1; rv:6.0) Gecko/20110814 Firefox/6.0", "Mozilla/5.0 (Windows NT 5.1; rv:6.0) Gecko/20100101 Firefox/6.0 FirePHP/0.6", "Mozilla/5.0 (Windows NT 5.0; WOW64; rv:6.0) Gecko/20100101 Firefox/6.0", "Mozilla/5.0 (X11; Linux i686 on x86_64; rv:5.0a2) Gecko/20110524 Firefox/5.0a2", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.28) Gecko/20120306 Firefox/5.0.1", "Mozilla/5.0 (Windows NT 6.1; U; ru; rv:5.0.1.6) Gecko/20110501 Firefox/5.0.1 Firefox/5.0.1", "mozilla/3.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/5.0.1", "Mozilla/5.0 (X11; U; Linux i586; de; rv:5.0) Gecko/20100101 Firefox/5.0", "Mozilla/5.0 (X11; U; Linux amd64; rv:5.0) Gecko/20100101 Firefox/5.0 (Debian)", "Mozilla/5.0 (X11; U; Linux amd64; en-US; rv:5.0) Gecko/20110619 Firefox/5.0", "Mozilla/5.0 (X11; Linux) Gecko Firefox/5.0", "Mozilla/5.0 (X11; Linux x86_64; rv:5.0) Gecko/20100101 Firefox/5.0 FirePHP/0.5", "Mozilla/5.0 (X11; Linux x86_64; rv:5.0) Gecko/20100101 Firefox/5.0 Firefox/5.0", "Mozilla/5.0 (X11; Linux x86_64) Gecko Firefox/5.0", "Mozilla/5.0 (X11; Linux ppc; rv:5.0) Gecko/20100101 Firefox/5.0", "Mozilla/5.0 (X11; Linux AMD64) Gecko Firefox/5.0", "Mozilla/5.0 (X11; FreeBSD amd64; rv:5.0) Gecko/20100101 Firefox/5.0", "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:5.0) Gecko/20100101 Firefox/5.0", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:5.0) Gecko/20110619 Firefox/5.0", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:5.0) Gecko/20100101 Firefox/5.0", "Mozilla/5.0 (Windows NT 6.1; rv:6.0) Gecko/20100101 Firefox/5.0", "Mozilla/5.0 (Windows NT 6.1.1; rv:5.0) Gecko/20100101 Firefox/5.0", "Mozilla/5.0 (Windows NT 5.2; WOW64; rv:5.0) Gecko/20100101 Firefox/5.0", "Mozilla/5.0 (Windows NT 5.1; U; rv:5.0) Gecko/20100101 Firefox/5.0", "Mozilla/5.0 (Windows NT 5.1; rv:2.0.1) Gecko/20100101 Firefox/5.0", "Mozilla/5.0 (Windows NT 5.0; WOW64; rv:5.0) Gecko/20100101 Firefox/5.0", "Mozilla/5.0 (Windows NT 5.0; rv:5.0) Gecko/20100101 Firefox/5.0", "Mozilla/5.0 (X11; Linux x86_64; rv:2.2a1pre) Gecko/20110324 Firefox/4.2a1pre", "Mozilla/5.0 (X11; Linux x86_64; rv:2.2a1pre) Gecko/20100101 Firefox/4.2a1pre", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.2a1pre) Gecko/20110324 Firefox/4.2a1pre", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.2a1pre) Gecko/20110323 Firefox/4.2a1pre", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.2a1pre) Gecko/20110208 Firefox/4.2a1pre", "Mozilla/5.0 (X11; Linux x86_64; rv:2.0b9pre) Gecko/20110111 Firefox/4.0b9pre", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b9pre) Gecko/20101228 Firefox/4.0b9pre", "Mozilla/5.0 (Windows NT 5.1; rv:2.0b9pre) Gecko/20110105 Firefox/4.0b9pre", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b8pre) Gecko/20101114 Firefox/4.0b8pre", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b8pre) Gecko/20101213 Firefox/4.0b8pre", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b8pre) Gecko/20101128 Firefox/4.0b8pre", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b8pre) Gecko/20101114 Firefox/4.0b8pre", "Mozilla/5.0 (Windows NT 5.1; rv:2.0b8pre) Gecko/20101127 Firefox/4.0b8pre", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0b8) Gecko/20100101 Firefox/4.0b8", "Mozilla/4.0 (compatible; Intel Mac OS X 10.6; rv:2.0b8) Gecko/20100101 Firefox/4.0b8)", "Mozilla/5.0 (Windows NT 6.1; rv:2.0b7pre) Gecko/20100921 Firefox/4.0b7pre", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b7) Gecko/20101111 Firefox/4.0b7", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b7) Gecko/20100101 Firefox/4.0b7", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b6pre) Gecko/20100903 Firefox/4.0b6pre", "Mozilla/5.0 (Windows NT 6.1; rv:2.0b6pre) Gecko/20100903 Firefox/4.0b6pre Firefox/4.0b6pre", "Mozilla/5.0 (X11; Linux x86_64; rv:2.0b4) Gecko/20100818 Firefox/4.0b4", "Mozilla/5.0 (X11; Linux i686; rv:2.0b3pre) Gecko/20100731 Firefox/4.0b3pre", "Mozilla/5.0 (Windows NT 5.2; rv:2.0b13pre) Gecko/20110304 Firefox/4.0b13pre", "Mozilla/5.0 (Windows NT 5.1; rv:2.0b13pre) Gecko/20110223 Firefox/4.0b13pre", "Mozilla/5.0 (X11; Linux i686; rv:2.0b12pre) Gecko/20110204 Firefox/4.0b12pre", "Mozilla/5.0 (X11; Linux i686; rv:2.0b12pre) Gecko/20100101 Firefox/4.0b12pre", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b11pre) Gecko/20110128 Firefox/4.0b11pre", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b11pre) Gecko/20110131 Firefox/4.0b11pre", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b11pre) Gecko/20110129 Firefox/4.0b11pre", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b11pre) Gecko/20110128 Firefox/4.0b11pre", "Mozilla/5.0 (Windows NT 6.1; rv:2.0b11pre) Gecko/20110126 Firefox/4.0b11pre", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0b11pre) Gecko/20110126 Firefox/4.0b11pre", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b10pre) Gecko/20110118 Firefox/4.0b10pre", "Mozilla/5.0 (Windows NT 6.1; rv:2.0b10pre) Gecko/20110113 Firefox/4.0b10pre", "Mozilla/5.0 (X11; Linux i686; rv:2.0b10) Gecko/20100101 Firefox/4.0b10", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:2.0b10) Gecko/20110126 Firefox/4.0b10", "Mozilla/5.0 (Windows NT 6.1; rv:2.0b10) Gecko/20110126 Firefox/4.0b10", "Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9.1.3) Gecko/20091020 Ubuntu/10.04 (lucid) Firefox/4.0.1", "Mozilla/5.0 (X11; Linux x86_64; rv:2.0.1) Gecko/20110506 Firefox/4.0.1", "Mozilla/5.0 (X11; Linux i686; rv:2.0.1) Gecko/20110518 Firefox/4.0.1", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:2.0.1) Gecko/20110606 Firefox/4.0.1", "Mozilla/5.0 (X11; U; Linux x86_64; pl-PL; rv:2.0) Gecko/20110307 Firefox/4.0", "Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:2.0) Gecko/20110404 Fedora/16-dev Firefox/4.0", "Mozilla/5.0 (X11; Arch Linux i686; rv:2.0) Gecko/20110321 Firefox/4.0", "Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.3) Gecko/20100401 Firefox/4.0 (.NET CLR 3.5.30729)", "Mozilla/5.0 (Windows NT 6.1; rv:2.0) Gecko/20110319 Firefox/4.0", "Mozilla/5.0 (Windows NT 6.1; rv:1.9) Gecko/20100101 Firefox/4.0", "Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.2) Gecko/20121223 Ubuntu/9.25 (jaunty) Firefox/3.8", "Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.2) Gecko/2008092313 Ubuntu/9.25 (jaunty) Firefox/3.8", "Mozilla/5.0 (X11; U; Linux i686; it-IT; rv:1.9.0.2) Gecko/2008092313 Ubuntu/9.25 (jaunty) Firefox/3.8", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.3) Gecko/20100401 Mozilla/5.0 (X11; U; Linux i686; it-IT; rv:1.9.0.2) Gecko/2008092313 Ubuntu/9.25 (jaunty) Firefox/3.8", "Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9.3a5pre) Gecko/20100526 Firefox/3.7a5pre", "Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2b5) Gecko/20091204 Firefox/3.6b5", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2b5) Gecko/20091204 Firefox/3.6b5", "Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.2b5) Gecko/20091204 Firefox/3.6b5", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2) Gecko/20091218 Firefox 3.6b5", "Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.2b4) Gecko/20091124 Firefox/3.6b4 (.NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2b4) Gecko/20091124 Firefox/3.6b4", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2b1) Gecko/20091014 Firefox/3.6b1 GTB5", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2a1pre) Gecko/20090428 Firefox/3.6a1pre", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2a1pre) Gecko/20090405 Firefox/3.6a1pre", "Mozilla/5.0 (X11; U; Linux i686; ru-RU; rv:1.9.2a1pre) Gecko/20090405 Ubuntu/9.04 (jaunty) Firefox/3.6a1pre", "Mozilla/5.0 (Windows; Windows NT 5.1; es-ES; rv:1.9.2a1pre) Gecko/20090402 Firefox/3.6a1pre", "Mozilla/5.0 (Windows; Windows NT 5.1; en-US; rv:1.9.2a1pre) Gecko/20090402 Firefox/3.6a1pre", "Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9.2a1pre) Gecko/20090402 Firefox/3.6a1pre (.NET CLR 3.5.30729)", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.9) Gecko/20100915 Gentoo Firefox/3.6.9", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.9) Gecko/20100827 Red Hat/3.6.9-2.el6 Firefox/3.6.9", "Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.9.2.9) Gecko/20100913 Firefox/3.6.9", "Mozilla/5.0 (Windows; U; Windows NT 6.1; rv:1.9.2.9) Gecko/20100913 Firefox/3.6.9", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.2.9) Gecko/20100824 Firefox/3.6.9 ( .NET CLR 3.5.30729; .NET CLR 4.0.20506)", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-GB; rv:1.9.2.9) Gecko/20100824 Firefox/3.6.9", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6;en-US; rv:1.9.2.9) Gecko/20100824 Firefox/3.6.9", "Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.9.2.8) Gecko/20101230 Firefox/3.6.8", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.8) Gecko/20100804 Gentoo Firefox/3.6.8", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.8) Gecko/20100723 SUSE/3.6.8-0.1.1 Firefox/3.6.8", "Mozilla/5.0 (X11; U; Linux i686; zh-CN; rv:1.9.2.8) Gecko/20100722 Ubuntu/10.04 (lucid) Firefox/3.6.8", "Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9.2.8) Gecko/20100723 Ubuntu/10.04 (lucid) Firefox/3.6.8", "Mozilla/5.0 (X11; U; Linux i686; fi-FI; rv:1.9.2.8) Gecko/20100723 Ubuntu/10.04 (lucid) Firefox/3.6.8", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.8) Gecko/20100727 Firefox/3.6.8", "Mozilla/5.0 (X11; U; Linux i686; de-DE; rv:1.9.2.8) Gecko/20100725 Gentoo Firefox/3.6.8", "Mozilla/5.0 (X11; U; FreeBSD i386; de-CH; rv:1.9.2.8) Gecko/20100729 Firefox/3.6.8", "Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8", "Mozilla/5.0 (Windows; U; Windows NT 6.1; pt-BR; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 GTB7.1", "Mozilla/5.0 (Windows; U; Windows NT 6.1; it; rv:1.9.2.8) Gecko/20100722 AskTbADAP/3.9.1.14019 Firefox/3.6.8", "Mozilla/5.0 (Windows; U; Windows NT 6.1; he; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8", "Mozilla/5.0 (Windows; U; Windows NT 6.1; fr; rv:1.9.2.8) Gecko/20100722 Firefox 3.6.8 GTB7.1", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-GB; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 ( .NET CLR 3.5.30729; .NET4.0C)", "Mozilla/5.0 (Windows; U; Windows NT 6.1; de; rv:1.9.2.8) Gecko/20100722 Firefox 3.6.8", "Mozilla/5.0 (Windows; U; Windows NT 6.1; de; rv:1.9.2.3) Gecko/20121221 Firefox/3.6.8", "Mozilla/5.0 (Windows; U; Windows NT 5.2; zh-TW; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8", "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8", "Mozilla/5.0 (Windows; U; Windows NT 5.1; tr; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 ( .NET CLR 3.5.30729; .NET4.0E)", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.7) Gecko/20100809 Fedora/3.6.7-1.fc14 Firefox/3.6.7", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.7) Gecko/20100723 Fedora/3.6.7-1.fc13 Firefox/3.6.7", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.7) Gecko/20100726 CentOS/3.6-3.el5.centos Firefox/3.6.7", "Mozilla/5.0 (Windows; U; Windows NT 6.1; hu; rv:1.9.2.7) Gecko/20100713 Firefox/3.6.7 GTB7.1", "Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.7 (.NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; pt-PT; rv:1.9.2.7) Gecko/20100713 Firefox/3.6.7 (.NET CLR 3.5.30729)", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.6) Gecko/20100628 Ubuntu/10.04 (lucid) Firefox/3.6.6 GTB7.1", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.6) Gecko/20100628 Ubuntu/10.04 (lucid) Firefox/3.6.6 GTB7.0", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.6) Gecko/20100628 Ubuntu/10.04 (lucid) Firefox/3.6.6 (.NET CLR 3.5.30729)", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.6) Gecko/20100628 Ubuntu/10.04 (lucid) Firefox/3.6.6", "Mozilla/5.0 (Windows; U; Windows NT 6.1; pt-PT; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6", "Mozilla/5.0 (Windows; U; Windows NT 6.1; it; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6 ( .NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6 (.NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 6.0; zh-CN; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6 GTB7.1", "Mozilla/5.0 (Windows; U; Windows NT 6.0; nl; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6", "Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6 ( .NET CLR 3.5.30729; .NET4.0E)", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; de-AT; rv:1.9.1.8) Gecko/20100625 Firefox/3.6.6", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.4) Gecko/20100614 Ubuntu/10.04 (lucid) Firefox/3.6.4", "Mozilla/5.0 (X11; U; Linux i686; fa; rv:1.8.1.4) Gecko/20100527 Firefox/3.6.4", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.4) Gecko/20100625 Gentoo Firefox/3.6.4", "Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-TW; rv:1.9.2.4) Gecko/20100611 Firefox/3.6.4 ( .NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4", "Mozilla/5.0 (Windows; U; Windows NT 6.1; ja; rv:1.9.2.4) Gecko/20100611 Firefox/3.6.4 GTB7.1", "Mozilla/5.0 (Windows; U; Windows NT 6.1; cs; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4 (.NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 6.0; zh-CN; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4", "Mozilla/5.0 (Windows; U; Windows NT 6.0; ja; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4 ( .NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 6.0; fr; rv:1.9.2.4) Gecko/20100523 Firefox/3.6.4 ( .NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.4) Gecko/20100527 Firefox/3.6.4 (.NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.4) Gecko/20100527 Firefox/3.6.4", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.4) Gecko/20100523 Firefox/3.6.4 ( .NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4 (.NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-CA; rv:1.9.2.4) Gecko/20100523 Firefox/3.6.4", "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.9.2.4) Gecko/20100611 Firefox/3.6.4 GTB7.0 ( .NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4 (.NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.2.4) Gecko/20100503 Firefox/3.6.4 ( .NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; nb-NO; rv:1.9.2.4) Gecko/20100611 Firefox/3.6.4 (.NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; ko; rv:1.9.2.4) Gecko/20100523 Firefox/3.6.4", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.3pre) Gecko/20100405 Firefox/3.6.3plugin1 ( .NET CLR 3.5.30729)", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; he; rv:1.9.1b4pre) Gecko/20100405 Firefox/3.6.3plugin1", "Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.2.3) Gecko/20100403 Fedora/3.6.3-4.fc13 Firefox/3.6.3", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.3) Gecko/20100403 Firefox/3.6.3", "Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.2.3) Gecko/20100401 SUSE/3.6.3-1.1 Firefox/3.6.3", "Mozilla/5.0 (X11; U; Linux i686; ko-KR; rv:1.9.2.3) Gecko/20100423 Ubuntu/10.04 (lucid) Firefox/3.6.3", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.3) Gecko/20100404 Ubuntu/10.04 (lucid) Firefox/3.6.3", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 GTB7.1", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.3", "Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.3) Gecko/20100423 Ubuntu/10.04 (lucid) Firefox/3.6.3", "Mozilla/5.0 (X11; U; Linux AMD64; en-US; rv:1.9.2.3) Gecko/20100403 Ubuntu/10.10 (maverick) Firefox/3.6.3", "Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 (.NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3", "Mozilla/5.0 (Windows; U; Windows NT 6.1; pl; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3", "Mozilla/5.0 (Windows; U; Windows NT 6.1; it; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3", "Mozilla/5.0 (Windows; U; Windows NT 6.1; hu; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 GTB7.1", "Mozilla/5.0 (Windows; U; Windows NT 6.1; es-ES; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 GTB7.1", "Mozilla/5.0 (Windows; U; Windows NT 6.1; es-ES; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 GTB7.0 ( .NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 6.1; es-ES; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 (.NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 6.1; cs; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 ( .NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 6.1; ca; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 (.NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 6.0; fr; rv:1.9.2.28) Gecko/20120306 Firefox/3.6.28", "Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.9.2.28) Gecko/20120306 AskTbSTC-SRS/3.13.1.18132 Firefox/3.6.28 (.NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.28) Gecko/20120306 Firefox/3.6.28 ( .NET CLR 3.5.30729; .NET4.0C)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9.2.25) Gecko/20111212 Firefox/3.6.25 ( .NET CLR 3.5.30729; .NET4.0C)", "Mozilla/5.0 (X11; U; Linux x86_64; it; rv:1.9.2.24) Gecko/20111101 SUSE/3.6.24-0.2.1 Firefox/3.6.24", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.2.24) Gecko/20111103 Firefox/3.6.24", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.24) Gecko/20111103 Firefox/3.6.24", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; fr; rv:1.9.2.23) Gecko/20110920 Firefox/3.6.23", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10.4; en-US; rv:1.9.2.22) Gecko/20110902 Firefox/3.6.22", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; it; rv:1.9.2.22) Gecko/20110902 Firefox/3.6.22", "Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.21) Gecko/20110830 Ubuntu/10.10 (maverick) Firefox/3.6.21", "Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20", "Mozilla/5.0 (X11; U; Linux x86_64; it; rv:1.9.2.20) Gecko/20110805 Ubuntu/10.04 (lucid) Firefox/3.6.20", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.20) Gecko/20110804 Red Hat/3.6-2.el5 Firefox/3.6.20", "Mozilla/5.0 (Windows; U; Windows NT 6.0; hu; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20", "Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20 ( .NET CLR 3.5.30729; .NET4.0E)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; hu; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20 (.NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.20) Gecko/20110803 AskTbFWV5/3.13.0.17701 Firefox/3.6.20 ( .NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; cs; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20", "Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2", "Mozilla/5.0 (Windows; U; Windows NT 6.1; fr; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2 GTB7.0", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.2) Gecko/20100316 AskTbSPC2/3.9.1.14019 Firefox/3.6.2", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2 (.NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2 ( .NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; pl; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2 GTB6 (.NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2 ( .NET CLR 3.0.04506.648)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2 ( .NET CLR 3.0.04506.30)", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.7; en-US; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.10pre) Gecko/20100902 Ubuntu/9.10 (karmic) Firefox/3.6.1pre", "Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.19", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10.4; en-GB; rv:1.9.2.19) Gecko/20110707 Firefox/3.6.19", "Mozilla/5.0 (X11; U; Linux x86_64; ru; rv:1.9.2.18) Gecko/20110628 Ubuntu/10.10 (maverick) Firefox/3.6.18", "Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.9.2.18) Gecko/20110614 Firefox/3.6.18 ( .NET CLR 3.5.30729; .NET4.0E)", "Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.2.18) Gecko/20110628 Ubuntu/10.10 (maverick) Firefox/3.6.18", "Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.18) Gecko/20110628 Ubuntu/10.10 (maverick) Firefox/3.6.18", "Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.18) Gecko/20110615 Ubuntu/10.10 (maverick) Firefox/3.6.18", "Mozilla/5.0 (Windows; U; Windows NT 6.1; pt-BR; rv:1.9.2.18) Gecko/20110614 Firefox/3.6.18 (.NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 6.1; ar; rv:1.9.2.18) Gecko/20110614 Firefox/3.6.18", "Mozilla/5.0 (Windows; U; Windows NT 6.0; pt-BR; rv:1.9.2.18) Gecko/20110614 Firefox/3.6.18 (.NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.2.18) Gecko/20110614 Firefox/3.6.18 ( .NET CLR 3.5.30729; .NET4.0E)", "Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-GB; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17", "Mozilla/5.0 (X11; Linux i686 on x86_64; rv:5.0) Gecko/20100101 Firefox/3.6.17 Firefox/3.6.17", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17", "Mozilla/5.0 (Windows; U; Windows NT 5.1; tr; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17", "Mozilla/5.0 (Windows; U; Windows NT 5.1; sl; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17 ( .NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; pt-BR; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17 (.NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17 ( .NET CLR 3.5.30729; .NET4.0E)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; hu; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17 (.NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17 ( .NET CLR 3.5.30729; .NET4.0E)", "Mozilla/5.0 (X11; U; Linux x86_64; ja-JP; rv:1.9.2.16) Gecko/20110323 Ubuntu/10.10 (maverick) Firefox/3.6.16", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.16) Gecko/20110323 Ubuntu/9.10 (karmic) Firefox/3.6.16 FirePHP/0.5", "Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16", "Mozilla/5.0 (Windows; U; Windows NT 6.1; fr; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16", "Mozilla/5.0 (Windows; U; Windows NT 6.0; pl; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16", "Mozilla/5.0 (Windows; U; Windows NT 5.1; ko; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16 ( .NET CLR 3.5.30729; .NET4.0E)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16 ( .NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en; rv:1.9.1.13) Gecko/20100914 Firefox/3.6.16", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.2.16) Gecko/20110319 AskTbUTR/3.11.3.15590 Firefox/3.6.16", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.16pre) Gecko/20110304 Ubuntu/10.10 (maverick) Firefox/3.6.15pre", "Mozilla/5.0 (X11; U; Linux i686; nl; rv:1.9.2.15) Gecko/20110303 Ubuntu/8.04 (hardy) Firefox/3.6.15", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.15) Gecko/20110303 Ubuntu/10.04 (lucid) Firefox/3.6.15 FirePHP/0.5", "Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.15) Gecko/20110330 CentOS/3.6-1.el5.centos Firefox/3.6.15", "Mozilla/5.0 (Windows; U; Windows NT 6.1; es-ES; rv:1.9.2.15) Gecko/20110303 Firefox/3.6.15", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.15) Gecko/20110303 Firefox/3.6.15 ( .NET CLR 3.5.30729; .NET4.0C) FirePHP/0.5", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.2.15) Gecko/20110303 AskTbBT4/3.11.3.15590 Firefox/3.6.15 ( .NET CLR 3.5.30729; .NET4.0C)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.15) Gecko/20110303 Firefox/3.6.15 (.NET CLR 3.5.30729)", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.14pre) Gecko/20110105 Firefox/3.6.14pre", "Mozilla/5.0 (X11; U; Linux armv7l; en-US; rv:1.9.2.14) Gecko/20110224 Firefox/3.6.14 MB860/Version.0.43.3.MB860.AmericaMovil.en.MX", "Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.14) Gecko/20110218 Firefox/3.6.14", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-AU; rv:1.9.2.14) Gecko/20110218 Firefox/3.6.14", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.2.14) Gecko/20110218 Firefox/3.6.14 GTB7.1 ( .NET CLR 3.5.30729)", "Mozilla/5.0 Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.2.13) Firefox/3.6.13", "Mozilla/5.0 (X11; U; Linux x86_64; pl-PL; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.04 (lucid) Firefox/3.6.13", "Mozilla/5.0 (X11; U; Linux x86_64; nb-NO; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.04 (lucid) Firefox/3.6.13", "Mozilla/5.0 (X11; U; Linux x86_64; it; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.04 (lucid) Firefox/3.6.13 (.NET CLR 3.5.30729)", "Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.2.13) Gecko/20110103 Fedora/3.6.13-1.fc14 Firefox/3.6.13", "Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101223 Gentoo Firefox/3.6.13", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101219 Gentoo Firefox/3.6.13", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101206 Red Hat/3.6-3.el4 Firefox/3.6.13", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101206 Firefox/3.6.13", "Mozilla/5.0 (X11; U; Linux x86_64; en-NZ; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.10 (maverick) Firefox/3.6.13", "Mozilla/5.0 (X11; U; Linux x86_64; en-GB; rv:1.9.2.13) Gecko/20101206 Ubuntu/9.10 (karmic) Firefox/3.6.13", "Mozilla/5.0 (X11; U; Linux x86_64; en-GB; rv:1.9.2.13) Gecko/20101206 Red Hat/3.6-2.el5 Firefox/3.6.13", "Mozilla/5.0 (X11; U; Linux x86_64; da-DK; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.10 (maverick) Firefox/3.6.13", "Mozilla/5.0 (X11; U; Linux MIPS32 1074Kf CPS QuadCore; en-US; rv:1.9.2.13) Gecko/20110103 Fedora/3.6.13-1.fc14 Firefox/3.6.13", "Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.10 (maverick) Firefox/3.6.13", "Mozilla/5.0 (X11; U; Linux i686; pt-BR; rv:1.9.2.13) Gecko/20101209 Fedora/3.6.13-1.fc13 Firefox/3.6.13", "Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.9.2.13) Gecko/20101206 Ubuntu/9.10 (karmic) Firefox/3.6.13", "Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.13) Gecko/20101209 CentOS/3.6-2.el5.centos Firefox/3.6.13", "Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.10 (maverick) Firefox/3.6.13", "Mozilla/5.0 (X11; U; NetBSD i386; en-US; rv:1.9.2.12) Gecko/20101030 Firefox/3.6.12", "Mozilla/5.0 (X11; U; Linux x86_64; es-MX; rv:1.9.2.12) Gecko/20101027 Ubuntu/10.04 (lucid) Firefox/3.6.12", "Mozilla/5.0 (X11; U; Linux x86_64; es-ES; rv:1.9.2.12) Gecko/20101027 Fedora/3.6.12-1.fc13 Firefox/3.6.12", "Mozilla/5.0 (X11; U; Linux x86_64; es-ES; rv:1.9.2.12) Gecko/20101026 SUSE/3.6.12-0.7.1 Firefox/3.6.12", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.12) Gecko/20101102 Gentoo Firefox/3.6.12", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.12) Gecko/20101102 Firefox/3.6.12", "Mozilla/5.0 (X11; U; Linux ppc; fr; rv:1.9.2.12) Gecko/20101027 Ubuntu/10.10 (maverick) Firefox/3.6.12", "Mozilla/5.0 (X11; U; Linux i686; ko-KR; rv:1.9.2.12) Gecko/20101027 Ubuntu/10.10 (maverick) Firefox/3.6.12", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.12) Gecko/20101114 Gentoo Firefox/3.6.12", "Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.2.12) Gecko/20101027 Ubuntu/10.10 (maverick) Firefox/3.6.12 GTB7.1", "Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.12) Gecko/20101027 Fedora/3.6.12-1.fc13 Firefox/3.6.12", "Mozilla/5.0 (X11; FreeBSD x86_64; rv:2.0) Gecko/20100101 Firefox/3.6.12", "Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12 ( .NET CLR 3.5.30729; .NET4.0E)", "Mozilla/5.0 (Windows; U; Windows NT 6.0; sv-SE; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12 (.NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; .NET CLR 3.5.21022)", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; de; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12 GTB5", "Mozilla/5.0 (X11; U; Linux x86_64; ru; rv:1.9.2.11) Gecko/20101028 CentOS/3.6-2.el5.centos Firefox/3.6.11", "Mozilla/5.0 (X11; U; Linux armv7l; en-GB; rv:1.9.2.3pre) Gecko/20100723 Firefox/3.6.11", "Mozilla/5.0 (Windows; U; Windows NT 5.2; ru; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11", "Mozilla/5.0 (Windows; U; Windows NT 5.2; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11", "Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11 ( .NET CLR 3.5.30729)", "Mozilla/5.0 (X11; U; Linux x86_64; zh-CN; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10", "Mozilla/5.0 (X11; U; Linux x86_64; pt-BR; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10", "Mozilla/5.0 (X11; U; Linux x86_64; pl-PL; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10 GTB7.1", "Mozilla/5.0 (X11; U; Linux x86_64; el-GR; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10", "Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10 GTB7.1", "Mozilla/5.0 (X11; U; Linux x86_64; cs-CZ; rv:1.9.2.10) Gecko/20100915 Ubuntu/10.04 (lucid) Firefox/3.6.10", "Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.2.10) Gecko/20100915 Ubuntu/10.04 (lucid) Firefox/3.6.10", "Mozilla/5.0 (X11; U; Linux i686; fr-FR; rv:1.9.2.10) Gecko/20100914 Firefox/3.6.10", "Mozilla/5.0 (X11; U; Linux i686; es-AR; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.10) Gecko/20100915 Ubuntu/9.04 (jaunty) Firefox/3.6.10", "Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.2.11) Gecko/20101013 Ubuntu/10.10 (maverick) Firefox/3.6.10", "Mozilla/5.0 (X11; U; Linux i686; en-CA; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10", "Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10", "Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.10) Gecko/20100915 Ubuntu/9.10 (karmic) Firefox/3.6.10", "Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.10) Gecko/20100915 Ubuntu/10.04 (lucid) Firefox/3.6.10", "Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.10) Gecko/20100914 SUSE/3.6.10-0.3.1 Firefox/3.6.10", "Mozilla/5.0 (Windows; U; Windows NT 6.1; ro; rv:1.9.2.10) Gecko/20100914 Firefox/3.6.10", "Mozilla/5.0 (Windows; U; Windows NT 6.1; nl; rv:1.9.2.10) Gecko/20100914 Firefox/3.6.10 ( .NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 6.1; fr; rv:1.9.2.10) Gecko/20100914 Firefox/3.6.10 (.NET CLR 3.5.30729)", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.1) Gecko/20100122 firefox/3.6.1", "Mozilla/5.0(Windows; U; Windows NT 7.0; rv:1.9.2) Gecko/20100101 Firefox/3.6", "Mozilla/5.0(Windows; U; Windows NT 5.2; rv:1.9.2) Gecko/20100101 Firefox/3.6", "Mozilla/5.0 (X11; U; x86_64 Linux; en_GB, en_US; rv:1.9.2) Gecko/20100115 Firefox/3.6", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2) Gecko/20100222 Ubuntu/10.04 (lucid) Firefox/3.6", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2) Gecko/20100130 Gentoo Firefox/3.6", "Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.2) Gecko/20100308 Ubuntu/10.04 (lucid) Firefox/3.6", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.2pre) Gecko/20100312 Ubuntu/9.04 (jaunty) Firefox/3.6", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2) Gecko/20100128 Gentoo Firefox/3.6", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2) Gecko/20100115 Ubuntu/10.04 (lucid) Firefox/3.6", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2) Gecko/20100115 Firefox/3.6 FirePHP/0.4", "Mozilla/5.0 (X11; Linux i686; rv:2.0) Gecko/20100101 Firefox/3.6", "Mozilla/5.0 (X11; FreeBSD i686) Firefox/3.6", "Mozilla/5.0 (Windows; U; Windows NT 6.1; ru-RU; rv:1.9.2) Gecko/20100105 MRA 5.6 (build 03278) Firefox/3.6 (.NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 6.1; lt; rv:1.9.2) Gecko/20100115 Firefox/3.6", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.3a3pre) Gecko/20100306 Firefox3.6 (.NET CLR 3.5.30729)", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.8) Gecko/20100806 Firefox/3.6", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.17) Gecko/20110420 Firefox/3.6", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-GB; rv:1.9.2.3) Gecko/20100401 Firefox/3.6;MEGAUPLOAD 1.0", "Mozilla/5.0 (Windows; U; Windows NT 6.1; ar; rv:1.9.2) Gecko/20100115 Firefox/3.6", "Mozilla/5.0 (Windows; U; Windows NT 6.0; ru; rv:1.9.2) Gecko/20100115 Firefox/3.6", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1b5pre) Gecko/20090517 Firefox/3.5b4pre (.NET CLR 3.5.30729)", "Mozilla/5.0 (X11; U; Gentoo Linux x86_64; pl-PL) Gecko Firefox", "Mozilla/5.0 (X11; ; Linux x86_64; rv:1.8.1.6) Gecko/20070802 Firefox", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.0.6) Gecko/2009011913 Firefox", "Mozilla/5.0 (Windows; U; Windows NT 5.1; de-DE; rv:1.9.2.20) Gecko/20110803 Firefox", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; rv:1.8.1.16) Gecko/20080702 Firefox", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.13) Gecko/20080313 Firefox", }); uaMap.put("Chrome", new String[]{ "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.67 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.67 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1944.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.116 Safari/537.36 Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B334b Safari/531.21.10", "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1664.3 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1664.3 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1623.0 Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.17 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.62 Safari/537.36", "Mozilla/5.0 (X11; CrOS i686 4319.74.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.2 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1468.0 Safari/537.36", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1467.0 Safari/537.36", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1464.0 Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1500.55 Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.90 Safari/537.36", "Mozilla/5.0 (X11; NetBSD) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36", "Mozilla/5.0 (X11; CrOS i686 3912.101.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.60 Safari/537.17", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1309.0 Safari/537.17", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.15 (KHTML, like Gecko) Chrome/24.0.1295.0 Safari/537.15", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.14 (KHTML, like Gecko) Chrome/24.0.1292.0 Safari/537.14", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/24.0.1290.1 Safari/537.13", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/24.0.1290.1 Safari/537.13", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/24.0.1290.1 Safari/537.13", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/24.0.1290.1 Safari/537.13", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/24.0.1284.0 Safari/537.13", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.6 Safari/537.11", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.6 Safari/537.11", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.26 Safari/537.11", "Mozilla/5.0 (Windows NT 6.0) yi; AppleWebKit/345667.12221 (KHTML, like Gecko) Chrome/23.0.1271.26 Safari/453667.1221", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.17 Safari/537.11", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_0) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.79 Safari/537.4", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.2 (KHTML, like Gecko) Chrome/22.0.1216.0 Safari/537.2", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1", "Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5", "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.22 (KHTML, like Gecko) Chrome/19.0.1047.0 Safari/535.22", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.21 (KHTML, like Gecko) Chrome/19.0.1042.0 Safari/535.21", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.21 (KHTML, like Gecko) Chrome/19.0.1041.0 Safari/535.21", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.20 (KHTML, like Gecko) Chrome/19.0.1036.7 Safari/535.20", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/18.6.872.0 Safari/535.2 UNTRUSTED/1.0 3gpp-gba UNTRUSTED/1.0", "Mozilla/5.0 (Macintosh; AMD Mac OS X 10_8_2) AppleWebKit/535.22 (KHTML, like Gecko) Chrome/18.6.872", "Mozilla/5.0 (X11; CrOS i686 1660.57.0) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.46 Safari/535.19", "Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.45 Safari/535.19", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.45 Safari/535.19", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.45 Safari/535.19", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Safari/535.19", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.151 Safari/535.19", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.19 (KHTML, like Gecko) Ubuntu/11.10 Chromium/18.0.1025.142 Chrome/18.0.1025.142 Safari/535.19", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.11 Safari/535.19", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11", "Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11", "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/11.10 Chromium/17.0.963.65 Chrome/17.0.963.65 Safari/535.11", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/11.04 Chromium/17.0.963.65 Chrome/17.0.963.65 Safari/535.11", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/10.10 Chromium/17.0.963.65 Chrome/17.0.963.65 Safari/535.11", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/11.10 Chromium/17.0.963.65 Chrome/17.0.963.65 Safari/535.11", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.65 Safari/535.11", "Mozilla/5.0 (X11; FreeBSD amd64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.65 Safari/535.11", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.65 Safari/535.11", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.65 Safari/535.11", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.65 Safari/535.11", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_4) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.65 Safari/535.11", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/11.04 Chromium/17.0.963.56 Chrome/17.0.963.56 Safari/535.11", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11", "Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.12 Safari/535.11", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.8 (KHTML, like Gecko) Chrome/17.0.940.0 Safari/535.8", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.77 Safari/535.7ad-imcjapan-syosyaman-xkgi3lqg03!wgz", "Mozilla/5.0 (X11; CrOS i686 1193.158.0) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.75 Safari/535.7", "Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.75 Safari/535.7", "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.75 Safari/535.7", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.63 Safari/535.7xs5D9rRDFpg2g", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.8 (KHTML, like Gecko) Chrome/16.0.912.63 Safari/535.8", "Mozilla/5.0 (Windows NT 5.2; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.63 Safari/535.7", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.36 Safari/535.7", "Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.36 Safari/535.7", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.36 Safari/535.7", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.6 (KHTML, like Gecko) Chrome/16.0.897.0 Safari/535.6", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.54 Safari/535.2", "Mozilla/5.0 (X11; FreeBSD i386) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.121 Safari/535.2", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.2 (KHTML, like Gecko) Ubuntu/11.10 Chromium/15.0.874.120 Chrome/15.0.874.120 Safari/535.2", "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.120 Safari/535.2", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.872.0 Safari/535.2", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.2 (KHTML, like Gecko) Ubuntu/11.04 Chromium/15.0.871.0 Chrome/15.0.871.0 Safari/535.2", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.864.0 Safari/535.2", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.861.0 Safari/535.2", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.861.0 Safari/535.2", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.861.0 Safari/535.2", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.860.0 Safari/535.2", "Chrome/15.0.860.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/15.0.860.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.186 Safari/535.1", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.834.0 Safari/535.1", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/11.04 Chromium/14.0.825.0 Chrome/14.0.825.0 Safari/535.1", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.824.0 Safari/535.1", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.815.10913 Safari/535.1", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.815.0 Safari/535.1", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/11.04 Chromium/14.0.814.0 Chrome/14.0.814.0 Safari/535.1", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.814.0 Safari/535.1", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/10.04 Chromium/14.0.813.0 Chrome/14.0.813.0 Safari/535.1", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.813.0 Safari/535.1", "Mozilla/5.0 (Windows NT 5.2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.813.0 Safari/535.1", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.813.0 Safari/535.1", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.813.0 Safari/535.1", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.812.0 Safari/535.1", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.811.0 Safari/535.1", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.810.0 Safari/535.1", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.810.0 Safari/535.1", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.809.0 Safari/535.1", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/10.10 Chromium/14.0.808.0 Chrome/14.0.808.0 Safari/535.1", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/10.04 Chromium/14.0.808.0 Chrome/14.0.808.0 Safari/535.1", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/10.04 Chromium/14.0.804.0 Chrome/14.0.804.0 Safari/535.1", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.803.0 Safari/535.1", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/11.04 Chromium/14.0.803.0 Chrome/14.0.803.0 Safari/535.1", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.803.0 Safari/535.1", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.803.0 Safari/535.1", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.803.0 Safari/535.1", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.803.0 Safari/535.1", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.801.0 Safari/535.1", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.801.0 Safari/535.1", "Mozilla/5.0 (Windows NT 5.2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.794.0 Safari/535.1", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.794.0 Safari/535.1", "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.792.0 Safari/535.1", "Mozilla/5.0 (Windows NT 5.2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.792.0 Safari/535.1", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.792.0 Safari/535.1", "Mozilla/5.0 (Macintosh; PPC Mac OS X 10_6_7) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.790.0 Safari/535.1", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.790.0 Safari/535.1", "Mozilla/5.0 (Windows; U; Windows NT 6.1) AppleWebKit/526.3 (KHTML, like Gecko) Chrome/14.0.564.21 Safari/526.3", "Mozilla/5.0 (X11; CrOS i686 13.587.48) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.43 Safari/535.1", "Mozilla/5.0 Slackware/13.37 (X11; U; Linux x86_64; en-US) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41", "Mozilla/5.0 ArchLinux (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/11.04 Chromium/13.0.782.41 Chrome/13.0.782.41 Safari/535.1", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1", "Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1", "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1", "Mozilla/5.0 (Windows NT 5.2; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_3) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_3) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.32 Safari/535.1", "Mozilla/5.0 (X11; Linux amd64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.24 Safari/535.1", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.24 Safari/535.1", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.24 Safari/535.1", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.220 Safari/535.1", "Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.220 Safari/535.1", "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.220 Safari/535.1", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.215 Safari/535.1", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.215 Safari/535.1", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.215 Safari/535.1", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.215 Safari/535.1", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.20 Safari/535.1", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.20 Safari/535.1", "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.20 Safari/535.1", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.20 Safari/535.1", "Mozilla/5.0 (X11; CrOS i686 0.13.587) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.14 Safari/535.1", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.107 Safari/535.1", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.107 Safari/535.1", "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.1 Safari/535.1", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.36 (KHTML, like Gecko) Chrome/13.0.766.0 Safari/534.36", "Mozilla/5.0 (X11; Linux amd64) AppleWebKit/534.36 (KHTML, like Gecko) Chrome/13.0.766.0 Safari/534.36", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.35 (KHTML, like Gecko) Ubuntu/10.10 Chromium/13.0.764.0 Chrome/13.0.764.0 Safari/534.35", "Mozilla/5.0 (X11; CrOS i686 0.13.507) AppleWebKit/534.35 (KHTML, like Gecko) Chrome/13.0.763.0 Safari/534.35", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.33 (KHTML, like Gecko) Ubuntu/9.10 Chromium/13.0.752.0 Chrome/13.0.752.0 Safari/534.33", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/534.31 (KHTML, like Gecko) Chrome/13.0.748.0 Safari/534.31", "Mozilla/5.0 (Windows NT 6.1; en-US) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.750.0 Safari/534.30", "Mozilla/5.0 (X11; CrOS i686 12.433.109) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.93 Safari/534.30", "Mozilla/5.0 (X11; CrOS i686 12.0.742.91) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.93 Safari/534.30", "Mozilla/5.0 Slackware/13.37 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/12.0.742.91", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.91 Chromium/12.0.742.91 Safari/534.30", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.68 Safari/534.30", "Mozilla/5.0 ArchLinux (X11; U; Linux x86_64; en-US) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.60 Safari/534.30", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.53 Safari/534.30", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.113 Safari/534.30", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.30 (KHTML, like Gecko) Ubuntu/11.04 Chromium/12.0.742.112 Chrome/12.0.742.112 Safari/534.30", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.30 (KHTML, like Gecko) Ubuntu/10.10 Chromium/12.0.742.112 Chrome/12.0.742.112 Safari/534.30", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.30 (KHTML, like Gecko) Ubuntu/10.04 Chromium/12.0.742.112 Chrome/12.0.742.112 Safari/534.30", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Ubuntu/11.04 Chromium/12.0.742.112 Chrome/12.0.742.112 Safari/534.30", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Ubuntu/10.10 Chromium/12.0.742.112 Chrome/12.0.742.112 Safari/534.30", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Ubuntu/10.04 Chromium/12.0.742.112 Chrome/12.0.742.112 Safari/534.30", "Mozilla/5.0 (Windows NT 7.1) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.112 Safari/534.30", "Mozilla/5.0 (Windows NT 5.2) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.112 Safari/534.30", "Mozilla/5.0 (Windows 8) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.112 Safari/534.30", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_6) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.112 Safari/534.30", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_4) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.112 Safari/534.30", "Mozilla/5.0 (X11; CrOS i686 12.433.216) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.105 Safari/534.30", "Mozilla/5.0 ArchLinux (X11; U; Linux x86_64; en-US) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30", "Mozilla/5.0 ArchLinux (X11; U; Linux x86_64; en-US) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Slackware/Chrome/12.0.742.100 Safari/534.30", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30", "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_4) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.724.100 Safari/534.30", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/534.25 (KHTML, like Gecko) Chrome/12.0.706.0 Safari/534.25", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/534.25 (KHTML, like Gecko) Chrome/12.0.704.0 Safari/534.25", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Ubuntu/10.10 Chromium/12.0.703.0 Chrome/12.0.703.0 Safari/534.24", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Ubuntu/10.10 Chromium/12.0.702.0 Chrome/12.0.702.0 Safari/534.24", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/12.0.702.0 Safari/534.24", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/12.0.702.0 Safari/534.24", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.700.3 Safari/534.24", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.699.0 Safari/534.24", "Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.699.0 Safari/534.24", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_6) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.698.0 Safari/534.24", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.697.0 Safari/534.24", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.71 Safari/534.24", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.68 Safari/534.24", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.68 Safari/534.24", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.68 Safari/534.24", "Mozilla/5.0 Slackware/13.37 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/11.0.696.50", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.43 Safari/534.24", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.34 Safari/534.24", "Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.34 Safari/534.24", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.3 Safari/534.24", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.3 Safari/534.24", "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.3 Safari/534.24", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.14 Safari/534.24", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.12 Safari/534.24", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_6) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.12 Safari/534.24", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Ubuntu/10.04 Chromium/11.0.696.0 Chrome/11.0.696.0 Safari/534.24", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.0 Safari/534.24", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.694.0 Safari/534.24", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.23 (KHTML, like Gecko) Chrome/11.0.686.3 Safari/534.23", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.21 (KHTML, like Gecko) Chrome/11.0.682.0 Safari/534.21", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.21 (KHTML, like Gecko) Chrome/11.0.678.0 Safari/534.21", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_7_0; en-US) AppleWebKit/534.21 (KHTML, like Gecko) Chrome/11.0.678.0 Safari/534.21", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.20 (KHTML, like Gecko) Chrome/11.0.672.2 Safari/534.20", "Mozilla/5.0 (Windows NT) AppleWebKit/534.20 (KHTML, like Gecko) Chrome/11.0.672.2 Safari/534.20", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; en-US) AppleWebKit/534.20 (KHTML, like Gecko) Chrome/11.0.672.2 Safari/534.20", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.20 (KHTML, like Gecko) Chrome/11.0.669.0 Safari/534.20", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.19 (KHTML, like Gecko) Chrome/11.0.661.0 Safari/534.19", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.18 (KHTML, like Gecko) Chrome/11.0.661.0 Safari/534.18", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; en-US) AppleWebKit/534.18 (KHTML, like Gecko) Chrome/11.0.660.0 Safari/534.18", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/11.0.655.0 Safari/534.17", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/11.0.655.0 Safari/534.17", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/11.0.654.0 Safari/534.17", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/11.0.652.0 Safari/534.17", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17", "Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.82 Safari/534.16", "Mozilla/5.0 (X11; U; Linux armv7l; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16", "Mozilla/5.0 (X11; U; FreeBSD x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16", "Mozilla/5.0 (X11; U; FreeBSD i386; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.134 Safari/534.16", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.134 Safari/534.16", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.134 Safari/534.16", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.134 Safari/534.16", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.648.133 Chrome/10.0.648.133 Safari/534.16", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.133 Safari/534.16", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.648.133 Chrome/10.0.648.133 Safari/534.16", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.133 Safari/534.16", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.133 Safari/534.16", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.133 Safari/534.16", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.133 Safari/534.16", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.648.127 Chrome/10.0.648.127 Safari/534.16", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.127 Safari/534.16", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.127 Safari/534.16", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.127 Safari/534.16", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.11 Safari/534.16", "Mozilla/5.0 (Windows; U; Windows NT 6.1; ru-RU; AppleWebKit/534.16; KHTML; like Gecko; Chrome/10.0.648.11;Safari/534.16)", "Mozilla/5.0 (Windows; U; Windows NT 6.1; ru-RU) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.11 Safari/534.16", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.11 Safari/534.16", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.648.0 Chrome/10.0.648.0 Safari/534.16", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.648.0 Chrome/10.0.648.0 Safari/534.16", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.0 Safari/534.16", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.642.0 Chrome/10.0.642.0 Safari/534.16", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.639.0 Safari/534.16", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.638.0 Safari/534.16", "Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.634.0 Safari/534.16", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.634.0 Safari/534.16", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 SUSE/10.0.626.0 (KHTML, like Gecko) Chrome/10.0.626.0 Safari/534.16", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.15 (KHTML, like Gecko) Chrome/10.0.613.0 Safari/534.15", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.15 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.613.0 Chrome/10.0.613.0 Safari/534.15", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.15 (KHTML, like Gecko) Ubuntu/10.04 Chromium/10.0.612.3 Chrome/10.0.612.3 Safari/534.15", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.15 (KHTML, like Gecko) Chrome/10.0.612.1 Safari/534.15", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.15 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.611.0 Chrome/10.0.611.0 Safari/534.15", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/10.0.602.0 Safari/534.14", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/10.0.601.0 Safari/534.14", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/10.0.601.0 Safari/534.14", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/540.0 (KHTML,like Gecko) Chrome/9.1.0.0 Safari/540.0", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/540.0 (KHTML, like Gecko) Ubuntu/10.10 Chrome/9.1.0.0 Safari/540.0", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/9.0.601.0 Safari/534.14", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Ubuntu/10.10 Chromium/9.0.600.0 Chrome/9.0.600.0 Safari/534.14", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/9.0.600.0 Safari/534.14", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.599.0 Safari/534.13", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-CA) AppleWebKit/534.13 (KHTML like Gecko) Chrome/9.0.597.98 Safari/534.13", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.84 Safari/534.13", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.44 Safari/534.13", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.19 Safari/534.13", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.15 Safari/534.13", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.15 Safari/534.13", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.107 Safari/534.13 v1333515017.9196", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.0 Safari/534.13", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.0 Safari/534.13", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.0 Safari/534.13", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.0 Safari/534.13", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.0 Safari/534.13", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.0 Safari/534.13", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.596.0 Safari/534.13", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Ubuntu/10.04 Chromium/9.0.595.0 Chrome/9.0.595.0 Safari/534.13", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Ubuntu/9.10 Chromium/9.0.592.0 Chrome/9.0.592.0 Safari/534.13", "Mozilla/5.0 (X11; U; Windows NT 6; en-US) AppleWebKit/534.12 (KHTML, like Gecko) Chrome/9.0.587.0 Safari/534.12", "Mozilla/5.0 (Windows U Windows NT 5.1 en-US) AppleWebKit/534.12 (KHTML, like Gecko) Chrome/9.0.583.0 Safari/534.12", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.12 (KHTML, like Gecko) Chrome/9.0.579.0 Safari/534.12", "Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US) AppleWebKit/534.12 (KHTML, like Gecko) Chrome/9.0.576.0 Safari/534.12", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/540.0 (KHTML, like Gecko) Ubuntu/10.10 Chrome/8.1.0.0 Safari/540.0", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.558.0 Safari/534.10", "Mozilla/5.0 (X11; U; CrOS i686 0.9.130; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.344 Safari/534.10", "Mozilla/5.0 (X11; U; CrOS i686 0.9.128; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.343 Safari/534.10", "Mozilla/5.0 (X11; U; CrOS i686 0.9.128; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.341 Safari/534.10", "Mozilla/5.0 (X11; U; CrOS i686 0.9.128; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.339 Safari/534.10", "Mozilla/5.0 (X11; U; CrOS i686 0.9.128; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.339", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Ubuntu/10.10 Chromium/8.0.552.237 Chrome/8.0.552.237 Safari/534.10", "Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.224 Safari/534.10", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/8.0.552.224 Safari/533.3", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.224 Safari/534.10", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.224 Safari/534.10", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.215 Safari/534.10", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.215 Safari/534.10", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.215 Safari/534.10", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.210 Safari/534.10", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.200 Safari/534.10", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.551.0 Safari/534.10", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/7.0.548.0 Safari/534.10", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/7.0.544.0 Safari/534.10", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.15) Gecko/20101027 Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/7.0.540.0 Safari/534.10", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/7.0.540.0 Safari/534.10", "Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/7.0.540.0 Safari/534.10", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/7.0.540.0 Safari/534.10", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.9 (KHTML, like Gecko) Chrome/7.0.531.0 Safari/534.9", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.8 (KHTML, like Gecko) Chrome/7.0.521.0 Safari/534.8", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.24 Safari/534.7", "Mozilla/5.0 (X11; U; Linux x86_64; fr-FR) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.514.0 Safari/534.7", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.514.0 Safari/534.7", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.514.0 Safari/534.7", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.6 (KHTML, like Gecko) Chrome/7.0.500.0 Safari/534.6", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.6 (KHTML, like Gecko) Chrome/7.0.498.0 Safari/534.6", "Mozilla/5.0 (ipad Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.6 (KHTML, like Gecko) Chrome/7.0.498.0 Safari/534.6", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/7.0.0 Safari/700.13", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.4 (KHTML, like Gecko) Chrome/6.0.481.0 Safari/534.4", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.53 Safari/534.3", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.33 Safari/534.3", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.470.0 Safari/534.3", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.464.0 Safari/534.3", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.464.0 Safari/534.3", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.463.0 Safari/534.3", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.462.0 Safari/534.3", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.462.0 Safari/534.3", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.461.0 Safari/534.3", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.461.0 Safari/534.3", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.461.0 Safari/534.3", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.460.0 Safari/534.3", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.460.0 Safari/534.3", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.460.0 Safari/534.3", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.459.0 Safari/534.3", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.458.1 Safari/534.3", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.458.1 Safari/534.3", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.458.1 Safari/534.3", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.458.1 Safari/534.3", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.458.1 Safari/534.3", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.458.0 Safari/534.3", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.458.0 Safari/534.3", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.457.0 Safari/534.3", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.456.0 Safari/534.3", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.2 (KHTML, like Gecko) Chrome/6.0.454.0 Safari/534.2", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.2 (KHTML, like Gecko) Chrome/6.0.454.0 Safari/534.2", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.2 (KHTML, like Gecko) Chrome/6.0.453.1 Safari/534.2", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-US) AppleWebKit/534.2 (KHTML, like Gecko) Chrome/6.0.453.1 Safari/534.2", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/534.2 (KHTML, like Gecko) Chrome/6.0.453.1 Safari/534.2", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.2 (KHTML, like Gecko) Chrome/6.0.451.0 Safari/534.2", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.1 SUSE/6.0.428.0 (KHTML, like Gecko) Chrome/6.0.428.0 Safari/534.1", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.428.0 Safari/534.1", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-GB) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.428.0 Safari/534.1", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-US) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.428.0 Safari/534.1", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.427.0 Safari/534.1", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.422.0 Safari/534.1", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.417.0 Safari/534.1", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.416.0 Safari/534.1", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.414.0 Safari/534.1", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.9 (KHTML, like Gecko) Chrome/6.0.400.0 Safari/533.9", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.8 (KHTML, like Gecko) Chrome/6.0.397.0 Safari/533.8", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/6.0", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.999 Safari/533.4", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_0; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.86 Safari/533.4", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.86 Safari/533.4", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_0; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.86 Safari/533.4", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.70 Safari/533.4", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.127 Safari/533.4", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.126 Safari/533.4", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; fr-FR) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.126 Safari/533.4", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.125 Safari/533.4", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.370.0 Safari/533.4", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.368.0 Safari/533.4", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.366.2 Safari/533.4", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.366.0 Safari/533.4", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.366.0 Safari/533.4", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.363.0 Safari/533.3", "Mozilla/5.0 (X11; U; OpenBSD i386; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.359.0 Safari/533.3", "Mozilla/5.0 (X11; U; x86_64 Linux; en_GB, en_US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.358.0 Safari/533.3", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.358.0 Safari/533.3", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.358.0 Safari/533.3", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.357.0 Safari/533.3", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.356.0 Safari/533.3", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.355.0 Safari/533.3", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.354.0 Safari/533.3", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.354.0 Safari/533.3", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.353.0 Safari/533.3", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.353.0 Safari/533.3", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.343.0 Safari/533.2", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.343.0 Safari/533.2", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_7_0; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.7 Safari/533.2", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.7 Safari/533.2", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.5 Safari/533.2", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.3 Safari/533.2", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.3 Safari/533.2", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.2 Safari/533.2", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2", "Mozilla/5.0 (X11; U; Linux i586; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.1 (KHTML, like Gecko) Chrome/5.0.335.0 Safari/533.1", "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/533.16 (KHTML, like Gecko) Chrome/5.0.335.0 Safari/533.16", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.9 (KHTML, like Gecko) Chrome/5.0.310.0 Safari/532.9", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.9 (KHTML, like Gecko) Chrome/5.0.309.0 Safari/532.9", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.9 (KHTML, like Gecko) Chrome/5.0.308.0 Safari/532.9", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_0; en-US) AppleWebKit/532.9 (KHTML, like Gecko) Chrome/5.0.307.11 Safari/532.9", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.9 (KHTML, like Gecko) Chrome/5.0.307.1 Safari/532.9", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.1.249.1025 Safari/532.5", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/532.8 (KHTML, like Gecko) Chrome/4.0.302.2 Safari/532.8", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.8 (KHTML, like Gecko) Chrome/4.0.288.1 Safari/532.8", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.8 (KHTML, like Gecko) Chrome/4.0.277.0 Safari/532.8", "Mozilla/5.0 (X11; U; Slackware Linux x86_64; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.30 Safari/532.5", "Mozilla/5.0 (Windows; U; Windows NT 6.1; it-IT) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.25 Safari/532.5", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.0 Safari/532.5", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_8; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.0 Safari/532.5", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.246.0 Safari/532.5", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.4 (KHTML, like Gecko) Chrome/4.0.241.0 Safari/532.4", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.4 (KHTML, like Gecko) Chrome/4.0.237.0 Safari/532.4 Debian", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.3 (KHTML, like Gecko) Chrome/4.0.227.0 Safari/532.3", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.3 (KHTML, like Gecko) Chrome/4.0.224.2 Safari/532.3", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.3 (KHTML, like Gecko) Chrome/4.0.223.5 Safari/532.3", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.4 Safari/532.2", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.3 Safari/532.2", "Mozilla/5.0 (Windows; U; Windows NT 5.1; de-DE) Chrome/4.0.223.3 Safari/532.2", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.2 Safari/532.2", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.2 Safari/532.2", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.2 Safari/532.2", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.2 Safari/532.2", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.1 Safari/532.2", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.1 Safari/532.2", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.1 Safari/532.2", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.0 Safari/532.2", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.8 Safari/532.2", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.7 Safari/532.2", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.6 Safari/532.2", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.6 Safari/532.2", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.6 Safari/532.2", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.5 Safari/532.2", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.5 Safari/532.2", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.5 Safari/532.2", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.5 Safari/532.2", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.4 Safari/532.2", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.4 Safari/532.2", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.4 Safari/532.2", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.4 Safari/532.2", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.3 Safari/532.2", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.3 Safari/532.2", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.3 Safari/532.2", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.2 Safari/532.2", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.2 Safari/532.2", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.12 Safari/532.2", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.12 Safari/532.2", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.12 Safari/532.2", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.1 Safari/532.2", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.0 Safari/532.2", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.8 Safari/532.2", "Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.8 Safari/532.2", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.8 Safari/532.2", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.8 Safari/532.2", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.7 Safari/532.2", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.6 Safari/532.2", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.6 Safari/532.2", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.6 Safari/532.2", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.3 Safari/532.2", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.0 Safari/532.2", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.220.1 Safari/532.1", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.6 Safari/532.1", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.5 Safari/532.1", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.5 Safari/532.1", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.4 Safari/532.1", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.3 Safari/532.1", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.3 Safari/532.1", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.3 Safari/532.1", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.0 Safari/532.1", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.1 Safari/532.1", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.1 Safari/532.1", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.1 Safari/532.1", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.1 Safari/532.1", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.1 Safari/532.1", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.1 Safari/532.1", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.0 Safari/532.1", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.0 Safari/532.1", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.0 Safari/532.1", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.0 Safari/532.1", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_0; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.212.1 Safari/532.1", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.212.1 Safari/532.1", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.212.0 Safari/532.0", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.212.0 Safari/532.1", "Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.212.0 Safari/532.0", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.212.0 Safari/532.0", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.212.0 Safari/532.0", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.212.0 Safari/532.0", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.212.0 Safari/532.0", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.7 Safari/532.0", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.7 Safari/532.0", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.4 Safari/532.0", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.4 Safari/532.0", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.4 Safari/532.0", "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.2 Safari/532.0", }); uaMap.put("Safari", new String[]{ "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/419 (KHTML, like Gecko) Safari/419.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/418.9 (KHTML, like Gecko) Safari/419.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/418.9.1 (KHTML, like Gecko) Safari/419.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fi-fi) AppleWebKit/418.8 (KHTML, like Gecko) Safari/419.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es-es) AppleWebKit/418.8 (KHTML, like Gecko) Safari/419.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es) AppleWebKit/419 (KHTML, like Gecko) Safari/419.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en_CA) AppleWebKit/419 (KHTML, like Gecko) Safari/419.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/419 (KHTML, like Gecko) Safari/419.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/418.9 (KHTML, like Gecko) Safari/419.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/418.8 (KHTML, like Gecko) Safari/419.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/419 (KHTML, like Gecko) Safari/419.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418.9.1 (KHTML, like Gecko) Safari/419.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418.9 (KHTML, like Gecko) Safari/419.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; tr-tr) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; sv-se) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; sv-se) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8_Adobe", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nl-nl) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nl-nl) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.9.2", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nl-nl) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nb-no) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nb-no) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.9.2", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/417.9 (KHTML, like Gecko)", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.2", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.9.2", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.2", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nl-nl) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nl-nl) AppleWebKit/416.11 (KHTML, like Gecko) Safari/416.12", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nl-nl) AppleWebKit/416.11 (KHTML, like Gecko) Safari/312", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nb-no) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ja-jp) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/416.11 (KHTML, like Gecko) Safari/416.12", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13_Adobe", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/416.12 (KHTML, like Gecko) Safari/412.5", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/416.11 (KHTML, like Gecko) Safari/416.12", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/416.11 (KHTML, like Gecko) Safari/416.12", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-ca) AppleWebKit/416.11 (KHTML, like Gecko) Safari/416.12", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/416.11 (KHTML, like Gecko) Safari/416.12", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/416.11 (KHTML, like Gecko)", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13_Adobe", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ja-jp) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.6", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5_Adobe", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5", "Mozilla/5.0 (Macintosh; U; PPC Mac OS; pl-pl) AppleWebKit/412 (KHTML, like Gecko) Safari/412", "Mozilla/5.0 (Macintosh; U; PPC Mac OS; en-en) AppleWebKit/412 (KHTML, like Gecko) Safari/412", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/412.6 (KHTML, like Gecko) Safari/412.2", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/412 (KHTML, like Gecko) Safari/412", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/412.6 (KHTML, like Gecko) Safari/412.2", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/412 (KHTML, like Gecko) Safari/412", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es-ES) AppleWebKit/412 (KHTML, like Gecko) Safari/412", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en_US) AppleWebKit/412 (KHTML, like Gecko) Safari/412", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/412.6 (KHTML, like Gecko) Safari/412.2", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/412 (KHTML, like Gecko) Safari/412 Privoxy/3.0", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/412 (KHTML, like Gecko) Safari/412", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/412.6.2 (KHTML, like Gecko) Safari/412.2.2", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/412.6.2 (KHTML, like Gecko)", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/412.6 (KHTML, like Gecko) Safari/412.2", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/412 (KHTML, like Gecko) Safari/412", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/412.6.2 (KHTML, like Gecko) Safari/412.2.2", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/412.6 (KHTML, like Gecko) Safari/412.2_Adobe", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/412.6 (KHTML, like Gecko) Safari/412.2", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/412.6 (KHTML, like Gecko)", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/412 (KHTML, like Gecko) Safari/412", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; sv-se) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.5", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.6", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.6", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.5", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.5", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.8.1 (KHTML, like Gecko) Safari/312.6", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.6", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.5", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.6", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.5", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.3.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/312.8.1 (KHTML, like Gecko) Safari/312.6", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.5_Adobe", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.5", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; sv-se) AppleWebKit/312.5.2 (KHTML, like Gecko) Safari/312.3.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; sv-se) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ja-jp) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.5.2 (KHTML, like Gecko) Safari/312.3.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.5 (KHTML, like Gecko) Safari/312.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/312.5.2 (KHTML, like Gecko) Safari/312.3.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/312.5 (KHTML, like Gecko) Safari/312.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es-es) AppleWebKit/312.5.2 (KHTML, like Gecko) Safari/312.3.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.5 (KHTML, like Gecko) Safari/312.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.5.2 (KHTML, like Gecko) Safari/312.3.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.5.2 (KHTML, like Gecko) Safari/125", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/125.9", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.5 (KHTML, like Gecko) Safari/312.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/312.5.2 (KHTML, like Gecko) Safari/312.3.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.1.1 (KHTML, like Gecko) Safari/312", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.1 (KHTML, like Gecko) Safari/125", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-ch) AppleWebKit/312.1.1 (KHTML, like Gecko) Safari/312", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-ca) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.1 (KHTML, like Gecko)", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.1.1 (KHTML, like Gecko) Safari/312", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/312.1.1 (KHTML, like Gecko) Safari/312", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312.3.1", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-ch) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/125.5.6 (KHTML, like Gecko) Safari/125.12", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.12", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.11", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-ch) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.12", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-ch) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.11", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/125.5.7 (KHTML, like Gecko) Safari/125.12", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/125.5.6 (KHTML, like Gecko) Safari/125.12", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.12", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.11", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5.7 (KHTML, like Gecko) Safari/125.12", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5.6 (KHTML, like Gecko) Safari/125.12", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.5.5", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.12", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.11", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.5.7 (KHTML, like Gecko) Safari/125.12", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.5.6 (KHTML, like Gecko) Safari/125.12_Adobe", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.5.6 (KHTML, like Gecko) Safari/125.12", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.12_Adobe", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.12", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ja-jp) AppleWebKit/125.4 (KHTML, like Gecko) Safari/125.9", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/125.5 (KHTML, like Gecko) Safari/125.9", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/125.4 (KHTML, like Gecko) Safari/125.9", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en_CA) AppleWebKit/125.4 (KHTML, like Gecko) Safari/125.9", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/125.4 (KHTML, like Gecko) Safari/125.9", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-au) AppleWebKit/125.4 (KHTML, like Gecko) Safari/125.9", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5 (KHTML, like Gecko) Safari/125.9", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.4 (KHTML, like Gecko) Safari/125.9", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.4 (KHTML, like Gecko) Safari/100", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.4 (KHTML, like Gecko) Safari/125.9", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es-es) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.8", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.7", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-gb) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.8", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.2 (KHTML, like Gecko) Safari/85.8", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.8", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.7", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.8", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.8", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.7", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/124 (KHTML, like Gecko) Safari/125.1", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/124 (KHTML, like Gecko) Safari/125", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/124 (KHTML, like Gecko) Safari/125", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/124 (KHTML, like Gecko)", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/124 (KHTML, like Gecko) Safari/125.1", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/124 (KHTML, like Gecko) Safari/125", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/85.8.5 (KHTML, like Gecko) Safari/85.8.1", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/85.8.5 (KHTML, like Gecko) Safari/85.8.1", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/85.8.5 (KHTML, like Gecko) Safari/85.8.1", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/85.8.2 (KHTML, like Gecko) Safari/85.8", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-gb) AppleWebKit/85.8.5 (KHTML, like Gecko) Safari/85.8.1", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/85.8.5 (KHTML, like Gecko) Safari/85.8.1", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/85.8.2 (KHTML, like Gecko) Safari/85.8.1", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/85.8.5 (KHTML, like Gecko) Safari/85.8.1", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/85.8.5 (KHTML, like Gecko) Safari/85", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/85.8.2 (KHTML, like Gecko) Safari/85.8", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; sv-se) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.5", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ja-jp) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.5", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.5", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.5", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.6", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.5", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.7", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.5", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.3) Gecko/2008092816 Mobile Safari 1.1.3", "Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN) AppleWebKit/533+ (KHTML, like Gecko)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/528.8 (KHTML, like Gecko)", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/534.34 (KHTML, like Gecko) Dooble/1.40 Safari/534.34", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fi-fi) AppleWebKit/420+ (KHTML, like Gecko) Safari/419.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.8.1 (KHTML, like Gecko) Safari/312.6", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/419.2 (KHTML, like Gecko) Safari/419.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/418.9.1 (KHTML, like Gecko) Safari/419.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-ch) AppleWebKit/85 (KHTML, like Gecko) Safari/85", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-CH) AppleWebKit/419.2 (KHTML, like Gecko) Safari/419.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; da-dk) AppleWebKit/522+ (KHTML, like Gecko) Safari/419.3", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_6; en-us) AppleWebKit/528.16 (KHTML, like Gecko)", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; it-IT) AppleWebKit/521.25 (KHTML, like Gecko) Safari/521.24", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-us) AppleWebKit/419.2.1 (KHTML, like Gecko) Safari/419.3", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/522.11.1 (KHTML, like Gecko) Safari/419.3", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/521.32.1 (KHTML, like Gecko) Safari/521.32.1", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit (KHTML, like Gecko)", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; es-es) AppleWebKit/531.22.7 (KHTML, like Gecko)", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-us) AppleWebKit/528.16 (KHTML, like Gecko)", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_5; it-it) AppleWebKit/525.18 (KHTML, like Gecko)", }); uaMap.put("Opera", new String[]{ "Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14", "Mozilla/5.0 (Windows NT 6.0; rv:2.0) Gecko/20100101 Firefox/4.0 Opera 12.14", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0) Opera 12.14", "Opera/12.80 (Windows NT 5.1; U; en) Presto/2.10.289 Version/12.02", "Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00", "Opera/9.80 (Windows NT 5.1; U; zh-sg) Presto/2.9.181 Version/12.00", "Opera/12.0(Windows NT 5.2;U;en)Presto/22.9.168 Version/12.00", "Opera/12.0(Windows NT 5.1;U;en)Presto/22.9.168 Version/12.00", "Mozilla/5.0 (Windows NT 5.1) Gecko/20100101 Firefox/14.0 Opera/12.0", "Opera/9.80 (Windows NT 6.1; WOW64; U; pt) Presto/2.10.229 Version/11.62", "Opera/9.80 (Windows NT 6.0; U; pl) Presto/2.10.229 Version/11.62", "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; fr) Presto/2.9.168 Version/11.52", "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; de) Presto/2.9.168 Version/11.52", "Opera/9.80 (Windows NT 5.1; U; en) Presto/2.9.168 Version/11.51", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; de) Opera 11.51", "Opera/9.80 (X11; Linux x86_64; U; fr) Presto/2.9.168 Version/11.50", "Opera/9.80 (X11; Linux i686; U; hu) Presto/2.9.168 Version/11.50", "Opera/9.80 (X11; Linux i686; U; ru) Presto/2.8.131 Version/11.11", "Opera/9.80 (X11; Linux i686; U; es-ES) Presto/2.8.131 Version/11.11", "Mozilla/5.0 (Windows NT 5.1; U; en; rv:1.8.1) Gecko/20061208 Firefox/5.0 Opera 11.11", "Opera/9.80 (X11; Linux x86_64; U; bg) Presto/2.8.131 Version/11.10", "Opera/9.80 (Windows NT 6.0; U; en) Presto/2.8.99 Version/11.10", "Opera/9.80 (Windows NT 5.1; U; zh-tw) Presto/2.8.131 Version/11.10", "Opera/9.80 (Windows NT 6.1; Opera Tablet/15165; U; en) Presto/2.8.149 Version/11.1", "Opera/9.80 (X11; Linux x86_64; U; Ubuntu/10.10 (maverick); pl) Presto/2.7.62 Version/11.01", "Opera/9.80 (X11; Linux i686; U; ja) Presto/2.7.62 Version/11.01", "Opera/9.80 (X11; Linux i686; U; fr) Presto/2.7.62 Version/11.01", "Opera/9.80 (Windows NT 6.1; U; zh-tw) Presto/2.7.62 Version/11.01", "Opera/9.80 (Windows NT 6.1; U; zh-cn) Presto/2.7.62 Version/11.01", "Opera/9.80 (Windows NT 6.1; U; sv) Presto/2.7.62 Version/11.01", "Opera/9.80 (Windows NT 6.1; U; en-US) Presto/2.7.62 Version/11.01", "Opera/9.80 (Windows NT 6.1; U; cs) Presto/2.7.62 Version/11.01", "Opera/9.80 (Windows NT 6.0; U; pl) Presto/2.7.62 Version/11.01", "Opera/9.80 (Windows NT 5.2; U; ru) Presto/2.7.62 Version/11.01", "Opera/9.80 (Windows NT 5.1; U;) Presto/2.7.62 Version/11.01", "Opera/9.80 (Windows NT 5.1; U; cs) Presto/2.7.62 Version/11.01", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.13) Gecko/20101213 Opera/9.80 (Windows NT 6.1; U; zh-tw) Presto/2.7.62 Version/11.01", "Mozilla/5.0 (Windows NT 6.1; U; nl; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 11.01", "Mozilla/5.0 (Windows NT 6.1; U; de; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 11.01", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; de) Opera 11.01", "Opera/9.80 (X11; Linux x86_64; U; pl) Presto/2.7.62 Version/11.00", "Opera/9.80 (X11; Linux i686; U; it) Presto/2.7.62 Version/11.00", "Opera/9.80 (Windows NT 6.1; U; zh-cn) Presto/2.6.37 Version/11.00", "Opera/9.80 (Windows NT 6.1; U; pl) Presto/2.7.62 Version/11.00", "Opera/9.80 (Windows NT 6.1; U; ko) Presto/2.7.62 Version/11.00", "Opera/9.80 (Windows NT 6.1; U; fi) Presto/2.7.62 Version/11.00", "Opera/9.80 (Windows NT 6.1; U; en-GB) Presto/2.7.62 Version/11.00", "Opera/9.80 (Windows NT 6.1 x64; U; en) Presto/2.7.62 Version/11.00", "Opera/9.80 (Windows NT 6.0; U; en) Presto/2.7.39 Version/11.00", "Opera/9.80 (Windows NT 5.1; U; ru) Presto/2.7.39 Version/11.00", "Opera/9.80 (Windows NT 5.1; U; MRA 5.5 (build 02842); ru) Presto/2.7.62 Version/11.00", "Opera/9.80 (Windows NT 5.1; U; it) Presto/2.7.62 Version/11.00", "Mozilla/5.0 (Windows NT 6.0; U; ja; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 11.00", "Mozilla/5.0 (Windows NT 5.1; U; pl; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 11.00", "Mozilla/5.0 (Windows NT 5.1; U; de; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 11.00", "Mozilla/4.0 (compatible; MSIE 8.0; X11; Linux x86_64; pl) Opera 11.00", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; fr) Opera 11.00", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; ja) Opera 11.00", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; en) Opera 11.00", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; pl) Opera 11.00", "Opera/9.80 (Windows NT 6.1; U; pl) Presto/2.6.31 Version/10.70", "Mozilla/5.0 (Windows NT 5.2; U; ru; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.70", "Mozilla/5.0 (Windows NT 5.1; U; zh-cn; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.70", "Opera/9.80 (Windows NT 5.2; U; zh-cn) Presto/2.6.30 Version/10.63", "Opera/9.80 (Windows NT 5.2; U; en) Presto/2.6.30 Version/10.63", "Opera/9.80 (Windows NT 5.1; U; MRA 5.6 (build 03278); ru) Presto/2.6.30 Version/10.63", "Opera/9.80 (Windows NT 5.1; U; pl) Presto/2.6.30 Version/10.62", "Mozilla/5.0 (X11; Linux x86_64; U; de; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.62", "Mozilla/4.0 (compatible; MSIE 8.0; X11; Linux x86_64; de) Opera 10.62", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; en) Opera 10.62", "Opera/9.80 (X11; Linux i686; U; pl) Presto/2.6.30 Version/10.61", "Opera/9.80 (X11; Linux i686; U; es-ES) Presto/2.6.30 Version/10.61", "Opera/9.80 (Windows NT 6.1; U; zh-cn) Presto/2.6.30 Version/10.61", "Opera/9.80 (Windows NT 6.1; U; en) Presto/2.6.30 Version/10.61", "Opera/9.80 (Windows NT 6.0; U; it) Presto/2.6.30 Version/10.61", "Opera/9.80 (Windows NT 5.2; U; ru) Presto/2.6.30 Version/10.61", "Opera/9.80 (Windows 98; U; de) Presto/2.6.30 Version/10.61", "Opera/9.80 (Macintosh; Intel Mac OS X; U; nl) Presto/2.6.30 Version/10.61", "Opera/9.80 (X11; Linux i686; U; en) Presto/2.5.27 Version/10.60", "Opera/9.80 (Windows NT 6.0; U; nl) Presto/2.6.30 Version/10.60", "Opera/10.60 (Windows NT 5.1; U; zh-cn) Presto/2.6.30 Version/10.60", "Opera/10.60 (Windows NT 5.1; U; en-US) Presto/2.6.30 Version/10.60", "Opera/9.80 (X11; Linux i686; U; it) Presto/2.5.24 Version/10.54", "Opera/9.80 (X11; Linux i686; U; en-GB) Presto/2.5.24 Version/10.53", "Mozilla/5.0 (Windows NT 5.1; U; zh-cn; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.53", "Mozilla/5.0 (Windows NT 5.1; U; Firefox/5.0; en; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.53", "Mozilla/5.0 (Windows NT 5.1; U; Firefox/4.5; en; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.53", "Mozilla/5.0 (Windows NT 5.1; U; Firefox/3.5; en; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.53", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; ko) Opera 10.53", "Opera/9.80 (Windows NT 6.1; U; fr) Presto/2.5.24 Version/10.52", "Opera/9.80 (Windows NT 6.1; U; en) Presto/2.5.22 Version/10.51", "Opera/9.80 (Windows NT 6.0; U; cs) Presto/2.5.22 Version/10.51", "Opera/9.80 (Windows NT 5.2; U; ru) Presto/2.5.22 Version/10.51", "Opera/9.80 (Linux i686; U; en) Presto/2.5.22 Version/10.51", "Mozilla/5.0 (Windows NT 6.1; U; en-GB; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.51", "Mozilla/5.0 (Linux i686; U; en; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.51", "Mozilla/4.0 (compatible; MSIE 8.0; Linux i686; en) Opera 10.51", "Opera/9.80 (Windows NT 6.1; U; zh-tw) Presto/2.5.22 Version/10.50", "Opera/9.80 (Windows NT 6.1; U; zh-cn) Presto/2.5.22 Version/10.50", "Opera/9.80 (Windows NT 6.1; U; sk) Presto/2.6.22 Version/10.50", "Opera/9.80 (Windows NT 6.1; U; ja) Presto/2.5.22 Version/10.50", "Opera/9.80 (Windows NT 6.0; U; zh-cn) Presto/2.5.22 Version/10.50", "Opera/9.80 (Windows NT 5.1; U; sk) Presto/2.5.22 Version/10.50", "Opera/9.80 (Windows NT 5.1; U; ru) Presto/2.5.22 Version/10.50", "Opera/10.50 (Windows NT 6.1; U; en-GB) Presto/2.2.2", "Opera/9.80 (S60; SymbOS; Opera Tablet/9174; U; en) Presto/2.7.81 Version/10.5", "Opera/9.80 (X11; U; Linux i686; en-US; rv:1.9.2.3) Presto/2.2.15 Version/10.10", "Opera/9.80 (X11; Linux x86_64; U; it) Presto/2.2.15 Version/10.10", "Opera/9.80 (Windows NT 6.1; U; de) Presto/2.2.15 Version/10.10", "Opera/9.80 (Windows NT 6.0; U; Gecko/20100115; pl) Presto/2.2.15 Version/10.10", "Opera/9.80 (Windows NT 6.0; U; en) Presto/2.2.15 Version/10.10", "Opera/9.80 (Windows NT 5.1; U; de) Presto/2.2.15 Version/10.10", "Opera/9.80 (Windows NT 5.1; U; cs) Presto/2.2.15 Version/10.10", "Mozilla/5.0 (Windows NT 6.0; U; tr; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 Opera 10.10", "Mozilla/4.0 (compatible; MSIE 6.0; X11; Linux i686; de) Opera 10.10", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 6.0; tr) Opera 10.10", "Opera/9.80 (X11; Linux x86_64; U; en-GB) Presto/2.2.15 Version/10.01", "Opera/9.80 (X11; Linux x86_64; U; en) Presto/2.2.15 Version/10.00", "Opera/9.80 (X11; Linux x86_64; U; de) Presto/2.2.15 Version/10.00", "Opera/9.80 (X11; Linux i686; U; ru) Presto/2.2.15 Version/10.00", "Opera/9.80 (X11; Linux i686; U; pt-BR) Presto/2.2.15 Version/10.00", "Opera/9.80 (X11; Linux i686; U; pl) Presto/2.2.15 Version/10.00", "Opera/9.80 (X11; Linux i686; U; nb) Presto/2.2.15 Version/10.00", "Opera/9.80 (X11; Linux i686; U; en-GB) Presto/2.2.15 Version/10.00", "Opera/9.80 (X11; Linux i686; U; en) Presto/2.2.15 Version/10.00", "Opera/9.80 (X11; Linux i686; U; Debian; pl) Presto/2.2.15 Version/10.00", "Opera/9.80 (X11; Linux i686; U; de) Presto/2.2.15 Version/10.00", "Opera/9.80 (Windows NT 6.1; U; zh-cn) Presto/2.2.15 Version/10.00", "Opera/9.80 (Windows NT 6.1; U; fi) Presto/2.2.15 Version/10.00", "Opera/9.80 (Windows NT 6.1; U; en) Presto/2.2.15 Version/10.00", "Opera/9.80 (Windows NT 6.1; U; de) Presto/2.2.15 Version/10.00", "Opera/9.80 (Windows NT 6.1; U; cs) Presto/2.2.15 Version/10.00", "Opera/9.80 (Windows NT 6.0; U; en) Presto/2.2.15 Version/10.00", "Opera/9.80 (Windows NT 6.0; U; de) Presto/2.2.15 Version/10.00", "Opera/9.80 (Windows NT 5.2; U; en) Presto/2.2.15 Version/10.00", "Opera/9.80 (Windows NT 5.1; U; zh-cn) Presto/2.2.15 Version/10.00", "Opera/9.80 (Windows NT 5.1; U; ru) Presto/2.2.15 Version/10.00", "Opera/9.99 (X11; U; sk)", "Opera/9.99 (Windows NT 5.1; U; pl) Presto/9.9.9", "Opera/9.80 (J2ME/MIDP; Opera Mini/5.0 (Windows; U; Windows NT 5.1; en) AppleWebKit/886; U; en) Presto/2.4.15", "Opera/9.70 (Linux ppc64 ; U; en) Presto/2.2.1", "Opera/9.70 (Linux i686 ; U; zh-cn) Presto/2.2.0", "Opera/9.70 (Linux i686 ; U; en-us) Presto/2.2.0", "Opera/9.70 (Linux i686 ; U; en) Presto/2.2.1", "Opera/9.70 (Linux i686 ; U; en) Presto/2.2.0", "Opera/9.70 (Linux i686 ; U; ; en) Presto/2.2.1", "Opera/9.70 (Linux i686 ; U; ; en) Presto/2.2.1", "Mozilla/5.0 (Linux i686 ; U; en; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 Opera 9.70", "Mozilla/4.0 (compatible; MSIE 6.0; Linux i686 ; en) Opera 9.70", "HTC_HD2_T8585 Opera/9.70 (Windows NT 5.1; U; de)", "Opera 9.7 (Windows NT 5.2; U; en)", "Opera/9.64(Windows NT 5.1; U; en) Presto/2.1.1", "Opera/9.64 (X11; Linux x86_64; U; pl) Presto/2.1.1", "Opera/9.64 (X11; Linux x86_64; U; hr) Presto/2.1.1", "Opera/9.64 (X11; Linux x86_64; U; en-GB) Presto/2.1.1", "Opera/9.64 (X11; Linux x86_64; U; en) Presto/2.1.1", "Opera/9.64 (X11; Linux x86_64; U; de) Presto/2.1.1", "Opera/9.64 (X11; Linux x86_64; U; cs) Presto/2.1.1", "Opera/9.64 (X11; Linux i686; U; tr) Presto/2.1.1", "Opera/9.64 (X11; Linux i686; U; sv) Presto/2.1.1", "Opera/9.64 (X11; Linux i686; U; pl) Presto/2.1.1", "Opera/9.64 (X11; Linux i686; U; nb) Presto/2.1.1", "Opera/9.64 (X11; Linux i686; U; Linux Mint; nb) Presto/2.1.1", "Opera/9.64 (X11; Linux i686; U; Linux Mint; it) Presto/2.1.1", "Opera/9.64 (X11; Linux i686; U; en) Presto/2.1.1", "Opera/9.64 (X11; Linux i686; U; de) Presto/2.1.1", "Opera/9.64 (X11; Linux i686; U; da) Presto/2.1.1", "Opera/9.64 (Windows NT 6.1; U; MRA 5.5 (build 02842); ru) Presto/2.1.1", "Opera/9.64 (Windows NT 6.1; U; de) Presto/2.1.1", "Opera/9.64 (Windows NT 6.0; U; zh-cn) Presto/2.1.1", "Opera/9.64 (Windows NT 6.0; U; pl) Presto/2.1.1", "Opera/9.63 (X11; Linux x86_64; U; ru) Presto/2.1.1", "Opera/9.63 (X11; Linux x86_64; U; cs) Presto/2.1.1" }); } public static String getRandomUserAgent() { double rand = Math.random() * 100; String browser = null; double count = 0.0; for (Entry freq : freqMap.entrySet()) { count += freq.getValue(); if (rand <= count) { browser = freq.getKey(); break; } } if (browser == null) { browser = "Chrome"; } String userAgents[] = uaMap.get(browser); return userAgents[(int) Math.floor(Math.random() * userAgents.length)]; } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/fetcher/URLFetchUtil.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.fetcher import java.io.InputStream import java.net.URL object URLFetchUtil { def getInputStreamFromURL(urlObj: URL, retries: Int = 5): Option[InputStream] = { for (_ <- 0 until retries) { val result = try { val request = urlObj.openConnection() request.setRequestProperty("User-Agent", RandomUserAgent.getRandomUserAgent) Some(request.getInputStream) } catch { case t: Throwable => //re-try None } if (result.isDefined) { return result } } None } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/fetcher/URLFetcherOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.fetcher import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{OutputPort, PhysicalOp, SchemaPropagationFunc} import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.operator.source.SourceOperatorDescriptor import org.apache.texera.amber.util.JSONUtils.objectMapper class URLFetcherOpDesc extends SourceOperatorDescriptor { @JsonProperty(required = true) @JsonSchemaTitle("URL") @JsonPropertyDescription( "Only accepts standard URL format" ) var url: String = _ @JsonProperty(required = true) @JsonSchemaTitle("Decoding") @JsonPropertyDescription( "The decoding method for the url content" ) var decodingMethod: DecodingMethod = _ override def sourceSchema(): Schema = { Schema() .add( "URL content", if (decodingMethod == DecodingMethod.UTF_8) AttributeType.STRING else AttributeType.ANY ) } override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .sourcePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.source.fetcher.URLFetcherOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPropagateSchema( SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema())) ) } override def operatorInfo: OperatorInfo = OperatorInfo( userFriendlyName = "URL Fetcher", operatorDescription = "Fetch the content of a single URL", operatorGroupName = OperatorGroupConstants.API_GROUP, inputPorts = List.empty, outputPorts = List(OutputPort()) ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/fetcher/URLFetcherOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.fetcher import org.apache.texera.amber.core.executor.SourceOperatorExecutor import org.apache.texera.amber.core.tuple.TupleLike import org.apache.texera.amber.operator.source.fetcher.URLFetchUtil.getInputStreamFromURL import org.apache.texera.amber.util.JSONUtils.objectMapper import org.apache.commons.io.IOUtils import java.net.URL class URLFetcherOpExec(descString: String) extends SourceOperatorExecutor { private val desc: URLFetcherOpDesc = objectMapper.readValue(descString, classOf[URLFetcherOpDesc]) override def produceTuple(): Iterator[TupleLike] = { val urlObj = new URL(desc.url) val input = getInputStreamFromURL(urlObj) val contentInputStream = input match { case Some(value) => value case None => IOUtils.toInputStream(s"Fetch failed for URL: $desc.url", "UTF-8") } Iterator(if (desc.decodingMethod == DecodingMethod.UTF_8) { TupleLike(IOUtils.toString(contentInputStream, "UTF-8")) } else { TupleLike(IOUtils.toByteArray(contentInputStream)) }) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/AutoClosingIterator.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan /** * A wrapper around an Interator that automatically invokes a cleanup function * when the iterator is fully consumed (i.e. hasNext returns false) * * This is useful for lazily reading resources like InputStreams and ensuring * they are properly closed once no more data is available * * @param iter the underlying iterator to consume * @param onClose a function to call once the iterator is exhausted */ class AutoClosingIterator[T](iter: Iterator[T], onClose: () => Unit) extends Iterator[T] { private var alreadyClosed = false override def hasNext: Boolean = { val hn = iter.hasNext if (!hn && !alreadyClosed) { onClose() alreadyClosed = true } hn } override def next(): T = iter.next() } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/FileAttributeType.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan; import com.fasterxml.jackson.annotation.JsonValue; import org.apache.texera.amber.core.tuple.AttributeType; public enum FileAttributeType { STRING("string", AttributeType.STRING), SINGLE_STRING("single string", AttributeType.STRING), INTEGER("integer", AttributeType.INTEGER), LONG("long", AttributeType.LONG), DOUBLE("double", AttributeType.DOUBLE), BOOLEAN("boolean", AttributeType.BOOLEAN), TIMESTAMP("timestamp", AttributeType.TIMESTAMP), BINARY("binary", AttributeType.BINARY), LARGE_BINARY("large binary", AttributeType.LARGE_BINARY); private final String name; private final AttributeType type; FileAttributeType(String name, AttributeType type) { this.name = name; this.type = type; } @JsonValue public String getName() { return this.name; } public AttributeType getType() { return this.type; } @Override public String toString() { return this.getName(); } public boolean isSingle() { return this == SINGLE_STRING || this == BINARY || this == LARGE_BINARY; } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/FileDecodingMethod.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan; import com.fasterxml.jackson.annotation.JsonValue; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; public enum FileDecodingMethod { UTF_8("UTF_8", StandardCharsets.UTF_8), UTF_16("UTF_16", StandardCharsets.UTF_16), ASCII("US_ASCII", StandardCharsets.US_ASCII); private final String name; private final Charset charset; FileDecodingMethod(String name, Charset charset) { this.name = name; this.charset = charset; } // use the name string instead of enum string in JSON @JsonValue public String getName() { return this.name; } @Override public String toString() { return this.getName(); } public Charset getCharset() { return this.charset; } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/ScanSourceOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan import com.fasterxml.jackson.annotation.{JsonIgnore, JsonProperty, JsonPropertyDescription} import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.storage.FileResolver import org.apache.texera.amber.core.tuple.Schema import org.apache.texera.amber.core.workflow.OutputPort import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.operator.source.SourceOperatorDescriptor import org.apache.commons.lang3.builder.EqualsBuilder import java.net.URI abstract class ScanSourceOpDesc extends SourceOperatorDescriptor { /** in the case we do not want to read the entire large file, but only * the first a few lines of it to do the type inference. */ @JsonIgnore var INFER_READ_LIMIT: Int = 100 @JsonProperty(required = true) @JsonSchemaTitle("File") @JsonDeserialize(contentAs = classOf[java.lang.String]) var fileName: Option[String] = None @JsonProperty(defaultValue = "UTF_8", required = true) @JsonSchemaTitle("File Encoding") @JsonPropertyDescription("decoding charset to use on input") var fileEncoding: FileDecodingMethod = FileDecodingMethod.UTF_8 @JsonIgnore var fileTypeName: Option[String] = None @JsonProperty() @JsonSchemaTitle("Limit") @JsonPropertyDescription("max output count") @JsonDeserialize(contentAs = classOf[Int]) var limit: Option[Int] = None @JsonProperty() @JsonSchemaTitle("Offset") @JsonPropertyDescription("starting point of output") @JsonDeserialize(contentAs = classOf[Int]) var offset: Option[Int] = None override def sourceSchema(): Schema = null override def operatorInfo: OperatorInfo = { val typeName = fileTypeName.getOrElse("Unknown") val displayName = if (typeName.isEmpty) "File Scan" else s"$typeName File Scan" val description = if (typeName.isEmpty) "Scan data from a file" else if ("AEIOUaeiou".contains(typeName.charAt(0))) s"Scan data from an $typeName file" else s"Scan data from a $typeName file" OperatorInfo( userFriendlyName = displayName, operatorDescription = description, OperatorGroupConstants.INPUT_GROUP, inputPorts = List.empty, outputPorts = List(OutputPort()) ) } def setResolvedFileName(uri: URI): Unit = { fileName = Some(uri.toASCIIString) } override def equals(that: Any): Boolean = EqualsBuilder.reflectionEquals(this, that, "context", "fileHandle") def fileResolved(): Boolean = fileName.isDefined && FileResolver.isFileResolved(fileName.get) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.arrow import com.fasterxml.jackson.annotation.JsonIgnoreProperties import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.storage.DocumentFactory import org.apache.texera.amber.core.tuple.Schema import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{PhysicalOp, SchemaPropagationFunc} import org.apache.texera.amber.operator.source.scan.ScanSourceOpDesc import org.apache.texera.amber.util.ArrowUtils import org.apache.texera.amber.util.JSONUtils.objectMapper import org.apache.arrow.memory.RootAllocator import org.apache.arrow.vector.ipc.ArrowFileReader import org.apache.arrow.vector.types.pojo.{Schema => ArrowSchema} import java.io.IOException import java.net.URI import java.nio.file.{Files, StandardOpenOption} import scala.util.Using @JsonIgnoreProperties(value = Array("fileEncoding")) class ArrowSourceOpDesc extends ScanSourceOpDesc { fileTypeName = Option("Arrow") @throws[IOException] override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .sourcePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.source.scan.arrow.ArrowSourceOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPropagateSchema( SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> inferSchema())) ) } /** * Infer Texera.Schema based on the top few lines of data. * * @return Texera.Schema build for this operator */ @Override def inferSchema(): Schema = { val file = DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asFile() val allocator = new RootAllocator() Using .Manager { use => val channel = use(Files.newByteChannel(file.toPath, StandardOpenOption.READ)) val reader = use(new ArrowFileReader(channel, allocator)) val arrowSchema: ArrowSchema = reader.getVectorSchemaRoot.getSchema ArrowUtils.toTexeraSchema(arrowSchema) } .getOrElse { throw new IOException("Failed to infer schema from Arrow file.") } } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.arrow import org.apache.texera.amber.core.executor.SourceOperatorExecutor import org.apache.texera.amber.core.storage.DocumentFactory import org.apache.texera.amber.core.tuple.TupleLike import org.apache.texera.amber.util.ArrowUtils import org.apache.texera.amber.util.JSONUtils.objectMapper import org.apache.arrow.memory.RootAllocator import org.apache.arrow.vector.VectorSchemaRoot import org.apache.arrow.vector.ipc.ArrowFileReader import java.net.URI import java.nio.file.{Files, StandardOpenOption} class ArrowSourceOpExec( descString: String ) extends SourceOperatorExecutor { private val desc: ArrowSourceOpDesc = objectMapper.readValue(descString, classOf[ArrowSourceOpDesc]) private var reader: Option[ArrowFileReader] = None private var root: Option[VectorSchemaRoot] = None private var allocator: Option[RootAllocator] = None override def open(): Unit = { try { val file = DocumentFactory.openReadonlyDocument(new URI(desc.fileName.get)).asFile() val alloc = new RootAllocator() allocator = Some(alloc) val channel = Files.newByteChannel(file.toPath, StandardOpenOption.READ) val arrowReader = new ArrowFileReader(channel, alloc) val vectorRoot = arrowReader.getVectorSchemaRoot reader = Some(arrowReader) root = Some(vectorRoot) } catch { case e: Exception => close() // Ensure resources are closed in case of an error throw new RuntimeException("Failed to open Arrow source", e) } } override def produceTuple(): Iterator[TupleLike] = { val rowIterator = new Iterator[TupleLike] { private var currentIndex = 0 private var currentBatchIndex = 0 override def hasNext: Boolean = { if (root.exists(_.getRowCount > currentIndex)) { true } else { reader.exists(arrowReader => { val hasMoreBatches = arrowReader.loadNextBatch() if (hasMoreBatches) { currentIndex = 0 currentBatchIndex += 1 true } else { false } }) } } override def next(): TupleLike = { root.map { vectorSchemaRoot => val tuple = ArrowUtils.getTexeraTuple(currentIndex, vectorSchemaRoot) currentIndex += 1 tuple }.get } } var tupleIterator = rowIterator.drop(desc.offset.getOrElse(0)) if (desc.limit.isDefined) tupleIterator = tupleIterator.take(desc.limit.get) tupleIterator } override def close(): Unit = { reader.foreach(_.close()) root.foreach(_.close()) allocator.foreach(_.close()) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.csv import com.fasterxml.jackson.annotation.{JsonInclude, JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import com.univocity.parsers.csv.{CsvFormat, CsvParser, CsvParserSettings} import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.storage.DocumentFactory import org.apache.texera.amber.core.tuple.AttributeTypeUtils.inferSchemaFromRows import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{PhysicalOp, SchemaPropagationFunc} import org.apache.texera.amber.operator.source.scan.ScanSourceOpDesc import org.apache.texera.amber.operator.source.scan.csv.CSVScanSourceOpExec import org.apache.texera.amber.util.JSONUtils.objectMapper import java.io.{IOException, InputStreamReader} import java.net.URI class CSVScanSourceOpDesc extends ScanSourceOpDesc { @JsonProperty(defaultValue = ",") @JsonSchemaTitle("Delimiter") @JsonPropertyDescription("delimiter to separate each line into fields") @JsonInclude(JsonInclude.Include.NON_ABSENT) var customDelimiter: Option[String] = None @JsonProperty(defaultValue = "true") @JsonSchemaTitle("Header") @JsonPropertyDescription("whether the CSV file contains a header line") var hasHeader: Boolean = true fileTypeName = Option("CSV") @throws[IOException] override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { // fill in default values if (customDelimiter.forall(_.isEmpty)) { customDelimiter = Option(",") } PhysicalOp .sourcePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.source.scan.csv.CSVScanSourceOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPropagateSchema( SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema())) ) } override def sourceSchema(): Schema = { if (customDelimiter.isEmpty || !fileResolved()) { return null } val stream = DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asInputStream() val inputReader = new InputStreamReader(stream, fileEncoding.getCharset) val csvFormat = new CsvFormat() csvFormat.setDelimiter(customDelimiter.get.charAt(0)) csvFormat.setLineSeparator("\n") val csvSetting = new CsvParserSettings() csvSetting.setMaxCharsPerColumn(-1) val maxColumns = CSVScanSourceOpExec.getMaxColumns csvSetting.setMaxColumns(maxColumns) csvSetting.setFormat(csvFormat) csvSetting.setHeaderExtractionEnabled(hasHeader) csvSetting.setNullValue("") val parser = new CsvParser(csvSetting) parser.beginParsing(inputReader) var data: Array[Array[String]] = Array() val readLimit = limit.getOrElse(INFER_READ_LIMIT).min(INFER_READ_LIMIT) for (_ <- 0 until readLimit) { val row = CSVScanSourceOpExec.parseNextRow(parser, maxColumns) if (row != null) { data = data :+ row } } parser.stopParsing() inputReader.close() val attributeTypeList: Array[AttributeType] = inferSchemaFromRows( data.iterator.asInstanceOf[Iterator[Array[Any]]] ) val header: Array[String] = if (hasHeader) Option(parser.getContext.headers()) .getOrElse((1 to attributeTypeList.length).map(i => "column-" + i).toArray) else (1 to attributeTypeList.length).map(i => "column-" + i).toArray header.indices.foldLeft(Schema()) { (schema, i) => schema.add(header(i), attributeTypeList(i)) } } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.csv import com.univocity.parsers.common.TextParsingException import com.univocity.parsers.csv.{CsvFormat, CsvParser, CsvParserSettings} import org.apache.texera.amber.core.executor.SourceOperatorExecutor import org.apache.texera.amber.core.storage.DocumentFactory import org.apache.texera.amber.core.tuple.{AttributeTypeUtils, Schema, TupleLike} import org.apache.texera.amber.util.JSONUtils.objectMapper import org.apache.texera.dao.SiteSettings import java.io.InputStreamReader import java.net.URI import scala.collection.immutable.ArraySeq import scala.util.Try class CSVScanSourceOpExec private[csv] (descString: String) extends SourceOperatorExecutor { val desc: CSVScanSourceOpDesc = objectMapper.readValue(descString, classOf[CSVScanSourceOpDesc]) var inputReader: InputStreamReader = _ var parser: CsvParser = _ var nextRow: Array[String] = _ var numRowGenerated = 0 private var maxColumns: Int = CSVScanSourceOpExec.DEFAULT_MAX_COLUMNS private val schema: Schema = desc.sourceSchema() override def produceTuple(): Iterator[TupleLike] = { val rowIterator = new Iterator[Array[String]] { override def hasNext: Boolean = { if (nextRow != null) { return true } nextRow = CSVScanSourceOpExec.parseNextRow(parser, maxColumns) nextRow != null } override def next(): Array[String] = { val ret = nextRow numRowGenerated += 1 nextRow = null ret } } var tupleIterator = rowIterator .drop(desc.offset.getOrElse(0)) .map(row => { try { TupleLike( ArraySeq.unsafeWrapArray( AttributeTypeUtils.parseFields(row.asInstanceOf[Array[Any]], schema) ): _* ) } catch { case _: Throwable => null } }) .filter(t => t != null) if (desc.limit.isDefined) tupleIterator = tupleIterator.take(desc.limit.get) tupleIterator } override def open(): Unit = { inputReader = new InputStreamReader( DocumentFactory.openReadonlyDocument(new URI(desc.fileName.get)).asInputStream(), desc.fileEncoding.getCharset ) val csvFormat = new CsvFormat() csvFormat.setDelimiter(desc.customDelimiter.get.charAt(0)) csvFormat.setLineSeparator("\n") csvFormat.setComment( '\u0000' ) // disable skipping lines starting with # (default comment character) val csvSetting = new CsvParserSettings() csvSetting.setMaxCharsPerColumn(-1) maxColumns = CSVScanSourceOpExec.getMaxColumns csvSetting.setMaxColumns(maxColumns) csvSetting.setFormat(csvFormat) csvSetting.setHeaderExtractionEnabled(desc.hasHeader) parser = new CsvParser(csvSetting) parser.beginParsing(inputReader) } override def close(): Unit = { if (parser != null) { parser.stopParsing() } if (inputReader != null) { inputReader.close() } } } object CSVScanSourceOpExec { val DEFAULT_MAX_COLUMNS = 512 def getMaxColumns: Int = SiteSettings.getInt("csv_parser_max_columns", DEFAULT_MAX_COLUMNS) /** * Wraps `parser.parseNext()` so a column-count overflow is reported to the user * as a clear instruction rather than a deep Univocity stack trace. Other parser * failures are rethrown unchanged. * * The thrown RuntimeException's message bubbles up through DataProcessor.handleExecutorException * and becomes the title of the console message that drives the top-of-page toast. */ def parseNextRow(parser: CsvParser, maxColumns: Int): Array[String] = { try parser.parseNext() catch { case e: TextParsingException if isColumnOverflow(e, maxColumns) => throw new RuntimeException(columnOverflowMessage(maxColumns), e) } } private[csv] def isColumnOverflow(e: TextParsingException, maxColumns: Int): Boolean = Option(e.getCause) .collect { case aioobe: ArrayIndexOutOfBoundsException => aioobe } .exists(aioobe => aioobeIndex(aioobe).exists(_ == maxColumns)) private def aioobeIndex(aioobe: ArrayIndexOutOfBoundsException): Option[Int] = { val msg = Option(aioobe.getMessage).getOrElse("") Try(msg.trim.toInt).toOption.orElse { raw"Index (\d+) out of bounds".r.findFirstMatchIn(msg).map(_.group(1).toInt) } } private[csv] def columnOverflowMessage(maxColumns: Int): String = s"Max columns of $maxColumns exceeded." } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.csv import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.github.tototoshi.csv.{CSVReader, DefaultCSVFormat} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.storage.DocumentFactory import org.apache.texera.amber.core.tuple.AttributeTypeUtils.inferSchemaFromRows import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{PhysicalOp, SchemaPropagationFunc} import org.apache.texera.amber.operator.source.scan.ScanSourceOpDesc import org.apache.texera.amber.util.JSONUtils.objectMapper import java.io.IOException import java.net.URI class ParallelCSVScanSourceOpDesc extends ScanSourceOpDesc { @JsonProperty(defaultValue = ",") @JsonSchemaTitle("Delimiter") @JsonPropertyDescription("delimiter to separate each line into fields") @JsonDeserialize(contentAs = classOf[java.lang.String]) var customDelimiter: Option[String] = None @JsonProperty(defaultValue = "true") @JsonSchemaTitle("Header") @JsonPropertyDescription("whether the CSV file contains a header line") var hasHeader: Boolean = true fileTypeName = Option("CSV") @throws[IOException] override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { // fill in default values if (customDelimiter.forall(_.isEmpty)) { customDelimiter = Option(",") } PhysicalOp .sourcePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.source.scan.csv.ParallelCSVScanSourceOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withParallelizable(true) .withPropagateSchema( SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema())) ) } override def sourceSchema(): Schema = { if (customDelimiter.isEmpty || !fileResolved()) { return null } val file = DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asFile() implicit object CustomFormat extends DefaultCSVFormat { override val delimiter: Char = customDelimiter.get.charAt(0) } var reader: CSVReader = CSVReader.open(file)(CustomFormat) val firstRow: Array[String] = reader.iterator.next().toArray reader.close() // reopen the file to read from the beginning reader = CSVReader.open(file.toPath.toString)(CustomFormat) if (hasHeader) reader.readNext() val attributeTypeList: Array[AttributeType] = inferSchemaFromRows( reader.iterator .take(limit.getOrElse(INFER_READ_LIMIT).min(INFER_READ_LIMIT)) .map(seq => seq.toArray) ) reader.close() // build schema based on inferred AttributeTypes Schema().add(firstRow.indices.map { i => new Attribute( if (hasHeader) firstRow(i) else s"column-${i + 1}", attributeTypeList(i) ) }) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.csv import org.apache.texera.amber.core.executor.SourceOperatorExecutor import org.apache.texera.amber.core.storage.DocumentFactory import org.apache.texera.amber.core.tuple.{Attribute, AttributeTypeUtils, TupleLike} import org.apache.texera.amber.operator.source.BufferedBlockReader import org.apache.texera.amber.util.JSONUtils.objectMapper import org.tukaani.xz.SeekableFileInputStream import java.net.URI import java.util import java.util.stream.{IntStream, Stream} import scala.collection.compat.immutable.ArraySeq class ParallelCSVScanSourceOpExec private[csv] ( descString: String, idx: Int = 0, workerCount: Int = 1 ) extends SourceOperatorExecutor { val desc: ParallelCSVScanSourceOpDesc = objectMapper.readValue(descString, classOf[ParallelCSVScanSourceOpDesc]) private var reader: BufferedBlockReader = _ private val schema = desc.sourceSchema() override def produceTuple(): Iterator[TupleLike] = new Iterator[TupleLike]() { override def hasNext: Boolean = reader.hasNext override def next(): TupleLike = { try { // obtain String representation of each field // a null value will present if omit in between fields, e.g., ['hello', null, 'world'] val line = reader.readLine if (line == null) { return null } var fields: Array[AnyRef] = line.toArray if (fields == null || util.Arrays.stream(fields).noneMatch(s => s != null)) { // discard tuple if it's null or it only contains null // which means it will always discard Tuple(null) from readLine() return null } // however the null values won't present if omitted in the end, we need to match nulls. if (fields.length != schema.getAttributes.size) fields = Stream .concat( util.Arrays.stream(fields), IntStream .range(0, schema.getAttributes.size - fields.length) .mapToObj((_: Int) => null) ) .toArray() // parse Strings into inferred AttributeTypes val parsedFields: Array[Any] = AttributeTypeUtils.parseFields( fields.asInstanceOf[Array[Any]], schema.getAttributes .map((attr: Attribute) => attr.getType) .toArray ) TupleLike(ArraySeq.unsafeWrapArray(parsedFields): _*) } catch { case _: Throwable => null } } }.filter(tuple => tuple != null) override def open(): Unit = { // here, the stream requires to be seekable, so datasetFileDesc creates a temp file here // TODO: consider a better way val file = DocumentFactory.openReadonlyDocument(new URI(desc.fileName.get)).asFile() val totalBytes: Long = file.length() // TODO: add support for limit // TODO: add support for offset val startOffset: Long = totalBytes / workerCount * idx val endOffset: Long = if (idx != workerCount - 1) totalBytes / workerCount * (idx + 1) else totalBytes val stream = new SeekableFileInputStream(file) stream.seek(startOffset) reader = new BufferedBlockReader( stream, endOffset - startOffset, desc.customDelimiter.get.charAt(0), null ) // skip line if this worker reads from middle of a file if (startOffset > 0) reader.readLine // skip line if this worker reads the start of a file, and the file has a header line if (startOffset == 0 && desc.hasHeader) reader.readLine } override def close(): Unit = reader.close() } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.csvOld import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.github.tototoshi.csv.{CSVReader, DefaultCSVFormat} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.storage.DocumentFactory import org.apache.texera.amber.core.tuple.AttributeTypeUtils.inferSchemaFromRows import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{PhysicalOp, SchemaPropagationFunc} import org.apache.texera.amber.operator.source.scan.ScanSourceOpDesc import org.apache.texera.amber.util.JSONUtils.objectMapper import java.io.IOException import java.net.URI class CSVOldScanSourceOpDesc extends ScanSourceOpDesc { @JsonProperty(defaultValue = ",") @JsonSchemaTitle("Delimiter") @JsonPropertyDescription("delimiter to separate each line into fields") var customDelimiter: Option[String] = Some(",") @JsonProperty(defaultValue = "true") @JsonSchemaTitle("Header") @JsonPropertyDescription("whether the CSV file contains a header line") var hasHeader: Boolean = true fileTypeName = Option("CSVOld") @throws[IOException] override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { // fill in default values if (customDelimiter.get.isEmpty) { customDelimiter = Option(",") } PhysicalOp .sourcePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.source.scan.csvOld.CSVOldScanSourceOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPropagateSchema( SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema())) ) } override def sourceSchema(): Schema = { if (customDelimiter.isEmpty || !fileResolved()) { return null } // infer schema from the first few lines of the file val file = DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asFile() implicit object CustomFormat extends DefaultCSVFormat { override val delimiter: Char = customDelimiter.get.charAt(0) } var reader: CSVReader = CSVReader.open(file, fileEncoding.getCharset.name())(CustomFormat) val firstRow: Array[String] = reader.iterator.next().toArray reader.close() // reopen the file to read from the beginning reader = CSVReader.open(file, fileEncoding.getCharset.name())(CustomFormat) val startOffset = offset.getOrElse(0) + (if (hasHeader) 1 else 0) val endOffset = startOffset + limit.getOrElse(INFER_READ_LIMIT).min(INFER_READ_LIMIT) val attributeTypeList: Array[AttributeType] = inferSchemaFromRows( reader.iterator .slice(startOffset, endOffset) .map(seq => seq.toArray) ) reader.close() // build schema based on inferred AttributeTypes Schema().add(firstRow.indices.map { i => new Attribute( if (hasHeader) firstRow(i) else s"column-${i + 1}", attributeTypeList(i) ) }) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.csvOld import com.github.tototoshi.csv.{CSVReader, DefaultCSVFormat} import org.apache.texera.amber.core.executor.SourceOperatorExecutor import org.apache.texera.amber.core.storage.DocumentFactory import org.apache.texera.amber.core.tuple.{Attribute, AttributeTypeUtils, Schema, TupleLike} import org.apache.texera.amber.util.JSONUtils.objectMapper import java.net.URI import scala.collection.compat.immutable.ArraySeq class CSVOldScanSourceOpExec private[csvOld] ( descString: String ) extends SourceOperatorExecutor { val desc: CSVOldScanSourceOpDesc = objectMapper.readValue(descString, classOf[CSVOldScanSourceOpDesc]) var reader: CSVReader = _ var rows: Iterator[Seq[String]] = _ val schema: Schema = desc.sourceSchema() override def produceTuple(): Iterator[TupleLike] = { val tuples = rows .map(fields => try { val parsedFields: Array[Any] = AttributeTypeUtils.parseFields( fields.toArray, schema.getAttributes .map((attr: Attribute) => attr.getType) .toArray ) TupleLike(ArraySeq.unsafeWrapArray(parsedFields): _*) } catch { case _: Throwable => null } ) .filter(tuple => tuple != null) if (desc.limit.isDefined) tuples.take(desc.limit.get) else { tuples } } override def open(): Unit = { implicit object CustomFormat extends DefaultCSVFormat { override val delimiter: Char = desc.customDelimiter.get.charAt(0) } val filePath = DocumentFactory.openReadonlyDocument(new URI(desc.fileName.get)).asFile().toPath reader = CSVReader.open(filePath.toString, desc.fileEncoding.getCharset.name())(CustomFormat) // skip line if this worker reads the start of a file, and the file has a header line val startOffset = desc.offset.getOrElse(0) + (if (desc.hasHeader) 1 else 0) rows = reader.iterator.drop(startOffset) } override def close(): Unit = { if (reader != null) { reader.close() } } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/file/FileScanOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.file import com.fasterxml.jackson.annotation.JsonProperty import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{ InputPort, OutputPort, PhysicalOp, SchemaPropagationFunc } import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.operator.source.SourceOperatorDescriptor import org.apache.texera.amber.operator.source.scan.FileDecodingMethod import org.apache.texera.amber.operator.source.scan.text.TextSourceOpDesc import org.apache.texera.amber.util.JSONUtils.objectMapper class FileScanOpDesc extends SourceOperatorDescriptor with TextSourceOpDesc { @JsonProperty(defaultValue = "UTF_8", required = true) @JsonSchemaTitle("Encoding") var fileEncoding: FileDecodingMethod = FileDecodingMethod.UTF_8 @JsonProperty(defaultValue = "false") @JsonSchemaTitle("Extract") val extract: Boolean = false @JsonProperty(defaultValue = "false") @JsonSchemaTitle("Include Filename") var outputFileName: Boolean = false override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .sourcePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.source.scan.file.FileScanOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPropagateSchema( SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema())) ) } override def sourceSchema(): Schema = { var schema = Schema() if (outputFileName) { schema = schema.add("filename", AttributeType.STRING) } schema.add(attributeName, attributeType.getType) } override def operatorInfo: OperatorInfo = OperatorInfo( userFriendlyName = "File Scan From Input", operatorDescription = "Scan data from file paths provided by input tuples", operatorGroupName = OperatorGroupConstants.INPUT_GROUP, inputPorts = List(InputPort(displayName = "Filename")), outputPorts = List(OutputPort()) ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/file/FileScanOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.file import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.storage.FileResolver import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} import org.apache.texera.amber.util.JSONUtils.objectMapper class FileScanOpExec private[scan] ( descString: String ) extends OperatorExecutor { private val desc: FileScanOpDesc = objectMapper.readValue(descString, classOf[FileScanOpDesc]) override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = { val originalFileName = tuple.getFields.collectFirst { case s: String => s }.get val fileName = FileResolver.resolve(originalFileName).toASCIIString FileScanUtils.createTuplesFromFile( fileName = fileName, displayFileName = originalFileName, attributeType = desc.attributeType, fileEncoding = desc.fileEncoding, extract = desc.extract, outputFileName = desc.outputFileName, fileScanOffset = desc.fileScanOffset, fileScanLimit = desc.fileScanLimit ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/file/FileScanSourceOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.file import com.fasterxml.jackson.annotation.{JsonIgnoreProperties, JsonProperty} import com.kjetland.jackson.jsonSchema.annotations.{ JsonSchemaInject, JsonSchemaString, JsonSchemaTitle } import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{PhysicalOp, SchemaPropagationFunc} import org.apache.texera.amber.operator.metadata.annotations.HideAnnotation import org.apache.texera.amber.operator.source.scan.text.TextSourceOpDesc import org.apache.texera.amber.operator.source.scan.{FileDecodingMethod, ScanSourceOpDesc} import org.apache.texera.amber.util.JSONUtils.objectMapper @JsonIgnoreProperties(value = Array("limit", "offset", "fileEncoding")) class FileScanSourceOpDesc extends ScanSourceOpDesc with TextSourceOpDesc { @JsonProperty(defaultValue = "UTF_8", required = true) @JsonSchemaTitle("Encoding") @JsonSchemaInject( strings = Array( new JsonSchemaString(path = HideAnnotation.hideTarget, value = "attributeType"), new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.equals), new JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = "binary") ) ) private val encoding: FileDecodingMethod = FileDecodingMethod.UTF_8 @JsonProperty(defaultValue = "false") @JsonSchemaTitle("Extract") val extract: Boolean = false @JsonProperty(defaultValue = "false") @JsonSchemaTitle("Include Filename") @JsonSchemaInject( strings = Array( new JsonSchemaString(path = HideAnnotation.hideTarget, value = "extract"), new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.equals), new JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = "false") ) ) val outputFileName: Boolean = false fileTypeName = Option("") override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .sourcePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.source.scan.file.FileScanSourceOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPropagateSchema( SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema())) ) } override def sourceSchema(): Schema = { var schema = Schema() if (outputFileName) { schema = schema.add("filename", AttributeType.STRING) } schema.add(attributeName, attributeType.getType) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/file/FileScanSourceOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.file import org.apache.texera.amber.core.executor.SourceOperatorExecutor import org.apache.texera.amber.core.tuple.TupleLike import org.apache.texera.amber.util.JSONUtils.objectMapper import java.io.IOException class FileScanSourceOpExec private[scan] ( descString: String ) extends SourceOperatorExecutor { private val desc: FileScanSourceOpDesc = objectMapper.readValue(descString, classOf[FileScanSourceOpDesc]) @throws[IOException] override def produceTuple(): Iterator[TupleLike] = { FileScanUtils.createTuplesFromFile( fileName = desc.fileName.get, attributeType = desc.attributeType, fileEncoding = desc.fileEncoding, extract = desc.extract, outputFileName = desc.outputFileName, fileScanOffset = desc.fileScanOffset, fileScanLimit = desc.fileScanLimit ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/file/FileScanUtils.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.file import org.apache.commons.compress.archivers.ArchiveStreamFactory import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream import org.apache.commons.io.IOUtils.toByteArray import org.apache.texera.amber.core.storage.DocumentFactory import org.apache.texera.amber.core.tuple.AttributeTypeUtils.parseField import org.apache.texera.amber.core.tuple.LargeBinary import org.apache.texera.amber.core.tuple.TupleLike import org.apache.texera.amber.operator.source.scan.{ AutoClosingIterator, FileAttributeType, FileDecodingMethod } import org.apache.texera.service.util.LargeBinaryOutputStream import java.io._ import java.net.URI import scala.collection.mutable import scala.jdk.CollectionConverters.IteratorHasAsScala private[file] object FileScanUtils { def createTuplesFromFile( fileName: String, displayFileName: String, attributeType: FileAttributeType, fileEncoding: FileDecodingMethod, extract: Boolean, outputFileName: Boolean, fileScanOffset: Option[Int], fileScanLimit: Option[Int] ): Iterator[TupleLike] = { val inputStream = DocumentFactory.openReadonlyDocument(new URI(fileName)).asInputStream() val closeables = mutable.ArrayBuffer.empty[AutoCloseable] var zipIn: ZipArchiveInputStream = null val archiveStream: InputStream = if (extract) { zipIn = new ArchiveStreamFactory() .createArchiveInputStream(new BufferedInputStream(inputStream)) .asInstanceOf[ZipArchiveInputStream] closeables += zipIn zipIn } else { closeables += inputStream inputStream } var filenameIt: Iterator[String] = Iterator.empty val fileEntries: Iterator[InputStream] = if (extract) { val (it1, it2) = Iterator .continually(zipIn.getNextEntry) .takeWhile(_ != null) .filterNot(_.getName.startsWith("__MACOSX")) .duplicate filenameIt = it1.map(_.getName) it2.map(_ => zipIn) } else { filenameIt = Iterator.single(displayFileName) Iterator(archiveStream) } val rawIterator: Iterator[TupleLike] = if (attributeType.isSingle) { fileEntries.zipAll(filenameIt, null, null).map { case (entry, entryFileName) => val fields = mutable.ListBuffer.empty[Any] if (outputFileName) { fields += entryFileName } fields += (attributeType match { case FileAttributeType.SINGLE_STRING => new String(toByteArray(entry), fileEncoding.getCharset) case FileAttributeType.LARGE_BINARY => val largeBinary = new LargeBinary() val out = new LargeBinaryOutputStream(largeBinary) try { val buffer = new Array[Byte](8192) var bytesRead = entry.read(buffer) while (bytesRead != -1) { out.write(buffer, 0, bytesRead) bytesRead = entry.read(buffer) } } finally { out.close() } largeBinary case _ => parseField(toByteArray(entry), attributeType.getType) }) TupleLike(fields.toSeq: _*) } } else { fileEntries.flatMap(entry => new BufferedReader(new InputStreamReader(entry, fileEncoding.getCharset)) .lines() .iterator() .asScala .slice( fileScanOffset.getOrElse(0), fileScanOffset.getOrElse(0) + fileScanLimit.getOrElse(Int.MaxValue) ) .map(line => TupleLike(attributeType match { case FileAttributeType.SINGLE_STRING => line case _ => parseField(line, attributeType.getType) }) ) ) } new AutoClosingIterator(rawIterator, () => closeables.foreach(_.close())) } def createTuplesFromFile( fileName: String, attributeType: FileAttributeType, fileEncoding: FileDecodingMethod, extract: Boolean, outputFileName: Boolean, fileScanOffset: Option[Int], fileScanLimit: Option[Int] ): Iterator[TupleLike] = { createTuplesFromFile( fileName = fileName, displayFileName = fileName, attributeType = attributeType, fileEncoding = fileEncoding, extract = extract, outputFileName = outputFileName, fileScanOffset = fileScanOffset, fileScanLimit = fileScanLimit ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.json import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.fasterxml.jackson.databind.JsonNode import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.storage.DocumentFactory import org.apache.texera.amber.core.tuple.AttributeTypeUtils.inferSchemaFromRows import org.apache.texera.amber.core.tuple.{Attribute, Schema} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{PhysicalOp, SchemaPropagationFunc} import org.apache.texera.amber.operator.source.scan.ScanSourceOpDesc import org.apache.texera.amber.util.JSONUtils.{JSONToMap, objectMapper} import java.io._ import java.net.URI import scala.collection.mutable.ArrayBuffer import scala.jdk.CollectionConverters.IteratorHasAsScala class JSONLScanSourceOpDesc extends ScanSourceOpDesc { @JsonProperty(required = true) @JsonPropertyDescription("flatten nested objects and arrays") var flatten: Boolean = false fileTypeName = Option("JSONL") @throws[IOException] override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .sourcePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.source.scan.json.JSONLScanSourceOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withParallelizable(true) .withPropagateSchema( SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema())) ) } override def sourceSchema(): Schema = { if (!fileResolved()) { return null } val stream = DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asInputStream() val reader = new BufferedReader(new InputStreamReader(stream, fileEncoding.getCharset)) var fieldNames = Set[String]() val allFields: ArrayBuffer[Map[String, String]] = ArrayBuffer() val startOffset = offset.getOrElse(0) val endOffset = startOffset + limit.getOrElse(INFER_READ_LIMIT).min(INFER_READ_LIMIT) reader .lines() .iterator() .asScala .slice(startOffset, endOffset) .foreach(line => { val root: JsonNode = objectMapper.readTree(line) if (root.isObject) { val fields: Map[String, String] = JSONToMap(root, flatten = flatten) fieldNames = fieldNames.++(fields.keySet) allFields += fields } }) val sortedFieldNames = fieldNames.toList.sorted reader.close() val attributeTypes = inferSchemaFromRows(allFields.iterator.map(fields => { val result = ArrayBuffer[Object]() for (fieldName <- sortedFieldNames) { if (fields.contains(fieldName)) { result += fields(fieldName) } else { result += null } } result.toArray })) Schema().add(sortedFieldNames.indices.map { i => new Attribute(sortedFieldNames(i), attributeTypes(i)) }) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.json import org.apache.texera.amber.core.executor.SourceOperatorExecutor import org.apache.texera.amber.core.storage.DocumentFactory import org.apache.texera.amber.core.tuple.AttributeTypeUtils.parseField import org.apache.texera.amber.core.tuple.TupleLike import org.apache.texera.amber.util.JSONUtils.{JSONToMap, objectMapper} import java.io.{BufferedReader, InputStreamReader} import java.net.URI import scala.jdk.CollectionConverters.IteratorHasAsScala import scala.util.{Failure, Success, Try} class JSONLScanSourceOpExec private[json] ( descString: String, idx: Int = 0, workerCount: Int = 1 ) extends SourceOperatorExecutor { private val desc: JSONLScanSourceOpDesc = objectMapper.readValue(descString, classOf[JSONLScanSourceOpDesc]) private var rows: Iterator[String] = _ private var reader: BufferedReader = _ private val schema = desc.sourceSchema() override def produceTuple(): Iterator[TupleLike] = { rows.flatMap { line => Try { val data = JSONToMap(objectMapper.readTree(line), desc.flatten).withDefaultValue(null) val fields = schema.getAttributeNames.map { fieldName => parseField(data(fieldName), schema.getAttribute(fieldName).getType) } TupleLike(fields: _*) } match { case Success(tuple) => Some(tuple) case Failure(_) => None } } } override def open(): Unit = { val stream = DocumentFactory.openReadonlyDocument(new URI(desc.fileName.get)).asInputStream() // count lines and partition the task to each worker reader = new BufferedReader( new InputStreamReader(stream, desc.fileEncoding.getCharset) ) val offsetValue = desc.offset.getOrElse(0) var lines = reader.lines().iterator().asScala.drop(offsetValue) if (desc.limit.isDefined) lines = lines.take(desc.limit.get) val (it1, it2) = lines.duplicate val count: Int = it1.map(_ => 1).sum val startOffset: Int = offsetValue + count / workerCount * idx val endOffset: Int = offsetValue + (if (idx != workerCount - 1) count / workerCount * (idx + 1) else count) rows = it2.iterator.slice(startOffset, endOffset) } override def close(): Unit = reader.close() } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/text/TextInputSourceOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.text import com.fasterxml.jackson.annotation.JsonProperty import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.tuple.Schema import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{OutputPort, PhysicalOp, SchemaPropagationFunc} import org.apache.texera.amber.operator.metadata.annotations.UIWidget import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.operator.source.SourceOperatorDescriptor import org.apache.texera.amber.util.JSONUtils.objectMapper class TextInputSourceOpDesc extends SourceOperatorDescriptor with TextSourceOpDesc { @JsonProperty(required = true) @JsonSchemaTitle("Text") @JsonSchemaInject(json = UIWidget.UIWidgetTextArea) var textInput: String = _ override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = PhysicalOp .sourcePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.source.scan.text.TextInputSourceOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPropagateSchema( SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema())) ) override def sourceSchema(): Schema = Schema().add(attributeName, attributeType.getType) override def operatorInfo: OperatorInfo = OperatorInfo( userFriendlyName = "Text Input", operatorDescription = "Source data from manually inputted text", OperatorGroupConstants.INPUT_GROUP, inputPorts = List.empty, outputPorts = List(OutputPort()) ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/text/TextInputSourceOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.text import org.apache.texera.amber.core.executor.SourceOperatorExecutor import org.apache.texera.amber.core.tuple.AttributeTypeUtils.parseField import org.apache.texera.amber.core.tuple.TupleLike import org.apache.texera.amber.operator.source.scan.FileAttributeType import org.apache.texera.amber.util.JSONUtils.objectMapper class TextInputSourceOpExec private[text] ( descString: String ) extends SourceOperatorExecutor { private val desc: TextInputSourceOpDesc = objectMapper.readValue(descString, classOf[TextInputSourceOpDesc]) override def produceTuple(): Iterator[TupleLike] = { (if (desc.attributeType.isSingle) { Iterator(desc.textInput) } else { desc.textInput.linesIterator.slice( desc.fileScanOffset.getOrElse(0), desc.fileScanOffset.getOrElse(0) + desc.fileScanLimit.getOrElse(Int.MaxValue) ) }).map(line => TupleLike(desc.attributeType match { case FileAttributeType.SINGLE_STRING => line case FileAttributeType.BINARY => line.getBytes case _ => parseField(line, desc.attributeType.getType) }) ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/text/TextSourceOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.text import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.kjetland.jackson.jsonSchema.annotations.{ JsonSchemaInject, JsonSchemaString, JsonSchemaTitle } import org.apache.texera.amber.operator.metadata.annotations.HideAnnotation import org.apache.texera.amber.operator.source.scan.FileAttributeType /** * TextSourceOpDesc is a trait holding commonly used properties and functions used for variations of text input processing * Create new, identical limit and offset fields with additional annotations to make hideable binary attributes * and strings that are in SingleTuple mode will always read the entire input, so limit / offset are disabled in these cases */ trait TextSourceOpDesc { @JsonProperty(defaultValue = "string", required = true) @JsonSchemaTitle("Attribute Type") var attributeType: FileAttributeType = FileAttributeType.STRING @JsonProperty(defaultValue = "line", required = true) @JsonSchemaTitle("Attribute Name") @JsonDeserialize(contentAs = classOf[java.lang.String]) var attributeName: String = "line" @JsonSchemaTitle("Limit") @JsonDeserialize(contentAs = classOf[Int]) @JsonSchemaInject( strings = Array( new JsonSchemaString(path = HideAnnotation.hideTarget, value = "attributeType"), new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.regex), new JsonSchemaString( path = HideAnnotation.hideExpectedValue, value = "^binary$|^single string$" ) ) ) var fileScanLimit: Option[Int] = None @JsonSchemaTitle("Offset") @JsonDeserialize(contentAs = classOf[Int]) @JsonSchemaInject( strings = Array( new JsonSchemaString(path = HideAnnotation.hideTarget, value = "attributeType"), new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.regex), new JsonSchemaString( path = HideAnnotation.hideExpectedValue, value = "^binary$|^single string$" ) ) ) var fileScanOffset: Option[Int] = None } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.sql import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema} import org.apache.texera.amber.operator.metadata.annotations.{ AutofillAttributeName, BatchByColumn, EnablePresets, UIWidget } import org.apache.texera.amber.operator.source.SourceOperatorDescriptor import java.sql._ abstract class SQLSourceOpDesc extends SourceOperatorDescriptor { @EnablePresets @JsonProperty(required = true) @JsonSchemaTitle("Host") var host: String = _ @EnablePresets @JsonProperty(required = true, defaultValue = "default") @JsonSchemaTitle("Port") @JsonPropertyDescription("A port number or 'default'") var port: String = _ @EnablePresets @JsonProperty(required = true) @JsonSchemaTitle("Database") var database: String = _ @EnablePresets @JsonProperty(required = true) @JsonSchemaTitle("Table Name") var table: String = _ @EnablePresets @JsonProperty(required = true) @JsonSchemaTitle("Username") var username: String = _ @JsonProperty(required = true) @JsonSchemaTitle("Password") @JsonSchemaInject(json = UIWidget.UIWidgetPassword) var password: String = _ @JsonProperty() @JsonSchemaTitle("Limit") @JsonPropertyDescription("max output count") @JsonDeserialize(contentAs = classOf[java.lang.Long]) var limit: Option[Long] = None @JsonProperty() @JsonSchemaTitle("Offset") @JsonPropertyDescription("starting point of output") @JsonDeserialize(contentAs = classOf[java.lang.Long]) var offset: Option[Long] = None @JsonProperty(defaultValue = "false") @JsonSchemaTitle("Keyword Search?") @JsonDeserialize(contentAs = classOf[java.lang.Boolean]) @JsonSchemaInject(json = """{"toggleHidden" : ["keywordSearchByColumn", "keywords"]}""") var keywordSearch: Option[Boolean] = Option(false) @JsonProperty() @JsonSchemaTitle("Keyword Search Column") @JsonDeserialize(contentAs = classOf[java.lang.String]) @AutofillAttributeName var keywordSearchByColumn: Option[String] = None @JsonProperty() @JsonSchemaTitle("Keywords to Search") @JsonDeserialize(contentAs = classOf[java.lang.String]) @JsonSchemaInject(json = UIWidget.UIWidgetTextArea) var keywords: Option[String] = None @JsonProperty(defaultValue = "false") @JsonSchemaTitle("Progressive?") @JsonDeserialize(contentAs = classOf[java.lang.Boolean]) @JsonSchemaInject(json = """{"toggleHidden" : ["batchByColumn", "min", "max", "interval"]}""") var progressive: Option[Boolean] = Option(false) @JsonProperty() @JsonSchemaTitle("Batch by Column") @JsonDeserialize(contentAs = classOf[java.lang.String]) @AutofillAttributeName var batchByColumn: Option[String] = None @JsonProperty(defaultValue = "auto") @JsonSchemaTitle("Min") @JsonDeserialize(contentAs = classOf[java.lang.String]) @BatchByColumn var min: Option[String] = None @JsonProperty(defaultValue = "auto") @JsonSchemaTitle("Max") @JsonDeserialize(contentAs = classOf[java.lang.String]) @BatchByColumn var max: Option[String] = None @JsonProperty(defaultValue = "1000000000") @JsonSchemaTitle("Batch by Interval") @BatchByColumn var interval = 0L override def sourceSchema(): Schema = querySchema // needs to define getters for sub classes to override Jackson Annotations def getKeywords: Option[String] = keywords /** * Establish a connection with the database server base on the info provided by the user * query the MetaData of the table and generate a Tuple.schema accordingly * the "switch" code block shows how SQL data types are mapped to Texera AttributeTypes * * @return Schema */ private def querySchema: Schema = { if ( this.host == null || this.port == null || this.database == null || this.table == null || this.username == null || this.password == null ) { return null } updatePort() try { val attributes = scala.collection.mutable.ListBuffer[Attribute]() val connection = establishConn connection.setReadOnly(true) val databaseMetaData = connection.getMetaData val columns = databaseMetaData.getColumns(null, null, this.table, null) while (columns.next()) { val columnName = columns.getString("COLUMN_NAME") val datatype = columns.getInt("DATA_TYPE") // Map JDBC data types to AttributeType val attributeType = datatype match { case Types.TINYINT | // -6 Types.TINYINT Types.SMALLINT | // 5 Types.SMALLINT Types.INTEGER => // 4 Types.INTEGER AttributeType.INTEGER case Types.FLOAT | // 6 Types.FLOAT Types.REAL | // 7 Types.REAL Types.DOUBLE | // 8 Types.DOUBLE Types.NUMERIC => // 3 Types.NUMERIC AttributeType.DOUBLE case Types.BIT | // -7 Types.BIT Types.BOOLEAN => // 16 Types.BOOLEAN AttributeType.BOOLEAN case Types.BINARY => // -2 Types.BINARY AttributeType.BINARY case Types.DATE | // 91 Types.DATE Types.TIME | // 92 Types.TIME Types.LONGVARCHAR | // -1 Types.LONGVARCHAR Types.CHAR | // 1 Types.CHAR Types.VARCHAR | // 12 Types.VARCHAR Types.NULL | // 0 Types.NULL Types.OTHER => // 1111 Types.OTHER AttributeType.STRING case Types.BIGINT => // -5 Types.BIGINT AttributeType.LONG case Types.TIMESTAMP => // 93 Types.TIMESTAMP AttributeType.TIMESTAMP case _ => throw new RuntimeException( this.getClass.getSimpleName + ": unknown data type: " + datatype ) } // Add the attribute to the list attributes += new Attribute(columnName, attributeType) } connection.close() Schema(attributes.toList) } catch { case e @ (_: SQLException | _: ClassCastException) => throw new RuntimeException( this.getClass.getSimpleName + " failed to connect to the database. " + e.getMessage ) } } @throws[SQLException] protected def establishConn: Connection = null protected def updatePort(): Unit } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.sql import org.apache.texera.amber.core.executor.SourceOperatorExecutor import org.apache.texera.amber.core.tuple.AttributeTypeUtils.{parseField, parseTimestamp} import org.apache.texera.amber.core.tuple._ import org.apache.texera.amber.util.JSONUtils.objectMapper import java.sql._ import scala.collection.mutable.ArrayBuffer import scala.util.control.Breaks.{break, breakable} abstract class SQLSourceOpExec(descString: String) extends SourceOperatorExecutor { val desc: SQLSourceOpDesc = objectMapper.readValue(descString, classOf[SQLSourceOpDesc]) var schema: Schema = _ var curLimit: Option[Long] = None var curOffset: Option[Long] = None // connection and query related val tableNames: ArrayBuffer[String] = ArrayBuffer() var batchByAttribute: Option[Attribute] = None var connection: Connection = _ private var curQuery: Option[PreparedStatement] = None private var curResultSet: Option[ResultSet] = None private var curLowerBound: Number = _ private var upperBound: Number = _ var cachedTuple: Option[Tuple] = None private var querySent: Boolean = false /** * A generator of a Tuple, which converted from a SQL row * * @return Iterator[TupleLike] */ override def produceTuple(): Iterator[TupleLike] = { new Iterator[TupleLike]() { override def hasNext: Boolean = { cachedTuple match { // if existing Tuple in cache, means there exist next Tuple. case Some(_) => true case None => // cache the next Tuple cachedTuple = Option(next()) cachedTuple.isDefined } } /** * Fetch the next row from resultSet, parse it into Tuple and return. * - If resultSet is exhausted, send the next query until no more queries are available. * - If no more queries, return null. * * @throws java.sql.SQLException all possible exceptions from JDBC * @return Tuple */ @throws[SQLException] override def next(): Tuple = { // if has the next Tuple in cache, return it and clear the cache cachedTuple.foreach(tuple => { cachedTuple = None return tuple }) // otherwise, send query to fetch for the next Tuple while (true) { breakable { curResultSet match { case Some(resultSet) => if (resultSet.next()) { // manually skip until the offset position in order to adapt to progressive batches curOffset.foreach(offset => { if (offset > 0) { curOffset = Option(offset - 1) break() } }) // construct Tuple from the next result. val tuple = buildTupleFromRow if (tuple == null) break() // update the limit in order to adapt to progressive batches curLimit.foreach(limit => { if (limit > 0) { curLimit = Some(limit - 1) } }) return tuple } else { // close the current resultSet and query curResultSet.foreach(resultSet => resultSet.close()) curQuery.foreach(query => query.close()) curResultSet = None curQuery = None break() } case None => curQuery = getNextQuery curQuery match { case Some(query) => curResultSet = Option(query.executeQuery) break() case None => curResultSet = None return null } } } } null } } } /** * Establish a connection to the database server and load statistics for constructing future queries. * - tableNames, to check if the input tableName exists on the database server, to prevent SQL injection. * - batchColumnBoundaries, to be used to split mini queries, if progressive mode is enabled. * * @throws java.sql.SQLException all possible exceptions from JDBC */ @throws[SQLException] override def open(): Unit = { batchByAttribute = if (desc.progressive.getOrElse(false)) Option(schema.getAttribute(desc.batchByColumn.get)) else None connection = establishConn() // load user table names from the given database loadTableNames() // validates the input table name if (!tableNames.contains(desc.table)) throw new RuntimeException("Can't find the given table `" + desc.table + "`.") // load for batch column value boundaries used to split mini queries if (desc.progressive.getOrElse(false)) initBatchColumnBoundaries() } /** * close resultSet, preparedStatement and connection * * @throws java.sql.SQLException all possible exceptions from JDBC */ @throws[SQLException] override def close(): Unit = { curResultSet.foreach(resultSet => resultSet.close()) curQuery.foreach(query => query.close()) if (connection != null) connection.close() } /** * Build a Tuple from a row of curResultSet * * @return the new Tuple * @throws java.sql.SQLException all possible exceptions from JDBC */ @throws[SQLException] protected def buildTupleFromRow: Tuple = { val tupleBuilder = Tuple.builder(schema) for (attr <- schema.getAttributes) { breakable { val columnName = attr.getName val columnType = attr.getType val value = curResultSet.get.getObject(columnName) if (value == null) { // add the field as null tupleBuilder.add(attr, null) break() } // otherwise, transform the type of the value tupleBuilder.add(attr, parseField(value, columnType)) } } tupleBuilder.build() } /** * Checks if there is a next query. * - This is mostly used for progressive mode: if the lower bound is * not yet reached upper bound, it will have next query. * - If it is not progressive mode, this method will return false when * invoked the second time. Which means there is only one query. * * @throws java.lang.IllegalArgumentException if the given batchByAttribute's type is * not supported to be incremental. * @return A boolean value whether there exists the next query or not. */ @throws[IllegalArgumentException] protected def hasNextQuery: Boolean = { batchByAttribute match { case Some(attribute) => attribute.getType match { case AttributeType.INTEGER | AttributeType.LONG | AttributeType.TIMESTAMP => curLowerBound.longValue <= upperBound.longValue case AttributeType.DOUBLE => curLowerBound.doubleValue <= upperBound.doubleValue case AttributeType.STRING | AttributeType.ANY | AttributeType.BOOLEAN | _ => throw new IllegalArgumentException("Unexpected type: " + attribute.getType) } case None => val hasNextQuery = !querySent querySent = true hasNextQuery } } protected def terminateSQL(queryBuilder: StringBuilder): Unit = { queryBuilder ++= ";" } protected def addOffset(queryBuilder: StringBuilder): Unit = { queryBuilder ++= " OFFSET ?" } protected def addLimit(queryBuilder: StringBuilder): Unit = { queryBuilder ++= " LIMIT ?" } protected def addBaseSelect(queryBuilder: StringBuilder): Unit = { queryBuilder ++= "\n" + "SELECT * FROM " + desc.table + " where 1 = 1" } /** * Add sliding window SQL statement, based on the batchByAttribute. * Supported types: * - Long, Int: simple incremental by interval, which is a Long value. * - Timestamp: convert to Long type, same as Long. * - Double: incremental by interval, faction part stays the same. * * There will be a lower bound and upper bound for each sliding window. * * The last window would be [lower, upper], while the other windows will * be [lower, nextLower) * * @param queryBuilder the target query builder * @throws java.lang.IllegalArgumentException if the given batchByAttribute's type is * not supported to be incremental. */ @throws[IllegalArgumentException] protected def addBatchSlidingWindow(queryBuilder: StringBuilder): Unit = { var nextLowerBound: Number = null var isLastBatch = false batchByAttribute match { case Some(attribute) => attribute.getType match { case AttributeType.INTEGER | AttributeType.LONG | AttributeType.TIMESTAMP => nextLowerBound = curLowerBound.longValue + desc.interval isLastBatch = nextLowerBound.longValue >= upperBound.longValue case AttributeType.DOUBLE => nextLowerBound = curLowerBound.doubleValue + desc.interval isLastBatch = nextLowerBound.doubleValue >= upperBound.doubleValue case AttributeType.BOOLEAN | AttributeType.STRING | AttributeType.ANY | _ => throw new IllegalArgumentException("Unexpected type: " + attribute.getType) } queryBuilder ++= " AND " + attribute.getName + " >= " + batchAttributeToString(curLowerBound) + " AND " + attribute.getName + (if (isLastBatch) " <= " + batchAttributeToString(upperBound) else " < " + batchAttributeToString(nextLowerBound)) case None => throw new IllegalArgumentException( "no valid batchByColumn to iterate: " + desc.batchByColumn.getOrElse("") ) } curLowerBound = nextLowerBound } /** * Convert the Number value to a String to be concatenate to SQL. * * @param value a Number, contains the value to be converted. * @throws java.lang.IllegalArgumentException when the batchByAttribute is missing or the type is unexpected * @return a String of that value */ @throws[IllegalArgumentException] protected def batchAttributeToString(value: Number): String = { batchByAttribute match { case Some(attribute) => attribute.getType match { case AttributeType.LONG | AttributeType.INTEGER | AttributeType.DOUBLE => String.valueOf(value) case AttributeType.TIMESTAMP => "'" + new Timestamp(value.longValue).toString + "'" case AttributeType.BOOLEAN | AttributeType.STRING | AttributeType.ANY | _ => throw new IllegalArgumentException("Unexpected type: " + attribute.getType) } case None => throw new IllegalArgumentException( "No valid batchByColumn to iterate: " + desc.batchByColumn.getOrElse("") ) } } /** * Fetch for a numeric value of the boundary of the batchByColumn. * * @param side either "MAX" or "MIN" for boundary * @throws java.lang.IllegalArgumentException if the batchByAttribute type is unexpected * @return a numeric value, could be Int, Long or Double */ @throws[IllegalArgumentException] protected def fetchBatchByBoundary(side: String): Number = { batchByAttribute match { case Some(attribute) => var result: Number = null val preparedStatement = connection.prepareStatement( "SELECT " + side + "(" + attribute.getName + ") FROM " + desc.table + ";" ) val resultSet = preparedStatement.executeQuery resultSet.next schema.getAttribute(attribute.getName).getType match { case AttributeType.INTEGER => result = resultSet.getInt(1) case AttributeType.LONG => result = resultSet.getLong(1) case AttributeType.TIMESTAMP => result = resultSet.getTimestamp(1).getTime case AttributeType.DOUBLE => result = resultSet.getDouble(1) case AttributeType.BOOLEAN | AttributeType.STRING | AttributeType.ANY | _ => throw new IllegalStateException("Unexpected value: " + attribute.getType) } resultSet.close() preparedStatement.close() result case None => 0 } } /** * Establishes the connection to database. * * @throws java.sql.SQLException all possible exceptions from JDBC * @return a SQL connection over JDBC */ @throws[SQLException] protected def establishConn(): Connection = null /** * Fetch all table names from the given database. This is used to * check the input table name to prevent from SQL injection. * * @throws java.sql.SQLException all possible exceptions from JDBC */ @throws[SQLException] protected def loadTableNames(): Unit protected def addFilterConditions(queryBuilder: StringBuilder): Unit /** * generate sql query string using the info provided by user. One of following * select * from TableName where 1 = 1 AND MATCH (ColumnName) AGAINST ( ? IN BOOLEAN MODE) LIMIT ?; * select * from TableName where 1 = 1 AND MATCH (ColumnName) AGAINST ( ? IN BOOLEAN MODE); * select * from TableName where 1 = 1 LIMIT ? ; * select * from TableName where 1 = 1; * * with an optional appropriate batchByColumn sliding window, * e.g. create_at >= '2017-01-14 03:47:59.0' AND create_at < '2017-01-15 03:47:59.0' * * Or a fixed offset [OFFSET ?] to be added if not progressive. * * @throws java.lang.IllegalArgumentException if the given batchByAttribute's type is * not supported to be incremental. * @return string of sql query */ @throws[IllegalArgumentException] protected def generateSqlQuery: Option[String] = { // in sql prepared statement, table name cannot be inserted using PreparedStatement.setString // so it has to be inserted here during sql query generation // table has to be verified to be existing in the given schema. val queryBuilder = new StringBuilder // Add base SELECT * with true condition // TODO: add more selection conditions, including alias addBaseSelect(queryBuilder) // add filter conditions if applicable addFilterConditions(queryBuilder) // add sliding window if progressive mode is enabled if (desc.progressive.getOrElse(false) && desc.batchByColumn.isDefined && desc.interval > 0L) addBatchSlidingWindow(queryBuilder) // add limit if provided if (curLimit.isDefined) { if (curLimit.get > 0) addLimit(queryBuilder) else // there should be no more queries as limit is equal or less than 0 return None } // add fixed offset if not progressive if (!desc.progressive.getOrElse(false) && curOffset.isDefined) addOffset(queryBuilder) // end terminateSQL(queryBuilder) Option(queryBuilder.result()) } /** * Get the next query. * - If progressive mode is enabled, this method will be invoked * many times, each yielding the next mini query. * - If progressive mode is not enabled, this method will be invoked * only once, returning the one giant query. * * @throws java.sql.SQLException all possible exceptions from JDBC * @return a PreparedStatement to be filled with values. */ @throws[SQLException] private def getNextQuery: Option[PreparedStatement] = { if (hasNextQuery) { val nextQuery = generateSqlQuery nextQuery match { case Some(query) => val preparedStatement = connection.prepareStatement(query) var curIndex = 1 // fill up the keywords val keywords = desc.keywords.orNull if ( desc.keywordSearch.getOrElse( false ) && desc.keywordSearchByColumn.orNull != null && keywords != null ) { preparedStatement.setString(curIndex, keywords) curIndex += 1 } // fill up limit curLimit match { case Some(limit) => if (limit > 0) preparedStatement.setLong(curIndex, limit) curIndex += 1 case None => } // fill up offset if progressive mode is not enabled if (!desc.progressive.getOrElse(false)) curOffset match { case Some(offset) => preparedStatement.setLong(curIndex, offset) case None => } Option(preparedStatement) case None => None } } else None } /** * Load the lower bound and upper bound of the batchByColumn. Those * bounds will be used in progressive mode to determine mini-queries. * * @throws java.sql.SQLException all possible exceptions from JDBC * @throws java.lang.IllegalArgumentException if the batchByAttribute is missing or the type is unexpected */ @throws[SQLException] @throws[IllegalArgumentException] private def initBatchColumnBoundaries(): Unit = { // TODO: add interval if (batchByAttribute.isDefined && desc.min.isDefined && desc.max.isDefined) { if (desc.min.get.equalsIgnoreCase("auto")) curLowerBound = fetchBatchByBoundary("MIN") else batchByAttribute.get.getType match { case AttributeType.TIMESTAMP => curLowerBound = parseTimestamp(desc.min.get).getTime case AttributeType.LONG => curLowerBound = desc.min.get.toLong case _ => throw new IllegalArgumentException(s"Unsupported type ${batchByAttribute.get.getType}") } if (desc.max.get.equalsIgnoreCase("auto")) upperBound = fetchBatchByBoundary("MAX") else batchByAttribute.get.getType match { case AttributeType.TIMESTAMP => upperBound = parseTimestamp(desc.max.get).getTime case AttributeType.LONG => upperBound = desc.max.get.toLong case _ => throw new IllegalArgumentException(s"Unsupported type ${batchByAttribute.get.getType}") } } else { throw new IllegalArgumentException( s"Missing required progressive configuration, $batchByAttribute, $desc.min or $desc.max." ) } } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBConnUtil.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.sql.asterixdb import kong.unirest.json.JSONObject import kong.unirest.{HttpResponse, JsonNode, Unirest} import scala.collection.mutable import scala.collection.mutable.Map import scala.jdk.CollectionConverters.IteratorHasAsScala import scala.util.{Failure, Success, Try} object AsterixDBConnUtil { // as asterixDB version update is unlikely to happen, this map // is only updated when a new AsterixDBSourceOpExec is initialized var asterixDBVersionMapping: Map[String, String] = Map() def queryAsterixDB( host: String, port: String, statement: String, format: String = "csv" ): Option[Iterator[AnyRef]] = { if (!asterixDBVersionMapping.contains(host)) updateAsterixDBVersionMapping(host, port) val asterixAPIEndpoint = "http://" + host + ":" + port + "/query/service" val response: HttpResponse[JsonNode] = Unirest .post(asterixAPIEndpoint) .header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") .header("Accept-Language", "en-us") .header("Accept-Encoding", "gzip, deflate") .field("statement", statement) .field( "format", if (asterixDBVersionMapping(host).equals("0.9.5")) format else "text/" + format ) .asJson() // if status is 200 OK, store the results if (response.getStatus == 200) { // return results Option(response.getBody.getObject.getJSONArray("results").iterator().asScala) } else throw new RuntimeException( "Send query to asterix failed: " + "error status: " + response.getStatusText + ", " + "error body: " + response.getBody.toString ) } def updateAsterixDBVersionMapping(host: String, port: String): Unit = { var response: HttpResponse[JsonNode] = null // check and determine API version response = Unirest.get("http://" + host + ":" + port + "/admin/version").asJson() if (response.getStatus == 200) asterixDBVersionMapping += (host -> response.getBody.getObject.getString( "git.build.version" )) } def fetchDataTypeFields( datatypeName: String, parentName: String, host: String, port: String ): Predef.Map[String, String] = { val result: mutable.Map[String, String] = mutable.Map() val response = queryAsterixDB( host, port, s"SELECT dt.Derived.Record.Fields FROM Metadata.`Datatype` dt where dt.DatatypeName = '$datatypeName';", format = "JSON" ) Try( response.get .next() .asInstanceOf[JSONObject] .getJSONArray("Fields") ) match { case Success(fields) => fields.forEach(field => { val fieldName: String = field.asInstanceOf[JSONObject].get("FieldName").toString val fieldType: String = field.asInstanceOf[JSONObject].get("FieldType").toString val fieldNameWithParent: String = (if (parentName.nonEmpty) parentName + "." else "") + fieldName if (fieldType.contains("type")) { val childMap = fetchDataTypeFields( fieldType, fieldNameWithParent, host, port ) result ++= childMap } else { result.put(fieldNameWithParent, fieldType) } }) case Failure(_) => // could due to the following reasons: // the specific type's metadata is not found // the current model does not have a good support for type of arrays, thus arrays are ignored } result.toMap } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.sql.asterixdb import com.fasterxml.jackson.annotation.{ JsonIgnoreProperties, JsonProperty, JsonPropertyDescription } import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import kong.unirest.json.JSONObject import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{OutputPort, PhysicalOp, SchemaPropagationFunc} import org.apache.texera.amber.operator.filter.FilterPredicate import org.apache.texera.amber.operator.metadata.annotations.{ AutofillAttributeName, AutofillAttributeNameList, UIWidget } import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.operator.source.sql.SQLSourceOpDesc import org.apache.texera.amber.operator.source.sql.asterixdb.AsterixDBConnUtil.{ fetchDataTypeFields, queryAsterixDB } import org.apache.texera.amber.util.JSONUtils.objectMapper @JsonIgnoreProperties(value = Array("username", "password")) class AsterixDBSourceOpDesc extends SQLSourceOpDesc { @JsonProperty(defaultValue = "false") @JsonSchemaTitle("Geo Search?") @JsonDeserialize(contentAs = classOf[java.lang.Boolean]) @JsonSchemaInject(json = """{"toggleHidden" : ["geoSearchByColumns", "geoSearchBoundingBox"]}""") var geoSearch: Option[Boolean] = Option(false) @JsonProperty() @JsonSchemaTitle("Geo Search By Columns") @JsonPropertyDescription( "column(s) to check if any of them is in the bounding box below" ) @AutofillAttributeNameList // TODO: set it to one column in the future since it implicitly adds OR semantics var geoSearchByColumns: List[String] = List.empty @JsonProperty() @JsonSchemaTitle("Geo Search Bounding Box") @JsonPropertyDescription( "at least 2 entries should be provided to form a bounding box. format of each entry: long, lat" ) var geoSearchBoundingBox: List[String] = List.empty @JsonProperty(defaultValue = "false") @JsonSchemaTitle("Regex Search?") @JsonDeserialize(contentAs = classOf[java.lang.Boolean]) @JsonSchemaInject(json = """{"toggleHidden" : ["regexSearchByColumn", "regex"]}""") var regexSearch: Option[Boolean] = Option(false) @JsonProperty() @JsonSchemaTitle("Regex Search By Column") @JsonDeserialize(contentAs = classOf[java.lang.String]) @AutofillAttributeName var regexSearchByColumn: Option[String] = None @JsonProperty() @JsonSchemaTitle("Regex to Search") @JsonDeserialize(contentAs = classOf[java.lang.String]) @JsonSchemaInject(json = UIWidget.UIWidgetTextArea) var regex: Option[String] = None @JsonProperty(defaultValue = "false") @JsonSchemaTitle("Filter Condition?") @JsonDeserialize(contentAs = classOf[java.lang.Boolean]) @JsonSchemaInject(json = """{"toggleHidden" : ["predicates"]}""") var filterCondition: Option[Boolean] = Option(false) @JsonProperty(value = "predicates", required = false) @JsonPropertyDescription("multiple predicates in OR") var filterPredicates: List[FilterPredicate] = List() @JsonProperty() @JsonSchemaTitle("Keywords to Search") @JsonDeserialize(contentAs = classOf[java.lang.String]) @JsonSchemaInject(json = UIWidget.UIWidgetTextArea) @JsonPropertyDescription( "\"['hello', 'world'], {'mode':'any'}\" OR \"['hello', 'world'], {'mode':'all'}\"" ) override def getKeywords: Option[String] = super.getKeywords override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = PhysicalOp .sourcePhysicalOp( workflowId, executionId, this.operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.source.sql.asterixdb.AsterixDBSourceOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPropagateSchema( SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema())) ) override def operatorInfo: OperatorInfo = OperatorInfo( "AsterixDB Source", "Read data from a AsterixDB instance", OperatorGroupConstants.DATABASE_GROUP, inputPorts = List.empty, outputPorts = List(OutputPort()) ) override def updatePort(): Unit = port = if (port.trim().equals("default")) "19002" else port override def sourceSchema(): Schema = { if (this.host == null || this.port == null || this.database == null || this.table == null) { return null } updatePort() // Query dataset's Datatype from Metadata.`Datatype` val datasetDataType = queryAsterixDB( host, port, s"SELECT DatatypeName FROM Metadata.`Dataset` ds where ds.`DatasetName`='$table';", format = "JSON" ).get.next().asInstanceOf[JSONObject].getString("DatatypeName") // Query field types from Metadata.`Datatype` val fields = fetchDataTypeFields(datasetDataType, "", host, port) // Collect attributes by sorting field names and mapping them to Attribute instances val attributes = fields.keys.toList.sorted.map { key => new Attribute(key, attributeTypeFromAsterixDBType(fields(key))) } Schema(attributes) } private def attributeTypeFromAsterixDBType(inputType: String): AttributeType = inputType match { case "boolean" => AttributeType.BOOLEAN case "int32" => AttributeType.INTEGER case "int64" => AttributeType.LONG case "float" | "double" => AttributeType.DOUBLE case "datetime" | "date" => AttributeType.TIMESTAMP case "string" | _ => AttributeType.STRING } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.sql.asterixdb import com.github.tototoshi.csv.CSVParser import org.apache.texera.amber.core.tuple.AttributeTypeUtils.parseField import org.apache.texera.amber.core.tuple.{AttributeType, Tuple, TupleLike} import org.apache.texera.amber.operator.source.sql.SQLSourceOpExec import org.apache.texera.amber.operator.source.sql.asterixdb.AsterixDBConnUtil.{ queryAsterixDB, updateAsterixDBVersionMapping } import org.apache.texera.amber.util.JSONUtils.objectMapper import java.sql._ import java.time.format.DateTimeFormatter import java.time.{ZoneId, ZoneOffset} import scala.util.control.Breaks.{break, breakable} import scala.util.{Failure, Success, Try} class AsterixDBSourceOpExec private[asterixdb] ( descString: String ) extends SQLSourceOpExec(descString) { override val desc: AsterixDBSourceOpDesc = objectMapper.readValue(descString, classOf[AsterixDBSourceOpDesc]) schema = desc.sourceSchema() // format Timestamp. TODO: move to some util package private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(ZoneId.from(ZoneOffset.UTC)) private var curQueryString: Option[String] = None private var curResultIterator: Option[Iterator[AnyRef]] = None override def open(): Unit = { // update AsterixDB API version upon open updateAsterixDBVersionMapping(desc.host, desc.port) super.open() } /** * A generator of a Tuple, which converted from a CSV row of fields from AsterixDB * * @return Iterator[TupleLike] */ override def produceTuple(): Iterator[TupleLike] = { new Iterator[TupleLike]() { override def hasNext: Boolean = { cachedTuple match { // if existing Tuple in cache, means there exist next Tuple. case Some(_) => true case None => // cache the next Tuple cachedTuple = Option(next()) cachedTuple.isDefined } } override def next(): Tuple = { // if has the next Tuple in cache, return it and clear the cache cachedTuple.foreach(tuple => { cachedTuple = None return tuple }) // otherwise, send query to fetch for the next Tuple while (true) { breakable { curResultIterator match { case Some(resultSet) => if (resultSet.hasNext) { // manually skip until the offset position in order to adapt to progressive batches curOffset.foreach(offset => { if (offset > 0) { curOffset = Option(offset - 1) break() } }) // construct Tuple from the next result. val tuple = buildTupleFromRow if (tuple == null) break() // update the limit in order to adapt to progressive batches curLimit.foreach(limit => { if (limit > 0) { curLimit = Option(limit - 1) } }) return tuple } else { // close the current resultSet and query curResultIterator = None curQueryString = None break() } case None => curQueryString = if (hasNextQuery) generateSqlQuery else None curQueryString match { case Some(query) => curResultIterator = queryAsterixDB(desc.host, desc.port, query) break() case None => curResultIterator = None return null } } } } null } } } /** * Build a Tuple from a row of curResultIterator * * @return the new Tuple */ override def buildTupleFromRow: Tuple = { val tupleBuilder = Tuple.builder(schema) val row = curResultIterator.get.next().toString var values: Option[List[String]] = None try { values = CSVParser.parse(row, '\\', ',', '"') if (values == null) { return null } for (i <- schema.getAttributes.indices) { val attr = schema.getAttributes(i) breakable { val columnType = attr.getType var value: String = null Try({ value = values.get(i) }) if (value == null || value.equals("null")) { // add the field as null tupleBuilder.add(attr, null) break() } // otherwise, transform the type of the value tupleBuilder.add( attr, parseField(value.stripSuffix("\"").stripPrefix("\""), columnType) ) } } tupleBuilder.build() } catch { case _: Exception => null } } /** * close curResultIterator, curQueryString */ override def close(): Unit = { curResultIterator = None curQueryString = None } /** * add naive support for full text search. * input is either * ['hello', 'world'], {'mode':'any'} * or * ['hello', 'world'], {'mode':'all'} * * @param queryBuilder queryBuilder for concatenation * @throws java.lang.IllegalArgumentException if attribute does not support string based search */ @throws[IllegalArgumentException] def addFilterConditions(queryBuilder: StringBuilder): Unit = { if (desc.keywordSearch.getOrElse(false)) { addKeywordSearch(queryBuilder) } if (desc.regexSearch.getOrElse(false)) { addRegexSearch(queryBuilder) } if (desc.geoSearch.getOrElse(false)) { addGeoSearch(queryBuilder) } if (desc.filterCondition.getOrElse(false)) { addGeneralFilterCondition(queryBuilder) } } private def addKeywordSearch(queryBuilder: StringBuilder): Unit = { val keywordSearchByColumn = desc.keywordSearchByColumn.orNull val keywords = desc.keywords.orNull if (keywordSearchByColumn != null && keywords != null) { val columnType = schema.getAttribute(keywordSearchByColumn).getType if (columnType == AttributeType.STRING) { queryBuilder ++= " AND ftcontains(" + keywordSearchByColumn + ", " + keywords + ") " } else throw new IllegalArgumentException("Can't do keyword search on type " + columnType.toString) } } private def addRegexSearch(queryBuilder: StringBuilder): Unit = { val regexSearchByColumn = desc.regexSearchByColumn.orNull val regex = desc.regex.orNull if (regexSearchByColumn != null && regex != null) { val regexColumnType = schema.getAttribute(regexSearchByColumn).getType if (regexColumnType == AttributeType.STRING) { queryBuilder ++= " AND regexp_contains(" + regexSearchByColumn + ", \"" + regex + "\") " } else throw new IllegalArgumentException( "Can't do regex search on type " + regexColumnType.toString ) } } private def addGeoSearch(queryBuilder: StringBuilder): Unit = { // geolocation must contain more than 1 points to from a rectangle or polygon if (desc.geoSearchBoundingBox.size > 1 && desc.geoSearchByColumns.nonEmpty) { val shape = { val points = desc.geoSearchBoundingBox.flatMap(s => s.split(",").map(sub => sub.toDouble)) if (desc.geoSearchBoundingBox.size == 2) { "create_rectangle(create_point(%.6f,%.6f), create_point(%.6f,%.6f))".format(points: _*) } else { "create_polygon([" + points.map(x => "%.6f".format(x)).mkString(",") + "])" } } queryBuilder ++= " AND (" queryBuilder ++= desc.geoSearchByColumns .map { attr => s"spatial_intersect($attr, $shape)" } .mkString(" OR ") queryBuilder ++= " ) " } } private def addGeneralFilterCondition(queryBuilder: StringBuilder): Unit = { if (desc.filterCondition.getOrElse(false) && desc.filterPredicates.nonEmpty) { val filterString = desc.filterPredicates .map(p => s"(${p.attribute} ${p.condition.getName} ${p.value})") .mkString(" OR ") queryBuilder ++= s" AND ( $filterString ) " } } /** * Fetch for a numeric value of the boundary of the batchByColumn. * * @param side either "MAX" or "MIN" for boundary * @return a numeric value, could be Int, Long or Double */ override def fetchBatchByBoundary(side: String): Number = { batchByAttribute match { case Some(attribute) => val resultString = queryAsterixDB( desc.host, desc.port, "SELECT " + side + "(" + attribute.getName + ") FROM " + desc.database + "." + desc.table + ";" ).get.next().toString.stripLineEnd Try( parseField( resultString.stripSuffix("\"").stripPrefix("\""), attribute.getType ) ) match { case Success(timestamp: Timestamp) => parseField(timestamp, AttributeType.LONG).asInstanceOf[Number] case Success(otherTypes) => otherTypes.asInstanceOf[Number] case Failure(_) => 0 } case None => 0 } } override def addBaseSelect(queryBuilder: StringBuilder): Unit = { queryBuilder ++= "\n" + s"SELECT ${schema.getAttributeNames.zipWithIndex .map((entry: (String, Int)) => { s"if_missing(${entry._1},null) field_${entry._2}" }) .mkString(", ")} FROM $desc.database.$desc.table WHERE 1 = 1 " } override def addLimit(queryBuilder: StringBuilder): Unit = { queryBuilder ++= " LIMIT " + curLimit.get } override def addOffset(queryBuilder: StringBuilder): Unit = { queryBuilder ++= " OFFSET " + curOffset.get } @throws[IllegalArgumentException] override def batchAttributeToString(value: Number): String = { batchByAttribute match { case Some(attribute) => attribute.getType match { case AttributeType.LONG | AttributeType.INTEGER | AttributeType.DOUBLE => String.valueOf(value) case AttributeType.TIMESTAMP => "datetime('" + formatter.format(new Timestamp(value.longValue).toInstant) + "')" case AttributeType.BOOLEAN | AttributeType.STRING | AttributeType.ANY | _ => throw new IllegalArgumentException("Unexpected type: " + attribute.getType) } case None => throw new IllegalArgumentException( "No valid batchByColumn to iterate: " + desc.batchByColumn.getOrElse("") ) } } /** * Fetch all table names from the given database. This is used to * check the input table name to prevent from SQL injection. */ override protected def loadTableNames(): Unit = { // fetch for all tables, it is also equivalent to a health check val tables = queryAsterixDB(desc.host, desc.port, "select `DatasetName` from Metadata.`Dataset`;") tables.get.foreach(table => { tableNames.append(table.toString.stripPrefix("\"").stripLineEnd.stripSuffix("\"")) }) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLConnUtil.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.sql.mysql import java.sql.{Connection, DriverManager, SQLException} object MySQLConnUtil { @throws[SQLException] def connect( host: String, port: String, database: String, username: String, password: String ): Connection = { val url = "jdbc:mysql://" + host + ":" + port + "/" + database + "?autoReconnect=true&useSSL=true" val connection = DriverManager.getConnection(url, username, password) // set to readonly to improve efficiency connection.setReadOnly(true) connection } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLSourceOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.sql.mysql import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{OutputPort, PhysicalOp, SchemaPropagationFunc} import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.operator.source.sql.SQLSourceOpDesc import org.apache.texera.amber.operator.source.sql.mysql.MySQLConnUtil.connect import org.apache.texera.amber.util.JSONUtils.objectMapper import java.sql.{Connection, SQLException} @deprecated("MySQL source operator is no longer executable.", "1.1.0-incubating") class MySQLSourceOpDesc extends SQLSourceOpDesc { override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = PhysicalOp .sourcePhysicalOp( workflowId, executionId, this.operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.source.sql.mysql.MySQLSourceOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPropagateSchema( SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema())) ) override def operatorInfo: OperatorInfo = OperatorInfo( "MySQL Source", "Read data from a MySQL instance", OperatorGroupConstants.DATABASE_GROUP, inputPorts = List.empty, outputPorts = List(OutputPort()) ) @throws[SQLException] override def establishConn: Connection = connect(host, port, database, username, password) override def updatePort(): Unit = port = if (port.trim().equals("default")) "3306" else port } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLSourceOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.sql.mysql import org.apache.texera.amber.core.tuple.AttributeType import org.apache.texera.amber.operator.source.sql.SQLSourceOpExec import org.apache.texera.amber.operator.source.sql.mysql.MySQLConnUtil.connect import org.apache.texera.amber.util.JSONUtils.objectMapper import java.sql._ @deprecated("MySQL source operator is no longer executable.", "1.1.0-incubating") class MySQLSourceOpExec private[mysql] ( descString: String ) extends SQLSourceOpExec(descString) { override val desc: MySQLSourceOpDesc = objectMapper.readValue(descString, classOf[MySQLSourceOpDesc]) schema = desc.sourceSchema() val FETCH_TABLE_NAMES_SQL = "SELECT table_name FROM information_schema.tables WHERE table_schema = ?;" @throws[SQLException] override def establishConn(): Connection = connect(desc.host, desc.port, desc.database, desc.username, desc.password) @throws[RuntimeException] override def addFilterConditions(queryBuilder: StringBuilder): Unit = { val keywordSearchByColumn = desc.keywordSearchByColumn.orNull if ( desc.keywordSearch.getOrElse(false) && keywordSearchByColumn != null && desc.keywords != null ) { val columnType = schema.getAttribute(keywordSearchByColumn).getType if (columnType == AttributeType.STRING) // in sql prepared statement, column name cannot be inserted using PreparedStatement.setString either queryBuilder ++= " AND MATCH(" + keywordSearchByColumn + ") AGAINST (? IN BOOLEAN MODE)" else throw new RuntimeException("Can't do keyword search on type " + columnType.toString) } } @throws[SQLException] override protected def loadTableNames(): Unit = { val preparedStatement = connection.prepareStatement(FETCH_TABLE_NAMES_SQL) preparedStatement.setString(1, desc.database) val resultSet = preparedStatement.executeQuery while ({ resultSet.next }) { tableNames += resultSet.getString(1) } resultSet.close() preparedStatement.close() } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLConnUtil.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.sql.postgresql import java.sql.{Connection, DriverManager, SQLException} object PostgreSQLConnUtil { @throws[SQLException] def connect( host: String, port: String, database: String, username: String, password: String ): Connection = { val url = "jdbc:postgresql://" + host + ":" + port + "/" + database val connection = DriverManager.getConnection(url, username, password) // set to readonly to improve efficiency connection.setReadOnly(true) connection } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLSourceOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.sql.postgresql import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{OutputPort, PhysicalOp, SchemaPropagationFunc} import org.apache.texera.amber.operator.metadata.annotations.UIWidget import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.operator.source.sql.SQLSourceOpDesc import org.apache.texera.amber.operator.source.sql.postgresql.PostgreSQLConnUtil.connect import org.apache.texera.amber.util.JSONUtils.objectMapper import java.sql.{Connection, SQLException} class PostgreSQLSourceOpDesc extends SQLSourceOpDesc { @JsonProperty() @JsonSchemaTitle("Keywords to Search") @JsonDeserialize(contentAs = classOf[java.lang.String]) @JsonSchemaInject(json = UIWidget.UIWidgetTextArea) @JsonPropertyDescription( "E.g. 'sore & throat' for AND; 'sore', 'throat' for OR. See official postgres documents for details." ) override def getKeywords: Option[String] = super.getKeywords override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = PhysicalOp .sourcePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.source.sql.postgresql.PostgreSQLSourceOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPropagateSchema( SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema())) ) override def operatorInfo: OperatorInfo = OperatorInfo( "PostgreSQL Source", "Read data from a PostgreSQL instance", OperatorGroupConstants.DATABASE_GROUP, inputPorts = List.empty, outputPorts = List(OutputPort()) ) @throws[SQLException] override def establishConn: Connection = connect(host, port, database, username, password) override protected def updatePort(): Unit = port = if (port.trim().equals("default")) "5432" else port } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLSourceOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.sql.postgresql import org.apache.texera.amber.core.tuple.AttributeType import org.apache.texera.amber.operator.source.sql.SQLSourceOpExec import org.apache.texera.amber.operator.source.sql.postgresql.PostgreSQLConnUtil.connect import org.apache.texera.amber.util.JSONUtils.objectMapper import java.sql._ class PostgreSQLSourceOpExec private[postgresql] (descString: String) extends SQLSourceOpExec(descString) { override val desc: PostgreSQLSourceOpDesc = objectMapper.readValue(descString, classOf[PostgreSQLSourceOpDesc]) schema = desc.sourceSchema() val FETCH_TABLE_NAMES_SQL = "SELECT table_name FROM information_schema.tables WHERE table_type='BASE TABLE';" @throws[SQLException] override def establishConn(): Connection = connect(desc.host, desc.port, desc.database, desc.username, desc.password) @throws[RuntimeException] override def addFilterConditions(queryBuilder: StringBuilder): Unit = { val keywordSearchByColumn = desc.keywordSearchByColumn.orNull if ( desc.keywordSearch.getOrElse(false) && keywordSearchByColumn != null && desc.keywords != null ) { val columnType = schema.getAttribute(keywordSearchByColumn).getType if (columnType == AttributeType.STRING) { // in sql prepared statement, column name cannot be inserted using PreparedStatement.setString either queryBuilder ++= " AND " + keywordSearchByColumn + " @@ to_tsquery(?)" // OPTIMIZE: no fulltext index is required, having a built fulltext index can help performance on large dataset. // OPTIMIZE: limited support on the default language, english. equivalent `to_tsquery('english', ?)` } else throw new RuntimeException("Can't do keyword search on type " + columnType.toString) } } @throws[SQLException] override protected def loadTableNames(): Unit = { val preparedStatement = connection.prepareStatement(FETCH_TABLE_NAMES_SQL) val resultSet = preparedStatement.executeQuery while ({ resultSet.next }) { tableNames += resultSet.getString(1) } resultSet.close() preparedStatement.close() } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/split/SplitOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.split import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.google.common.base.Preconditions import com.kjetland.jackson.jsonSchema.annotations.{ JsonSchemaInject, JsonSchemaString, JsonSchemaTitle } import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow._ import org.apache.texera.amber.operator.LogicalOp import org.apache.texera.amber.operator.metadata.annotations.HideAnnotation import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.util.JSONUtils.objectMapper class SplitOpDesc extends LogicalOp { @JsonSchemaTitle("Split Percentage") @JsonProperty(defaultValue = "80") @JsonPropertyDescription("percentage of data going to the upper port") var k: Int = 80 @JsonSchemaTitle("Auto-Generate Seed") @JsonPropertyDescription("Shuffle the data based on a random seed") @JsonProperty(defaultValue = "true") var random: Boolean = true @JsonSchemaTitle("Seed") @JsonProperty(defaultValue = "1") @JsonPropertyDescription("An int for reproducible output across multiple runs") @JsonSchemaInject( strings = Array( new JsonSchemaString(path = HideAnnotation.hideTarget, value = "random"), new JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.equals), new JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = "true") ) ) var seed: Int = 1 override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.split.SplitOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withParallelizable(false) .withPropagateSchema( SchemaPropagationFunc(inputSchemas => { Preconditions.checkArgument(inputSchemas.size == 1) val outputSchema = inputSchemas.values.head operatorInfo.outputPorts.map(port => port.id -> outputSchema).toMap }) ) } override def operatorInfo: OperatorInfo = { OperatorInfo( userFriendlyName = "Split", operatorDescription = "Split data to two different ports", operatorGroupName = OperatorGroupConstants.UTILITY_GROUP, inputPorts = List(InputPort()), outputPorts = List( OutputPort(PortIdentity()), OutputPort(PortIdentity(1)) ), dynamicInputPorts = true, dynamicOutputPorts = true ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/split/SplitOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.split import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.util.JSONUtils.objectMapper import scala.util.Random class SplitOpExec( descString: String ) extends OperatorExecutor { val desc: SplitOpDesc = objectMapper.readValue(descString, classOf[SplitOpDesc]) var random: Random = _ override def open(): Unit = { random = if (desc.random) new Random() else new Random(desc.seed) } override def close(): Unit = { random = null } override def processTupleMultiPort( tuple: Tuple, port: Int ): Iterator[(TupleLike, Option[PortIdentity])] = { val isTraining = random.nextInt(100) < desc.k // training output port: 0, testing output port: 1 val port = if (isTraining) PortIdentity(0) else PortIdentity(1) Iterator.single((tuple, Some(port))) } override def processTuple(tuple: Tuple, port: Int): Iterator[Tuple] = ??? } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/substringSearch/SubstringSearchOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.substringSearch import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PhysicalOp} import org.apache.texera.amber.operator.filter.FilterOpDesc import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.util.JSONUtils.objectMapper class SubstringSearchOpDesc extends FilterOpDesc { @JsonProperty(required = true) @JsonSchemaTitle("attribute") @JsonPropertyDescription("column to search substring on") @AutofillAttributeName var attribute: String = _ @JsonProperty(required = true) @JsonSchemaTitle("Substring") @JsonPropertyDescription("substring") var substring: String = _ @JsonProperty(required = true, defaultValue = "false") @JsonSchemaTitle("Case Sensitive") @JsonPropertyDescription("Whether the substring match is case sensitive.") var isCaseSensitive: Boolean = false override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.substringSearch.SubstringSearchOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) } override def operatorInfo: OperatorInfo = OperatorInfo( userFriendlyName = "Substring Search", operatorDescription = "Search for Substring(s) in a string column", operatorGroupName = OperatorGroupConstants.SEARCH_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort()), supportReconfiguration = true ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/substringSearch/SubstringSearchOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.substringSearch import org.apache.texera.amber.core.tuple.Tuple import org.apache.texera.amber.operator.filter.FilterOpExec import org.apache.texera.amber.util.JSONUtils.objectMapper class SubstringSearchOpExec(descString: String) extends FilterOpExec { private val desc: SubstringSearchOpDesc = objectMapper.readValue(descString, classOf[SubstringSearchOpDesc]) this.setFilterFunc(findSubstring) private def findSubstring(tuple: Tuple): Boolean = { val content = tuple.getField(desc.attribute).toString if (desc.isCaseSensitive) { content.contains(desc.substring) } else { content.toLowerCase.contains(desc.substring.toLowerCase) } } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/symmetricDifference/SymmetricDifferenceOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.symmetricDifference import com.google.common.base.Preconditions import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow._ import org.apache.texera.amber.operator.LogicalOp import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} class SymmetricDifferenceOpDesc extends LogicalOp { override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.symmetricDifference.SymmetricDifferenceOpExec" ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPartitionRequirement(List(Option(HashPartition()), Option(HashPartition()))) .withDerivePartition(_ => HashPartition(List())) .withPropagateSchema(SchemaPropagationFunc(inputSchemas => { Preconditions.checkArgument(inputSchemas.values.toSet.size == 1) val outputSchema = inputSchemas.values.head operatorInfo.outputPorts.map(port => port.id -> outputSchema).toMap })) } override def operatorInfo: OperatorInfo = OperatorInfo( "SymmetricDifference", "find the symmetric difference (the set of elements which are in either of the sets, but not in their intersection) of two inputs", OperatorGroupConstants.SET_GROUP, inputPorts = List(InputPort(PortIdentity(0)), InputPort(PortIdentity(1))), outputPorts = List(OutputPort(blocking = true)) ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/symmetricDifference/SymmetricDifferenceOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.symmetricDifference import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} import scala.collection.mutable class SymmetricDifferenceOpExec extends OperatorExecutor { private var leftSet: mutable.HashSet[Tuple] = _ private var rightSet: mutable.HashSet[Tuple] = _ private var exhaustedCounter: Int = _ override def open(): Unit = { leftSet = new mutable.HashSet[Tuple]() rightSet = new mutable.HashSet[Tuple]() exhaustedCounter = 0 } override def close(): Unit = { leftSet.clear() rightSet.clear() } override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = { // add the tuple to corresponding set if (port == 0) leftSet += tuple else rightSet += tuple Iterator() } override def onFinish(port: Int): Iterator[TupleLike] = { exhaustedCounter += 1 if (2 == exhaustedCounter) { // both streams are exhausted, take the intersect and return the results leftSet.union(rightSet).diff(leftSet.intersect(rightSet)).iterator } else { // only one of the stream is exhausted, continue accepting tuples Iterator() } } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/typecasting/TypeCastingOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.typecasting import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.tuple.{AttributeTypeUtils, Schema} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow._ import org.apache.texera.amber.operator.map.MapOpDesc import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.util.JSONUtils.objectMapper class TypeCastingOpDesc extends MapOpDesc { @JsonProperty(required = true) @JsonSchemaTitle("TypeCasting Units") @JsonPropertyDescription("Multiple type castings") var typeCastingUnits: List[TypeCastingUnit] = List.empty override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { if (typeCastingUnits == null) typeCastingUnits = List.empty PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.typecasting.TypeCastingOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPropagateSchema( SchemaPropagationFunc { inputSchemas: Map[PortIdentity, Schema] => val outputSchema = typeCastingUnits.foldLeft(inputSchemas.values.head) { (schema, unit) => AttributeTypeUtils.SchemaCasting(schema, unit.attribute, unit.resultType) } Map(operatorInfo.outputPorts.head.id -> outputSchema) } ) } override def operatorInfo: OperatorInfo = { OperatorInfo( "Type Casting", "Cast between types", OperatorGroupConstants.CLEANING_GROUP, List(InputPort()), List(OutputPort()) ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/typecasting/TypeCastingOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.typecasting import org.apache.texera.amber.core.tuple.{AttributeTypeUtils, Tuple, TupleLike} import org.apache.texera.amber.operator.map.MapOpExec import org.apache.texera.amber.util.JSONUtils.objectMapper class TypeCastingOpExec(descString: String) extends MapOpExec { private val desc: TypeCastingOpDesc = objectMapper.readValue(descString, classOf[TypeCastingOpDesc]) this.setMapFunc(castTuple) private def castTuple(tuple: Tuple): TupleLike = AttributeTypeUtils.tupleCasting( tuple, desc.typeCastingUnits .map(typeCastingUnit => typeCastingUnit.attribute -> typeCastingUnit.resultType) .toMap ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/typecasting/TypeCastingUnit.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.typecasting; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle; import org.apache.texera.amber.core.tuple.AttributeType; import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName; @JsonSchemaInject(json = "{" + " \"attributeTypeRules\": {" + " \"attribute\": {" + " \"allOf\": [" + " {" + " \"if\": {" + " \"resultType\": {" + " \"valEnum\": [\"integer\"]" + " }" + " }," + " \"then\": {" + " \"enum\": [\"string\", \"long\", \"double\", \"boolean\"]" + " }" + " }," + " {" + " \"if\": {" + " \"resultType\": {" + " \"valEnum\": [\"double\"]" + " }" + " }," + " \"then\": {" + " \"enum\": [\"string\", \"integer\", \"long\", \"boolean\"]" + " }" + " }," + " {" + " \"if\": {" + " \"resultType\": {" + " \"valEnum\": [\"boolean\"]" + " }" + " }," + " \"then\": {" + " \"enum\": [\"string\", \"integer\", \"long\", \"double\"]" + " }" + " }," + " {" + " \"if\": {" + " \"resultType\": {" + " \"valEnum\": [\"long\"]" + " }" + " }," + " \"then\": {" + " \"enum\": [\"string\", \"integer\", \"double\", \"boolean\", \"timestamp\"]" + " }" + " }," + " {" + " \"if\": {" + " \"resultType\": {" + " \"valEnum\": [\"timestamp\"]" + " }" + " }," + " \"then\": {" + " \"enum\": [\"string\", \"long\"]" + " }" + " }" + " " + " ]" + " }" + " }" + "}" ) public class TypeCastingUnit { @JsonProperty(required = true) @JsonSchemaTitle("Attribute") @JsonPropertyDescription("Attribute for type casting") @AutofillAttributeName public String attribute; @JsonProperty(required = true) @JsonSchemaTitle("Cast type") @JsonPropertyDescription("Result type after type casting") public AttributeType resultType; //TODO: override equals to pass equality check for typecasting operator during cache status update } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/java/JavaUDFOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.udf.java import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.google.common.base.Preconditions import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.executor.OpExecWithCode import org.apache.texera.amber.core.tuple.{Attribute, Schema} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow._ import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.operator.{LogicalOp, PortDescription, StateTransferFunc} import scala.util.{Success, Try} class JavaUDFOpDesc extends LogicalOp { @JsonProperty( required = true, defaultValue = "import org.apache.texera.amber.operator.map.MapOpExec;\n" + "import org.apache.texera.amber.core.tuple.Tuple;\n" + "import org.apache.texera.amber.core.tuple.TupleLike;\n" + "import scala.Function1;\n" + "import java.io.Serializable;\n" + "\n" + "public class JavaUDFOpExec extends MapOpExec {\n" + " public JavaUDFOpExec () {\n" + " this.setMapFunc((Function1 & Serializable) this::processTuple);\n" + " }\n" + " \n" + " public TupleLike processTuple(Tuple tuple) {\n" + " return tuple;\n" + " }\n" + "}" ) @JsonSchemaTitle("Java UDF script") @JsonPropertyDescription("Input your code here") var code: String = "" @JsonProperty(required = true, defaultValue = "1") @JsonSchemaTitle("Worker count") @JsonPropertyDescription("Specify how many parallel workers to launch") var workers: Int = Int.box(1) @JsonProperty(required = true, defaultValue = "true") @JsonSchemaTitle("Retain input columns") @JsonPropertyDescription("Keep the original input columns?") var retainInputColumns: Boolean = Boolean.box(false) @JsonProperty @JsonSchemaTitle("Extra output column(s)") @JsonPropertyDescription( "Name of the newly added output columns that the UDF will produce, if any" ) var outputColumns: List[Attribute] = List() override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { Preconditions.checkArgument(workers >= 1, "Need at least 1 worker.", Array()) val opInfo = this.operatorInfo val partitionRequirement: List[Option[PartitionInfo]] = if (inputPorts != null) { inputPorts.map(p => Option(p.partitionRequirement)) } else { opInfo.inputPorts.map(_ => None) } val propagateSchema = (inputSchemas: Map[PortIdentity, Schema]) => { val inputSchema = inputSchemas(operatorInfo.inputPorts.head.id) var outputSchema = if (retainInputColumns) inputSchema else Schema() // For any javaUDFType, it can add custom output columns (attributes). if (outputColumns != null) { if (retainInputColumns) { // Check if columns are duplicated for (column <- outputColumns) { if (inputSchema.containsAttribute(column.getName)) throw new RuntimeException("Column name " + column.getName + " already exists!") } } // Add custom output columns outputSchema = outputSchema.add(outputColumns) } Map(operatorInfo.outputPorts.head.id -> outputSchema) } if (workers > 1) PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithCode(code, "java") ) .withDerivePartition(_ => UnknownPartition()) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPartitionRequirement(partitionRequirement) .withIsOneToManyOp(true) .withParallelizable(true) .withSuggestedWorkerNum(workers) .withPropagateSchema(SchemaPropagationFunc(propagateSchema)) else PhysicalOp .manyToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithCode(code, "java") ) .withDerivePartition(_ => UnknownPartition()) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPartitionRequirement(partitionRequirement) .withIsOneToManyOp(true) .withParallelizable(false) .withPropagateSchema(SchemaPropagationFunc(propagateSchema)) } override def operatorInfo: OperatorInfo = { val inputPortInfo = if (inputPorts != null) { inputPorts.zipWithIndex.map { case (portDesc: PortDescription, idx) => InputPort( PortIdentity(idx), displayName = portDesc.displayName, disallowMultiLinks = portDesc.disallowMultiInputs, dependencies = portDesc.dependencies.map(idx => PortIdentity(idx)) ) } } else { List(InputPort()) } val outputPortInfo = if (outputPorts != null) { outputPorts.zipWithIndex.map { case (portDesc, idx) => OutputPort(PortIdentity(idx), displayName = portDesc.displayName) } } else { List(OutputPort()) } OperatorInfo( "Java UDF", "User-defined function operator in Java script", OperatorGroupConstants.JAVA_GROUP, inputPortInfo, outputPortInfo, dynamicInputPorts = true, dynamicOutputPorts = true, supportReconfiguration = true, allowPortCustomization = true ) } override def runtimeReconfiguration( workflowId: WorkflowIdentity, executionId: ExecutionIdentity, oldLogicalOp: LogicalOp, newLogicalOp: LogicalOp ): Try[(PhysicalOp, Option[StateTransferFunc])] = { Success(newLogicalOp.getPhysicalOp(workflowId, executionId), None) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/DualInputPortsPythonUDFOpDescV2.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.udf.python import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.google.common.base.Preconditions import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.executor.OpExecWithCode import org.apache.texera.amber.core.tuple.{Attribute, Schema} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow._ import org.apache.texera.amber.operator.LogicalOp import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} class DualInputPortsPythonUDFOpDescV2 extends LogicalOp { @JsonProperty( required = true, defaultValue = "# Choose from the following templates:\n" + "# \n" + "# from pytexera import *\n" + "# \n" + "# class ProcessTupleOperator(UDFOperatorV2):\n" + "# \n" + "# @overrides\n" + "# def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n" + "# yield tuple_\n" + "# \n" + "# class ProcessBatchOperator(UDFBatchOperator):\n" + "# BATCH_SIZE = 10 # must be a positive integer\n" + "# \n" + "# @overrides\n" + "# def process_batch(self, batch: Batch, port: int) -> Iterator[Optional[BatchLike]]:\n" + "# yield batch\n" + "# \n" + "# class ProcessTableOperator(UDFTableOperator):\n" + "# \n" + "# @overrides\n" + "# def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n" + "# yield table\n" ) @JsonSchemaTitle("Python script") @JsonPropertyDescription("Input your code here") var code: String = "" @JsonProperty(required = true, defaultValue = "1") @JsonSchemaTitle("Worker count") @JsonPropertyDescription("Specify how many parallel workers to launch") var workers: Int = Int.box(1) @JsonProperty(required = true, defaultValue = "true") @JsonSchemaTitle("Retain input columns") @JsonPropertyDescription("Keep the original input columns?") var retainInputColumns: Boolean = Boolean.box(false) @JsonProperty @JsonSchemaTitle("Extra output column(s)") @JsonPropertyDescription( "Name of the newly added output columns that the UDF will produce, if any" ) var outputColumns: List[Attribute] = List() override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { Preconditions.checkArgument(workers >= 1, "Need at least 1 worker.", Array()) val physicalOp = if (workers > 1) { PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithCode(code, "python") ) .withParallelizable(true) .withSuggestedWorkerNum(workers) } else { PhysicalOp .manyToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithCode(code, "python") ) .withParallelizable(false) } physicalOp .withDerivePartition(_ => UnknownPartition()) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPropagateSchema( SchemaPropagationFunc(inputSchemas => { Preconditions.checkArgument(inputSchemas.size == 2) val inputSchema = inputSchemas(operatorInfo.inputPorts(1).id) var outputSchema = if (retainInputColumns) inputSchema else Schema() // For any pythonUDFType, add custom output columns (attributes). if (outputColumns != null) { if (retainInputColumns) { // Check if columns are duplicated for (column <- outputColumns) { if (inputSchema.containsAttribute(column.getName)) throw new RuntimeException(s"Column name ${column.getName} already exists!") } } // Add custom output columns outputSchema = outputSchema.add(outputColumns) } Map(operatorInfo.outputPorts.head.id -> outputSchema) }) ) } override def operatorInfo: OperatorInfo = OperatorInfo( "2-in Python UDF", "User-defined function operator in Python script", OperatorGroupConstants.PYTHON_GROUP, inputPorts = List( InputPort(PortIdentity(), displayName = "model"), InputPort( PortIdentity(1), displayName = "tuples", dependencies = List(PortIdentity(0)) ) ), outputPorts = List(OutputPort()) ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/LambdaAttributeUnit.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.udf.python; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaBool; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaString; import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle; import org.apache.texera.amber.core.tuple.AttributeType; import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeNameLambda; import org.apache.texera.amber.operator.metadata.annotations.HideAnnotation; import java.util.Objects; public class LambdaAttributeUnit { @JsonProperty(required = true) @JsonSchemaTitle("Attribute Name") @AutofillAttributeNameLambda public String attributeName; @JsonProperty @JsonSchemaTitle("New Attribute Name") @JsonSchemaInject( strings = { @JsonSchemaString(path = HideAnnotation.hideTarget, value = "attributeName"), @JsonSchemaString(path = HideAnnotation.hideType, value = HideAnnotation.Type.regex), @JsonSchemaString(path = HideAnnotation.hideExpectedValue, value = "(?!Add New Column).*") }, bools = @JsonSchemaBool(path = HideAnnotation.hideOnNull, value = true) ) public String newAttributeName; @JsonProperty(required = true) @JsonSchemaTitle("Attribute Type") public AttributeType attributeType; @JsonProperty(required = true) @JsonSchemaTitle("Expression") public String expression; @JsonCreator public LambdaAttributeUnit( @JsonProperty("attributeName") String attributeName, @JsonProperty("expression") String expression, @JsonProperty("newAttributeName") String newAttributeName, @JsonProperty("attributeType") AttributeType attributeType) { this.attributeName = attributeName; this.expression = expression; this.newAttributeName = newAttributeName; this.attributeType = attributeType; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; LambdaAttributeUnit that = (LambdaAttributeUnit) o; return Objects.equals(attributeName, that.attributeName) && Objects.equals(expression, that.expression) && Objects.equals(newAttributeName, that.newAttributeName) && Objects.equals(attributeType, that.attributeType); } @Override public int hashCode() { return Objects.hash(attributeName, expression, newAttributeName, attributeType); } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/PythonLambdaFunctionOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.udf.python import com.google.common.base.Preconditions import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeTypeUtils, Schema} import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity} import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} class PythonLambdaFunctionOpDesc extends PythonOperatorDescriptor { @JsonSchemaTitle("Add/Modify column(s)") var lambdaAttributeUnits: List[LambdaAttributeUnit] = List() override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { Preconditions.checkArgument(inputSchemas.size == 1) Preconditions.checkArgument(lambdaAttributeUnits.nonEmpty) val inputSchema = inputSchemas.values.head var outputSchema = inputSchema // Add new attributes for (unit <- lambdaAttributeUnits) { if (unit.attributeName.equalsIgnoreCase("Add New Column")) { if (outputSchema.containsAttribute(unit.newAttributeName)) { throw new RuntimeException( s"Column name ${unit.newAttributeName} already exists!" ) } if (unit.newAttributeName != null && unit.newAttributeName.nonEmpty) { outputSchema = outputSchema.add(unit.newAttributeName, unit.attributeType) } } } // Type casting for (unit <- lambdaAttributeUnits) { if (!unit.attributeName.equalsIgnoreCase("Add New Column")) { outputSchema = AttributeTypeUtils.SchemaCasting(outputSchema, unit.attributeName, unit.attributeType) } } Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo( "Python Lambda Function", "Modify or add a new column with more ease", OperatorGroupConstants.PYTHON_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort()), supportReconfiguration = true ) override def generatePythonCode(): String = { // build the python udf code var code: String = "from pytexera import *\n" + "class ProcessTupleOperator(UDFOperatorV2):\n" + " @overrides\n" + " def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n" if (lambdaAttributeUnits != null) { for (unit <- lambdaAttributeUnits) { val attrName = if (unit.attributeName.equalsIgnoreCase("Add New Column")) unit.newAttributeName else unit.attributeName if (unit.expression != null && unit.expression.nonEmpty) { code += s""" tuple_['$attrName'] = ${unit.expression}\n""" } else throw new RuntimeException( s"Column name ${attrName}'s expression shouldn't be null or empty!" ) } } code + " yield tuple_\n" } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/PythonTableReducerOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.udf.python import com.google.common.base.Preconditions import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.Schema import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity} import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} class PythonTableReducerOpDesc extends PythonOperatorDescriptor { @JsonSchemaTitle("Output columns") var lambdaAttributeUnits: List[LambdaAttributeUnit] = List() override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { Preconditions.checkArgument(lambdaAttributeUnits.nonEmpty) val outputSchema = lambdaAttributeUnits.foldLeft(Schema()) { (schema, unit) => schema.add(unit.attributeName, unit.attributeType) } Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo( "Python Table Reducer", "Reduce Table to Tuple", OperatorGroupConstants.PYTHON_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort()) ) override def generatePythonCode(): String = { val outputTable = lambdaAttributeUnits .map(unit => s"""\"${unit.attributeName}\": ${unit.expression}""") .mkString("{", ", ", "}") s""" |from pytexera import * |class ProcessTableOperator(UDFTableOperator): | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | yield $outputTable |""".stripMargin } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/PythonUDFOpDescV2.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.udf.python import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.google.common.base.Preconditions import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.executor.OpExecWithCode import org.apache.texera.amber.core.tuple.{Attribute, Schema} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow._ import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.operator.{LogicalOp, PortDescription, StateTransferFunc} import scala.util.{Success, Try} class PythonUDFOpDescV2 extends LogicalOp { @JsonProperty( required = true, defaultValue = "# Choose from the following templates:\n" + "# \n" + "# from pytexera import *\n" + "# \n" + "# class ProcessTupleOperator(UDFOperatorV2):\n" + "# \n" + "# @overrides\n" + "# def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]:\n" + "# yield tuple_\n" + "# \n" + "# class ProcessBatchOperator(UDFBatchOperator):\n" + "# BATCH_SIZE = 10 # must be a positive integer\n" + "# \n" + "# @overrides\n" + "# def process_batch(self, batch: Batch, port: int) -> Iterator[Optional[BatchLike]]:\n" + "# yield batch\n" + "# \n" + "# class ProcessTableOperator(UDFTableOperator):\n" + "# \n" + "# @overrides\n" + "# def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:\n" + "# yield table\n" ) @JsonSchemaTitle("Python script") @JsonPropertyDescription("Input your code here") var code: String = "" @JsonProperty(required = true, defaultValue = "1") @JsonSchemaTitle("Worker count") @JsonPropertyDescription("Specify how many parallel workers to launch") var workers: Int = Int.box(1) @JsonProperty(required = true, defaultValue = "true") @JsonSchemaTitle("Retain input columns") @JsonPropertyDescription("Keep the original input columns?") var retainInputColumns: Boolean = Boolean.box(false) @JsonProperty @JsonSchemaTitle("Extra output column(s)") @JsonPropertyDescription( "Name of the newly added output columns that the UDF will produce, if any" ) var outputColumns: List[Attribute] = List() override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { Preconditions.checkArgument(workers >= 1, "Need at least 1 worker.", Array()) val opInfo = this.operatorInfo val partitionRequirement: List[Option[PartitionInfo]] = if (inputPorts != null) { inputPorts.map(p => Option(p.partitionRequirement)) } else { opInfo.inputPorts.map(_ => None) } val propagateSchema = (inputSchemas: Map[PortIdentity, Schema]) => { val inputSchema = inputSchemas(operatorInfo.inputPorts.head.id) var outputSchema = if (retainInputColumns) inputSchema else Schema() // Add custom output columns if defined if (outputColumns != null) { if (retainInputColumns) { // Check for duplicate column names for (column <- outputColumns) { if (inputSchema.containsAttribute(column.getName)) { throw new RuntimeException(s"Column name ${column.getName} already exists!") } } } // Add output columns to the schema outputSchema = outputSchema.add(outputColumns) } Map(operatorInfo.outputPorts.head.id -> outputSchema) } val physicalOp = if (workers > 1) { PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithCode(code, "python") ) .withParallelizable(true) .withSuggestedWorkerNum(workers) } else { PhysicalOp .manyToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithCode(code, "python") ) .withParallelizable(false) } physicalOp .withDerivePartition(_ => UnknownPartition()) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPartitionRequirement(partitionRequirement) .withIsOneToManyOp(true) .withPropagateSchema(SchemaPropagationFunc(propagateSchema)) } override def operatorInfo: OperatorInfo = { val inputPortInfo = if (inputPorts != null) { inputPorts.zipWithIndex.map { case (portDesc: PortDescription, idx) => InputPort( PortIdentity(idx), displayName = portDesc.displayName, disallowMultiLinks = portDesc.disallowMultiInputs, dependencies = portDesc.dependencies.map(idx => PortIdentity(idx)) ) } } else { List(InputPort()) } val outputPortInfo = if (outputPorts != null) { outputPorts.zipWithIndex.map { case (portDesc, idx) => OutputPort(PortIdentity(idx), displayName = portDesc.displayName) } } else { List(OutputPort()) } OperatorInfo( "Python UDF", "User-defined function operator in Python script", OperatorGroupConstants.PYTHON_GROUP, inputPortInfo, outputPortInfo, dynamicInputPorts = true, dynamicOutputPorts = true, supportReconfiguration = true, allowPortCustomization = true ) } override def runtimeReconfiguration( workflowId: WorkflowIdentity, executionId: ExecutionIdentity, oldLogicalOp: LogicalOp, newLogicalOp: LogicalOp ): Try[(PhysicalOp, Option[StateTransferFunc])] = { Success(newLogicalOp.getPhysicalOp(workflowId, executionId), None) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/source/PythonUDFSourceOpDescV2.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.udf.python.source import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.executor.OpExecWithCode import org.apache.texera.amber.core.tuple.{Attribute, Schema} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{OutputPort, PhysicalOp, SchemaPropagationFunc} import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.operator.source.SourceOperatorDescriptor class PythonUDFSourceOpDescV2 extends SourceOperatorDescriptor { @JsonProperty( required = true, defaultValue = "# from pytexera import *\n" + "# class GenerateOperator(UDFSourceOperator):\n" + "# \n" + "# @overrides\n" + "# \n" + "# def produce(self) -> Iterator[Union[TupleLike, TableLike, None]]:\n" + "# yield\n" ) @JsonSchemaTitle("Python script") @JsonPropertyDescription("Input your code here") var code: String = _ @JsonProperty(required = true, defaultValue = "1") @JsonSchemaTitle("Worker count") @JsonPropertyDescription("Specify how many parallel workers to launch") var workers: Int = 1 @JsonProperty() @JsonSchemaTitle("Columns") @JsonPropertyDescription("The columns of the source") var columns: List[Attribute] = List.empty override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { require(workers >= 1, "Need at least 1 worker.") val physicalOp = PhysicalOp .sourcePhysicalOp(workflowId, executionId, operatorIdentifier, OpExecWithCode(code, "python")) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withIsOneToManyOp(true) .withPropagateSchema( SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema())) ) .withLocationPreference(Option.empty) if (workers > 1) { physicalOp .withParallelizable(true) .withSuggestedWorkerNum(workers) } else { physicalOp.withParallelizable(false) } } override def operatorInfo: OperatorInfo = { OperatorInfo( "1-out Python UDF", "User-defined function operator in Python script", OperatorGroupConstants.PYTHON_GROUP, List.empty, // No input ports for a source operator List(OutputPort()), supportReconfiguration = true ) } override def sourceSchema(): Schema = { if (columns != null && columns.nonEmpty) { Schema().add(columns) } else { Schema() } } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/r/RUDFOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.udf.r import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.google.common.base.Preconditions import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.executor.OpExecWithCode import org.apache.texera.amber.core.tuple.{Attribute, Schema} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow._ import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.operator.{LogicalOp, PortDescription, StateTransferFunc} import scala.util.{Success, Try} class RUDFOpDesc extends LogicalOp { @JsonProperty( required = true, defaultValue = "# If using Table API:\n" + "# function(table, port) { \n" + "# return (table) \n" + "# }\n" + "\n" + "# If using Tuple API:\n" + "# library(coro)\n" + "# coro::generator(function(tuple, port) {\n" + "# yield (tuple)\n" + "# })" ) @JsonSchemaTitle("R UDF Script") @JsonPropertyDescription("Input your code here") var code: String = "" @JsonProperty(required = true, defaultValue = "1") @JsonSchemaTitle("Worker count") @JsonPropertyDescription("Specify how many parallel workers to launch") var workers: Int = Int.box(1) @JsonProperty(required = true, defaultValue = "false") @JsonSchemaTitle("Use Tuple API?") @JsonPropertyDescription("Check this box to use Tuple API, leave unchecked to use Table API") var useTupleAPI = false @JsonProperty(required = true, defaultValue = "true") @JsonSchemaTitle("Retain input columns") @JsonPropertyDescription("Keep the original input columns?") var retainInputColumns: Boolean = Boolean.box(false) @JsonProperty @JsonSchemaTitle("Extra output column(s)") @JsonPropertyDescription( "Name of the newly added output columns that the UDF will produce, if any" ) var outputColumns: List[Attribute] = List() override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { Preconditions.checkArgument(workers >= 1, "Need at least 1 worker.", Array()) val opInfo = this.operatorInfo val partitionRequirement: List[Option[PartitionInfo]] = if (inputPorts != null) { inputPorts.map(p => Option(p.partitionRequirement)) } else { opInfo.inputPorts.map(_ => None) } val propagateSchema = (inputSchemas: Map[PortIdentity, Schema]) => { val inputSchema = inputSchemas(operatorInfo.inputPorts.head.id) var outputSchema = if (retainInputColumns) inputSchema else Schema() // Add custom output columns if provided if (outputColumns != null) { if (retainInputColumns) { // Check for duplicate column names for (column <- outputColumns) { if (inputSchema.containsAttribute(column.getName)) { throw new RuntimeException(s"Column name ${column.getName} already exists!") } } } // Add output columns to the schema outputSchema = outputSchema.add(outputColumns) } Map(operatorInfo.outputPorts.head.id -> outputSchema) } val r_operator_type = if (useTupleAPI) "r-tuple" else "r-table" if (workers > 1) { PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithCode(code, r_operator_type) ) .withParallelizable(true) .withSuggestedWorkerNum(workers) } else { PhysicalOp .manyToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithCode(code, r_operator_type) ) .withParallelizable(false) }.withDerivePartition(_ => UnknownPartition()) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPartitionRequirement(partitionRequirement) .withIsOneToManyOp(true) .withPropagateSchema(SchemaPropagationFunc(propagateSchema)) } override def operatorInfo: OperatorInfo = { val inputPortInfo = if (inputPorts != null) { inputPorts.zipWithIndex.map { case (portDesc: PortDescription, idx) => InputPort( PortIdentity(idx), displayName = portDesc.displayName, disallowMultiLinks = portDesc.disallowMultiInputs, dependencies = portDesc.dependencies.map(idx => PortIdentity(idx)) ) } } else { List(InputPort()) } val outputPortInfo = if (outputPorts != null) { outputPorts.zipWithIndex.map { case (portDesc, idx) => OutputPort(PortIdentity(idx), displayName = portDesc.displayName) } } else { List(OutputPort()) } OperatorInfo( "R UDF", "User-defined function operator in R script", OperatorGroupConstants.R_GROUP, inputPortInfo, outputPortInfo, dynamicInputPorts = true, allowPortCustomization = true ) } override def runtimeReconfiguration( workflowId: WorkflowIdentity, executionId: ExecutionIdentity, oldLogicalOp: LogicalOp, newLogicalOp: LogicalOp ): Try[(PhysicalOp, Option[StateTransferFunc])] = { Success(newLogicalOp.getPhysicalOp(workflowId, executionId), None) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/r/RUDFSourceOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.udf.r import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.executor.OpExecWithCode import org.apache.texera.amber.core.tuple.{Attribute, Schema} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{OutputPort, PhysicalOp, SchemaPropagationFunc} import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.operator.source.SourceOperatorDescriptor class RUDFSourceOpDesc extends SourceOperatorDescriptor { @JsonProperty( required = true, defaultValue = "# If using Table API:\n" + "# function() { \n" + "# return (data.frame(Column_Here = \"Value_Here\")) \n" + "# }\n" + "\n" + "# If using Tuple API:\n" + "# library(coro)\n" + "# coro::generator(function() {\n" + "# yield (list(text= \"hello world!\"))\n" + "# })" ) @JsonSchemaTitle("R Source UDF Script") @JsonPropertyDescription("Input your code here") var code: String = _ @JsonProperty(required = true, defaultValue = "1") @JsonSchemaTitle("Worker count") @JsonPropertyDescription("Specify how many parallel workers to launch") var workers: Int = 1 @JsonProperty(required = true, defaultValue = "false") @JsonSchemaTitle("Use Tuple API?") @JsonPropertyDescription("Check this box to use Tuple API, leave unchecked to use Table API") var useTupleAPI: Boolean = false @JsonProperty() @JsonSchemaTitle("Columns") @JsonPropertyDescription("The columns of the source") var columns: List[Attribute] = List.empty override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { val rOperatorType = if (useTupleAPI) "r-tuple" else "r-table" require(workers >= 1, "Need at least 1 worker.") val physicalOp = PhysicalOp .sourcePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithCode(code, rOperatorType) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withIsOneToManyOp(true) .withPropagateSchema( SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> sourceSchema())) ) .withLocationPreference(None) if (workers > 1) { physicalOp .withParallelizable(true) .withSuggestedWorkerNum(workers) } else { physicalOp.withParallelizable(false) } } override def operatorInfo: OperatorInfo = { OperatorInfo( "1-out R UDF", "User-defined function operator in R script", OperatorGroupConstants.R_GROUP, List.empty, // No input ports for a source operator List(OutputPort()) ) } override def sourceSchema(): Schema = { if (columns != null && columns.nonEmpty) { Schema().add(columns) } else { Schema() } } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/union/UnionOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.union import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PhysicalOp} import org.apache.texera.amber.operator.LogicalOp import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} class UnionOpDesc extends LogicalOp { override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName("org.apache.texera.amber.operator.union.UnionOpExec") ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) } override def operatorInfo: OperatorInfo = OperatorInfo( "Union", "Unions the output rows from multiple input operators", OperatorGroupConstants.SET_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort()) ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/union/UnionOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.union import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} class UnionOpExec extends OperatorExecutor { override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = { Iterator(tuple) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/unneststring/UnnestStringOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.unneststring import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.tuple.AttributeType import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{ InputPort, OutputPort, PhysicalOp, SchemaPropagationFunc } import org.apache.texera.amber.operator.flatmap.FlatMapOpDesc import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.util.JSONUtils.objectMapper class UnnestStringOpDesc extends FlatMapOpDesc { @JsonProperty(value = "Delimiter", required = true, defaultValue = ",") @JsonPropertyDescription("string that separates the data") var delimiter: String = _ @JsonProperty(value = "Attribute", required = true) @JsonPropertyDescription("column of the string to unnest") @AutofillAttributeName var attribute: String = _ @JsonProperty(value = "Result attribute", required = true, defaultValue = "unnestResult") @JsonPropertyDescription("column name of the unnest result") var resultAttribute: String = _ override def operatorInfo: OperatorInfo = OperatorInfo( userFriendlyName = "Unnest String", operatorDescription = "Unnest the string values in the column separated by a delimiter to multiple values", operatorGroupName = OperatorGroupConstants.UTILITY_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort()) ) override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.unneststring.UnnestStringOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPropagateSchema( SchemaPropagationFunc(inputSchemas => { val outputSchema = Option(resultAttribute) .filter(_.trim.nonEmpty) .map(attr => inputSchemas.values.head.add(attr, AttributeType.STRING)) .getOrElse(throw new RuntimeException("Result attribute cannot be empty")) Map(operatorInfo.outputPorts.head.id -> outputSchema) }) ) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/unneststring/UnnestStringOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.unneststring import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} import org.apache.texera.amber.operator.flatmap.FlatMapOpExec import org.apache.texera.amber.util.JSONUtils.objectMapper class UnnestStringOpExec(descString: String) extends FlatMapOpExec { private val desc: UnnestStringOpDesc = objectMapper.readValue(descString, classOf[UnnestStringOpDesc]) setFlatMapFunc(splitByDelimiter) private def splitByDelimiter(tuple: Tuple): Iterator[TupleLike] = { desc.delimiter.r .split(tuple.getField(desc.attribute).toString) .filter(_.nonEmpty) .iterator .map(split => TupleLike(tuple.getFields ++ Seq(split))) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/util/OperatorDescriptorUtils.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.util import scala.collection.mutable import scala.jdk.CollectionConverters._ object OperatorDescriptorUtils { /** * Tries to equally partition a integer goal into n total number of workers. * In the case that the goal is not a multiple of worker count, * this function tries to spread out the remainder evenly to the workers. * * @param goal total goal to reach for all workers * @param totalNumWorkers total number of workers * @return a list which size is equal to totalNumWorkers, each number is the goal assigned for that worker index */ def equallyPartitionGoal(goal: Int, totalNumWorkers: Int): List[Int] = { val goalPerWorker = mutable.ArrayBuffer.fill(totalNumWorkers)(goal / totalNumWorkers) // integer division // divide up the remainder, give 1 to the first n workers for (worker <- 0 until goal % totalNumWorkers) { goalPerWorker(worker) = goalPerWorker(worker) + 1 } goalPerWorker.toList } def toImmutableMap[K, V]( javaMap: java.util.Map[K, V] ): scala.collection.immutable.Map[K, V] = { javaMap.asScala.toMap } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/DotPlot/DotPlotOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.DotPlot import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder import javax.validation.constraints.NotNull class DotPlotOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "Count Attribute", required = true) @JsonSchemaTitle("Count Attribute") @JsonPropertyDescription("the attribute for the counting of the dot plot") @AutofillAttributeName @NotNull(message = "Count Attribute column cannot be empty") var countAttribute: EncodableString = "" override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Dot Plot", "Visualize data using a dot plot", OperatorGroupConstants.VISUALIZATION_BASIC_GROUP ) def createPlotlyFigure(): PythonTemplateBuilder = { pyb""" | table = table.groupby([$countAttribute])[$countAttribute].count().reset_index(name='counts') | fig = px.strip(table, x='counts', y=$countAttribute, orientation='h', color=$countAttribute, | color_discrete_sequence=px.colors.qualitative.Dark2) | | fig.update_traces(marker=dict(size=12, line=dict(width=2, color='DarkSlateGrey'))) | | fig.update_layout(margin=dict(t=0, b=0, l=0, r=0)) |""" } override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * | |import plotly.express as px |import plotly.graph_objects as go |import plotly.io | |class ProcessTableOperator(UDFTableOperator): | | def render_error(self, error_msg): | return '''

DotPlot is not available.

|

Reasons are: {}

| '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("Input table is empty.")} | return | ${createPlotlyFigure()} | if table.empty: | yield {'html-content': self.render_error("No valid rows left (every row has at least 1 missing value).")} | return | # convert fig to html content | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/IcicleChart/IcicleChartOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.IcicleChart import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.operator.visualization.hierarchychart.HierarchySection import org.apache.texera.amber.pybuilder.PythonTemplateBuilder import javax.validation.constraints.{NotEmpty, NotNull} // type constraint: value can only be numeric @JsonSchemaInject(json = """ { "attributeTypeRules": { "value": { "enum": ["integer", "long", "double"] } } } """) class IcicleChartOpDesc extends PythonOperatorDescriptor { @JsonProperty(required = true) @JsonSchemaTitle("Hierarchy Path") @JsonPropertyDescription( "hierarchy of attributes from a root (higher-level category) to leaves (lower-level category)" ) @NotEmpty(message = "Hierarchy path list cannot be empty") var hierarchy: List[HierarchySection] = List() @JsonProperty(value = "value", required = true) @JsonSchemaTitle("Value Column") @JsonPropertyDescription("the value associated with the size of each sector in the chart") @AutofillAttributeName @NotNull(message = "Value column cannot be empty") var value: EncodableString = "" override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Icicle Chart", "Visualize hierarchical data from root to leaves", OperatorGroupConstants.VISUALIZATION_BASIC_GROUP ) private def getIcicleAttributesInPython: String = hierarchy.map(c => pyb"${c.attributeName}").mkString(",") def manipulateTable(): PythonTemplateBuilder = { val attributes = getIcicleAttributesInPython pyb""" | table[$value] = table[table[$value] > 0][$value] # remove non-positive numbers from the data | table.dropna(subset = [$attributes], inplace = True) #remove missing values |""" } def createPlotlyFigure(): PythonTemplateBuilder = { assert(hierarchy.nonEmpty) val attributes = getIcicleAttributesInPython pyb""" | fig = px.icicle(table, path=[$attributes], values=$value, | color=$value, hover_data=[$attributes], | color_continuous_scale='RdBu') |""" } override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * | |import plotly.express as px |import plotly.graph_objects as go |import plotly.io |import numpy as np | |class ProcessTableOperator(UDFTableOperator): | | # Generate custom error message as html string | def render_error(self, error_msg) -> str: | return '''

Icicle chart is not available.

|

Reason is: {}

| '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return | ${manipulateTable()} | if table.empty: | yield {'html-content': self.render_error("value column contains only non-positive numbers or nulls.")} | return | ${createPlotlyFigure()} | # convert fig to html content | fig.update_layout(margin=dict(l=0, r=0, b=0, t=0)) | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ImageUtility.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization object ImageUtility { def encodeImageToHTML(): String = { s""" | import base64 | try: | encoded_image_data = base64.b64encode(binary_image_data) | encoded_image_str = encoded_image_data.decode("utf-8") | except Exception as e: | yield {'html-content': self.render_error("Binary input is not valid")} | return | html = f'Image' |""".stripMargin } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ImageViz/ImageVisualizerOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.ImageViz import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder class ImageVisualizerOpDesc extends PythonOperatorDescriptor { @JsonProperty(required = true) @JsonSchemaTitle("image content column") @JsonPropertyDescription("The Binary data of the Image") @AutofillAttributeName var binaryContent: EncodableString = _ override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Image Visualizer", "visualize image content", OperatorGroupConstants.VISUALIZATION_MEDIA_GROUP ) def createBinaryData(): PythonTemplateBuilder = { assert(binaryContent.nonEmpty) pyb""" | binary_image_data = tuple_[$binaryContent] |""" } override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * |import base64 |from io import BytesIO | |class ProcessTupleOperator(UDFOperatorV2): | images_html = [] | | def render_error(self, error_msg): | return f'

Image is not available.

Reason: {error_msg}

' | | def encode_image_to_html(self, binary_image_data): | try: | encoded_image_data = base64.b64encode(binary_image_data) | encoded_image_str = encoded_image_data.decode("utf-8") | html = f'Image' | return html | except Exception as e: | return self.render_error("Binary input is not valid") | | @overrides | def process_tuple(self, tuple_: Tuple, port: int) -> Iterator[Optional[TupleLike]]: | ${createBinaryData()} | self.images_html.append(self.encode_image_to_html(binary_image_data)) | yield | | @overrides | def on_finish(self, port: int) -> Iterator[Optional[TupleLike]]: | all_images_html = "
" + "".join(self.images_html) + "
" | yield {"html-content": all_images_html} |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ScatterMatrixChart/ScatterMatrixChartOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.ScatterMatrixChart import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.{ AutofillAttributeName, AutofillAttributeNameList } import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder @JsonSchemaInject(json = """ { "attributeTypeRules": { "value": { "enum": ["integer", "long", "double"] } } } """) class ScatterMatrixChartOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "Selected Attributes", required = true) @JsonSchemaTitle("Selected Attributes") @JsonPropertyDescription("The axes of each scatter plot in the matrix.") @AutofillAttributeNameList var selectedAttributes: List[EncodableString] = _ @JsonProperty(value = "Color", required = true) @JsonSchemaTitle("Color Column") @JsonPropertyDescription("Column to color points") @AutofillAttributeName var color: EncodableString = "" override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Scatter Matrix Chart", "Visualize datasets in a Scatter Matrix", OperatorGroupConstants.VISUALIZATION_STATISTICAL_GROUP ) def createPlotlyFigure(): PythonTemplateBuilder = { assert(selectedAttributes.nonEmpty) val list_Attributes = selectedAttributes.map(attribute => pyb"""$attribute""").mkString(",") pyb""" | fig = px.scatter_matrix(table, dimensions=[$list_Attributes], color=$color) | fig.update_layout(margin=dict(t=0, b=0, l=0, r=0)) |""" } override def generatePythonCode(): String = { val finalcode = pyb""" |from pytexera import * | |import plotly.express as px |import plotly.graph_objects as go |import plotly.io |import numpy as np | |class ProcessTableOperator(UDFTableOperator): | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | ${createPlotlyFigure()} | # convert fig to html content | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} | |""" finalcode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/barChart/BarChartOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.barChart import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder import javax.validation.constraints.NotNull //type constraint: value can only be numeric @JsonSchemaInject(json = """ { "attributeTypeRules": { "value": { "enum": ["integer", "long", "double"] } } } """) class BarChartOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "value", required = true) @JsonSchemaTitle("Value Column") @JsonPropertyDescription("The value associated with each category") @AutofillAttributeName @NotNull(message = "Value column cannot be empty") var value: EncodableString = "" @JsonProperty(required = true) @JsonSchemaTitle("Fields") @JsonPropertyDescription("Visualize categorical data in a Bar Chart") @AutofillAttributeName @NotNull(message = "Fields cannot be empty") var fields: EncodableString = "" @JsonProperty(defaultValue = "No Selection", required = false) @JsonSchemaTitle("Category Column") @JsonPropertyDescription("Optional - Select a column to Color Code the Categories") @AutofillAttributeName var categoryColumn: EncodableString = "" @JsonProperty(defaultValue = "false") @JsonSchemaTitle("Horizontal Orientation") @JsonPropertyDescription("Orientation Style") var horizontalOrientation: Boolean = _ @JsonProperty(required = false) @JsonSchemaTitle("Pattern") @JsonPropertyDescription("Add texture to the chart based on an attribute") @AutofillAttributeName var pattern: EncodableString = "" override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Bar Chart", "Visualize data in a Bar Chart", OperatorGroupConstants.VISUALIZATION_BASIC_GROUP ) def manipulateTable(): PythonTemplateBuilder = { assert(value.nonEmpty, "Value column cannot be empty") assert(fields.nonEmpty, "Fields cannot be empty") pyb""" | table = table.dropna(subset = [$value, $fields]) #remove missing values |""" } override def generatePythonCode(): String = { var isHorizontalOrientation = "False" if (horizontalOrientation) isHorizontalOrientation = "True" var isPatternSelected = "False" if (pattern != "") isPatternSelected = "True" var isCategoryColumn = "False" if (categoryColumn != "No Selection") isCategoryColumn = "True" val finalCode = pyb""" |from pytexera import * | |import plotly.express as px |import pandas as pd |import plotly.graph_objects as go |import plotly.io |import json |import pickle |import plotly | |class ProcessTableOperator(UDFTableOperator): | | # Generate custom error message as html string | def render_error(self, error_msg) -> str: | return '''

Bar chart is not available.

|

Reason is: {}

| '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | ${manipulateTable()} | if not table.empty and $fields != $value: | if $isHorizontalOrientation: | fig = go.Figure(px.bar(table, y=$fields, x=$value, color=$categoryColumn if $isCategoryColumn else None, pattern_shape=$pattern if $isPatternSelected else None, orientation = 'h')) | else: | fig = go.Figure(px.bar(table, y=$value, x=$fields, color=$categoryColumn if $isCategoryColumn else None, pattern_shape=$pattern if $isPatternSelected else None)) | fig.update_layout(margin=dict(l=0, r=0, t=0, b=0)) | html = plotly.io.to_html(fig, include_plotlyjs = 'cdn', auto_play = False) | # use latest plotly lib in html | #html = html.replace('https://cdn.plot.ly/plotly-2.3.1.min.js', 'https://cdn.plot.ly/plotly-2.18.2.min.js') | elif $fields == $value: | html = self.render_error('Fields should not have the same value.') | elif table.empty: | html = self.render_error('Table should not have any empty/null values or fields.') | yield {'html-content':html} | """ finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/boxViolinPlot/BoxViolinPlotOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.boxViolinPlot import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription, JsonPropertyOrder} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder @JsonPropertyOrder(Array("value", "quartileType", "horizontalOrientation", "violinPlot")) @JsonSchemaInject(json = """ { "attributeTypeRules": { "value": { "enum": ["integer", "long", "double"] } } } """) class BoxViolinPlotOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "value", required = true) @JsonSchemaTitle("Value Column") @JsonPropertyDescription("Data column for box plot") @AutofillAttributeName var value: EncodableString = "" @JsonProperty( value = "Quartile Method", required = true, defaultValue = "linear" ) var quartileType: BoxViolinPlotQuartileFunction = _ @JsonProperty(defaultValue = "false") @JsonSchemaTitle("Horizontal Orientation") @JsonPropertyDescription("Orientation style") var horizontalOrientation: Boolean = _ @JsonProperty(defaultValue = "false") @JsonSchemaTitle("Violin Plot") @JsonPropertyDescription( "Check this box to overlay a violin plot on the box plot; otherwise, show only the box plot" ) var violinPlot: Boolean = _ override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Box/Violin Plot", "Visualize data using either a Box Plot or a Violin Plot. Box plots are drawn as a box with a vertical line down the middle which is mean value, and has horizontal lines attached to each side (known as “whiskers”). Violin plots provide more detail by showing a smoothed density curve on each side, and also include a box plot inside for comparison.", OperatorGroupConstants.VISUALIZATION_STATISTICAL_GROUP ) def manipulateTable(): PythonTemplateBuilder = { assert(value.nonEmpty) pyb""" | table = table.dropna(subset = [$value]) #remove missing values | |""" } def createPlotlyFigure(): PythonTemplateBuilder = { val horizontal = if (horizontalOrientation) "True" else "False" val violin = if (violinPlot) "True" else "False" pyb""" | if($violin): | if ($horizontal): | fig = px.violin(table, x=$value, box=True, points='all') | else: | fig = px.violin(table, y=$value, box=True, points='all') | else: | if($horizontal): | fig = px.box(table, x=$value,boxmode="overlay", points='all') | else: | fig = px.box(table, y=$value,boxmode="overlay", points='all') | fig.update_traces(quartilemethod="${quartileType.getQuartiletype}", col=1) | fig.update_layout(margin=dict(t=0, b=0, l=0, r=0)) |""" } override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * | |import plotly.express as px |import pandas as pd |import plotly.graph_objects as go |import plotly.io |import json |import pickle |import plotly | |class ProcessTableOperator(UDFTableOperator): | | # Generate custom error message as html string | def render_error(self, error_msg) -> str: | return '''

Box/Violin Plot is not available.

|

Reason is: {}

| '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return | ${manipulateTable()} | if table.empty: | yield {'html-content': self.render_error("value column contains only non-positive numbers or nulls.")} | return | ${createPlotlyFigure()} | # convert fig to html content | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} | """ finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/boxViolinPlot/BoxViolinPlotQuartileFunction.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.boxViolinPlot; import com.fasterxml.jackson.annotation.JsonValue; public enum BoxViolinPlotQuartileFunction { LINEAR("linear"), INCLUSIVE("inclusive"), EXCLUSIVE("exclusive"); private final String quartiletype; BoxViolinPlotQuartileFunction(String quartiletype) { this.quartiletype = quartiletype; } @JsonValue public String getQuartiletype() { return this.quartiletype; } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/bubbleChart/BubbleChartOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.bubbleChart import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder import javax.validation.constraints.NotNull /** * Visualization Operator to visualize results as a Bubble Chart * User specifies 2 columns to use for the x, y labels. Size of bubbles determined via * third column of data. Bubbles can be sorted via color using a fourth column. */ // type can be numerical only class BubbleChartOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "xValue", required = true) @JsonSchemaTitle("X-Column") @JsonPropertyDescription("Data column for the x-axis") @AutofillAttributeName @NotNull(message = "xValue column cannot be empty") var xValue: EncodableString = "" @JsonProperty(value = "yValue", required = true) @JsonSchemaTitle("Y-Column") @JsonPropertyDescription("Data column for the y-axis") @AutofillAttributeName @NotNull(message = "yValue column cannot be empty") var yValue: EncodableString = "" @JsonProperty(value = "zValue", required = true) @JsonSchemaTitle("Z-Column") @JsonPropertyDescription("Data column to determine bubble size") @AutofillAttributeName @NotNull(message = "zValue column cannot be empty") var zValue: EncodableString = "" @JsonProperty(value = "enableColor", defaultValue = "false") @JsonSchemaTitle("Enable Color") @JsonPropertyDescription("Colors bubbles using a data column") var enableColor: Boolean = false @JsonProperty(value = "colorCategory", required = true) @JsonSchemaTitle("Color-Column") @JsonPropertyDescription("Picks data column to color bubbles with if color is enabled") @AutofillAttributeName @NotNull(message = "colorCategory column cannot be empty") var colorCategory: EncodableString = "" override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Bubble Chart", "a 3D Scatter Plot; Bubbles are graphed using x and y labels, and their sizes determined by a z-value.", OperatorGroupConstants.VISUALIZATION_BASIC_GROUP ) def manipulateTable(): PythonTemplateBuilder = { assert(xValue.nonEmpty && yValue.nonEmpty && zValue.nonEmpty) pyb""" | # drops rows with missing values pertaining to relevant columns | table.dropna(subset=[$xValue, $yValue, $zValue], inplace = True) | |""" } def createPlotlyFigure(): PythonTemplateBuilder = { assert(xValue.nonEmpty && yValue.nonEmpty && zValue.nonEmpty) pyb""" | if '$enableColor' == 'true': | fig = go.Figure(px.scatter(table, x=$xValue, y=$yValue, size=$zValue, size_max=100, color=$colorCategory)) | else: | fig = go.Figure(px.scatter(table, x=$xValue, y=$yValue, size=$zValue, size_max=100)) |""" } override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * | |import plotly.express as px |import plotly.graph_objects as go |import plotly.io |import numpy as np | | |class ProcessTableOperator(UDFTableOperator): | | def render_error(self, error_msg): | return '''

TreeMap is not available.

|

Reasons are: {}

| '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("Input table is empty.")} | return | ${manipulateTable()} | ${createPlotlyFigure()} | if table.empty: | yield {'html-content': self.render_error("No valid rows left (every row has at least 1 missing value).")} | return | fig.update_layout(margin=dict(l=0, r=0, b=0, t=0)) | html = plotly.io.to_html(fig, include_plotlyjs = 'cdn', auto_play = False) | yield {'html-content':html} |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/bulletChart/BulletChartOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.bulletChart import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import java.util import java.util.{List => JList} import scala.jdk.CollectionConverters._ /** * Visualization Operator to visualize results as a Bullet Chart */ class BulletChartOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "value", required = true) @JsonSchemaTitle("Value") @JsonPropertyDescription("The actual value to display on the bullet chart") @AutofillAttributeName var value: EncodableString = "" @JsonProperty(value = "deltaReference", required = true) @JsonSchemaTitle("Delta Reference") @JsonPropertyDescription("The reference value for the delta indicator. e.g., 100") var deltaReference: EncodableString = "" @JsonProperty(value = "thresholdValue", required = false) @JsonSchemaTitle("Threshold Value") @JsonPropertyDescription("The performance threshold value. e.g., 100") var thresholdValue: EncodableString = "" @JsonProperty(value = "steps", required = false) @JsonSchemaTitle("Steps") @JsonPropertyDescription("Optional: Each step includes a start and end value e.g., 0, 100.") var steps: JList[BulletChartStepDefinition] = new util.ArrayList[BulletChartStepDefinition]() override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema().add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Bullet Chart", """Visualize data using a Bullet Chart that shows a primary quantitative bar and delta indicator. |Optional elements such as qualitative ranges (steps) and a performance threshold are displayed only when provided.""".stripMargin, OperatorGroupConstants.VISUALIZATION_FINANCIAL_GROUP ) override def generatePythonCode(): String = { // Convert the Scala list of steps into a list of dictionaries val stepsStr = if (steps != null && !steps.isEmpty) { val stepsSeq = steps.asScala.map(step => pyb"""{"start": ${step.start}, "end": ${step.end}}""") "[" + stepsSeq.mkString(", ") + "]" } else { "[]" } val finalCode = pyb""" |from pytexera import * |import plotly.graph_objects as go |import plotly.io as pio |import json | |class ProcessTableOperator(UDFTableOperator): | | # Render an error message in HTML format | def render_error(self, error_msg) -> str: | return '''

Bullet chart is not available.

|

Reason: {}

'''.format(error_msg) | | # Generate a list of grayscale HSL colors with decreasing brightness | def generate_gray_gradient(self, step_count): | colors = [] | for i in range(step_count): | lightness = 90 - (i * (60 / max(1, step_count - 1))) | colors.append(f"hsl(0, 0%, {lightness}%)") | return colors | | # Validate and convert user-provided step definitions | def generate_valid_steps(self, steps_data): | valid_steps = [] | self.step_errors = [] | | for index, step in enumerate(steps_data): | start = step.get('start', '') | end = step.get('end', '') | if start and end: | try: | s_val = float(start) | e_val = float(end) | if s_val < e_val: | valid_steps.append({"start": s_val, "end": e_val}) | else: | self.step_errors.append(f"Step {index + 1}: start ≥ end ({s_val} ≥ {e_val})") | except Exception as e: | self.step_errors.append(f"Step {index + 1}: Invalid step values: start='{start}', end='{end}'") | return valid_steps | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("Input table is empty.")} | return | | try: | value_col = $value | delta_ref = float($deltaReference) if $deltaReference.strip() else 0 | | if value_col not in table.columns: | yield {'html-content': self.render_error(f"Column '{value_col}' not found in input table.")} | return | | table = table.dropna(subset=[value_col]) | if table.empty: | yield {'html-content': self.render_error("No valid data rows found after dropping nulls.")} | return | | try: | threshold_val = float($thresholdValue) if $thresholdValue.strip() else None | except ValueError: | threshold_val = None | | # Parse and validate steps input | try: | steps_data = $stepsStr | valid_steps = self.generate_valid_steps(steps_data) | step_colors = self.generate_gray_gradient(len(valid_steps)) | steps_list = [] | for index, step_data in enumerate(valid_steps): | color = step_colors[index] | steps_list.append({ | "range": [step_data["start"], step_data["end"]], | "color": color | }) | except Exception: | steps_list = [] | | # Iterate through up to 10 rows of the input table | count = 0 | html_chunks = [] | for _, row in table.iterrows(): | if count >= 10: # Limit to 10 charts | break | try: | actual = float(row[value_col]) | ref = delta_ref | | # Construct gauge configuration | gauge_config = {'shape': 'bullet'} | if steps_list: | gauge_config['steps'] = steps_list | | max_range_values = [actual, ref] | if threshold_val is not None: | max_range_values.append(threshold_val) | | if steps_list: | for r in steps_list: | max_range_values.append(r["range"][1]) | | gauge_config['axis'] = {"range": [0, max(max_range_values) * 1.2]} | | if threshold_val is not None: | gauge_config["threshold"] = { | "value": threshold_val, | "line": {"color": "red", "width": 2}, | "thickness": 1 | } | | fig = go.Figure(go.Indicator( | mode="number+gauge+delta", | value=actual, | delta={"reference": ref}, | gauge=gauge_config, | domain={"x": [0.1, 1], "y": [0.1, 0.9]}, | title={"text": value_col} | )) | | fig.update_layout(margin=dict(l=80, r=20, b=40, t=40), height=150) | html_chunk = pio.to_html(fig, include_plotlyjs='cdn', auto_play=False) | | # Add step error | if hasattr(self, 'step_errors') and self.step_errors: | error_notes = "
Step Errors:
    " + "".join([f"
  • {msg}
  • " for msg in self.step_errors]) + "
" | html_chunk += error_notes | | html_chunks.append(html_chunk) | count += 1 | | except Exception as e: | html_chunks.append(self.render_error(f"Error generating bullet chart: {str(e)}")) | | # Combine HTML chunks into final output | final_html = "
" + "".join(html_chunks) + "
" | yield {"html-content": final_html} | except Exception as e: | yield {'html-content': self.render_error(f"General error: {str(e)}")} |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/bulletChart/BulletChartStepDefinition.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.bulletChart import com.fasterxml.jackson.annotation.{JsonCreator, JsonProperty} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString /** * Defines a step range used for qualitative segments in the Bullet Chart. */ class BulletChartStepDefinition @JsonCreator() ( @JsonProperty("start") @JsonSchemaTitle("Start") var start: EncodableString, @JsonProperty("end") @JsonSchemaTitle("End") var end: EncodableString ) ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/candlestickChart/CandlestickChartOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.candlestickChart import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} class CandlestickChartOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "date", required = true) @JsonSchemaTitle("Date Column") @JsonPropertyDescription("the date of the candlestick") @AutofillAttributeName var date: EncodableString = "" @JsonProperty(value = "open", required = true) @JsonSchemaTitle("Opening Price Column") @JsonPropertyDescription("the opening price of the candlestick") @AutofillAttributeName var open: EncodableString = "" @JsonProperty(value = "high", required = true) @JsonSchemaTitle("Highest Price Column") @JsonPropertyDescription("the highest price of the candlestick") @AutofillAttributeName var high: EncodableString = "" @JsonProperty(value = "low", required = true) @JsonSchemaTitle("Lowest Price Column") @JsonPropertyDescription("the lowest price of the candlestick") @AutofillAttributeName var low: EncodableString = "" @JsonProperty(value = "close", required = true) @JsonSchemaTitle("Closing Price Column") @JsonPropertyDescription("the closing price of the candlestick") @AutofillAttributeName var close: EncodableString = "" override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Candlestick Chart", "Visualize data in a Candlestick Chart", OperatorGroupConstants.VISUALIZATION_FINANCIAL_GROUP ) override def generatePythonCode(): String = { pyb""" |from pytexera import * | |import plotly.graph_objects as go |import pandas as pd | |class ProcessTableOperator(UDFTableOperator): | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | # Convert table to dictionary | table_dict = table.to_dict() | | # Create a DataFrame from the dictionary | df = pd.DataFrame(table_dict) | | fig = go.Figure(data=[go.Candlestick( | x=df[$date], | open=df[$open], | high=df[$high], | low=df[$low], | close=df[$close] | )]) | fig.update_layout(title='Candlestick Chart') | html = fig.to_html(include_plotlyjs='cdn', full_html=False) | yield {'html-content': html} |""".encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/carpetPlot/CarpetPlotOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.carpetPlot import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import javax.validation.constraints.NotNull class CarpetPlotOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "a", required = true) @NotNull(message = "A-axis Attribute cannot be empty") @JsonSchemaTitle("First Parameter Axis Column") @JsonPropertyDescription("Column representing the first parameter axis (a)") @AutofillAttributeName var a: EncodableString = "" @JsonProperty(value = "b", required = true) @NotNull(message = "B-axis Attribute cannot be empty") @JsonSchemaTitle("Second Parameter Axis Column") @JsonPropertyDescription("Column representing the second parameter axis (b)") @AutofillAttributeName var b: EncodableString = "" @JsonProperty(value = "y", required = true) @NotNull(message = "Y Value cannot be empty") @JsonSchemaTitle("Value Column") @JsonPropertyDescription("Column representing the value at each (a, b) coordinate") @AutofillAttributeName var y: EncodableString = "" override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Carpet Plot", "Visualize data in a Carpet Plot", OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP ) override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * |import plotly.graph_objects as go |import plotly.io as pio | |class ProcessTableOperator(UDFTableOperator): | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | | if table.empty: | yield {"html-content": "

Input table is empty

"} | return | | a_col = $a | b_col = $b | y_col = $y | | for col in [a_col, b_col, y_col]: | if col not in table.columns: | yield {"html-content": f"

Column '{col}' not found

"} | return | | table = table.dropna(subset=[a_col, b_col, y_col]) | | if table.empty: | yield {"html-content": "

No valid rows after removing nulls

"} | return | | try: | table[a_col] = table[a_col].astype(float) | table[b_col] = table[b_col].astype(float) | table[y_col] = table[y_col].astype(float) | except Exception as e: | yield {"html-content": f"

Error converting input columns to numeric values: {str(e)}

"} | return | | try: | fig = go.Figure(go.Carpet( | a=table[a_col], | b=table[b_col], | y=table[y_col] | )) | html = pio.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {"html-content": html} | except Exception as e: | yield {"html-content": f"

Error generating carpet plot: {str(e)}

"} |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/choroplethMap/ChoroplethMapOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.choroplethMap import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder @JsonSchemaInject(json = """ { "attributeTypeRules": { "locations": { "enum": ["string"] }, "color": { "enum": ["integer", "long", "double"] } } } """) class ChoroplethMapOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "locations", required = true) @JsonSchemaTitle("Locations Column") @JsonPropertyDescription( "Column used to describe location. Currently only supports countries and needs to be three-letter ISO country code" ) @AutofillAttributeName var locations: EncodableString = "" @JsonProperty(value = "color", required = true) @JsonSchemaTitle("Color Column") @JsonPropertyDescription( "Column used to determine intensity of color of the region" ) @AutofillAttributeName var color: EncodableString = "" override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Choropleth Map", "Visualize data using a Choropleth Map that uses shades of colors to show differences in properties or quantities between regions", OperatorGroupConstants.VISUALIZATION_ADVANCED_GROUP ) def manipulateTable(): PythonTemplateBuilder = { assert(locations.nonEmpty) assert(color.nonEmpty) pyb""" | table.dropna(subset=[$locations, $color], inplace = True) |""" } def createPlotlyFigure(): PythonTemplateBuilder = { assert(locations.nonEmpty && color.nonEmpty) pyb""" | fig = px.choropleth(table, locations=$locations, color=$color, color_continuous_scale=px.colors.sequential.Plasma) | fig.update_layout(margin={"r":0,"t":0,"l":0,"b":0}) |""" } override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * | |import plotly.express as px |import plotly.io |import plotly | |class ProcessTableOperator(UDFTableOperator): | | # Generate custom error message as html string | def render_error(self, error_msg) -> str: | return '''

Choropleth map is not available.

|

Reason is: {}

| '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("Input table is empty.")} | return | ${manipulateTable()} | if table.empty: | yield {'html-content': self.render_error("No valid rows left (every row has at least 1 missing value).")} | return | ${createPlotlyFigure()} | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/continuousErrorBands/BandConfig.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.continuousErrorBands import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.visualization.lineChart.LineConfig class BandConfig extends LineConfig { @JsonProperty(required = true) @JsonSchemaTitle("Y-Axis Upper Bound") @JsonPropertyDescription("Represents upper bound error of y-values") @AutofillAttributeName var yUpper: EncodableString = "" @JsonProperty(required = true) @JsonSchemaTitle("Y-Axis Lower Bound") @JsonPropertyDescription("Represents lower bound error of y-values") @AutofillAttributeName var yLower: EncodableString = "" @JsonProperty(required = false) @JsonSchemaTitle("Fill Color") @JsonPropertyDescription("must be a valid CSS color or hex color string") var fillColor: EncodableString = "" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/continuousErrorBands/ContinuousErrorBandsOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.continuousErrorBands import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder import java.util import scala.jdk.CollectionConverters.ListHasAsScala class ContinuousErrorBandsOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "xLabel", required = false, defaultValue = "X Axis") @JsonSchemaTitle("X Label") @JsonPropertyDescription("Label used for x axis") var xLabel: EncodableString = "" @JsonProperty(value = "yLabel", required = false, defaultValue = "Y Axis") @JsonSchemaTitle("Y Label") @JsonPropertyDescription("Label used for y axis") var yLabel: EncodableString = "" @JsonProperty(value = "bands", required = true) var bands: util.List[BandConfig] = _ override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Continuous Error Bands", "Visualize error or uncertainty along a continuous line", OperatorGroupConstants.VISUALIZATION_STATISTICAL_GROUP ) def createPlotlyFigure(): PythonTemplateBuilder = { val bandsPart = bands.asScala .map { bandConf => val colorPart = if (bandConf.color != "") { pyb"line={'color':${bandConf.color}}, marker={'color':${bandConf.color}}, " } else { "" } val fillColorPart = if (bandConf.fillColor != "") { pyb"fillcolor=${bandConf.fillColor}, " } else { "" } val namePart = if (bandConf.name != "") { pyb"name=${bandConf.name}" } else { pyb"name=${bandConf.yValue}" } pyb"""fig.add_trace(go.Scatter( x=table[${bandConf.xValue}], y=table[${bandConf.yUpper}], mode='lines', marker=dict(color="#444"), line=dict(width=0), showlegend=False, $namePart )) fig.add_trace(go.Scatter( x=table[${bandConf.xValue}], y=table[${bandConf.yLower}], mode='lines', marker=dict(color="#444"), line=dict(width=0), fill='tonexty', showlegend=False, $fillColorPart $namePart )) fig.add_trace(go.Scatter( x=table[${bandConf.xValue}], y=table[${bandConf.yValue}], mode='${bandConf.mode.getModeInPlotly}', $colorPart $namePart ))""" } pyb""" | fig = go.Figure() | ${bandsPart.mkString("\n ")} | fig.update_layout(margin=dict(t=0, b=0, l=0, r=0), | xaxis_title=$xLabel, | yaxis_title=$yLabel, | hovermode="x") |""" } override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * | |import plotly.express as px |import plotly.graph_objs as go |import plotly.io | |class ProcessTableOperator(UDFTableOperator): | | # Generate custom error message as html string | def render_error(self, error_msg) -> str: | return '''

Continuous Error Bands is not available.

|

Reason is: {}

| '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return | ${createPlotlyFigure()} | # convert fig to html content | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/contourPlot/ContourPlotColoringFunction.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.contourPlot; import com.fasterxml.jackson.annotation.JsonValue; public enum ContourPlotColoringFunction { HEATMAP("heatmap"), LINES("lines"), NONE("none"); private final String coloringMethod; ContourPlotColoringFunction(String coloringMethod) { this.coloringMethod = coloringMethod; } @JsonValue public String getColoringMethod() { return this.coloringMethod; } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/contourPlot/ContourPlotOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.contourPlot import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} class ContourPlotOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "x", required = true) @JsonSchemaTitle("x") @JsonPropertyDescription("The column name of X-axis") @AutofillAttributeName var x: EncodableString = "" @JsonProperty(value = "y", required = true) @JsonSchemaTitle("y") @JsonPropertyDescription("The column name of Y-axis") @AutofillAttributeName var y: EncodableString = "" @JsonProperty(value = "z", required = true) @JsonSchemaTitle("z") @JsonPropertyDescription("The column name of color bar") @AutofillAttributeName var z: EncodableString = "" @JsonProperty(required = false, defaultValue = "10") @JsonSchemaTitle("Grid Size") @JsonPropertyDescription("Grid resolution of the final image") var gridSize: EncodableString = "" @JsonProperty(required = false, defaultValue = "true") @JsonSchemaTitle("Connect Gaps") @JsonPropertyDescription("Automatically fill in the missing parts") var connectGaps: Boolean = Boolean.box(false) @JsonProperty( value = "Coloring Method", required = false, defaultValue = "heatmap" ) var coloringMethod: ContourPlotColoringFunction = _ override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Contour Plot", "Displays terrain or gradient variations in a Contour Plot", OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP ) override def generatePythonCode(): String = { pyb"""from pytexera import * |import numpy as np |import plotly.graph_objects as go |from scipy.interpolate import griddata |import plotly.io as pio | |class ProcessTableOperator(UDFTableOperator): | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | x = table[$x].values | y = table[$y].values | z = table[$z].values | grid_size = int($gridSize) | connGaps = True if '$connectGaps' == 'true' else False | | grid_x, grid_y = np.meshgrid(np.linspace(min(x), max(x), grid_size), np.linspace(min(y), max(y), grid_size)) | grid_z = griddata((x, y), z, (grid_x, grid_y), method='cubic') | | fig = go.Figure(data=go.Contour( | x=np.linspace(min(x), max(x), grid_size), | y=np.linspace(min(y), max(y), grid_size), | z=grid_z, | connectgaps=connGaps, | contours_coloring ='${coloringMethod.getColoringMethod}', | colorbar_title=$z | )) | fig.update_layout(title='Contour Plot') | html = pio.to_html(fig, include_plotlyjs='cdn', full_html=False) | yield {'html-content': html} |""".encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/dendrogram/DendrogramOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.dendrogram import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder class DendrogramOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "xVal", required = true) @JsonSchemaTitle("Value X Column") @JsonPropertyDescription("The x values of points in dendrogram") @AutofillAttributeName var xVal: EncodableString = "" @JsonProperty(value = "yVal", required = true) @JsonSchemaTitle("Value Y Column") @JsonPropertyDescription("The y value of points in dendrogram") @AutofillAttributeName var yVal: EncodableString = "" @JsonProperty(value = "Labels", required = true) @JsonSchemaTitle("Labels") @JsonPropertyDescription("The label of points in dendrogram") @AutofillAttributeName var labels: EncodableString = "" @JsonProperty(defaultValue = "", required = false) @JsonSchemaTitle("Color Threshold") @JsonPropertyDescription("Value at which separation of clusters will be made") var threshold: EncodableString = "" override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Dendrogram", "Visualize data in a Dendrogram", OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP ) private def createDendrogram(): PythonTemplateBuilder = { assert(xVal.nonEmpty) assert(yVal.nonEmpty) assert(labels.nonEmpty) val strippedThreshold: EncodableString = threshold.trim val isThreshold = if (strippedThreshold.nonEmpty) pyb"color_threshold=$strippedThreshold" else "color_threshold=None" pyb""" | x = np.array(table[$xVal]) | y = np.array(table[$yVal]) | data = np.column_stack((x, y)) | labels = table[$labels].tolist() | | fig = ff.create_dendrogram(data, labels=labels, $isThreshold) | fig.update_layout(yaxis_title="Linkage Distance", margin=dict(l=0, r=0, b=0, t=0)) |""" } override def generatePythonCode(): String = { val finalcode = pyb""" |from pytexera import * | |import plotly.express as px |import plotly.figure_factory as ff |import plotly.io |import pandas as pd |import numpy as np | |class ProcessTableOperator(UDFTableOperator): | def render_error(self, error_msg): | return '''

Dendrogram is not available.

|

Reason is: {}

| '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return | ${createDendrogram()} | # convert fig to html content | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} | |""" finalcode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/dumbbellPlot/DumbbellDotConfig.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.dumbbellPlot import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import javax.validation.constraints.NotNull @JsonSchemaInject(json = """ { "attributeTypeRules": { "dot": { "enum": ["integer", "long", "double"] } } } """) class DumbbellDotConfig { @JsonProperty(value = "dot", required = true) @JsonSchemaTitle("Dot Column Value") @JsonPropertyDescription("value for dot axis") @AutofillAttributeName @NotNull(message = "Dot Column Value cannot be empty") var dotValue: EncodableString = "" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/dumbbellPlot/DumbbellPlotOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.dumbbellPlot import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder import java.util import javax.validation.constraints.{NotBlank, NotNull} import scala.jdk.CollectionConverters.CollectionHasAsScala //type constraint: measurementColumnName can only be a numeric column @JsonSchemaInject(json = """ { "attributeTypeRules": { "measurementColumnName": { "enum": ["integer", "long", "double"] } } } """) class DumbbellPlotOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "categoryColumnName", required = true) @JsonSchemaTitle("Category Column Name") @JsonPropertyDescription("the name of the category column") @AutofillAttributeName @NotNull(message = "Category Column Name cannot be empty") var categoryColumnName: EncodableString = "" @JsonProperty(value = "dumbbellStartValue", required = true) @JsonSchemaTitle("Dumbbell Start Value") @JsonPropertyDescription("the start point value of each dumbbell") @NotBlank(message = "Dumbbell Start Value cannot be empty") var dumbbellStartValue: EncodableString = "" @JsonProperty(value = "dumbbellEndValue", required = true) @JsonSchemaTitle("Dumbbell End Value") @JsonPropertyDescription("the end value of each dumbbell") @NotBlank(message = "Dumbbell End Value cannot be empty") var dumbbellEndValue: EncodableString = "" @JsonProperty(value = "measurementColumnName", required = true) @JsonSchemaTitle("Measurement Column Name") @JsonPropertyDescription("the name of the measurement column") @AutofillAttributeName @NotNull(message = "Measurement Column Name cannot be empty") var measurementColumnName: EncodableString = "" @JsonProperty(value = "comparedColumnName", required = true) @JsonSchemaTitle("Compared Column Name") @JsonPropertyDescription("the column name that is being compared") @AutofillAttributeName @NotNull(message = "Compared Column Name cannot be empty") var comparedColumnName: EncodableString = "" @JsonProperty(value = "dots", required = false) var dots: util.List[DumbbellDotConfig] = _ @JsonProperty(value = "showLegends", required = false) @JsonSchemaTitle("Show Legends?") @JsonPropertyDescription("whether to show legends in the graph") var showLegends: Boolean = false override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Dumbbell Plot", "Visualize data in a Dumbbell Plot. A dumbbell plot (also known as a lollipop chart) is typically used to compare two distinct values or time points for the same entity.", OperatorGroupConstants.VISUALIZATION_BASIC_GROUP ) def createPlotlyDumbbellLineFigure(): PythonTemplateBuilder = { val dumbbellValues = pyb"$dumbbellStartValue, $dumbbellEndValue" var showLegendsOption = "showlegend=False" if (showLegends) { showLegendsOption = "showlegend=True" } pyb""" | | entityNames = list(table[$comparedColumnName].unique()) | entityNames = sorted(entityNames, reverse=True) | categoryValues = [$dumbbellValues] | filtered_table = table[(table[$comparedColumnName].isin(entityNames)) & | (table[$categoryColumnName].isin(categoryValues))] | | # Create the dumbbell line using Plotly | fig = go.Figure() | color = 'black' | for entity in entityNames: | entity_data = filtered_table[filtered_table[$comparedColumnName] == entity] | fig.add_trace(go.Scatter(x=entity_data[$measurementColumnName], | y=[entity]*len(entity_data), | mode='lines', | name=entity, | line=dict(color=color))) | | fig.update_layout(xaxis_title=$measurementColumnName, | yaxis_title=$comparedColumnName, | yaxis=dict(categoryorder='array', categoryarray=entityNames), | $showLegendsOption | ) |""" } def addPlotlyDots(): PythonTemplateBuilder = { var dotColumnNames = "" if (dots != null && dots.size() != 0) { dotColumnNames = dots.asScala .map { dot => pyb"${dot.dotValue}" } .mkString(",") } pyb""" | dotColumnNames = [$dotColumnNames] | if len(dotColumnNames) > 0: | for dotColumn in dotColumnNames: | # Extract dot data for each entity | for entity in entityNames: | entity_dot_data = filtered_table[filtered_table[$comparedColumnName] == entity] | # Extract X and Y values for the dot | x_values = entity_dot_data[dotColumn].values | y_values = [entity] * len(x_values) | # Add scatter plot for dots | fig.add_trace(go.Scatter(x=x_values, y=y_values, | mode='markers', | name=entity + ' ' + dotColumn, | marker=dict(color='black', size=5))) # Customize color and size as needed |""" } override def generatePythonCode(): String = { pyb""" |from pytexera import * | |import plotly.express as px |import plotly.graph_objects as go |import plotly.io |import numpy as np | |class ProcessTableOperator(UDFTableOperator): | def render_error(self, error_msg): | return '''

DumbbellPlot is not available.

|

Reason is: {}

| '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return | ${createPlotlyDumbbellLineFigure()} | ${addPlotlyDots()} | # convert fig to html content | fig.update_layout(margin=dict(l=0, r=0, b=60, t=0)) | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} | |""".encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ecdfPlot/ECDFPlotOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.ecdfPlot import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.pybuilder.PythonTemplateBuilder import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import javax.validation.constraints.NotNull @JsonSchemaInject( json = """{"attributeTypeRules":{"valueColumn":{"enum":["integer","long","double"]}}}""" ) class ECDFPlotOpDesc extends PythonOperatorDescriptor { @JsonProperty(required = true) @JsonSchemaTitle("Value Column") @JsonPropertyDescription("Numeric column used to compute the empirical cumulative distribution.") @AutofillAttributeName @NotNull(message = "Value column cannot be empty") var valueColumn: EncodableString = "" @JsonProperty(required = false) @JsonSchemaTitle("Color Column") @JsonPropertyDescription("Optional column for coloring ECDF lines by group.") @AutofillAttributeName var colorColumn: EncodableString = "" @JsonProperty(required = false) @JsonSchemaTitle("Separate By Column") @JsonPropertyDescription("Optional column for splitting ECDF plots into subplots.") @AutofillAttributeName var separateBy: EncodableString = "" @JsonProperty(required = false, defaultValue = "probability") @JsonSchemaTitle("Y Axis Mode") @JsonPropertyDescription("Display cumulative probability, raw count, or cumulative sum.") @JsonSchemaInject( json = """{ "enum": ["probability", "count", "sum"], "default": "probability" }""" ) var yAxisMode: String = "probability" @JsonProperty(required = false, defaultValue = "standard") @JsonSchemaTitle("CDF Mode") @JsonPropertyDescription( "'standard' shows P(X ≤ x), 'reversed' shows P(X ≥ x), " + "'complementary' shows 1 - P(X ≤ x)." ) @JsonSchemaInject( json = """{ "enum": ["standard", "reversed", "complementary"], "default": "standard" }""" ) var cdfMode: EncodableString = "standard" @JsonProperty(required = false, defaultValue = "vertical") @JsonSchemaTitle("Orientation") @JsonPropertyDescription("Plot ECDF vertically or horizontally.") @JsonSchemaInject(json = """{ "enum": ["vertical", "horizontal"], "default": "vertical" }""") var orientation: EncodableString = "vertical" @JsonProperty(required = false, defaultValue = "false") @JsonSchemaTitle("Show Markers") @JsonPropertyDescription("Display sample markers on the ECDF line.") var showMarkers: Boolean = false @JsonProperty(required = false, defaultValue = "none") @JsonSchemaTitle("Marginal Plot") @JsonPropertyDescription("Optional marginal plot to display alongside the ECDF.") @JsonSchemaInject( json = """{ "enum": ["none", "histogram", "rug"], "default": "none" }""" ) var marginal: EncodableString = "none" override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Empirical Cumulative Distribution Plot", "Visualize the empirical cumulative distribution of a numeric column.", OperatorGroupConstants.VISUALIZATION_STATISTICAL_GROUP ) override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema().add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } def manipulateTable(): PythonTemplateBuilder = { assert(valueColumn.nonEmpty) val requiredCols = List( Some(pyb"$valueColumn"), Option.when(colorColumn.nonEmpty)(pyb"$colorColumn"), Option.when(separateBy.nonEmpty)(pyb"$separateBy") ).flatten val requiredColsExpr = requiredCols.mkString(", ") pyb""" | required_cols = [$requiredColsExpr] | table.dropna(subset=required_cols, inplace=True) | table[$valueColumn] = pd.to_numeric(table[$valueColumn], errors='coerce') | table.dropna(subset=[$valueColumn], inplace=True) |""" } def createPlotlyFigure(): PythonTemplateBuilder = { assert(valueColumn.nonEmpty) val args = scala.collection.mutable.ArrayBuffer[PythonTemplateBuilder]( pyb"table", pyb"x=$valueColumn" ) if (colorColumn.nonEmpty) args += pyb"color=$colorColumn" if (separateBy.nonEmpty) args += pyb"facet_col=$separateBy" yAxisMode match { case "count" => args += pyb"ecdfnorm=None" case "sum" => args += pyb"ecdfnorm=None" case _ => } if (yAxisMode == "sum") args += pyb"y=$valueColumn" if (cdfMode != "standard") args += pyb"ecdfmode=$cdfMode" if (orientation == "horizontal") args += pyb"orientation='h'" if (showMarkers) args += pyb"markers=True" if (marginal != "none") args += pyb"marginal=$marginal" val joinedArgs = args.mkString(", ") pyb""" | fig = px.ecdf($joinedArgs) | fig.update_layout(margin=dict(l=0, r=0, t=30, b=0)) |""" } override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * | |import pandas as pd |import plotly.express as px |import plotly.io | |class ProcessTableOperator(UDFTableOperator): | def render_error(self, error_msg): | return '''

Empirical cumulative distribution plot is not available.

|

Reason is: {}

| '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return | ${manipulateTable()} | if table.empty: | yield {'html-content': self.render_error("no valid rows left after removing missing or non-numeric values.")} | return | ${createPlotlyFigure()} | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/figureFactoryTable/FigureFactoryTableConfig.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.figureFactoryTable import com.fasterxml.jackson.annotation.JsonProperty import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName class FigureFactoryTableConfig { @JsonProperty(required = true) @JsonSchemaTitle("Attribute Name") @AutofillAttributeName var attributeName: EncodableString = "" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/figureFactoryTable/FigureFactoryTableOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.figureFactoryTable import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder class FigureFactoryTableOpDesc extends PythonOperatorDescriptor { @JsonProperty(required = false) @JsonSchemaTitle("Font Size") @JsonPropertyDescription("Font size of the Figure Factory Table") var fontSize: Double = 12 @JsonProperty(required = false) @JsonSchemaTitle("Font Color (Hex Code)") @JsonPropertyDescription("Font color of the Figure Factory Table") var fontColor: EncodableString = "#000000" @JsonProperty(required = false) @JsonSchemaTitle("Row Height") @JsonPropertyDescription("Row height of the Figure Factory Table") var rowHeight: Double = 30 @JsonPropertyDescription("List of columns to include in the figure factory table") @JsonProperty(value = "add attribute", required = true) var columns: List[FigureFactoryTableConfig] = List() private def getAttributes: String = columns.map(c => pyb"""${c.attributeName}""").mkString(",") def manipulateTable(): PythonTemplateBuilder = { assert(columns.nonEmpty) val attributes = getAttributes pyb""" | # drops rows with missing values pertaining to relevant columns | table = table.dropna(subset=[$attributes]) | |""" } def createFigureFactoryTablePlotlyFigure(): PythonTemplateBuilder = { assert(columns.nonEmpty) val intFontSize: Option[Double] = Option(fontSize) val intRowHeight: Option[Double] = Option(rowHeight) assert(intFontSize.isDefined && intFontSize.get >= 0) assert(intRowHeight.isDefined && intRowHeight.get >= 30) val attributes = getAttributes pyb""" | filtered_table = table[[$attributes]] | headers = filtered_table.columns.tolist() | cell_values = [filtered_table[col].tolist() for col in headers] | | data = [headers] + list(map(list, zip(*cell_values))) | fig = ff.create_table(data, height_constant = ${intRowHeight.get}, font_colors=[$fontColor]) | | # Make text size larger | for i in range(len(fig.layout.annotations)): | fig.layout.annotations[i].font.size = ${intFontSize.get} | |""" } override def generatePythonCode(): String = { s""" |from pytexera import * |import plotly.figure_factory as ff |import plotly.io | |class TableChartOperator(UDFTableOperator): | | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return | | ${manipulateTable()} | | if table.empty: | yield {'html-content': self.render_error("value column contains only non-positive numbers or nulls.")} | return | | ${createFigureFactoryTablePlotlyFigure()} | fig.update_layout(margin=dict(l=0, r=0, b=0, t=0)) | html_content = plotly.io.to_html(fig, include_plotlyjs='cdn', include_mathjax='cdn') | yield {'html-content': html_content} """.stripMargin } override def operatorInfo: OperatorInfo = { OperatorInfo.forVisualization( "Figure Factory Table", "Visualize data in a figure factory table", OperatorGroupConstants.VISUALIZATION_BASIC_GROUP ) } override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/filledAreaPlot/FilledAreaPlotOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.filledAreaPlot import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder import javax.validation.constraints.NotNull class FilledAreaPlotOpDesc extends PythonOperatorDescriptor { @JsonProperty(required = true) @JsonSchemaTitle("X-axis Attribute") @JsonPropertyDescription("The attribute for your x-axis") @AutofillAttributeName @NotNull(message = "X-axis Attribute cannot be empty") var x: EncodableString = "" @JsonProperty(required = true) @JsonSchemaTitle("Y-axis Attribute") @JsonPropertyDescription("The attribute for your y-axis") @AutofillAttributeName @NotNull(message = "Y-axis Attribute cannot be empty") var y: EncodableString = "" @JsonProperty(required = false) @JsonSchemaTitle("Line Group") @JsonPropertyDescription("The attribute for group of each line") @AutofillAttributeName var lineGroup: EncodableString = "" @JsonProperty(required = false) @JsonSchemaTitle("Color") @JsonPropertyDescription("Choose an attribute to color the plot") @AutofillAttributeName var color: EncodableString = "" @JsonProperty(required = true) @JsonSchemaTitle("Split Plot by Line Group") @JsonPropertyDescription("Do you want to split the graph") var facetColumn: Boolean = false @JsonProperty(required = false) @JsonSchemaTitle("Pattern") @JsonPropertyDescription("Add texture to the chart based on an attribute") @AutofillAttributeName var pattern: EncodableString = "" override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Filled Area Plot", "Visualize data in a filled area plot", OperatorGroupConstants.VISUALIZATION_BASIC_GROUP ) def createPlotlyFigure(): PythonTemplateBuilder = { assert(x.nonEmpty) assert(y.nonEmpty) if (facetColumn) { assert(lineGroup.nonEmpty) } val colorArg = if (color.nonEmpty) pyb""", color=$color""" else "" val facetColumnArg = if (facetColumn) pyb""", facet_col=$lineGroup""" else "" val lineGroupArg = if (lineGroup.nonEmpty) pyb""", line_group=$lineGroup""" else "" val patternParam = if (pattern.nonEmpty) pyb""", pattern_shape=$pattern""" else "" pyb""" | fig = px.area(table, x=$x, y=$y$colorArg$facetColumnArg$lineGroupArg$patternParam) |""" } // The function below checks whether there are more than 5 percents of the groups have disjoint sets of x attributes. def performTableCheck(): PythonTemplateBuilder = { pyb""" | error = "" | if $x not in columns or $y not in columns: | error = "missing attributes" | elif $lineGroup != "": | grouped = table.groupby($lineGroup) | x_values = None | | tolerance = (len(grouped) // 100) * 5 | count = 0 | | for _, group in grouped: | if x_values == None: | x_values = set(group[$x].unique()) | elif set(group[$x].unique()).intersection(x_values): | X_values = x_values.union(set(group[$x].unique())) | elif not set(group[$x].unique()).intersection(x_values): | count += 1 | if count > tolerance: | error = "X attributes not shared across groups" |""" } override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * | |import plotly |import plotly.express as px |import plotly.graph_objects as go |import plotly.io | |class ProcessTableOperator(UDFTableOperator): | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | columns = list(table.columns) | ${performTableCheck()} | | if error == "": | ${createPlotlyFigure()} | fig.update_layout(margin=dict(l=0, r=0, b=0, t=0)) | | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} | elif error == "X attributes not shared across groups": | | html = '''

Plot is not available, because:

|
  • X attribute is not shared across all line groups
  • | ''' | | yield {'html-content': html} | elif error == "missing attributes": | | html = '''

    Plot is not available, because:

    |
  • X or Y attribute does not exist
  • | ''' | | yield {'html-content': html} |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/funnelPlot/FunnelPlotOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.funnelPlot import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder @JsonSchemaInject(json = """ { "attributeTypeRules": { "title": "string" } } """) class FunnelPlotOpDesc extends PythonOperatorDescriptor { @JsonProperty(required = true) @JsonSchemaTitle("X Column") @JsonPropertyDescription("Data column for the x-axis") @AutofillAttributeName var x: EncodableString = "" @JsonProperty(required = true) @JsonSchemaTitle("Y Column") @JsonPropertyDescription("Data column for the y-axis") @AutofillAttributeName var y: EncodableString = "" @JsonProperty(required = false) @JsonSchemaTitle("Color Column") @JsonPropertyDescription("Column to categorically colorize funnel sections") @AutofillAttributeName var color: EncodableString = "" override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Funnel Plot", "Visualize data in a Funnel Plot", OperatorGroupConstants.VISUALIZATION_FINANCIAL_GROUP ) private def createPlotlyFigure(): PythonTemplateBuilder = { assert(x.nonEmpty) assert(y.nonEmpty) val colorArg = if (color.nonEmpty) pyb""", color=$color""" else "" pyb""" | fig = go.Figure(px.funnel(table, x =$x, y = $y$colorArg)) | fig.update_layout( | scene=dict( | xaxis_title='X: ' + $x, | yaxis_title='Y: ' + $y, | ), | margin=dict(t=0, b=0, l=0, r=0) | ) |""" } override def generatePythonCode(): String = { val finalcode = pyb""" |from pytexera import * | |import plotly.express as px |import plotly.graph_objects as go |import plotly.io |import pandas as pd |import numpy as np | |class ProcessTableOperator(UDFTableOperator): | def render_error(self, error_msg): | return '''

    Chart is not available.

    |

    Reason is: {}

    | '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return | ${createPlotlyFigure()} | # convert fig to html content | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} | |""" finalcode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ganttChart/GanttChartOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.ganttChart import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder import javax.validation.constraints.NotNull @JsonSchemaInject(json = """ { "attributeTypeRules": { "start": { "enum": ["timestamp"] }, "finish": { "enum": ["timestamp"] } } } """) class GanttChartOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "start", required = true) @JsonSchemaTitle("Start Datetime Column") @JsonPropertyDescription("the start timestamp of the task") @AutofillAttributeName @NotNull(message = "Start Datetime Column cannot be empty") var start: EncodableString = "" @JsonProperty(value = "finish", required = true) @JsonSchemaTitle("Finish Datetime Column") @JsonPropertyDescription("the end timestamp of the task") @AutofillAttributeName @NotNull(message = "Finish Datetime Column cannot be empty") var finish: EncodableString = "" @JsonProperty(value = "task", required = true) @JsonSchemaTitle("Task Column") @JsonPropertyDescription("the name of the task") @AutofillAttributeName @NotNull(message = "Task Column cannot be empty") var task: EncodableString = "" @JsonProperty(value = "color", required = false) @JsonSchemaTitle("Color Column") @JsonPropertyDescription("column to color tasks") @AutofillAttributeName var color: EncodableString = "" @JsonProperty(required = false) @JsonSchemaTitle("Pattern") @JsonPropertyDescription("Add texture to the chart based on an attribute") @AutofillAttributeName var pattern: EncodableString = "" override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Gantt Chart", "A Gantt chart is a type of bar chart that illustrates a project schedule. The chart lists the tasks to be performed on the vertical axis, and time intervals on the horizontal axis. The width of the horizontal bars in the graph shows the duration of each activity.", OperatorGroupConstants.VISUALIZATION_BASIC_GROUP ) def manipulateTable(): PythonTemplateBuilder = { val optionalFilterTable = if (color.nonEmpty) pyb"&(table[$color].notnull())" else "" pyb""" | table = table[(table[$start].notnull())&(table[$finish].notnull())&(table[$finish].notnull())$optionalFilterTable].copy() |""" } def createPlotlyFigure(): PythonTemplateBuilder = { val colorSetting = if (color.nonEmpty) pyb", color=$color" else pyb"" val patternParam = if (pattern.nonEmpty) pyb", pattern_shape=$pattern" else pyb"" pyb""" | fig = px.timeline(table, x_start=$start, x_end=$finish, y=$task $colorSetting $patternParam) | fig.update_yaxes(autorange='reversed') | fig.update_layout(margin=dict(t=0, b=0, l=0, r=0)) |""" } override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * | |import plotly.express as px |import plotly.graph_objects as go |import plotly.io |import numpy as np | |class ProcessTableOperator(UDFTableOperator): | def render_error(self, error_msg): | return '''

    Gantt Chart is not available.

    |

    Reason: {}

    | '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("Input table is empty.")} | return | ${manipulateTable()} | if table.empty: | yield {'html-content': self.render_error("One or more of your input columns have all missing values")} | return | ${createPlotlyFigure()} | # convert fig to html content | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/gaugeChart/GaugeChartOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.amber.operator.visualization.gaugeChart import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.scala.DefaultScalaModule import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} class GaugeChartOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "value", required = true) @JsonSchemaTitle("Gauge Value") @JsonPropertyDescription("The primary value displayed on the gauge chart") @AutofillAttributeName var value: EncodableString = "" @JsonProperty(value = "delta", required = false) @JsonSchemaTitle("Delta") @JsonPropertyDescription("The baseline value used to calculate the delta from the gauge value") var delta: EncodableString = "" @JsonProperty(value = "threshold", required = false) @JsonSchemaTitle("Threshold Value") @JsonPropertyDescription("Defines a boundary or target value shown on the gauge chart") var threshold: EncodableString = "" @JsonProperty(value = "steps", required = false) @JsonSchemaTitle("Steps") @JsonPropertyDescription("List of step ranges for the gauge") var steps: List[GaugeChartSteps] = List() override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema().add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Gauge Chart", "Visualize a single value with a radial gauge chart, showing progress towards a goal with optional steps, threshold, and delta.", OperatorGroupConstants.VISUALIZATION_FINANCIAL_GROUP ) private val mapper = new ObjectMapper() mapper.registerModule(DefaultScalaModule) private def serializeSteps(steps: List[GaugeChartSteps]): String = { mapper.writeValueAsString(steps) } override def generatePythonCode(): String = { val stepsStr: EncodableString = serializeSteps(steps) pyb""" |from pytexera import * |import plotly.graph_objects as go |import plotly.io as pio |import json | |class ProcessTableOperator(UDFTableOperator): | | def render_error(self, error_msg) -> str: | return '''

    Gauge chart is not available.

    |

    Reason: {}

    '''.format(error_msg) | | def generate_gray_gradient(self, step_count): | colors = [] | for i in range(step_count): | lightness = 90 - (i * (60 / max(1, step_count - 1))) | colors.append(f"hsl(0, 0%, {lightness}%)") | return colors | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("Input table is empty.")} | return | | try: | gauge_value = $value | try: | delta_ref = float($delta) if $delta.strip() else None | except ValueError: | delta_ref = None | try: | threshold_val = float($threshold) if $threshold.strip() else None | except ValueError: | threshold_val = None | | table = table.dropna(subset=[gauge_value]) | if table.empty: | yield {'html-content': self.render_error("No non-null rows found for the value column.")} | return | | try: | valid_steps = json.loads($stepsStr) | step_colors = self.generate_gray_gradient(len(valid_steps)) | steps_list = [] | for index, step_data in enumerate(valid_steps): | color = step_colors[index] | steps_list.append({ | "range": [float(step_data["start"]), float(step_data["end"])], | "color": color | }) | except Exception: | steps_list = [] | | html_chunks = [] | for _, row in table.iterrows(): | try: | actual = float(row[gauge_value]) | max_val = actual | if delta_ref is not None: | max_val = max(max_val, delta_ref) | if threshold_val is not None: | max_val = max(max_val, threshold_val) | if steps_list: | for r in steps_list: | max_val = max(max_val, r["range"][1]) | | gauge_config = {'axis': {'range': [None, max_val * 1.2]}} | if steps_list: | gauge_config['steps'] = steps_list | if threshold_val is not None: | gauge_config['threshold'] = { | "value": threshold_val, | "line": {"color": "red", "width": 3}, | "thickness": 0.75 | } | | mode_parts = ["number", "gauge"] | if delta_ref is not None: | mode_parts.append("delta") | mode = "+".join(mode_parts) | delta_config = {"reference": delta_ref} if delta_ref is not None else None | | fig = go.Figure(go.Indicator( | mode=mode, | value=actual, | delta=delta_config, | gauge=gauge_config, | domain={"x": [0, 1], "y": [0, 1]}, | title={"text": gauge_value} | )) | | fig.update_layout(margin=dict(l=20, r=20, b=40, t=60), height=250) | html_chunk = pio.to_html(fig, include_plotlyjs='cdn', auto_play=False) | | if hasattr(self, 'step_errors') and self.step_errors: | error_notes = "
    Step Errors:
      " + "".join([f"
    • {msg}
    • " for msg in self.step_errors]) + "
    " | html_chunk += error_notes | | html_chunks.append(html_chunk) | except Exception as e: | html_chunks.append(self.render_error(f"Error generating chart: {str(e)}")) | | yield {"html-content": "
    " + "".join(html_chunks) + "
    "} | | except Exception as e: | yield {'html-content': self.render_error(f"General error: {str(e)}")} |""".encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/gaugeChart/GaugeChartSteps.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.amber.operator.visualization.gaugeChart import com.fasterxml.jackson.annotation.JsonProperty import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString class GaugeChartSteps { @JsonProperty("start") @JsonSchemaTitle("Start") var start: EncodableString = "" @JsonProperty("end") @JsonSchemaTitle("End") var end: EncodableString = "" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/heatMap/HeatMapOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.heatMap import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder class HeatMapOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "x", required = true) @JsonSchemaTitle("Value X Column") @JsonPropertyDescription("the values along the x-axis") @AutofillAttributeName var x: EncodableString = "" @JsonProperty(value = "y", required = true) @JsonSchemaTitle("Value Y Column") @JsonPropertyDescription("the values along the y-axis") @AutofillAttributeName var y: EncodableString = "" @JsonProperty(value = "Values", required = true) @JsonSchemaTitle("Values") @JsonPropertyDescription("the values of the heatmap") @AutofillAttributeName var value: EncodableString = "" override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Heatmap", "Visualize data in a HeatMap Chart", OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP ) private def createHeatMap(): PythonTemplateBuilder = { assert(x.nonEmpty) assert(y.nonEmpty) assert(value.nonEmpty) pyb""" | heatmap = go.Heatmap(z=table[$value],x=table[$x],y=table[$y]) | layout = go.Layout(margin=dict(l=0, r=0, b=0, t=0)) | fig = go.Figure(data=[heatmap], layout=layout) |""" } override def generatePythonCode(): String = { val finalcode = pyb""" |from pytexera import * | |import plotly.express as px |import plotly.graph_objects as go |import plotly.io |import pandas as pd |import numpy as np | |class ProcessTableOperator(UDFTableOperator): | def render_error(self, error_msg): | return '''

    Chart is not available.

    |

    Reason is: {}

    | '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return | ${createHeatMap()} | # convert fig to html content | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} | |""" finalcode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/hierarchychart/HierarchyChartOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.hierarchychart import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder import javax.validation.constraints.{NotEmpty, NotNull} // type constraint: value can only be numeric @JsonSchemaInject(json = """ { "attributeTypeRules": { "value": { "enum": ["integer", "long", "double"] } } } """) class HierarchyChartOpDesc extends PythonOperatorDescriptor { @JsonProperty(required = true) @JsonSchemaTitle("Chart Type") @JsonPropertyDescription("Treemap or Sunburst") @NotNull(message = "Hierarchy Chart Type cannot be empty") var hierarchyChartType: HierarchyChartType = _ @JsonProperty(required = true) @JsonSchemaTitle("Hierarchy Path") @JsonPropertyDescription( "Hierarchy of attributes from a higher-level category to lower-level category" ) @NotEmpty(message = "Hierarchy path list cannot be empty") var hierarchy: List[HierarchySection] = List() @JsonProperty(value = "value", required = true) @JsonSchemaTitle("Value Column") @JsonPropertyDescription("The value associated with the size of each sector in the chart") @AutofillAttributeName @NotNull(message = "Value column cannot be empty") var value: EncodableString = "" override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Hierarchy Chart", "Visualize data in hierarchy", OperatorGroupConstants.VISUALIZATION_BASIC_GROUP ) private def getHierarchyAttributesInPython: String = hierarchy.map(c => pyb"${c.attributeName}").mkString(",") def manipulateTable(): PythonTemplateBuilder = { assert(value.nonEmpty) val attributes = getHierarchyAttributesInPython pyb""" | table[$value] = table[table[$value] > 0][$value] # remove non-positive numbers from the data | table.dropna(subset = [$attributes], inplace = True) #remove missing values |""" } def createPlotlyFigure(): PythonTemplateBuilder = { assert(hierarchy.nonEmpty) val attributes = getHierarchyAttributesInPython pyb""" | fig = px.${hierarchyChartType.getPlotlyExpressApiName}(table, path=[$attributes], values=$value, | color=$value, hover_data=[$attributes], | color_continuous_scale='RdBu') |""" } override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * | |import plotly.express as px |import plotly.graph_objects as go |import plotly.io |import numpy as np | |class ProcessTableOperator(UDFTableOperator): | | # Generate custom error message as html string | def render_error(self, error_msg) -> str: | return '''

    Hierarchy chart is not available.

    |

    Reason is: {}

    | '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return | ${manipulateTable()} | if table.empty: | yield {'html-content': self.render_error("value column contains only non-positive numbers or nulls.")} | return | ${createPlotlyFigure()} | # convert fig to html content | fig.update_layout(margin=dict(l=0, r=0, b=0, t=0)) | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/hierarchychart/HierarchyChartType.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.hierarchychart; import com.fasterxml.jackson.annotation.JsonValue; public enum HierarchyChartType { TREEMAP("treemap"), SUNBURSTCHART("sunburst"); private final String plotlyExpressApiName; HierarchyChartType(String plotlyExpressApiName) { this.plotlyExpressApiName = plotlyExpressApiName; } // use the name string instead of enum string in JSON\ @JsonValue public String getPlotlyExpressApiName() { return this.plotlyExpressApiName; } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/hierarchychart/HierarchySection.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.hierarchychart import com.fasterxml.jackson.annotation.JsonProperty import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import javax.validation.constraints.NotNull // This is a hack to get a order-preserve selection from the combobox // TODO: remove it after we enabled a order-preserve combobox on the frontend. class HierarchySection { @JsonProperty(required = true) @JsonSchemaTitle("Attribute Name") @AutofillAttributeName @NotNull(message = "Attribute Name cannot be empty") var attributeName: EncodableString = "" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/histogram/HistogramChartOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.histogram import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder class HistogramChartOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "value", required = true) @JsonSchemaTitle("Value Column") @JsonPropertyDescription("Column for counting values.") @AutofillAttributeName var value: EncodableString = "" @JsonProperty(required = false) @JsonSchemaTitle("Color Column") @JsonPropertyDescription("Column for differentiating data by its value.") @AutofillAttributeName var color: EncodableString = "" @JsonProperty(required = false) @JsonSchemaTitle("SeparateBy Column") @JsonPropertyDescription("Column for separating histogram chart by its value.") @AutofillAttributeName var separateBy: EncodableString = "" @JsonProperty(required = false, defaultValue = "") @JsonSchemaTitle("Distribution Type") @JsonPropertyDescription("Distribution type (rug, box, violin).") var marginal: EncodableString = "" @JsonProperty(required = false) @JsonSchemaTitle("Pattern") @JsonPropertyDescription("Add texture to the chart based on an attribute") @AutofillAttributeName var pattern: EncodableString = "" override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Histogram", "Visualize data in a Histogram Chart", OperatorGroupConstants.VISUALIZATION_STATISTICAL_GROUP ) def createPlotlyFigure(): PythonTemplateBuilder = { assert(value.nonEmpty) var colorParam = pyb"" var categoryParam = pyb"" var marginalParam = pyb"" var patternParam = pyb"" if (color.nonEmpty) colorParam = pyb", color = $color" if (separateBy.nonEmpty) categoryParam = pyb", facet_col = $separateBy" if (marginal.nonEmpty) marginalParam = pyb", marginal=$marginal" if (pattern != "") patternParam = pyb", pattern_shape=$pattern" pyb""" | fig = px.histogram(table, x = $value, text_auto = True $colorParam $categoryParam $marginalParam $patternParam) | fig.update_layout(margin=dict(l=0, r=0, t=0, b=0)) |""" } override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * | |import plotly.express as px |import plotly.graph_objects as go |import plotly.io |import numpy as np | |class ProcessTableOperator(UDFTableOperator): | def render_error(self, error_msg): | return '''

    Histogram chart is not available.

    |

    Reason is: {}

    | '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return | ${createPlotlyFigure()} | # convert fig to html content | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} | |""" finalCode.encode } override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/histogram2d/Histogram2DOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.amber.operator.visualization.histogram2d import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} class Histogram2DOpDesc extends PythonOperatorDescriptor { @JsonProperty(required = true) @JsonSchemaTitle("X Column") @JsonPropertyDescription("Numeric column for the X axis bins.") @AutofillAttributeName var xColumn: EncodableString = "" @JsonProperty(required = true) @JsonSchemaTitle("Y Column") @JsonPropertyDescription("Numeric column for the Y axis bins.") @AutofillAttributeName var yColumn: EncodableString = "" @JsonProperty(required = true, defaultValue = "10") @JsonSchemaTitle("X Bins") @JsonPropertyDescription("Number of bins along the X axis (Default: 10)") var xBins: Int = 10 @JsonProperty(required = true, defaultValue = "10") @JsonSchemaTitle("Y Bins") @JsonPropertyDescription("Number of bins along the Y axis (Default: 10)") var yBins: Int = 10 @JsonProperty(required = false, defaultValue = "density") @JsonSchemaTitle("Normalization") @JsonPropertyDescription( "Type of histogram normalization" ) var normalize: NormalizationType = NormalizationType.DENSITY override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Histogram2D", "Displays a bivariate histogram as a density heatmap", OperatorGroupConstants.VISUALIZATION_STATISTICAL_GROUP ) override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema().add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def generatePythonCode(): String = { assert(xBins > 0, s"X Bins must be > 0, but got $xBins") assert(yBins > 0, s"Y Bins must be > 0, but got $yBins") val normArg = pyb"histnorm='${normalize.getValue}'," pyb""" |from pytexera import * |import plotly.express as px |import plotly.io | |class ProcessTableOperator(UDFTableOperator): | def render_error(self, msg): | return f"

    2D Histogram failed

    {msg}

    " | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | # Empty-table guard | if table.empty: | yield {"html-content": self.render_error("Input table is empty.")} | return | | # Drop rows with missing x/y | table.dropna(subset=[$xColumn, $yColumn], inplace=True) | if table.empty: | yield {"html-content": self.render_error("No rows after dropping nulls.")} | return | | fig = px.density_heatmap( | table, | x=$xColumn, | y=$yColumn, | nbinsx=$xBins, | nbinsy=$yBins, | $normArg | text_auto=True | ) | | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {"html-content": html} |""".encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/histogram2d/NormalizationType.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.amber.operator.visualization.histogram2d; import com.fasterxml.jackson.annotation.JsonValue; public enum NormalizationType { DENSITY("density"), PROBABILITY("probability"), PERCENT("percent"); private final String value; NormalizationType(String value) { this.value = value; } @JsonValue public String getValue() { return this.value; } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/htmlviz/HtmlVizOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.htmlviz import com.fasterxml.jackson.annotation.JsonProperty import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{PhysicalOp, SchemaPropagationFunc} import org.apache.texera.amber.operator.LogicalOp import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.util.JSONUtils.objectMapper /** * HTML Visualization operator to render any given HTML code * This is the description of the operator */ class HtmlVizOpDesc extends LogicalOp { @JsonProperty(required = true) @JsonSchemaTitle("HTML content") @AutofillAttributeName var htmlContentAttrName: String = _ override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .oneToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.visualization.htmlviz.HtmlVizOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPropagateSchema( SchemaPropagationFunc(inputSchemas => { val outputSchema = Schema().add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) }) ) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "HTML Visualizer", "Render the result of HTML content", OperatorGroupConstants.VISUALIZATION_MEDIA_GROUP ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/htmlviz/HtmlVizOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.htmlviz import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} import org.apache.texera.amber.util.JSONUtils.objectMapper /** * HTML Visualization operator to render any given HTML code */ class HtmlVizOpExec(descString: String) extends OperatorExecutor { private val desc: HtmlVizOpDesc = objectMapper.readValue(descString, classOf[HtmlVizOpDesc]) override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = Iterator(TupleLike(tuple.getField[Any](desc.htmlContentAttrName))) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/lineChart/LineChartOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.lineChart import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder import java.util import scala.jdk.CollectionConverters.ListHasAsScala class LineChartOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "yLabel", required = false, defaultValue = "Y Axis") @JsonSchemaTitle("Y Label") @JsonPropertyDescription("the label for y axis") var yLabel: EncodableString = "" @JsonProperty(value = "xLabel", required = false, defaultValue = "X Axis") @JsonSchemaTitle("X Label") @JsonPropertyDescription("the label for x axis") var xLabel: EncodableString = "" @JsonProperty(value = "lines", required = true) var lines: util.List[LineConfig] = new util.ArrayList[LineConfig]() override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Line Chart", "View the result in line chart", OperatorGroupConstants.VISUALIZATION_BASIC_GROUP ) def createPlotlyFigure(): PythonTemplateBuilder = { assert(lines != null && !lines.isEmpty, "At least one line must be configured") val linesPart = lines.asScala .map { lineConf => val colorPart = if (lineConf.color != "") { pyb"line={'color':${lineConf.color}}, marker={'color':${lineConf.color}}, " } else { pyb"" } val namePart = if (lineConf.name != "") { pyb"name=${lineConf.name}" } else { pyb"name=${lineConf.yValue}" } pyb"""fig.add_trace(go.Scatter( x=table[${lineConf.xValue}], y=table[${lineConf.yValue}], mode='${lineConf.mode.getModeInPlotly}', $colorPart $namePart ))""" } pyb""" | fig = go.Figure() | ${linesPart.mkString("\n ")} | fig.update_layout(margin=dict(t=0, b=0, l=0, r=0), | xaxis_title=$xLabel, | yaxis_title=$yLabel) |""" } override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * | |import plotly.express as px |import plotly.graph_objects as go |import plotly.io |import numpy as np | |class ProcessTableOperator(UDFTableOperator): | | # Generate custom error message as html string | def render_error(self, error_msg) -> str: | return '''

    Line chart is not available.

    |

    Reason is: {}

    | '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return | ${createPlotlyFigure()} | # convert fig to html content | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/lineChart/LineConfig.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.lineChart import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import javax.validation.constraints.NotNull //type constraint: value can only be numeric @JsonSchemaInject(json = """ { "attributeTypeRules": { "yValue": { "enum": ["integer", "long", "double"] }, "xValue": { "enum": ["integer", "long", "double"] } } } """) class LineConfig { @JsonProperty(value = "y", required = true) @JsonSchemaTitle("Y Value") @JsonPropertyDescription("value for y axis") @AutofillAttributeName @NotNull(message = "Y Value cannot be empty") var yValue: EncodableString = "" @JsonProperty(value = "x", required = true) @JsonSchemaTitle("X Value") @JsonPropertyDescription("value for x axis") @AutofillAttributeName @NotNull(message = "X Value cannot be empty") var xValue: EncodableString = "" @JsonProperty( value = "mode", required = true, defaultValue = "line with dots" ) @JsonSchemaTitle("Line Mode") @NotNull(message = "Line Mode cannot be empty") var mode: LineMode = LineMode.LINE_WITH_DOTS @JsonProperty(value = "name", required = false) @JsonSchemaTitle("Line Name") var name: EncodableString = "" @JsonProperty(value = "color", required = false) @JsonSchemaTitle("Line Color") @JsonPropertyDescription("must be a valid CSS color or hex color string") var color: EncodableString = "" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/lineChart/LineMode.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.lineChart; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; public enum LineMode { LINE("line"), DOTS("dots"), LINE_WITH_DOTS("line with dots"); private final String mode; LineMode(String mode) { this.mode = mode; } // Handle custom deserialization for enum @JsonCreator public static LineMode fromString(String value) { for (LineMode mode : LineMode.values()) { if (mode.mode.equalsIgnoreCase(value)) { return mode; } } throw new IllegalArgumentException("Unknown line mode: " + value); } @JsonValue public String getMode() { return mode; } public String getModeInPlotly() { // make the mode string compatible with plotly API. switch (this) { case DOTS: return "markers"; case LINE: return "lines"; case LINE_WITH_DOTS: return "lines+markers"; default: throw new UnsupportedOperationException("line mode is not supported"); } } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/nestedTable/NestedTableConfig.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.amber.operator.visualization.nestedTable import com.fasterxml.jackson.annotation.JsonProperty import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName class NestedTableConfig { @JsonProperty(required = true) @JsonSchemaTitle("Attribute group") var attributeGroup: EncodableString = "" @JsonProperty(required = true) @JsonSchemaTitle("Original attribute Name") @AutofillAttributeName var originalName: EncodableString = "" @JsonProperty(value = "name", required = false) @JsonSchemaTitle("New Attribute Name") var newName: EncodableString = "" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/nestedTable/NestedTableOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.amber.operator.visualization.nestedTable import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder import java.util import scala.jdk.CollectionConverters.ListHasAsScala class NestedTableOpDesc extends PythonOperatorDescriptor { @JsonPropertyDescription( "List of columns to include in the nested table chart and their subgroup" ) @JsonProperty(value = "add attribute", required = true) var includedColumns: util.List[NestedTableConfig] = _ override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Nested Table", "Visualize Data in a Depth Two Nested Table", OperatorGroupConstants.VISUALIZATION_GROUP ) private def createNestedTable(): PythonTemplateBuilder = { val sortedColumns = includedColumns.asScala.sortBy(_.attributeGroup) pyb""" | columns = pd.MultiIndex.from_tuples([ | ${sortedColumns .map { config => val name = if (config.newName != null && config.newName.nonEmpty) config.newName else config.originalName pyb"(${config.attributeGroup}, $name)" } .mkString(",\n ")} | ]) | | data = [] | for _, row in table.iterrows(): | data.append([ | ${sortedColumns .map(config => pyb"row[${config.originalName}]") .mkString(", ")} | ]) | | df = pd.DataFrame(data, columns=columns) | | styles = [ | {'selector': 'th', 'props': [('background-color', '#f2f2f2'), | ('color', 'black'), | ('font-weight', 'bold'), | ('border', '1px solid #ddd'), | ('padding', '8px'), | ('text-align', 'center')]}, | {'selector': 'td', 'props': [('border', '1px solid #ddd'), | ('padding', '8px'), | ('text-align', 'center')]}, | {'selector': 'caption', 'props': [('caption-side', 'top'), | ('font-size', '16pt'), | ('font-weight', 'bold'), | ('text-align', 'left'), | ('padding', '10px')]}, | {'selector': '.row_heading', 'props': [('text-align', 'left'), | ('font-weight', 'normal')]}, | {'selector': '.blank.level0', 'props': [('display', 'none')]} | ] | | styled_table = ( | df.style | .set_table_styles(styles) | .format(precision=2, na_rep="") | .set_table_attributes('class="dataframe"') | .hide(axis="index") | ) |""" } override def generatePythonCode(): String = { val finalcode = pyb""" |from pytexera import * | |import pandas as pd |import numpy as np | |class ProcessTableOperator(UDFTableOperator): | def render_error(self, error_msg): | return '''

    Nested Table is not available.

    |

    Reason is: {}

    | '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return | ${createNestedTable()} | # convert table to html content | html = styled_table.to_html() | yield {'html-content': html} | |""" finalcode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/networkGraph/NetworkGraphOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.networkGraph import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder class NetworkGraphOpDesc extends PythonOperatorDescriptor { @JsonProperty(required = true) @JsonSchemaTitle("Source Column") @JsonPropertyDescription("Source node for edge in graph") @AutofillAttributeName var source: EncodableString = "" @JsonProperty(required = true) @JsonSchemaTitle("Destination Column") @JsonPropertyDescription("Destination node for edge in graph") @AutofillAttributeName var destination: EncodableString = "" @JsonProperty(defaultValue = "Network Graph") @JsonSchemaTitle("Title") var title: EncodableString = "" override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Network Graph", "Visualize data in a network graph", OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP ) def manipulateTable(): PythonTemplateBuilder = { assert(source.nonEmpty) assert(destination.nonEmpty) pyb""" | table = table.dropna(subset = [$source]) #remove missing values | table = table.dropna(subset = [$destination]) #remove missing values |""" } override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * |import pandas as pd |import plotly.graph_objects as go |import plotly.io |import json |import pickle |import plotly |import networkx as nx | |class ProcessTableOperator(UDFTableOperator): | def render_error(self, error_msg): | return '''

    Network graph is not available.

    |

    Reason is: {}

    | '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if not table.empty: | sources = table[$source] | destinations = table[$destination] | nodes = set(sources + destinations) | G = nx.Graph() | for node in nodes: | G.add_node(node) | for i, j in table.iterrows(): | G.add_edges_from([(j[$source], j[$destination])]) | pos = nx.spring_layout(G, k=0.5, iterations=50) | for n, p in pos.items(): | G.nodes[n]['pos'] = p | | edge_trace = go.Scatter( | x=[], | y=[], | name='Edges', | line=dict(width=0.5, color='#888'), | hoverinfo='none', | mode='lines', | visible=True | ) | | for edge in G.edges(): | x0, y0 = G.nodes[edge[0]]['pos'] | x1, y1 = G.nodes[edge[1]]['pos'] | edge_trace['x'] += tuple([x0, x1, None]) | edge_trace['y'] += tuple([y0, y1, None]) | | node_trace = go.Scatter( | x=[], | y=[], | name='Nodes', | text=[], | mode='markers', | hoverinfo='text', | visible=True, | marker=dict( | showscale=True, | colorscale='plasma', | reversescale=True, | color=[], | size=15, | colorbar=dict( | thickness=10, | title='Node Connections', | xanchor='left', | titleside='right' | ), | line=dict(width=0) | ) | ) | | for node in G.nodes(): | x, y = G.nodes[node]['pos'] | node_trace['x'] += tuple([x]) | node_trace['y'] += tuple([y]) | | for node, adjacencies in enumerate(G.adjacency()): | node_trace['marker']['color'] += tuple([len(adjacencies[1])]) | node_info = str(adjacencies[0]) + ': ' + str(len(adjacencies[1])) + ' connections.' | node_trace['text'] += tuple([node_info]) | | fig = go.Figure( | data=[edge_trace, node_trace], | layout=go.Layout( | title='
    '+$title, | hovermode='closest', | showlegend=False, | margin=dict(b=20, l=5, r=5, t=40), | annotations=[ | dict( | text='', | showarrow=False, | xref="paper", | yref="paper" | ) | ], | xaxis=dict(showgrid=False, zeroline=False, showticklabels=False), | yaxis=dict(showgrid=False, zeroline=False, showticklabels=False) | ) | ) | fig.update_layout( | margin=dict(l=0, r=0, t=0, b=0), | legend=dict( | itemclick=False, | itemdoubleclick=False | ) | ) | | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | else: | html = self.render_error('Table should not have any empty/null values or fields.') | | yield {'html-content': html} | |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/parallelCoordinatesPlot/ParallelCoordinatesPlotOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.parallelCoordinatesPlot import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.{ AutofillAttributeName, AutofillAttributeNameList } import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.pybuilder.PythonTemplateBuilder import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import javax.validation.constraints.{NotNull, Size} // type constraint: value can only be numeric @JsonSchemaInject(json = """ { "attributeTypeRules": { "dimensions": { "enum": ["integer", "long", "double"] } } } """) class ParallelCoordinatesPlotOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "dimensions", required = true) @JsonSchemaTitle("Dimensions") @JsonPropertyDescription("List of numeric columns to visualize as parallel axes") @AutofillAttributeNameList @NotNull(message = "Dimensions cannot be empty") @Size(min = 1, message = "At least one dimension is required") var dimensions: List[EncodableString] = List() @JsonProperty(value = "color", required = false) @JsonSchemaTitle("Color Column") @JsonPropertyDescription("Column used to color or group the lines") @AutofillAttributeName var color: EncodableString = _ override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Parallel Coordinates Plot", "Visualize multivariate data using parallel coordinate axes", OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP ) override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } def manipulateTable(): PythonTemplateBuilder = { val dimCols = dimensions.map(c => pyb"$c").mkString(",") val colorFilter = if (color != null && color.nonEmpty) pyb"&(table[$color].notnull())" else "" pyb""" | table = table[table[[$dimCols]].notnull().all(axis=1)$colorFilter].copy() |""" } def createPlotlyFigure(): PythonTemplateBuilder = { val dimCols = dimensions.map(c => pyb"$c").mkString(",") val colorArg = if (color != null && color.nonEmpty) pyb", color=$color" else "" pyb""" | fig = px.parallel_coordinates( | table, | dimensions=[$dimCols]$colorArg | ) |""" } override def generatePythonCode(): String = { val finalcode = pyb""" |from pytexera import * | |import plotly.express as px |import plotly.io | |class ProcessTableOperator(UDFTableOperator): | | def render_error(self, error_msg): | return '''

    Parallel coordinates plot is not available.

    |

    Reason is: {}

    | '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("Input table is empty.")} | return | ${manipulateTable()} | if table.empty: | yield {'html-content': self.render_error("No valid rows after filtering.")} | return | ${createPlotlyFigure()} | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} |""" finalcode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/pieChart/PieChartOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.pieChart import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder import javax.validation.constraints.NotNull // type constraint: value can only be numeric @JsonSchemaInject(json = """ { "attributeTypeRules": { "value": { "enum": ["integer", "long", "double"] } } } """) class PieChartOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "value", required = true) @JsonSchemaTitle("Value Column") @JsonPropertyDescription("The value associated with slice of pie") @AutofillAttributeName @NotNull(message = "Value column cannot be empty") var value: EncodableString = "" @JsonProperty(value = "name", required = true) @JsonSchemaTitle("Name Column") @JsonPropertyDescription("The name of the slice of pie") @AutofillAttributeName @NotNull(message = "Name column cannot be empty") var name: EncodableString = "" override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Pie Chart", "Visualize data in a Pie Chart", OperatorGroupConstants.VISUALIZATION_BASIC_GROUP ) def manipulateTable(): PythonTemplateBuilder = { assert(value.nonEmpty) pyb""" | table.dropna(subset = [$value, $name], inplace = True) #remove missing values |""" } def createPlotlyFigure(): PythonTemplateBuilder = { assert(value.nonEmpty) pyb""" | fig = px.pie(table, names=$name, values=$value) | fig.update_traces(textposition='inside', textinfo='percent+label') | fig.update_layout(margin=dict(t=0, b=0, l=0, r=0)) |""" } override def generatePythonCode(): String = { val finalcode = pyb""" |from pytexera import * | |import plotly.express as px |import plotly.graph_objects as go |import plotly.io |import numpy as np | |class ProcessTableOperator(UDFTableOperator): | def render_error(self, error_msg): | return '''

    PieChart is not available.

    |

    Reason is: {}

    | '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | original_table = table | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return | ${manipulateTable()} | if table.empty: | yield {'html-content': self.render_error("value column contains only non-positive numbers.")} | return | duplicates = table.duplicated(subset=[$name]) | if duplicates.any(): | yield {'html-content': self.render_error("duplicates in name column, need to aggregate")} | return | ${createPlotlyFigure()} | # convert fig to html content | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} | |""" finalcode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/polarChart/PolarChartOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.polarChart import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.core.workflow.OutputPort.OutputMode import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity} import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext class PolarChartOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "r", required = true) @JsonSchemaTitle("r") @JsonPropertyDescription("The column name for radial values (must be numeric)") @AutofillAttributeName var r: EncodableString = "" @JsonProperty(value = "theta", required = true) @JsonSchemaTitle("theta") @JsonPropertyDescription("The column name for angular values (must be numeric)") @AutofillAttributeName var theta: EncodableString = "" override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo( "Polar Chart", "Displays data points in a polar scatter plot", OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort(mode = OutputMode.SINGLE_SNAPSHOT)) ) override def generatePythonCode(): String = { val finalCode = pyb"""from pytexera import * |import plotly.graph_objects as go |import plotly.io as pio |import numpy as np | |class ProcessTableOperator(UDFTableOperator): | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | | if table is None or table.empty: | yield {'html-content': '

    No data available for Polar Chart

    '} | return | | if $r not in table.columns or $theta not in table.columns: | yield {'html-content': '

    Selected columns not found in input table

    '} | return | | if not np.issubdtype(table[$r].dtype, np.number) or not np.issubdtype(table[$theta].dtype, np.number): | yield {'html-content': '

    Selected columns must be numeric

    '} | return | | r_vals = table[$r].values | theta_vals = table[$theta].values | | fig = go.Figure(data=go.Scatterpolargl( | r=r_vals, | theta=theta_vals, | mode='markers', | marker=dict( | size=10, | opacity=0.7, | line=dict(color='white') | ) | )) | | fig.update_layout( | title='Polar Chart', | showlegend=False | ) | | html = pio.to_html(fig, include_plotlyjs='cdn', full_html=False) | yield {'html-content': html} |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/quiverPlot/QuiverPlotOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.quiverPlot import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder @JsonSchemaInject(json = """ { "attributeTypeRules": { "value": { "enum": ["integer", "long", "double"] } } } """) class QuiverPlotOpDesc extends PythonOperatorDescriptor { //property panel variable: 4 requires: {x,y,u,v}, all columns should only contain numerical data @JsonProperty(value = "x", required = true) @JsonSchemaTitle("x") @JsonPropertyDescription("Column for the x-coordinate of the starting point") @AutofillAttributeName var x: EncodableString = "" @JsonProperty(value = "y", required = true) @JsonSchemaTitle("y") @JsonPropertyDescription("Column for the y-coordinate of the starting point") @AutofillAttributeName var y: EncodableString = "" @JsonProperty(value = "u", required = true) @JsonSchemaTitle("u") @JsonPropertyDescription("Column for the vector component in the x-direction") @AutofillAttributeName var u: EncodableString = "" @JsonProperty(value = "v", required = true) @JsonSchemaTitle("v") @JsonPropertyDescription("Column for the vector component in the y-direction") @AutofillAttributeName var v: EncodableString = "" override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Quiver Plot", "Visualize vector data in a Quiver Plot", OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP ) //data cleaning for missing value def manipulateTable(): PythonTemplateBuilder = { pyb""" | table = table.dropna() #remove missing values |""" } override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * |import pandas as pd |import plotly.figure_factory as ff |import numpy as np |import plotly.io |import plotly.graph_objects as go | |class ProcessTableOperator(UDFTableOperator): | | def render_error(self, error_msg): | return '''

    Quiver Plot is not available.

    |

    Reasons are: {}

    | '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("Input table is empty.")} | return | | required_columns = {$x, $y, $u, $v} | if not required_columns.issubset(table.columns): | yield {'html-content': self.render_error(f"Input table must contain columns: {', '.join(required_columns)}")} | return | | ${manipulateTable()} | | def type_check(value): | return isinstance(value,(int,float)) | for col in required_columns: | if not table[col].apply(type_check).all(): | yield {"html-content": "Type error: All columns should only contain numerical data"} | return | | try: | #graph the quiver plot | fig = ff.create_quiver( | table[$x], table[$y], | table[$u], table[$v], | scale=0.1 | ) | html = fig.to_html(include_plotlyjs='cdn', full_html=False) | except Exception as e: | yield {'html-content': self.render_error(f"Plotly error: {str(e)}")} | return | | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/radarChart/RadarChartOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.radarChart import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.{ AutofillAttributeName, AutofillAttributeNameList } import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.pybuilder.PythonTemplateBuilder import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import javax.validation.constraints.NotNull // type constraint: value can only be numeric @JsonSchemaInject(json = """ { "attributeTypeRules": { "valueColumns": { "enum": ["integer", "long", "double"] } } } """) class RadarChartOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "nameColumn", required = true) @JsonSchemaTitle("Name Column") @JsonPropertyDescription("Column containing entity names for each radar") @AutofillAttributeName @NotNull(message = "Name column cannot be empty") var nameColumn: EncodableString = "" @JsonProperty(value = "valueColumns", required = true) @JsonSchemaTitle("Value Columns") @JsonPropertyDescription("Columns containing numeric values for radar chart axes") @AutofillAttributeNameList var valueColumns: List[EncodableString] = _ @JsonProperty(value = "fillOpacity", required = true) @JsonSchemaTitle("Fill Opacity") @JsonPropertyDescription( "Opacity value for radar chart fill from 0.0 (transparent) to 1.0 (opaque)" ) @JsonSchemaInject(json = """{ "minimum": 0.0, "maximum": 1.0, "default": 0.5 }""") var fillOpacity: Double = 0.5 override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Radar Chart", "Visualize data in a Radar Chart", OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP ) override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } def manipulateTable(): PythonTemplateBuilder = { assert(nameColumn.nonEmpty) assert(valueColumns != null && valueColumns.nonEmpty) val valueColsList = valueColumns.map(col => pyb"""$col""").mkString(", ") pyb""" | required_cols = [$nameColumn, $valueColsList] | table.dropna(subset=required_cols, inplace=True) | value_cols = [$valueColsList] | for col in value_cols: | table[col] = pd.to_numeric(table[col], errors='coerce') | table.dropna(subset=value_cols, inplace=True) |""" } def createPlotlyFigure(): PythonTemplateBuilder = { val valueColsList = valueColumns.map(col => pyb"""$col""").mkString(", ") pyb""" | fig = go.Figure() | categories = [$valueColsList] | | for idx, row in table.iterrows(): | values = [row[col] for col in categories] | values.append(values[0]) | categories_closed = categories + [categories[0]] | | fig.add_trace(go.Scatterpolar( | r=values, | theta=categories_closed, | fill='toself', | name=str(row[$nameColumn]), | opacity=$fillOpacity | )) | | fig.update_layout( | polar=dict( | radialaxis=dict( | visible=True, | range=[0, None] | ) | ), | showlegend=True, | margin=dict(t=40, b=40, l=40, r=40) | ) |""" } override def generatePythonCode(): String = { val finalcode = pyb""" |from pytexera import * | |import plotly.graph_objects as go |import plotly.io |import pandas as pd | |class ProcessTableOperator(UDFTableOperator): | def render_error(self, error_msg): | return '''

    RadarChart is not available.

    |

    Reason is: {}

    | '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return | ${manipulateTable()} | if table.empty: | yield {'html-content': self.render_error("input table is empty after removing missing values.")} | return | ${createPlotlyFigure()} | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} | |""" finalcode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/radarPlot/RadarPlotLinePattern.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.amber.operator.visualization.radarPlot; import com.fasterxml.jackson.annotation.JsonValue; public enum RadarPlotLinePattern { SOLID("solid"), DASH("dash"), DOT("dot"); private final String linePattern; RadarPlotLinePattern(String linePattern) { this.linePattern = linePattern; } @JsonValue public String getLinePattern() { return this.linePattern; } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.radarPlot import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.operator.metadata.annotations.{ AutofillAttributeName, AutofillAttributeNameList } import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.pybuilder.PythonTemplateBuilder import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext @JsonSchemaInject(json = """ { "attributeTypeRules": { "selectedAttributes": { "enum": ["integer", "long", "double"] } } } """) class RadarPlotOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "selectedAttributes", required = true) @JsonSchemaTitle("Axes") @JsonPropertyDescription("Numeric columns to use as radar axes") @AutofillAttributeNameList var selectedAttributes: List[EncodableString] = _ @JsonProperty(value = "traceNameAttribute", defaultValue = "No Selection", required = false) @JsonSchemaTitle("Trace Name Column") @JsonPropertyDescription("Optional - Select a column to use for naming each radar trace") @AutofillAttributeName var traceNameAttribute: EncodableString = "" @JsonProperty( value = "traceColorAttribute", defaultValue = "No Selection", required = false ) @JsonSchemaTitle("Trace Color Column") @JsonPropertyDescription( "Optional - Select a column to use for coloring each radar trace (note: if there are too many traces with distinct coloring values, colors may repeat)" ) @AutofillAttributeName var traceColorAttribute: EncodableString = "" @JsonProperty(value = "linePattern", defaultValue = "solid", required = true) @JsonPropertyDescription("Pattern of the lines connecting points on the radar plot") var linePattern: RadarPlotLinePattern = _ @JsonProperty(value = "maxNormalize", defaultValue = "true", required = true) @JsonSchemaTitle("Max Normalize") @JsonPropertyDescription( "Normalize radar plot values by scaling them relative to the maximum value on their respective axes" ) var maxNormalize: Boolean = true @JsonProperty(value = "fillTrace", defaultValue = "true", required = true) @JsonSchemaTitle("Fill Trace") @JsonPropertyDescription("Fill the area within each radar trace") var fillTrace: Boolean = true @JsonProperty(value = "showMarkers", defaultValue = "true", required = true) @JsonSchemaTitle("Show Point Markers") @JsonPropertyDescription("Display point markers on the radar plot") var showMarkers: Boolean = true @JsonProperty(value = "showLegend", defaultValue = "true", required = false) @JsonSchemaTitle("Show Legend") @JsonPropertyDescription( "Display the legend (note: without the legend, you are unable to selectively hide or show traces in the plot)" ) var showLegend: Boolean = true override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Radar Plot", "View the result in a radar plot.", OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP ) private def toPythonBool(value: Boolean): String = if (value) "True" else "False" private def optionalColumnExpr(column: EncodableString): PythonTemplateBuilder = Option(column).filterNot(col => col.isEmpty || col == "No Selection") match { case Some(col) => pyb"$col" case None => pyb"None" } def generateRadarPlotCode(): PythonTemplateBuilder = { val attributes = Option(selectedAttributes).getOrElse(Nil) val attrList = attributes.map(attr => pyb"$attr").mkString(", ") val traceNameCol = optionalColumnExpr(traceNameAttribute) val traceColorCol = optionalColumnExpr(traceColorAttribute) pyb""" | categories = [$attrList] | if not categories: | yield {'html-content': self.render_error("No columns selected as axes.")} | return | | trace_name_col = $traceNameCol | trace_color_col = $traceColorCol | line_pattern = "${linePattern.getLinePattern}" | max_normalize = ${toPythonBool(maxNormalize)} | fill_trace = ${toPythonBool(fillTrace)} | show_markers = ${toPythonBool(showMarkers)} | show_legend = ${toPythonBool(showLegend)} | | selected_table_df = table[categories].astype(float) | selected_table = selected_table_df.values | | trace_names = ( | table[trace_name_col].values if trace_name_col | else np.full(len(table), "", dtype=object) | ) | | trace_colors = [None] * len(table) | if trace_color_col: | unique_vals = table[trace_color_col].unique() | color_map = {val: px.colors.qualitative.Plotly[idx % len(px.colors.qualitative.Plotly)] | for idx, val in enumerate(unique_vals)} | nan_color = '#000000' | trace_colors = table[trace_color_col].map(color_map).fillna(nan_color).values | | hover_texts = [] | for idx, row in enumerate(selected_table): | name_prefix = str(trace_names[idx]) + "
    " if trace_names[idx] else "" | row_hover_texts = [] | for attr, value in zip(categories, row): | row_hover_texts.append(name_prefix + attr + ": " + str(value)) | hover_texts.append(row_hover_texts) | | if max_normalize: | max_vals = selected_table_df.max().values | max_vals[max_vals == 0] = 1 | selected_table = selected_table / max_vals | | selected_table = np.nan_to_num(selected_table) | | fig = go.Figure() | | for idx, row in enumerate(selected_table): | # To connect ensure all points in the radar trace are connected | closed_row = row.tolist() + [row[0]] | closed_categories = categories + [categories[0]] | closed_hover_texts = hover_texts[idx] + [hover_texts[idx][0]] | | fig.add_trace(go.Scatterpolar( | r=closed_row, | theta=closed_categories, | fill='toself' if fill_trace else 'none', | name=str(trace_names[idx]) if trace_names[idx] else "", | text=closed_hover_texts, | hoverinfo="text", | mode="lines+markers" if show_markers else "lines", | line=dict(dash=line_pattern, color=trace_colors[idx] if trace_colors[idx] else None), | marker=dict(color=trace_colors[idx]) if trace_colors[idx] else {} | )) | | fig.update_layout( | polar=dict(radialaxis=dict(visible=True)), | showlegend=show_legend, | width=600, | height=600 | ) |""" } override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * |import numpy as np |import plotly.graph_objects as go |import plotly.express as px |import plotly.io | |class ProcessTableOperator(UDFTableOperator): | | def render_error(self, error_msg): | return '''

    Radar Plot is not available.

    |

    Reason is: {}

    | '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int): | if table.empty: | yield {'html-content': self.render_error("Input table is empty.")} | return | | ${generateRadarPlotCode()} | | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False, config={'responsive': True}) | yield {'html-content': html} |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/rangeSlider/RangeSliderHandleDuplicateFunction.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.rangeSlider; import com.fasterxml.jackson.annotation.JsonValue; // Returns the string representation of the duplicate handling strategy and enables the list selection in property window public enum RangeSliderHandleDuplicateFunction { NOTHING("Nothing"), MEAN("Mean"), SUM("Sum"); private final String duplicateType; RangeSliderHandleDuplicateFunction(String duplicateType) { this.duplicateType = duplicateType; } @JsonValue public String getFunctionType() { return this.duplicateType; } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/rangeSlider/RangeSliderOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.rangeSlider import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder import javax.validation.constraints.NotNull // type constraint: value can only be numeric @JsonSchemaInject(json = """ { "attributeTypeRules": { "value": { "enum": ["integer", "long", "double"] } } } """) class RangeSliderOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "Y-axis", required = true) @JsonSchemaTitle("Y-axis") @JsonPropertyDescription("The name of the column to represent y-axis") @AutofillAttributeName @NotNull(message = "Y-axis cannot be empty") var yAxis: EncodableString = "" @JsonProperty(value = "X-axis", required = true) @JsonSchemaTitle("X-axis") @JsonPropertyDescription("The name of the column to represent the x-axis") @AutofillAttributeName @NotNull(message = "X-axis cannot be empty") var xAxis: EncodableString = "" @JsonProperty(value = "Duplicates", required = false) @JsonSchemaTitle("Handle Duplicates") @JsonPropertyDescription("How to handle duplicate values in y-axis") var duplicateType: RangeSliderHandleDuplicateFunction = RangeSliderHandleDuplicateFunction.NOTHING override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Range Slider", "Visualize data in a Range Slider", OperatorGroupConstants.VISUALIZATION_BASIC_GROUP ) def manipulateTable(): PythonTemplateBuilder = { pyb""" | table = table.dropna(subset=[$xAxis, $yAxis]) | functionType = '${duplicateType.getFunctionType}' | if functionType.lower() == "mean": | table = table.groupby($xAxis)[$yAxis].mean().reset_index() #get mean of values | elif functionType.lower() == "sum": | table = table.groupby($xAxis)[$yAxis].sum().reset_index() #get sum of values |""" } def createPlotlyFigure(): PythonTemplateBuilder = { pyb""" | # Create figure | fig = go.Figure() | | fig.add_trace(go.Scatter(x=table[$xAxis], y=table[$yAxis], mode = "markers+lines")) | | # Add range slider | fig.update_layout( | xaxis_title=$xAxis, | yaxis_title=$yAxis, | xaxis=dict( | rangeslider=dict( | visible=True | ) | ) | ) |""" } override def generatePythonCode(): String = { val finalcode = pyb""" |from pytexera import * | |import plotly.express as px |import plotly.graph_objects as go |import plotly.io |import numpy as np | |class ProcessTableOperator(UDFTableOperator): | def render_error(self, error_msg): | return '''

    RangeChart is not available.

    |

    Reason is: {}

    | '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | original_table = table | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return | if $yAxis.strip() == "" or $xAxis.strip() == "": | yield {'html-content': self.render_error("Y-axis or X-axis is empty")} | return | ${manipulateTable()} | ${createPlotlyFigure()} | # convert fig to html content | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} |""" finalcode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/sankeyDiagram/SankeyDiagramOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.sankeyDiagram import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder import javax.validation.constraints.NotNull class SankeyDiagramOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "Source Attribute", required = true) @JsonSchemaTitle("Source Attribute") @JsonPropertyDescription("The source node of the Sankey diagram") @AutofillAttributeName @NotNull(message = "Source Attribute cannot be empty") var sourceAttribute: EncodableString = "" @JsonProperty(value = "Target Attribute", required = true) @JsonSchemaTitle("Target Attribute") @JsonPropertyDescription("The target node of the Sankey diagram") @AutofillAttributeName @NotNull(message = "Target Attribute cannot be empty") var targetAttribute: EncodableString = "" @JsonProperty(value = "Value Attribute", required = true) @JsonSchemaTitle("Value Attribute") @JsonPropertyDescription("The value/volume of the flow between source and target") @AutofillAttributeName @NotNull(message = "Value Attribute cannot be empty") var valueAttribute: EncodableString = "" override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Sankey Diagram", "Visualize data using a Sankey diagram", OperatorGroupConstants.VISUALIZATION_BASIC_GROUP ) def createPlotlyFigure(): PythonTemplateBuilder = { pyb""" | # Grouping source, target, and summing value for the Sankey diagram | table = table.groupby([$sourceAttribute, $targetAttribute])[$valueAttribute].sum().reset_index(name='value') | | # Create a list of unique labels from both source and target | labels = pd.concat([table[$sourceAttribute], table[$targetAttribute]]).unique().tolist() | | # Create indices for source and target from the label list | table['source_index'] = table[$sourceAttribute].apply(lambda x: labels.index(x)) | table['target_index'] = table[$targetAttribute].apply(lambda x: labels.index(x)) | | # Create the Sankey diagram | fig = go.Figure(data=[go.Sankey( | node=dict( | pad=15, | thickness=20, | line=dict(color="black", width=0.5), | label=labels, | color="blue" | ), | link=dict( | source=table['source_index'].tolist(), | target=table['target_index'].tolist(), | value=table['value'].tolist() | ) | )]) | | fig.update_layout(title_text="Sankey Diagram", font_size=10) |""" } override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * |import plotly.graph_objects as go |import plotly.io |import pandas as pd | |class ProcessTableOperator(UDFTableOperator): | | def render_error(self, error_msg): | return '''

    Sankey Diagram is not available.

    |

    Reasons are: {}

    | '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("Input table is empty.")} | return | ${createPlotlyFigure()} | if table.empty: | yield {'html-content': self.render_error("No valid rows left (every row has at least 1 missing value).")} | return | # convert fig to html content | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/scatter3DChart/Scatter3dChartOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.scatter3DChart import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder @JsonSchemaInject(json = """ { "attributeTypeRules": { "title": "string" } } """) class Scatter3dChartOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "x", required = true) @JsonSchemaTitle("X Column") @JsonPropertyDescription("Data column for the x-axis") @AutofillAttributeName var x: EncodableString = "" @JsonProperty(value = "y", required = true) @JsonSchemaTitle("Y Column") @JsonPropertyDescription("Data column for the y-axis") @AutofillAttributeName var y: EncodableString = "" @JsonProperty(value = "z", required = true) @JsonSchemaTitle("Z Column") @JsonPropertyDescription("Data column for the z-axis") @AutofillAttributeName var z: EncodableString = "" override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Scatter3D Chart", "Visualize data in a Scatter3D Plot", OperatorGroupConstants.VISUALIZATION_ADVANCED_GROUP ) private def createPlotlyFigure(): PythonTemplateBuilder = { assert(x.nonEmpty) assert(y.nonEmpty) assert(z.nonEmpty) pyb""" | fig = go.Figure(data=[go.Scatter3d( | x=table[$x], | y=table[$y], | z=table[$z], | mode='markers', | marker=dict( | size=12, | colorscale='Viridis', | opacity=0.8 | ) | )]) | fig.update_traces(marker=dict(size=5, opacity=0.8)) | fig.update_layout( | scene=dict( | xaxis_title='X:' + $x, | yaxis_title='Y:' + $y, | zaxis_title='Z:' + $z | ), | margin=dict(t=0, b=0, l=0, r=0) | ) |""" } override def generatePythonCode(): String = { val finalcode = pyb""" |from pytexera import * | |import plotly.express as px |import plotly.graph_objects as go |import plotly.io |import pandas as pd |import numpy as np | |class ProcessTableOperator(UDFTableOperator): | def render_error(self, error_msg): | return '''

    Chart is not available.

    |

    Reason is: {}

    | '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return | ${createPlotlyFigure()} | # convert fig to html content | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} | |""" finalcode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/scatterplot/ScatterplotOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.scatterplot import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder import javax.validation.constraints.NotNull @JsonSchemaInject( json = "{" + " \"attributeTypeRules\": {" + " \"xColumn\":{" + " \"enum\": [\"integer\", \"double\"]" + " }," + " \"yColumn\":{" + " \"enum\": [\"integer\", \"double\"]" + " }" + " }" + "}" ) class ScatterplotOpDesc extends PythonOperatorDescriptor { @JsonProperty(required = true) @JsonSchemaTitle("X-Column") @JsonPropertyDescription("X Column") @AutofillAttributeName @NotNull(message = "X-Column cannot be null") private val xColumn: EncodableString = "" @JsonProperty(required = true) @JsonSchemaTitle("Y-Column") @JsonPropertyDescription("Y Column") @AutofillAttributeName @NotNull(message = "Y-Column cannot be null") private val yColumn: EncodableString = "" @JsonProperty(required = false) @JsonSchemaTitle("Alpha Value") @JsonPropertyDescription("Alpha (opacity) value from 0.0 (transparent) to 1.0 (opaque)") @JsonSchemaInject(json = """{ "minimum": 0.0, "maximum": 1.0, "default": 1.0 }""") private val alpha: Double = 1.0 @JsonProperty(required = false) @JsonSchemaTitle("Color-Column") @JsonPropertyDescription( "Dots will be assigned different colors based on their values of this column" ) @AutofillAttributeName private val colorColumn: EncodableString = "" @JsonProperty(required = false, defaultValue = "false") @JsonSchemaTitle("log scale X") @JsonPropertyDescription("Values in X-column is log-scaled") var xLogScale: Boolean = false @JsonProperty(required = false, defaultValue = "false") @JsonSchemaTitle("log scale Y") @JsonPropertyDescription("Values in Y-column is log-scaled") var yLogScale: Boolean = false @JsonProperty(required = false) @JsonSchemaTitle("Hover column") @JsonPropertyDescription("Column value to display when a dot is hovered over") @AutofillAttributeName var hoverName: EncodableString = "" override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Scatter Plot", "View the result in a scatterplot", OperatorGroupConstants.VISUALIZATION_BASIC_GROUP ) def manipulateTable(): PythonTemplateBuilder = { assert(xColumn.nonEmpty && yColumn.nonEmpty) val colorColExpr = if (colorColumn.nonEmpty) { pyb"$colorColumn" } else { pyb"" } pyb""" | # drops rows with missing values pertaining to relevant columns | table.dropna(subset=[$xColumn, $yColumn, $colorColExpr], inplace = True) | |""" } def createPlotlyFigure(): PythonTemplateBuilder = { assert(xColumn.nonEmpty && yColumn.nonEmpty) val args = scala.collection.mutable.ArrayBuffer( pyb"x=$xColumn", pyb"y=$yColumn", pyb"opacity=$alpha" ) if (colorColumn.nonEmpty) args += pyb"color=$colorColumn" if (xLogScale) args += pyb"log_x=True" if (yLogScale) args += pyb"log_y=True" if (hoverName.nonEmpty) args += pyb"hover_name=$hoverName" val joined = args.mkString(", ") pyb""" | fig = go.Figure(px.scatter(table, $joined)) | fig.update_layout(margin=dict(l=0, r=0, t=0, b=0)) |""" } override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * | |import plotly.express as px |import plotly.graph_objects as go |import plotly.io |import numpy as np | | |class ProcessTableOperator(UDFTableOperator): | | def render_error(self, error_msg): | return '''

    Scatter Plot is not available.

    |

    Reasons are: {}

    | '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("Input table is empty.")} | return | ${manipulateTable()} | ${createPlotlyFigure()} | if table.empty: | yield {'html-content': self.render_error("No valid rows left (every row has at least 1 missing value).")} | return | html = plotly.io.to_html(fig, include_plotlyjs = 'cdn', auto_play = False) | yield {'html-content':html} |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/stripChart/StripChartOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.stripChart import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} class StripChartOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "x", required = true) @JsonSchemaTitle("X-Axis Column") @JsonPropertyDescription("Column containing numeric values for the x-axis") @AutofillAttributeName var x: EncodableString = "" @JsonProperty(value = "y", required = true) @JsonSchemaTitle("Y-Axis Column") @JsonPropertyDescription("Column containing categorical values for the y-axis") @AutofillAttributeName var y: EncodableString = "" @JsonProperty(value = "colorBy", required = false) @JsonSchemaTitle("Color By") @JsonPropertyDescription("Optional - Color points by category") @AutofillAttributeName var colorBy: EncodableString = "" @JsonProperty(value = "facetColumn", required = false) @JsonSchemaTitle("Facet Column") @JsonPropertyDescription("Optional - Create separate subplots for each category") @AutofillAttributeName var facetColumn: EncodableString = "" override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Strip Chart", "Visualize distribution of data points as a strip plot", OperatorGroupConstants.VISUALIZATION_STATISTICAL_GROUP ) override def generatePythonCode(): String = { val colorByParam = if (colorBy != null && colorBy.nonEmpty) pyb", color=$colorBy" else "" val facetColParam = if (facetColumn != null && facetColumn.nonEmpty) pyb", facet_col=$facetColumn" else "" pyb"""from pytexera import * |import plotly.express as px |import plotly.io as pio | |class ProcessTableOperator(UDFTableOperator): | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | x_values = table[$x] | y_values = table[$y] | | # Create data dictionary | data = {$x: x_values, $y: y_values} | | # Add optional color column if specified | if $colorBy: | data[$colorBy] = table[$colorBy] | | # Add optional facet column if specified | if $facetColumn: | data[$facetColumn] = table[$facetColumn] | | # Create strip chart | fig = px.strip( | data, | x=$x, | y=$y$colorByParam$facetColParam | ) | | # Update layout for better visualization | fig.update_traces(marker=dict(size=8, line=dict(width=0.5, color='DarkSlateGrey'))) | fig.update_layout( | xaxis_title=$x, | yaxis_title=$y, | hovermode='closest' | ) | | # Convert to HTML | html = pio.to_html(fig, include_plotlyjs='cdn', full_html=False) | yield {'html-content': html} |""".encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/tablesChart/TablesConfig.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.tablesChart import com.fasterxml.jackson.annotation.JsonProperty import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import javax.validation.constraints.NotNull class TablesConfig { @JsonProperty(required = true) @JsonSchemaTitle("Attribute Name") @AutofillAttributeName @NotNull(message = "Attribute Name cannot be empty") var attributeName: EncodableString = "" } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/tablesChart/TablesPlotOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.tablesChart import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder import javax.validation.constraints.NotEmpty class TablesPlotOpDesc extends PythonOperatorDescriptor { @JsonPropertyDescription("List of columns to include in the table chart") @JsonProperty(value = "add attribute", required = true) @NotEmpty(message = "Included columns list cannot be empty") var includedColumns: List[TablesConfig] = List() private def getAttributes: String = includedColumns.map(c => pyb"""${c.attributeName}""").mkString("','") def manipulateTable(): PythonTemplateBuilder = { assert(includedColumns.nonEmpty) val attributes = getAttributes pyb""" | # drops rows with missing values pertaining to relevant columns | table = table.dropna(subset=[$attributes]) | |""" } def createPlotlyFigure(): PythonTemplateBuilder = { assert(includedColumns.nonEmpty) val attributes = getAttributes pyb""" | | filtered_table = table[[$attributes]] | headers = filtered_table.columns.tolist() | cell_values = [filtered_table[col].tolist() for col in headers] | | fig = go.Figure(data=[go.Table( | header=dict(values=headers), | cells=dict(values=cell_values) | )]) | | |""" } override def generatePythonCode(): String = { pyb""" |from pytexera import * |import plotly.graph_objects as go |import plotly.io |class TableChartOperator(UDFTableOperator): | | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return | | ${manipulateTable()} | | if table.empty: | yield {'html-content': self.render_error("value column contains only non-positive numbers or nulls.")} | return | | ${createPlotlyFigure()} | fig.update_layout(margin=dict(l=0, r=0, b=0, t=0)) | html_content = plotly.io.to_html(fig, include_plotlyjs='cdn') | yield {'html-content': html_content} """.encode } override def operatorInfo: OperatorInfo = { OperatorInfo.forVisualization( "Tables Plot", "Visualize data in a table chart.", OperatorGroupConstants.VISUALIZATION_BASIC_GROUP ) } override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ternaryContour/TernaryContourOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.ternaryContour import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.core.workflow.OutputPort.OutputMode import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity} import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder /** * Visualization Operator for Ternary Plots. * * This operator uses three data fields to construct a ternary plot. * The points can optionally be color coded using a data field. */ class TernaryContourOpDesc extends PythonOperatorDescriptor { // Add annotations for the first variable @JsonProperty(value = "firstVariable", required = true) @JsonSchemaTitle("Variable 1") @JsonPropertyDescription("First variable data field") @AutofillAttributeName var firstVariable: EncodableString = "" // Add annotations for the second variable @JsonProperty(value = "secondVariable", required = true) @JsonSchemaTitle("Variable 2") @JsonPropertyDescription("Second variable data field") @AutofillAttributeName var secondVariable: EncodableString = "" // Add annotations for the third variable @JsonProperty(value = "thirdVariable", required = true) @JsonSchemaTitle("Variable 3") @JsonPropertyDescription("Third variable data field") @AutofillAttributeName var thirdVariable: EncodableString = "" // Add annotations for the fourth variable @JsonProperty(value = "fourthVariable", required = true) @JsonSchemaTitle("Measured Value") @JsonPropertyDescription("Measured value data field") @AutofillAttributeName var fourthVariable: EncodableString = "" // OperatorInfo instance describing ternary plot override def operatorInfo: OperatorInfo = OperatorInfo( userFriendlyName = "Ternary Contour", operatorDescription = "Shows how a measured value changes across all mixtures of three components that sum to a constant", operatorGroupName = OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort(mode = OutputMode.SINGLE_SNAPSHOT)) ) override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } /** Returns a Python string that drops any tuples with missing values */ def manipulateTable(): PythonTemplateBuilder = { // Check for any empty data field names assert( firstVariable.nonEmpty && secondVariable.nonEmpty && thirdVariable.nonEmpty && fourthVariable.nonEmpty ) pyb""" | # Remove any tuples that contain missing values | table.dropna(subset=[$firstVariable, $secondVariable, $thirdVariable, $fourthVariable], inplace = True) | | #Remove rows where any of the first three variables are negative | table = table[(table[[$firstVariable, $secondVariable, $thirdVariable]] >= 0).all(axis=1)] | | #Remove zero-sum rows | s = table[$firstVariable] + table[$secondVariable] + table[$thirdVariable] | table = table[s > 0] |""" } /** Returns a Python string that creates the ternary contour plot figure */ def createPlotlyFigure(): PythonTemplateBuilder = { pyb""" | A = table[$firstVariable].to_numpy() | B = table[$secondVariable].to_numpy() | C = table[$thirdVariable].to_numpy() | Z = table[$fourthVariable].to_numpy() | fig = ff.create_ternary_contour(np.array([A,B,C]), Z, pole_labels=[$firstVariable, $secondVariable, $thirdVariable], interp_mode='cartesian') |""" } /** Returns a Python string that yields the html content of the ternary contour plot */ override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * | |import plotly.io |import plotly.figure_factory as ff |import numpy as np | |class ProcessTableOperator(UDFTableOperator): | | # Generate custom error message as html string | def render_error(self, error_msg): | return '''

    TernaryContour is not available.

    |

    Reasons are: {}

    | '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("Input table is empty.")} | return | ${manipulateTable()} | if table.empty: | yield {'html-content': self.render_error("No valid rows left (every row has at least 1 missing value).")} | return | ${createPlotlyFigure()} | # Convert fig to html content | html = plotly.io.to_html(fig, include_plotlyjs = 'cdn', auto_play = False) | yield {'html-content':html} |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ternaryPlot/TernaryPlotOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.ternaryPlot import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder /** * Visualization Operator for Ternary Plots. * * This operator uses three data fields to construct a ternary plot. * The points can optionally be color coded using a data field. */ class TernaryPlotOpDesc extends PythonOperatorDescriptor { // Add annotations for the first variable @JsonProperty(value = "firstVariable", required = true) @JsonSchemaTitle("Variable 1") @JsonPropertyDescription("First variable data field") @AutofillAttributeName var firstVariable: EncodableString = "" // Add annotations for the second variable @JsonProperty(value = "secondVariable", required = true) @JsonSchemaTitle("Variable 2") @JsonPropertyDescription("Second variable data field") @AutofillAttributeName var secondVariable: EncodableString = "" // Add annotations for the third variable @JsonProperty(value = "thirdVariable", required = true) @JsonSchemaTitle("Variable 3") @JsonPropertyDescription("Third variable data field") @AutofillAttributeName var thirdVariable: EncodableString = "" // Add annotations for enabling color and selecting its associated data field @JsonProperty(value = "colorEnabled", defaultValue = "false") @JsonSchemaTitle("Categorize by Color") @JsonPropertyDescription("Optionally color points using a data field") var colorEnabled: Boolean = false @JsonProperty(value = "colorDataField", required = false) @JsonSchemaTitle("Color Data Field") @JsonPropertyDescription("Specify the data field to color") @AutofillAttributeName var colorDataField: EncodableString = "" // OperatorInfo instance describing ternary plot override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( userFriendlyName = "Ternary Plot", operatorDescription = "Points are graphed on a Ternary Plot using 3 specified data fields", operatorGroupName = OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP ) override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } /** Returns a Python string that drops any tuples with missing values */ def manipulateTable(): PythonTemplateBuilder = { // Check for any empty data field names assert(firstVariable.nonEmpty && secondVariable.nonEmpty && thirdVariable.nonEmpty) pyb""" | # Remove any tuples that contain missing values | table.dropna(subset=[$firstVariable, $secondVariable, $thirdVariable], inplace = True) |""" } /** Returns a Python string that creates the ternary plot figure */ def createPlotlyFigure(): PythonTemplateBuilder = { pyb""" | if '$colorEnabled' == 'true' and $colorDataField != "": | fig = px.scatter_ternary(table, a=$firstVariable, b=$secondVariable, c=$thirdVariable, color=$colorDataField) | else: | fig = px.scatter_ternary(table, a=$firstVariable, b=$secondVariable, c=$thirdVariable) |""" } /** Returns a Python string that yields the html content of the ternary plot */ override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * | |import plotly.express as px |import plotly.io | |class ProcessTableOperator(UDFTableOperator): | | # Generate custom error message as html string | def render_error(self, error_msg): | return '''

    TernaryPlot is not available.

    |

    Reasons are: {}

    | '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("Input table is empty.")} | return | ${manipulateTable()} | if table.empty: | yield {'html-content': self.render_error("No valid rows left (every row has at least 1 missing value).")} | return | ${createPlotlyFigure()} | # Convert fig to html content | html = plotly.io.to_html(fig, include_plotlyjs = 'cdn', auto_play = False) | yield {'html-content':html} |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/timeSeriesplot/TimeSeriesOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.timeSeriesplot import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import javax.validation.constraints.{NotBlank, NotNull} class TimeSeriesOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "timeColumn", required = true) @JsonSchemaTitle("Time Column") @JsonPropertyDescription("The column containing time/date values (e.g., Date, Timestamp).") @AutofillAttributeName @NotNull(message = "Time Column cannot be empty") var timeColumn: EncodableString = "" @JsonProperty(value = "valueColumn", required = true) @JsonSchemaTitle("Value Column") @JsonPropertyDescription("The numerical column to plot on the Y-axis (e.g., Sales, Temperature).") @JsonSchemaInject(json = """{"enum": "autofill"}""") @AutofillAttributeName @NotNull(message = "Value Column cannot be empty") var valueColumn: EncodableString = "" @JsonProperty(value = "categoryColumn", required = false, defaultValue = "No Selection") @JsonSchemaTitle("Category Column") @JsonPropertyDescription("Optional - A categorical column to create separate lines.") @AutofillAttributeName var CategoryColumn: EncodableString = "No Selection" @JsonProperty(value = "facetColumn", required = false, defaultValue = "No Selection") @JsonSchemaTitle("Facet Column") @JsonPropertyDescription("Optional - A column to create separate subplots.") @AutofillAttributeName var facetColumn: EncodableString = "No Selection" @JsonProperty(value = "line", defaultValue = "line", required = true) @JsonSchemaTitle("Plot Type") @JsonPropertyDescription("Select the type of time series plot (line, area).") @NotBlank(message = "Plot Type cannot be empty") var plotType: String = "line" @JsonProperty(value = "slider", defaultValue = "false") @JsonSchemaTitle("Show Range Slider") @JsonPropertyDescription("Display a range slider at the bottom of the plot.") var showRangeSlider: Boolean = _ override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema().add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Time Series Plot", "Visualize trends and patterns over time.", OperatorGroupConstants.VISUALIZATION_BASIC_GROUP ) override def generatePythonCode(): String = { val dropnaCols = List(timeColumn, valueColumn) ++ (if (CategoryColumn != "No Selection") Some(CategoryColumn) else None) ++ (if (facetColumn != "No Selection") Some(facetColumn) else None) val dropnaStr = dropnaCols.map(c => pyb"$c").mkString("[", ", ", "]") val colorArg = if (CategoryColumn != "No Selection") pyb", color=$CategoryColumn" else "" val facetArg = if (facetColumn != "No Selection") pyb", facet_col=$facetColumn" else "" val plotFunc = if (plotType == "area") "px.area" else "px.line" val showSlider = if (showRangeSlider) "True" else "False" pyb""" |from pytexera import * |import plotly.express as px |import plotly.io |import pandas as pd | |class ProcessTableOperator(UDFTableOperator): | | def render_error(self, msg) -> str: | return f"

    Time Series Plot is not available.

    Reason: {msg}

    " | | @overrides | def process_table(self, table: Table, port: int): | if table.empty: | yield {'html-content': self.render_error("Input table is empty.")} | return | | try: | table[$timeColumn] = pd.to_datetime(table[$timeColumn], errors='coerce') | table = table.dropna(subset=$dropnaStr).sort_values(by=$timeColumn) | | if table.empty: | yield {'html-content': self.render_error("Table became empty after filtering.")} | return | | fig = $plotFunc(table, x=$timeColumn, y=$valueColumn$colorArg$facetArg) | | if $showSlider: | fig.update_xaxes(rangeslider_visible=True) | | fig.update_layout( | margin=dict(l=0, r=0, t=30, b=0), | title=dict(text="Time Series Plot", x=0.5), | xaxis_title=$timeColumn, | yaxis_title=$valueColumn, | template="plotly_white" | ) | | html = plotly.io.to_html(fig, include_plotlyjs='cdn', full_html=False) | yield {'html-content': html} | | except Exception as e: | yield {'html-content': self.render_error(str(e))} |""".encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/treeplot/TreePlotOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.treeplot import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} /** * Visualization Operator for Tree Plots. * * This operator uses a single column containing parent-child pairs * to construct and visualize an interactive, top-down tree that automatically * sizes itself and supports intuitive scroll/pinch zooming. */ class TreePlotOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "Edge List Column", required = true) @JsonSchemaTitle("Edge List Column") @JsonPropertyDescription("Column with [parent, child] pairs") @AutofillAttributeName var edgeListColumn: EncodableString = "" override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( userFriendlyName = "Tree Plot", operatorDescription = "Visualize hierarchical data as a top-down, interactive, auto-sizing tree", operatorGroupName = OperatorGroupConstants.VISUALIZATION_STATISTICAL_GROUP ) override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { Map( operatorInfo.outputPorts.head.id -> Schema() .add("html-content", AttributeType.STRING) ) } override def generatePythonCode(): String = { assert(edgeListColumn.nonEmpty) pyb""" |from pytexera import * | |import plotly.graph_objects as go |import plotly.io |import igraph |from igraph import Graph, EdgeSeq |import pandas as pd |import ast | |class ProcessTableOperator(UDFTableOperator): | | def render_error(self, error_msg): | return f'''

    Tree Plot is not available.

    |

    Reason: {error_msg}

    ''' | | def make_annotations(self, pos, text): | font_color = 'rgb(250,250,250)' | node_color = '#6175c1' | font_size = 10 | | annotations = [] | for k, (node_name, coords) in enumerate(pos.items()): | annotations.append( | dict( | text=text[k], | x=coords[0], | y=coords[1], | xref='x1', yref='y1', | font=dict(color=font_color, size=font_size), | showarrow=False, | align='center', | bordercolor='rgb(50,50,50)', | borderwidth=1, | borderpad=5, | bgcolor=node_color, | opacity=0.8 | ) | ) | return annotations | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("Input table is empty.")} | return | | edges = [] | for item in table[$edgeListColumn].dropna(): | try: | edge = ast.literal_eval(str(item)) | if isinstance(edge, (list, tuple)) and len(edge) == 2: | edges.append(list(edge)) | except (ValueError, SyntaxError): | pass | | if not edges: | yield {'html-content': self.render_error("No valid [parent, child] pairs found in column " + $edgeListColumn + ".")} | return | | G = Graph.TupleList(edges, directed=True) | labels = G.vs['name'] | | layout_algorithm = 'rt' | try: | lay = G.layout(layout_algorithm) | except Exception as e: | yield {'html-content': self.render_error(f"Layout algorithm '{layout_algorithm}' failed: {e}")} | return | | HORIZONTAL_DENSITY = 120 | VERTICAL_DENSITY = 120 | PADDING = 200 | MIN_WIDTH = 800 | MIN_HEIGHT = 600 | | if len(lay.coords) > 1: | x_coords, y_coords = zip(*lay.coords) | x_range = max(x_coords) - min(x_coords) | y_range = max(y_coords) - min(y_coords) | plot_width = max(MIN_WIDTH, x_range * HORIZONTAL_DENSITY + PADDING) | plot_height = max(MIN_HEIGHT, y_range * VERTICAL_DENSITY + PADDING) | else: | plot_width = MIN_WIDTH | plot_height = MIN_HEIGHT | | # Invert the y-axis to make the tree grow top-down. | position = {k: (lay[k][0], -lay[k][1]) for k in range(len(labels))} | | Xe = [] | Ye = [] | for edge in G.get_edgelist(): | Xe += [position[edge[0]][0], position[edge[1]][0], None] | Ye += [position[edge[0]][1], position[edge[1]][1], None] | | fig = go.Figure() | | fig.add_trace(go.Scatter(x=Xe, y=Ye, mode='lines', | line=dict(color='rgb(210,210,210)', width=1), | hoverinfo='none')) | | axis = dict(showline=False, zeroline=False, showgrid=False, showticklabels=False) | | fig.update_layout(title='Tree Plot', | width=int(plot_width), | height=int(plot_height), | annotations=self.make_annotations(position, labels), | font_size=12, | showlegend=False, | xaxis=axis, | yaxis=axis, | margin=dict(l=40, r=40, b=85, t=100), | dragmode='pan', | hovermode='closest', | plot_bgcolor='rgb(248,248,248)') | | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} | |""".encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/urlviz/UrlVizOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.urlviz import com.fasterxml.jackson.annotation.JsonProperty import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{PhysicalOp, SchemaPropagationFunc} import org.apache.texera.amber.operator.LogicalOp import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.util.JSONUtils.objectMapper /** * URL Visualization operator to render any content in given URL link * This is the description of the operator */ @JsonSchemaInject(json = """ { "attributeTypeRules": { "urlContentAttrName": { "enum": ["string"] } } } """) class UrlVizOpDesc extends LogicalOp { @JsonProperty(required = true) @JsonSchemaTitle("URL content") @AutofillAttributeName val urlContentAttrName: String = "" override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity ): PhysicalOp = { PhysicalOp .manyToOnePhysicalOp( workflowId, executionId, operatorIdentifier, OpExecWithClassName( "org.apache.texera.amber.operator.visualization.urlviz.UrlVizOpExec", objectMapper.writeValueAsString(this) ) ) .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPropagateSchema( SchemaPropagationFunc(_ => { val outputSchema = Schema().add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) }) ) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "URL Visualizer", "Render the content of URL", OperatorGroupConstants.VISUALIZATION_MEDIA_GROUP ) } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/urlviz/UrlVizOpExec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.urlviz import org.apache.texera.amber.core.executor.OperatorExecutor import org.apache.texera.amber.core.tuple.{Tuple, TupleLike} import org.apache.texera.amber.util.JSONUtils.objectMapper /** * URL Visualization operator to render any given URL link */ class UrlVizOpExec(descString: String) extends OperatorExecutor { private val desc: UrlVizOpDesc = objectMapper.readValue(descString, classOf[UrlVizOpDesc]) override def processTuple(tuple: Tuple, port: Int): Iterator[TupleLike] = { val iframe = s""" | | | | |""".stripMargin Iterator(TupleLike(iframe)) } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/volcanoPlot/VolcanoPlotOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.volcanoPlot import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} class VolcanoPlotOpDesc extends PythonOperatorDescriptor { @JsonProperty(required = true) @JsonSchemaTitle("Effect Size (log2 Fold Change)") @JsonPropertyDescription( "Select the column representing the effect size or magnitude " + "of change between two experimental groups. This value is typically a log2 fold change " + "and is used for the x-axis of the volcano plot." ) @AutofillAttributeName var effectColumn: EncodableString = "" @JsonProperty(required = true) @JsonSchemaTitle("P-Value Column") @JsonPropertyDescription( "Select the column representing the p-value associated with the " + "statistical test for each feature. This value is transformed using -log10(p-value) and " + "plotted on the y-axis to indicate statistical significance." ) @AutofillAttributeName var pvalueColumn: EncodableString = "" override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( userFriendlyName = "Volcano Plot", operatorDescription = "Displays statistical significance versus effect size", operatorGroupName = OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP ) override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def generatePythonCode(): String = { pyb""" |from pytexera import * |import plotly.express as px |import plotly.io |import numpy as np | |class ProcessTableOperator(UDFTableOperator): | | def render_error(self, msg): | return f"

    Volcano Plot failed

    {msg}

    " | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {"html-content": self.render_error("Input table is empty.")} | return | | if $pvalueColumn not in table.columns or $effectColumn not in table.columns: | yield {"html-content": self.render_error("Missing required columns in table.")} | return | | # Filter out non-positive p-values to avoid math errors | table = table[table[$pvalueColumn] > 0] | if table.empty: | yield {"html-content": self.render_error("No rows with valid p-values.")} | return | | table["-log10(pvalue)"] = -np.log10(table[$pvalueColumn]) | | fig = px.scatter( | table, | x=$effectColumn, | y="-log10(pvalue)", | hover_name=table.columns[0], | color=$effectColumn, | color_continuous_scale="RdBu", | title="Volcano Plot" | ) | | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {"html-content": html} |""".encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/waterfallChart/WaterfallChartOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.waterfallChart import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder class WaterfallChartOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "xColumn", required = true) @JsonSchemaTitle("X Axis Values") @JsonPropertyDescription("The column representing categories or stages") @AutofillAttributeName var xColumn: EncodableString = _ @JsonProperty(value = "yColumn", required = true) @JsonSchemaTitle("Y Axis Values") @JsonPropertyDescription("The column representing numeric values for each stage") @AutofillAttributeName var yColumn: EncodableString = _ override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Waterfall Chart", "Visualize data as a waterfall chart", OperatorGroupConstants.VISUALIZATION_FINANCIAL_GROUP ) def createPlotlyFigure(): PythonTemplateBuilder = { pyb""" | x_values = table[$xColumn] | y_values = table[$yColumn] | | fig = go.Figure(go.Waterfall( | name="Waterfall", orientation="v", | measure=["relative"] * (len(y_values) - 1) + ["total"], | x=x_values, | y=y_values, | textposition="outside", | text=[f"{v:+}" for v in y_values], | connector={"line": {"color": "rgb(63, 63, 63)"}} | )) | | fig.update_layout(showlegend=True, waterfallgap=0.3) |""" } override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * | |import plotly.graph_objects as go |import plotly.io | |class ProcessTableOperator(UDFTableOperator): | | # Generate custom error message as html string | def render_error(self, error_msg) -> str: | return '''

    Waterfall chart is not available.

    |

    Reason is: {}

    | '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return | ${createPlotlyFigure()} | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/windRoseChart/WindRoseChartOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.windRoseChart import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.core.workflow.OutputPort.OutputMode import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity} import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder import javax.validation.constraints.NotNull class WindRoseChartOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "rColumn", required = true) @JsonSchemaTitle("Radial Values (r)") @JsonPropertyDescription("Numeric values representing magnitude (e.g., frequency)") @AutofillAttributeName @NotNull(message = "Radial Values (r) column must be selected.") var rColumn: EncodableString = _ @JsonProperty(value = "thetaColumn", required = true) @JsonSchemaTitle("Angular Values (θ)") @JsonPropertyDescription("Direction or angle categories (e.g., N, NE, E)") @AutofillAttributeName @NotNull(message = "Angular Values (θ) column must be selected.") var thetaColumn: EncodableString = _ @JsonProperty(value = "colorColumn", required = false) @JsonSchemaTitle("Color Group") @JsonPropertyDescription("Optional grouping column (e.g., wind strength)") @AutofillAttributeName var colorColumn: EncodableString = _ override def operatorInfo: OperatorInfo = OperatorInfo( userFriendlyName = "Wind Rose Chart", operatorDescription = "Displays wind distribution using a polar bar chart", operatorGroupName = OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort(mode = OutputMode.SINGLE_SNAPSHOT)) ) override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } def createPlotlyFigure(): PythonTemplateBuilder = { val colorArg = if (colorColumn != null && colorColumn.nonEmpty) pyb""" | color=$colorColumn, |""" else pyb"" pyb""" | fig = px.bar_polar( | table, | r=$rColumn, | theta=$thetaColumn, |$colorArg | color_discrete_sequence=px.colors.sequential.Plasma_r | ) |""" } override def generatePythonCode(): String = { val finalCode = pyb""" |from pytexera import * | |import plotly.graph_objects as go |import plotly.io |import plotly.express as px | |class ProcessTableOperator(UDFTableOperator): | | # Generate custom error message as html string | def render_error(self, error_msg) -> str: | return '''

    Wind Rose chart is not available.

    |

    Reason is: {}

    | '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return | if table[$rColumn].dtype.kind not in ["i", "u", "f"]: | yield {'html-content': self.render_error( | "Radial column must be numeric (int, float, or double)." | )} | return | ${createPlotlyFigure()} | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html} |""" finalCode.encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/wordCloud/WordCloudOpDesc.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.wordCloud import com.fasterxml.jackson.annotation.JsonProperty import com.kjetland.jackson.jsonSchema.annotations.{ JsonSchemaInject, JsonSchemaInt, JsonSchemaTitle } import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.operator.visualization.ImageUtility import org.apache.texera.amber.pybuilder.PythonTemplateBuilder class WordCloudOpDesc extends PythonOperatorDescriptor { @JsonProperty(required = true) @JsonSchemaTitle("Text column") @AutofillAttributeName var textColumn: EncodableString = "" @JsonProperty(defaultValue = "100") @JsonSchemaTitle("Number of most frequent words") @JsonSchemaInject(ints = Array(new JsonSchemaInt(path = "exclusiveMinimum", value = 0))) var topN: Integer = 100 override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = OperatorInfo.forVisualization( "Word Cloud", "Generate word cloud for texts", OperatorGroupConstants.VISUALIZATION_MEDIA_GROUP ) def manipulateTable(): PythonTemplateBuilder = { pyb""" | table.dropna(subset = [$textColumn], inplace = True) #remove missing values | table = table[table[$textColumn].str.contains(r'\w', regex=True)] |""" } def createWordCloudFigure(): PythonTemplateBuilder = { pyb""" | text = ' '.join(table[$textColumn]) | | # Generate an image in a FHD resolution | from wordcloud import WordCloud, STOPWORDS | wordcloud = WordCloud(width=1920, height=1080, stopwords=set(STOPWORDS), max_words=$topN, background_color='white', include_numbers=True).generate(text) | | from io import BytesIO | image_stream = BytesIO() | wordcloud.to_image().save(image_stream, format='PNG') | binary_image_data = image_stream.getvalue() |""" } override def generatePythonCode(): String = { pyb""" |from pytexera import * | |class ProcessTableOperator(UDFTableOperator): | | # Generate custom error message as html string | def render_error(self, error_msg) -> str: | return '''

    Wordcloud is not available.

    |

    Reason is: {}

    | '''.format(error_msg) | | @overrides | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return | ${manipulateTable()} | if table.empty: | yield {'html-content': self.render_error("text column does not contain words or contains only nulls.")} | return | ${createWordCloudFigure()} | ${ImageUtility.encodeImageToHTML()} | yield {'html-content': html} |""".encode } } ================================================ FILE: common/workflow-operator/src/main/scala/org/apache/texera/amber/util/ObjectMapperUtils.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.amber.util import org.apache.texera.amber.operator.metadata.OperatorMetadataGenerator object ObjectMapperUtils { /** * Explicitly start a thread to let the objectMapper load all logical operator classes for serde/deserde. * * This call prevents the initial delay of serialization & deserialization in other application logics. */ def warmupObjectMapperForOperatorsSerde(): Unit = { val thread = new Thread( new Runnable { override def run(): Unit = { OperatorMetadataGenerator.generateAllOperatorMetadata() } }, "ObjectMapperWarmupForOperatorsThread" ) thread.start() } } ================================================ FILE: common/workflow-operator/src/test/resources/100.jsonl ================================================ {"id":186199912341,"first_name":"Costanza","last_name":"Hawyes","email":"chawyes0@ameblo.jp","gender":"Genderfluid","ip_address":"175.145.10.21","flagged":true,"year":1995,"created_at":"2005-12-10T03:46:36Z"} {"id":791955784098,"first_name":"Richie","last_name":"Yanyushkin","email":"ryanyushkin1@ucoz.ru","gender":"Bigender","ip_address":"206.231.183.126","flagged":false,"year":2000,"created_at":"2010-03-30T00:49:39Z"} {"id":988731692969,"first_name":"Martin","last_name":"Karpf","email":"mkarpf2@cnet.com","gender":"Male","ip_address":"205.107.213.118","flagged":false,"year":2007,"created_at":"2013-10-26T23:05:48Z"} {"id":1081558226892,"first_name":"Elizabeth","last_name":"Selkirk","email":"eselkirk3@mashable.com","gender":"Male","flagged":true,"year":2008,"created_at":"2001-07-27T21:49:54Z"} {"id":897813868540,"first_name":"Frank","last_name":"Georgeon","email":"fgeorgeon4@printfriendly.com","gender":"Bigender","flagged":false,"year":2006,"created_at":"2019-04-20T23:18:23Z"} {"id":1010304209635,"first_name":"Genni","last_name":"MacLese","email":"gmaclese5@illinois.edu","gender":"Male","ip_address":"71.221.255.236","flagged":false,"year":1995,"created_at":"2005-03-14T07:54:27Z"} {"id":513093768109,"first_name":"Ursa","last_name":"Daburn","email":"udaburn6@paypal.com","gender":"Genderqueer","ip_address":"6.102.149.82","flagged":true,"year":1995,"created_at":"2017-10-26T07:46:52Z"} {"id":1126296922615,"first_name":"Whit","last_name":"Lippo","email":"wlippo7@psu.edu","gender":"Male","flagged":true,"year":1998,"created_at":"2011-06-15T12:33:03Z"} {"id":362785897296,"first_name":"Hersh","email":"hmccuish8@ebay.co.uk","gender":"Agender","ip_address":"231.225.20.129","flagged":true,"year":2008,"created_at":"2014-05-05T00:49:25Z"} {"id":990564202666,"first_name":"Nessi","last_name":"Roan","email":"nroan9@squarespace.com","gender":"Agender","flagged":true,"year":2003,"created_at":"2020-06-27T05:58:11Z"} {"id":1047115072081,"first_name":"Nobie","email":"nhennemanna@g.co","gender":"Bigender","flagged":true,"year":1986,"created_at":"2008-07-08T15:59:43Z"} {"id":844522669072,"first_name":"Debra","last_name":"Skim","email":"dskimb@apache.org","gender":"Polygender","flagged":true,"year":2010,"created_at":"2021-04-03T03:19:23Z"} {"id":1213494768337,"first_name":"Paulie","last_name":"Boland","email":"pbolandc@nba.com","gender":"Female","flagged":true,"year":2005,"created_at":"2003-01-06T08:18:15Z"} {"id":666582696758,"first_name":"Agustin","last_name":"McElree","email":"amcelreed@cnet.com","gender":"Genderfluid","ip_address":"72.43.212.69","flagged":true,"year":1999,"created_at":"2016-06-18T14:04:50Z"} {"id":226782116420,"first_name":"Dilan","last_name":"Ewbank","email":"dewbanke@arizona.edu","gender":"Non-binary","ip_address":"227.178.81.147","flagged":false,"year":1996,"created_at":"2003-03-21T00:18:51Z"} {"id":206918707128,"first_name":"Caresa","last_name":"Amberger","email":"cambergerf@arizona.edu","gender":"Genderfluid","ip_address":"16.52.243.119","flagged":true,"year":2001,"created_at":"2008-01-29T23:12:42Z"} {"id":999526808076,"first_name":"Ancell","last_name":"Cadagan","email":"acadagang@digg.com","gender":"Genderfluid","flagged":true,"year":1994,"created_at":"2002-12-26T15:16:03Z"} {"id":1091458471557,"first_name":"Dulciana","email":"dmcwhinh@hp.com","gender":"Female","ip_address":"171.167.27.57","flagged":false,"year":2005,"created_at":"2003-07-11T19:17:58Z"} {"id":804833580549,"first_name":"Adler","last_name":"McPhilip","email":"amcphilipi@jimdo.com","gender":"Bigender","ip_address":"8.153.243.223","flagged":false,"year":1979,"created_at":"2010-11-13T15:25:56Z"} {"id":751165619291,"first_name":"Brita","last_name":"Weddup","email":"bweddupj@ucsd.edu","gender":"Polygender","flagged":true,"year":2009,"created_at":"2018-06-27T17:30:12Z"} {"id":767014826369,"first_name":"Magnum","last_name":"Horlick","email":"mhorlickk@google.pl","gender":"Agender","ip_address":"101.236.46.139","flagged":false,"year":1995,"created_at":"2010-10-09T20:14:07Z"} {"id":711544046719,"first_name":"Florenza","last_name":"Demcak","email":"fdemcakl@123-reg.co.uk","gender":"Non-binary","ip_address":"120.113.184.63","flagged":false,"year":2004,"created_at":"2007-06-04T07:09:41Z"} {"id":1022852113804,"first_name":"Corrie","last_name":"Gueny","email":"cguenym@pinterest.com","gender":"Non-binary","flagged":true,"year":1986,"created_at":"2018-03-29T21:30:39Z"} {"test_object":{"array": [{"inner": 1, "another": 2},{"inner": 3, "another": 4}]},"id":1032800400604,"first_name":"Dicky","last_name":"Livezey","email":"dlivezeyn@wsj.com","gender":"Non-binary","flagged":false,"year":1995,"created_at":"2014-10-07T03:23:45Z"} {"id":606572988669,"first_name":"Pablo","last_name":"Heliar","email":"pheliaro@ow.ly","gender":"Polygender","flagged":false,"year":1964,"created_at":"2004-10-17T17:05:25Z"} {"id":715019978569,"first_name":"Benn","last_name":"Hugk","email":"bhugkp@japanpost.jp","gender":"Male","flagged":true,"year":2000,"created_at":"2002-04-04T09:02:40Z"} {"id":266310620259,"first_name":"Oneida","last_name":"Georgiades","email":"ogeorgiadesq@amazon.co.uk","gender":"Agender","flagged":false,"year":2002,"created_at":"2005-01-24T05:53:13Z"} {"id":588738215969,"first_name":"Judon","email":"jcasajuanar@tiny.cc","gender":"Female","ip_address":"165.97.78.144","flagged":false,"year":2006,"created_at":"2021-03-13T11:39:03Z"} {"id":781239301046,"first_name":"Pepito","last_name":"Francesch","email":"pfranceschs@surveymonkey.com","gender":"Female","ip_address":"246.88.155.119","flagged":true,"year":2012,"created_at":"2009-07-01T21:33:47Z"} {"id":358241319650,"first_name":"Rickert","last_name":"Vaskin","email":"rvaskint@sbwire.com","gender":"Genderfluid","ip_address":"241.222.73.189","flagged":false,"year":1987,"created_at":"2003-09-10T04:42:29Z"} {"id":1070740140647,"first_name":"Gillan","last_name":"Gergely","email":"ggergelyu@tinypic.com","gender":"Genderfluid","flagged":true,"year":1998,"created_at":"2018-10-16T21:56:27Z"} {"id":922016611950,"first_name":"Rosalie","last_name":"Holborn","email":"rholbornv@diigo.com","gender":"Non-binary","ip_address":"96.198.8.127","flagged":false,"year":1984,"created_at":"2010-05-21T06:52:19Z"} {"id":955664025444,"first_name":"Arlette","last_name":"Avery","email":"aaveryw@about.me","gender":"Polygender","flagged":true,"year":1987,"created_at":"2002-11-25T11:21:29Z"} {"id":1059025102411,"first_name":"James","last_name":"Asipenko","email":"jasipenkox@goo.ne.jp","gender":"Female","ip_address":"28.234.234.218","flagged":false,"year":2004,"created_at":"2009-03-29T06:30:05Z"} {"id":812773302656,"first_name":"Ricki","last_name":"Bouda","email":"rbouday@icq.com","gender":"Agender","ip_address":"34.168.107.243","flagged":true,"year":1996,"created_at":"2003-11-20T20:06:09Z"} {"id":448091978845,"first_name":"Johanna","last_name":"Kellick","gender":"Male","flagged":false} {"id":815776331856,"first_name":"Laurent","last_name":"Ormiston","email":"lormiston10@gnu.org","gender":"Genderqueer","flagged":true,"year":1995,"created_at":"2002-07-16T08:24:24Z"} {"id":902831439381,"first_name":"Mandel","last_name":"Velte","email":"mvelte11@issuu.com","gender":"Non-binary","flagged":true,"year":2012,"created_at":"2004-06-10T08:15:25Z"} {"id":169918319893,"first_name":"Sammie","last_name":"Sexty","email":"ssexty12@g.co","gender":"Genderqueer","flagged":false,"year":1999,"created_at":"2007-01-30T21:01:34Z"} {"id":480080215984,"first_name":"Petronilla","last_name":"Tussaine","email":"ptussaine13@dion.ne.jp","gender":"Polygender","ip_address":"116.6.121.226","flagged":true,"year":2008,"created_at":"2017-04-16T01:34:16Z"} {"id":665842344335,"first_name":"Lazaro","last_name":"Aronow","email":"laronow14@time.com","gender":"Male","ip_address":"241.43.119.174","flagged":true,"year":2004,"created_at":"2020-06-01T05:07:49Z"} {"id":349674304279,"first_name":"Shea","last_name":"Dugall","gender":"Female","ip_address":"46.202.198.233","flagged":false} {"first_name":"Berta","email":"bwigginton16@yandex.ru","gender":"Polygender","ip_address":"207.184.131.158","year":1988,"created_at":"2005-02-20T10:52:21Z"} {"id":308965977872,"first_name":"Mychal","gender":"Polygender","flagged":true} {"id":557865318679,"first_name":"Loreen","email":"lprimak18@answers.com","gender":"Genderfluid","flagged":true,"year":1994,"created_at":"2014-10-02T17:48:31Z"} {"id":888144164713,"first_name":"Olimpia","email":"omuckleston19@bandcamp.com","gender":"Genderfluid","flagged":false,"year":2007,"created_at":"2004-08-12T10:27:25Z"} {"id":797385211447,"first_name":"Abby","last_name":"Oganesian","email":"aoganesian1a@ucoz.ru","gender":"Agender","ip_address":"244.224.141.60","flagged":false,"year":2008,"created_at":"2004-01-18T15:57:51Z"} {"id":766174677180,"first_name":"Sigrid","last_name":"Spellacey","email":"sspellacey1b@jigsy.com","gender":"Non-binary","ip_address":"98.42.11.228","flagged":false,"year":1986,"created_at":"2011-12-25T23:30:31Z"} {"id":1123714826629,"first_name":"Sile","last_name":"Langworthy","email":"slangworthy1c@weibo.com","gender":"Bigender","flagged":true,"year":2003,"created_at":"2015-09-09T17:00:50Z"} {"id":658572008560,"first_name":"Chaddy","last_name":"Paget","email":"cpaget1d@booking.com","gender":"Male","flagged":true,"year":2004,"created_at":"2004-10-06T23:12:12Z"} {"id":1069105521866,"first_name":"Tracey","email":"tlegrand1e@wisc.edu","flagged":true,"year":2005,"created_at":"2011-04-14T03:41:19Z"} {"id":1150988421152,"first_name":"Claire","last_name":"Styles","email":"cstyles1f@free.fr","gender":"Agender","ip_address":"186.230.233.45","flagged":true,"year":1994,"created_at":"2003-03-03T17:32:37Z"} {"id":522438403794,"first_name":"Bart","last_name":"Sleep","gender":"Agender","ip_address":"252.113.32.255","flagged":false} {"id":743538707878,"first_name":"Loydie","last_name":"Pollicote","email":"lpollicote1h@msu.edu","gender":"Non-binary","flagged":true,"year":2012,"created_at":"2015-03-25T00:07:45Z"} {"id":1017309689594,"first_name":"Wolfie","email":"wsamwell1i@csmonitor.com","gender":"Male","flagged":true,"year":2008,"created_at":"2016-09-06T04:41:57Z"} {"id":746005059890,"first_name":"Alfons","last_name":"Didsbury","email":"adidsbury1j@un.org","gender":"Bigender","flagged":true,"year":1980,"created_at":"2009-02-08T14:38:18Z"} {"id":772541197163,"first_name":"Fransisco","last_name":"McGrill","email":"fmcgrill1k@dell.com","gender":"Genderqueer","ip_address":"136.128.121.83","flagged":false,"year":1995,"created_at":"2005-07-01T03:52:04Z"} {"id":458285213320,"first_name":"Gilda","last_name":"Janjic","gender":"Male","ip_address":"205.103.25.96","flagged":true} {"id":376206481553,"first_name":"Garth","last_name":"Packington","email":"gpackington1m@bravesites.com","gender":"Bigender","ip_address":"80.209.220.27","flagged":false,"year":2011,"created_at":"2014-09-26T21:56:35Z"} {"id":150428065208,"first_name":"Elonore","email":"emccarle1n@jigsy.com","gender":"Agender","flagged":false,"year":1992,"created_at":"2015-09-02T03:24:43Z"} {"id":266115904164,"first_name":"Magda","last_name":"Feavyour","email":"mfeavyour1o@seattletimes.com","gender":"Genderfluid","ip_address":"167.252.133.247","flagged":false,"year":2012,"created_at":"2011-07-09T15:43:20Z"} {"id":851062224711,"first_name":"Far","last_name":"Cornock","email":"fcornock1p@blog.com","gender":"Genderqueer","flagged":true,"year":2009,"created_at":"2015-01-17T18:17:27Z"} {"id":622360231608,"first_name":"Annadiana","last_name":"Philson","email":"aphilson1q@wikimedia.org","gender":"Genderqueer","flagged":true,"year":1991,"created_at":"2010-01-03T21:04:09Z"} {"id":529097503922,"first_name":"Aeriell","last_name":"Straker","email":"astraker1r@bluehost.com","gender":"Male","ip_address":"166.149.245.238","flagged":true,"year":1981,"created_at":"2011-09-28T08:14:41Z"} {"id":921608714048,"first_name":"Aila","last_name":"Duplan","email":"aduplan1s@ed.gov","gender":"Bigender","flagged":true,"year":2010,"created_at":"2003-01-13T15:31:46Z"} {"id":252482550964,"first_name":"Josie","last_name":"Folshom","email":"jfolshom1t@163.com","gender":"Genderfluid","ip_address":"95.95.234.231","flagged":false,"year":2003,"created_at":"2005-11-25T11:51:19Z"} {"id":523077272734,"first_name":"Mahmud","last_name":"Allum","email":"mallum1u@desdev.cn","gender":"Non-binary","flagged":true,"year":2006,"created_at":"2015-06-01T20:11:17Z"} {"id":194703637418,"first_name":"Lee","email":"lklimus1v@amazonaws.com","gender":"Agender","flagged":true,"year":1998,"created_at":"2014-05-25T04:20:39Z"} {"id":988390903420,"first_name":"Fidole","last_name":"Dudeney","email":"fdudeney1w@blogtalkradio.com","gender":"Genderqueer","ip_address":"210.164.84.230","flagged":true,"year":1996,"created_at":"2012-04-27T10:40:19Z"} {"id":1211944760205,"first_name":"Issie","last_name":"Whitney","email":"iwhitney1x@hatena.ne.jp","gender":"Polygender","flagged":false,"year":2012,"created_at":"2017-10-26T00:23:52Z"} {"id":352660490585,"first_name":"Esme","last_name":"Souley","email":"esouley1y@gizmodo.com","gender":"Male","ip_address":"11.136.33.54","flagged":false,"year":2005,"created_at":"2005-12-14T08:29:45Z"} {"id":914455921146,"first_name":"Betsy","last_name":"Coronas","email":"bcoronas1z@flickr.com","gender":"Polygender","ip_address":"221.73.255.101","flagged":false,"year":2012,"created_at":"2017-10-18T19:38:51Z"} {"id":587221981370,"first_name":"Johnath","last_name":"Firle","email":"jfirle20@cornell.edu","gender":"Genderfluid","flagged":false,"year":1988,"created_at":"2017-01-01T08:50:20Z"} {"id":290854565473,"first_name":"Maire","last_name":"Wikey","email":"mwikey21@acquirethisname.com","gender":"Female","flagged":true,"year":2004,"created_at":"2013-01-28T21:03:41Z"} {"id":885862854751,"first_name":"Hynda","last_name":"Monsey","email":"hmonsey22@booking.com","gender":"Male","ip_address":"103.19.35.18","flagged":true,"year":1996,"created_at":"2006-12-30T03:39:24Z"} {"id":499362903597,"first_name":"Ximenez","last_name":"Ballentime","email":"xballentime23@boston.com","gender":"Genderfluid","ip_address":"196.18.131.121","flagged":false,"year":2012,"created_at":"2013-05-25T16:09:13Z"} {"id":725172063804,"first_name":"Mary","email":"mguillerman24@photobucket.com","gender":"Female","ip_address":"197.82.147.206","flagged":false,"year":2012,"created_at":"2013-09-13T22:53:35Z"} {"id":295273978492,"first_name":"Rockey","last_name":"Mowett","email":"rmowett25@scientificamerican.com","gender":"Female","ip_address":"179.114.91.18","flagged":false,"year":2003,"created_at":"2011-07-10T20:00:43Z"} {"id":794492026258,"first_name":"Munroe","last_name":"Crippes","email":"mcrippes26@go.com","gender":"Agender","ip_address":"251.115.141.125","flagged":true,"year":1996,"created_at":"2004-04-14T20:27:46Z"} {"id":866570730240,"first_name":"Thayne","last_name":"Carlsson","email":"tcarlsson27@prlog.org","gender":"Polygender","flagged":false,"year":1995,"created_at":"2009-09-05T08:44:26Z"} {"first_name":"Carlee","last_name":"Boake","email":"cboake28@reuters.com","gender":"Male","ip_address":"255.103.137.193","year":1985,"created_at":"2013-06-15T06:40:20Z"} {"id":133106210779,"first_name":"Daveta","last_name":"Absolon","email":"dabsolon29@disqus.com","gender":"Genderfluid","ip_address":"244.47.100.69","flagged":false,"year":1994,"created_at":"2006-12-14T08:08:46Z"} {"id":363911220671,"first_name":"Kellen","last_name":"Ipgrave","email":"kipgrave2a@skyrock.com","gender":"Male","flagged":true,"year":2008,"created_at":"2002-04-14T12:55:53Z"} {"id":639154157060,"first_name":"Rodi","last_name":"Djekic","email":"rdjekic2b@nasa.gov","gender":"Genderfluid","flagged":true,"year":2005,"created_at":"2006-06-30T10:02:50Z"} {"id":383148874144,"first_name":"Trista","last_name":"Brislane","email":"tbrislane2c@ed.gov","gender":"Non-binary","ip_address":"166.243.137.195","flagged":false,"year":2006,"created_at":"2011-06-25T14:46:11Z"} {"id":899363222797,"first_name":"Buffy","last_name":"Syalvester","email":"bsyalvester2d@webeden.co.uk","gender":"Genderfluid","ip_address":"107.111.100.233","flagged":false,"year":1991,"created_at":"2012-04-22T01:30:48Z"} {"id":247355384628,"first_name":"Korella","last_name":"Garron","email":"kgarron2e@newsvine.com","gender":"Female","flagged":true,"year":1989,"created_at":"2008-08-16T11:02:17Z"} {"id":904418387549,"first_name":"Elia","email":"eruttgers2f@un.org","gender":"Polygender","flagged":false,"year":2002,"created_at":"2014-05-17T08:10:13Z"} {"id":416171596315,"first_name":"Janelle","last_name":"Woodier","email":"jwoodier2g@sfgate.com","gender":"Bigender","ip_address":"219.26.27.162","flagged":true,"year":2013,"created_at":"2007-12-20T09:45:05Z"} {"first_name":"Henrietta","last_name":"Siene","email":"hsiene2h@i2i.jp","gender":"Agender","year":1988,"created_at":"2005-10-20T08:22:41Z"} {"id":920660541500,"first_name":"Cheston","last_name":"Eisold","email":"ceisold2i@auda.org.au","gender":"Non-binary","flagged":false,"year":2001,"created_at":"2007-04-22T11:38:46Z"} {"id":748484900067,"first_name":"Noach","last_name":"Duffitt","email":"nduffitt2j@hugedomains.com","gender":"Bigender","ip_address":"232.37.194.31","flagged":true,"year":1992,"created_at":"2018-07-10T18:27:38Z"} {"id":1084814982722,"first_name":"Kirsteni","last_name":"Mibourne","email":"kmibourne2k@wikipedia.org","gender":"Agender","flagged":true,"year":1993,"created_at":"2001-08-25T22:28:40Z"} {"id":720067996085,"first_name":"Derward","last_name":"Roseaman","email":"droseaman2l@mayoclinic.com","gender":"Male","ip_address":"8.180.166.143","flagged":false,"year":1988,"created_at":"2003-07-25T07:48:04Z"} {"id":247167449740,"first_name":"Krissie","last_name":"Payley","email":"kpayley2m@creativecommons.org","gender":"Male","ip_address":"43.229.234.246","flagged":false,"year":1996,"created_at":"2007-05-18T13:23:15Z"} {"id":925627025965,"first_name":"Cassius","last_name":"Petticrow","email":"cpetticrow2n@sakura.ne.jp","gender":"Agender","ip_address":"32.116.234.116","flagged":false,"year":1994,"created_at":"2001-06-19T00:59:18Z"} {"id":722485137293,"first_name":"Olivier","email":"ocrumbie2o@exblog.jp","gender":"Polygender","ip_address":"69.178.89.253","flagged":true,"year":1996,"created_at":"2010-12-11T11:41:08Z"} {"id":464751954354,"first_name":"Blinny","last_name":"Schimon","email":"bschimon2p@wikia.com","gender":"Female","ip_address":"63.169.194.139","flagged":false,"year":2010,"created_at":"2018-06-17T06:42:35Z"} {"id":1162752832804,"first_name":"Giovanna","last_name":"Rops","email":"grops2q@flickr.com","gender":"Genderqueer","ip_address":"237.230.138.192","flagged":true,"year":2011,"created_at":"2014-03-18T00:46:01Z"} {"id":774141397791,"first_name":"Maurizia","last_name":"Girvan","email":"mgirvan2r@huffingtonpost.com","gender":"Genderfluid","ip_address":"184.77.46.62","flagged":false,"year":1999,"created_at":"2015-07-23T01:35:51Z"} ================================================ FILE: common/workflow-operator/src/test/resources/1000.jsonl ================================================ {"id":495348462616,"first_name":"Lishe","last_name":"Pilley","email":"lpilley0@webs.com","gender":"Bigender","flagged":true,"year":1994,"created_at":"2010-05-03T19:54:08Z"} {"id":287236128953,"first_name":"Rodolphe","last_name":"Costar","email":"rcostar1@histats.com","gender":"Male","ip_address":"210.113.208.235","flagged":true,"year":1987,"created_at":"2018-02-07T21:43:53Z"} {"id":1018026352286,"first_name":"Christoforo","last_name":"Chapelle","email":"cchapelle2@g.co","gender":"Female","flagged":false,"year":1966,"created_at":"2015-01-08T04:34:39Z"} {"id":621164635498,"first_name":"Emma","last_name":"Creffield","email":"ecreffield3@tripadvisor.com","gender":"Female","ip_address":"247.226.31.147","flagged":false,"year":1996,"created_at":"2003-10-01T22:22:45Z"} {"id":770484167861,"first_name":"Marylou","last_name":"Eshelby","email":"meshelby4@bloglines.com","gender":"Male","ip_address":"44.25.242.157","flagged":false,"year":2012,"created_at":"2002-04-07T17:18:33Z"} {"first_name":"Madison","last_name":"Niese","email":"mniese5@squarespace.com","gender":"Genderfluid","year":2012,"created_at":"2003-12-17T14:41:16Z"} {"id":904762805781,"first_name":"Jojo","last_name":"Voce","email":"jvoce6@google.pl","ip_address":"169.66.133.168","flagged":false,"year":2005,"created_at":"2005-03-04T17:08:02Z"} {"test_object":{"array": [{"inner": 1, "another": 2},{"inner": 3, "another": 4}]},"id":734741756781,"first_name":"Darill","last_name":"Heamus","email":"dheamus7@123-reg.co.uk","gender":"Bigender","flagged":false,"year":2003,"created_at":"2014-02-20T22:41:07Z"} {"id":948019440425,"first_name":"Wilhelmina","last_name":"Upstell","email":"wupstell8@comcast.net","gender":"Agender","ip_address":"196.91.9.87","flagged":false,"year":1991,"created_at":"2004-08-06T21:45:43Z"} {"id":1223799635431,"first_name":"Dennison","last_name":"Di Bartolomeo","email":"ddibartolomeo9@google.es","gender":"Agender","ip_address":"17.214.34.199","flagged":true,"year":2001,"created_at":"2007-02-01T08:53:39Z"} {"id":1093091605047,"first_name":"Tuck","last_name":"Scrancher","email":"tscranchera@diigo.com","gender":"Genderfluid","flagged":true,"year":2003,"created_at":"2001-07-01T04:24:02Z"} {"id":490191648533,"first_name":"Wilmer","last_name":"Cumbridge","email":"wcumbridgeb@wufoo.com","gender":"Male","ip_address":"29.201.250.59","flagged":true,"year":1985,"created_at":"2006-06-14T01:45:42Z"} {"id":798906925542,"first_name":"Bayard","last_name":"Ghiron","email":"bghironc@opera.com","gender":"Agender","ip_address":"246.79.214.149","flagged":true,"year":2011,"created_at":"2011-04-04T17:21:51Z"} {"id":338855242358,"first_name":"Janaye","last_name":"Togwell","email":"jtogwelld@prweb.com","ip_address":"61.231.147.44","flagged":false,"year":2001,"created_at":"2002-05-21T15:40:09Z"} {"id":126611634270,"first_name":"Kenon","last_name":"Broadley","email":"kbroadleye@ezinearticles.com","gender":"Genderfluid","ip_address":"180.214.246.158","flagged":false,"year":2008,"created_at":"2003-11-13T04:04:31Z"} {"id":655262664359,"first_name":"Dulcy","last_name":"Titt","email":"dtittf@g.co","gender":"Non-binary","ip_address":"129.54.221.111","flagged":true,"year":1996,"created_at":"2004-07-26T06:00:06Z"} {"id":665576635835,"first_name":"Alf","last_name":"Dikes","email":"adikesg@intel.com","gender":"Bigender","ip_address":"221.89.236.131","flagged":true,"year":1995,"created_at":"2019-01-29T12:55:21Z"} {"id":973965339010,"first_name":"Rod","last_name":"Kollach","email":"rkollachh@symantec.com","gender":"Agender","ip_address":"8.86.137.144","flagged":false,"year":1996,"created_at":"2001-06-14T11:59:35Z"} {"id":360939416453,"first_name":"Eartha","last_name":"MacConnechie","email":"emacconnechiei@census.gov","gender":"Agender","flagged":true,"year":2012,"created_at":"2020-04-01T22:35:53Z"} {"id":861521009192,"first_name":"Renado","last_name":"Orman","email":"rormanj@privacy.gov.au","gender":"Female","flagged":false,"year":2010,"created_at":"2005-08-20T20:14:21Z"} {"first_name":"William","last_name":"Nendick","email":"wnendickk@redcross.org","gender":"Bigender","ip_address":"250.226.198.156","year":1999,"created_at":"2001-10-01T11:44:48Z"} {"id":829759742392,"first_name":"Vince","last_name":"Gabbott","email":"vgabbottl@fastcompany.com","gender":"Bigender","ip_address":"61.132.86.205","flagged":true,"year":2009,"created_at":"2007-07-05T11:48:59Z"} {"first_name":"Irena","last_name":"Dionis","email":"idionism@sohu.com","gender":"Genderfluid","year":2010,"created_at":"2011-01-14T20:51:01Z"} {"id":190511254847,"first_name":"Corbie","email":"cphilon@apple.com","gender":"Polygender","flagged":true,"year":2006,"created_at":"2011-03-26T22:28:13Z"} {"id":597237664993,"first_name":"Odessa","email":"otattersillo@yolasite.com","gender":"Polygender","ip_address":"74.34.147.207","flagged":true,"year":2001,"created_at":"2004-06-28T15:39:05Z"} {"id":176270668022,"first_name":"Ruttger","last_name":"Maddra","email":"rmaddrap@mlb.com","gender":"Bigender","flagged":true,"year":1996,"created_at":"2019-06-28T15:50:03Z"} {"id":633008417634,"first_name":"Sorcha","last_name":"Knivett","email":"sknivettq@youku.com","gender":"Non-binary","ip_address":"82.194.251.226","flagged":false,"year":2007,"created_at":"2004-03-19T10:19:30Z"} {"id":661048861598,"first_name":"Garry","last_name":"Ainley","email":"gainleyr@abc.net.au","gender":"Female","ip_address":"152.220.70.152","flagged":false,"year":1999,"created_at":"2013-04-19T13:13:35Z"} {"id":397654175873,"first_name":"Ray","last_name":"Hryniewicki","email":"rhryniewickis@fema.gov","gender":"Female","ip_address":"232.51.37.67","flagged":true,"year":1999,"created_at":"2013-04-25T21:11:30Z"} {"id":1032374295311,"first_name":"Georgeanna","last_name":"Layus","email":"glayust@mtv.com","gender":"Female","ip_address":"198.100.34.160","flagged":true,"year":1987,"created_at":"2003-05-16T07:55:08Z"} {"id":1213175695392,"first_name":"Maude","last_name":"Clemson","email":"mclemsonu@washingtonpost.com","gender":"Male","flagged":false,"year":1991,"created_at":"2011-10-17T03:25:42Z"} {"id":902834279291,"first_name":"Kristos","last_name":"Boundley","email":"kboundleyv@boston.com","gender":"Male","ip_address":"127.77.79.117","flagged":false,"year":2005,"created_at":"2007-05-02T21:56:17Z"} {"id":1200161088297,"first_name":"Perren","last_name":"Gogan","email":"pgoganw@wikipedia.org","gender":"Genderqueer","flagged":true,"year":2002,"created_at":"2005-10-29T15:49:41Z"} {"id":297687500885,"first_name":"Frannie","email":"fwhittax@nhs.uk","gender":"Bigender","flagged":true,"year":2007,"created_at":"2015-03-21T14:46:41Z"} {"id":354887789951,"first_name":"Fedora","last_name":"Bortolotti","email":"fbortolottiy@gov.uk","gender":"Female","ip_address":"126.249.47.50","flagged":true,"year":1999,"created_at":"2015-01-25T13:13:28Z"} {"id":627926301354,"first_name":"Bette-ann","last_name":"McNeilly","email":"bmcneillyz@a8.net","gender":"Bigender","flagged":false,"year":1999,"created_at":"2019-06-01T09:49:29Z"} {"id":899085211270,"first_name":"Johannah","last_name":"Breit","email":"jbreit10@miibeian.gov.cn","gender":"Genderfluid","ip_address":"192.89.25.158","flagged":false,"year":2005,"created_at":"2018-02-01T14:39:27Z"} {"id":690104101915,"first_name":"Ellsworth","last_name":"Checkley","email":"echeckley11@forbes.com","gender":"Male","flagged":false,"year":2002,"created_at":"2018-04-19T18:53:45Z"} {"id":1133986314642,"first_name":"Carolus","last_name":"Burlay","email":"cburlay12@sfgate.com","gender":"Male","ip_address":"80.126.45.100","flagged":false,"year":1999,"created_at":"2007-01-12T04:27:49Z"} {"id":1155620828245,"first_name":"Gloriane","last_name":"Borne","email":"gborne13@marketwatch.com","gender":"Polygender","ip_address":"44.233.196.9","flagged":true,"year":2002,"created_at":"2016-01-17T06:40:46Z"} {"id":645056785207,"first_name":"Anica","last_name":"Langeren","email":"alangeren14@blogger.com","gender":"Bigender","flagged":true,"year":2002,"created_at":"2013-10-28T13:02:10Z"} {"id":245801507960,"first_name":"Shawnee","last_name":"Boribal","email":"sboribal15@usda.gov","gender":"Agender","ip_address":"238.48.40.254","flagged":true,"year":2009,"created_at":"2009-08-10T19:57:15Z"} {"id":310305081100,"first_name":"Jaquelin","last_name":"Trowler","email":"jtrowler16@prlog.org","gender":"Agender","ip_address":"224.111.245.206","flagged":true,"year":1997,"created_at":"2013-07-01T17:59:04Z"} {"id":783900048055,"first_name":"Desirae","last_name":"Kaines","email":"dkaines17@gizmodo.com","gender":"Polygender","flagged":true,"year":2007,"created_at":"2007-03-11T03:06:41Z"} {"id":963562644140,"first_name":"Mallory","last_name":"MacCahey","email":"mmaccahey18@t-online.de","gender":"Female","ip_address":"180.84.168.179","flagged":false,"year":2007,"created_at":"2013-01-12T14:59:31Z"} {"id":876406495845,"first_name":"Haze","last_name":"Partkya","email":"hpartkya19@studiopress.com","gender":"Genderqueer","ip_address":"211.77.34.17","flagged":false,"year":1987,"created_at":"2004-07-29T14:32:58Z"} {"id":827967771639,"first_name":"Ellynn","last_name":"Geale","email":"egeale1a@pcworld.com","gender":"Genderfluid","ip_address":"166.31.1.32","flagged":false,"year":1988,"created_at":"2018-04-28T10:14:29Z"} {"id":311890054672,"first_name":"Chloe","last_name":"Stille","email":"cstille1b@house.gov","gender":"Non-binary","ip_address":"134.117.108.160","flagged":false,"year":2008,"created_at":"2017-01-14T08:53:50Z"} {"id":212259795020,"first_name":"Rosaline","email":"rmohring1c@delicious.com","gender":"Male","flagged":true,"year":2001,"created_at":"2013-04-21T00:11:32Z"} {"id":970067294310,"first_name":"Ashely","last_name":"Masdon","email":"amasdon1d@soundcloud.com","gender":"Non-binary","ip_address":"169.188.11.42","flagged":true,"year":2011,"created_at":"2002-07-13T12:06:54Z"} {"id":1189030784703,"first_name":"Markus","last_name":"Mapp","email":"mmapp1e@elegantthemes.com","gender":"Genderfluid","ip_address":"29.246.248.145","flagged":true,"year":2011,"created_at":"2020-07-04T13:22:32Z"} {"id":330101394597,"first_name":"Zulema","email":"zchristal1f@yolasite.com","gender":"Genderfluid","flagged":true,"year":2002,"created_at":"2001-06-17T16:10:15Z"} {"id":1185095495797,"first_name":"Gene","last_name":"Grevile","email":"ggrevile1g@bigcartel.com","gender":"Non-binary","ip_address":"168.60.30.175","flagged":true,"year":2007,"created_at":"2006-08-12T20:35:25Z"} {"id":176393315945,"first_name":"Teriann","last_name":"Sillwood","email":"tsillwood1h@people.com.cn","gender":"Genderfluid","ip_address":"94.206.220.56","flagged":false,"year":2008,"created_at":"2013-02-16T23:59:34Z"} {"id":164018715480,"first_name":"Marjory","last_name":"Medland","email":"mmedland1i@toplist.cz","gender":"Non-binary","ip_address":"50.53.252.87","flagged":true,"year":2003,"created_at":"2014-05-01T17:42:21Z"} {"id":505458860439,"first_name":"Kirby","email":"khurdiss1j@buzzfeed.com","gender":"Female","flagged":true,"year":1990,"created_at":"2013-04-19T10:18:31Z"} {"id":233009291217,"first_name":"Ondrea","last_name":"Bernardotte","email":"obernardotte1k@1und1.de","gender":"Polygender","ip_address":"185.227.221.99","flagged":true,"year":2004,"created_at":"2010-10-29T08:03:41Z"} {"id":130388943917,"first_name":"Conchita","last_name":"Alsopp","email":"calsopp1l@hexun.com","gender":"Non-binary","ip_address":"43.226.54.235","flagged":true,"year":2001,"created_at":"2006-11-18T13:48:47Z"} {"first_name":"Dorie","last_name":"Pinnere","email":"dpinnere1m@geocities.jp","gender":"Bigender","ip_address":"108.247.24.64","year":2012,"created_at":"2018-05-11T09:46:33Z"} {"id":1204204148934,"first_name":"Torry","last_name":"McKeating","email":"tmckeating1n@seattletimes.com","ip_address":"195.34.19.90","flagged":true,"year":1986,"created_at":"2008-08-12T11:40:15Z"} {"first_name":"Edith","last_name":"MacScherie","email":"emacscherie1o@angelfire.com","gender":"Bigender","ip_address":"57.91.108.65","year":1999,"created_at":"2010-01-31T18:07:43Z"} {"id":1076329499342,"first_name":"Nicole","email":"ngradly1p@psu.edu","gender":"Genderfluid","ip_address":"94.140.66.45","flagged":true,"year":1992,"created_at":"2018-03-11T17:10:20Z"} {"id":1149826539778,"first_name":"Ricca","last_name":"Brixham","email":"rbrixham1q@hugedomains.com","gender":"Agender","ip_address":"213.33.105.75","flagged":true,"year":2008,"created_at":"2012-08-17T02:01:06Z"} {"id":613355956079,"first_name":"Ana","last_name":"Briant","email":"abriant1r@ycombinator.com","gender":"Agender","flagged":true,"year":1993,"created_at":"2014-11-25T23:40:25Z"} {"id":727066596571,"first_name":"Elvira","last_name":"Willcott","email":"ewillcott1s@vk.com","gender":"Female","ip_address":"184.225.62.114","flagged":false,"year":2012,"created_at":"2007-12-09T05:14:05Z"} {"id":162973713337,"first_name":"Fernande","last_name":"Alekhov","gender":"Agender","ip_address":"184.116.132.165","flagged":true} {"id":605989436974,"first_name":"Farrell","last_name":"Gauntlett","email":"fgauntlett1u@networksolutions.com","gender":"Male","ip_address":"176.17.245.16","flagged":true,"year":2006,"created_at":"2001-07-14T06:21:22Z"} {"id":482434002399,"first_name":"Clayborne","email":"charvie1v@forbes.com","gender":"Non-binary","ip_address":"82.109.254.163","flagged":true,"year":2003,"created_at":"2014-06-01T20:57:24Z"} {"id":254538755109,"first_name":"Quintin","last_name":"Camps","email":"qcamps1w@cnn.com","gender":"Genderfluid","flagged":true,"year":2004,"created_at":"2001-07-08T21:06:00Z"} {"id":475273116710,"first_name":"Beltran","last_name":"Gillatt","email":"bgillatt1x@mediafire.com","gender":"Agender","ip_address":"225.147.152.40","flagged":true,"year":1999,"created_at":"2011-01-04T06:43:36Z"} {"id":993120302775,"first_name":"Ericha","email":"estonard1y@prnewswire.com","gender":"Non-binary","ip_address":"212.246.98.172","flagged":false,"year":1998,"created_at":"2018-01-13T09:41:50Z"} {"id":431171048519,"first_name":"Alicea","last_name":"Ilden","email":"ailden1z@wisc.edu","gender":"Genderfluid","flagged":true,"year":1995,"created_at":"2016-02-14T18:43:31Z"} {"id":147482453970,"first_name":"Cindy","last_name":"Denerley","email":"cdenerley20@toplist.cz","gender":"Genderqueer","ip_address":"108.75.27.202","flagged":true,"year":2006,"created_at":"2008-03-10T21:28:49Z"} {"id":328868019149,"first_name":"Agatha","last_name":"Easeman","email":"aeaseman21@earthlink.net","gender":"Genderfluid","ip_address":"188.196.77.126","flagged":true,"year":2006,"created_at":"2010-12-12T01:18:14Z"} {"id":889855548990,"first_name":"Goraud","last_name":"Mangham","email":"gmangham22@networkadvertising.org","gender":"Polygender","ip_address":"139.203.39.22","flagged":false,"year":2007,"created_at":"2011-10-08T06:15:35Z"} {"id":973557680045,"first_name":"Fancy","last_name":"Guisler","email":"fguisler23@ocn.ne.jp","gender":"Agender","flagged":false,"year":1994,"created_at":"2005-02-14T03:39:51Z"} {"id":609343185326,"first_name":"Adina","last_name":"Dally","email":"adally24@ow.ly","gender":"Male","flagged":true,"year":1992,"created_at":"2018-12-03T11:42:22Z"} {"id":621603189384,"first_name":"Frasier","last_name":"Sproat","email":"fsproat25@blog.com","gender":"Polygender","flagged":true,"year":1987,"created_at":"2004-09-16T03:30:51Z"} {"id":1072162353821,"first_name":"Debra","last_name":"Gresswood","email":"dgresswood26@ca.gov","gender":"Genderqueer","ip_address":"68.63.183.32","flagged":true,"year":2007,"created_at":"2021-02-18T11:01:06Z"} {"id":783864136437,"first_name":"Morie","last_name":"Casone","email":"mcasone27@wikimedia.org","gender":"Non-binary","ip_address":"247.82.43.205","flagged":false,"year":2006,"created_at":"2016-04-09T03:38:31Z"} {"id":924225673797,"first_name":"Tore","last_name":"Bridson","email":"tbridson28@tripod.com","gender":"Female","flagged":false,"year":2000,"created_at":"2002-02-19T19:11:45Z"} {"id":279793080249,"first_name":"Desiree","last_name":"Wackly","email":"dwackly29@miitbeian.gov.cn","gender":"Male","flagged":true,"year":2007,"created_at":"2012-05-19T17:52:27Z"} {"id":812786329580,"first_name":"Yard","last_name":"Arnowicz","email":"yarnowicz2a@t-online.de","gender":"Genderfluid","flagged":false,"year":1995,"created_at":"2018-03-13T16:07:03Z"} {"id":928404434930,"first_name":"Padraic","last_name":"Skarin","email":"pskarin2b@linkedin.com","gender":"Genderfluid","flagged":false,"year":2005,"created_at":"2005-08-13T13:16:39Z"} {"id":731935680890,"first_name":"Ogdan","last_name":"Case","gender":"Male","ip_address":"166.14.249.79","flagged":true} {"id":129845800422,"first_name":"Annetta","last_name":"Craik","email":"acraik2d@usa.gov","gender":"Genderqueer","ip_address":"246.142.65.72","flagged":true,"year":2001,"created_at":"2015-12-30T13:35:08Z"} {"id":757912851223,"first_name":"Olivier","last_name":"Iohananof","email":"oiohananof2e@bloomberg.com","gender":"Genderfluid","flagged":true,"year":1989,"created_at":"2020-06-15T13:22:01Z"} {"id":406072566721,"first_name":"Benita","last_name":"Cosin","email":"bcosin2f@ameblo.jp","gender":"Genderfluid","ip_address":"165.78.227.176","flagged":false,"year":1999,"created_at":"2006-11-13T05:07:01Z"} {"first_name":"Carlyle","last_name":"Caldeyroux","gender":"Non-binary","ip_address":"244.209.158.22"} {"id":574643066902,"first_name":"Hy","last_name":"Darrel","email":"hdarrel2h@nsw.gov.au","gender":"Polygender","ip_address":"174.199.232.58","flagged":false,"year":1998,"created_at":"2003-01-23T20:56:19Z"} {"id":207502987549,"first_name":"Evy","email":"enavaro2i@zimbio.com","gender":"Male","ip_address":"120.125.64.214","flagged":true,"year":2001,"created_at":"2010-11-26T15:10:23Z"} {"id":490908302513,"first_name":"Colver","last_name":"Carnalan","email":"ccarnalan2j@nsw.gov.au","gender":"Polygender","ip_address":"72.220.239.21","flagged":true,"year":2004,"created_at":"2003-06-02T05:28:21Z"} {"id":308490862756,"first_name":"Lilllie","last_name":"Rivilis","email":"lrivilis2k@networksolutions.com","gender":"Male","ip_address":"0.34.172.17","flagged":true,"year":2001,"created_at":"2003-12-12T22:07:24Z"} {"id":1178467696656,"first_name":"Dillie","last_name":"Guille","email":"dguille2l@csmonitor.com","gender":"Genderqueer","ip_address":"18.46.185.192","flagged":false,"year":1996,"created_at":"2012-06-16T23:12:34Z"} {"first_name":"Spense","email":"shindrich2m@netscape.com","gender":"Female","year":2001,"created_at":"2006-03-22T23:58:12Z"} {"id":339232175107,"first_name":"Roth","last_name":"De Coursey","email":"rdecoursey2n@clickbank.net","gender":"Non-binary","ip_address":"147.239.123.114","flagged":true,"year":2010,"created_at":"2020-02-22T20:10:13Z"} {"id":266540901326,"first_name":"Carney","last_name":"Riccioppo","email":"criccioppo2o@github.io","gender":"Bigender","flagged":true,"year":2008,"created_at":"2012-05-11T07:50:54Z"} {"id":1047361483078,"first_name":"Nigel","last_name":"Breckin","email":"nbreckin2p@chicagotribune.com","gender":"Agender","ip_address":"65.206.141.40","flagged":true,"year":2001,"created_at":"2008-07-02T01:44:05Z"} {"id":1060194990002,"first_name":"Jobie","last_name":"Janiszewski","email":"jjaniszewski2q@cmu.edu","gender":"Male","flagged":true,"year":1995,"created_at":"2018-11-03T08:10:20Z"} {"first_name":"Euphemia","last_name":"Leek","email":"eleek2r@goo.gl","gender":"Agender","ip_address":"212.174.116.135","year":2003,"created_at":"2008-07-07T00:44:23Z"} {"id":1056409238545,"first_name":"Nathan","last_name":"Legging","email":"nlegging2s@spotify.com","gender":"Bigender","flagged":true,"year":2001,"created_at":"2013-02-02T03:43:48Z"} {"id":1172416477219,"first_name":"Clarinda","email":"cgasking2t@aboutads.info","gender":"Genderfluid","ip_address":"126.140.244.202","flagged":true,"year":2001,"created_at":"2006-02-18T21:36:12Z"} {"id":678106094067,"first_name":"Cori","last_name":"Gilburt","email":"cgilburt2u@dailymotion.com","gender":"Genderqueer","ip_address":"1.63.100.2","flagged":false,"year":2010,"created_at":"2015-10-04T03:19:26Z"} {"id":560708830750,"first_name":"Penelopa","last_name":"Salmen","email":"psalmen2v@ucla.edu","gender":"Female","ip_address":"40.70.54.156","flagged":true,"year":1984,"created_at":"2019-01-05T14:29:39Z"} {"id":953592959020,"first_name":"Halimeda","last_name":"Zettler","email":"hzettler2w@netscape.com","gender":"Female","flagged":false,"year":2011,"created_at":"2004-04-29T06:23:40Z"} {"id":807903944665,"first_name":"Xylia","last_name":"Jeffery","email":"xjeffery2x@1688.com","gender":"Polygender","ip_address":"137.75.253.180","flagged":false,"year":2008,"created_at":"2011-06-21T15:46:14Z"} {"id":353430852139,"first_name":"Bartolomeo","last_name":"Bittlestone","email":"bbittlestone2y@edublogs.org","gender":"Genderqueer","ip_address":"5.210.220.88","flagged":false,"year":1981,"created_at":"2013-05-07T14:30:45Z"} {"id":488612710952,"first_name":"Godiva","last_name":"Puttergill","email":"gputtergill2z@nytimes.com","gender":"Male","ip_address":"219.122.204.209","flagged":true,"year":1987,"created_at":"2002-03-25T02:02:21Z"} {"id":870217230788,"first_name":"Laney","last_name":"Rogeon","email":"lrogeon30@google.com","gender":"Genderqueer","ip_address":"218.216.70.231","flagged":true,"year":1994,"created_at":"2011-07-07T21:44:32Z"} {"id":549439607511,"first_name":"Jacquelin","last_name":"Brimicombe","gender":"Polygender","ip_address":"100.67.145.98","flagged":true} {"id":700046854807,"first_name":"Joana","last_name":"Stalley","gender":"Agender","flagged":false} {"id":426907016540,"first_name":"Jobi","last_name":"Ingerfield","email":"jingerfield33@chronoengine.com","gender":"Non-binary","ip_address":"166.145.122.113","flagged":false,"year":2011,"created_at":"2012-01-18T08:11:13Z"} {"id":1075114433339,"first_name":"Moreen","last_name":"Gatman","email":"mgatman34@godaddy.com","gender":"Agender","flagged":false,"year":1998,"created_at":"2016-06-23T07:31:22Z"} {"id":1156847942269,"first_name":"Kizzie","last_name":"Stripling","email":"kstripling35@newsvine.com","gender":"Polygender","flagged":false,"year":1993,"created_at":"2005-01-26T23:25:07Z"} {"id":208150131580,"first_name":"Emilia","last_name":"Sproule","email":"esproule36@pen.io","gender":"Polygender","flagged":false,"year":2012,"created_at":"2006-03-31T20:48:24Z"} {"id":553249655387,"first_name":"Sapphira","last_name":"Syms","email":"ssyms37@ezinearticles.com","gender":"Polygender","flagged":false,"year":2005,"created_at":"2011-12-20T14:12:58Z"} {"id":713308201024,"first_name":"Jacquie","last_name":"MacCardle","email":"jmaccardle38@dropbox.com","gender":"Bigender","ip_address":"155.97.163.58","flagged":false,"year":2004,"created_at":"2007-03-26T07:52:02Z"} {"id":878003289447,"first_name":"Amerigo","last_name":"Rosle","gender":"Female","ip_address":"118.212.14.47","flagged":true} {"id":879474417847,"first_name":"Randie","last_name":"Franiak","email":"rfraniak3a@youku.com","gender":"Agender","ip_address":"209.70.5.156","flagged":false,"year":2000,"created_at":"2014-04-22T17:05:19Z"} {"id":1138396983614,"first_name":"Milly","last_name":"Bouldon","email":"mbouldon3b@amazon.co.uk","gender":"Genderfluid","flagged":true,"year":2009,"created_at":"2003-03-15T19:29:31Z"} {"id":916139544475,"first_name":"Shantee","last_name":"Rengger","email":"srengger3c@miibeian.gov.cn","gender":"Genderqueer","flagged":false,"year":2004,"created_at":"2010-08-27T02:35:23Z"} {"id":183838694843,"first_name":"Gilbertina","last_name":"Western","email":"gwestern3d@sourceforge.net","gender":"Genderqueer","ip_address":"40.223.128.109","flagged":false,"year":1989,"created_at":"2016-02-17T11:16:00Z"} {"id":385422035148,"first_name":"Corey","last_name":"Potteril","email":"cpotteril3e@irs.gov","gender":"Bigender","flagged":false,"year":1993,"created_at":"2019-10-02T00:45:10Z"} {"id":743991615962,"first_name":"Arliene","last_name":"Bestiman","email":"abestiman3f@vk.com","gender":"Agender","ip_address":"231.106.201.234","flagged":false,"year":1991,"created_at":"2020-02-16T05:25:55Z"} {"id":524835767902,"first_name":"Rozina","last_name":"Vandrill","email":"rvandrill3g@shutterfly.com","gender":"Bigender","ip_address":"127.197.228.22","flagged":false,"year":2001,"created_at":"2012-09-25T16:06:06Z"} {"id":772570080521,"first_name":"Felicio","last_name":"Pea","email":"fpea3h@amazon.com","gender":"Genderfluid","ip_address":"133.44.200.171","flagged":true,"year":2010,"created_at":"2007-05-25T16:10:14Z"} {"id":283648384036,"first_name":"Nikos","email":"nsmowton3i@fotki.com","gender":"Male","flagged":true,"year":1999,"created_at":"2009-12-26T00:14:36Z"} {"id":578568745565,"first_name":"Karine","email":"kshepperd3j@a8.net","gender":"Genderqueer","ip_address":"23.104.184.195","flagged":false,"year":2011,"created_at":"2009-08-30T18:03:27Z"} {"id":262187843395,"first_name":"Myrtia","last_name":"Pinke","email":"mpinke3k@mlb.com","gender":"Genderfluid","ip_address":"185.86.238.100","flagged":false,"year":1985,"created_at":"2011-05-12T10:51:35Z"} {"id":852886479587,"first_name":"Olly","last_name":"Fendlow","email":"ofendlow3l@plala.or.jp","gender":"Genderqueer","ip_address":"169.108.44.86","flagged":false,"year":1995,"created_at":"2015-08-15T17:42:58Z"} {"id":205981882198,"first_name":"Giacinta","last_name":"Johananoff","email":"gjohananoff3m@bizjournals.com","gender":"Genderqueer","flagged":false,"year":1988,"created_at":"2014-06-23T10:39:13Z"} {"id":131833261526,"first_name":"Robbie","last_name":"Pedrielli","email":"rpedrielli3n@chronoengine.com","gender":"Non-binary","ip_address":"202.205.217.121","flagged":true,"year":1987,"created_at":"2020-12-19T14:09:14Z"} {"id":967969780177,"first_name":"Alexina","last_name":"Attewill","email":"aattewill3o@pbs.org","gender":"Agender","ip_address":"11.217.52.230","flagged":true,"year":2005,"created_at":"2001-11-10T22:12:24Z"} {"id":857947708962,"first_name":"Bren","email":"bfalkner3p@miitbeian.gov.cn","gender":"Genderqueer","ip_address":"120.94.180.0","flagged":true,"year":2003,"created_at":"2004-09-28T19:41:05Z"} {"id":271308158174,"first_name":"Binky","last_name":"Brewett","email":"bbrewett3q@altervista.org","gender":"Female","ip_address":"210.230.57.105","flagged":false,"year":1909,"created_at":"2014-08-28T11:20:18Z"} {"id":167611479785,"first_name":"Alfi","last_name":"Filer","email":"afiler3r@chronoengine.com","gender":"Agender","ip_address":"174.3.102.155","flagged":true,"year":1976,"created_at":"2013-03-23T04:32:46Z"} {"id":151090704262,"first_name":"Warde","last_name":"Alejo","email":"walejo3s@ehow.com","gender":"Polygender","ip_address":"136.152.52.98","flagged":false,"year":2006,"created_at":"2004-12-16T10:40:51Z"} {"id":1001295717366,"first_name":"Astra","last_name":"Morehall","email":"amorehall3t@yelp.com","gender":"Bigender","flagged":false,"year":1986,"created_at":"2014-06-17T22:23:31Z"} {"id":1025822588862,"first_name":"Cati","last_name":"Collingridge","email":"ccollingridge3u@51.la","gender":"Genderqueer","flagged":true,"year":2009,"created_at":"2002-01-27T11:46:22Z"} {"id":797300267506,"first_name":"Harli","email":"hledgerton3v@techcrunch.com","gender":"Polygender","ip_address":"229.13.40.192","flagged":true,"year":1997,"created_at":"2002-05-28T00:01:14Z"} {"id":492983687325,"first_name":"Elane","last_name":"Dohms","email":"edohms3w@cpanel.net","gender":"Non-binary","ip_address":"81.130.151.128","flagged":false,"year":2005,"created_at":"2009-10-15T15:38:09Z"} {"id":639997243893,"first_name":"Holly","last_name":"Conquest","email":"hconquest3x@macromedia.com","gender":"Genderfluid","flagged":true,"year":1995,"created_at":"2019-12-03T09:15:45Z"} {"id":514074720403,"first_name":"Sargent","last_name":"Snailham","email":"ssnailham3y@nydailynews.com","gender":"Genderqueer","flagged":false,"year":1989,"created_at":"2003-07-25T01:18:01Z"} {"id":906026542780,"first_name":"Cart","email":"cstledger3z@gizmodo.com","gender":"Genderfluid","flagged":false,"year":2012,"created_at":"2002-10-08T19:52:24Z"} {"id":159926758016,"first_name":"Lazaro","last_name":"Silverthorne","email":"lsilverthorne40@bloomberg.com","gender":"Agender","ip_address":"71.244.2.220","flagged":true,"year":1997,"created_at":"2009-09-18T01:11:57Z"} {"id":524175803030,"first_name":"Wyn","email":"wbootman41@bing.com","gender":"Non-binary","ip_address":"132.98.180.172","flagged":false,"year":2003,"created_at":"2005-10-12T13:09:33Z"} {"id":1020011062335,"first_name":"Maude","last_name":"Steels","email":"msteels42@mashable.com","gender":"Non-binary","ip_address":"237.118.228.207","flagged":true,"year":1986,"created_at":"2020-10-13T06:00:25Z"} {"id":642691581819,"first_name":"Herc","last_name":"Oloman","gender":"Bigender","ip_address":"141.171.91.255","flagged":false} {"id":851958223030,"first_name":"Rodrick","last_name":"Jackling","email":"rjackling44@myspace.com","gender":"Male","ip_address":"25.57.158.151","flagged":true,"year":1984,"created_at":"2002-01-07T12:29:17Z"} {"id":1057049178994,"first_name":"Deina","last_name":"Goalby","email":"dgoalby45@businesswire.com","gender":"Polygender","ip_address":"247.96.67.249","flagged":false,"year":1995,"created_at":"2010-11-26T04:33:27Z"} {"id":1040767675345,"first_name":"Porty","last_name":"Moreno","email":"pmoreno46@state.gov","gender":"Genderfluid","flagged":true,"year":1985,"created_at":"2006-11-21T15:45:32Z"} {"id":979401938029,"first_name":"Alexandro","last_name":"Koschek","email":"akoschek47@europa.eu","gender":"Male","ip_address":"87.126.238.16","flagged":true,"year":2007,"created_at":"2019-09-26T07:10:52Z"} {"id":345229222781,"first_name":"Livvie","last_name":"Brussels","email":"lbrussels48@alexa.com","gender":"Female","flagged":false,"year":2006,"created_at":"2012-07-04T01:48:50Z"} {"id":407138115524,"first_name":"Gracia","last_name":"Samber","email":"gsamber49@indiatimes.com","gender":"Female","ip_address":"52.221.247.69","flagged":true,"year":2012,"created_at":"2003-09-26T05:51:10Z"} {"id":124846755068,"first_name":"Errick","last_name":"Hardy-Piggin","email":"ehardypiggin4a@marketwatch.com","gender":"Male","ip_address":"138.201.86.170","flagged":false,"year":1995,"created_at":"2003-07-12T02:17:13Z"} {"id":935534622934,"first_name":"Rupert","last_name":"Tillett","email":"rtillett4b@loc.gov","gender":"Polygender","flagged":false,"year":2013,"created_at":"2014-09-08T08:06:56Z"} {"id":1106726234060,"first_name":"Cherry","last_name":"Matoshin","email":"cmatoshin4c@creativecommons.org","gender":"Genderfluid","flagged":false,"year":2010,"created_at":"2014-01-25T10:44:09Z"} {"id":703355396773,"first_name":"Anthea","last_name":"Andreu","email":"aandreu4d@wired.com","gender":"Bigender","ip_address":"33.57.84.37","flagged":false,"year":2001,"created_at":"2009-09-09T00:08:31Z"} {"id":1174645005611,"first_name":"Britteny","email":"bshaddock4e@cnbc.com","gender":"Genderqueer","flagged":true,"year":1997,"created_at":"2019-08-06T14:44:17Z"} {"id":249102478386,"first_name":"Hamish","last_name":"Boylin","email":"hboylin4f@stanford.edu","gender":"Female","ip_address":"125.92.232.1","flagged":true,"year":2006,"created_at":"2004-03-05T09:28:06Z"} {"id":673459810992,"first_name":"Briney","last_name":"Reily","email":"breily4g@wired.com","gender":"Polygender","flagged":true,"year":1993,"created_at":"2006-04-17T04:26:52Z"} {"id":612777147022,"first_name":"Cinda","last_name":"Ramshaw","email":"cramshaw4h@furl.net","gender":"Genderqueer","ip_address":"17.140.210.177","flagged":false,"year":1998,"created_at":"2010-02-10T04:08:53Z"} {"id":743208884395,"first_name":"Carly","last_name":"Danieli","email":"cdanieli4i@weather.com","gender":"Male","flagged":false,"year":1993,"created_at":"2001-07-18T18:16:10Z"} {"id":739609629841,"first_name":"Lorette","last_name":"McKim","email":"lmckim4j@pagesperso-orange.fr","gender":"Non-binary","ip_address":"48.158.150.190","flagged":false,"year":1993,"created_at":"2005-11-01T13:11:33Z"} {"id":235245236121,"first_name":"Rodolfo","email":"rivons4k@samsung.com","gender":"Genderqueer","ip_address":"229.49.130.218","flagged":true,"year":2007,"created_at":"2013-07-07T11:28:52Z"} {"id":1230352625156,"first_name":"Doll","last_name":"MacDonell","email":"dmacdonell4l@unicef.org","gender":"Genderqueer","flagged":false,"year":2009,"created_at":"2019-10-01T23:34:58Z"} {"id":710272035305,"first_name":"Sibeal","email":"sbail4m@behance.net","gender":"Agender","flagged":false,"year":1956,"created_at":"2009-07-11T18:33:57Z"} {"id":767473952103,"first_name":"Kinny","last_name":"Wilsee","email":"kwilsee4n@wikipedia.org","gender":"Genderqueer","flagged":false,"year":2006,"created_at":"2006-10-14T02:13:53Z"} {"id":701328556021,"first_name":"Lisle","last_name":"Chestnutt","email":"lchestnutt4o@jalbum.net","gender":"Male","flagged":false,"year":2000,"created_at":"2018-05-25T21:03:11Z"} {"id":843668242551,"first_name":"Alane","last_name":"Lambdin","email":"alambdin4p@foxnews.com","gender":"Male","ip_address":"33.75.173.45","flagged":false,"year":2010,"created_at":"2007-02-17T06:07:04Z"} {"id":473815728201,"first_name":"Kania","last_name":"Macia","email":"kmacia4q@wisc.edu","gender":"Female","flagged":false,"year":2011,"created_at":"2004-03-27T12:00:12Z"} {"id":218935070063,"first_name":"Kip","last_name":"Nesby","email":"knesby4r@reverbnation.com","gender":"Polygender","flagged":false,"year":2009,"created_at":"2011-05-15T13:20:05Z"} {"id":1182247833824,"first_name":"Ervin","last_name":"Baudins","email":"ebaudins4s@irs.gov","gender":"Agender","flagged":false,"year":1995,"created_at":"2020-06-30T09:36:33Z"} {"id":423797931479,"first_name":"Frank","last_name":"Gillard","email":"fgillard4t@state.gov","gender":"Non-binary","ip_address":"94.28.129.137","flagged":true,"year":1994,"created_at":"2006-03-15T17:12:22Z"} {"id":580316574629,"first_name":"Ealasaid","email":"eeannetta4u@addthis.com","gender":"Male","ip_address":"139.43.239.5","flagged":false,"year":1984,"created_at":"2018-01-13T19:00:51Z"} {"id":163449843507,"first_name":"Cherice","last_name":"Lidgely","email":"clidgely4v@abc.net.au","gender":"Non-binary","ip_address":"170.101.224.116","flagged":false,"year":2010,"created_at":"2011-05-18T10:55:40Z"} {"id":1218682864903,"first_name":"Corny","last_name":"Carstairs","email":"ccarstairs4w@paypal.com","gender":"Female","ip_address":"169.158.57.234","flagged":false,"year":2006,"created_at":"2010-04-09T21:50:34Z"} {"id":643064166507,"first_name":"Lucine","last_name":"Schroder","email":"lschroder4x@dmoz.org","gender":"Non-binary","ip_address":"216.166.25.241","flagged":true,"year":1993,"created_at":"2016-04-09T08:59:54Z"} {"id":744328541442,"first_name":"Florella","last_name":"Rumbold","email":"frumbold4y@free.fr","gender":"Polygender","flagged":true,"year":2010,"created_at":"2015-10-27T11:18:48Z"} {"id":815039578683,"first_name":"Debora","last_name":"Scole","email":"dscole4z@jigsy.com","gender":"Agender","ip_address":"39.157.146.69","flagged":true,"year":1996,"created_at":"2007-05-26T02:27:39Z"} {"id":1049472093576,"first_name":"Saxon","last_name":"Dufer","email":"sdufer50@feedburner.com","gender":"Bigender","ip_address":"198.166.141.37","flagged":false,"year":1997,"created_at":"2011-12-26T21:11:46Z"} {"id":843379151732,"first_name":"Burlie","last_name":"O'Bruen","email":"bobruen51@fc2.com","gender":"Non-binary","ip_address":"112.31.183.187","flagged":false,"year":2000,"created_at":"2019-09-16T12:45:54Z"} {"id":892855835148,"first_name":"Merl","last_name":"Cruz","email":"mcruz52@china.com.cn","gender":"Non-binary","flagged":false,"year":2004,"created_at":"2018-12-08T07:32:08Z"} {"id":499440235722,"first_name":"Obadias","last_name":"Robet","email":"orobet53@blogger.com","gender":"Bigender","ip_address":"27.189.182.43","flagged":false,"year":2008,"created_at":"2013-07-21T06:14:27Z"} {"id":180288149129,"first_name":"Berty","email":"bamyes54@1und1.de","gender":"Non-binary","ip_address":"146.18.200.142","flagged":false,"year":1988,"created_at":"2009-08-08T13:31:15Z"} {"id":750415637632,"first_name":"Abbott","last_name":"Kendle","email":"akendle55@nih.gov","gender":"Polygender","ip_address":"129.75.228.85","flagged":true,"year":1993,"created_at":"2020-08-09T23:12:58Z"} {"id":154701944484,"first_name":"Titos","last_name":"Middas","gender":"Genderfluid","ip_address":"150.90.40.255","flagged":true} {"id":169958143763,"first_name":"Hyacinthie","last_name":"Josovitz","email":"hjosovitz57@histats.com","gender":"Polygender","ip_address":"36.4.3.232","flagged":true,"year":1999,"created_at":"2007-06-25T05:27:26Z"} {"id":447926003365,"first_name":"Alvy","last_name":"De Carteret","email":"adecarteret58@godaddy.com","ip_address":"72.32.169.165","flagged":false,"year":1998,"created_at":"2001-10-13T04:07:18Z"} {"id":1113249335532,"first_name":"Martino","last_name":"Philpotts","email":"mphilpotts59@sourceforge.net","gender":"Non-binary","ip_address":"216.54.24.4","flagged":false,"year":1998,"created_at":"2009-04-19T11:58:47Z"} {"id":227565080323,"first_name":"Silvain","last_name":"Caller","email":"scaller5a@sciencedirect.com","gender":"Non-binary","flagged":false,"year":1996,"created_at":"2019-08-09T18:36:24Z"} {"id":144878722347,"first_name":"Mara","last_name":"Corringham","email":"mcorringham5b@ca.gov","gender":"Male","flagged":true,"year":1996,"created_at":"2007-02-23T07:08:39Z"} {"id":1061278022272,"first_name":"Sammy","last_name":"Sorel","email":"ssorel5c@soup.io","gender":"Bigender","ip_address":"151.226.87.133","flagged":false,"year":2000,"created_at":"2004-07-04T02:58:34Z"} {"id":853168673543,"first_name":"Lonni","last_name":"Mungin","email":"lmungin5d@google.es","gender":"Polygender","ip_address":"227.125.252.255","flagged":true,"year":2005,"created_at":"2006-05-19T03:48:22Z"} {"id":794158395925,"first_name":"Jori","last_name":"Coats","email":"jcoats5e@pcworld.com","gender":"Bigender","flagged":true,"year":2001,"created_at":"2007-03-20T12:31:11Z"} {"id":266835996524,"first_name":"Dorris","last_name":"Leaning","email":"dleaning5f@desdev.cn","flagged":true,"year":1996,"created_at":"2011-11-25T19:37:41Z"} {"id":131386693745,"first_name":"Sheryl","email":"sganderton5g@nifty.com","gender":"Genderqueer","ip_address":"108.0.12.255","flagged":false,"year":1998,"created_at":"2018-12-15T04:45:07Z"} {"id":978016770837,"first_name":"Iggy","last_name":"Pigott","email":"ipigott5h@squarespace.com","gender":"Agender","ip_address":"235.27.25.172","flagged":false,"year":1966,"created_at":"2012-02-10T22:22:04Z"} {"id":1001353027131,"first_name":"Turner","last_name":"Reder","email":"treder5i@parallels.com","gender":"Polygender","ip_address":"28.141.251.33","flagged":true,"year":2002,"created_at":"2015-01-27T04:54:57Z"} {"id":555865483068,"first_name":"Gerta","last_name":"Petersen","email":"gpetersen5j@ox.ac.uk","gender":"Polygender","ip_address":"106.168.116.94","flagged":false,"year":2008,"created_at":"2011-08-16T09:53:50Z"} {"id":1192620976159,"first_name":"Dominic","last_name":"Rignall","email":"drignall5k@xrea.com","gender":"Agender","ip_address":"16.136.150.59","flagged":true,"year":1987,"created_at":"2019-06-28T09:26:36Z"} {"id":270109683579,"first_name":"Winna","last_name":"Sexty","email":"wsexty5l@yale.edu","gender":"Bigender","ip_address":"219.173.225.186","flagged":true,"year":1994,"created_at":"2005-09-09T01:05:37Z"} {"id":1183220682571,"first_name":"Uriel","last_name":"Jozefiak","email":"ujozefiak5m@blog.com","gender":"Non-binary","flagged":true,"year":1971,"created_at":"2017-01-02T12:06:28Z"} {"id":694398822699,"first_name":"Itch","last_name":"Swales","email":"iswales5n@disqus.com","gender":"Genderqueer","flagged":false,"year":2005,"created_at":"2011-12-15T22:53:29Z"} {"id":193880013220,"first_name":"Jodee","last_name":"Faraday","email":"jfaraday5o@hostgator.com","gender":"Bigender","ip_address":"75.43.156.201","flagged":false,"year":2000,"created_at":"2014-09-07T23:53:33Z"} {"id":840551328968,"first_name":"Susy","last_name":"Greenway","email":"sgreenway5p@cornell.edu","gender":"Genderqueer","ip_address":"209.9.98.128","flagged":true,"year":1992,"created_at":"2019-05-06T04:48:26Z"} {"first_name":"Carmel","last_name":"Linley","email":"clinley5q@bing.com","gender":"Genderqueer","ip_address":"33.199.215.246","year":1984,"created_at":"2005-02-24T04:41:39Z"} {"id":991576770614,"first_name":"Conway","last_name":"Limming","email":"climming5r@amazon.com","gender":"Genderfluid","flagged":true,"year":2010,"created_at":"2011-04-11T19:55:23Z"} {"id":990288960656,"first_name":"Annadiane","last_name":"Emblow","email":"aemblow5s@artisteer.com","gender":"Female","ip_address":"66.159.97.201","flagged":false,"year":2000,"created_at":"2012-03-17T21:17:41Z"} {"id":877032939024,"first_name":"Abelard","last_name":"Prawle","email":"aprawle5t@elegantthemes.com","gender":"Male","flagged":true,"year":1987,"created_at":"2019-06-03T17:18:00Z"} {"id":878214677951,"first_name":"Sydney","last_name":"Onge","email":"songe5u@twitter.com","gender":"Polygender","ip_address":"177.173.67.146","flagged":false,"year":1997,"created_at":"2006-03-31T16:15:14Z"} {"id":1012736201538,"first_name":"Kimberlyn","last_name":"Bulleyn","email":"kbulleyn5v@vimeo.com","gender":"Agender","ip_address":"12.29.162.163","flagged":true,"year":1979,"created_at":"2020-03-04T22:45:25Z"} {"id":736965990732,"first_name":"Vern","last_name":"Frostdicke","email":"vfrostdicke5w@home.pl","gender":"Bigender","ip_address":"45.231.140.247","flagged":true,"year":2008,"created_at":"2018-04-19T21:19:17Z"} {"id":900044037398,"first_name":"Trixy","last_name":"Whittall","email":"twhittall5x@gizmodo.com","gender":"Genderqueer","ip_address":"11.193.34.183","flagged":true,"year":2002,"created_at":"2017-03-26T22:36:31Z"} {"id":143777017973,"first_name":"Gilburt","last_name":"Djokic","email":"gdjokic5y@sciencedaily.com","gender":"Bigender","flagged":true,"year":1999,"created_at":"2006-09-18T13:23:05Z"} {"id":1029263696743,"first_name":"Selene","last_name":"Gawne","gender":"Genderqueer","flagged":true} {"id":881244566774,"first_name":"Morganica","last_name":"Darcey","email":"mdarcey60@uol.com.br","gender":"Bigender","flagged":false,"year":2005,"created_at":"2010-01-01T01:25:02Z"} {"id":1130692534104,"first_name":"Henka","last_name":"Johansen","email":"hjohansen61@etsy.com","gender":"Bigender","ip_address":"103.129.176.134","flagged":false,"year":2009,"created_at":"2013-01-09T07:36:01Z"} {"id":667574693816,"first_name":"Milena","last_name":"Heaford","email":"mheaford62@quantcast.com","gender":"Polygender","flagged":true,"year":1994,"created_at":"2004-08-25T08:26:34Z"} {"id":185981394116,"first_name":"Johnathon","last_name":"Arrol","email":"jarrol63@usatoday.com","gender":"Genderfluid","flagged":false,"year":2011,"created_at":"2012-08-20T22:35:59Z"} {"id":1215640028865,"first_name":"Franciska","last_name":"Mullany","email":"fmullany64@netlog.com","gender":"Genderqueer","flagged":false,"year":2004,"created_at":"2014-12-16T17:30:43Z"} {"id":180210058392,"first_name":"Adriaens","last_name":"Yukhnev","email":"ayukhnev65@blog.com","gender":"Non-binary","flagged":false,"year":2003,"created_at":"2019-05-11T00:10:36Z"} {"id":880567109595,"first_name":"Yasmin","last_name":"Tudbald","email":"ytudbald66@google.fr","gender":"Female","flagged":false,"year":2009,"created_at":"2003-04-06T07:59:58Z"} {"id":334558262846,"first_name":"Helen","email":"hkyles67@nydailynews.com","gender":"Polygender","flagged":true,"year":2002,"created_at":"2003-09-19T09:27:30Z"} {"id":1193225380678,"first_name":"Kirsteni","last_name":"Joyner","email":"kjoyner68@spiegel.de","gender":"Polygender","ip_address":"65.9.220.57","flagged":true,"year":2013,"created_at":"2015-04-02T09:16:24Z"} {"id":222515965242,"first_name":"Traci","last_name":"Garstang","email":"tgarstang69@yolasite.com","gender":"Male","flagged":true,"year":2000,"created_at":"2017-01-05T04:53:33Z"} {"id":414088697208,"first_name":"Vikki","last_name":"Authers","email":"vauthers6a@biblegateway.com","gender":"Polygender","flagged":true,"year":2002,"created_at":"2006-05-15T01:51:49Z"} {"id":584093543088,"first_name":"Morgan","email":"mgladbach6b@netvibes.com","gender":"Genderfluid","ip_address":"131.106.238.194","flagged":false,"year":2010,"created_at":"2007-01-15T21:46:33Z"} {"id":518181442913,"first_name":"Wayne","email":"wglencrash6c@ed.gov","gender":"Genderqueer","ip_address":"247.109.9.96","flagged":false,"year":2009,"created_at":"2003-09-02T10:46:30Z"} {"id":690738332777,"first_name":"Pip","last_name":"McCalum","gender":"Polygender","ip_address":"7.205.97.108","flagged":false} {"first_name":"Mariann","last_name":"Lumsdall","email":"mlumsdall6e@prweb.com","gender":"Bigender","ip_address":"19.93.237.107","year":1972,"created_at":"2013-10-25T09:22:53Z"} {"id":507730680103,"first_name":"Samuel","last_name":"Keeney","email":"skeeney6f@ameblo.jp","gender":"Genderfluid","ip_address":"89.142.188.176","flagged":true,"year":2002,"created_at":"2005-09-04T22:35:06Z"} {"id":1125471212617,"first_name":"Ilyse","last_name":"Meadway","email":"imeadway6g@cbslocal.com","gender":"Genderqueer","ip_address":"153.25.208.161","flagged":false,"year":1998,"created_at":"2008-11-12T08:13:54Z"} {"id":131236137817,"first_name":"My","last_name":"Schober","email":"mschober6h@lycos.com","gender":"Agender","ip_address":"37.146.145.228","flagged":false,"year":2011,"created_at":"2002-12-14T17:12:40Z"} {"first_name":"Abbott","last_name":"Brignall","email":"abrignall6i@usa.gov","gender":"Female","year":1994,"created_at":"2016-12-29T14:41:34Z"} {"id":1137704980076,"first_name":"Beryle","last_name":"Burtwhistle","gender":"Polygender","ip_address":"161.57.113.187","flagged":false} {"id":1170838715292,"first_name":"Chantalle","last_name":"Gavrielli","email":"cgavrielli6k@cafepress.com","gender":"Bigender","flagged":true,"year":1999,"created_at":"2017-01-19T00:52:30Z"} {"id":645845517790,"first_name":"Bonita","last_name":"Evitts","email":"bevitts6l@unicef.org","gender":"Bigender","flagged":false,"year":2011,"created_at":"2008-11-24T08:06:47Z"} {"id":242829048239,"first_name":"Maryjo","last_name":"Leithgoe","email":"mleithgoe6m@hostgator.com","gender":"Polygender","ip_address":"4.100.173.18","flagged":false,"year":1994,"created_at":"2001-06-19T23:40:11Z"} {"id":270512602154,"first_name":"Darrin","last_name":"Duham","email":"dduham6n@nba.com","gender":"Bigender","flagged":true,"year":2004,"created_at":"2016-02-16T22:12:47Z"} {"id":925037447839,"first_name":"Obed","last_name":"Swapp","email":"oswapp6o@hatena.ne.jp","gender":"Genderqueer","ip_address":"34.205.189.171","flagged":true,"year":2004,"created_at":"2018-07-01T15:38:19Z"} {"id":937964409755,"first_name":"Mendie","last_name":"Indruch","email":"mindruch6p@sbwire.com","gender":"Polygender","flagged":true,"year":2003,"created_at":"2008-12-26T17:06:16Z"} {"id":331751208617,"first_name":"Mata","last_name":"Tuffield","email":"mtuffield6q@amazon.com","flagged":false,"year":2005,"created_at":"2005-06-12T18:48:29Z"} {"id":902171507854,"first_name":"Sally","last_name":"Hinge","email":"shinge6r@woothemes.com","gender":"Polygender","ip_address":"163.95.90.137","flagged":false,"year":2012,"created_at":"2018-04-15T15:26:11Z"} {"id":876653197592,"first_name":"Camala","last_name":"Wield","email":"cwield6s@time.com","gender":"Male","flagged":false,"year":2001,"created_at":"2010-11-15T08:08:01Z"} {"id":885291669711,"first_name":"Freda","last_name":"D'Ruel","email":"fdruel6t@columbia.edu","gender":"Non-binary","ip_address":"120.26.127.50","flagged":true,"year":1994,"created_at":"2015-12-22T14:43:34Z"} {"id":945668448032,"first_name":"Oneida","last_name":"Cansdill","email":"ocansdill6u@howstuffworks.com","gender":"Polygender","ip_address":"200.245.198.128","flagged":true,"year":1993,"created_at":"2001-08-12T22:49:33Z"} {"id":643145181805,"first_name":"Winny","last_name":"Dunabie","email":"wdunabie6v@slate.com","gender":"Female","ip_address":"39.167.111.43","flagged":true,"year":1997,"created_at":"2008-01-07T23:31:03Z"} {"id":959252233666,"first_name":"Sancho","email":"ssavil6w@japanpost.jp","gender":"Non-binary","ip_address":"91.47.55.227","flagged":false,"year":2007,"created_at":"2020-02-13T16:18:24Z"} {"id":1133750784897,"first_name":"Karee","last_name":"Jell","email":"kjell6x@psu.edu","gender":"Male","ip_address":"254.155.94.253","flagged":true,"year":1985,"created_at":"2012-01-10T10:07:26Z"} {"id":740184029047,"first_name":"Marthe","last_name":"Hacquard","email":"mhacquard6y@independent.co.uk","gender":"Non-binary","ip_address":"42.169.225.221","flagged":false,"year":2009,"created_at":"2017-01-11T10:45:51Z"} {"id":1032040930203,"first_name":"Skye","email":"sesplin6z@yale.edu","gender":"Genderqueer","ip_address":"61.88.231.240","flagged":false,"year":1993,"created_at":"2008-12-08T11:09:25Z"} {"id":794917124908,"first_name":"Shellie","last_name":"D'Abbot-Doyle","email":"sdabbotdoyle70@answers.com","gender":"Male","flagged":false,"year":1984,"created_at":"2019-12-27T14:08:46Z"} {"id":1197403512986,"first_name":"Dedra","last_name":"Biskupski","gender":"Bigender","ip_address":"23.107.1.212","flagged":true} {"id":1208523157752,"first_name":"June","last_name":"Mixture","email":"jmixture72@dailymotion.com","gender":"Polygender","ip_address":"162.212.251.232","flagged":true,"year":2009,"created_at":"2019-12-09T15:04:55Z"} {"id":378861506696,"first_name":"George","last_name":"Siviter","email":"gsiviter73@globo.com","gender":"Female","ip_address":"12.152.236.79","flagged":false,"year":1988,"created_at":"2003-05-02T20:57:49Z"} {"id":321197532108,"first_name":"Tanner","last_name":"Dreye","gender":"Male","flagged":false} {"id":597955159427,"first_name":"Evyn","last_name":"Sheeres","email":"esheeres75@arstechnica.com","gender":"Agender","flagged":false,"year":1997,"created_at":"2001-09-22T14:40:55Z"} {"id":124763380611,"first_name":"Erie","last_name":"Bear","email":"ebear76@tuttocitta.it","gender":"Bigender","flagged":true,"year":1992,"created_at":"2017-07-24T08:17:13Z"} {"id":829290234568,"first_name":"Tibold","last_name":"Kienl","email":"tkienl77@dailymotion.com","gender":"Male","ip_address":"134.200.75.30","flagged":true,"year":2008,"created_at":"2009-02-11T20:03:32Z"} {"id":236340544289,"first_name":"Conney","last_name":"Gepp","email":"cgepp78@ihg.com","gender":"Genderqueer","flagged":false,"year":1995,"created_at":"2018-08-27T20:58:38Z"} {"id":331694238236,"first_name":"Shari","last_name":"Stansall","email":"sstansall79@liveinternet.ru","gender":"Male","ip_address":"191.173.170.21","flagged":false,"year":1995,"created_at":"2017-08-22T06:50:17Z"} {"id":190153460628,"first_name":"Brod","email":"bcordier7a@yandex.ru","gender":"Male","flagged":false,"year":2005,"created_at":"2015-09-28T03:47:03Z"} {"id":1102180553540,"first_name":"Nicolina","last_name":"Cathenod","email":"ncathenod7b@multiply.com","gender":"Non-binary","ip_address":"172.198.136.28","flagged":true,"year":2006,"created_at":"2002-07-11T08:10:00Z"} {"id":373604378829,"first_name":"Bartram","last_name":"Walliker","gender":"Polygender","ip_address":"91.253.202.41","flagged":true} {"first_name":"Jillian","last_name":"Mordacai","email":"jmordacai7d@studiopress.com","gender":"Non-binary","year":1991,"created_at":"2005-10-03T00:40:12Z"} {"id":188094611274,"first_name":"Shadow","last_name":"Kensy","email":"skensy7e@gnu.org","gender":"Non-binary","ip_address":"45.174.216.44","flagged":true,"year":1992,"created_at":"2010-05-01T07:03:35Z"} {"id":727384812938,"first_name":"Jayne","last_name":"Belding","email":"jbelding7f@mysql.com","gender":"Agender","ip_address":"60.79.240.219","flagged":false,"year":2001,"created_at":"2018-07-06T04:08:02Z"} {"id":470237078526,"first_name":"Rodger","email":"rpicheford7g@alibaba.com","gender":"Agender","ip_address":"30.183.51.207","flagged":false,"year":1994,"created_at":"2018-02-18T13:09:27Z"} {"id":1058770619958,"first_name":"Rosemary","last_name":"Rumming","email":"rrumming7h@comcast.net","gender":"Female","ip_address":"180.231.150.210","flagged":true,"year":2010,"created_at":"2005-06-12T02:34:17Z"} {"id":516807953705,"first_name":"Karna","last_name":"Bummfrey","email":"kbummfrey7i@washingtonpost.com","gender":"Bigender","flagged":true,"year":2006,"created_at":"2005-03-29T09:46:25Z"} {"id":993677312559,"first_name":"Wendy","last_name":"Welden","email":"wwelden7j@google.it","gender":"Male","flagged":false,"year":2004,"created_at":"2003-04-28T16:08:08Z"} {"id":738005761994,"first_name":"Ginnie","last_name":"Pettiward","email":"gpettiward7k@vkontakte.ru","gender":"Agender","ip_address":"143.100.207.164","flagged":true,"year":2006,"created_at":"2008-01-07T03:19:55Z"} {"id":1161051052631,"first_name":"Gilburt","last_name":"Otton","email":"gotton7l@google.ru","gender":"Bigender","ip_address":"4.62.16.147","flagged":false,"year":2004,"created_at":"2002-01-27T11:52:55Z"} {"first_name":"Ephrem","last_name":"Taffee","email":"etaffee7m@mlb.com","gender":"Genderfluid","year":1996,"created_at":"2001-05-21T04:46:50Z"} {"id":1231939769165,"first_name":"Lorinda","last_name":"Pirrey","email":"lpirrey7n@gnu.org","gender":"Polygender","flagged":false,"year":2000,"created_at":"2007-10-04T05:05:24Z"} {"id":1046788648973,"first_name":"Cloe","last_name":"Martinet","email":"cmartinet7o@list-manage.com","gender":"Genderfluid","ip_address":"41.168.215.193","flagged":true,"year":1997,"created_at":"2014-05-17T12:12:09Z"} {"id":1045591343643,"first_name":"Uriel","last_name":"Slipper","email":"uslipper7p@opera.com","gender":"Non-binary","flagged":true,"year":1992,"created_at":"2014-09-29T12:04:45Z"} {"first_name":"Emily","last_name":"Ollier","email":"eollier7q@va.gov","gender":"Bigender","year":1995,"created_at":"2006-07-30T03:37:49Z"} {"id":1113444808696,"first_name":"Dennis","last_name":"Northey","email":"dnorthey7r@blogtalkradio.com","gender":"Female","ip_address":"132.13.137.41","flagged":false,"year":2003,"created_at":"2013-04-03T23:44:19Z"} {"id":757528432943,"first_name":"Yves","email":"ycantua7s@economist.com","gender":"Non-binary","flagged":false,"year":2006,"created_at":"2010-08-17T04:57:31Z"} {"id":563057758435,"first_name":"Joete","email":"jleimster7t@stumbleupon.com","gender":"Genderqueer","ip_address":"6.180.95.170","flagged":false,"year":1997,"created_at":"2002-01-14T22:01:41Z"} {"id":1043082705908,"first_name":"Noam","last_name":"Greeveson","gender":"Agender","flagged":false} {"id":461897558409,"first_name":"Elise","last_name":"Weafer","email":"eweafer7v@about.com","gender":"Non-binary","ip_address":"30.93.34.31","flagged":false,"year":2005,"created_at":"2002-08-17T09:22:33Z"} {"id":924419941718,"first_name":"Lloyd","last_name":"Sarjent","email":"lsarjent7w@yolasite.com","gender":"Genderfluid","ip_address":"42.214.139.6","flagged":true,"year":2002,"created_at":"2021-01-28T09:01:22Z"} {"id":465564142923,"first_name":"Alli","last_name":"Keers","email":"akeers7x@uol.com.br","gender":"Agender","flagged":false,"year":2012,"created_at":"2017-12-15T22:10:06Z"} {"id":1205592259440,"first_name":"Bryana","last_name":"Faireclough","email":"bfaireclough7y@vimeo.com","gender":"Bigender","ip_address":"167.237.32.107","flagged":false,"year":2011,"created_at":"2017-10-10T12:16:54Z"} {"id":1216727431477,"first_name":"Nesta","last_name":"Wilkison","email":"nwilkison7z@netvibes.com","gender":"Bigender","ip_address":"220.210.50.4","flagged":true,"year":2009,"created_at":"2007-09-02T20:20:42Z"} {"id":670472572139,"first_name":"Ursulina","last_name":"Goldup","email":"ugoldup80@tmall.com","gender":"Non-binary","flagged":true,"year":2005,"created_at":"2006-05-12T21:45:25Z"} {"id":984373967083,"first_name":"Annaliese","last_name":"Tunaclift","email":"atunaclift81@army.mil","gender":"Genderqueer","ip_address":"116.159.207.222","flagged":false,"year":2000,"created_at":"2001-09-12T23:51:48Z"} {"id":317989595228,"first_name":"Eb","last_name":"Vlasenkov","email":"evlasenkov82@imgur.com","gender":"Genderfluid","flagged":true,"year":1993,"created_at":"2006-05-01T21:18:47Z"} {"id":446363310987,"first_name":"D'arcy","last_name":"Axcell","email":"daxcell83@umich.edu","gender":"Polygender","ip_address":"232.175.43.30","flagged":false,"year":2009,"created_at":"2006-05-22T14:58:01Z"} {"id":194263733555,"first_name":"Tony","email":"todeoran84@qq.com","gender":"Genderfluid","flagged":false,"year":2003,"created_at":"2005-12-10T21:58:18Z"} {"id":716480331823,"first_name":"Miller","last_name":"Blatherwick","email":"mblatherwick85@ycombinator.com","gender":"Genderfluid","flagged":false,"year":2010,"created_at":"2016-01-25T15:42:07Z"} {"id":1232566423727,"first_name":"Cooper","last_name":"Franceschino","email":"cfranceschino86@fotki.com","gender":"Bigender","flagged":false,"year":2007,"created_at":"2007-07-15T05:32:42Z"} {"id":539442863605,"first_name":"Codi","gender":"Non-binary","ip_address":"88.45.181.78","flagged":true} {"id":1095349088913,"first_name":"Kai","last_name":"MacKilroe","email":"kmackilroe88@wikipedia.org","gender":"Genderqueer","flagged":false,"year":1965,"created_at":"2009-01-28T14:37:52Z"} {"id":225223514675,"first_name":"Lyell","last_name":"Bradburn","email":"lbradburn89@cisco.com","gender":"Genderfluid","ip_address":"139.150.57.158","flagged":true,"year":1988,"created_at":"2007-11-01T11:14:09Z"} {"id":1231431961431,"first_name":"Dave","last_name":"McWhirter","email":"dmcwhirter8a@yellowpages.com","gender":"Non-binary","ip_address":"167.231.3.213","flagged":false,"year":2011,"created_at":"2003-05-03T14:23:33Z"} {"id":785581496060,"first_name":"Gerrie","last_name":"Eykel","email":"geykel8b@dion.ne.jp","gender":"Agender","ip_address":"170.19.3.93","flagged":true,"year":1987,"created_at":"2015-10-24T13:16:40Z"} {"id":799382736245,"first_name":"Carita","last_name":"Barnwille","email":"cbarnwille8c@hugedomains.com","gender":"Bigender","ip_address":"227.236.58.95","flagged":false,"year":2006,"created_at":"2004-04-27T13:18:35Z"} {"id":281846347453,"first_name":"Sansone","last_name":"Ivanikov","email":"sivanikov8d@myspace.com","gender":"Genderfluid","ip_address":"70.236.136.68","flagged":false,"year":1988,"created_at":"2008-06-09T15:15:40Z"} {"id":1072266953517,"first_name":"Rosamond","last_name":"Varfalameev","email":"rvarfalameev8e@plala.or.jp","gender":"Genderqueer","ip_address":"198.97.161.159","flagged":false,"year":2006,"created_at":"2011-11-04T10:54:19Z"} {"id":289027620544,"first_name":"Sheppard","last_name":"Spadaro","email":"sspadaro8f@dropbox.com","gender":"Agender","flagged":true,"year":2002,"created_at":"2018-08-02T21:24:04Z"} {"id":459585977189,"first_name":"Joyan","last_name":"Van Salzberger","email":"jvansalzberger8g@tinypic.com","gender":"Genderqueer","ip_address":"155.231.73.35","flagged":false,"year":1999,"created_at":"2001-09-08T08:50:21Z"} {"id":256743967505,"first_name":"Beryl","email":"bmcglynn8h@wufoo.com","gender":"Agender","flagged":true,"year":2006,"created_at":"2018-06-09T00:09:24Z"} {"id":440366046345,"first_name":"Cami","last_name":"Gerding","email":"cgerding8i@drupal.org","gender":"Genderfluid","flagged":false,"year":1993,"created_at":"2017-12-26T16:55:28Z"} {"id":767435363870,"first_name":"Electra","email":"emizzi8j@freewebs.com","gender":"Genderfluid","flagged":false,"year":2009,"created_at":"2005-10-17T15:11:32Z"} {"id":706386614556,"first_name":"Ilse","last_name":"Hindes","email":"ihindes8k@ehow.com","gender":"Agender","ip_address":"226.241.255.108","flagged":true,"year":2007,"created_at":"2014-06-25T15:12:20Z"} {"id":453386896290,"first_name":"Wallis","last_name":"Fries","email":"wfries8l@mediafire.com","gender":"Bigender","flagged":true,"year":1991,"created_at":"2017-05-29T15:40:19Z"} {"id":1099622415058,"first_name":"Augustine","email":"abearham8m@ning.com","gender":"Genderqueer","flagged":true,"year":2011,"created_at":"2009-06-17T12:07:55Z"} {"id":325159832643,"first_name":"Vaughan","last_name":"Cockrill","email":"vcockrill8n@techcrunch.com","gender":"Female","ip_address":"157.74.221.77","flagged":true,"year":1994,"created_at":"2014-11-06T21:42:45Z"} {"id":1060205720774,"first_name":"Nolana","last_name":"Mangon","email":"nmangon8o@omniture.com","gender":"Bigender","flagged":true,"year":2009,"created_at":"2018-01-29T18:47:13Z"} {"id":788699535003,"first_name":"Ellene","last_name":"Alti","email":"ealti8p@globo.com","gender":"Polygender","flagged":false,"year":2008,"created_at":"2005-11-11T11:19:35Z"} {"id":369580227914,"first_name":"Eldon","last_name":"Costigan","email":"ecostigan8q@blogger.com","gender":"Non-binary","ip_address":"54.105.19.107","flagged":true,"year":2008,"created_at":"2016-07-19T07:32:36Z"} {"id":1011467594665,"first_name":"Stanly","last_name":"Domegan","email":"sdomegan8r@paginegialle.it","gender":"Male","ip_address":"139.12.182.230","flagged":false,"year":2002,"created_at":"2011-05-28T04:35:34Z"} {"id":1057345923647,"first_name":"Guillema","last_name":"Chezelle","email":"gchezelle8s@blog.com","gender":"Male","ip_address":"243.222.175.78","flagged":false,"year":2001,"created_at":"2003-10-20T20:55:18Z"} {"id":896063976112,"first_name":"Quintina","last_name":"Giurio","email":"qgiurio8t@vk.com","gender":"Non-binary","flagged":false,"year":1999,"created_at":"2002-08-17T10:53:30Z"} {"id":138586088897,"first_name":"Olwen","last_name":"Haslen","email":"ohaslen8u@opera.com","gender":"Genderfluid","flagged":false,"year":1995,"created_at":"2009-12-17T01:58:22Z"} {"id":807423684681,"first_name":"Teodoro","email":"tdiboll8v@businessweek.com","gender":"Non-binary","flagged":true,"year":2007,"created_at":"2012-12-22T13:13:38Z"} {"id":1091307495708,"first_name":"Levin","last_name":"Parkman","email":"lparkman8w@aol.com","gender":"Male","flagged":true,"year":2009,"created_at":"2007-01-06T11:03:18Z"} {"id":626429391384,"first_name":"Jessee","last_name":"Nuth","email":"jnuth8x@psu.edu","gender":"Female","flagged":true,"year":2001,"created_at":"2007-12-29T13:38:39Z"} {"id":723398798804,"first_name":"Verla","last_name":"Staning","email":"vstaning8y@usnews.com","gender":"Bigender","ip_address":"22.190.92.95","flagged":false,"year":2011,"created_at":"2007-05-31T08:13:19Z"} {"id":178752099095,"first_name":"Amos","last_name":"MacFarlan","ip_address":"132.211.108.194","flagged":true} {"id":150719272626,"first_name":"Luca","last_name":"Hartlebury","email":"lhartlebury90@zdnet.com","gender":"Genderfluid","ip_address":"200.84.166.168","flagged":true,"year":2012,"created_at":"2004-08-12T12:44:47Z"} {"id":352394300872,"first_name":"Dilan","email":"ddelwater91@wix.com","gender":"Agender","ip_address":"155.125.227.204","flagged":true,"year":2010,"created_at":"2017-07-19T04:23:42Z"} {"id":967625910119,"first_name":"Gaspard","last_name":"Hugk","email":"ghugk92@free.fr","gender":"Male","ip_address":"49.187.126.153","flagged":false,"year":2000,"created_at":"2009-11-08T21:39:32Z"} {"id":689929643849,"first_name":"Sigismondo","last_name":"Orsman","email":"sorsman93@omniture.com","gender":"Agender","ip_address":"76.221.16.51","flagged":false,"year":2006,"created_at":"2012-08-15T22:33:52Z"} {"id":1068240872349,"first_name":"Valida","last_name":"Ducarne","email":"vducarne94@nytimes.com","gender":"Bigender","ip_address":"14.249.114.218","flagged":true,"year":2008,"created_at":"2011-08-22T17:17:47Z"} {"id":253601824690,"first_name":"Kali","last_name":"Grandison","email":"kgrandison95@cmu.edu","gender":"Genderqueer","flagged":true,"year":2006,"created_at":"2005-09-15T19:14:55Z"} {"id":971478669564,"first_name":"Audry","last_name":"Sulter","email":"asulter96@cbsnews.com","gender":"Polygender","ip_address":"70.90.172.83","flagged":false,"year":1996,"created_at":"2016-08-03T10:28:50Z"} {"id":359262230940,"first_name":"Brandea","last_name":"Hollyer","email":"bhollyer97@zdnet.com","gender":"Bigender","ip_address":"252.188.204.176","flagged":false,"year":2008,"created_at":"2015-03-30T13:32:33Z"} {"id":505759420204,"first_name":"Ronnica","last_name":"Gauntley","email":"rgauntley98@edublogs.org","gender":"Agender","ip_address":"171.94.255.166","flagged":false,"year":2006,"created_at":"2002-05-15T23:31:35Z"} {"id":579857908968,"first_name":"Frederick","last_name":"Oliff","email":"foliff99@europa.eu","gender":"Bigender","ip_address":"124.113.97.227","flagged":true,"year":1991,"created_at":"2016-10-13T15:40:07Z"} {"id":395659014433,"first_name":"Willey","last_name":"Ruben","email":"wruben9a@mail.ru","gender":"Genderfluid","flagged":false,"year":1984,"created_at":"2007-08-22T23:34:43Z"} {"id":606893090477,"first_name":"Sid","last_name":"O' Mullane","email":"somullane9b@nature.com","gender":"Female","flagged":true,"year":2005,"created_at":"2014-03-13T21:33:18Z"} {"id":623148479796,"first_name":"Janot","last_name":"Byrch","email":"jbyrch9c@tmall.com","gender":"Genderqueer","flagged":false,"year":1984,"created_at":"2007-03-01T16:21:56Z"} {"id":1006451953590,"first_name":"Hewe","last_name":"Langston","email":"hlangston9d@bloglines.com","gender":"Non-binary","ip_address":"25.193.114.7","flagged":true,"year":1996,"created_at":"2013-08-27T09:55:00Z"} {"first_name":"Chick","last_name":"Corck","email":"ccorck9e@mozilla.com","gender":"Genderqueer","ip_address":"195.102.52.142","year":2011,"created_at":"2019-12-12T23:55:07Z"} {"id":470663789130,"first_name":"Isabelita","last_name":"Ouchterlony","email":"iouchterlony9f@irs.gov","gender":"Agender","ip_address":"119.139.34.183","flagged":false,"year":2009,"created_at":"2011-08-18T03:51:52Z"} {"id":1219619411195,"first_name":"Reinaldo","email":"rivakhno9g@unc.edu","gender":"Polygender","ip_address":"86.38.16.82","flagged":false,"year":2013,"created_at":"2016-09-08T19:49:06Z"} {"id":1077655893903,"first_name":"Theobald","last_name":"Normansell","email":"tnormansell9h@wisc.edu","gender":"Genderfluid","flagged":true,"year":2009,"created_at":"2014-11-15T21:36:55Z"} {"id":1007319065668,"first_name":"Shurlock","last_name":"Prestland","email":"sprestland9i@cpanel.net","gender":"Male","flagged":false,"year":2007,"created_at":"2019-11-01T09:12:07Z"} {"id":1033121336895,"first_name":"Liam","last_name":"Burgon","email":"lburgon9j@npr.org","gender":"Male","flagged":false,"year":2002,"created_at":"2012-01-11T20:42:19Z"} {"id":758794217580,"first_name":"Benjamin","last_name":"Relton","email":"brelton9k@wired.com","gender":"Polygender","flagged":true,"year":2006,"created_at":"2006-10-19T19:15:13Z"} {"id":1202592824783,"first_name":"Quincey","last_name":"Brea","email":"qbrea9l@cbc.ca","gender":"Polygender","ip_address":"131.66.96.236","flagged":false,"year":2012,"created_at":"2006-05-14T05:30:30Z"} {"id":801944073349,"first_name":"Misty","last_name":"Auston","email":"mauston9m@cam.ac.uk","gender":"Polygender","ip_address":"239.252.124.120","flagged":false,"year":1995,"created_at":"2016-09-18T03:22:18Z"} {"id":612873423113,"first_name":"Lizzie","last_name":"Audsley","email":"laudsley9n@fotki.com","gender":"Genderqueer","flagged":false,"year":1997,"created_at":"2009-02-18T18:43:21Z"} {"id":185164097624,"first_name":"Ronnica","email":"rpina9o@arstechnica.com","gender":"Agender","flagged":true,"year":1966,"created_at":"2021-01-14T13:08:33Z"} {"id":248528475905,"first_name":"Myrvyn","last_name":"Howie","email":"mhowie9p@nsw.gov.au","gender":"Genderfluid","ip_address":"118.1.21.90","flagged":true,"year":1992,"created_at":"2016-09-16T06:33:47Z"} {"id":597505516503,"first_name":"Lewes","last_name":"Mepham","email":"lmepham9q@yale.edu","gender":"Non-binary","ip_address":"182.103.92.227","flagged":false,"year":2009,"created_at":"2009-12-04T21:35:43Z"} {"id":132454615910,"first_name":"Wilden","last_name":"Roslen","email":"wroslen9r@whitehouse.gov","gender":"Female","ip_address":"32.78.68.44","flagged":false,"year":2006,"created_at":"2012-08-20T12:43:43Z"} {"id":522958897253,"first_name":"Franky","last_name":"Fishly","email":"ffishly9s@vimeo.com","gender":"Genderfluid","ip_address":"250.225.71.80","flagged":true,"year":1990,"created_at":"2005-08-21T04:47:12Z"} {"id":352715193565,"first_name":"Brantley","last_name":"Dziwisz","email":"bdziwisz9t@china.com.cn","gender":"Genderfluid","flagged":true,"year":2002,"created_at":"2005-05-30T09:37:01Z"} {"id":152665800939,"first_name":"Ardella","last_name":"Bushe","email":"abushe9u@cloudflare.com","gender":"Bigender","ip_address":"135.114.255.169","flagged":false,"year":1995,"created_at":"2004-02-09T05:51:00Z"} {"id":238085272244,"first_name":"Margeaux","email":"mthundercliffe9v@cbsnews.com","gender":"Agender","ip_address":"93.36.32.231","flagged":false,"year":2004,"created_at":"2010-12-10T22:59:30Z"} {"id":1066139091336,"first_name":"Pincas","last_name":"Hayward","email":"phayward9w@addthis.com","gender":"Bigender","ip_address":"234.115.6.50","flagged":false,"year":2001,"created_at":"2013-05-10T22:19:08Z"} {"id":911549212045,"first_name":"Billie","last_name":"Brackley","email":"bbrackley9x@opera.com","gender":"Genderqueer","ip_address":"227.11.153.193","flagged":false,"year":1989,"created_at":"2011-08-11T12:52:49Z"} {"id":1190122483863,"first_name":"Kathleen","last_name":"Ashment","email":"kashment9y@mediafire.com","gender":"Agender","flagged":true,"year":2009,"created_at":"2019-01-29T00:03:50Z"} {"id":877832424684,"first_name":"Manny","last_name":"Favel","gender":"Bigender","flagged":false} {"id":746966900514,"first_name":"Nellie","last_name":"Patroni","email":"npatronia0@list-manage.com","gender":"Genderqueer","ip_address":"163.142.128.33","flagged":false,"year":2001,"created_at":"2020-05-28T03:15:21Z"} {"id":616245960689,"first_name":"Julius","last_name":"Massimi","email":"jmassimia1@intel.com","gender":"Agender","ip_address":"79.244.48.170","flagged":true,"year":1992,"created_at":"2016-01-20T03:18:36Z"} {"id":429593983109,"first_name":"Franky","last_name":"O'Driscoll","email":"fodriscolla2@archive.org","gender":"Female","ip_address":"191.23.217.33","flagged":true,"year":2006,"created_at":"2017-12-21T22:05:18Z"} {"id":820425976405,"first_name":"Jerry","last_name":"Malyon","email":"jmalyona3@hc360.com","gender":"Agender","flagged":false,"year":2011,"created_at":"2008-12-20T14:43:58Z"} {"id":304860221130,"first_name":"Raquela","last_name":"Pasterfield","email":"rpasterfielda4@elpais.com","gender":"Agender","ip_address":"93.75.69.60","flagged":false,"year":1996,"created_at":"2016-01-06T12:13:42Z"} {"id":419721856480,"first_name":"Perry","email":"pmcveightya5@imageshack.us","gender":"Non-binary","flagged":true,"year":2005,"created_at":"2020-05-09T17:31:11Z"} {"id":1156076686851,"first_name":"Merline","last_name":"O'Calleran","email":"mocallerana6@unc.edu","gender":"Genderqueer","ip_address":"50.49.157.94","flagged":false,"year":1993,"created_at":"2003-01-31T11:08:07Z"} {"id":569532316704,"first_name":"Archaimbaud","last_name":"McVee","email":"amcveea7@hugedomains.com","gender":"Female","ip_address":"148.16.165.62","flagged":true,"year":2004,"created_at":"2004-11-20T23:10:58Z"} {"id":1080226446744,"first_name":"Arline","last_name":"Buckam","email":"abuckama8@uol.com.br","gender":"Male","ip_address":"66.133.109.167","flagged":true,"year":2000,"created_at":"2011-08-16T02:46:59Z"} {"id":613050727718,"first_name":"Berkeley","last_name":"Jacobi","email":"bjacobia9@angelfire.com","gender":"Polygender","ip_address":"195.125.5.1","flagged":false,"year":2006,"created_at":"2003-04-17T10:25:00Z"} {"id":1087764849141,"first_name":"Hastings","email":"hsinnockaa@cargocollective.com","gender":"Non-binary","ip_address":"82.134.223.161","flagged":true,"year":2002,"created_at":"2008-09-23T19:57:18Z"} {"id":715198360584,"first_name":"Glenine","last_name":"Tammadge","email":"gtammadgeab@bigcartel.com","gender":"Male","ip_address":"206.75.8.0","flagged":true,"year":1998,"created_at":"2016-11-23T05:01:42Z"} {"id":648400299646,"first_name":"Sibyl","last_name":"Goundry","email":"sgoundryac@uol.com.br","gender":"Female","flagged":false,"year":1999,"created_at":"2014-03-03T19:44:14Z"} {"id":674191158963,"first_name":"Wolfy","last_name":"Banghe","email":"wbanghead@soup.io","gender":"Male","flagged":true,"year":2008,"created_at":"2012-05-07T05:15:38Z"} {"id":471187908965,"first_name":"Gwennie","last_name":"Waything","email":"gwaythingae@istockphoto.com","gender":"Agender","flagged":true,"year":1981,"created_at":"2014-02-04T07:18:24Z"} {"id":751369244945,"first_name":"Misha","last_name":"Wolford","email":"mwolfordaf@opensource.org","gender":"Male","flagged":true,"year":2006,"created_at":"2017-02-15T22:11:01Z"} {"id":799236355240,"first_name":"Marlena","last_name":"De Dei","gender":"Polygender","flagged":false} {"id":409172185483,"first_name":"Harriette","last_name":"Gianolo","email":"hgianoloah@foxnews.com","gender":"Bigender","ip_address":"30.63.88.188","flagged":false,"year":1995,"created_at":"2017-12-21T16:13:20Z"} {"id":778548960087,"first_name":"Kimble","last_name":"Dymock","email":"kdymockai@salon.com","gender":"Agender","ip_address":"93.237.28.70","flagged":true,"year":1994,"created_at":"2005-08-30T05:03:18Z"} {"id":152525768179,"first_name":"Lela","last_name":"Berceros","email":"lbercerosaj@yelp.com","gender":"Non-binary","ip_address":"84.135.126.205","flagged":false,"year":1954,"created_at":"2008-04-21T08:24:28Z"} {"id":219910373450,"first_name":"Cassandre","last_name":"Offer","email":"cofferak@wikipedia.org","gender":"Female","ip_address":"235.169.9.173","flagged":false,"year":2012,"created_at":"2001-09-09T14:59:11Z"} {"id":822282696536,"first_name":"King","last_name":"Koppelmann","email":"kkoppelmannal@scribd.com","gender":"Bigender","ip_address":"215.201.102.140","flagged":false,"year":1997,"created_at":"2013-10-11T08:57:19Z"} {"id":128229107861,"first_name":"Raddy","last_name":"McCreedy","email":"rmccreedyam@yandex.ru","gender":"Genderqueer","ip_address":"2.139.80.25","flagged":true,"year":1989,"created_at":"2002-03-18T03:07:35Z"} {"id":209570929791,"first_name":"Quintus","last_name":"Vankeev","email":"qvankeevan@wordpress.com","gender":"Male","flagged":false,"year":2006,"created_at":"2018-06-27T19:23:02Z"} {"id":1200730719554,"first_name":"Darryl","last_name":"Petracci","email":"dpetracciao@time.com","gender":"Agender","ip_address":"166.74.213.239","flagged":false,"year":2012,"created_at":"2009-03-23T10:13:13Z"} {"id":320847796025,"first_name":"Minna","last_name":"Sargood","email":"msargoodap@pbs.org","gender":"Polygender","ip_address":"122.74.22.246","flagged":false,"year":2001,"created_at":"2012-12-25T02:34:39Z"} {"id":1215300243768,"first_name":"Rosco","last_name":"Pennacci","email":"rpennacciaq@cbsnews.com","gender":"Agender","ip_address":"253.184.24.161","flagged":false,"year":2007,"created_at":"2014-11-04T12:26:51Z"} {"id":393754302365,"first_name":"Trixi","last_name":"Toppas","email":"ttoppasar@desdev.cn","gender":"Female","flagged":true,"year":2005,"created_at":"2016-12-16T04:45:39Z"} {"id":556928758249,"first_name":"Adelbert","email":"areynishas@wikia.com","gender":"Genderfluid","ip_address":"88.70.51.187","flagged":false,"year":2001,"created_at":"2009-06-02T20:25:10Z"} {"id":1161100614229,"first_name":"Madelina","last_name":"Ferrillo","email":"mferrilloat@narod.ru","gender":"Bigender","ip_address":"17.251.158.87","flagged":false,"year":2007,"created_at":"2015-11-08T14:31:44Z"} {"id":1105316842641,"first_name":"Trudie","last_name":"Gorriessen","ip_address":"144.229.110.89","flagged":true} {"id":449935143842,"first_name":"Kristal","last_name":"Eisikowitch","email":"keisikowitchav@ycombinator.com","gender":"Genderqueer","ip_address":"208.103.97.244","flagged":true,"year":2005,"created_at":"2002-12-20T04:58:10Z"} {"id":920802402999,"first_name":"Mahala","last_name":"Sparwell","email":"msparwellaw@senate.gov","gender":"Male","ip_address":"174.252.29.118","flagged":false,"year":2000,"created_at":"2010-10-14T16:37:48Z"} {"id":160090251754,"first_name":"Demetrius","last_name":"Richfield","email":"drichfieldax@foxnews.com","gender":"Polygender","ip_address":"104.231.67.239","flagged":true,"year":2011,"created_at":"2004-12-21T11:40:55Z"} {"id":468792865771,"first_name":"Fayette","last_name":"Hardage","email":"fhardageay@independent.co.uk","gender":"Male","flagged":true,"year":1999,"created_at":"2004-01-02T01:18:03Z"} {"id":1131320025338,"first_name":"Rycca","last_name":"Wheelton","email":"rwheeltonaz@tmall.com","gender":"Polygender","flagged":false,"year":1997,"created_at":"2008-05-27T14:45:36Z"} {"id":499267519046,"first_name":"Stuart","last_name":"Kiffin","email":"skiffinb0@yolasite.com","gender":"Genderqueer","flagged":false,"year":2011,"created_at":"2015-11-24T23:25:24Z"} {"id":374199936158,"first_name":"Gweneth","last_name":"O'Quin","email":"goquinb1@nba.com","gender":"Female","flagged":false,"year":2005,"created_at":"2019-03-11T13:26:06Z"} {"id":906819375302,"first_name":"Adan","last_name":"Lind","email":"alindb2@chicagotribune.com","gender":"Male","ip_address":"81.40.4.71","flagged":true,"year":2003,"created_at":"2016-08-16T01:52:30Z"} {"id":950441248150,"first_name":"Sissie","last_name":"Wilding","email":"swildingb3@wikispaces.com","gender":"Genderqueer","flagged":true,"year":2000,"created_at":"2003-01-06T16:36:45Z"} {"id":152117484292,"first_name":"Son","last_name":"Rowland","email":"srowlandb4@youtu.be","gender":"Agender","flagged":false,"year":1992,"created_at":"2011-12-12T00:00:03Z"} {"id":300556640874,"first_name":"Temple","last_name":"Daftor","email":"tdaftorb5@ted.com","gender":"Non-binary","ip_address":"239.13.56.152","flagged":false,"year":2011,"created_at":"2003-02-23T08:34:53Z"} {"id":435826703457,"first_name":"Sissy","last_name":"MacAulay","email":"smacaulayb6@deliciousdays.com","gender":"Non-binary","flagged":true,"year":2010,"created_at":"2008-07-03T14:34:12Z"} {"id":969817214160,"first_name":"Jodie","last_name":"Banasik","email":"jbanasikb7@blogs.com","gender":"Polygender","ip_address":"113.163.172.36","flagged":false,"year":2006,"created_at":"2015-11-20T09:28:44Z"} {"id":883274413234,"first_name":"Ange","last_name":"Wilds","email":"awildsb8@merriam-webster.com","gender":"Genderqueer","ip_address":"75.243.194.162","flagged":true,"year":1998,"created_at":"2010-04-13T04:39:30Z"} {"id":865660342929,"first_name":"Eunice","last_name":"Ryott","email":"eryottb9@wix.com","gender":"Genderqueer","ip_address":"6.170.232.213","flagged":true,"year":2000,"created_at":"2016-09-09T12:36:30Z"} {"id":1073167123609,"first_name":"Carolus","last_name":"Ciccotti","email":"cciccottiba@bandcamp.com","gender":"Non-binary","ip_address":"142.213.67.249","flagged":true,"year":2008,"created_at":"2005-05-15T18:34:00Z"} {"id":929700630203,"first_name":"Josy","last_name":"Mathou","email":"jmathoubb@a8.net","gender":"Male","ip_address":"27.63.133.237","flagged":true,"year":2004,"created_at":"2006-10-12T09:52:46Z"} {"id":695294622069,"first_name":"Wenda","last_name":"Monsey","gender":"Genderfluid","ip_address":"20.223.198.148","flagged":true} {"id":1187645581531,"first_name":"Ashly","email":"aemmetbd@ftc.gov","gender":"Genderqueer","ip_address":"231.204.173.99","flagged":true,"year":2005,"created_at":"2021-02-04T11:04:43Z"} {"id":218086677765,"first_name":"Merna","last_name":"Smogur","email":"msmogurbe@php.net","gender":"Genderqueer","ip_address":"162.172.53.161","flagged":true,"year":2000,"created_at":"2011-02-04T07:31:55Z"} {"id":652247108517,"first_name":"Noby","email":"ningamellsbf@ucla.edu","gender":"Male","ip_address":"253.61.215.171","flagged":true,"year":1987,"created_at":"2002-06-06T10:34:29Z"} {"id":1056174232088,"first_name":"Paloma","email":"ppettefordbg@feedburner.com","gender":"Agender","ip_address":"121.143.48.95","flagged":false,"year":2010,"created_at":"2001-05-18T12:13:43Z"} {"id":666688364749,"first_name":"Ximenes","last_name":"Rames","email":"xramesbh@nytimes.com","gender":"Female","ip_address":"15.88.78.246","flagged":true,"year":2009,"created_at":"2001-09-08T08:09:33Z"} {"id":310448673540,"first_name":"Hersch","last_name":"Kordt","email":"hkordtbi@ucoz.com","gender":"Non-binary","ip_address":"128.217.9.130","flagged":false,"year":2002,"created_at":"2011-01-28T15:31:57Z"} {"id":474728116254,"first_name":"Lincoln","last_name":"Josefs","email":"ljosefsbj@slideshare.net","gender":"Genderfluid","ip_address":"122.91.134.25","flagged":true,"year":2001,"created_at":"2009-10-30T16:47:19Z"} {"id":826652585833,"first_name":"Cheri","last_name":"Scargill","email":"cscargillbk@tmall.com","gender":"Bigender","flagged":true,"year":1992,"created_at":"2001-12-28T05:16:09Z"} {"id":977469333157,"first_name":"Blair","email":"bblanshardbl@etsy.com","gender":"Male","flagged":true,"year":2010,"created_at":"2010-01-13T17:34:03Z"} {"id":298373700297,"first_name":"Lanny","last_name":"Shales","email":"lshalesbm@ucsd.edu","gender":"Agender","ip_address":"168.180.38.228","flagged":true,"year":2005,"created_at":"2013-06-29T06:51:51Z"} {"id":338076791400,"first_name":"Germayne","last_name":"Plewes","email":"gplewesbn@timesonline.co.uk","gender":"Female","ip_address":"70.25.209.63","flagged":false,"year":2003,"created_at":"2016-08-04T19:00:27Z"} {"id":344145768314,"first_name":"Junina","last_name":"Hulme","email":"jhulmebo@spiegel.de","gender":"Genderfluid","ip_address":"195.211.164.212","flagged":false,"year":1995,"created_at":"2002-05-17T01:20:24Z"} {"id":507743334967,"first_name":"Lexi","last_name":"McFayden","email":"lmcfaydenbp@macromedia.com","gender":"Female","ip_address":"75.180.4.175","flagged":false,"year":1986,"created_at":"2012-07-07T14:02:27Z"} {"id":1000905704332,"first_name":"Jacquelin","last_name":"Darch","email":"jdarchbq@nba.com","gender":"Female","flagged":true,"year":1980,"created_at":"2019-12-01T15:28:10Z"} {"first_name":"Clemens","last_name":"Bon","email":"cbonbr@joomla.org","gender":"Polygender","ip_address":"250.174.96.7","year":1991,"created_at":"2002-09-19T01:42:36Z"} {"id":258569584762,"first_name":"Mina","last_name":"Baythrop","email":"mbaythropbs@vinaora.com","gender":"Male","flagged":false,"year":2004,"created_at":"2015-06-19T15:18:13Z"} {"id":129125406541,"first_name":"Micheal","last_name":"Duncan","email":"mduncanbt@fc2.com","gender":"Male","flagged":true,"year":2005,"created_at":"2007-10-25T02:02:32Z"} {"id":536381192883,"first_name":"Clyve","last_name":"Gounel","email":"cgounelbu@businessweek.com","gender":"Polygender","flagged":false,"year":2005,"created_at":"2021-01-08T06:30:35Z"} {"id":323088791917,"first_name":"Kelli","last_name":"Cowdroy","email":"kcowdroybv@liveinternet.ru","gender":"Female","ip_address":"200.161.129.210","flagged":true,"year":2010,"created_at":"2017-04-01T05:40:08Z"} {"id":470353303955,"first_name":"Kassi","last_name":"Guinane","gender":"Female","flagged":true} {"id":329058913896,"first_name":"Gael","last_name":"Norcliff","email":"gnorcliffbx@hatena.ne.jp","gender":"Female","ip_address":"33.58.117.102","flagged":false,"year":2001,"created_at":"2013-10-06T04:35:33Z"} {"id":760536582831,"first_name":"Clywd","last_name":"Aldred","email":"caldredby@wix.com","gender":"Agender","ip_address":"87.108.33.253","flagged":true,"year":1978,"created_at":"2012-01-11T19:29:34Z"} {"id":881910432090,"first_name":"Dalli","last_name":"Eric","email":"dericbz@tripadvisor.com","gender":"Female","ip_address":"87.23.81.139","flagged":true,"year":2010,"created_at":"2018-10-21T23:39:10Z"} {"id":679064220516,"first_name":"Querida","last_name":"Staunton","email":"qstauntonc0@ibm.com","gender":"Genderfluid","ip_address":"241.197.178.104","flagged":true,"year":1997,"created_at":"2020-05-02T04:00:41Z"} {"id":433943284128,"first_name":"Lissa","email":"lmugglestonec1@quantcast.com","gender":"Bigender","flagged":true,"year":1999,"created_at":"2008-09-05T21:33:37Z"} {"id":1142432380944,"first_name":"Trumann","last_name":"Hourston","email":"thourstonc2@disqus.com","gender":"Female","flagged":true,"year":1988,"created_at":"2015-11-28T02:30:43Z"} {"id":241810827931,"first_name":"Tildy","email":"thandkec3@etsy.com","gender":"Female","ip_address":"99.140.225.172","flagged":true,"year":1994,"created_at":"2006-11-17T12:31:58Z"} {"id":414572748220,"first_name":"Rivy","email":"rmccutcheonc4@goo.ne.jp","gender":"Non-binary","ip_address":"30.24.233.162","flagged":true,"year":1987,"created_at":"2015-02-23T13:15:48Z"} {"id":728584040609,"first_name":"Sebastien","last_name":"de Chastelain","email":"sdechastelainc5@twitter.com","gender":"Bigender","flagged":true,"year":2007,"created_at":"2012-10-24T02:41:20Z"} {"id":731925287229,"first_name":"Etienne","last_name":"Gandey","email":"egandeyc6@si.edu","gender":"Male","flagged":true,"year":2006,"created_at":"2014-01-11T15:07:15Z"} {"id":365731332638,"first_name":"Gibb","email":"ghancockc7@mozilla.com","gender":"Female","ip_address":"97.96.33.129","flagged":false,"year":1968,"created_at":"2011-09-12T03:10:46Z"} {"id":419429317574,"first_name":"Stu","last_name":"Ebsworth","gender":"Non-binary","flagged":false} {"id":282396573782,"first_name":"Nissy","last_name":"Wagnerin","email":"nwagnerinc9@about.me","gender":"Non-binary","ip_address":"190.239.60.159","flagged":false,"year":2012,"created_at":"2015-07-19T14:57:23Z"} {"id":379953678469,"first_name":"Sayres","email":"sescrittca@blogspot.com","gender":"Male","flagged":false,"year":2004,"created_at":"2004-11-07T19:36:38Z"} {"id":792688527738,"first_name":"Wat","last_name":"Wasielewski","email":"wwasielewskicb@tamu.edu","gender":"Genderfluid","ip_address":"133.64.101.251","flagged":false,"year":2011,"created_at":"2003-11-28T19:56:42Z"} {"id":414617596414,"first_name":"Udall","last_name":"Scobie","email":"uscobiecc@comsenz.com","gender":"Agender","ip_address":"169.233.226.190","flagged":true,"year":2004,"created_at":"2002-11-26T15:32:48Z"} {"id":713737646264,"first_name":"Aksel","last_name":"Stanner","email":"astannercd@abc.net.au","gender":"Bigender","ip_address":"118.115.82.247","flagged":false,"year":1998,"created_at":"2011-03-19T09:45:22Z"} {"id":542317252477,"first_name":"Gina","last_name":"Holdren","email":"gholdrence@disqus.com","gender":"Non-binary","flagged":true,"year":1989,"created_at":"2002-06-13T10:10:54Z"} {"id":1061972036763,"first_name":"Christiana","last_name":"Cardillo","email":"ccardillocf@deliciousdays.com","gender":"Bigender","flagged":false,"year":1994,"created_at":"2012-12-09T12:57:03Z"} {"id":558997111024,"first_name":"Vale","last_name":"Hubery","email":"vhuberycg@mediafire.com","gender":"Agender","flagged":false,"year":1989,"created_at":"2002-12-08T23:06:29Z"} {"id":174002259724,"first_name":"Demetrius","last_name":"Gill","email":"dgillch@kickstarter.com","flagged":true,"year":2011,"created_at":"2015-10-20T00:11:49Z"} {"id":1110545654613,"first_name":"Callida","last_name":"Teasell","email":"cteasellci@wired.com","gender":"Female","ip_address":"146.62.178.82","flagged":true,"year":1992,"created_at":"2001-11-27T07:04:05Z"} {"id":965485503472,"first_name":"Harald","last_name":"Larraway","email":"hlarrawaycj@umich.edu","gender":"Non-binary","flagged":false,"year":1998,"created_at":"2007-02-28T11:26:48Z"} {"id":742324517452,"first_name":"Mac","last_name":"Birden","email":"mbirdenck@jigsy.com","gender":"Agender","flagged":false,"year":1980,"created_at":"2017-06-24T20:59:02Z"} {"id":131922558752,"first_name":"Trey","email":"twasscl@google.com.au","gender":"Genderqueer","flagged":true,"year":2007,"created_at":"2016-04-18T14:11:05Z"} {"id":441432155036,"first_name":"Warden","last_name":"Rocca","email":"wroccacm@marketwatch.com","gender":"Polygender","ip_address":"78.185.140.173","flagged":true,"year":1997,"created_at":"2011-10-31T21:09:43Z"} {"id":365650741843,"first_name":"Donaugh","last_name":"Welds","email":"dweldscn@friendfeed.com","gender":"Genderqueer","ip_address":"102.22.104.230","flagged":false,"year":1994,"created_at":"2008-03-29T08:56:06Z"} {"id":201798734750,"first_name":"Rutger","last_name":"Rugge","email":"rruggeco@shinystat.com","gender":"Agender","flagged":true,"year":1996,"created_at":"2008-08-01T13:34:11Z"} {"first_name":"Arvy","email":"adundendalecp@webs.com","gender":"Genderfluid","ip_address":"111.167.68.250","year":1998,"created_at":"2014-09-02T18:16:39Z"} {"id":1189184673457,"first_name":"Farly","last_name":"Pampling","email":"fpamplingcq@storify.com","gender":"Bigender","flagged":true,"year":2012,"created_at":"2015-05-08T19:41:03Z"} {"id":877532452318,"first_name":"Mace","last_name":"Tregonna","email":"mtregonnacr@pbs.org","gender":"Male","ip_address":"11.114.161.110","flagged":true,"year":2009,"created_at":"2003-04-09T22:54:20Z"} {"id":145650841386,"first_name":"Jobey","last_name":"Eddington","email":"jeddingtoncs@dailymotion.com","gender":"Female","ip_address":"50.254.117.68","flagged":true,"year":2005,"created_at":"2020-01-02T09:20:01Z"} {"id":962333742896,"first_name":"Isaak","last_name":"Creak","email":"icreakct@soup.io","gender":"Bigender","flagged":true,"year":2000,"created_at":"2016-04-01T08:13:59Z"} {"id":455464982166,"first_name":"Horacio","last_name":"Cammack","email":"hcammackcu@posterous.com","gender":"Genderfluid","ip_address":"86.73.70.1","flagged":true,"year":1997,"created_at":"2013-05-02T15:47:59Z"} {"id":1068066641056,"first_name":"Shandee","last_name":"Pitkins","email":"spitkinscv@blinklist.com","gender":"Genderqueer","ip_address":"125.70.135.174","flagged":true,"year":1988,"created_at":"2002-11-06T04:09:57Z"} {"id":553998805956,"first_name":"Cassie","last_name":"Hovenden","email":"chovendencw@miibeian.gov.cn","gender":"Agender","flagged":false,"year":1994,"created_at":"2020-09-14T04:18:39Z"} {"id":1054885825756,"first_name":"Crystal","last_name":"Atcheson","email":"catchesoncx@cnbc.com","gender":"Female","flagged":true,"year":2011,"created_at":"2008-03-17T15:36:43Z"} {"id":543317385142,"first_name":"Ellie","last_name":"Emanuele","email":"eemanuelecy@umn.edu","gender":"Female","ip_address":"117.169.75.185","flagged":true,"year":2008,"created_at":"2003-04-24T18:12:54Z"} {"id":433763661004,"first_name":"Elsy","last_name":"Kneath","email":"ekneathcz@ameblo.jp","gender":"Genderfluid","ip_address":"147.26.194.180","flagged":true,"year":2002,"created_at":"2005-11-22T00:14:57Z"} {"id":1192901684059,"first_name":"Gleda","last_name":"Edghinn","email":"gedghinnd0@jimdo.com","gender":"Female","flagged":false,"year":2002,"created_at":"2019-07-17T12:48:47Z"} {"first_name":"Jerrie","last_name":"Downgate","email":"jdowngated1@naver.com","gender":"Male","ip_address":"54.193.2.157","year":2008,"created_at":"2008-04-12T03:00:03Z"} {"id":1168879342710,"first_name":"Tiphanie","last_name":"Harroway","email":"tharrowayd2@apple.com","gender":"Bigender","ip_address":"147.187.220.45","flagged":true,"year":2006,"created_at":"2020-09-19T13:52:53Z"} {"id":515524624507,"first_name":"Wesley","last_name":"Garriock","email":"wgarriockd3@usda.gov","gender":"Genderqueer","flagged":false,"year":2009,"created_at":"2019-06-29T14:39:00Z"} {"id":407451131030,"first_name":"Brooks","last_name":"Crotch","email":"bcrotchd4@spiegel.de","flagged":false,"year":1996,"created_at":"2001-10-16T06:14:55Z"} {"id":1134107942405,"first_name":"Gordie","last_name":"Elliss","email":"gellissd5@weather.com","flagged":false,"year":2003,"created_at":"2019-01-27T14:52:39Z"} {"id":154374342849,"first_name":"Aubrette","last_name":"Apperley","email":"aapperleyd6@bloglines.com","gender":"Non-binary","ip_address":"224.118.90.101","flagged":true,"year":2009,"created_at":"2016-07-31T10:06:28Z"} {"id":947297263175,"first_name":"Graham","last_name":"Moncur","email":"gmoncurd7@pinterest.com","gender":"Agender","ip_address":"171.148.252.192","flagged":true,"year":2008,"created_at":"2007-01-23T06:03:12Z"} {"id":156458371533,"first_name":"Zita","last_name":"Fant","email":"zfantd8@dailymotion.com","gender":"Bigender","flagged":false,"year":2006,"created_at":"2011-07-21T12:00:19Z"} {"id":129116067467,"first_name":"Ellery","last_name":"Gooderick","email":"egooderickd9@hatena.ne.jp","gender":"Genderfluid","ip_address":"121.153.136.243","flagged":true,"year":2011,"created_at":"2006-05-30T17:20:10Z"} {"id":1012337771330,"first_name":"Eugenio","email":"ecordda@dedecms.com","gender":"Non-binary","flagged":true,"year":2011,"created_at":"2009-08-25T10:47:45Z"} {"id":248796985724,"first_name":"Cristobal","email":"cbairstowdb@princeton.edu","gender":"Genderqueer","flagged":true,"year":1994,"created_at":"2015-11-03T19:33:05Z"} {"id":133867879677,"first_name":"Penny","last_name":"Ivens","email":"pivensdc@house.gov","gender":"Genderqueer","flagged":true,"year":1998,"created_at":"2004-06-21T18:48:53Z"} {"id":261463690012,"first_name":"Vida","last_name":"Wethered","email":"vwethereddd@amazon.co.uk","gender":"Female","flagged":true,"year":1986,"created_at":"2011-11-21T05:53:29Z"} {"id":825899619885,"first_name":"Jania","last_name":"Kupis","email":"jkupisde@over-blog.com","gender":"Male","ip_address":"215.132.129.213","flagged":true,"year":2004,"created_at":"2016-01-19T01:49:08Z"} {"id":905269167021,"first_name":"Alphard","last_name":"Dillaway","email":"adillawaydf@1und1.de","gender":"Agender","flagged":false,"year":2003,"created_at":"2011-10-11T20:23:57Z"} {"id":518757966685,"first_name":"Davie","last_name":"Sirmond","email":"dsirmonddg@networksolutions.com","gender":"Male","ip_address":"137.45.134.253","flagged":false,"year":1996,"created_at":"2012-05-19T12:14:54Z"} {"id":1215858320704,"first_name":"Dara","last_name":"Grew","email":"dgrewdh@ameblo.jp","gender":"Non-binary","ip_address":"199.121.158.64","flagged":true,"year":2011,"created_at":"2012-02-15T00:14:19Z"} {"id":1124108499716,"first_name":"Jinny","last_name":"McGrorty","gender":"Female","ip_address":"232.201.130.110","flagged":false} {"id":873114006323,"first_name":"Stanislas","last_name":"Surgeon","email":"ssurgeondj@independent.co.uk","gender":"Genderfluid","ip_address":"126.204.3.90","flagged":true,"year":2008,"created_at":"2003-10-23T00:21:38Z"} {"id":1036592693385,"first_name":"Ophelia","last_name":"Frapwell","email":"ofrapwelldk@berkeley.edu","gender":"Agender","ip_address":"19.186.96.211","flagged":false,"year":2001,"created_at":"2020-08-27T20:39:16Z"} {"id":955162554436,"first_name":"Percy","last_name":"McKellen","email":"pmckellendl@nationalgeographic.com","gender":"Bigender","ip_address":"177.216.152.223","flagged":false,"year":2006,"created_at":"2011-07-31T21:24:00Z"} {"id":947869766722,"first_name":"Sharl","last_name":"Brimner","email":"sbrimnerdm@t.co","gender":"Non-binary","ip_address":"52.112.190.49","flagged":false,"year":2013,"created_at":"2010-03-19T23:24:34Z"} {"id":514935752092,"first_name":"Westbrook","last_name":"Clynter","gender":"Genderfluid","ip_address":"157.127.220.147","flagged":false} {"id":763710958076,"first_name":"Ingaborg","last_name":"Rouge","email":"irougedo@skyrock.com","gender":"Male","flagged":false,"year":2006,"created_at":"2012-06-22T21:35:28Z"} {"id":155848490711,"first_name":"Georgetta","last_name":"Carden","email":"gcardendp@1688.com","gender":"Non-binary","ip_address":"153.217.231.207","flagged":true,"year":1993,"created_at":"2011-09-30T18:20:19Z"} {"id":413390442648,"first_name":"Alison","last_name":"Montford","email":"amontforddq@unblog.fr","ip_address":"118.7.222.6","flagged":true,"year":2009,"created_at":"2005-07-21T10:26:17Z"} {"id":789434776516,"first_name":"Pierrette","last_name":"Bickmore","email":"pbickmoredr@engadget.com","gender":"Genderqueer","ip_address":"191.203.36.199","flagged":true,"year":1970,"created_at":"2020-03-11T04:59:18Z"} {"id":321661532179,"first_name":"Velvet","last_name":"Reddie","email":"vreddieds@huffingtonpost.com","gender":"Genderfluid","ip_address":"94.61.241.53","flagged":false,"year":1998,"created_at":"2002-01-02T17:49:09Z"} {"id":1090221169879,"first_name":"Marlo","last_name":"Barnby","email":"mbarnbydt@shop-pro.jp","gender":"Genderfluid","ip_address":"40.70.245.105","flagged":true,"year":1988,"created_at":"2014-12-09T02:01:13Z"} {"id":475185496766,"first_name":"Rivi","last_name":"Burridge","email":"rburridgedu@blogger.com","gender":"Female","flagged":false,"year":2002,"created_at":"2004-08-16T18:46:00Z"} {"id":861181952718,"first_name":"Maurice","last_name":"Aldus","email":"maldusdv@nba.com","gender":"Non-binary","flagged":false,"year":2013,"created_at":"2004-11-12T14:50:19Z"} {"id":162809703484,"first_name":"Ferris","last_name":"Pasmore","email":"fpasmoredw@ftc.gov","gender":"Female","flagged":true,"year":1985,"created_at":"2003-06-01T14:02:09Z"} {"id":484660741383,"first_name":"Dickie","last_name":"Heskin","email":"dheskindx@bigcartel.com","gender":"Female","ip_address":"141.136.191.141","flagged":true,"year":2005,"created_at":"2004-01-23T20:58:57Z"} {"id":812858144835,"first_name":"Maud","last_name":"Dunbabin","email":"mdunbabindy@cbc.ca","gender":"Agender","ip_address":"118.227.58.171","flagged":true,"year":2008,"created_at":"2017-07-02T00:01:40Z"} {"first_name":"Osbert","last_name":"Phipson","email":"ophipsondz@hud.gov","gender":"Genderfluid","ip_address":"167.1.163.124","year":2005,"created_at":"2010-02-20T13:25:06Z"} {"id":580628765542,"first_name":"Francesca","last_name":"Bambrugh","email":"fbambrughe0@storify.com","gender":"Non-binary","ip_address":"50.75.47.204","flagged":true,"year":1996,"created_at":"2014-04-21T09:13:50Z"} {"first_name":"Emili","last_name":"Cochran","email":"ecochrane1@ocn.ne.jp","gender":"Female","year":1985,"created_at":"2013-07-09T05:02:48Z"} {"id":273862969986,"first_name":"Fancie","email":"feadee2@reuters.com","gender":"Bigender","ip_address":"119.8.111.79","flagged":true,"year":2005,"created_at":"2006-03-03T13:28:51Z"} {"id":388341694862,"first_name":"Wes","last_name":"Merriton","email":"wmerritone3@usgs.gov","gender":"Female","ip_address":"18.224.138.113","flagged":false,"year":1992,"created_at":"2003-07-09T19:06:02Z"} {"id":649988311436,"first_name":"Alphonso","last_name":"Goodship","email":"agoodshipe4@hhs.gov","gender":"Non-binary","ip_address":"97.107.35.168","flagged":false,"year":2012,"created_at":"2009-10-30T04:03:58Z"} {"id":592800580550,"first_name":"Deirdre","last_name":"Grangier","email":"dgrangiere5@about.com","gender":"Male","ip_address":"175.36.126.47","flagged":true,"year":2010,"created_at":"2003-04-28T07:40:39Z"} {"id":512523444598,"first_name":"Boyd","last_name":"Trencher","email":"btrenchere6@addtoany.com","gender":"Male","flagged":false,"year":2011,"created_at":"2001-09-19T03:43:49Z"} {"id":210590561129,"first_name":"Webster","last_name":"Dobbs","email":"wdobbse7@columbia.edu","gender":"Polygender","ip_address":"196.148.91.1","flagged":false,"year":1969,"created_at":"2013-03-05T07:34:15Z"} {"id":660865156816,"first_name":"Cicely","last_name":"Borsi","email":"cborsie8@cnn.com","gender":"Bigender","ip_address":"217.229.231.13","flagged":false,"year":1991,"created_at":"2010-01-31T14:33:20Z"} {"id":1146366090597,"first_name":"Fransisco","last_name":"Orgee","email":"forgeee9@elpais.com","gender":"Agender","ip_address":"203.9.75.105","flagged":false,"year":2008,"created_at":"2003-05-28T19:28:06Z"} {"id":1126523610445,"first_name":"Mariel","last_name":"Gadie","email":"mgadieea@angelfire.com","gender":"Non-binary","ip_address":"96.216.32.194","flagged":false,"year":2011,"created_at":"2008-11-12T20:08:19Z"} {"id":178687419409,"first_name":"Clarey","last_name":"Stoffels","email":"cstoffelseb@barnesandnoble.com","gender":"Bigender","flagged":false,"year":1996,"created_at":"2019-03-13T13:21:27Z"} {"id":589304408015,"first_name":"Fernandina","last_name":"Winsom","email":"fwinsomec@senate.gov","gender":"Non-binary","flagged":true,"year":1998,"created_at":"2012-02-29T06:47:00Z"} {"id":1142959536021,"first_name":"Loretta","last_name":"Jira","email":"ljiraed@chicagotribune.com","gender":"Agender","ip_address":"55.227.29.94","flagged":true,"year":2009,"created_at":"2019-11-26T19:13:35Z"} {"id":902654454084,"first_name":"Lucias","last_name":"Vondracek","email":"lvondracekee@theglobeandmail.com","gender":"Non-binary","ip_address":"155.253.51.18","flagged":true,"year":2004,"created_at":"2009-09-11T00:24:28Z"} {"id":1226867259463,"first_name":"Bobbette","last_name":"Veeler","email":"bveeleref@youku.com","gender":"Polygender","ip_address":"169.166.19.50","flagged":true,"year":1998,"created_at":"2015-02-23T18:43:56Z"} {"id":727333117843,"first_name":"Tim","last_name":"Sinnock","email":"tsinnockeg@seattletimes.com","gender":"Genderqueer","ip_address":"89.157.155.55","flagged":true,"year":2002,"created_at":"2018-06-06T17:47:10Z"} {"id":1211320424553,"first_name":"Gayle","last_name":"Lyddyard","email":"glyddyardeh@symantec.com","gender":"Polygender","flagged":false,"year":2012,"created_at":"2013-06-24T06:42:26Z"} {"id":518241974685,"first_name":"Yuri","last_name":"Luetkemeyers","email":"yluetkemeyersei@w3.org","gender":"Agender","ip_address":"44.151.75.25","flagged":false,"year":1999,"created_at":"2007-10-09T05:21:39Z"} {"id":536262441280,"first_name":"Carri","last_name":"Scarre","gender":"Genderfluid","ip_address":"122.15.240.106","flagged":false} {"id":921883518466,"first_name":"Lotte","last_name":"Stains","email":"lstainsek@china.com.cn","gender":"Genderfluid","flagged":false,"year":1986,"created_at":"2006-10-12T14:33:15Z"} {"id":667925880581,"first_name":"Rubetta","last_name":"Norsister","email":"rnorsisterel@upenn.edu","gender":"Female","ip_address":"12.29.98.253","flagged":false,"year":2003,"created_at":"2001-05-19T12:34:51Z"} {"id":664059325543,"first_name":"Guglielma","last_name":"Newsham","email":"gnewshamem@umich.edu","gender":"Bigender","ip_address":"140.47.122.238","flagged":false,"year":2009,"created_at":"2004-10-20T20:16:59Z"} {"id":1137218316426,"first_name":"Adriane","last_name":"Symondson","gender":"Genderfluid","flagged":true} {"id":188880299402,"first_name":"Ugo","last_name":"Sumpner","gender":"Polygender","ip_address":"182.213.167.121","flagged":false} {"id":394548777784,"first_name":"Lorrie","email":"ldrewittep@prnewswire.com","gender":"Agender","ip_address":"39.28.196.223","flagged":true,"year":2013,"created_at":"2009-12-13T22:23:41Z"} {"id":278137765937,"first_name":"Portie","last_name":"Chritchlow","email":"pchritchloweq@dyndns.org","gender":"Non-binary","ip_address":"54.191.85.201","flagged":false,"year":1974,"created_at":"2002-05-25T05:28:39Z"} {"id":593807944235,"first_name":"Maribeth","last_name":"Rieflin","gender":"Agender","ip_address":"247.156.170.40","flagged":false} {"id":836041773848,"first_name":"Gert","last_name":"Rabidge","email":"grabidgees@people.com.cn","gender":"Genderfluid","flagged":true,"year":2006,"created_at":"2011-04-18T01:15:20Z"} {"id":1068526947913,"first_name":"Maurizia","last_name":"Rhodef","email":"mrhodefet@seattletimes.com","gender":"Non-binary","ip_address":"239.38.9.180","flagged":false,"year":2011,"created_at":"2016-08-12T13:56:51Z"} {"id":568557206601,"first_name":"Floris","last_name":"Malcher","email":"fmalchereu@dmoz.org","gender":"Bigender","flagged":false,"year":2009,"created_at":"2015-11-29T17:50:33Z"} {"id":252235310124,"first_name":"Debi","last_name":"Benedetti","email":"dbenedettiev@phpbb.com","gender":"Female","flagged":false,"year":1992,"created_at":"2011-05-12T14:06:23Z"} {"id":828470997729,"first_name":"Anny","last_name":"Vales","email":"avalesew@twitter.com","gender":"Agender","flagged":true,"year":2003,"created_at":"2001-04-15T14:14:38Z"} {"id":999790611723,"first_name":"Chrissy","last_name":"Kember","email":"ckemberex@unblog.fr","gender":"Male","flagged":true,"year":2008,"created_at":"2017-12-06T12:17:10Z"} {"id":166089124137,"first_name":"Ainslie","last_name":"Kaveney","gender":"Polygender","ip_address":"126.213.64.220","flagged":true} {"id":879099166074,"first_name":"Berton","last_name":"Plenty","email":"bplentyez@webnode.com","gender":"Genderqueer","ip_address":"234.63.217.220","flagged":false,"year":2006,"created_at":"2008-05-01T22:06:42Z"} {"id":332954650442,"first_name":"Frederigo","last_name":"Bale","email":"fbalef0@networkadvertising.org","gender":"Genderqueer","flagged":false,"year":1994,"created_at":"2016-07-17T06:06:16Z"} {"id":1016994143357,"first_name":"Augustine","last_name":"Sissot","email":"asissotf1@time.com","gender":"Polygender","ip_address":"227.228.75.174","flagged":false,"year":1995,"created_at":"2012-04-06T08:43:35Z"} {"id":1210748937440,"first_name":"Angelo","email":"astandfieldf2@phoca.cz","gender":"Bigender","ip_address":"184.226.212.114","flagged":false,"year":1989,"created_at":"2003-10-05T23:23:25Z"} {"id":344309368256,"first_name":"Yehudi","email":"ygarrardf3@yandex.ru","gender":"Genderfluid","flagged":true,"year":1992,"created_at":"2011-06-23T01:13:39Z"} {"id":995868264347,"first_name":"Borg","last_name":"Towe","email":"btowef4@merriam-webster.com","gender":"Genderqueer","flagged":false,"year":1994,"created_at":"2007-12-10T18:21:32Z"} {"id":745649006575,"first_name":"Irma","last_name":"Waller","email":"iwallerf5@bbb.org","gender":"Bigender","flagged":true,"year":2001,"created_at":"2012-05-28T19:16:02Z"} {"id":844319559038,"first_name":"Maddi","last_name":"Trowill","email":"mtrowillf6@dedecms.com","gender":"Polygender","flagged":false,"year":1992,"created_at":"2001-11-21T19:30:20Z"} {"id":553883784916,"first_name":"Nahum","email":"ntryf7@cbc.ca","gender":"Polygender","ip_address":"150.88.146.182","flagged":false,"year":1993,"created_at":"2014-03-05T17:51:01Z"} {"id":726418164928,"first_name":"Axel","email":"ahrinchenkof8@ustream.tv","gender":"Male","flagged":true,"year":2000,"created_at":"2007-02-27T00:14:48Z"} {"id":625628992317,"first_name":"Valaria","last_name":"Middlemiss","email":"vmiddlemissf9@geocities.com","gender":"Polygender","flagged":false,"year":1991,"created_at":"2014-02-16T13:16:36Z"} {"id":127869283887,"first_name":"Shannan","last_name":"Casaro","gender":"Female","ip_address":"103.65.177.235","flagged":true} {"id":392476506483,"first_name":"Eadith","last_name":"Lorkin","email":"elorkinfb@delicious.com","gender":"Agender","flagged":false,"year":2007,"created_at":"2018-02-11T08:18:56Z"} {"id":695059006501,"first_name":"Bobbye","last_name":"Devil","email":"bdevilfc@china.com.cn","gender":"Non-binary","flagged":true,"year":1992,"created_at":"2005-11-03T20:33:53Z"} {"id":784942789247,"first_name":"Kerrin","last_name":"Cheese","email":"kcheesefd@earthlink.net","gender":"Female","ip_address":"194.111.182.125","flagged":false,"year":2007,"created_at":"2010-10-27T20:05:00Z"} {"id":702877824357,"first_name":"Ashla","last_name":"Jore","email":"ajorefe@umich.edu","gender":"Polygender","flagged":false,"year":2001,"created_at":"2014-12-07T04:35:38Z"} {"id":673245881297,"first_name":"Jon","last_name":"Geary","email":"jgearyff@nba.com","gender":"Male","flagged":false,"year":2000,"created_at":"2007-07-20T10:30:18Z"} {"id":179192397676,"first_name":"Malvin","last_name":"Kibard","email":"mkibardfg@amazonaws.com","gender":"Polygender","flagged":true,"year":1999,"created_at":"2011-08-21T00:59:22Z"} {"id":226462351436,"first_name":"Renell","last_name":"Domenico","email":"rdomenicofh@auda.org.au","gender":"Genderqueer","flagged":true,"year":1987,"created_at":"2020-07-05T02:35:19Z"} {"id":387686705104,"first_name":"Phedra","last_name":"Klain","email":"pklainfi@toplist.cz","gender":"Polygender","flagged":false,"year":1991,"created_at":"2008-06-19T04:46:42Z"} {"id":146710241315,"first_name":"Meggi","last_name":"Abdon","email":"mabdonfj@angelfire.com","gender":"Agender","flagged":false,"year":2003,"created_at":"2002-11-16T11:26:38Z"} {"id":1074758015056,"first_name":"Arlina","email":"aflowerdewfk@altervista.org","gender":"Genderqueer","ip_address":"169.249.245.99","flagged":true,"year":2012,"created_at":"2010-10-06T06:00:08Z"} {"id":467697804647,"first_name":"Jameson","last_name":"Edwin","email":"jedwinfl@yellowbook.com","gender":"Genderqueer","flagged":true,"year":1985,"created_at":"2020-10-01T12:13:31Z"} {"id":986554078822,"first_name":"Dewitt","last_name":"Benfell","email":"dbenfellfm@eepurl.com","gender":"Polygender","ip_address":"25.95.58.40","flagged":true,"year":1993,"created_at":"2019-07-30T00:27:38Z"} {"id":265582373783,"first_name":"Joshuah","last_name":"Duchenne","email":"jduchennefn@pen.io","gender":"Male","ip_address":"176.173.175.94","flagged":false,"year":2012,"created_at":"2010-03-30T19:25:00Z"} {"id":1168858814032,"first_name":"Travers","last_name":"Race","email":"tracefo@ebay.com","gender":"Genderfluid","ip_address":"254.151.103.135","flagged":false,"year":1988,"created_at":"2016-01-04T05:05:20Z"} {"id":921978123862,"first_name":"Ernest","last_name":"Stote","email":"estotefp@csmonitor.com","gender":"Agender","flagged":true,"year":1992,"created_at":"2002-05-30T18:14:40Z"} {"id":212975764323,"first_name":"Kira","last_name":"Sawford","email":"ksawfordfq@privacy.gov.au","gender":"Genderqueer","ip_address":"60.40.167.153","flagged":false,"year":2012,"created_at":"2010-03-18T10:52:07Z"} {"id":964516594747,"first_name":"Shanna","last_name":"Riedel","email":"sriedelfr@loc.gov","gender":"Agender","flagged":false,"year":1986,"created_at":"2019-11-07T12:52:03Z"} {"id":494203789068,"first_name":"Theo","last_name":"Fordyce","email":"tfordycefs@ucoz.ru","gender":"Female","ip_address":"28.199.114.16","flagged":true,"year":1987,"created_at":"2002-07-01T18:49:15Z"} {"id":1190803733706,"first_name":"Teirtza","last_name":"Bewshire","email":"tbewshireft@ucoz.ru","gender":"Female","ip_address":"129.46.239.123","flagged":false,"year":2008,"created_at":"2010-09-27T12:50:56Z"} {"id":680571907461,"first_name":"Vonni","last_name":"Giacaponi","email":"vgiacaponifu@accuweather.com","gender":"Polygender","flagged":false,"year":1997,"created_at":"2005-06-13T18:10:48Z"} {"id":318850614608,"first_name":"Marie-jeanne","last_name":"Beckhurst","email":"mbeckhurstfv@arizona.edu","gender":"Bigender","ip_address":"168.32.34.153","flagged":true,"year":1990,"created_at":"2020-12-13T23:55:21Z"} {"id":713148229038,"first_name":"Tami","last_name":"Breeder","email":"tbreederfw@smugmug.com","gender":"Male","ip_address":"12.167.25.107","flagged":true,"year":1997,"created_at":"2019-08-18T20:50:12Z"} {"id":707776633176,"first_name":"Grover","last_name":"Lindholm","email":"glindholmfx@tripadvisor.com","gender":"Polygender","ip_address":"160.174.18.19","flagged":false,"year":1987,"created_at":"2003-03-13T16:25:07Z"} {"id":204877774915,"first_name":"Gussie","last_name":"Flatman","email":"gflatmanfy@washington.edu","gender":"Non-binary","ip_address":"44.15.25.190","flagged":true,"year":2006,"created_at":"2010-01-27T17:06:21Z"} {"id":598674860265,"first_name":"Miriam","last_name":"Asplen","email":"masplenfz@sogou.com","gender":"Bigender","ip_address":"23.128.36.183","flagged":true,"year":2000,"created_at":"2018-12-03T18:23:17Z"} {"id":549999801496,"first_name":"Francesco","last_name":"Chamberlaine","email":"fchamberlaineg0@sitemeter.com","gender":"Female","ip_address":"70.201.172.205","flagged":false,"year":1996,"created_at":"2004-11-29T16:24:46Z"} {"id":347198761655,"first_name":"Dorothy","last_name":"Duffitt","email":"dduffittg1@cbc.ca","gender":"Genderqueer","ip_address":"19.248.141.70","flagged":true,"year":2008,"created_at":"2004-03-02T02:04:15Z"} {"id":615676050867,"first_name":"Worthy","last_name":"Bareham","email":"wbarehamg2@cafepress.com","gender":"Agender","ip_address":"229.76.37.65","flagged":true,"year":2001,"created_at":"2008-12-31T10:40:37Z"} {"id":332741510601,"first_name":"Philipa","last_name":"Bulbeck","email":"pbulbeckg3@sciencedirect.com","gender":"Bigender","flagged":false,"year":1997,"created_at":"2010-10-24T16:21:44Z"} {"id":1189367770155,"first_name":"Hubert","last_name":"Strover","email":"hstroverg4@myspace.com","gender":"Male","flagged":false,"year":1998,"created_at":"2014-09-23T11:08:03Z"} {"id":273674604897,"first_name":"Padriac","last_name":"Sooper","email":"psooperg5@symantec.com","gender":"Polygender","ip_address":"50.59.174.180","flagged":true,"year":1992,"created_at":"2011-10-10T15:54:58Z"} {"id":591236218560,"first_name":"Tyrus","last_name":"Anker","email":"tankerg6@blog.com","gender":"Polygender","flagged":true,"year":1999,"created_at":"2019-02-20T06:54:51Z"} {"id":717653405619,"first_name":"Magda","last_name":"Hewkin","email":"mhewking7@census.gov","ip_address":"192.44.11.38","flagged":true,"year":2004,"created_at":"2017-02-05T16:01:25Z"} {"id":321222350333,"first_name":"Drusi","last_name":"Jacklin","email":"djackling8@bloglovin.com","gender":"Polygender","ip_address":"199.183.59.112","flagged":false,"year":1988,"created_at":"2008-03-24T17:43:37Z"} {"id":502638370043,"first_name":"Benita","last_name":"Duley","email":"bduleyg9@sina.com.cn","gender":"Polygender","ip_address":"220.131.17.98","flagged":true,"year":2002,"created_at":"2005-05-10T05:40:30Z"} {"id":434104344977,"first_name":"Maddi","last_name":"Hassall","email":"mhassallga@qq.com","gender":"Non-binary","ip_address":"241.51.185.79","flagged":false,"year":2011,"created_at":"2015-04-11T23:30:20Z"} {"id":895893226101,"first_name":"Kimmy","last_name":"Bonanno","email":"kbonannogb@utexas.edu","gender":"Polygender","flagged":false,"year":2004,"created_at":"2016-06-27T23:56:37Z"} {"id":813606838725,"first_name":"Kandace","last_name":"Bullick","email":"kbullickgc@moonfruit.com","gender":"Male","ip_address":"111.16.186.208","flagged":true,"year":1998,"created_at":"2007-07-15T06:44:58Z"} {"id":1194813766660,"first_name":"Gabey","last_name":"Quaif","email":"gquaifgd@adobe.com","gender":"Polygender","ip_address":"212.145.179.234","flagged":true,"year":1990,"created_at":"2006-09-20T03:59:25Z"} {"id":533884041776,"first_name":"Danella","last_name":"D'Arrigo","email":"ddarrigoge@pinterest.com","gender":"Bigender","ip_address":"135.73.117.208","flagged":false,"year":1998,"created_at":"2013-06-16T05:59:23Z"} {"id":1136043148967,"first_name":"Asa","last_name":"Hebner","email":"ahebnergf@networksolutions.com","gender":"Female","flagged":false,"year":1998,"created_at":"2020-04-01T08:01:46Z"} {"id":315616919263,"first_name":"Nobe","last_name":"Norcliffe","email":"nnorcliffegg@go.com","gender":"Genderfluid","flagged":false,"year":2003,"created_at":"2011-02-15T14:43:06Z"} {"first_name":"Evvie","last_name":"Landman","email":"elandmangh@hugedomains.com","gender":"Agender","ip_address":"212.91.28.151","year":2010,"created_at":"2006-12-01T17:03:32Z"} {"first_name":"Gardner","last_name":"Burlingame","email":"gburlingamegi@tumblr.com","gender":"Non-binary","year":2012,"created_at":"2018-10-03T01:13:01Z"} {"id":646925464063,"first_name":"Hyatt","last_name":"Toffel","email":"htoffelgj@free.fr","gender":"Female","ip_address":"7.16.254.114","flagged":true,"year":2011,"created_at":"2001-10-11T19:56:21Z"} {"id":755090110715,"first_name":"Matthieu","last_name":"Eake","email":"meakegk@lulu.com","gender":"Non-binary","flagged":false,"year":1982,"created_at":"2020-06-08T23:25:41Z"} {"id":616353545482,"first_name":"Rancell","email":"rmarciskewskigl@squarespace.com","gender":"Female","ip_address":"118.218.39.23","flagged":true,"year":2011,"created_at":"2020-05-15T17:59:34Z"} {"id":886149848438,"first_name":"Kevyn","last_name":"Filipovic","email":"kfilipovicgm@auda.org.au","gender":"Female","ip_address":"86.34.141.157","flagged":true,"year":1996,"created_at":"2007-09-29T06:20:33Z"} {"id":753989932003,"first_name":"Grove","last_name":"Olivia","email":"goliviagn@cnn.com","gender":"Genderqueer","ip_address":"49.206.96.15","flagged":true,"year":1986,"created_at":"2019-05-17T22:10:23Z"} {"id":1171181863443,"first_name":"Dodi","email":"dhallowsgo@jiathis.com","gender":"Polygender","flagged":false,"year":2011,"created_at":"2008-06-20T10:48:44Z"} {"id":910639021226,"first_name":"Grant","last_name":"Greyes","email":"ggreyesgp@i2i.jp","gender":"Male","ip_address":"175.75.221.166","flagged":false,"year":1984,"created_at":"2004-06-25T08:36:42Z"} {"id":715876284348,"first_name":"Guenevere","last_name":"Stiegar","email":"gstiegargq@booking.com","gender":"Female","ip_address":"142.16.136.219","flagged":false,"year":1997,"created_at":"2005-09-26T18:14:24Z"} {"id":913960248362,"first_name":"Tucky","last_name":"Vousden","email":"tvousdengr@tumblr.com","gender":"Bigender","ip_address":"218.17.72.102","flagged":true,"year":1994,"created_at":"2010-11-04T01:37:10Z"} {"id":765551661320,"first_name":"Merrily","last_name":"Grindall","email":"mgrindallgs@nba.com","gender":"Male","flagged":false,"year":2005,"created_at":"2009-11-19T20:33:53Z"} {"id":170901641054,"first_name":"Elroy","last_name":"Askew","email":"easkewgt@geocities.jp","gender":"Polygender","flagged":true,"year":2010,"created_at":"2019-05-08T09:45:52Z"} {"first_name":"Anne","last_name":"Makey","email":"amakeygu@w3.org","gender":"Non-binary","year":2008,"created_at":"2008-03-14T15:39:43Z"} {"id":1185115422856,"first_name":"Eugenia","last_name":"A'Barrow","email":"eabarrowgv@surveymonkey.com","gender":"Polygender","ip_address":"198.49.54.106","flagged":true,"year":2010,"created_at":"2015-05-22T23:03:22Z"} {"id":673157771141,"first_name":"Merry","last_name":"Twydell","email":"mtwydellgw@paypal.com","gender":"Bigender","ip_address":"82.193.56.107","flagged":true,"year":1994,"created_at":"2016-05-09T17:18:01Z"} {"id":893937365525,"first_name":"Borg","last_name":"Wylder","email":"bwyldergx@symantec.com","gender":"Genderqueer","ip_address":"21.229.39.103","flagged":false,"year":2006,"created_at":"2012-09-10T03:15:15Z"} {"id":1005559242306,"first_name":"Riannon","last_name":"Sterte","email":"rstertegy@paypal.com","gender":"Bigender","ip_address":"12.134.252.219","flagged":true,"year":1992,"created_at":"2017-10-12T20:55:32Z"} {"id":390370737495,"first_name":"Orton","last_name":"Howgill","email":"ohowgillgz@spiegel.de","gender":"Agender","ip_address":"131.105.191.134","flagged":true,"year":2000,"created_at":"2001-08-02T06:56:29Z"} {"id":377267764441,"first_name":"Bar","last_name":"Costanza","email":"bcostanzah0@census.gov","gender":"Female","flagged":true,"year":2000,"created_at":"2006-04-24T16:39:16Z"} {"id":308801494168,"first_name":"Merrile","gender":"Genderfluid","ip_address":"192.202.156.155","flagged":false} {"id":643045866748,"first_name":"Irene","last_name":"Tomanek","email":"itomanekh2@thetimes.co.uk","gender":"Polygender","flagged":true,"year":1998,"created_at":"2016-08-24T16:08:47Z"} {"id":1205090320248,"first_name":"Kean","last_name":"Buckney","email":"kbuckneyh3@ask.com","gender":"Agender","ip_address":"101.15.21.159","flagged":true,"year":1999,"created_at":"2005-07-07T10:03:26Z"} {"id":1050303366033,"first_name":"Kassie","last_name":"Deinert","email":"kdeinerth4@tinypic.com","gender":"Agender","flagged":true,"year":2009,"created_at":"2019-09-14T08:28:56Z"} {"id":366875906826,"first_name":"Jaye","last_name":"Pollitt","email":"jpollitth5@huffingtonpost.com","gender":"Non-binary","ip_address":"103.152.14.54","flagged":true,"year":1991,"created_at":"2008-02-03T19:21:12Z"} {"id":697865451335,"first_name":"Bonnie","last_name":"Tillett","email":"btilletth6@independent.co.uk","gender":"Genderfluid","flagged":true,"year":2006,"created_at":"2013-07-14T14:55:38Z"} {"first_name":"Nickola","last_name":"Laker","email":"nlakerh7@cisco.com","gender":"Non-binary","ip_address":"42.134.74.243","year":2006,"created_at":"2002-01-22T05:55:03Z"} {"id":132904880438,"first_name":"Ashil","last_name":"Sainsbury-Brown","email":"asainsburybrownh8@ifeng.com","gender":"Male","ip_address":"134.203.170.236","flagged":false,"year":2000,"created_at":"2018-09-24T08:07:06Z"} {"id":172083220818,"first_name":"Kahaleel","last_name":"Lorkings","email":"klorkingsh9@cbslocal.com","gender":"Bigender","ip_address":"219.236.183.90","flagged":false,"year":1997,"created_at":"2004-09-29T02:29:26Z"} {"id":511902896265,"first_name":"Nikita","email":"ncuckooha@studiopress.com","gender":"Genderfluid","flagged":true,"year":1989,"created_at":"2006-10-23T05:07:29Z"} {"id":901774281785,"first_name":"Auroora","last_name":"Comini","email":"acominihb@cafepress.com","gender":"Male","ip_address":"195.135.163.188","flagged":true,"year":1995,"created_at":"2004-03-26T16:41:41Z"} {"id":1034650873912,"first_name":"Jaclin","last_name":"Rickards","email":"jrickardshc@dyndns.org","gender":"Male","ip_address":"156.140.160.155","flagged":false,"year":1998,"created_at":"2003-09-23T23:06:12Z"} {"id":308158758945,"first_name":"Kessiah","last_name":"Brounfield","email":"kbrounfieldhd@stumbleupon.com","gender":"Female","ip_address":"39.218.59.32","flagged":false,"year":1995,"created_at":"2007-12-07T04:01:55Z"} {"id":1068110013826,"first_name":"Griffin","last_name":"Goodbairn","email":"ggoodbairnhe@google.ca","gender":"Male","flagged":true,"year":2006,"created_at":"2014-05-24T20:30:09Z"} {"id":337740966615,"first_name":"Ardenia","last_name":"McKeefry","email":"amckeefryhf@mapquest.com","gender":"Male","flagged":false,"year":2007,"created_at":"2005-11-23T10:12:32Z"} {"id":867665336769,"first_name":"Eveline","last_name":"Wride","email":"ewridehg@marketwatch.com","gender":"Male","flagged":false,"year":2005,"created_at":"2015-06-08T03:30:52Z"} {"id":677920129305,"first_name":"Tonnie","email":"tkarpehh@dagondesign.com","gender":"Male","flagged":true,"year":2009,"created_at":"2007-08-23T00:35:49Z"} {"id":128974073205,"first_name":"Betta","last_name":"Leagas","email":"bleagashi@icio.us","gender":"Female","flagged":true,"year":2007,"created_at":"2019-05-25T21:05:32Z"} {"id":1041424151450,"first_name":"Mill","last_name":"Sheepy","email":"msheepyhj@live.com","gender":"Agender","flagged":true,"year":1993,"created_at":"2011-03-22T20:49:32Z"} {"id":1228146520166,"first_name":"Alisun","last_name":"Quant","email":"aquanthk@marketwatch.com","gender":"Bigender","flagged":false,"year":1986,"created_at":"2010-07-12T22:46:40Z"} {"id":872822339414,"first_name":"Edwin","last_name":"Askem","email":"easkemhl@imageshack.us","gender":"Polygender","flagged":true,"year":1993,"created_at":"2020-01-31T16:18:28Z"} {"id":1059336832730,"first_name":"Marika","email":"mpeterihm@vimeo.com","gender":"Genderfluid","flagged":false,"year":2012,"created_at":"2003-12-31T05:37:21Z"} {"id":1018528303970,"first_name":"Tadeo","last_name":"Cumberpatch","email":"tcumberpatchhn@fotki.com","gender":"Polygender","flagged":true,"year":1990,"created_at":"2011-06-08T14:23:27Z"} {"id":311314993422,"first_name":"Fabiano","last_name":"Lismer","email":"flismerho@spotify.com","gender":"Agender","flagged":false,"year":2007,"created_at":"2006-09-06T21:36:16Z"} {"id":617818726101,"first_name":"Greer","email":"gesmondehp@yahoo.co.jp","gender":"Bigender","flagged":true,"year":1986,"created_at":"2005-10-02T17:19:43Z"} {"id":637486095239,"first_name":"Javier","email":"jpeaseyhq@naver.com","gender":"Polygender","ip_address":"101.1.71.244","flagged":false,"year":1993,"created_at":"2011-04-15T01:41:11Z"} {"id":550290532379,"first_name":"Floria","last_name":"Battill","email":"fbattillhr@privacy.gov.au","gender":"Bigender","ip_address":"151.144.108.114","flagged":false,"year":2005,"created_at":"2002-02-13T16:46:20Z"} {"id":927000506698,"first_name":"Gabi","last_name":"Warman","email":"gwarmanhs@senate.gov","gender":"Bigender","ip_address":"81.116.33.237","flagged":true,"year":1992,"created_at":"2009-09-17T21:24:22Z"} {"id":1168109353412,"first_name":"Raynor","last_name":"Youthead","email":"ryoutheadht@elpais.com","gender":"Bigender","flagged":true,"year":1996,"created_at":"2012-01-27T06:39:57Z"} {"id":666545587824,"first_name":"Isidro","last_name":"Daal","email":"idaalhu@omniture.com","gender":"Agender","ip_address":"37.202.8.145","flagged":true,"year":1994,"created_at":"2010-12-12T07:06:46Z"} {"id":516735313098,"first_name":"Kerianne","last_name":"Sturgeon","gender":"Polygender","ip_address":"254.99.56.19","flagged":true} {"id":725840937612,"first_name":"Deborah","last_name":"Lauxmann","email":"dlauxmannhw@nhs.uk","gender":"Genderfluid","ip_address":"106.19.205.181","flagged":false,"year":2006,"created_at":"2012-11-30T16:23:33Z"} {"id":892388995661,"first_name":"Ransom","email":"rsnellehx@netvibes.com","gender":"Male","ip_address":"102.152.1.168","flagged":false,"year":2002,"created_at":"2014-07-10T03:34:58Z"} {"id":729813225826,"first_name":"Jobie","last_name":"Braunroth","email":"jbraunrothhy@moonfruit.com","gender":"Female","ip_address":"141.148.180.185","flagged":false,"year":1998,"created_at":"2004-11-21T18:20:39Z"} {"id":315748605955,"first_name":"Ainsley","email":"ahalksworthhz@google.com.au","gender":"Non-binary","flagged":true,"year":1989,"created_at":"2010-03-01T15:16:48Z"} {"id":782056031035,"first_name":"Mayer","email":"mjahani0@ebay.co.uk","gender":"Genderqueer","ip_address":"170.130.96.220","flagged":false,"year":1987,"created_at":"2015-10-31T05:01:30Z"} {"id":974596652703,"first_name":"Fawne","last_name":"Tremblet","email":"ftrembleti1@pinterest.com","gender":"Female","ip_address":"63.37.54.91","flagged":false,"year":2011,"created_at":"2013-09-18T18:22:18Z"} {"id":314317905002,"first_name":"Colan","last_name":"Vurley","email":"cvurleyi2@opensource.org","gender":"Non-binary","ip_address":"178.250.230.162","flagged":false,"year":1997,"created_at":"2021-03-11T12:02:42Z"} {"id":365254963324,"first_name":"Erek","last_name":"Wickins","email":"ewickinsi3@buzzfeed.com","gender":"Agender","ip_address":"101.243.167.240","flagged":false,"year":1995,"created_at":"2016-08-08T00:48:06Z"} {"id":1209898240381,"first_name":"Temp","last_name":"O'Garmen","email":"togarmeni4@studiopress.com","gender":"Female","flagged":false,"year":2007,"created_at":"2007-05-25T10:48:12Z"} {"id":912522841474,"first_name":"Morris","last_name":"Corzor","email":"mcorzori5@360.cn","gender":"Genderqueer","ip_address":"79.247.71.33","flagged":true,"year":1994,"created_at":"2015-11-22T00:10:25Z"} {"id":1070175218980,"first_name":"Penni","last_name":"Cortes","email":"pcortesi6@merriam-webster.com","gender":"Genderqueer","flagged":true,"year":1991,"created_at":"2004-10-19T22:23:19Z"} {"id":1209018053380,"first_name":"Georges","last_name":"Stove","email":"gstovei7@123-reg.co.uk","gender":"Female","ip_address":"159.178.232.94","flagged":false,"year":2000,"created_at":"2002-09-06T16:58:44Z"} {"id":265198251256,"first_name":"Fee","last_name":"Duggon","email":"fduggoni8@mysql.com","gender":"Non-binary","ip_address":"179.47.224.90","flagged":false,"year":2012,"created_at":"2012-10-27T09:38:48Z"} {"id":1070621065261,"first_name":"Mariana","last_name":"Southers","email":"msouthersi9@washingtonpost.com","gender":"Bigender","flagged":true,"year":2009,"created_at":"2009-01-08T15:47:01Z"} {"id":1105896002674,"first_name":"Rutger","last_name":"Meekins","email":"rmeekinsia@hc360.com","gender":"Genderfluid","ip_address":"195.98.179.6","flagged":true,"year":2006,"created_at":"2019-04-20T08:59:03Z"} {"id":1011220887048,"first_name":"Manon","last_name":"Urling","email":"murlingib@hhs.gov","gender":"Male","ip_address":"167.101.228.175","flagged":false,"year":2007,"created_at":"2002-11-07T01:26:47Z"} {"id":147585522809,"first_name":"Rickey","email":"rwhitemanic@abc.net.au","gender":"Polygender","flagged":true,"year":2012,"created_at":"2018-09-27T14:04:05Z"} {"id":471874347398,"first_name":"Martina","last_name":"Quimby","email":"mquimbyid@networkadvertising.org","gender":"Genderfluid","flagged":false,"year":1993,"created_at":"2008-11-22T11:15:55Z"} {"id":453714523202,"first_name":"Bessie","last_name":"Hilbourne","email":"bhilbourneie@microsoft.com","gender":"Bigender","flagged":true,"year":2007,"created_at":"2005-11-20T00:11:22Z"} {"id":206503612171,"first_name":"Leigh","last_name":"Mottley","email":"lmottleyif@technorati.com","gender":"Female","ip_address":"2.48.221.231","flagged":false,"year":2012,"created_at":"2004-07-22T07:56:06Z"} {"id":657084224448,"first_name":"Lennard","last_name":"MacQuaker","email":"lmacquakerig@yahoo.co.jp","gender":"Male","ip_address":"109.181.123.48","flagged":false,"year":2000,"created_at":"2008-12-08T07:37:32Z"} {"id":211037877457,"first_name":"Lois","last_name":"Farherty","email":"lfarhertyih@bloomberg.com","gender":"Male","ip_address":"89.224.17.222","flagged":false,"year":2010,"created_at":"2009-01-24T02:07:05Z"} {"id":921558958012,"first_name":"Koressa","last_name":"Waszczykowski","email":"kwaszczykowskiii@trellian.com","gender":"Male","ip_address":"82.48.49.50","flagged":true,"year":2007,"created_at":"2015-12-14T17:51:08Z"} {"first_name":"Kellsie","last_name":"Bassingham","gender":"Male","ip_address":"198.30.185.242"} {"id":581722503449,"first_name":"Blair","email":"bsellstromik@skyrock.com","gender":"Genderfluid","flagged":false,"year":1998,"created_at":"2014-09-08T01:58:12Z"} {"id":307451097076,"first_name":"Bibby","last_name":"Pedron","email":"bpedronil@nps.gov","gender":"Genderqueer","flagged":false,"year":1991,"created_at":"2006-09-04T02:00:56Z"} {"id":895703844459,"first_name":"Emelita","last_name":"Folk","email":"efolkim@xing.com","gender":"Genderfluid","ip_address":"178.217.131.150","flagged":true,"year":2013,"created_at":"2012-03-27T01:39:10Z"} {"id":471590042694,"first_name":"Yuri","last_name":"Peeke","email":"ypeekein@nasa.gov","gender":"Polygender","ip_address":"109.39.127.250","flagged":true,"year":1999,"created_at":"2017-06-01T08:24:01Z"} {"id":1021611611921,"first_name":"Celle","last_name":"Burrett","email":"cburrettio@accuweather.com","gender":"Genderfluid","ip_address":"128.62.148.67","flagged":false,"year":2007,"created_at":"2020-12-14T23:06:08Z"} {"id":1156834982358,"first_name":"Chelsea","last_name":"Gibson","email":"cgibsonip@google.com.au","gender":"Genderfluid","ip_address":"54.139.59.66","flagged":false,"year":1994,"created_at":"2004-10-21T23:46:04Z"} {"id":951430167190,"first_name":"Tiler","last_name":"Walworche","email":"twalworcheiq@reverbnation.com","gender":"Genderqueer","flagged":true,"year":2001,"created_at":"2007-12-22T21:27:21Z"} {"id":372216634000,"first_name":"Tommie","last_name":"Cohane","email":"tcohaneir@techcrunch.com","gender":"Female","flagged":true,"year":2011,"created_at":"2011-04-29T11:41:15Z"} {"id":788575054401,"first_name":"Caro","last_name":"Letson","email":"cletsonis@usgs.gov","gender":"Female","ip_address":"164.153.123.125","flagged":false,"year":1988,"created_at":"2018-01-05T20:28:50Z"} {"id":1096026422160,"first_name":"Tobias","last_name":"Timblett","email":"ttimblettit@digg.com","gender":"Non-binary","flagged":false,"year":1993,"created_at":"2002-12-01T01:30:50Z"} {"id":727549232900,"first_name":"Sterne","email":"sdoelleiu@photobucket.com","gender":"Male","flagged":true,"year":2009,"created_at":"2010-04-24T17:01:59Z"} {"id":612975078285,"first_name":"Eduard","email":"emellyiv@ning.com","gender":"Genderqueer","flagged":true,"year":2005,"created_at":"2014-09-17T13:40:14Z"} {"id":1101144786061,"first_name":"Steffane","last_name":"Kristufek","email":"skristufekiw@samsung.com","gender":"Polygender","ip_address":"233.158.134.245","flagged":false,"year":2006,"created_at":"2013-03-03T22:19:59Z"} {"id":358210942173,"first_name":"Leonhard","last_name":"Rentelll","email":"lrentelllix@nba.com","gender":"Agender","ip_address":"38.102.37.48","flagged":false,"year":2011,"created_at":"2008-09-07T00:25:45Z"} {"id":839181018032,"first_name":"Krishnah","last_name":"Tineman","email":"ktinemaniy@furl.net","gender":"Non-binary","ip_address":"152.70.88.152","flagged":true,"year":1993,"created_at":"2003-11-21T04:43:23Z"} {"id":251252215031,"first_name":"Andie","last_name":"McKernon","email":"amckernoniz@reference.com","gender":"Female","flagged":false,"year":2003,"created_at":"2011-04-05T04:28:06Z"} {"id":673818781351,"first_name":"Keefer","last_name":"Lown","email":"klownj0@weebly.com","gender":"Agender","flagged":true,"year":1995,"created_at":"2012-12-11T17:24:49Z"} {"id":228170993974,"first_name":"Elissa","last_name":"Gladhill","email":"egladhillj1@hhs.gov","gender":"Genderqueer","ip_address":"252.108.190.140","flagged":true,"year":1987,"created_at":"2003-10-21T22:21:25Z"} {"id":668522819003,"first_name":"Matelda","last_name":"Lerohan","email":"mlerohanj2@odnoklassniki.ru","gender":"Agender","flagged":false,"year":2005,"created_at":"2004-09-21T01:47:55Z"} {"id":653048658719,"first_name":"Rem","email":"rotridgej3@edublogs.org","gender":"Polygender","flagged":true,"year":2008,"created_at":"2008-04-07T21:55:48Z"} {"id":201301610941,"first_name":"Annissa","last_name":"MacAulay","gender":"Non-binary","flagged":true} {"id":576236130054,"first_name":"Trudi","last_name":"Newnham","email":"tnewnhamj5@msn.com","gender":"Polygender","ip_address":"134.28.163.93","flagged":false,"year":2008,"created_at":"2010-04-13T04:57:28Z"} {"id":881102198750,"first_name":"Laural","last_name":"Glasscock","email":"lglasscockj6@alibaba.com","gender":"Female","ip_address":"33.9.128.5","flagged":false,"year":1995,"created_at":"2020-12-04T10:42:36Z"} {"id":500567120042,"first_name":"Ricoriki","last_name":"Lewins","gender":"Genderqueer","flagged":false} {"id":281108793129,"first_name":"Westley","last_name":"Ubsdell","email":"wubsdellj8@bing.com","gender":"Male","flagged":true,"year":2001,"created_at":"2016-03-29T06:57:00Z"} {"id":774956474286,"first_name":"Olivier","last_name":"Klassmann","email":"oklassmannj9@topsy.com","gender":"Genderqueer","ip_address":"87.46.153.6","flagged":false,"year":2009,"created_at":"2006-11-26T05:44:38Z"} {"id":1142670023077,"first_name":"Adena","last_name":"Friberg","email":"afribergja@oakley.com","gender":"Male","ip_address":"87.205.19.182","flagged":true,"year":2010,"created_at":"2010-10-27T04:14:41Z"} {"id":473192713100,"first_name":"Edsel","last_name":"Wiseman","email":"ewisemanjb@home.pl","gender":"Genderfluid","ip_address":"198.246.14.65","flagged":true,"year":1984,"created_at":"2006-06-09T09:45:38Z"} {"id":1109810411416,"first_name":"Gelya","last_name":"Minto","email":"gmintojc@desdev.cn","gender":"Agender","flagged":true,"year":1995,"created_at":"2016-04-26T22:09:27Z"} {"id":367434584769,"first_name":"Frannie","last_name":"Jendas","email":"fjendasjd@digg.com","gender":"Agender","ip_address":"246.169.83.233","flagged":true,"year":1995,"created_at":"2005-03-11T03:39:59Z"} {"id":447165935024,"first_name":"Gelya","last_name":"Marte","email":"gmarteje@businesswire.com","gender":"Genderqueer","flagged":true,"year":2003,"created_at":"2019-01-11T03:42:03Z"} {"id":375109926036,"first_name":"Gabbey","last_name":"Fauguel","email":"gfaugueljf@linkedin.com","gender":"Agender","ip_address":"153.109.151.40","flagged":true,"year":1995,"created_at":"2001-07-10T02:13:31Z"} {"id":588442123528,"first_name":"Thatch","last_name":"Sulter","email":"tsulterjg@ovh.net","gender":"Non-binary","ip_address":"118.212.33.54","flagged":false,"year":1997,"created_at":"2017-09-19T10:15:25Z"} {"id":534718258351,"first_name":"Nell","last_name":"Spurritt","email":"nspurrittjh@mit.edu","gender":"Bigender","flagged":false,"year":2004,"created_at":"2009-08-28T14:51:37Z"} {"id":451279744848,"first_name":"Lanny","last_name":"Yellep","email":"lyellepji@psu.edu","gender":"Genderfluid","flagged":false,"year":2007,"created_at":"2015-04-03T04:40:15Z"} {"id":383026361499,"first_name":"Cece","email":"cbleibaumjj@blogs.com","gender":"Genderqueer","flagged":false,"year":2011,"created_at":"2008-09-13T14:40:14Z"} {"id":542595280374,"first_name":"Casi","email":"ckitchinerjk@live.com","gender":"Male","ip_address":"53.59.238.32","flagged":true,"year":2006,"created_at":"2013-09-01T07:16:59Z"} {"id":698142387440,"first_name":"Cecile","last_name":"Diviny","email":"cdivinyjl@wunderground.com","gender":"Agender","flagged":true,"year":2003,"created_at":"2008-06-04T12:59:50Z"} {"id":965707654145,"first_name":"Helaine","last_name":"Cureton","email":"hcuretonjm@exblog.jp","gender":"Non-binary","flagged":true,"year":2008,"created_at":"2006-03-23T10:53:26Z"} {"id":179724317243,"first_name":"Joela","email":"jthrelfalljn@qq.com","gender":"Genderfluid","ip_address":"205.113.195.164","flagged":false,"year":2006,"created_at":"2006-01-26T00:28:11Z"} {"id":590934279540,"first_name":"Cecil","last_name":"Brusby","email":"cbrusbyjo@unesco.org","gender":"Non-binary","flagged":true,"year":1997,"created_at":"2004-12-08T07:39:03Z"} {"id":151613361964,"first_name":"Odetta","last_name":"Lyness","email":"olynessjp@nps.gov","gender":"Genderfluid","ip_address":"26.122.248.153","flagged":true,"year":2009,"created_at":"2020-08-30T17:31:21Z"} {"id":155172092070,"first_name":"Adrian","last_name":"Klemensiewicz","email":"aklemensiewiczjq@amazon.co.uk","gender":"Non-binary","ip_address":"144.35.179.177","flagged":true,"year":1992,"created_at":"2011-02-15T15:45:07Z"} {"id":1173441262705,"first_name":"Xymenes","last_name":"Pople","email":"xpoplejr@t-online.de","gender":"Genderqueer","ip_address":"22.127.54.97","flagged":false,"year":1987,"created_at":"2004-06-10T20:23:49Z"} {"id":708666883147,"first_name":"Louise","last_name":"McDonagh","email":"lmcdonaghjs@patch.com","gender":"Genderfluid","ip_address":"9.253.141.8","flagged":true,"year":2010,"created_at":"2014-01-07T18:09:35Z"} {"id":503334430393,"first_name":"Matty","last_name":"Rubi","email":"mrubijt@google.fr","gender":"Genderfluid","ip_address":"224.109.99.63","flagged":true,"year":1993,"created_at":"2016-07-29T20:40:23Z"} {"id":151917436024,"first_name":"Cybill","last_name":"Millin","email":"cmillinju@icq.com","gender":"Genderqueer","flagged":false,"year":2012,"created_at":"2009-10-21T22:39:00Z"} {"id":223825080114,"first_name":"Modestine","last_name":"Campany","email":"mcampanyjv@umn.edu","gender":"Genderqueer","flagged":true,"year":2012,"created_at":"2019-05-14T02:35:23Z"} {"id":341814157076,"first_name":"Joye","last_name":"Hackelton","email":"jhackeltonjw@e-recht24.de","gender":"Genderfluid","ip_address":"118.175.196.135","flagged":true,"year":2001,"created_at":"2004-04-14T07:26:49Z"} {"id":124616645109,"first_name":"Ruprecht","last_name":"Attwool","email":"rattwooljx@google.nl","gender":"Female","ip_address":"94.109.90.29","flagged":true,"year":2002,"created_at":"2016-07-14T23:19:26Z"} {"id":728445759785,"first_name":"Grantham","last_name":"Verson","email":"gversonjy@google.fr","gender":"Genderqueer","ip_address":"14.5.216.179","flagged":false,"year":2012,"created_at":"2014-06-03T13:39:40Z"} {"id":933204848712,"first_name":"David","last_name":"Hamby","email":"dhambyjz@google.com","gender":"Genderqueer","ip_address":"56.58.61.113","flagged":false,"year":2002,"created_at":"2011-06-07T07:57:10Z"} {"id":1176006633651,"first_name":"Napoleon","email":"npabstk0@1688.com","gender":"Non-binary","ip_address":"70.199.134.201","flagged":false,"year":2002,"created_at":"2013-01-31T05:55:36Z"} {"id":1177964735391,"first_name":"Addie","last_name":"Lytle","email":"alytlek1@accuweather.com","gender":"Agender","flagged":true,"year":1996,"created_at":"2005-11-10T10:27:37Z"} {"id":645620178683,"first_name":"Hildagard","last_name":"Terne","email":"hternek2@geocities.com","gender":"Bigender","flagged":false,"year":2006,"created_at":"2007-10-03T19:42:21Z"} {"first_name":"Lori","last_name":"Lifton","email":"lliftonk3@sitemeter.com","gender":"Genderfluid","ip_address":"184.130.168.16","year":1993,"created_at":"2018-06-07T08:43:13Z"} {"id":142456071051,"first_name":"Lawry","last_name":"St. Quintin","email":"lstquintink4@reference.com","gender":"Male","flagged":false,"year":1990,"created_at":"2004-10-13T01:10:21Z"} {"id":687880634199,"first_name":"Francois","last_name":"Satterley","gender":"Female","ip_address":"233.96.176.177","flagged":true} {"id":1143261171408,"first_name":"Kirsteni","last_name":"Janway","email":"kjanwayk6@google.ru","gender":"Polygender","ip_address":"148.91.157.234","flagged":false,"year":2005,"created_at":"2011-05-11T06:50:57Z"} {"id":1194616789435,"first_name":"Kim","last_name":"Imloch","email":"kimlochk7@myspace.com","ip_address":"222.247.211.125","flagged":false,"year":1976,"created_at":"2021-01-24T20:14:24Z"} {"id":825334281827,"first_name":"Aviva","last_name":"Bydaway","email":"abydawayk8@google.ca","gender":"Female","ip_address":"102.139.214.130","flagged":true,"year":2010,"created_at":"2010-12-27T23:24:22Z"} {"id":773293141751,"first_name":"Mace","last_name":"Batchelor","email":"mbatchelork9@amazon.com","gender":"Non-binary","flagged":false,"year":1985,"created_at":"2021-02-23T05:46:47Z"} {"id":1051275464128,"first_name":"Aline","last_name":"Labin","email":"alabinka@apple.com","gender":"Agender","ip_address":"255.39.140.231","flagged":true,"year":1986,"created_at":"2017-08-31T06:17:46Z"} {"id":1117016441907,"first_name":"Charles","last_name":"Linzee","email":"clinzeekb@quantcast.com","gender":"Male","flagged":false,"year":2000,"created_at":"2021-04-05T16:21:04Z"} {"id":441790728316,"first_name":"Jaquenette","last_name":"Ropartz","email":"jropartzkc@unc.edu","gender":"Male","ip_address":"93.243.38.77","flagged":true,"year":2003,"created_at":"2018-01-18T00:25:54Z"} {"id":582984043208,"first_name":"Osmond","last_name":"Benson","gender":"Female","ip_address":"53.240.143.87","flagged":false} {"id":163140549787,"first_name":"Solly","last_name":"Gommes","email":"sgommeske@irs.gov","gender":"Female","flagged":true,"year":1988,"created_at":"2009-02-13T14:24:38Z"} {"id":220688585001,"first_name":"Jacintha","last_name":"Hirschmann","email":"jhirschmannkf@illinois.edu","gender":"Male","ip_address":"181.35.196.233","flagged":true,"year":2009,"created_at":"2003-02-25T07:02:46Z"} {"id":1219217391698,"first_name":"Hulda","last_name":"Cheshir","email":"hcheshirkg@paypal.com","gender":"Genderqueer","flagged":false,"year":1995,"created_at":"2011-05-26T04:46:06Z"} {"id":447810319140,"first_name":"Franky","last_name":"Fantone","email":"ffantonekh@geocities.jp","gender":"Genderqueer","ip_address":"47.221.121.45","flagged":false,"year":2012,"created_at":"2014-08-07T21:25:59Z"} {"id":508613569162,"first_name":"Jinny","last_name":"MacKeeg","email":"jmackeegki@theatlantic.com","gender":"Non-binary","ip_address":"20.156.235.78","flagged":false,"year":1986,"created_at":"2016-03-07T01:31:59Z"} {"id":289851427058,"first_name":"Willem","last_name":"Croston","email":"wcrostonkj@upenn.edu","gender":"Bigender","flagged":true,"year":1993,"created_at":"2002-06-28T07:14:00Z"} {"id":819338636248,"first_name":"Revkah","last_name":"Filoniere","email":"rfilonierekk@github.io","gender":"Female","ip_address":"244.85.45.249","flagged":true,"year":1998,"created_at":"2008-05-17T23:47:06Z"} {"id":322922480285,"first_name":"Seline","last_name":"Hurdle","email":"shurdlekl@weebly.com","gender":"Male","flagged":true,"year":2001,"created_at":"2012-03-29T09:17:23Z"} {"id":791979869644,"first_name":"Stern","last_name":"Cudworth","email":"scudworthkm@nifty.com","gender":"Polygender","flagged":false,"year":2007,"created_at":"2002-10-17T06:46:17Z"} {"id":708746056612,"first_name":"Chicky","last_name":"Shard","email":"cshardkn@mediafire.com","gender":"Female","ip_address":"157.19.217.44","flagged":true,"year":2002,"created_at":"2008-04-01T04:31:09Z"} {"id":565285843088,"first_name":"Anderea","last_name":"Londing","email":"alondingko@pbs.org","gender":"Genderfluid","flagged":false,"year":1996,"created_at":"2011-08-18T23:39:06Z"} {"id":525421136425,"first_name":"Enriqueta","last_name":"Resun","email":"eresunkp@hubpages.com","gender":"Genderqueer","ip_address":"70.207.141.38","flagged":true,"year":1987,"created_at":"2020-11-29T06:59:43Z"} {"id":747476070099,"first_name":"Towny","last_name":"Tyrer","email":"ttyrerkq@merriam-webster.com","gender":"Genderqueer","ip_address":"228.71.4.82","flagged":true,"year":2009,"created_at":"2017-11-22T08:30:01Z"} {"id":601815789735,"first_name":"Hubey","last_name":"Kernock","email":"hkernockkr@pinterest.com","gender":"Agender","flagged":false,"year":1994,"created_at":"2013-08-28T15:50:55Z"} {"id":483631397447,"first_name":"Karna","email":"kestoileks@fc2.com","gender":"Genderqueer","flagged":false,"year":1992,"created_at":"2007-05-11T06:51:03Z"} {"id":1116487745585,"first_name":"Nels","last_name":"Antognazzi","email":"nantognazzikt@feedburner.com","flagged":false,"year":1995,"created_at":"2004-05-29T03:49:57Z"} {"first_name":"Candi","last_name":"Aberdeen","gender":"Bigender","ip_address":"231.171.209.66"} {"id":387593228719,"first_name":"Viola","last_name":"Eefting","email":"veeftingkv@nature.com","gender":"Female","ip_address":"105.68.174.171","flagged":true,"year":2007,"created_at":"2020-06-11T22:10:46Z"} {"id":1198535275095,"first_name":"Klara","last_name":"Volks","email":"kvolkskw@buzzfeed.com","gender":"Male","ip_address":"190.213.83.212","flagged":false,"year":1996,"created_at":"2002-10-22T00:11:32Z"} {"id":1213350922723,"first_name":"Nelie","last_name":"Rominov","email":"nrominovkx@washington.edu","gender":"Genderqueer","ip_address":"57.126.26.160","flagged":false,"year":2005,"created_at":"2011-06-13T01:54:34Z"} {"id":697759998095,"first_name":"Jada","last_name":"Soars","email":"jsoarsky@abc.net.au","gender":"Non-binary","flagged":true,"year":1994,"created_at":"2019-01-18T04:46:17Z"} {"first_name":"Inge","last_name":"Williams","email":"iwilliamskz@ibm.com","gender":"Bigender","year":2007,"created_at":"2017-01-30T20:35:16Z"} {"id":531048622237,"first_name":"Devonna","last_name":"Leroux","email":"dlerouxl0@naver.com","gender":"Agender","ip_address":"137.144.72.206","flagged":true,"year":1984,"created_at":"2013-10-19T06:11:27Z"} {"id":507498140614,"first_name":"Allyn","last_name":"Harrow","email":"aharrowl1@twitpic.com","gender":"Genderfluid","flagged":true,"year":1997,"created_at":"2013-05-27T23:17:46Z"} {"id":794194885263,"first_name":"Benedetta","last_name":"Urlich","email":"burlichl2@prlog.org","gender":"Female","flagged":true,"year":2002,"created_at":"2007-03-22T13:09:23Z"} {"id":1168162133800,"first_name":"Consuelo","last_name":"Laxon","email":"claxonl3@e-recht24.de","gender":"Bigender","flagged":true,"year":1996,"created_at":"2006-09-08T22:10:40Z"} {"id":164220265016,"first_name":"Rosemary","last_name":"Chorlton","email":"rchorltonl4@zdnet.com","gender":"Agender","ip_address":"225.24.174.6","flagged":true,"year":2008,"created_at":"2014-11-07T02:18:30Z"} {"id":556197011455,"first_name":"Arvy","last_name":"Glassford","email":"aglassfordl5@ow.ly","gender":"Agender","ip_address":"99.197.230.198","flagged":true,"year":2006,"created_at":"2013-07-30T02:22:05Z"} {"id":182377137890,"first_name":"Ted","last_name":"Scantlebury","email":"tscantleburyl6@eepurl.com","gender":"Bigender","ip_address":"153.44.177.56","flagged":false,"year":2002,"created_at":"2002-11-18T18:24:31Z"} {"id":643802104527,"first_name":"Clemmy","email":"cmorol7@arstechnica.com","gender":"Polygender","flagged":true,"year":2007,"created_at":"2015-06-20T07:08:09Z"} {"id":406604711900,"first_name":"Homerus","last_name":"Marflitt","email":"hmarflittl8@dyndns.org","gender":"Agender","ip_address":"202.28.161.132","flagged":false,"year":1994,"created_at":"2020-04-26T23:11:07Z"} {"id":825279707064,"first_name":"Jamil","last_name":"Garnul","email":"jgarnull9@tamu.edu","gender":"Genderfluid","ip_address":"118.126.229.241","flagged":false,"year":2011,"created_at":"2002-09-21T01:21:43Z"} {"id":897916632506,"first_name":"Honor","email":"holynnla@123-reg.co.uk","gender":"Polygender","ip_address":"106.81.218.47","flagged":true,"year":1996,"created_at":"2020-03-12T21:47:24Z"} {"id":586084518192,"first_name":"Kennan","last_name":"Elizabeth","email":"kelizabethlb@about.me","gender":"Agender","ip_address":"123.162.72.90","flagged":true,"year":2011,"created_at":"2008-01-18T06:10:20Z"} {"id":591955946832,"first_name":"Dukey","last_name":"Gatch","email":"dgatchlc@ucoz.com","gender":"Female","ip_address":"21.105.13.35","flagged":true,"year":2008,"created_at":"2015-09-03T10:32:08Z"} {"id":261191510791,"first_name":"Nicolis","last_name":"Simonaitis","email":"nsimonaitisld@over-blog.com","gender":"Bigender","flagged":false,"year":2000,"created_at":"2006-02-10T02:08:14Z"} {"id":214764543285,"first_name":"Sammy","last_name":"Bissex","gender":"Non-binary","ip_address":"100.116.56.54","flagged":false} {"id":635568267684,"first_name":"Cynthie","last_name":"Meus","email":"cmeuslf@apache.org","gender":"Female","ip_address":"29.180.107.95","flagged":true,"year":1988,"created_at":"2004-06-18T15:20:10Z"} {"id":945292562068,"first_name":"Hedy","last_name":"Lambourne","email":"hlambournelg@biblegateway.com","gender":"Genderfluid","ip_address":"0.64.29.63","flagged":false,"year":1992,"created_at":"2007-09-17T12:02:15Z"} {"id":1072776798755,"first_name":"Arlee","last_name":"Kighly","email":"akighlylh@usatoday.com","gender":"Genderfluid","ip_address":"127.244.136.80","flagged":true,"year":1992,"created_at":"2004-07-27T22:53:09Z"} {"id":967416033937,"first_name":"Leroy","last_name":"Pennoni","email":"lpennonili@livejournal.com","gender":"Agender","flagged":false,"year":1988,"created_at":"2013-07-15T21:01:06Z"} {"id":676592091153,"first_name":"Mari","last_name":"Dymocke","email":"mdymockelj@bandcamp.com","gender":"Genderfluid","flagged":false,"year":1996,"created_at":"2005-01-09T05:13:22Z"} {"id":628717495049,"first_name":"Antonina","last_name":"Rathe","email":"arathelk@paginegialle.it","gender":"Genderqueer","ip_address":"20.88.90.106","flagged":false,"year":2012,"created_at":"2015-04-27T14:09:31Z"} {"id":625714257682,"first_name":"Dyann","last_name":"Davenall","email":"ddavenallll@sbwire.com","gender":"Male","flagged":true,"year":1989,"created_at":"2013-10-14T04:13:49Z"} {"id":1076827546402,"first_name":"Jillene","last_name":"Seage","email":"jseagelm@theguardian.com","gender":"Genderqueer","flagged":false,"year":1994,"created_at":"2007-01-25T08:04:59Z"} {"id":815668297757,"first_name":"Barbara-anne","last_name":"Mutton","email":"bmuttonln@privacy.gov.au","gender":"Genderqueer","flagged":false,"year":2008,"created_at":"2017-03-14T06:13:47Z"} {"id":242210329634,"first_name":"Mikkel","last_name":"Haigh","email":"mhaighlo@angelfire.com","gender":"Male","ip_address":"224.79.96.80","flagged":false,"year":2012,"created_at":"2004-05-14T16:27:33Z"} {"id":1012490557143,"first_name":"Bertrando","last_name":"Haugen","email":"bhaugenlp@studiopress.com","gender":"Genderqueer","flagged":true,"year":2002,"created_at":"2001-07-09T08:03:53Z"} {"id":314928330141,"first_name":"Maurizio","last_name":"McGoogan","email":"mmcgooganlq@go.com","gender":"Agender","flagged":false,"year":2009,"created_at":"2016-05-18T00:59:52Z"} {"id":131331170518,"first_name":"Gerhardine","last_name":"Mersh","email":"gmershlr@dagondesign.com","gender":"Genderfluid","ip_address":"221.101.90.239","flagged":true,"year":2010,"created_at":"2006-09-29T01:14:47Z"} {"id":145258570661,"first_name":"Kathlin","last_name":"Deeson","email":"kdeesonls@wufoo.com","gender":"Polygender","ip_address":"99.91.123.200","flagged":false,"year":1994,"created_at":"2004-07-28T15:59:14Z"} {"id":522047630643,"first_name":"Georgette","last_name":"Coveny","email":"gcovenylt@pen.io","gender":"Agender","flagged":false,"year":2001,"created_at":"2019-08-23T17:07:14Z"} {"id":169983502370,"first_name":"Arel","email":"ashilvocklu@dmoz.org","gender":"Agender","flagged":true,"year":1996,"created_at":"2018-02-23T00:33:53Z"} {"id":856932740433,"first_name":"Kanya","last_name":"McKerton","email":"kmckertonlv@wikispaces.com","gender":"Genderfluid","flagged":false,"year":2005,"created_at":"2011-07-31T13:12:09Z"} {"id":208842291136,"first_name":"Keely","last_name":"Strodder","email":"kstrodderlw@prnewswire.com","gender":"Polygender","flagged":true,"year":1992,"created_at":"2008-01-03T03:39:23Z"} {"id":601355538100,"first_name":"Kristen","email":"kgunstonelx@buzzfeed.com","gender":"Genderfluid","ip_address":"229.143.75.195","flagged":true,"year":2003,"created_at":"2020-05-10T12:30:11Z"} {"id":629006159087,"first_name":"Beryle","last_name":"Meegan","email":"bmeeganly@ow.ly","gender":"Agender","ip_address":"94.201.228.89","flagged":false,"year":1994,"created_at":"2003-08-04T22:23:00Z"} {"id":1001614765034,"first_name":"Debora","last_name":"Kew","email":"dkewlz@va.gov","gender":"Polygender","flagged":false,"year":2011,"created_at":"2014-08-02T23:17:20Z"} {"id":414466075904,"first_name":"Rriocard","last_name":"Palumbo","email":"rpalumbom0@desdev.cn","gender":"Bigender","ip_address":"73.120.177.124","flagged":true,"year":1992,"created_at":"2011-11-12T12:30:50Z"} {"id":412781314812,"first_name":"Hedy","gender":"Genderfluid","flagged":true} {"id":603095597151,"first_name":"Ezechiel","last_name":"Wickrath","email":"ewickrathm2@java.com","gender":"Agender","ip_address":"223.143.123.87","flagged":true,"year":2000,"created_at":"2020-06-16T01:08:17Z"} {"id":1036262517226,"first_name":"Berkeley","last_name":"Banting","email":"bbantingm3@who.int","gender":"Bigender","ip_address":"169.236.160.132","flagged":false,"year":2002,"created_at":"2019-07-14T17:47:04Z"} {"id":300597849764,"first_name":"Audra","last_name":"Butson","email":"abutsonm4@home.pl","gender":"Genderfluid","ip_address":"172.120.18.92","flagged":true,"year":1996,"created_at":"2005-07-01T17:06:26Z"} {"id":334626871034,"first_name":"Gerri","last_name":"Prettyjohns","email":"gprettyjohnsm5@cargocollective.com","gender":"Female","flagged":false,"year":1996,"created_at":"2015-11-10T08:48:15Z"} {"id":415273106269,"first_name":"Wilow","last_name":"Crimpe","email":"wcrimpem6@hhs.gov","gender":"Agender","flagged":false,"year":2011,"created_at":"2006-08-01T11:50:53Z"} {"id":859969384570,"first_name":"Verge","last_name":"Acland","email":"vaclandm7@cam.ac.uk","gender":"Polygender","flagged":false,"year":1992,"created_at":"2015-03-17T10:30:18Z"} {"id":955385123632,"first_name":"Bailey","last_name":"Holstein","email":"bholsteinm8@wiley.com","gender":"Polygender","flagged":true,"year":2004,"created_at":"2020-10-25T10:09:44Z"} {"id":838354716229,"first_name":"Matelda","last_name":"Currum","email":"mcurrumm9@theglobeandmail.com","gender":"Male","ip_address":"14.161.224.168","flagged":false,"year":2003,"created_at":"2018-05-10T19:48:52Z"} {"id":588139350900,"first_name":"Phillis","last_name":"Kleszinski","email":"pkleszinskima@edublogs.org","gender":"Female","ip_address":"149.48.216.200","flagged":true,"year":2000,"created_at":"2008-11-03T13:38:28Z"} {"id":150880621187,"first_name":"Dyane","last_name":"Dourin","email":"ddourinmb@wsj.com","gender":"Genderfluid","ip_address":"8.40.221.7","flagged":false,"year":2010,"created_at":"2018-03-29T11:23:10Z"} {"id":797205086120,"first_name":"Harmonie","last_name":"Uc","email":"hucmc@desdev.cn","gender":"Bigender","flagged":true,"year":2004,"created_at":"2005-10-13T14:31:58Z"} {"id":486575553926,"first_name":"Abigale","email":"achongmd@ca.gov","gender":"Female","flagged":false,"year":1992,"created_at":"2004-04-15T14:41:51Z"} {"id":954387847105,"first_name":"Rowland","last_name":"McCullogh","email":"rmcculloghme@acquirethisname.com","gender":"Agender","flagged":true,"year":1992,"created_at":"2009-12-18T23:43:18Z"} {"id":483862076775,"first_name":"Reggy","last_name":"Gilroy","email":"rgilroymf@php.net","gender":"Bigender","flagged":false,"year":2011,"created_at":"2011-06-12T17:50:44Z"} {"id":982659838701,"first_name":"Kelley","last_name":"Peer","email":"kpeermg@unc.edu","gender":"Female","ip_address":"197.226.102.246","flagged":true,"year":1992,"created_at":"2014-10-16T00:26:21Z"} {"id":852905592946,"first_name":"Sheff","last_name":"Gerbel","email":"sgerbelmh@nationalgeographic.com","gender":"Genderqueer","flagged":true,"year":2006,"created_at":"2013-07-26T03:30:43Z"} {"id":847837366451,"first_name":"Gustavo","last_name":"Parkin","email":"gparkinmi@bizjournals.com","gender":"Female","ip_address":"20.84.122.235","flagged":true,"year":2011,"created_at":"2002-11-16T11:40:06Z"} {"id":1202715215617,"first_name":"Torrence","email":"tcortinmj@e-recht24.de","gender":"Genderfluid","ip_address":"19.130.91.144","flagged":true,"year":2003,"created_at":"2013-02-15T13:49:42Z"} {"id":345336799893,"first_name":"Benedetto","last_name":"Gerssam","email":"bgerssammk@ameblo.jp","gender":"Male","ip_address":"90.221.97.86","flagged":true,"year":2007,"created_at":"2015-01-04T19:30:23Z"} {"id":180267518577,"first_name":"Elfie","last_name":"Slinn","gender":"Polygender","ip_address":"27.59.203.31","flagged":true} {"id":490615514483,"first_name":"Helenelizabeth","last_name":"Clementet","email":"hclementetmm@ihg.com","gender":"Bigender","ip_address":"233.194.70.119","flagged":false,"year":1989,"created_at":"2009-12-26T23:27:18Z"} {"id":729806123433,"first_name":"Nerissa","last_name":"Castri","email":"ncastrimn@behance.net","gender":"Male","flagged":true,"year":2001,"created_at":"2004-08-28T03:22:25Z"} {"id":544251281826,"first_name":"Shelley","last_name":"Rainsdon","email":"srainsdonmo@reddit.com","gender":"Non-binary","ip_address":"38.26.72.10","flagged":true,"year":2009,"created_at":"2010-08-27T20:36:49Z"} {"id":662403265032,"first_name":"Lorne","email":"lmcclounanmp@nytimes.com","gender":"Agender","flagged":true,"year":2008,"created_at":"2007-11-20T16:35:39Z"} {"id":749472124241,"first_name":"Kelly","last_name":"O'Nowlan","gender":"Polygender","flagged":true} {"id":374035830528,"first_name":"Nikki","last_name":"Crebo","email":"ncrebomr@rakuten.co.jp","gender":"Agender","flagged":true,"year":1986,"created_at":"2016-12-31T04:35:15Z"} {"id":420092770117,"first_name":"Stanwood","last_name":"Hungerford","email":"shungerfordms@cnbc.com","gender":"Polygender","flagged":false,"year":2004,"created_at":"2004-06-22T11:42:19Z"} {"id":1089772822105,"first_name":"Stanleigh","email":"sscirmanmt@ox.ac.uk","gender":"Non-binary","flagged":false,"year":2009,"created_at":"2014-07-02T04:28:37Z"} {"id":294780139892,"first_name":"Thekla","last_name":"Troni","email":"ttronimu@cbslocal.com","gender":"Genderqueer","ip_address":"35.168.40.84","flagged":false,"year":2008,"created_at":"2012-12-29T14:38:26Z"} {"id":1192926821885,"first_name":"Wilhelmina","last_name":"D'Antoni","email":"wdantonimv@artisteer.com","gender":"Male","flagged":true,"year":1999,"created_at":"2005-01-11T01:28:34Z"} {"id":447999159148,"first_name":"Forrester","last_name":"Conrath","email":"fconrathmw@gnu.org","gender":"Genderfluid","ip_address":"225.54.4.23","flagged":true,"year":1993,"created_at":"2007-08-04T01:44:22Z"} {"id":311575884248,"first_name":"Jo-anne","last_name":"Nisen","email":"jnisenmx@a8.net","gender":"Male","flagged":true,"year":1985,"created_at":"2001-08-29T04:12:11Z"} {"first_name":"Robinet","last_name":"Greggs","email":"rgreggsmy@mayoclinic.com","gender":"Bigender","ip_address":"145.133.3.215","year":2010,"created_at":"2012-08-03T07:00:19Z"} {"id":919679484866,"first_name":"Chilton","last_name":"Pilmoor","email":"cpilmoormz@omniture.com","gender":"Male","flagged":false,"year":2001,"created_at":"2008-03-02T19:02:20Z"} {"id":218098137048,"first_name":"Wright","last_name":"Robbins","email":"wrobbinsn0@hibu.com","gender":"Bigender","ip_address":"91.207.181.197","flagged":true,"year":2012,"created_at":"2010-07-30T08:26:18Z"} {"id":1180005444190,"first_name":"Salim","last_name":"Huxstep","email":"shuxstepn1@alexa.com","gender":"Genderfluid","ip_address":"5.175.194.236","flagged":true,"year":1992,"created_at":"2009-12-29T04:28:55Z"} {"id":467268179253,"first_name":"Werner","last_name":"Highman","gender":"Male","flagged":false} {"id":883073754056,"first_name":"Arvy","last_name":"Ilsley","email":"ailsleyn3@cbsnews.com","gender":"Male","ip_address":"104.88.112.181","flagged":false,"year":2002,"created_at":"2020-03-31T23:34:35Z"} {"id":764560887633,"first_name":"Basilio","last_name":"Bugdale","email":"bbugdalen4@uol.com.br","gender":"Male","flagged":false,"year":2006,"created_at":"2006-06-19T11:37:24Z"} {"id":186015292977,"first_name":"Halimeda","last_name":"Barron","email":"hbarronn5@fc2.com","gender":"Genderfluid","ip_address":"153.255.189.139","flagged":false,"year":1987,"created_at":"2019-06-16T00:39:55Z"} {"id":544250511100,"first_name":"Adrea","last_name":"Weathey","email":"aweatheyn6@themeforest.net","gender":"Male","ip_address":"37.242.235.86","flagged":false,"year":1989,"created_at":"2007-10-26T13:33:41Z"} {"id":756697594632,"first_name":"Barbey","last_name":"Lutz","email":"blutzn7@networkadvertising.org","gender":"Genderqueer","flagged":false,"year":1995,"created_at":"2006-12-14T13:19:30Z"} {"id":338428353347,"first_name":"Rhianna","last_name":"McCloch","email":"rmcclochn8@skype.com","gender":"Agender","flagged":false,"year":2008,"created_at":"2015-01-18T00:01:45Z"} {"id":484138305469,"first_name":"Sauveur","last_name":"Corbet","email":"scorbetn9@diigo.com","gender":"Genderfluid","flagged":false,"year":2002,"created_at":"2003-04-16T01:36:00Z"} {"id":942768552236,"first_name":"Leonardo","last_name":"Merill","email":"lmerillna@paginegialle.it","gender":"Female","ip_address":"191.50.38.248","flagged":false,"year":1994,"created_at":"2016-05-05T13:58:21Z"} {"id":868722830519,"first_name":"Orlan","last_name":"Jeannard","email":"ojeannardnb@wiley.com","gender":"Polygender","ip_address":"121.72.146.143","flagged":true,"year":2007,"created_at":"2008-08-11T22:23:51Z"} {"id":604434668508,"first_name":"Caitlin","last_name":"Armit","email":"carmitnc@live.com","gender":"Bigender","ip_address":"160.189.251.6","flagged":true,"year":2004,"created_at":"2020-08-15T15:53:21Z"} {"id":987713685003,"first_name":"Codi","last_name":"Chamney","email":"cchamneynd@meetup.com","gender":"Polygender","ip_address":"20.44.51.248","flagged":true,"year":2004,"created_at":"2019-09-11T11:05:10Z"} {"id":247088823145,"first_name":"Modesta","last_name":"Dutnell","email":"mdutnellne@wisc.edu","gender":"Agender","ip_address":"93.96.92.190","flagged":false,"year":1986,"created_at":"2009-11-07T19:06:41Z"} {"id":586892498447,"first_name":"Trina","last_name":"Wildey","email":"twildeynf@list-manage.com","gender":"Non-binary","ip_address":"113.228.35.62","flagged":true,"year":1994,"created_at":"2010-03-15T01:32:37Z"} {"id":1042903647283,"first_name":"Jodi","last_name":"Grimsey","email":"jgrimseyng@goodreads.com","gender":"Polygender","ip_address":"163.59.85.124","flagged":false,"year":2004,"created_at":"2010-03-30T12:11:49Z"} {"id":1230388071253,"first_name":"Ursulina","last_name":"Dumbar","email":"udumbarnh@reuters.com","gender":"Bigender","flagged":true,"year":1995,"created_at":"2015-07-05T13:13:14Z"} {"id":310742842277,"first_name":"Judah","last_name":"Bamlett","email":"jbamlettni@surveymonkey.com","gender":"Genderqueer","flagged":false,"year":1989,"created_at":"2011-04-01T16:37:36Z"} {"first_name":"Olivier","last_name":"Bricksey","email":"obrickseynj@icio.us","gender":"Agender","ip_address":"143.245.5.25","year":2003,"created_at":"2018-05-18T09:23:26Z"} {"id":1211517743686,"first_name":"Denni","last_name":"Minor","email":"dminornk@hatena.ne.jp","gender":"Bigender","ip_address":"122.80.75.238","flagged":false,"year":2006,"created_at":"2010-09-11T02:56:00Z"} {"id":140880026569,"first_name":"Emelina","last_name":"McClary","email":"emcclarynl@addtoany.com","gender":"Female","ip_address":"172.44.136.50","flagged":false,"year":2010,"created_at":"2014-06-13T02:16:05Z"} {"id":272593653641,"first_name":"Jeth","last_name":"Timoney","email":"jtimoneynm@clickbank.net","gender":"Genderqueer","ip_address":"30.213.128.148","flagged":false,"year":1967,"created_at":"2018-12-26T14:21:43Z"} {"id":929346885397,"first_name":"Heidie","last_name":"Smiz","email":"hsmiznn@i2i.jp","gender":"Agender","flagged":true,"year":2009,"created_at":"2007-07-02T14:59:31Z"} {"id":1224238519895,"first_name":"Kerwin","last_name":"Gerhartz","email":"kgerhartzno@yale.edu","gender":"Polygender","ip_address":"17.7.24.79","flagged":true,"year":2002,"created_at":"2005-08-24T20:48:55Z"} {"id":134623018947,"first_name":"Veronica","last_name":"Stoite","email":"vstoitenp@wisc.edu","gender":"Bigender","flagged":true,"year":1991,"created_at":"2008-09-24T21:30:00Z"} {"id":988437582887,"first_name":"Franny","last_name":"Spain","email":"fspainnq@umich.edu","gender":"Genderfluid","ip_address":"150.164.83.125","flagged":false,"year":1990,"created_at":"2004-08-04T02:39:43Z"} {"id":1063711539758,"first_name":"Nichols","last_name":"Mc Meekin","email":"nmcmeekinnr@bloglovin.com","gender":"Bigender","flagged":true,"year":2001,"created_at":"2020-07-16T03:24:07Z"} {"id":210613905358,"first_name":"Wayland","last_name":"Joiris","email":"wjoirisns@ftc.gov","gender":"Male","ip_address":"247.116.146.241","flagged":true,"year":2008,"created_at":"2015-05-27T19:59:34Z"} {"id":306090278015,"first_name":"Neala","email":"nstampfernt@about.me","gender":"Non-binary","flagged":true,"year":2007,"created_at":"2006-01-21T00:37:07Z"} {"id":1092605528827,"first_name":"Hort","last_name":"Gyngell","email":"hgyngellnu@java.com","gender":"Genderfluid","flagged":false,"year":1986,"created_at":"2002-05-01T15:47:50Z"} {"id":810381391035,"first_name":"Milton","email":"mschulznv@vimeo.com","gender":"Genderqueer","ip_address":"233.93.148.140","flagged":false,"year":2006,"created_at":"2007-11-06T17:18:11Z"} {"id":678104986843,"first_name":"Cassey","last_name":"Bonsale","email":"cbonsalenw@slideshare.net","gender":"Male","flagged":false,"year":2009,"created_at":"2012-03-13T11:32:35Z"} {"id":895324575350,"first_name":"Edith","last_name":"Glyde","email":"eglydenx@slate.com","gender":"Bigender","ip_address":"1.59.186.180","flagged":false,"year":2011,"created_at":"2015-05-23T20:25:37Z"} {"id":442288046625,"first_name":"Grady","last_name":"Bartholomaus","email":"gbartholomausny@salon.com","gender":"Bigender","flagged":true,"year":2004,"created_at":"2002-09-13T23:11:31Z"} {"id":759669685398,"first_name":"Karil","last_name":"Deabill","email":"kdeabillnz@themeforest.net","gender":"Polygender","flagged":false,"year":1988,"created_at":"2011-04-21T07:03:29Z"} {"id":318464527414,"first_name":"Hildagard","last_name":"Canero","email":"hcaneroo0@tripadvisor.com","gender":"Bigender","ip_address":"8.29.74.31","flagged":true,"year":1989,"created_at":"2011-04-09T07:19:24Z"} {"id":1119753039251,"first_name":"Jeth","email":"jgougho1@ocn.ne.jp","gender":"Genderqueer","flagged":false,"year":2007,"created_at":"2014-03-06T06:44:49Z"} {"id":778442157597,"first_name":"Carly","last_name":"Nation","email":"cnationo2@dion.ne.jp","gender":"Genderqueer","ip_address":"151.56.50.178","flagged":true,"year":2006,"created_at":"2003-08-10T23:41:03Z"} {"id":508114411589,"first_name":"Izaak","last_name":"Hryniewicz","email":"ihryniewiczo3@cisco.com","gender":"Agender","ip_address":"215.218.129.206","flagged":false,"year":2000,"created_at":"2003-05-24T15:33:26Z"} {"id":1093777464910,"first_name":"Adamo","last_name":"Coping","email":"acopingo4@zdnet.com","gender":"Bigender","ip_address":"121.3.84.165","flagged":true,"year":2006,"created_at":"2018-07-03T17:28:08Z"} {"id":712122575789,"first_name":"Linnet","last_name":"Glavin","email":"lglavino5@mac.com","gender":"Agender","ip_address":"93.149.106.6","flagged":true,"year":2005,"created_at":"2014-09-16T10:29:28Z"} {"id":654222686770,"first_name":"Genna","last_name":"Penella","email":"gpenellao6@woothemes.com","gender":"Genderqueer","ip_address":"178.108.103.92","flagged":false,"year":1999,"created_at":"2014-05-20T12:43:20Z"} {"id":982322957476,"first_name":"Bernadette","last_name":"Jowle","email":"bjowleo7@eepurl.com","gender":"Female","ip_address":"56.224.200.58","flagged":false,"year":1994,"created_at":"2009-08-20T07:57:26Z"} {"id":408142707324,"first_name":"Vittoria","email":"vfowneso8@goodreads.com","gender":"Genderqueer","ip_address":"170.226.109.255","flagged":true,"year":2005,"created_at":"2006-04-12T06:49:45Z"} {"id":179180028446,"first_name":"Becka","email":"bmineghellio9@cbsnews.com","gender":"Genderqueer","ip_address":"101.79.20.9","flagged":false,"year":2006,"created_at":"2008-07-25T01:55:40Z"} {"id":1045953349636,"first_name":"Anabel","last_name":"Sidlow","email":"asidlowoa@businesswire.com","gender":"Genderqueer","ip_address":"28.240.28.210","flagged":false,"year":2003,"created_at":"2015-07-29T18:17:03Z"} {"id":322743595635,"first_name":"Gabrila","email":"gbastinob@sogou.com","gender":"Non-binary","ip_address":"15.76.187.146","flagged":false,"year":2002,"created_at":"2020-08-10T06:44:15Z"} {"id":251795455726,"first_name":"Tanhya","last_name":"Tettersell","email":"ttetterselloc@dropbox.com","gender":"Bigender","flagged":true,"year":2000,"created_at":"2009-04-03T16:12:16Z"} {"id":1030978319098,"first_name":"Basile","last_name":"Ivens","email":"bivensod@narod.ru","gender":"Polygender","ip_address":"171.28.110.106","flagged":false,"year":1995,"created_at":"2008-08-15T18:50:52Z"} {"id":580600016613,"first_name":"Sayer","email":"scurseyoe@youtu.be","gender":"Agender","flagged":false,"year":1995,"created_at":"2019-12-03T05:04:26Z"} {"id":589160730580,"first_name":"Oralie","last_name":"Libby","email":"olibbyof@google.nl","gender":"Female","ip_address":"78.52.130.227","flagged":true,"year":1996,"created_at":"2018-10-31T21:22:58Z"} {"id":708170124973,"first_name":"Donica","last_name":"Hanbury-Brown","email":"dhanburybrownog@parallels.com","gender":"Genderfluid","flagged":false,"year":2000,"created_at":"2001-09-08T17:48:23Z"} {"id":1067649833999,"first_name":"Marilee","last_name":"Edmed","email":"medmedoh@cbslocal.com","gender":"Female","flagged":true,"year":2010,"created_at":"2009-06-22T13:12:54Z"} {"id":1042906572730,"first_name":"Farrah","last_name":"McGirr","email":"fmcgirroi@t.co","gender":"Female","ip_address":"4.94.231.34","flagged":true,"year":2011,"created_at":"2019-05-31T04:43:20Z"} {"id":582507502161,"first_name":"Ephrem","email":"egallellioj@businesswire.com","gender":"Genderqueer","ip_address":"13.37.233.0","flagged":false,"year":1992,"created_at":"2012-01-18T01:50:53Z"} {"id":491345370417,"first_name":"Izabel","last_name":"Charkham","email":"icharkhamok@freewebs.com","gender":"Agender","ip_address":"74.77.174.128","flagged":false,"year":1987,"created_at":"2016-12-16T04:17:24Z"} {"id":637122792296,"first_name":"Elena","last_name":"Abyss","email":"eabyssol@foxnews.com","gender":"Non-binary","ip_address":"60.217.150.178","flagged":false,"year":1994,"created_at":"2014-06-24T01:02:43Z"} {"id":385984850094,"first_name":"Maris","last_name":"Faldoe","email":"mfaldoeom@techcrunch.com","gender":"Male","ip_address":"237.105.92.105","flagged":false,"year":1973,"created_at":"2010-10-17T05:19:38Z"} {"id":866977959949,"first_name":"Kerrin","email":"kruddleon@odnoklassniki.ru","gender":"Female","flagged":false,"year":2001,"created_at":"2002-04-17T18:06:34Z"} {"id":526566177828,"first_name":"Liana","email":"lhawksworthoo@drupal.org","gender":"Female","ip_address":"217.132.233.101","flagged":true,"year":1992,"created_at":"2010-08-05T22:25:01Z"} {"id":773112686368,"first_name":"Dwayne","last_name":"Jeaves","email":"djeavesop@dot.gov","gender":"Agender","flagged":true,"year":2010,"created_at":"2017-06-27T14:07:57Z"} {"id":216725810092,"first_name":"Thatch","last_name":"Torbet","email":"ttorbetoq@tmall.com","gender":"Bigender","ip_address":"15.179.31.26","flagged":true,"year":2000,"created_at":"2013-08-04T12:36:55Z"} {"id":715597203366,"first_name":"Durante","last_name":"Barson","email":"dbarsonor@wisc.edu","gender":"Genderfluid","flagged":true,"year":2010,"created_at":"2011-07-12T14:03:03Z"} {"id":244058849526,"first_name":"Kelsey","last_name":"Stollenhof","gender":"Genderqueer","ip_address":"255.197.85.219","flagged":true} {"id":1212527857422,"first_name":"Noami","last_name":"Bassham","email":"nbasshamot@hatena.ne.jp","gender":"Male","ip_address":"76.109.132.89","flagged":true,"year":2009,"created_at":"2008-05-22T22:18:15Z"} {"id":986311598366,"first_name":"Kathe","last_name":"Bere","email":"kbereou@wufoo.com","gender":"Female","ip_address":"2.101.215.14","flagged":true,"year":1999,"created_at":"2017-04-03T07:27:49Z"} {"id":750021477733,"first_name":"Ogdon","last_name":"Rapelli","email":"orapelliov@elegantthemes.com","gender":"Genderqueer","flagged":true,"year":2007,"created_at":"2020-04-25T09:04:18Z"} {"id":592919586122,"first_name":"Ragnar","last_name":"Bottomley","email":"rbottomleyow@wired.com","gender":"Genderqueer","ip_address":"174.238.174.87","flagged":true,"year":1997,"created_at":"2004-01-08T04:03:42Z"} {"id":769328315017,"first_name":"Lauralee","last_name":"Durbyn","email":"ldurbynox@friendfeed.com","gender":"Non-binary","ip_address":"168.197.64.216","flagged":false,"year":2011,"created_at":"2010-12-20T16:08:10Z"} {"id":457939763380,"first_name":"Carmina","last_name":"Pantone","email":"cpantoneoy@reddit.com","gender":"Agender","flagged":true,"year":1986,"created_at":"2009-11-15T23:01:57Z"} {"id":228067147023,"first_name":"Conan","last_name":"Penk","email":"cpenkoz@hud.gov","gender":"Male","ip_address":"50.59.129.238","flagged":true,"year":1989,"created_at":"2014-01-19T10:51:07Z"} {"id":128508066420,"first_name":"Munroe","last_name":"Resun","email":"mresunp0@unicef.org","gender":"Non-binary","ip_address":"39.102.189.33","flagged":false,"year":2006,"created_at":"2007-11-06T02:58:57Z"} {"id":1174120600063,"first_name":"Kevan","last_name":"Probbin","email":"kprobbinp1@cisco.com","gender":"Genderfluid","ip_address":"230.149.75.233","flagged":false,"year":1996,"created_at":"2007-06-14T19:13:53Z"} {"id":333843714581,"first_name":"Kathye","last_name":"Rojas","email":"krojasp2@163.com","gender":"Genderqueer","flagged":false,"year":2003,"created_at":"2020-09-06T12:28:41Z"} {"id":575192954408,"first_name":"Guglielmo","last_name":"Rosenthal","email":"grosenthalp3@plala.or.jp","gender":"Male","flagged":false,"year":1985,"created_at":"2009-01-18T22:40:28Z"} {"first_name":"Fannie","email":"fluptonp4@redcross.org","gender":"Genderfluid","ip_address":"187.216.13.162","year":1985,"created_at":"2003-09-10T11:43:00Z"} {"id":1096288811361,"first_name":"Jo","last_name":"Stapleford","email":"jstaplefordp5@google.es","gender":"Non-binary","ip_address":"56.222.227.239","flagged":false,"year":1969,"created_at":"2001-11-04T08:10:53Z"} {"id":999089691483,"first_name":"Gennie","last_name":"Scanlin","email":"gscanlinp6@odnoklassniki.ru","gender":"Polygender","flagged":false,"year":2006,"created_at":"2007-09-04T01:29:22Z"} {"id":883077667779,"first_name":"Glynda","last_name":"Penman","email":"gpenmanp7@ebay.co.uk","gender":"Genderfluid","ip_address":"18.181.124.222","flagged":true,"year":1998,"created_at":"2001-11-05T10:57:24Z"} {"id":1153028706702,"first_name":"Fraze","last_name":"Terron","email":"fterronp8@vinaora.com","gender":"Bigender","ip_address":"237.98.47.81","flagged":true,"year":2002,"created_at":"2007-05-18T05:52:56Z"} {"id":943626675755,"first_name":"Kerrill","last_name":"Mibourne","email":"kmibournep9@networksolutions.com","gender":"Bigender","flagged":false,"year":2010,"created_at":"2005-08-20T07:15:42Z"} {"id":1078594127769,"first_name":"Sela","last_name":"Stefanovic","gender":"Genderfluid","ip_address":"32.38.213.191","flagged":true} {"id":366396184736,"first_name":"Stanwood","last_name":"Keemar","gender":"Female","flagged":false} {"id":971276061429,"first_name":"Lynn","last_name":"Partridge","email":"lpartridgepc@over-blog.com","gender":"Polygender","ip_address":"90.81.138.137","flagged":false,"year":2007,"created_at":"2020-09-17T09:53:40Z"} {"id":627331162447,"first_name":"Ash","last_name":"Wederell","email":"awederellpd@disqus.com","gender":"Male","flagged":false,"year":1984,"created_at":"2011-06-22T23:48:52Z"} {"id":315065450204,"first_name":"Dallis","last_name":"Whyard","email":"dwhyardpe@dailymotion.com","gender":"Male","ip_address":"68.3.21.171","flagged":true,"year":1994,"created_at":"2013-06-19T00:33:09Z"} {"id":468266583282,"first_name":"Courtney","last_name":"Witt","email":"cwittpf@shinystat.com","gender":"Bigender","ip_address":"121.8.83.80","flagged":true,"year":2011,"created_at":"2012-10-12T15:29:12Z"} {"first_name":"Burtie","last_name":"Bridger","email":"bbridgerpg@jigsy.com","gender":"Male","year":2006,"created_at":"2012-12-04T10:03:12Z"} {"id":410427481588,"first_name":"Melania","last_name":"Petroff","email":"mpetroffph@github.com","gender":"Non-binary","flagged":false,"year":2010,"created_at":"2009-06-25T14:48:08Z"} {"id":1046227587840,"first_name":"Vida","last_name":"Matis","email":"vmatispi@bloomberg.com","gender":"Female","ip_address":"246.94.166.165","flagged":false,"year":2011,"created_at":"2008-01-15T17:04:09Z"} {"id":627510579022,"first_name":"Gardiner","email":"gcrightonpj@de.vu","gender":"Non-binary","ip_address":"57.195.165.38","flagged":false,"year":2008,"created_at":"2004-12-11T21:15:40Z"} {"id":646173944943,"first_name":"Zaria","last_name":"Rudwell","email":"zrudwellpk@amazon.co.uk","gender":"Bigender","ip_address":"248.112.45.8","flagged":false,"year":1999,"created_at":"2011-05-30T04:02:08Z"} {"id":594882915488,"first_name":"Archibald","last_name":"Witherspoon","email":"awitherspoonpl@cbc.ca","gender":"Agender","ip_address":"152.241.60.62","flagged":true,"year":1992,"created_at":"2008-09-12T14:04:00Z"} {"id":674751574969,"first_name":"Arte","last_name":"Wasiel","email":"awasielpm@springer.com","gender":"Bigender","flagged":false,"year":1999,"created_at":"2002-11-12T12:02:57Z"} {"id":151928930192,"first_name":"Raine","last_name":"Wanell","email":"rwanellpn@creativecommons.org","gender":"Genderfluid","flagged":false,"year":1987,"created_at":"2009-06-17T16:23:14Z"} {"id":656115216521,"first_name":"Maxie","last_name":"Duell","email":"mduellpo@aol.com","gender":"Polygender","ip_address":"236.27.254.134","flagged":true,"year":1992,"created_at":"2015-09-08T00:46:15Z"} {"id":1082440298067,"first_name":"Nolana","email":"nnottingampp@mapquest.com","gender":"Genderfluid","flagged":false,"year":1993,"created_at":"2019-11-18T08:19:08Z"} {"id":804345634215,"first_name":"Kaylee","last_name":"Stillwell","email":"kstillwellpq@auda.org.au","gender":"Polygender","ip_address":"146.4.113.48","flagged":true,"year":1992,"created_at":"2019-06-11T20:05:18Z"} {"id":577288159963,"first_name":"Ransom","last_name":"Gelderd","email":"rgelderdpr@infoseek.co.jp","gender":"Non-binary","flagged":true,"year":1998,"created_at":"2009-12-20T14:55:18Z"} {"first_name":"Toby","last_name":"Reavey","email":"treaveyps@techcrunch.com","gender":"Male","year":2007,"created_at":"2020-09-27T19:47:22Z"} {"first_name":"Pinchas","last_name":"Knott","email":"pknottpt@mapquest.com","gender":"Bigender","ip_address":"193.175.79.31","year":2013,"created_at":"2018-12-31T01:35:41Z"} {"id":1094044662466,"first_name":"Sallyanne","last_name":"Meddows","email":"smeddowspu@vinaora.com","gender":"Non-binary","flagged":true,"year":2001,"created_at":"2003-03-14T22:11:04Z"} {"id":763851683352,"first_name":"Max","last_name":"Ardling","email":"mardlingpv@dell.com","gender":"Bigender","ip_address":"7.56.70.88","flagged":true,"year":1995,"created_at":"2019-08-11T17:30:01Z"} {"id":296232056903,"first_name":"Waldemar","last_name":"Coaker","email":"wcoakerpw@mediafire.com","gender":"Genderqueer","ip_address":"141.114.220.26","flagged":true,"year":2002,"created_at":"2012-01-02T18:12:10Z"} {"id":186134700970,"first_name":"Oneida","last_name":"Fahey","email":"ofaheypx@acquirethisname.com","gender":"Female","flagged":true,"year":2006,"created_at":"2014-06-01T01:31:07Z"} {"id":1217857255438,"first_name":"Way","last_name":"Lynde","email":"wlyndepy@hexun.com","gender":"Genderqueer","flagged":false,"year":1995,"created_at":"2001-09-25T06:49:41Z"} {"id":745946465745,"first_name":"Geoffry","email":"gheasleypz@alexa.com","gender":"Polygender","ip_address":"11.199.36.72","flagged":false,"year":1998,"created_at":"2016-03-16T19:24:02Z"} {"id":965716488412,"first_name":"Putnam","last_name":"Macieja","email":"pmaciejaq0@woothemes.com","gender":"Male","ip_address":"251.23.8.221","flagged":true,"year":2010,"created_at":"2018-05-29T08:46:58Z"} {"id":395564707327,"first_name":"Lenna","email":"llubeq1@ebay.co.uk","gender":"Non-binary","ip_address":"38.187.7.98","flagged":true,"year":2009,"created_at":"2012-09-10T00:36:28Z"} {"id":492445398359,"first_name":"Corri","last_name":"Kissock","email":"ckissockq2@google.com.hk","gender":"Genderqueer","flagged":false,"year":1997,"created_at":"2020-04-22T17:11:34Z"} {"id":593275018904,"first_name":"Livvyy","last_name":"Kinglake","email":"lkinglakeq3@usa.gov","gender":"Genderqueer","ip_address":"211.78.232.88","flagged":true,"year":2005,"created_at":"2004-01-01T17:12:02Z"} {"id":547201969713,"first_name":"Pru","last_name":"Jaspar","gender":"Genderfluid","flagged":false} {"id":131468295051,"first_name":"Ardenia","last_name":"Breakwell","email":"abreakwellq5@omniture.com","gender":"Genderfluid","flagged":false,"year":2007,"created_at":"2017-12-14T09:20:48Z"} {"id":1215011239910,"first_name":"Farrand","last_name":"Metham","email":"fmethamq6@rambler.ru","gender":"Male","flagged":true,"year":2004,"created_at":"2009-04-30T09:51:44Z"} {"id":1086049708047,"first_name":"Robbert","last_name":"Markham","email":"rmarkhamq7@msn.com","gender":"Bigender","ip_address":"116.32.112.169","flagged":false,"year":1997,"created_at":"2011-10-01T00:20:17Z"} {"first_name":"Alessandra","last_name":"Napton","email":"anaptonq8@icq.com","gender":"Agender","year":2008,"created_at":"2011-04-30T03:30:08Z"} {"id":291541783007,"first_name":"Viviana","last_name":"Foynes","email":"vfoynesq9@instagram.com","gender":"Male","flagged":false,"year":1986,"created_at":"2014-11-12T11:49:12Z"} {"id":726837752063,"first_name":"Lonee","last_name":"Leisk","email":"lleiskqa@exblog.jp","gender":"Polygender","ip_address":"206.83.58.41","flagged":true,"year":2005,"created_at":"2017-06-06T18:22:20Z"} {"id":580411336386,"first_name":"Hymie","last_name":"Bergstram","email":"hbergstramqb@mapquest.com","gender":"Genderfluid","ip_address":"94.56.206.252","flagged":true,"year":1993,"created_at":"2007-01-02T21:10:42Z"} {"id":842593409603,"first_name":"Jere","last_name":"Daveley","email":"jdaveleyqc@newyorker.com","gender":"Male","flagged":false,"year":1996,"created_at":"2010-12-08T19:55:57Z"} {"id":997020848360,"first_name":"Erina","last_name":"Dessent","gender":"Non-binary","ip_address":"213.58.101.171","flagged":false} {"id":600420366592,"first_name":"Nathan","last_name":"Pembridge","email":"npembridgeqe@nytimes.com","gender":"Genderfluid","ip_address":"57.15.221.151","flagged":true,"year":1996,"created_at":"2005-12-27T14:55:45Z"} {"id":1085573271761,"first_name":"Jeno","last_name":"Padgett","email":"jpadgettqf@wp.com","gender":"Agender","ip_address":"245.198.228.246","flagged":false,"year":2010,"created_at":"2007-10-10T11:25:57Z"} {"id":1021419485885,"first_name":"Audrie","last_name":"Mays","email":"amaysqg@blogspot.com","gender":"Polygender","ip_address":"21.99.17.47","flagged":false,"year":1989,"created_at":"2020-03-15T18:40:54Z"} {"id":662873103405,"first_name":"Sibby","last_name":"Dicte","email":"sdicteqh@homestead.com","gender":"Bigender","ip_address":"18.53.191.230","flagged":true,"year":2000,"created_at":"2016-12-17T22:40:46Z"} {"id":136168751713,"first_name":"Adelle","last_name":"Falconbridge","email":"afalconbridgeqi@comsenz.com","gender":"Genderfluid","ip_address":"144.189.245.248","flagged":false,"year":2010,"created_at":"2014-05-17T17:53:21Z"} {"id":867366371613,"first_name":"Shir","last_name":"Shottin","email":"sshottinqj@abc.net.au","gender":"Bigender","flagged":false,"year":1990,"created_at":"2012-03-23T14:22:38Z"} {"id":488415254905,"first_name":"Georgia","last_name":"Maleck","email":"gmaleckqk@nature.com","gender":"Male","ip_address":"216.42.245.6","flagged":false,"year":2006,"created_at":"2007-10-01T14:42:14Z"} {"id":231663151797,"first_name":"Valida","last_name":"Houlahan","email":"vhoulahanql@pen.io","gender":"Polygender","ip_address":"3.66.47.237","flagged":true,"year":2000,"created_at":"2017-02-10T01:53:29Z"} {"first_name":"Winny","last_name":"McGuggy","email":"wmcguggyqm@paginegialle.it","gender":"Genderfluid","ip_address":"192.240.65.129","year":2010,"created_at":"2003-06-22T00:38:04Z"} {"id":385532475251,"first_name":"Cly","last_name":"Bartkowiak","email":"cbartkowiakqn@sitemeter.com","gender":"Non-binary","flagged":false,"year":2004,"created_at":"2008-11-29T23:24:37Z"} {"id":357764476033,"first_name":"Nikolos","last_name":"Slocombe","email":"nslocombeqo@google.cn","gender":"Genderqueer","flagged":true,"year":2008,"created_at":"2010-01-05T17:43:56Z"} {"id":128769464258,"first_name":"Otes","last_name":"Mercer","email":"omercerqp@blogspot.com","gender":"Genderfluid","flagged":false,"year":2000,"created_at":"2009-08-10T09:38:10Z"} {"id":1096286168876,"first_name":"Rudolf","last_name":"Scanderet","email":"rscanderetqq@google.co.jp","gender":"Bigender","ip_address":"30.190.101.2","flagged":false,"year":1991,"created_at":"2016-05-19T14:42:47Z"} {"id":597644548182,"first_name":"Jack","email":"jgarmonqr@nationalgeographic.com","gender":"Genderqueer","ip_address":"174.191.127.240","flagged":true,"year":2010,"created_at":"2018-10-09T07:09:52Z"} {"first_name":"Patrizia","last_name":"Eskrigg","email":"peskriggqs@purevolume.com","gender":"Female","ip_address":"247.157.224.51","year":1995,"created_at":"2014-03-07T09:55:08Z"} {"id":719214387383,"first_name":"Lonnie","email":"lbuckieqt@constantcontact.com","gender":"Female","ip_address":"40.47.52.205","flagged":true,"year":1993,"created_at":"2003-10-20T04:33:30Z"} {"id":1201974375784,"first_name":"Bobbee","last_name":"Propper","email":"bpropperqu@techcrunch.com","gender":"Non-binary","flagged":false,"year":2011,"created_at":"2012-05-21T08:14:47Z"} {"id":1020170760079,"first_name":"Julie","last_name":"Ghost","email":"jghostqv@cmu.edu","gender":"Polygender","ip_address":"64.94.41.47","flagged":false,"year":1996,"created_at":"2003-10-27T19:40:52Z"} {"id":1222723825914,"first_name":"Kathie","last_name":"Nansom","email":"knansomqw@slideshare.net","gender":"Bigender","ip_address":"210.226.176.64","flagged":true,"year":2008,"created_at":"2016-10-25T19:27:23Z"} {"id":1004241630589,"first_name":"Karilynn","last_name":"Zebedee","email":"kzebedeeqx@cnbc.com","gender":"Bigender","ip_address":"164.153.118.23","flagged":false,"year":1994,"created_at":"2001-10-29T17:43:04Z"} {"id":460602817589,"first_name":"Aldin","last_name":"Pamment","email":"apammentqy@dedecms.com","gender":"Polygender","ip_address":"232.18.178.122","flagged":true,"year":2009,"created_at":"2008-12-14T04:21:56Z"} {"id":456005394837,"first_name":"Brigham","last_name":"Titchmarsh","email":"btitchmarshqz@g.co","gender":"Polygender","ip_address":"70.219.47.174","flagged":false,"year":2003,"created_at":"2014-02-24T23:26:00Z"} {"id":912089158859,"first_name":"Deana","last_name":"Spring","email":"dspringr0@gnu.org","gender":"Bigender","flagged":false,"year":1995,"created_at":"2014-10-26T19:13:47Z"} {"id":1189045636141,"first_name":"Anastasie","last_name":"Meredith","email":"ameredithr1@cbc.ca","gender":"Genderqueer","ip_address":"92.201.108.101","flagged":false,"year":2005,"created_at":"2003-05-11T15:52:59Z"} {"id":980408613421,"first_name":"Nathanil","last_name":"Whittlesea","email":"nwhittlesear2@nba.com","gender":"Female","flagged":false,"year":1993,"created_at":"2005-12-10T00:22:42Z"} {"id":1024410419590,"first_name":"Sybilla","email":"stoplinr3@instagram.com","gender":"Male","flagged":false,"year":2010,"created_at":"2011-07-08T13:49:13Z"} {"id":475104643489,"first_name":"Alidia","email":"adoucher4@dagondesign.com","gender":"Polygender","ip_address":"130.84.222.124","flagged":false,"year":1995,"created_at":"2004-10-18T09:21:22Z"} {"id":496501203539,"first_name":"Janek","last_name":"Morse","email":"jmorser5@guardian.co.uk","gender":"Genderfluid","ip_address":"18.80.17.131","flagged":true,"year":2004,"created_at":"2016-03-29T09:26:40Z"} {"id":1132862188362,"first_name":"Thedrick","last_name":"O'Shirine","email":"toshiriner6@businesswire.com","gender":"Male","flagged":true,"year":1989,"created_at":"2013-10-15T06:13:15Z"} {"id":281203338393,"first_name":"Elfreda","last_name":"Congram","gender":"Female","ip_address":"10.197.30.140","flagged":false} {"id":983158115237,"first_name":"Allyn","last_name":"Fick","email":"afickr8@linkedin.com","gender":"Genderqueer","ip_address":"6.87.83.124","flagged":false,"year":2001,"created_at":"2005-05-23T17:27:14Z"} {"id":1050379450504,"first_name":"Myrah","last_name":"Suermeier","gender":"Male","flagged":true} {"id":610262510583,"first_name":"Bruis","email":"bendersra@nbcnews.com","gender":"Bigender","ip_address":"138.82.31.220","flagged":false,"year":1991,"created_at":"2002-05-11T03:03:57Z"} {"id":340656443848,"first_name":"Elayne","last_name":"Kimmerling","email":"ekimmerlingrb@mit.edu","ip_address":"148.154.193.131","flagged":true,"year":2004,"created_at":"2012-03-04T02:44:18Z"} {"id":257747347613,"first_name":"Dorthea","last_name":"Rangall","email":"drangallrc@aol.com","ip_address":"126.181.247.117","flagged":false,"year":1992,"created_at":"2020-11-15T11:42:29Z"} {"id":1009480575819,"first_name":"Dal","last_name":"Mellhuish","email":"dmellhuishrd@mashable.com","gender":"Non-binary","ip_address":"132.24.141.252","flagged":true,"year":2006,"created_at":"2005-07-10T23:52:20Z"} {"id":556658387852,"first_name":"Sibyl","last_name":"Edsall","email":"sedsallre@cloudflare.com","gender":"Bigender","ip_address":"157.151.125.2","flagged":false,"year":1985,"created_at":"2008-08-12T00:18:21Z"} {"id":695559963673,"first_name":"Scott","email":"sdennistounrf@marriott.com","gender":"Genderfluid","ip_address":"112.116.212.12","flagged":true,"year":2000,"created_at":"2001-12-30T06:03:47Z"} {"first_name":"Kristal","last_name":"Bygrave","gender":"Bigender","ip_address":"114.141.1.142"} {"id":213628019272,"first_name":"Gerrie","last_name":"Vayro","email":"gvayrorh@sina.com.cn","gender":"Genderfluid","ip_address":"100.176.254.57","flagged":false,"year":2006,"created_at":"2020-12-27T18:10:19Z"} {"id":291602808547,"first_name":"Suzy","last_name":"Dobney","email":"sdobneyri@ebay.com","gender":"Bigender","ip_address":"25.243.91.123","flagged":true,"year":2006,"created_at":"2009-01-18T22:00:30Z"} {"id":684372832614,"first_name":"Lorianna","last_name":"Woolmington","email":"lwoolmingtonrj@51.la","gender":"Male","ip_address":"140.59.118.25","flagged":true,"year":1985,"created_at":"2010-09-04T07:58:35Z"} {"id":840834621946,"first_name":"Federico","email":"fnutbeamrk@yahoo.com","gender":"Genderfluid","flagged":false,"year":1990,"created_at":"2015-01-31T01:46:46Z"} {"id":901810967077,"first_name":"Elinore","last_name":"Simkin","email":"esimkinrl@4shared.com","gender":"Non-binary","ip_address":"112.234.19.83","flagged":true,"year":1995,"created_at":"2013-03-24T09:23:50Z"} {"id":493596034921,"first_name":"Giffer","last_name":"Rockcliff","email":"grockcliffrm@merriam-webster.com","gender":"Polygender","flagged":true,"year":2005,"created_at":"2020-08-29T15:48:45Z"} {"id":960079865651,"first_name":"Ahmad","last_name":"Rookledge","email":"arookledgern@yandex.ru","gender":"Non-binary","ip_address":"27.40.229.110","flagged":true,"year":2011,"created_at":"2015-02-23T16:48:04Z"} {"id":1211300792258,"first_name":"Livvyy","last_name":"Winteringham","gender":"Agender","ip_address":"137.176.128.18","flagged":false} {"id":683403723560,"first_name":"Minnnie","last_name":"Richfield","email":"mrichfieldrp@homestead.com","gender":"Female","ip_address":"200.107.177.97","flagged":true,"year":1998,"created_at":"2006-11-08T12:28:43Z"} {"id":1162296000918,"first_name":"Petronilla","last_name":"Enrique","email":"penriquerq@sbwire.com","gender":"Female","flagged":false,"year":1997,"created_at":"2018-04-20T22:30:22Z"} {"id":1035748214604,"first_name":"Norry","last_name":"Mongenot","email":"nmongenotrr@uiuc.edu","gender":"Genderfluid","ip_address":"70.240.160.57","flagged":false,"year":1983,"created_at":"2001-08-20T17:59:25Z"} ================================================ FILE: common/workflow-operator/src/test/resources/country_sales_headerless_small.csv ================================================ Australia and Oceania,Tuvalu,Baby Food,Offline,H,5/28/2010,669165933,6/27/2010,9925,255.28,159.42,2533654.00,1582243.50,951410.50 Central America and the Caribbean,Grenada,Cereal,Online,C,8/22/2012,963881480,9/15/2012,2804,205.70,117.11,576782.80,328376.44,248406.36 Europe,Russia,Office Supplies,Offline,L,5/2/2014,341417157,5/8/2014,1779,651.21,524.96,1158502.59,933903.84,224598.75 Sub-Saharan Africa,Sao Tome and Principe,Fruits,Online,C,6/20/2014,514321792,7/5/2014,8102,9.33,6.92,75591.66,56065.84,19525.82 Sub-Saharan Africa,Rwanda,Office Supplies,Offline,L,2/1/2013,115456712,2/6/2013,5062,651.21,524.96,3296425.02,2657347.52,639077.50 Australia and Oceania,Solomon Islands,Baby Food,Online,C,2/4/2015,547995746,2/21/2015,2974,255.28,159.42,759202.72,474115.08,285087.64 Sub-Saharan Africa,Angola,Household,Offline,M,4/23/2011,135425221,4/27/2011,4187,668.27,502.54,2798046.49,2104134.98,693911.51 Sub-Saharan Africa,Burkina Faso,Vegetables,Online,H,7/17/2012,871543967,7/27/2012,8082,154.06,90.93,1245112.92,734896.26,510216.66 Sub-Saharan Africa,Republic of the Congo,Personal Care,Offline,M,7/14/2015,770463311,8/25/2015,6070,81.73,56.67,496101.10,343986.90,152114.20 Sub-Saharan Africa,Senegal,Cereal,Online,H,4/18/2014,616607081,5/30/2014,6593,205.70,117.11,1356180.10,772106.23,584073.87 Asia,Kyrgyzstan,Vegetables,Online,H,6/24/2011,814711606,7/12/2011,124,154.06,90.93,19103.44,11275.32,7828.12 Sub-Saharan Africa,Cape Verde,Clothes,Offline,H,8/2/2014,939825713,8/19/2014,4168,109.28,35.84,455479.04,149381.12,306097.92 Asia,Bangladesh,Clothes,Online,L,1/13/2017,187310731,3/1/2017,8263,109.28,35.84,902980.64,296145.92,606834.72 Central America and the Caribbean,Honduras,Household,Offline,H,2/8/2017,522840487,2/13/2017,8974,668.27,502.54,5997054.98,4509793.96,1487261.02 Asia,Mongolia,Personal Care,Offline,C,2/19/2014,832401311,2/23/2014,4901,81.73,56.67,400558.73,277739.67,122819.06 Europe,Bulgaria,Clothes,Online,M,4/23/2012,972292029,6/3/2012,1673,109.28,35.84,182825.44,59960.32,122865.12 Asia,Sri Lanka,Cosmetics,Offline,M,11/19/2016,419123971,12/18/2016,6952,437.20,263.33,3039414.40,1830670.16,1208744.24 Sub-Saharan Africa,Cameroon,Beverages,Offline,C,4/1/2015,519820964,4/18/2015,5430,47.45,31.79,257653.50,172619.70,85033.80 Asia,Turkmenistan,Household,Offline,L,12/30/2010,441619336,1/20/2011,3830,668.27,502.54,2559474.10,1924728.20,634745.90 Australia and Oceania,East Timor,Meat,Online,L,7/31/2012,322067916,9/11/2012,5908,421.89,364.69,2492526.12,2154588.52,337937.60 Europe,Norway,Baby Food,Online,L,5/14/2014,819028031,6/28/2014,7450,255.28,159.42,1901836.00,1187679.00,714157.00 Europe,Portugal,Baby Food,Online,H,7/31/2015,860673511,9/3/2015,1273,255.28,159.42,324971.44,202941.66,122029.78 Central America and the Caribbean,Honduras,Snacks,Online,L,6/30/2016,795490682,7/26/2016,2225,152.58,97.44,339490.50,216804.00,122686.50 Australia and Oceania,New Zealand,Fruits,Online,H,9/8/2014,142278373,10/4/2014,2187,9.33,6.92,20404.71,15134.04,5270.67 Europe,Moldova ,Personal Care,Online,L,5/7/2016,740147912,5/10/2016,5070,81.73,56.67,414371.10,287316.90,127054.20 Europe,France,Cosmetics,Online,H,5/22/2017,898523128,6/5/2017,1815,437.20,263.33,793518.00,477943.95,315574.05 Australia and Oceania,Kiribati,Fruits,Online,M,10/13/2014,347140347,11/10/2014,5398,9.33,6.92,50363.34,37354.16,13009.18 Sub-Saharan Africa,Mali,Fruits,Online,L,5/7/2010,686048400,5/10/2010,5822,9.33,6.92,54319.26,40288.24,14031.02 Europe,Norway,Beverages,Offline,C,7/18/2014,435608613,7/30/2014,5124,47.45,31.79,243133.80,162891.96,80241.84 Sub-Saharan Africa,The Gambia,Household,Offline,L,5/26/2012,886494815,6/9/2012,2370,668.27,502.54,1583799.90,1191019.80,392780.10 Europe,Switzerland,Cosmetics,Offline,M,9/17/2012,249693334,10/20/2012,8661,437.20,263.33,3786589.20,2280701.13,1505888.07 Sub-Saharan Africa,South Sudan,Personal Care,Offline,C,12/29/2013,406502997,1/28/2014,2125,81.73,56.67,173676.25,120423.75,53252.50 Australia and Oceania,Australia,Office Supplies,Online,C,10/27/2015,158535134,11/25/2015,2924,651.21,524.96,1904138.04,1534983.04,369155.00 Asia,Myanmar,Household,Offline,H,1/16/2015,177713572,3/1/2015,8250,668.27,502.54,5513227.50,4145955.00,1367272.50 Sub-Saharan Africa,Djibouti,Snacks,Online,M,2/25/2017,756274640,2/25/2017,7327,152.58,97.44,1117953.66,713942.88,404010.78 Central America and the Caribbean,Costa Rica,Personal Care,Offline,L,5/8/2017,456767165,5/21/2017,6409,81.73,56.67,523807.57,363198.03,160609.54 Middle East and North Africa,Syria,Fruits,Online,L,11/22/2011,162052476,12/3/2011,3784,9.33,6.92,35304.72,26185.28,9119.44 Sub-Saharan Africa,The Gambia,Meat,Online,M,1/14/2017,825304400,1/23/2017,4767,421.89,364.69,2011149.63,1738477.23,272672.40 Asia,Brunei,Office Supplies,Online,L,4/1/2012,320009267,5/8/2012,6708,651.21,524.96,4368316.68,3521431.68,846885.00 Europe,Bulgaria,Office Supplies,Online,M,2/16/2012,189965903,2/28/2012,3987,651.21,524.96,2596374.27,2093015.52,503358.75 Sub-Saharan Africa,Niger,Personal Care,Online,H,3/11/2017,699285638,3/28/2017,3015,81.73,56.67,246415.95,170860.05,75555.90 Middle East and North Africa,Azerbaijan,Cosmetics,Online,M,2/6/2010,382392299,2/25/2010,7234,437.20,263.33,3162704.80,1904929.22,1257775.58 Sub-Saharan Africa,The Gambia,Cereal,Offline,H,6/7/2012,994022214,6/8/2012,2117,205.70,117.11,435466.90,247921.87,187545.03 Europe,Slovakia,Vegetables,Online,H,10/6/2012,759224212,11/10/2012,171,154.06,90.93,26344.26,15549.03,10795.23 Asia,Myanmar,Clothes,Online,H,11/14/2015,223359620,11/18/2015,5930,109.28,35.84,648030.40,212531.20,435499.20 Sub-Saharan Africa,Comoros,Cereal,Offline,H,3/29/2016,902102267,4/29/2016,962,205.70,117.11,197883.40,112659.82,85223.58 Europe,Iceland,Cosmetics,Online,C,12/31/2016,331438481,12/31/2016,8867,437.20,263.33,3876652.40,2334947.11,1541705.29 Europe,Switzerland,Personal Care,Online,M,12/23/2010,617667090,1/31/2011,273,81.73,56.67,22312.29,15470.91,6841.38 Europe,Macedonia,Clothes,Offline,C,10/14/2014,787399423,11/14/2014,7842,109.28,35.84,856973.76,281057.28,575916.48 Sub-Saharan Africa,Mauritania,Office Supplies,Offline,C,1/11/2012,837559306,1/13/2012,1266,651.21,524.96,824431.86,664599.36,159832.50 Europe,Albania,Clothes,Online,C,2/2/2010,385383069,3/18/2010,2269,109.28,35.84,247956.32,81320.96,166635.36 Sub-Saharan Africa,Lesotho,Fruits,Online,L,8/18/2013,918419539,9/18/2013,9606,9.33,6.92,89623.98,66473.52,23150.46 Middle East and North Africa,Saudi Arabia,Cereal,Online,M,3/25/2013,844530045,3/28/2013,4063,205.70,117.11,835759.10,475817.93,359941.17 Sub-Saharan Africa,Sierra Leone,Office Supplies,Offline,M,11/26/2011,441888415,1/7/2012,3457,651.21,524.96,2251232.97,1814786.72,436446.25 Sub-Saharan Africa,Sao Tome and Principe,Fruits,Offline,H,9/17/2013,508980977,10/24/2013,7637,9.33,6.92,71253.21,52848.04,18405.17 Sub-Saharan Africa,Cote d'Ivoire,Clothes,Online,C,6/8/2012,114606559,6/27/2012,3482,109.28,35.84,380512.96,124794.88,255718.08 Australia and Oceania,Fiji,Clothes,Offline,C,6/30/2010,647876489,8/1/2010,9905,109.28,35.84,1082418.40,354995.20,727423.20 Europe,Austria,Cosmetics,Offline,H,2/23/2015,868214595,3/2/2015,2847,437.20,263.33,1244708.40,749700.51,495007.89 Europe,United Kingdom,Household,Online,L,1/5/2012,955357205,2/14/2012,282,668.27,502.54,188452.14,141716.28,46735.86 Sub-Saharan Africa,Djibouti,Cosmetics,Offline,H,4/7/2014,259353148,4/19/2014,7215,437.20,263.33,3154398.00,1899925.95,1254472.05 Australia and Oceania,Australia,Cereal,Offline,H,6/9/2013,450563752,7/2/2013,682,205.70,117.11,140287.40,79869.02,60418.38 Europe,San Marino,Baby Food,Online,L,6/26/2013,569662845,7/1/2013,4750,255.28,159.42,1212580.00,757245.00,455335.00 Sub-Saharan Africa,Cameroon,Office Supplies,Online,M,11/7/2011,177636754,11/15/2011,5518,651.21,524.96,3593376.78,2896729.28,696647.50 Middle East and North Africa,Libya,Clothes,Offline,H,10/30/2010,705784308,11/17/2010,6116,109.28,35.84,668356.48,219197.44,449159.04 Central America and the Caribbean,Haiti,Cosmetics,Offline,H,10/13/2013,505716836,11/16/2013,1705,437.20,263.33,745426.00,448977.65,296448.35 Sub-Saharan Africa,Rwanda,Cosmetics,Offline,H,10/11/2013,699358165,11/25/2013,4477,437.20,263.33,1957344.40,1178928.41,778415.99 Sub-Saharan Africa,Gabon,Personal Care,Offline,L,7/8/2012,228944623,7/9/2012,8656,81.73,56.67,707454.88,490535.52,216919.36 Central America and the Caribbean,Belize,Clothes,Offline,M,7/25/2016,807025039,9/7/2016,5498,109.28,35.84,600821.44,197048.32,403773.12 Europe,Lithuania,Office Supplies,Offline,H,10/24/2010,166460740,11/17/2010,8287,651.21,524.96,5396577.27,4350343.52,1046233.75 Sub-Saharan Africa,Madagascar,Clothes,Offline,L,4/25/2015,610425555,5/28/2015,7342,109.28,35.84,802333.76,263137.28,539196.48 Asia,Turkmenistan,Office Supplies,Online,M,4/23/2013,462405812,5/20/2013,5010,651.21,524.96,3262562.10,2630049.60,632512.50 Middle East and North Africa,Libya,Fruits,Online,L,8/14/2015,816200339,9/30/2015,673,9.33,6.92,6279.09,4657.16,1621.93 Sub-Saharan Africa,Democratic Republic of the Congo,Beverages,Online,C,5/26/2011,585920464,7/15/2011,5741,47.45,31.79,272410.45,182506.39,89904.06 Sub-Saharan Africa,Djibouti,Cereal,Online,H,5/20/2017,555990016,6/17/2017,8656,205.70,117.11,1780539.20,1013704.16,766835.04 Middle East and North Africa,Pakistan,Cosmetics,Offline,L,7/5/2013,231145322,8/16/2013,9892,437.20,263.33,4324782.40,2604860.36,1719922.04 North America,Mexico,Household,Offline,C,11/6/2014,986435210,12/12/2014,6954,668.27,502.54,4647149.58,3494663.16,1152486.42 Australia and Oceania,Federated States of Micronesia,Beverages,Online,C,10/28/2014,217221009,11/15/2014,9379,47.45,31.79,445033.55,298158.41,146875.14 Asia,Laos,Vegetables,Offline,C,9/15/2011,789176547,10/23/2011,3732,154.06,90.93,574951.92,339350.76,235601.16 Europe,Monaco,Baby Food,Offline,H,5/29/2012,688288152,6/2/2012,8614,255.28,159.42,2198981.92,1373243.88,825738.04 Australia and Oceania,Samoa ,Cosmetics,Online,H,7/20/2013,670854651,8/7/2013,9654,437.20,263.33,4220728.80,2542187.82,1678540.98 Europe,Spain,Household,Offline,L,10/21/2012,213487374,11/30/2012,4513,668.27,502.54,3015902.51,2267963.02,747939.49 Middle East and North Africa,Lebanon,Clothes,Online,L,9/18/2012,663110148,10/8/2012,7884,109.28,35.84,861563.52,282562.56,579000.96 Middle East and North Africa,Iran,Cosmetics,Online,H,11/15/2016,286959302,12/8/2016,6489,437.20,263.33,2836990.80,1708748.37,1128242.43 Sub-Saharan Africa,Zambia,Snacks,Online,L,1/4/2011,122583663,1/5/2011,4085,152.58,97.44,623289.30,398042.40,225246.90 Sub-Saharan Africa,Kenya,Vegetables,Online,L,3/18/2012,827844560,4/7/2012,6457,154.06,90.93,994765.42,587135.01,407630.41 North America,Mexico,Personal Care,Offline,L,2/17/2012,430915820,3/20/2012,6422,81.73,56.67,524870.06,363934.74,160935.32 Sub-Saharan Africa,Sao Tome and Principe,Beverages,Offline,C,1/16/2011,180283772,1/21/2011,8829,47.45,31.79,418936.05,280673.91,138262.14 Sub-Saharan Africa,The Gambia,Baby Food,Offline,M,2/3/2014,494747245,3/20/2014,5559,255.28,159.42,1419101.52,886215.78,532885.74 Middle East and North Africa,Kuwait,Fruits,Online,M,4/30/2012,513417565,5/18/2012,522,9.33,6.92,4870.26,3612.24,1258.02 Europe,Slovenia,Beverages,Offline,C,10/23/2016,345718562,11/25/2016,4660,47.45,31.79,221117.00,148141.40,72975.60 Sub-Saharan Africa,Sierra Leone,Office Supplies,Offline,H,12/6/2016,621386563,12/14/2016,948,651.21,524.96,617347.08,497662.08,119685.00 Australia and Oceania,Australia,Beverages,Offline,H,7/7/2014,240470397,7/11/2014,9389,47.45,31.79,445508.05,298476.31,147031.74 Middle East and North Africa,Azerbaijan,Office Supplies,Online,M,6/13/2012,423331391,7/24/2012,2021,651.21,524.96,1316095.41,1060944.16,255151.25 Europe,Romania,Cosmetics,Online,H,11/26/2010,660643374,12/25/2010,7910,437.20,263.33,3458252.00,2082940.30,1375311.70 Central America and the Caribbean,Nicaragua,Beverages,Offline,C,2/8/2011,963392674,3/21/2011,8156,47.45,31.79,387002.20,259279.24,127722.96 Sub-Saharan Africa,Mali,Clothes,Online,M,7/26/2011,512878119,9/3/2011,888,109.28,35.84,97040.64,31825.92,65214.72 Asia,Malaysia,Fruits,Offline,L,11/11/2011,810711038,12/28/2011,6267,9.33,6.92,58471.11,43367.64,15103.47 Sub-Saharan Africa,Sierra Leone,Vegetables,Offline,C,6/1/2016,728815257,6/29/2016,1485,154.06,90.93,228779.10,135031.05,93748.05 North America,Mexico,Personal Care,Offline,M,7/30/2015,559427106,8/8/2015,5767,81.73,56.67,471336.91,326815.89,144521.02 Sub-Saharan Africa,Mozambique,Household,Offline,L,2/10/2012,665095412,2/15/2012,5367,668.27,502.54,3586605.09,2697132.18,889472.91 ================================================ FILE: common/workflow-operator/src/test/resources/country_sales_headerless_small_multi_line.csv ================================================ Australia and Oceania,Tuvalu,Baby Food,Offline,H,5/28/2010,669165933,6/27/2010,9925,255.28,159.42,2533654.00,1582243.50,951410.50 Central America and the Caribbean,Grenada,Cereal,Online,C,8/22/2012,963881480,9/15/2012,2804,205.70,117.11,576782.80,328376.44,248406.36 Europe,Russia,Office Supplies,Offline,L,5/2/2014,341417157,5/8/2014,1779,651.21,524.96,1158502.59,933903.84,224598.75 Sub-Saharan Africa,Sao Tome and Principe,Fruits,Online,C,6/20/2014,514321792,7/5/2014,8102,9.33,6.92,75591.66,56065.84,19525.82 Sub-Saharan Africa,Rwanda,Office Supplies,Offline,L,2/1/2013,115456712,2/6/2013,5062,651.21,524.96,3296425.02,2657347.52,639077.50 Australia and Oceania,Solomon Islands,Baby Food,Online,C,2/4/2015,547995746,2/21/2015,2974,255.28,159.42,759202.72,474115.08,285087.64 Sub-Saharan Africa,Angola,Household,Offline,M,4/23/2011,135425221,4/27/2011,4187,668.27,502.54,2798046.49,2104134.98,693911.51 Sub-Saharan Africa,Burkina Faso,Vegetables,Online,H,7/17/2012,871543967,7/27/2012,8082,154.06,90.93,1245112.92,734896.26,510216.66 Sub-Saharan Africa,"Republic,of,the,Congo",Personal Care,Offline,M,7/14/2015 ,770463311,8/25/2015 ,6070,81.73 ,56.67 ,496101.10 ,343986.90 ,152114.20 Sub-Saharan Africa,Senegal,Cereal,Online,H,4/18/2014,616607081,5/30/2014,6593,205.70,117.11,1356180.10,772106.23,584073.87 Asia,Kyrgyzstan,Vegetables,Online,H,6/24/2011,814711606,7/12/2011,124,154.06,90.93,19103.44,11275.32,7828.12 Sub-Saharan Africa,"Cape Verde",Clothes,Offline,H,8/2/2014 ,939825713,8/19/2014 ,4168,109.28,35.84 ,455479.04 ,149381.12 ,306097.92 Asia,Bangladesh,Clothes,Online,L,1/13/2017,187310731,3/1/2017,8263,109.28,35.84,902980.64,296145.92,606834.72 Central America and the Caribbean,Honduras,Household,Offline,H,2/8/2017,522840487,2/13/2017,8974,668.27,502.54,5997054.98,4509793.96,1487261.02 Asia,Mongolia,Personal Care,Offline,C,2/19/2014,832401311,2/23/2014,4901,81.73,56.67,400558.73,277739.67,122819.06 Europe,Bulgaria,Clothes,Online,M,4/23/2012,972292029,6/3/2012,1673,109.28,35.84,182825.44,59960.32,122865.12 Asia,Sri Lanka,Cosmetics,Offline,M,11/19/2016,419123971,12/18/2016,6952,437.20,263.33,3039414.40,1830670.16,1208744.24 Sub-Saharan Africa,Cameroon,Beverages,Offline,C,4/1/2015,519820964,4/18/2015,5430,47.45,31.79,257653.50,172619.70,85033.80 Asia,Turkmenistan,Household,Offline,L,12/30/2010,441619336,1/20/2011,3830,668.27,502.54,2559474.10,1924728.20,634745.90 Australia and Oceania,East Timor,Meat,Online,L,7/31/2012,322067916,9/11/2012,5908,421.89,364.69,2492526.12,2154588.52,337937.60 Europe,Norway,Baby Food,Online,L,5/14/2014,819028031,6/28/2014,7450,255.28,159.42,1901836.00,1187679.00,714157.00 Europe,Portugal,Baby Food,Online,H,7/31/2015,860673511,9/3/2015,1273,255.28,159.42,324971.44,202941.66,122029.78 Central America and the Caribbean,Honduras,Snacks,Online,L,6/30/2016,795490682,7/26/2016,2225,152.58,97.44,339490.50,216804.00,122686.50 Australia and Oceania,New Zealand,Fruits,Online,H,9/8/2014,142278373,10/4/2014,2187,9.33,6.92,20404.71,15134.04,5270.67 Europe,Moldova ,Personal Care,Online,L,5/7/2016,740147912,5/10/2016,5070,81.73,56.67,414371.10,287316.90,127054.20 Europe,France,Cosmetics,Online,H,5/22/2017,898523128,6/5/2017,1815,437.20,263.33,793518.00,477943.95,315574.05 Australia and Oceania,Kiribati,Fruits,Online,M,10/13/2014,347140347,11/10/2014,5398,9.33,6.92,50363.34,37354.16,13009.18 Sub-Saharan Africa,Mali,Fruits,Online,L,5/7/2010,686048400,5/10/2010,5822,9.33,6.92,54319.26,40288.24,14031.02 Europe,Norway,Beverages,Offline,C,7/18/2014,435608613,7/30/2014,5124,47.45,31.79,243133.80,162891.96,80241.84 Sub-Saharan Africa,The Gambia,Household,Offline,L,5/26/2012,886494815,6/9/2012,2370,668.27,502.54,1583799.90,1191019.80,392780.10 Europe,Switzerland,Cosmetics,Offline,M,9/17/2012,249693334,10/20/2012,8661,437.20,263.33,3786589.20,2280701.13,1505888.07 Sub-Saharan Africa,South Sudan,Personal Care,Offline,C,12/29/2013,406502997,1/28/2014,2125,81.73,56.67,173676.25,120423.75,53252.50 Australia and Oceania,Australia,Office Supplies,Online,C,10/27/2015,158535134,11/25/2015,2924,651.21,524.96,1904138.04,1534983.04,369155.00 Asia,Myanmar,Household,Offline,H,1/16/2015,177713572,3/1/2015,8250,668.27,502.54,5513227.50,4145955.00,1367272.50 Sub-Saharan Africa,Djibouti,Snacks,Online,M,2/25/2017,756274640,2/25/2017,7327,152.58,97.44,1117953.66,713942.88,404010.78 Central America and the Caribbean,Costa Rica,Personal Care,Offline,L,5/8/2017,456767165,5/21/2017,6409,81.73,56.67,523807.57,363198.03,160609.54 Middle East and North Africa,Syria,Fruits,Online,L,11/22/2011,162052476,12/3/2011,3784,9.33,6.92,35304.72,26185.28,9119.44 Sub-Saharan Africa,The Gambia,Meat,Online,M,1/14/2017,825304400,1/23/2017,4767,421.89,364.69,2011149.63,1738477.23,272672.40 Asia,Brunei,Office Supplies,Online,L,4/1/2012,320009267,5/8/2012,6708,651.21,524.96,4368316.68,3521431.68,846885.00 Europe,Bulgaria,Office Supplies,Online,M,2/16/2012,189965903,2/28/2012,3987,651.21,524.96,2596374.27,2093015.52,503358.75 Sub-Saharan Africa,Niger,Personal Care,Online,H,3/11/2017,699285638,3/28/2017,3015,81.73,56.67,246415.95,170860.05,75555.90 Middle East and North Africa,Azerbaijan,Cosmetics,Online,M,2/6/2010,382392299,2/25/2010,7234,437.20,263.33,3162704.80,1904929.22,1257775.58 Sub-Saharan Africa,The Gambia,Cereal,Offline,H,6/7/2012,994022214,6/8/2012,2117,205.70,117.11,435466.90,247921.87,187545.03 Europe,Slovakia,Vegetables,Online,H,10/6/2012,759224212,11/10/2012,171,154.06,90.93,26344.26,15549.03,10795.23 Asia,Myanmar,Clothes,Online,H,11/14/2015,223359620,11/18/2015,5930,109.28,35.84,648030.40,212531.20,435499.20 Sub-Saharan Africa,Comoros,Cereal,Offline,H,3/29/2016,902102267,4/29/2016,962,205.70,117.11,197883.40,112659.82,85223.58 Europe,Iceland,Cosmetics,Online,C,12/31/2016,331438481,12/31/2016,8867,437.20,263.33,3876652.40,2334947.11,1541705.29 Europe,Switzerland,Personal Care,Online,M,12/23/2010,617667090,1/31/2011,273,81.73,56.67,22312.29,15470.91,6841.38 Europe,Macedonia,Clothes,Offline,C,10/14/2014,787399423,11/14/2014,7842,109.28,35.84,856973.76,281057.28,575916.48 Sub-Saharan Africa,Mauritania,Office Supplies,Offline,C,1/11/2012,837559306,1/13/2012,1266,651.21,524.96,824431.86,664599.36,159832.50 Europe,Albania,Clothes,Online,C,2/2/2010,385383069,3/18/2010,2269,109.28,35.84,247956.32,81320.96,166635.36 Sub-Saharan Africa,Lesotho,Fruits,Online,L,8/18/2013,918419539,9/18/2013,9606,9.33,6.92,89623.98,66473.52,23150.46 Middle East and North Africa,Saudi Arabia,Cereal,Online,M,3/25/2013,844530045,3/28/2013,4063,205.70,117.11,835759.10,475817.93,359941.17 Sub-Saharan Africa,Sierra Leone,"Office Supplies",Offline,M,11/26/2011,441888415,1/7/2012 ,3457,651.21,524.96,2251232.97,1814786.72,436446.25 Sub-Saharan Africa,Sao Tome and Principe,Fruits,Offline,H,9/17/2013,508980977,10/24/2013,7637,9.33,6.92,71253.21,52848.04,18405.17 Sub-Saharan Africa,Cote d'Ivoire,Clothes,Online,C,6/8/2012,114606559,6/27/2012,3482,109.28,35.84,380512.96,124794.88,255718.08 Australia and Oceania,Fiji,Clothes,Offline,C,6/30/2010,647876489,8/1/2010,9905,109.28,35.84,1082418.40,354995.20,727423.20 Europe,Austria,Cosmetics,Offline,H,2/23/2015,868214595,3/2/2015,2847,437.20,263.33,1244708.40,749700.51,495007.89 Europe,United Kingdom,Household,Online,L,1/5/2012,955357205,2/14/2012,282,668.27,502.54,188452.14,141716.28,46735.86 Sub-Saharan Africa,Djibouti,Cosmetics,Offline,H,4/7/2014,259353148,4/19/2014,7215,437.20,263.33,3154398.00,1899925.95,1254472.05 Australia and Oceania,Australia,Cereal,Offline,H,6/9/2013,450563752,7/2/2013,682,205.70,117.11,140287.40,79869.02,60418.38 Europe,San Marino,Baby Food,Online,L,6/26/2013,569662845,7/1/2013,4750,255.28,159.42,1212580.00,757245.00,455335.00 Sub-Saharan Africa,Cameroon,Office Supplies,Online,M,11/7/2011,177636754,11/15/2011,5518,651.21,524.96,3593376.78,2896729.28,696647.50 Middle East and North Africa,Libya,Clothes,Offline,H,10/30/2010,705784308,11/17/2010,6116,109.28,35.84,668356.48,219197.44,449159.04 Central America and the Caribbean,Haiti,Cosmetics,Offline,H,10/13/2013,505716836,11/16/2013,1705,437.20,263.33,745426.00,448977.65,296448.35 Sub-Saharan Africa,Rwanda,Cosmetics,Offline,H,10/11/2013,699358165,11/25/2013,4477,437.20,263.33,1957344.40,1178928.41,778415.99 Sub-Saharan Africa,Gabon,Personal Care,Offline,L,7/8/2012,228944623,7/9/2012,8656,81.73,56.67,707454.88,490535.52,216919.36 Central America and the Caribbean,Belize,Clothes,Offline,M,7/25/2016,807025039,9/7/2016,5498,109.28,35.84,600821.44,197048.32,403773.12 Europe,Lithuania,Office Supplies,Offline,H,10/24/2010,166460740,11/17/2010,8287,651.21,524.96,5396577.27,4350343.52,1046233.75 Sub-Saharan Africa,Madagascar,Clothes,Offline,L,4/25/2015,610425555,5/28/2015,7342,109.28,35.84,802333.76,263137.28,539196.48 Asia,Turkmenistan,Office Supplies,Online,M,4/23/2013,462405812,5/20/2013,5010,651.21,524.96,3262562.10,2630049.60,632512.50 Middle East and North Africa,Libya,Fruits,Online,L,8/14/2015,816200339,9/30/2015,673,9.33,6.92,6279.09,4657.16,1621.93 Sub-Saharan Africa,Democratic Republic of the Congo,Beverages,Online,C,5/26/2011,585920464,7/15/2011,5741,47.45,31.79,272410.45,182506.39,89904.06 Sub-Saharan Africa,Djibouti,Cereal,Online,H,5/20/2017,555990016,6/17/2017,8656,205.70,117.11,1780539.20,1013704.16,766835.04 Middle East and North Africa,Pakistan,Cosmetics,Offline,L,7/5/2013,231145322,8/16/2013,9892,437.20,263.33,4324782.40,2604860.36,1719922.04 North America,Mexico,Household,Offline,C,11/6/2014,986435210,12/12/2014,6954,668.27,502.54,4647149.58,3494663.16,1152486.42 Australia and Oceania,Federated States of Micronesia,Beverages,Online,C,10/28/2014,217221009,11/15/2014,9379,47.45,31.79,445033.55,298158.41,146875.14 Asia,Laos,Vegetables,Offline,C,9/15/2011,789176547,10/23/2011,3732,154.06,90.93,574951.92,339350.76,235601.16 Europe,Monaco,Baby Food,Offline,H,5/29/2012,688288152,6/2/2012,8614,255.28,159.42,2198981.92,1373243.88,825738.04 Australia and Oceania,Samoa ,Cosmetics,Online,H,7/20/2013,670854651,8/7/2013,9654,437.20,263.33,4220728.80,2542187.82,1678540.98 Europe,Spain,Household,Offline,L,10/21/2012,213487374,11/30/2012,4513,668.27,502.54,3015902.51,2267963.02,747939.49 Middle East and North Africa,Lebanon,Clothes,Online,L,9/18/2012,663110148,10/8/2012,7884,109.28,35.84,861563.52,282562.56,579000.96 Middle East and North Africa,Iran,Cosmetics,Online,H,11/15/2016,286959302,12/8/2016,6489,437.20,263.33,2836990.80,1708748.37,1128242.43 Sub-Saharan Africa,Zambia,Snacks,Online,L,1/4/2011,122583663,1/5/2011,4085,152.58,97.44,623289.30,398042.40,225246.90 Sub-Saharan Africa,Kenya,Vegetables,Online,L,3/18/2012,827844560,4/7/2012,6457,154.06,90.93,994765.42,587135.01,407630.41 North America,Mexico,Personal Care,Offline,L,2/17/2012,430915820,3/20/2012,6422,81.73,56.67,524870.06,363934.74,160935.32 Sub-Saharan Africa,Sao Tome and Principe,Beverages,Offline,C,1/16/2011,180283772,1/21/2011,8829,47.45,31.79,418936.05,280673.91,138262.14 Sub-Saharan Africa,The Gambia,Baby Food,Offline,M,2/3/2014,494747245,3/20/2014,5559,255.28,159.42,1419101.52,886215.78,532885.74 Middle East and North Africa,Kuwait,Fruits,Online,M,4/30/2012,513417565,5/18/2012,522,9.33,6.92,4870.26,3612.24,1258.02 Europe,Slovenia,Beverages,Offline,C,10/23/2016,345718562,11/25/2016,4660,47.45,31.79,221117.00,148141.40,72975.60 Sub-Saharan Africa,Sierra Leone,Office Supplies,Offline,H,12/6/2016,621386563,12/14/2016,948,651.21,524.96,617347.08,497662.08,119685.00 Australia and Oceania,Australia,Beverages,Offline,H,7/7/2014,240470397,7/11/2014,9389,47.45,31.79,445508.05,298476.31,147031.74 Middle East and North Africa,Azerbaijan,Office Supplies,Online,M,6/13/2012,423331391,7/24/2012,2021,651.21,524.96,1316095.41,1060944.16,255151.25 Europe,Romania,Cosmetics,Online,H,11/26/2010,660643374,12/25/2010,7910,437.20,263.33,3458252.00,2082940.30,1375311.70 Central America and the Caribbean,Nicaragua,Beverages,Offline,C,2/8/2011,963392674,3/21/2011,8156,47.45,31.79,387002.20,259279.24,127722.96 Sub-Saharan Africa,Mali,Clothes,Online,M,7/26/2011,512878119,9/3/2011,888,109.28,35.84,97040.64,31825.92,65214.72 Asia,Malaysia,Fruits,Offline,L,11/11/2011,810711038,12/28/2011,6267,9.33,6.92,58471.11,43367.64,15103.47 Sub-Saharan Africa,Sierra Leone,Vegetables,Offline,C,6/1/2016,728815257,6/29/2016,1485,154.06,90.93,228779.10,135031.05,93748.05 North America,Mexico,"Personal Care",Offline,M,7/30/2015 ,559427106,8/8/2015,5767,81.73 ,56.67 ,471336.91 ,326815.89 ,144521.02 Sub-Saharan Africa,Mozambique,Household,Offline,L,2/10/2012,665095412,2/15/2012,5367,668.27,502.54,3586605.09,2697132.18,889472.91 ================================================ FILE: common/workflow-operator/src/test/resources/country_sales_headerless_small_multi_line_custom_delimiter.csv ================================================ Australia and Oceania;Tuvalu;Baby Food;Offline;H;5/28/2010;669165933;6/27/2010;9925;255.28;159.42;2533654.00;1582243.50;951410.50 Central America and the Caribbean;Grenada;Cereal;Online;C;8/22/2012;963881480;9/15/2012;2804;205.70;117.11;576782.80;328376.44;248406.36 Europe;Russia;Office Supplies;Offline;L;5/2/2014;341417157;5/8/2014;1779;651.21;524.96;1158502.59;933903.84;224598.75 Sub-Saharan Africa;Sao Tome and Principe;Fruits;Online;C;6/20/2014;514321792;7/5/2014;8102;9.33;6.92;75591.66;56065.84;19525.82 Sub-Saharan Africa;Rwanda;Office Supplies;Offline;L;2/1/2013;115456712;2/6/2013;5062;651.21;524.96;3296425.02;2657347.52;639077.50 Australia and Oceania;Solomon Islands;Baby Food;Online;C;2/4/2015;547995746;2/21/2015;2974;255.28;159.42;759202.72;474115.08;285087.64 Sub-Saharan Africa;Angola;Household;Offline;M;4/23/2011;135425221;4/27/2011;4187;668.27;502.54;2798046.49;2104134.98;693911.51 Sub-Saharan Africa;Burkina Faso;Vegetables;Online;H;7/17/2012;871543967;7/27/2012;8082;154.06;90.93;1245112.92;734896.26;510216.66 Sub-Saharan Africa;Republic of the Congo;Personal Care;Offline;M;7/14/2015;770463311;8/25/2015;6070;81.73;56.67;496101.10;343986.90;152114.20 Sub-Saharan Africa;Senegal;Cereal;Online;H;4/18/2014;616607081;5/30/2014;6593;205.70;117.11;1356180.10;772106.23;584073.87 Asia;Kyrgyzstan;Vegetables;Online;H;6/24/2011;814711606;7/12/2011;124;154.06;90.93;19103.44;11275.32;7828.12 Sub-Saharan Africa;Cape Verde;Clothes;Offline;H;8/2/2014;939825713;8/19/2014;4168;109.28;35.84;455479.04;149381.12;306097.92 Asia;Bangladesh;Clothes;Online;L;1/13/2017;187310731;3/1/2017;8263;109.28;35.84;902980.64;296145.92;606834.72 Central America and the Caribbean;Honduras;Household;Offline;H;2/8/2017;522840487;2/13/2017;8974;668.27;502.54;5997054.98;4509793.96;1487261.02 Asia;Mongolia;Personal Care;Offline;C;2/19/2014;832401311;2/23/2014;4901;81.73;56.67;400558.73;277739.67;122819.06 Europe;Bulgaria;Clothes;Online;M;4/23/2012;972292029;6/3/2012;1673;109.28;35.84;182825.44;59960.32;122865.12 Asia;Sri Lanka;Cosmetics;Offline;M;11/19/2016;419123971;12/18/2016;6952;437.20;263.33;3039414.40;1830670.16;1208744.24 Sub-Saharan Africa;Cameroon;Beverages;Offline;C;4/1/2015;519820964;4/18/2015;5430;47.45;31.79;257653.50;172619.70;85033.80 Asia;Turkmenistan;Household;Offline;L;12/30/2010;441619336;1/20/2011;3830;668.27;502.54;2559474.10;1924728.20;634745.90 Australia and Oceania;East Timor;Meat;Online;L;7/31/2012;322067916;9/11/2012;5908;421.89;364.69;2492526.12;2154588.52;337937.60 Europe;Norway;Baby Food;Online;L;5/14/2014;819028031;6/28/2014;7450;255.28;159.42;1901836.00;1187679.00;714157.00 Europe;Portugal;Baby Food;Online;H;7/31/2015;860673511;9/3/2015;1273;255.28;159.42;324971.44;202941.66;122029.78 Central America and the Caribbean;Honduras;Snacks;Online;L;6/30/2016;795490682;7/26/2016;2225;152.58;97.44;339490.50;216804.00;122686.50 Australia and Oceania;New Zealand;Fruits;Online;H;9/8/2014;142278373;10/4/2014;2187;9.33;6.92;20404.71;15134.04;5270.67 Europe;Moldova ;Personal Care;Online;L;5/7/2016;740147912;5/10/2016;5070;81.73;56.67;414371.10;287316.90;127054.20 Europe;France;Cosmetics;Online;H;5/22/2017;898523128;6/5/2017;1815;437.20;263.33;793518.00;477943.95;315574.05 Australia and Oceania;Kiribati;Fruits;Online;M;10/13/2014;347140347;11/10/2014;5398;9.33;6.92;50363.34;37354.16;13009.18 Sub-Saharan Africa;Mali;Fruits;Online;L;5/7/2010;686048400;5/10/2010;5822;9.33;6.92;54319.26;40288.24;14031.02 Europe;Norway;Beverages;Offline;C;7/18/2014;435608613;7/30/2014;5124;47.45;31.79;243133.80;162891.96;80241.84 Sub-Saharan Africa;The Gambia;Household;Offline;L;5/26/2012;886494815;6/9/2012;2370;668.27;502.54;1583799.90;1191019.80;392780.10 Europe;Switzerland;Cosmetics;Offline;M;9/17/2012;249693334;10/20/2012;8661;437.20;263.33;3786589.20;2280701.13;1505888.07 Sub-Saharan Africa;South Sudan;Personal Care;Offline;C;12/29/2013;406502997;1/28/2014;2125;81.73;56.67;173676.25;120423.75;53252.50 Australia and Oceania;Australia;Office Supplies;Online;C;10/27/2015;158535134;11/25/2015;2924;651.21;524.96;1904138.04;1534983.04;369155.00 Asia;Myanmar;Household;Offline;H;1/16/2015;177713572;3/1/2015;8250;668.27;502.54;5513227.50;4145955.00;1367272.50 Sub-Saharan Africa;Djibouti;Snacks;Online;M;2/25/2017;756274640;2/25/2017;7327;152.58;97.44;1117953.66;713942.88;404010.78 Central America and the Caribbean;Costa Rica;Personal Care;Offline;L;5/8/2017;456767165;5/21/2017;6409;81.73;56.67;523807.57;363198.03;160609.54 Middle East and North Africa;Syria;Fruits;Online;L;11/22/2011;162052476;12/3/2011;3784;9.33;6.92;35304.72;26185.28;9119.44 Sub-Saharan Africa;The Gambia;Meat;Online;M;1/14/2017;825304400;1/23/2017;4767;421.89;364.69;2011149.63;1738477.23;272672.40 Asia;Brunei;Office Supplies;Online;L;4/1/2012;320009267;5/8/2012;6708;651.21;524.96;4368316.68;3521431.68;846885.00 Europe;Bulgaria;Office Supplies;Online;M;2/16/2012;189965903;2/28/2012;3987;651.21;524.96;2596374.27;2093015.52;503358.75 Sub-Saharan Africa;Niger;Personal Care;Online;H;3/11/2017;699285638;3/28/2017;3015;81.73;56.67;246415.95;170860.05;75555.90 Middle East and North Africa;Azerbaijan;Cosmetics;Online;M;2/6/2010;382392299;2/25/2010;7234;437.20;263.33;3162704.80;1904929.22;1257775.58 Sub-Saharan Africa;The Gambia;Cereal;Offline;H;6/7/2012;994022214;6/8/2012;2117;205.70;117.11;435466.90;247921.87;187545.03 Europe;Slovakia;Vegetables;Online;H;10/6/2012;759224212;11/10/2012;171;154.06;90.93;26344.26;15549.03;10795.23 Asia;Myanmar;Clothes;Online;H;11/14/2015;223359620;11/18/2015;5930;109.28;35.84;648030.40;212531.20;435499.20 Sub-Saharan Africa;Comoros;Cereal;Offline;H;3/29/2016;902102267;4/29/2016;962;205.70;117.11;197883.40;112659.82;85223.58 Europe;Iceland;Cosmetics;Online;C;12/31/2016;331438481;12/31/2016;8867;437.20;263.33;3876652.40;2334947.11;1541705.29 Europe;Switzerland;Personal Care;Online;M;12/23/2010;617667090;1/31/2011;273;81.73;56.67;22312.29;15470.91;6841.38 Europe;Macedonia;Clothes;Offline;C;10/14/2014;787399423;11/14/2014;7842;109.28;35.84;856973.76;281057.28;575916.48 Sub-Saharan Africa;Mauritania;Office Supplies;Offline;C;1/11/2012;837559306;1/13/2012;1266;651.21;524.96;824431.86;664599.36;159832.50 Europe;Albania;Clothes;Online;C;2/2/2010;385383069;3/18/2010;2269;109.28;35.84;247956.32;81320.96;166635.36 Sub-Saharan Africa;Lesotho;Fruits;Online;L;8/18/2013;918419539;9/18/2013;9606;9.33;6.92;89623.98;66473.52;23150.46 Middle East and North Africa;Saudi Arabia;Cereal;Online;M;3/25/2013;844530045;3/28/2013;4063;205.70;117.11;835759.10;475817.93;359941.17 Sub-Saharan Africa;Sierra Leone;Office Supplies;Offline;M;11/26/2011;441888415;1/7/2012;3457;651.21;524.96;2251232.97;1814786.72;436446.25 Sub-Saharan Africa;Sao Tome and Principe;Fruits;Offline;H;9/17/2013;508980977;10/24/2013;7637;9.33;6.92;71253.21;52848.04;18405.17 Sub-Saharan Africa;Cote d'Ivoire;Clothes;Online;C;6/8/2012;114606559;6/27/2012;3482;109.28;35.84;380512.96;124794.88;255718.08 Australia and Oceania;Fiji;Clothes;Offline;C;6/30/2010;647876489;8/1/2010;9905;109.28;35.84;1082418.40;354995.20;727423.20 Europe;Austria;Cosmetics;Offline;H;2/23/2015;868214595;3/2/2015;2847;437.20;263.33;1244708.40;749700.51;495007.89 Europe;United Kingdom;Household;Online;L;1/5/2012;955357205;2/14/2012;282;668.27;502.54;188452.14;141716.28;46735.86 Sub-Saharan Africa;Djibouti;Cosmetics;Offline;H;4/7/2014;259353148;4/19/2014;7215;437.20;263.33;3154398.00;1899925.95;1254472.05 Australia and Oceania;Australia;Cereal;Offline;H;6/9/2013;450563752;7/2/2013;682;205.70;117.11;140287.40;79869.02;60418.38 Europe;San Marino;Baby Food;Online;L;6/26/2013;569662845;7/1/2013;4750;255.28;159.42;1212580.00;757245.00;455335.00 Sub-Saharan Africa;Cameroon;Office Supplies;Online;M;11/7/2011;177636754;11/15/2011;5518;651.21;524.96;3593376.78;2896729.28;696647.50 Middle East and North Africa;Libya;Clothes;Offline;H;10/30/2010;705784308;11/17/2010;6116;109.28;35.84;668356.48;219197.44;449159.04 Central America and the Caribbean;Haiti;Cosmetics;Offline;H;10/13/2013;505716836;11/16/2013;1705;437.20;263.33;745426.00;448977.65;296448.35 Sub-Saharan Africa;Rwanda;Cosmetics;Offline;H;10/11/2013;699358165;11/25/2013;4477;437.20;263.33;1957344.40;1178928.41;778415.99 Sub-Saharan Africa;Gabon;Personal Care;Offline;L;7/8/2012;228944623;7/9/2012;8656;81.73;56.67;707454.88;490535.52;216919.36 Central America and the Caribbean;Belize;Clothes;Offline;M;7/25/2016;807025039;9/7/2016;5498;109.28;35.84;600821.44;197048.32;403773.12 Europe;Lithuania;Office Supplies;Offline;H;10/24/2010;166460740;11/17/2010;8287;651.21;524.96;5396577.27;4350343.52;1046233.75 Sub-Saharan Africa;Madagascar;Clothes;Offline;L;4/25/2015;610425555;5/28/2015;7342;109.28;35.84;802333.76;263137.28;539196.48 Asia;Turkmenistan;Office Supplies;Online;M;4/23/2013;462405812;5/20/2013;5010;651.21;524.96;3262562.10;2630049.60;632512.50 Middle East and North Africa;Libya;Fruits;Online;L;8/14/2015;816200339;9/30/2015;673;9.33;6.92;6279.09;4657.16;1621.93 Sub-Saharan Africa;Democratic Republic of the Congo;Beverages;Online;C;5/26/2011;585920464;7/15/2011;5741;47.45;31.79;272410.45;182506.39;89904.06 Sub-Saharan Africa;Djibouti;Cereal;Online;H;5/20/2017;555990016;6/17/2017;8656;205.70;117.11;1780539.20;1013704.16;766835.04 Middle East and North Africa;Pakistan;Cosmetics;Offline;L;7/5/2013;231145322;8/16/2013;9892;437.20;263.33;4324782.40;2604860.36;1719922.04 North America;Mexico;Household;Offline;C;11/6/2014;986435210;12/12/2014;6954;668.27;502.54;4647149.58;3494663.16;1152486.42 Australia and Oceania;Federated States of Micronesia;Beverages;Online;C;10/28/2014;217221009;11/15/2014;9379;47.45;31.79;445033.55;298158.41;146875.14 Asia;Laos;Vegetables;Offline;C;9/15/2011;789176547;10/23/2011;3732;154.06;90.93;574951.92;339350.76;235601.16 Europe;Monaco;Baby Food;Offline;H;5/29/2012;688288152;6/2/2012;8614;255.28;159.42;2198981.92;1373243.88;825738.04 Australia and Oceania;Samoa ;Cosmetics;Online;H;7/20/2013;670854651;8/7/2013;9654;437.20;263.33;4220728.80;2542187.82;1678540.98 Europe;Spain;Household;Offline;L;10/21/2012;213487374;11/30/2012;4513;668.27;502.54;3015902.51;2267963.02;747939.49 Middle East and North Africa;Lebanon;Clothes;Online;L;9/18/2012;663110148;10/8/2012;7884;109.28;35.84;861563.52;282562.56;579000.96 Middle East and North Africa;Iran;Cosmetics;Online;H;11/15/2016;286959302;12/8/2016;6489;437.20;263.33;2836990.80;1708748.37;1128242.43 Sub-Saharan Africa;Zambia;Snacks;Online;L;1/4/2011;122583663;1/5/2011;4085;152.58;97.44;623289.30;398042.40;225246.90 Sub-Saharan Africa;Kenya;Vegetables;Online;L;3/18/2012;827844560;4/7/2012;6457;154.06;90.93;994765.42;587135.01;407630.41 North America;Mexico;Personal Care;Offline;L;2/17/2012;430915820;3/20/2012;6422;81.73;56.67;524870.06;363934.74;160935.32 Sub-Saharan Africa;Sao Tome and Principe;Beverages;Offline;C;1/16/2011;180283772;1/21/2011;8829;47.45;31.79;418936.05;280673.91;138262.14 Sub-Saharan Africa;The Gambia;Baby Food;Offline;M;2/3/2014;494747245;3/20/2014;5559;255.28;159.42;1419101.52;886215.78;532885.74 Middle East and North Africa;Kuwait;Fruits;Online;M;4/30/2012;513417565;5/18/2012;522;9.33;6.92;4870.26;3612.24;1258.02 Europe;Slovenia;Beverages;Offline;C;10/23/2016;345718562;11/25/2016;4660;47.45;31.79;221117.00;148141.40;72975.60 Sub-Saharan Africa;Sierra Leone;Office Supplies;Offline;H;12/6/2016;621386563;12/14/2016;948;651.21;524.96;617347.08;497662.08;119685.00 Australia and Oceania;Australia;Beverages;Offline;H;7/7/2014;240470397;7/11/2014;9389;47.45;31.79;445508.05;298476.31;147031.74 Middle East and North Africa;Azerbaijan;Office Supplies;Online;M;6/13/2012;423331391;7/24/2012;2021;651.21;524.96;1316095.41;1060944.16;255151.25 Europe;Romania;Cosmetics;Online;H;11/26/2010;660643374;12/25/2010;7910;437.20;263.33;3458252.00;2082940.30;1375311.70 Central America and the Caribbean;Nicaragua;Beverages;Offline;C;2/8/2011;963392674;3/21/2011;8156;47.45;31.79;387002.20;259279.24;127722.96 Sub-Saharan Africa;Mali;Clothes;Online;M;7/26/2011;512878119;9/3/2011;888;109.28;35.84;97040.64;31825.92;65214.72 Asia;Malaysia;Fruits;Offline;L;11/11/2011;810711038;12/28/2011;6267;9.33;6.92;58471.11;43367.64;15103.47 Sub-Saharan Africa;Sierra Leone;Vegetables;Offline;C;6/1/2016;728815257;6/29/2016;1485;154.06;90.93;228779.10;135031.05;93748.05 North America;Mexico;Personal Care;Offline;M;7/30/2015;559427106;8/8/2015;5767;81.73;56.67;471336.91;326815.89;144521.02 Sub-Saharan Africa;Mozambique;Household;Offline;L;2/10/2012;665095412;2/15/2012;5367;668.27;502.54;3586605.09;2697132.18;889472.91 ================================================ FILE: common/workflow-operator/src/test/resources/country_sales_medium.csv ================================================ [File too large to display: 11.7 MB] ================================================ FILE: common/workflow-operator/src/test/resources/country_sales_small.csv ================================================ Region,Country,Item Type,Sales Channel,Order Priority,Order Date,Order ID,Ship Date,Units Sold,Unit Price,Unit Cost,Total Revenue,Total Cost,Total Profit Australia and Oceania,Tuvalu,Baby Food,Offline,H,5/28/2010,669165933,6/27/2010,9925,255.28,159.42,2533654.00,1582243.50,951410.50 Central America and the Caribbean,Grenada,Cereal,Online,C,8/22/2012,963881480,9/15/2012,2804,205.70,117.11,576782.80,328376.44,248406.36 Europe,Russia,Office Supplies,Offline,L,5/2/2014,341417157,5/8/2014,1779,651.21,524.96,1158502.59,933903.84,224598.75 Sub-Saharan Africa,Sao Tome and Principe,Fruits,Online,C,6/20/2014,514321792,7/5/2014,8102,9.33,6.92,75591.66,56065.84,19525.82 Sub-Saharan Africa,Rwanda,Office Supplies,Offline,L,2/1/2013,115456712,2/6/2013,5062,651.21,524.96,3296425.02,2657347.52,639077.50 Australia and Oceania,Solomon Islands,Baby Food,Online,C,2/4/2015,547995746,2/21/2015,2974,255.28,159.42,759202.72,474115.08,285087.64 Sub-Saharan Africa,Angola,Household,Offline,M,4/23/2011,135425221,4/27/2011,4187,668.27,502.54,2798046.49,2104134.98,693911.51 Sub-Saharan Africa,Burkina Faso,Vegetables,Online,H,7/17/2012,871543967,7/27/2012,8082,154.06,90.93,1245112.92,734896.26,510216.66 Sub-Saharan Africa,Republic of the Congo,Personal Care,Offline,M,7/14/2015,770463311,8/25/2015,6070,81.73,56.67,496101.10,343986.90,152114.20 Sub-Saharan Africa,Senegal,Cereal,Online,H,4/18/2014,616607081,5/30/2014,6593,205.70,117.11,1356180.10,772106.23,584073.87 Asia,Kyrgyzstan,Vegetables,Online,H,6/24/2011,814711606,7/12/2011,124,154.06,90.93,19103.44,11275.32,7828.12 Sub-Saharan Africa,Cape Verde,Clothes,Offline,H,8/2/2014,939825713,8/19/2014,4168,109.28,35.84,455479.04,149381.12,306097.92 Asia,Bangladesh,Clothes,Online,L,1/13/2017,187310731,3/1/2017,8263,109.28,35.84,902980.64,296145.92,606834.72 Central America and the Caribbean,Honduras,Household,Offline,H,2/8/2017,522840487,2/13/2017,8974,668.27,502.54,5997054.98,4509793.96,1487261.02 Asia,Mongolia,Personal Care,Offline,C,2/19/2014,832401311,2/23/2014,4901,81.73,56.67,400558.73,277739.67,122819.06 Europe,Bulgaria,Clothes,Online,M,4/23/2012,972292029,6/3/2012,1673,109.28,35.84,182825.44,59960.32,122865.12 Asia,Sri Lanka,Cosmetics,Offline,M,11/19/2016,419123971,12/18/2016,6952,437.20,263.33,3039414.40,1830670.16,1208744.24 Sub-Saharan Africa,Cameroon,Beverages,Offline,C,4/1/2015,519820964,4/18/2015,5430,47.45,31.79,257653.50,172619.70,85033.80 Asia,Turkmenistan,Household,Offline,L,12/30/2010,441619336,1/20/2011,3830,668.27,502.54,2559474.10,1924728.20,634745.90 Australia and Oceania,East Timor,Meat,Online,L,7/31/2012,322067916,9/11/2012,5908,421.89,364.69,2492526.12,2154588.52,337937.60 Europe,Norway,Baby Food,Online,L,5/14/2014,819028031,6/28/2014,7450,255.28,159.42,1901836.00,1187679.00,714157.00 Europe,Portugal,Baby Food,Online,H,7/31/2015,860673511,9/3/2015,1273,255.28,159.42,324971.44,202941.66,122029.78 Central America and the Caribbean,Honduras,Snacks,Online,L,6/30/2016,795490682,7/26/2016,2225,152.58,97.44,339490.50,216804.00,122686.50 Australia and Oceania,New Zealand,Fruits,Online,H,9/8/2014,142278373,10/4/2014,2187,9.33,6.92,20404.71,15134.04,5270.67 Europe,Moldova ,Personal Care,Online,L,5/7/2016,740147912,5/10/2016,5070,81.73,56.67,414371.10,287316.90,127054.20 Europe,France,Cosmetics,Online,H,5/22/2017,898523128,6/5/2017,1815,437.20,263.33,793518.00,477943.95,315574.05 Australia and Oceania,Kiribati,Fruits,Online,M,10/13/2014,347140347,11/10/2014,5398,9.33,6.92,50363.34,37354.16,13009.18 Sub-Saharan Africa,Mali,Fruits,Online,L,5/7/2010,686048400,5/10/2010,5822,9.33,6.92,54319.26,40288.24,14031.02 Europe,Norway,Beverages,Offline,C,7/18/2014,435608613,7/30/2014,5124,47.45,31.79,243133.80,162891.96,80241.84 Sub-Saharan Africa,The Gambia,Household,Offline,L,5/26/2012,886494815,6/9/2012,2370,668.27,502.54,1583799.90,1191019.80,392780.10 Europe,Switzerland,Cosmetics,Offline,M,9/17/2012,249693334,10/20/2012,8661,437.20,263.33,3786589.20,2280701.13,1505888.07 Sub-Saharan Africa,South Sudan,Personal Care,Offline,C,12/29/2013,406502997,1/28/2014,2125,81.73,56.67,173676.25,120423.75,53252.50 Australia and Oceania,Australia,Office Supplies,Online,C,10/27/2015,158535134,11/25/2015,2924,651.21,524.96,1904138.04,1534983.04,369155.00 Asia,Myanmar,Household,Offline,H,1/16/2015,177713572,3/1/2015,8250,668.27,502.54,5513227.50,4145955.00,1367272.50 Sub-Saharan Africa,Djibouti,Snacks,Online,M,2/25/2017,756274640,2/25/2017,7327,152.58,97.44,1117953.66,713942.88,404010.78 Central America and the Caribbean,Costa Rica,Personal Care,Offline,L,5/8/2017,456767165,5/21/2017,6409,81.73,56.67,523807.57,363198.03,160609.54 Middle East and North Africa,Syria,Fruits,Online,L,11/22/2011,162052476,12/3/2011,3784,9.33,6.92,35304.72,26185.28,9119.44 Sub-Saharan Africa,The Gambia,Meat,Online,M,1/14/2017,825304400,1/23/2017,4767,421.89,364.69,2011149.63,1738477.23,272672.40 Asia,Brunei,Office Supplies,Online,L,4/1/2012,320009267,5/8/2012,6708,651.21,524.96,4368316.68,3521431.68,846885.00 Europe,Bulgaria,Office Supplies,Online,M,2/16/2012,189965903,2/28/2012,3987,651.21,524.96,2596374.27,2093015.52,503358.75 Sub-Saharan Africa,Niger,Personal Care,Online,H,3/11/2017,699285638,3/28/2017,3015,81.73,56.67,246415.95,170860.05,75555.90 Middle East and North Africa,Azerbaijan,Cosmetics,Online,M,2/6/2010,382392299,2/25/2010,7234,437.20,263.33,3162704.80,1904929.22,1257775.58 Sub-Saharan Africa,The Gambia,Cereal,Offline,H,6/7/2012,994022214,6/8/2012,2117,205.70,117.11,435466.90,247921.87,187545.03 Europe,Slovakia,Vegetables,Online,H,10/6/2012,759224212,11/10/2012,171,154.06,90.93,26344.26,15549.03,10795.23 Asia,Myanmar,Clothes,Online,H,11/14/2015,223359620,11/18/2015,5930,109.28,35.84,648030.40,212531.20,435499.20 Sub-Saharan Africa,Comoros,Cereal,Offline,H,3/29/2016,902102267,4/29/2016,962,205.70,117.11,197883.40,112659.82,85223.58 Europe,Iceland,Cosmetics,Online,C,12/31/2016,331438481,12/31/2016,8867,437.20,263.33,3876652.40,2334947.11,1541705.29 Europe,Switzerland,Personal Care,Online,M,12/23/2010,617667090,1/31/2011,273,81.73,56.67,22312.29,15470.91,6841.38 Europe,Macedonia,Clothes,Offline,C,10/14/2014,787399423,11/14/2014,7842,109.28,35.84,856973.76,281057.28,575916.48 Sub-Saharan Africa,Mauritania,Office Supplies,Offline,C,1/11/2012,837559306,1/13/2012,1266,651.21,524.96,824431.86,664599.36,159832.50 Europe,Albania,Clothes,Online,C,2/2/2010,385383069,3/18/2010,2269,109.28,35.84,247956.32,81320.96,166635.36 Sub-Saharan Africa,Lesotho,Fruits,Online,L,8/18/2013,918419539,9/18/2013,9606,9.33,6.92,89623.98,66473.52,23150.46 Middle East and North Africa,Saudi Arabia,Cereal,Online,M,3/25/2013,844530045,3/28/2013,4063,205.70,117.11,835759.10,475817.93,359941.17 Sub-Saharan Africa,Sierra Leone,Office Supplies,Offline,M,11/26/2011,441888415,1/7/2012,3457,651.21,524.96,2251232.97,1814786.72,436446.25 Sub-Saharan Africa,Sao Tome and Principe,Fruits,Offline,H,9/17/2013,508980977,10/24/2013,7637,9.33,6.92,71253.21,52848.04,18405.17 Sub-Saharan Africa,Cote d'Ivoire,Clothes,Online,C,6/8/2012,114606559,6/27/2012,3482,109.28,35.84,380512.96,124794.88,255718.08 Australia and Oceania,Fiji,Clothes,Offline,C,6/30/2010,647876489,8/1/2010,9905,109.28,35.84,1082418.40,354995.20,727423.20 Europe,Austria,Cosmetics,Offline,H,2/23/2015,868214595,3/2/2015,2847,437.20,263.33,1244708.40,749700.51,495007.89 Europe,United Kingdom,Household,Online,L,1/5/2012,955357205,2/14/2012,282,668.27,502.54,188452.14,141716.28,46735.86 Sub-Saharan Africa,Djibouti,Cosmetics,Offline,H,4/7/2014,259353148,4/19/2014,7215,437.20,263.33,3154398.00,1899925.95,1254472.05 Australia and Oceania,Australia,Cereal,Offline,H,6/9/2013,450563752,7/2/2013,682,205.70,117.11,140287.40,79869.02,60418.38 Europe,San Marino,Baby Food,Online,L,6/26/2013,569662845,7/1/2013,4750,255.28,159.42,1212580.00,757245.00,455335.00 Sub-Saharan Africa,Cameroon,Office Supplies,Online,M,11/7/2011,177636754,11/15/2011,5518,651.21,524.96,3593376.78,2896729.28,696647.50 Middle East and North Africa,Libya,Clothes,Offline,H,10/30/2010,705784308,11/17/2010,6116,109.28,35.84,668356.48,219197.44,449159.04 Central America and the Caribbean,Haiti,Cosmetics,Offline,H,10/13/2013,505716836,11/16/2013,1705,437.20,263.33,745426.00,448977.65,296448.35 Sub-Saharan Africa,Rwanda,Cosmetics,Offline,H,10/11/2013,699358165,11/25/2013,4477,437.20,263.33,1957344.40,1178928.41,778415.99 Sub-Saharan Africa,Gabon,Personal Care,Offline,L,7/8/2012,228944623,7/9/2012,8656,81.73,56.67,707454.88,490535.52,216919.36 Central America and the Caribbean,Belize,Clothes,Offline,M,7/25/2016,807025039,9/7/2016,5498,109.28,35.84,600821.44,197048.32,403773.12 Europe,Lithuania,Office Supplies,Offline,H,10/24/2010,166460740,11/17/2010,8287,651.21,524.96,5396577.27,4350343.52,1046233.75 Sub-Saharan Africa,Madagascar,Clothes,Offline,L,4/25/2015,610425555,5/28/2015,7342,109.28,35.84,802333.76,263137.28,539196.48 Asia,Turkmenistan,Office Supplies,Online,M,4/23/2013,462405812,5/20/2013,5010,651.21,524.96,3262562.10,2630049.60,632512.50 Middle East and North Africa,Libya,Fruits,Online,L,8/14/2015,816200339,9/30/2015,673,9.33,6.92,6279.09,4657.16,1621.93 Sub-Saharan Africa,Democratic Republic of the Congo,Beverages,Online,C,5/26/2011,585920464,7/15/2011,5741,47.45,31.79,272410.45,182506.39,89904.06 Sub-Saharan Africa,Djibouti,Cereal,Online,H,5/20/2017,555990016,6/17/2017,8656,205.70,117.11,1780539.20,1013704.16,766835.04 Middle East and North Africa,Pakistan,Cosmetics,Offline,L,7/5/2013,231145322,8/16/2013,9892,437.20,263.33,4324782.40,2604860.36,1719922.04 North America,Mexico,Household,Offline,C,11/6/2014,986435210,12/12/2014,6954,668.27,502.54,4647149.58,3494663.16,1152486.42 Australia and Oceania,Federated States of Micronesia,Beverages,Online,C,10/28/2014,217221009,11/15/2014,9379,47.45,31.79,445033.55,298158.41,146875.14 Asia,Laos,Vegetables,Offline,C,9/15/2011,789176547,10/23/2011,3732,154.06,90.93,574951.92,339350.76,235601.16 Europe,Monaco,Baby Food,Offline,H,5/29/2012,688288152,6/2/2012,8614,255.28,159.42,2198981.92,1373243.88,825738.04 Australia and Oceania,Samoa ,Cosmetics,Online,H,7/20/2013,670854651,8/7/2013,9654,437.20,263.33,4220728.80,2542187.82,1678540.98 Europe,Spain,Household,Offline,L,10/21/2012,213487374,11/30/2012,4513,668.27,502.54,3015902.51,2267963.02,747939.49 Middle East and North Africa,Lebanon,Clothes,Online,L,9/18/2012,663110148,10/8/2012,7884,109.28,35.84,861563.52,282562.56,579000.96 Middle East and North Africa,Iran,Cosmetics,Online,H,11/15/2016,286959302,12/8/2016,6489,437.20,263.33,2836990.80,1708748.37,1128242.43 Sub-Saharan Africa,Zambia,Snacks,Online,L,1/4/2011,122583663,1/5/2011,4085,152.58,97.44,623289.30,398042.40,225246.90 Sub-Saharan Africa,Kenya,Vegetables,Online,L,3/18/2012,827844560,4/7/2012,6457,154.06,90.93,994765.42,587135.01,407630.41 North America,Mexico,Personal Care,Offline,L,2/17/2012,430915820,3/20/2012,6422,81.73,56.67,524870.06,363934.74,160935.32 Sub-Saharan Africa,Sao Tome and Principe,Beverages,Offline,C,1/16/2011,180283772,1/21/2011,8829,47.45,31.79,418936.05,280673.91,138262.14 Sub-Saharan Africa,The Gambia,Baby Food,Offline,M,2/3/2014,494747245,3/20/2014,5559,255.28,159.42,1419101.52,886215.78,532885.74 Middle East and North Africa,Kuwait,Fruits,Online,M,4/30/2012,513417565,5/18/2012,522,9.33,6.92,4870.26,3612.24,1258.02 Europe,Slovenia,Beverages,Offline,C,10/23/2016,345718562,11/25/2016,4660,47.45,31.79,221117.00,148141.40,72975.60 Sub-Saharan Africa,Sierra Leone,Office Supplies,Offline,H,12/6/2016,621386563,12/14/2016,948,651.21,524.96,617347.08,497662.08,119685.00 Australia and Oceania,Australia,Beverages,Offline,H,7/7/2014,240470397,7/11/2014,9389,47.45,31.79,445508.05,298476.31,147031.74 Middle East and North Africa,Azerbaijan,Office Supplies,Online,M,6/13/2012,423331391,7/24/2012,2021,651.21,524.96,1316095.41,1060944.16,255151.25 Europe,Romania,Cosmetics,Online,H,11/26/2010,660643374,12/25/2010,7910,437.20,263.33,3458252.00,2082940.30,1375311.70 Central America and the Caribbean,Nicaragua,Beverages,Offline,C,2/8/2011,963392674,3/21/2011,8156,47.45,31.79,387002.20,259279.24,127722.96 Sub-Saharan Africa,Mali,Clothes,Online,M,7/26/2011,512878119,9/3/2011,888,109.28,35.84,97040.64,31825.92,65214.72 Asia,Malaysia,Fruits,Offline,L,11/11/2011,810711038,12/28/2011,6267,9.33,6.92,58471.11,43367.64,15103.47 Sub-Saharan Africa,Sierra Leone,Vegetables,Offline,C,6/1/2016,728815257,6/29/2016,1485,154.06,90.93,228779.10,135031.05,93748.05 North America,Mexico,Personal Care,Offline,M,7/30/2015,559427106,8/8/2015,5767,81.73,56.67,471336.91,326815.89,144521.02 Sub-Saharan Africa,Mozambique,Household,Offline,L,2/10/2012,665095412,2/15/2012,5367,668.27,502.54,3586605.09,2697132.18,889472.91 ================================================ FILE: common/workflow-operator/src/test/resources/country_sales_small_multi_line.csv ================================================ Region,Country,Item Type,Sales Channel,Order Priority,Order Date,Order ID,Ship Date,Units Sold,Unit Price,Unit Cost,Total Revenue,Total Cost,Total Profit Australia and Oceania,Tuvalu,Baby Food,Offline,H,5/28/2010,669165933,6/27/2010,9925,255.28,159.42,2533654.00,1582243.50,951410.50 Central America and the Caribbean,Grenada,Cereal,Online,C,8/22/2012,963881480,9/15/2012,2804,205.70,117.11,576782.80,328376.44,248406.36 Europe,Russia,Office Supplies,Offline,L,5/2/2014,341417157,5/8/2014,1779,651.21,524.96,1158502.59,933903.84,224598.75 Sub-Saharan Africa,Sao Tome and Principe,Fruits,Online,C,6/20/2014,514321792,7/5/2014,8102,9.33,6.92,75591.66,56065.84,19525.82 Sub-Saharan Africa,Rwanda,Office Supplies,Offline,L,2/1/2013,115456712,2/6/2013,5062,651.21,524.96,3296425.02,2657347.52,639077.50 Australia and Oceania,Solomon Islands,Baby Food,Online,C,2/4/2015,547995746,2/21/2015,2974,255.28,159.42,759202.72,474115.08,285087.64 Sub-Saharan Africa,Angola,Household,Offline,M,4/23/2011,135425221,4/27/2011,4187,668.27,502.54,2798046.49,2104134.98,693911.51 Sub-Saharan Africa,Burkina Faso,Vegetables,Online,H,7/17/2012,871543967,7/27/2012,8082,154.06,90.93,1245112.92,734896.26,510216.66 Sub-Saharan Africa,"Republic,of,the,Congo",Personal Care,Offline,M,7/14/2015 ,770463311,8/25/2015 ,6070,81.73 ,56.67 ,496101.10 ,343986.90 ,152114.20 Sub-Saharan Africa,Senegal,Cereal,Online,H,4/18/2014,616607081,5/30/2014,6593,205.70,117.11,1356180.10,772106.23,584073.87 Asia,Kyrgyzstan,Vegetables,Online,H,6/24/2011,814711606,7/12/2011,124,154.06,90.93,19103.44,11275.32,7828.12 Sub-Saharan Africa,"Cape Verde",Clothes,Offline,H,8/2/2014 ,939825713,8/19/2014 ,4168,109.28,35.84 ,455479.04 ,149381.12 ,306097.92 Asia,Bangladesh,Clothes,Online,L,1/13/2017,187310731,3/1/2017,8263,109.28,35.84,902980.64,296145.92,606834.72 Central America and the Caribbean,Honduras,Household,Offline,H,2/8/2017,522840487,2/13/2017,8974,668.27,502.54,5997054.98,4509793.96,1487261.02 Asia,Mongolia,Personal Care,Offline,C,2/19/2014,832401311,2/23/2014,4901,81.73,56.67,400558.73,277739.67,122819.06 Europe,Bulgaria,Clothes,Online,M,4/23/2012,972292029,6/3/2012,1673,109.28,35.84,182825.44,59960.32,122865.12 Asia,Sri Lanka,Cosmetics,Offline,M,11/19/2016,419123971,12/18/2016,6952,437.20,263.33,3039414.40,1830670.16,1208744.24 Sub-Saharan Africa,Cameroon,Beverages,Offline,C,4/1/2015,519820964,4/18/2015,5430,47.45,31.79,257653.50,172619.70,85033.80 Asia,Turkmenistan,Household,Offline,L,12/30/2010,441619336,1/20/2011,3830,668.27,502.54,2559474.10,1924728.20,634745.90 Australia and Oceania,East Timor,Meat,Online,L,7/31/2012,322067916,9/11/2012,5908,421.89,364.69,2492526.12,2154588.52,337937.60 Europe,Norway,Baby Food,Online,L,5/14/2014,819028031,6/28/2014,7450,255.28,159.42,1901836.00,1187679.00,714157.00 Europe,Portugal,Baby Food,Online,H,7/31/2015,860673511,9/3/2015,1273,255.28,159.42,324971.44,202941.66,122029.78 Central America and the Caribbean,Honduras,Snacks,Online,L,6/30/2016,795490682,7/26/2016,2225,152.58,97.44,339490.50,216804.00,122686.50 Australia and Oceania,New Zealand,Fruits,Online,H,9/8/2014,142278373,10/4/2014,2187,9.33,6.92,20404.71,15134.04,5270.67 Europe,Moldova ,Personal Care,Online,L,5/7/2016,740147912,5/10/2016,5070,81.73,56.67,414371.10,287316.90,127054.20 Europe,France,Cosmetics,Online,H,5/22/2017,898523128,6/5/2017,1815,437.20,263.33,793518.00,477943.95,315574.05 Australia and Oceania,Kiribati,Fruits,Online,M,10/13/2014,347140347,11/10/2014,5398,9.33,6.92,50363.34,37354.16,13009.18 Sub-Saharan Africa,Mali,Fruits,Online,L,5/7/2010,686048400,5/10/2010,5822,9.33,6.92,54319.26,40288.24,14031.02 Europe,Norway,Beverages,Offline,C,7/18/2014,435608613,7/30/2014,5124,47.45,31.79,243133.80,162891.96,80241.84 Sub-Saharan Africa,The Gambia,Household,Offline,L,5/26/2012,886494815,6/9/2012,2370,668.27,502.54,1583799.90,1191019.80,392780.10 Europe,Switzerland,Cosmetics,Offline,M,9/17/2012,249693334,10/20/2012,8661,437.20,263.33,3786589.20,2280701.13,1505888.07 Sub-Saharan Africa,South Sudan,Personal Care,Offline,C,12/29/2013,406502997,1/28/2014,2125,81.73,56.67,173676.25,120423.75,53252.50 Australia and Oceania,Australia,Office Supplies,Online,C,10/27/2015,158535134,11/25/2015,2924,651.21,524.96,1904138.04,1534983.04,369155.00 Asia,Myanmar,Household,Offline,H,1/16/2015,177713572,3/1/2015,8250,668.27,502.54,5513227.50,4145955.00,1367272.50 Sub-Saharan Africa,Djibouti,Snacks,Online,M,2/25/2017,756274640,2/25/2017,7327,152.58,97.44,1117953.66,713942.88,404010.78 Central America and the Caribbean,Costa Rica,Personal Care,Offline,L,5/8/2017,456767165,5/21/2017,6409,81.73,56.67,523807.57,363198.03,160609.54 Middle East and North Africa,Syria,Fruits,Online,L,11/22/2011,162052476,12/3/2011,3784,9.33,6.92,35304.72,26185.28,9119.44 Sub-Saharan Africa,The Gambia,Meat,Online,M,1/14/2017,825304400,1/23/2017,4767,421.89,364.69,2011149.63,1738477.23,272672.40 Asia,Brunei,Office Supplies,Online,L,4/1/2012,320009267,5/8/2012,6708,651.21,524.96,4368316.68,3521431.68,846885.00 Europe,Bulgaria,Office Supplies,Online,M,2/16/2012,189965903,2/28/2012,3987,651.21,524.96,2596374.27,2093015.52,503358.75 Sub-Saharan Africa,Niger,Personal Care,Online,H,3/11/2017,699285638,3/28/2017,3015,81.73,56.67,246415.95,170860.05,75555.90 Middle East and North Africa,Azerbaijan,Cosmetics,Online,M,2/6/2010,382392299,2/25/2010,7234,437.20,263.33,3162704.80,1904929.22,1257775.58 Sub-Saharan Africa,The Gambia,Cereal,Offline,H,6/7/2012,994022214,6/8/2012,2117,205.70,117.11,435466.90,247921.87,187545.03 Europe,Slovakia,Vegetables,Online,H,10/6/2012,759224212,11/10/2012,171,154.06,90.93,26344.26,15549.03,10795.23 Asia,Myanmar,Clothes,Online,H,11/14/2015,223359620,11/18/2015,5930,109.28,35.84,648030.40,212531.20,435499.20 Sub-Saharan Africa,Comoros,Cereal,Offline,H,3/29/2016,902102267,4/29/2016,962,205.70,117.11,197883.40,112659.82,85223.58 Europe,Iceland,Cosmetics,Online,C,12/31/2016,331438481,12/31/2016,8867,437.20,263.33,3876652.40,2334947.11,1541705.29 Europe,Switzerland,Personal Care,Online,M,12/23/2010,617667090,1/31/2011,273,81.73,56.67,22312.29,15470.91,6841.38 Europe,Macedonia,Clothes,Offline,C,10/14/2014,787399423,11/14/2014,7842,109.28,35.84,856973.76,281057.28,575916.48 Sub-Saharan Africa,Mauritania,Office Supplies,Offline,C,1/11/2012,837559306,1/13/2012,1266,651.21,524.96,824431.86,664599.36,159832.50 Europe,Albania,Clothes,Online,C,2/2/2010,385383069,3/18/2010,2269,109.28,35.84,247956.32,81320.96,166635.36 Sub-Saharan Africa,Lesotho,Fruits,Online,L,8/18/2013,918419539,9/18/2013,9606,9.33,6.92,89623.98,66473.52,23150.46 Middle East and North Africa,Saudi Arabia,Cereal,Online,M,3/25/2013,844530045,3/28/2013,4063,205.70,117.11,835759.10,475817.93,359941.17 Sub-Saharan Africa,Sierra Leone,"Office Supplies",Offline,M,11/26/2011,441888415,1/7/2012 ,3457,651.21,524.96,2251232.97,1814786.72,436446.25 Sub-Saharan Africa,Sao Tome and Principe,Fruits,Offline,H,9/17/2013,508980977,10/24/2013,7637,9.33,6.92,71253.21,52848.04,18405.17 Sub-Saharan Africa,Cote d'Ivoire,Clothes,Online,C,6/8/2012,114606559,6/27/2012,3482,109.28,35.84,380512.96,124794.88,255718.08 Australia and Oceania,Fiji,Clothes,Offline,C,6/30/2010,647876489,8/1/2010,9905,109.28,35.84,1082418.40,354995.20,727423.20 Europe,Austria,Cosmetics,Offline,H,2/23/2015,868214595,3/2/2015,2847,437.20,263.33,1244708.40,749700.51,495007.89 Europe,United Kingdom,Household,Online,L,1/5/2012,955357205,2/14/2012,282,668.27,502.54,188452.14,141716.28,46735.86 Sub-Saharan Africa,Djibouti,Cosmetics,Offline,H,4/7/2014,259353148,4/19/2014,7215,437.20,263.33,3154398.00,1899925.95,1254472.05 Australia and Oceania,Australia,Cereal,Offline,H,6/9/2013,450563752,7/2/2013,682,205.70,117.11,140287.40,79869.02,60418.38 Europe,San Marino,Baby Food,Online,L,6/26/2013,569662845,7/1/2013,4750,255.28,159.42,1212580.00,757245.00,455335.00 Sub-Saharan Africa,Cameroon,Office Supplies,Online,M,11/7/2011,177636754,11/15/2011,5518,651.21,524.96,3593376.78,2896729.28,696647.50 Middle East and North Africa,Libya,Clothes,Offline,H,10/30/2010,705784308,11/17/2010,6116,109.28,35.84,668356.48,219197.44,449159.04 Central America and the Caribbean,Haiti,Cosmetics,Offline,H,10/13/2013,505716836,11/16/2013,1705,437.20,263.33,745426.00,448977.65,296448.35 Sub-Saharan Africa,Rwanda,Cosmetics,Offline,H,10/11/2013,699358165,11/25/2013,4477,437.20,263.33,1957344.40,1178928.41,778415.99 Sub-Saharan Africa,Gabon,Personal Care,Offline,L,7/8/2012,228944623,7/9/2012,8656,81.73,56.67,707454.88,490535.52,216919.36 Central America and the Caribbean,Belize,Clothes,Offline,M,7/25/2016,807025039,9/7/2016,5498,109.28,35.84,600821.44,197048.32,403773.12 Europe,Lithuania,Office Supplies,Offline,H,10/24/2010,166460740,11/17/2010,8287,651.21,524.96,5396577.27,4350343.52,1046233.75 Sub-Saharan Africa,Madagascar,Clothes,Offline,L,4/25/2015,610425555,5/28/2015,7342,109.28,35.84,802333.76,263137.28,539196.48 Asia,Turkmenistan,Office Supplies,Online,M,4/23/2013,462405812,5/20/2013,5010,651.21,524.96,3262562.10,2630049.60,632512.50 Middle East and North Africa,Libya,Fruits,Online,L,8/14/2015,816200339,9/30/2015,673,9.33,6.92,6279.09,4657.16,1621.93 Sub-Saharan Africa,Democratic Republic of the Congo,Beverages,Online,C,5/26/2011,585920464,7/15/2011,5741,47.45,31.79,272410.45,182506.39,89904.06 Sub-Saharan Africa,Djibouti,Cereal,Online,H,5/20/2017,555990016,6/17/2017,8656,205.70,117.11,1780539.20,1013704.16,766835.04 Middle East and North Africa,Pakistan,Cosmetics,Offline,L,7/5/2013,231145322,8/16/2013,9892,437.20,263.33,4324782.40,2604860.36,1719922.04 North America,Mexico,Household,Offline,C,11/6/2014,986435210,12/12/2014,6954,668.27,502.54,4647149.58,3494663.16,1152486.42 Australia and Oceania,Federated States of Micronesia,Beverages,Online,C,10/28/2014,217221009,11/15/2014,9379,47.45,31.79,445033.55,298158.41,146875.14 Asia,Laos,Vegetables,Offline,C,9/15/2011,789176547,10/23/2011,3732,154.06,90.93,574951.92,339350.76,235601.16 Europe,Monaco,Baby Food,Offline,H,5/29/2012,688288152,6/2/2012,8614,255.28,159.42,2198981.92,1373243.88,825738.04 Australia and Oceania,Samoa ,Cosmetics,Online,H,7/20/2013,670854651,8/7/2013,9654,437.20,263.33,4220728.80,2542187.82,1678540.98 Europe,Spain,Household,Offline,L,10/21/2012,213487374,11/30/2012,4513,668.27,502.54,3015902.51,2267963.02,747939.49 Middle East and North Africa,Lebanon,Clothes,Online,L,9/18/2012,663110148,10/8/2012,7884,109.28,35.84,861563.52,282562.56,579000.96 Middle East and North Africa,Iran,Cosmetics,Online,H,11/15/2016,286959302,12/8/2016,6489,437.20,263.33,2836990.80,1708748.37,1128242.43 Sub-Saharan Africa,Zambia,Snacks,Online,L,1/4/2011,122583663,1/5/2011,4085,152.58,97.44,623289.30,398042.40,225246.90 Sub-Saharan Africa,Kenya,Vegetables,Online,L,3/18/2012,827844560,4/7/2012,6457,154.06,90.93,994765.42,587135.01,407630.41 North America,Mexico,Personal Care,Offline,L,2/17/2012,430915820,3/20/2012,6422,81.73,56.67,524870.06,363934.74,160935.32 Sub-Saharan Africa,Sao Tome and Principe,Beverages,Offline,C,1/16/2011,180283772,1/21/2011,8829,47.45,31.79,418936.05,280673.91,138262.14 Sub-Saharan Africa,The Gambia,Baby Food,Offline,M,2/3/2014,494747245,3/20/2014,5559,255.28,159.42,1419101.52,886215.78,532885.74 Middle East and North Africa,Kuwait,Fruits,Online,M,4/30/2012,513417565,5/18/2012,522,9.33,6.92,4870.26,3612.24,1258.02 Europe,Slovenia,Beverages,Offline,C,10/23/2016,345718562,11/25/2016,4660,47.45,31.79,221117.00,148141.40,72975.60 Sub-Saharan Africa,Sierra Leone,Office Supplies,Offline,H,12/6/2016,621386563,12/14/2016,948,651.21,524.96,617347.08,497662.08,119685.00 Australia and Oceania,Australia,Beverages,Offline,H,7/7/2014,240470397,7/11/2014,9389,47.45,31.79,445508.05,298476.31,147031.74 Middle East and North Africa,Azerbaijan,Office Supplies,Online,M,6/13/2012,423331391,7/24/2012,2021,651.21,524.96,1316095.41,1060944.16,255151.25 Europe,Romania,Cosmetics,Online,H,11/26/2010,660643374,12/25/2010,7910,437.20,263.33,3458252.00,2082940.30,1375311.70 Central America and the Caribbean,Nicaragua,Beverages,Offline,C,2/8/2011,963392674,3/21/2011,8156,47.45,31.79,387002.20,259279.24,127722.96 Sub-Saharan Africa,Mali,Clothes,Online,M,7/26/2011,512878119,9/3/2011,888,109.28,35.84,97040.64,31825.92,65214.72 Asia,Malaysia,Fruits,Offline,L,11/11/2011,810711038,12/28/2011,6267,9.33,6.92,58471.11,43367.64,15103.47 Sub-Saharan Africa,Sierra Leone,Vegetables,Offline,C,6/1/2016,728815257,6/29/2016,1485,154.06,90.93,228779.10,135031.05,93748.05 North America,Mexico,"Personal Care",Offline,M,7/30/2015 ,559427106,8/8/2015,5767,81.73 ,56.67 ,471336.91 ,326815.89 ,144521.02 Sub-Saharan Africa,Mozambique,Household,Offline,L,2/10/2012,665095412,2/15/2012,5367,668.27,502.54,3586605.09,2697132.18,889472.91 ================================================ FILE: common/workflow-operator/src/test/resources/line_numbers.txt ================================================ line1 line2 line3 line4 line5 line6 line7 line8 line9 line10 ================================================ FILE: common/workflow-operator/src/test/resources/line_numbers_crlf.txt ================================================ line1 line2 line3 line4 line5 line6 line7 line8 line9 line10 ================================================ FILE: common/workflow-operator/src/test/resources/numbers.txt ================================================ 1 2 3 4 5 6 7 8 9 10 ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/aggregate/AggregateOpSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.aggregate import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple} import org.apache.texera.amber.util.JSONUtils.objectMapper import org.scalatest.funsuite.AnyFunSuite class AggregateOpSpec extends AnyFunSuite { /** Helpers */ private def makeAggregationOp( fn: AggregationFunction, attributeName: String, resultName: String ): AggregationOperation = { val operation = new AggregationOperation() operation.aggFunction = fn operation.attribute = attributeName operation.resultAttribute = resultName operation } private def makeSchema(fields: (String, AttributeType)*): Schema = Schema(fields.map { case (n, t) => new Attribute(n, t) }.toList) private def makeTuple(schema: Schema, values: Any*): Tuple = Tuple(schema, values.toArray) test("getAggregationAttribute keeps original type for SUM") { val operation = makeAggregationOp(AggregationFunction.SUM, "amount", "total_amount") val attr = operation.getAggregationAttribute(AttributeType.DOUBLE) assert(attr.getName == "total_amount") assert(attr.getType == AttributeType.DOUBLE) } test("getAggregationAttribute maps COUNT result to INTEGER regardless of input type") { val operation = makeAggregationOp(AggregationFunction.COUNT, "quantity", "row_count") val attr = operation.getAggregationAttribute(AttributeType.LONG) assert(attr.getName == "row_count") assert(attr.getType == AttributeType.INTEGER) } test("getAggregationAttribute maps CONCAT result type to STRING") { val operation = makeAggregationOp(AggregationFunction.CONCAT, "tag", "all_tags") val attr = operation.getAggregationAttribute(AttributeType.INTEGER) assert(attr.getName == "all_tags") assert(attr.getType == AttributeType.STRING) } test("getAggregationAttribute maps AVERAGE result type to DOUBLE regardless of input") { val operation = makeAggregationOp(AggregationFunction.AVERAGE, "price", "avg_price") val attr = operation.getAggregationAttribute(AttributeType.LONG) assert(attr.getName == "avg_price") assert(attr.getType == AttributeType.DOUBLE) } test("getAggregationAttribute keeps original type for MIN") { val operation = makeAggregationOp(AggregationFunction.MIN, "ts", "min_ts") val attr = operation.getAggregationAttribute(AttributeType.TIMESTAMP) assert(attr.getName == "min_ts") assert(attr.getType == AttributeType.TIMESTAMP) } test("getAggregationAttribute keeps original type for MAX") { val operation = makeAggregationOp(AggregationFunction.MAX, "score", "max_score") val attr = operation.getAggregationAttribute(AttributeType.DOUBLE) assert(attr.getName == "max_score") assert(attr.getType == AttributeType.DOUBLE) } test("getAggregationAttribute throws RuntimeException when aggFunction is null") { val operation = new AggregationOperation() operation.attribute = "src" operation.resultAttribute = "out" // aggFunction left null on purpose assertThrows[RuntimeException] { operation.getAggregationAttribute(AttributeType.INTEGER) } } // --------------------------------------------------------------------------- // Basic DistributedAggregation behaviour via AggregationOperation.getAggFunc // --------------------------------------------------------------------------- test("SUM aggregation over INTEGER column adds values correctly") { val schema = makeSchema("amount" -> AttributeType.INTEGER) val tuple1 = makeTuple(schema, 5) val tuple2 = makeTuple(schema, 7) val tuple3 = makeTuple(schema, 3) val operation = makeAggregationOp(AggregationFunction.SUM, "amount", "total_amount") val agg = operation.getAggFunc(AttributeType.INTEGER) var partial = agg.init() partial = agg.iterate(partial, tuple1) partial = agg.iterate(partial, tuple2) partial = agg.iterate(partial, tuple3) val result = agg.finalAgg(partial).asInstanceOf[Number].intValue() assert(result == 15) } test("SUM aggregation over DOUBLE column keeps fractional part") { val schema = makeSchema("score" -> AttributeType.DOUBLE) val tuple1 = makeTuple(schema, 1.25) val tuple2 = makeTuple(schema, 2.75) val operation = makeAggregationOp(AggregationFunction.SUM, "score", "total_score") val agg = operation.getAggFunc(AttributeType.DOUBLE) var partial = agg.init() partial = agg.iterate(partial, tuple1) partial = agg.iterate(partial, tuple2) val result = agg.finalAgg(partial).asInstanceOf[java.lang.Double].doubleValue() assert(math.abs(result - 4.0) < 1e-6) } test("COUNT aggregation with attribute == null counts all rows") { val schema = makeSchema("points" -> AttributeType.INTEGER) val tuple1 = makeTuple(schema, 10) val tuple2 = makeTuple(schema, null) val tuple3 = makeTuple(schema, 20) val operation = makeAggregationOp(AggregationFunction.COUNT, null, "row_count") val agg = operation.getAggFunc(AttributeType.INTEGER) var partial = agg.init() partial = agg.iterate(partial, tuple1) partial = agg.iterate(partial, tuple2) partial = agg.iterate(partial, tuple3) val result = agg.finalAgg(partial).asInstanceOf[Number].intValue() assert(result == 3) } test("COUNT aggregation with attribute set only counts non-null values") { val schema = makeSchema("points" -> AttributeType.INTEGER) val tuple1 = makeTuple(schema, 10) val tuple2 = makeTuple(schema, null) val tuple3 = makeTuple(schema, 5) val operation = makeAggregationOp(AggregationFunction.COUNT, "points", "non_null_points") val agg = operation.getAggFunc(AttributeType.INTEGER) var partial = agg.init() partial = agg.iterate(partial, tuple1) partial = agg.iterate(partial, tuple2) partial = agg.iterate(partial, tuple3) val result = agg.finalAgg(partial).asInstanceOf[Number].intValue() assert(result == 2) } test("CONCAT aggregation concatenates string representations with commas") { val schema = makeSchema("tag" -> AttributeType.STRING) val tuple1 = makeTuple(schema, "red") val tuple2 = makeTuple(schema, null) val tuple3 = makeTuple(schema, "blue") val operation = makeAggregationOp(AggregationFunction.CONCAT, "tag", "all_tags") val agg = operation.getAggFunc(AttributeType.STRING) var partial = agg.init() partial = agg.iterate(partial, tuple1) partial = agg.iterate(partial, tuple2) partial = agg.iterate(partial, tuple3) val result = agg.finalAgg(partial).asInstanceOf[String] assert(result == "red,,blue") } test("MIN aggregation finds smallest INTEGER and returns null when given no values") { val schema = makeSchema("temperature" -> AttributeType.INTEGER) val tuple1 = makeTuple(schema, 10) val tuple2 = makeTuple(schema, -2) val tuple3 = makeTuple(schema, 5) val operation = makeAggregationOp(AggregationFunction.MIN, "temperature", "min_temp") val agg = operation.getAggFunc(AttributeType.INTEGER) // Empty case: never iterate, just finalize init val emptyPartial = agg.init() val emptyResult = agg.finalAgg(emptyPartial) assert(emptyResult == null) // Non-empty case var partial = agg.init() partial = agg.iterate(partial, tuple1) partial = agg.iterate(partial, tuple2) partial = agg.iterate(partial, tuple3) val result = agg.finalAgg(partial).asInstanceOf[Number].intValue() assert(result == -2) } test("MAX aggregation finds largest LONG value") { val schema = makeSchema("latency" -> AttributeType.LONG) val tuple1 = makeTuple(schema, 100L) val tuple2 = makeTuple(schema, 50L) val tuple3 = makeTuple(schema, 250L) val operation = makeAggregationOp(AggregationFunction.MAX, "latency", "max_latency") val agg = operation.getAggFunc(AttributeType.LONG) var partial = agg.init() partial = agg.iterate(partial, tuple1) partial = agg.iterate(partial, tuple2) partial = agg.iterate(partial, tuple3) val result = agg.finalAgg(partial).asInstanceOf[java.lang.Long].longValue() assert(result == 250L) } test( "MIN aggregation (DOUBLE) is solid: empty/null/NaN, infinities, signed zero, and many values" ) { val schema = makeSchema("temperature" -> AttributeType.DOUBLE) val operation = makeAggregationOp(AggregationFunction.MIN, "temperature", "min_temp") val agg = operation.getAggFunc(AttributeType.DOUBLE) // ----------------------- // 0) Empty input => null // ----------------------- val emptyPartial = agg.init() val emptyResult = agg.finalAgg(emptyPartial) assert(emptyResult == null) // --------------------------------------------------------- // 1) Only nulls / only NaNs => null // --------------------------------------------------------- val onlyNulls = Seq(makeTuple(schema, null), makeTuple(schema, null), makeTuple(schema, null)) var partialNulls = agg.init() onlyNulls.foreach(tp => partialNulls = agg.iterate(partialNulls, tp)) assert(agg.finalAgg(partialNulls) == null) val onlyNaNs = Seq(makeTuple(schema, Double.NaN), makeTuple(schema, Double.NaN)) var partialNans = agg.init() onlyNaNs.foreach(tp => partialNans = agg.iterate(partialNans, tp)) assert(agg.finalAgg(partialNans) == null) // --------------------------------------------------------- // 2) Basic decimals + negatives: should find the true minimum // --------------------------------------------------------- val basics = Seq( makeTuple(schema, 10.25), makeTuple(schema, -2.5), makeTuple(schema, 5.0), makeTuple(schema, -2.5000000001), // slightly smaller than -2.5 makeTuple(schema, 1.0e-12) ) var partial = agg.init() basics.foreach(tp => partial = agg.iterate(partial, tp)) val basicResult = agg.finalAgg(partial).asInstanceOf[Number].doubleValue() assert(basicResult == -2.5000000001) // --------------------------------------------------------- // 3) NaN + null interleaving must not poison the result // (especially if NaN appears first) // --------------------------------------------------------- val mixed = Seq( makeTuple(schema, Double.NaN), makeTuple(schema, null), makeTuple(schema, 3.14159), makeTuple(schema, -0.125), makeTuple(schema, Double.NaN), makeTuple(schema, -9999.0), makeTuple(schema, null) ) var partialMixed = agg.init() mixed.foreach(tp => partialMixed = agg.iterate(partialMixed, tp)) val mixedResult = agg.finalAgg(partialMixed).asInstanceOf[Number].doubleValue() assert(mixedResult == -9999.0) // --------------------------------------------------------- // 4) Infinities: min should be -Infinity if present // --------------------------------------------------------- val infinities = Seq( makeTuple(schema, Double.PositiveInfinity), makeTuple(schema, 42.0), makeTuple(schema, Double.NegativeInfinity), makeTuple(schema, -1.0) ) var partialInf = agg.init() infinities.foreach(tp => partialInf = agg.iterate(partialInf, tp)) val infResult = agg.finalAgg(partialInf).asInstanceOf[Number].doubleValue() assert(infResult.isNegInfinity) // --------------------------------------------------------- // 5) Signed zero: MIN(-0.0, +0.0) should be -0.0 // --------------------------------------------------------- val signedZero = Seq(makeTuple(schema, 0.0), makeTuple(schema, -0.0), makeTuple(schema, 0.0)) var partialZero = agg.init() signedZero.foreach(tp => partialZero = agg.iterate(partialZero, tp)) val zeroValue = agg.finalAgg(partialZero).asInstanceOf[Number].doubleValue() val zeroBits = java.lang.Double.doubleToRawLongBits(zeroValue) val negZeroBits = java.lang.Double.doubleToRawLongBits(-0.0) assert(zeroBits == negZeroBits) // --------------------------------------------------------- // 6) Stress test: many values, compare against a reference min // Reference rule here: ignore null and NaN; return null if none left. // --------------------------------------------------------- val rng = new scala.util.Random(1337) val values: Seq[java.lang.Double] = (1 to 10000).map { index => index % 250 match { case 0 => null case 1 => Double.NaN case 2 => Double.PositiveInfinity case 3 => Double.NegativeInfinity case 4 => -0.0 case _ => // wide-ish range with some tiny magnitudes too val sign = if (rng.nextBoolean()) 1.0 else -1.0 sign * (rng.nextDouble() * 1.0e6) / (if (rng.nextInt(20) == 0) 1.0e12 else 1.0) } } val expected: java.lang.Double = { var found = false var currentMin = 0.0 values.foreach { x => if (x != null && !java.lang.Double.isNaN(x)) { if (!found) { currentMin = x; found = true } else if (java.lang.Double.compare(x, currentMin) < 0) currentMin = x } } if (!found) null else currentMin } var partStress = agg.init() values.foreach(v => partStress = agg.iterate(partStress, makeTuple(schema, v))) val gotAny = agg.finalAgg(partStress) if (expected == null) { assert(gotAny == null) } else { val got = gotAny.asInstanceOf[Number].doubleValue() // exact match: should be one of the seen inputs, no tolerance needed if ( expected == 0.0 && java.lang.Double.doubleToRawLongBits(expected) != java.lang.Double .doubleToRawLongBits(got) ) { // If expected is -0.0, enforce it assert( java.lang.Double.doubleToRawLongBits(got) == java.lang.Double.doubleToRawLongBits( expected ) ) } else { assert(got == expected) } } } test("MAX aggregation finds largest DOUBLE value") { val maxValue = 99.144 val schema = makeSchema("debt" -> AttributeType.DOUBLE) val tuple1 = makeTuple(schema, -100.123) val tuple2 = makeTuple(schema, 50.12) val tuple3 = makeTuple(schema, maxValue) val operation = makeAggregationOp(AggregationFunction.MAX, "debt", "nax_debt") val agg = operation.getAggFunc(AttributeType.DOUBLE) var partial = agg.init() partial = agg.iterate(partial, tuple1) partial = agg.iterate(partial, tuple2) partial = agg.iterate(partial, tuple3) val result = agg.finalAgg(partial).asInstanceOf[java.lang.Double].doubleValue() assert(result == maxValue) } test("AVERAGE aggregation ignores nulls and returns null when all values are null") { val schema = makeSchema("price" -> AttributeType.DOUBLE) val tuple1 = makeTuple(schema, 10.0) val tuple2 = makeTuple(schema, null) val tuple3 = makeTuple(schema, 20.0) val operation = makeAggregationOp(AggregationFunction.AVERAGE, "price", "avg_price") val agg = operation.getAggFunc(AttributeType.DOUBLE) // Mixed null and non-null var partial = agg.init() partial = agg.iterate(partial, tuple1) partial = agg.iterate(partial, tuple2) partial = agg.iterate(partial, tuple3) val avg = agg.finalAgg(partial).asInstanceOf[java.lang.Double].doubleValue() assert(math.abs(avg - 15.0) < 1e-6) // All nulls val allNull = makeTuple(schema, null) var partialAllNull = agg.init() partialAllNull = agg.iterate(partialAllNull, allNull) val allNullResult = agg.finalAgg(partialAllNull) assert(allNullResult == null) } // --------------------------------------------------------------------------- // getFinal behaviour // --------------------------------------------------------------------------- test("getFinal rewrites COUNT into SUM over the intermediate result attribute") { val operation = makeAggregationOp(AggregationFunction.COUNT, "price", "price_count") val finalOp = operation.getFinal assert(finalOp.aggFunction == AggregationFunction.SUM) assert(finalOp.attribute == "price_count") assert(finalOp.resultAttribute == "price_count") } test("getFinal keeps non-COUNT aggregation function and rewires attribute to resultAttribute") { val operation = makeAggregationOp(AggregationFunction.SUM, "amount", "total_amount") val finalOp = operation.getFinal assert(finalOp.aggFunction == AggregationFunction.SUM) assert(finalOp.attribute == "total_amount") assert(finalOp.resultAttribute == "total_amount") } // --------------------------------------------------------------------------- // AggregateOpExec: integration-style tests with groupBy // --------------------------------------------------------------------------- test("AggregateOpExec groups by a single key and computes SUM per group") { // schema: city (group key), sales val schema = makeSchema( "city" -> AttributeType.STRING, "sales" -> AttributeType.INTEGER ) val tuple1 = makeTuple(schema, "NY", 10) val tuple2 = makeTuple(schema, "SF", 20) val tuple3 = makeTuple(schema, "NY", 5) val desc = new AggregateOpDesc() val sumAgg = makeAggregationOp(AggregationFunction.SUM, "sales", "total_sales") desc.aggregations = List(sumAgg) desc.groupByKeys = List("city") val descJson = objectMapper.writeValueAsString(desc) val exec = new AggregateOpExec(descJson) exec.open() exec.processTuple(tuple1, 0) exec.processTuple(tuple2, 0) exec.processTuple(tuple3, 0) val results = exec.onFinish(0).toList // Expect two output rows: (NY, 15) and (SF, 20) val resultMap = results.map { tupleLike => val fields = tupleLike.getFields val city = fields(0).asInstanceOf[String] val total = fields(1).asInstanceOf[Number].intValue() city -> total }.toMap assert(resultMap.size == 2) assert(resultMap("NY") == 15) assert(resultMap("SF") == 20) } test("AggregateOpExec performs global SUM and COUNT when there are no groupBy keys") { // schema: region (ignored for aggregation), revenue val schema = makeSchema( "region" -> AttributeType.STRING, "revenue" -> AttributeType.INTEGER ) val tuple1 = makeTuple(schema, "west", 100) val tuple2 = makeTuple(schema, "east", 200) val tuple3 = makeTuple(schema, "west", 50) val desc = new AggregateOpDesc() val sumAgg = makeAggregationOp(AggregationFunction.SUM, "revenue", "total_revenue") val countAgg = makeAggregationOp(AggregationFunction.COUNT, "revenue", "row_count") desc.aggregations = List(sumAgg, countAgg) desc.groupByKeys = List() // global aggregation val descJson = objectMapper.writeValueAsString(desc) val exec = new AggregateOpExec(descJson) exec.open() exec.processTuple(tuple1, 0) exec.processTuple(tuple2, 0) exec.processTuple(tuple3, 0) val results = exec.onFinish(0).toList assert(results.size == 1) val fields = results.head.getFields // No group keys, so fields(0) is SUM(revenue), fields(1) is COUNT(revenue) val totalRevenue = fields(0).asInstanceOf[Number].intValue() val rowCount = fields(1).asInstanceOf[Number].intValue() assert(totalRevenue == 350) assert(rowCount == 3) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/aggregate/AggregationOperationSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.aggregate import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple} import org.scalatest.flatspec.AnyFlatSpec /** * Coverage notes: * `AggregateOpSpec` (in this same package) already exercises the happy paths for * `getAggregationAttribute`, the per-kind `init` / `iterate` / `finalAgg` semantics * (SUM/COUNT/AVERAGE/MIN/MAX/CONCAT, including null-handling and AVERAGE-of-empty), * and the `getFinal` rewrite. This spec deliberately does NOT duplicate those. * * What this spec adds: * - `getAggFunc` validation errors (unsupported attribute types on SUM/MIN/MAX, * null aggFunction). Note: SUM/MIN/MAX do accept TIMESTAMP alongside numerics. * - The CONCAT-specific `merge` partial-combination behavior — `AggregateOpSpec` * exercises iterate/finalAgg but never calls `merge` directly. * - A two-stage worker→final pipeline that runs a real partial aggregation * on each "worker", emits a partial tuple, then applies `getFinal` and * re-aggregates the partials end-to-end. * - `AveragePartialObj` (a plain `case class`, not a value class) field * exposure and case-class value equality / hashCode. */ class AggregationOperationSpec extends AnyFlatSpec { // --- helpers --------------------------------------------------------------- private def schemaWith(name: String, t: AttributeType): Schema = new Schema(new Attribute(name, t)) private def tupleOf(name: String, t: AttributeType, value: AnyRef): Tuple = Tuple.builder(schemaWith(name, t)).add(new Attribute(name, t), value).build() private def op( func: AggregationFunction, attribute: String = "v", resultAttribute: String = "r" ): AggregationOperation = { val o = new AggregationOperation() o.aggFunction = func o.attribute = attribute o.resultAttribute = resultAttribute o } // --- getAggFunc: type validation (not covered in AggregateOpSpec) ---------- "AggregationOperation.getAggFunc" should "throw UnsupportedOperationException for unsupported attribute types on SUM" in { // SUM accepts INTEGER/LONG/DOUBLE/TIMESTAMP; STRING is rejected. val ex = intercept[UnsupportedOperationException] { op(AggregationFunction.SUM).getAggFunc(AttributeType.STRING) } assert(ex.getMessage.contains("Unsupported attribute type for sum")) } it should "throw UnsupportedOperationException for unsupported attribute types on MIN and MAX" in { // MIN/MAX accept INTEGER/LONG/DOUBLE/TIMESTAMP; STRING and BOOLEAN are rejected. intercept[UnsupportedOperationException] { op(AggregationFunction.MIN).getAggFunc(AttributeType.STRING) } intercept[UnsupportedOperationException] { op(AggregationFunction.MAX).getAggFunc(AttributeType.BOOLEAN) } } it should "throw UnsupportedOperationException when aggFunction is null" in { val ex = intercept[UnsupportedOperationException] { op(null).getAggFunc(AttributeType.INTEGER) } assert(ex.getMessage.contains("Unknown aggregation function")) } // --- CONCAT partial merge (iterate is covered in AggregateOpSpec) ---------- "CONCAT aggregation merge" should "join two non-empty partials with a comma and short-circuit when either is empty" in { val agg = op(AggregationFunction.CONCAT).getAggFunc(AttributeType.STRING) assert(agg.merge("foo", "bar") == "foo,bar") assert(agg.merge("", "bar") == "bar") assert(agg.merge("foo", "") == "foo") assert(agg.merge("", "") == "") } // --- partial + final pipeline ---------------------------------------------- "Worker → final aggregation pipeline" should "give the same total as a single-pass COUNT when partials are re-aggregated via getFinal" in { // Two "workers" each run a COUNT over their slice of the data. Each // worker emits a partial output (an Integer count). The "final" stage // re-aggregates those partial outputs as a SUM over the result column, // which getFinal is supposed to produce. val workerOp = op(AggregationFunction.COUNT, attribute = "v", resultAttribute = "row_count") val workerAgg = workerOp.getAggFunc(AttributeType.INTEGER) val w1Tuples = Seq( tupleOf("v", AttributeType.INTEGER, Int.box(10)), tupleOf("v", AttributeType.INTEGER, null), tupleOf("v", AttributeType.INTEGER, Int.box(20)) ) val w1State = w1Tuples.foldLeft(workerAgg.init())(workerAgg.iterate) val w1Out = workerAgg.finalAgg(w1State).asInstanceOf[Integer] assert(w1Out == 2, "worker 1 saw two non-null values") val w2Tuples = Seq( tupleOf("v", AttributeType.INTEGER, Int.box(30)), tupleOf("v", AttributeType.INTEGER, Int.box(40)), tupleOf("v", AttributeType.INTEGER, Int.box(50)) ) val w2State = w2Tuples.foldLeft(workerAgg.init())(workerAgg.iterate) val w2Out = workerAgg.finalAgg(w2State).asInstanceOf[Integer] assert(w2Out == 3) // Final stage: re-aggregate the partial counts via getFinal. val finalOp = workerOp.getFinal assert(finalOp.aggFunction == AggregationFunction.SUM) assert(finalOp.attribute == "row_count") val finalAgg = finalOp.getAggFunc(AttributeType.INTEGER) val partial1 = tupleOf("row_count", AttributeType.INTEGER, w1Out) val partial2 = tupleOf("row_count", AttributeType.INTEGER, w2Out) val finalState = finalAgg.iterate(finalAgg.iterate(finalAgg.init(), partial1), partial2) val finalCount = finalAgg.finalAgg(finalState).asInstanceOf[Integer] assert(finalCount == 5, "summing partial counts must match a single-pass COUNT") } it should "give the same total as a single-pass SUM when partials are re-aggregated via getFinal" in { // For SUM, getFinal keeps aggFunction = SUM and rebinds attribute to the // result column. The pipeline must produce the same total as a single-pass // SUM over all the input tuples. val workerOp = op(AggregationFunction.SUM, attribute = "v", resultAttribute = "total") val workerAgg = workerOp.getAggFunc(AttributeType.INTEGER) val groups = Seq( Seq(Int.box(1), Int.box(2), Int.box(3)), Seq(Int.box(10), Int.box(20)) ) val partials: Seq[Integer] = groups.map { values => val state = values .map(v => tupleOf("v", AttributeType.INTEGER, v)) .foldLeft(workerAgg.init())(workerAgg.iterate) workerAgg.finalAgg(state).asInstanceOf[Integer] } assert(partials == Seq(6: Integer, 30: Integer)) val finalOp = workerOp.getFinal assert(finalOp.aggFunction == AggregationFunction.SUM) assert(finalOp.attribute == "total") val finalAgg = finalOp.getAggFunc(AttributeType.INTEGER) val finalState = partials .map(p => tupleOf("total", AttributeType.INTEGER, p)) .foldLeft(finalAgg.init())(finalAgg.iterate) val finalSum = finalAgg.finalAgg(finalState).asInstanceOf[Integer] assert(finalSum == 36, "single-pass SUM(1+2+3+10+20) == 36") } // --- AveragePartialObj ----------------------------------------------------- "AveragePartialObj" should "expose its sum and count fields and support value equality" in { val a = AveragePartialObj(10.0, 4) val b = AveragePartialObj(10.0, 4) assert(a.sum == 10.0) assert(a.count == 4) assert(a == b) assert(a.hashCode == b.hashCode) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/cartesianProduct/CartesianProductOpExecSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.cartesianProduct import org.apache.texera.amber.core.tuple._ import org.apache.texera.amber.core.workflow.PortIdentity import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class CartesianProductOpExecSpec extends AnyFlatSpec with BeforeAndAfter { val leftPort: Int = 0 val rightPort: Int = 1 var opDesc: CartesianProductOpDesc = _ var opExec: CartesianProductOpExec = _ def generate_tuple(schema: Schema, value: Option[Int]): Tuple = { Tuple .builder(schema) .addSequentially( (1 to schema.getAttributes.length).map(_ => value.map(_.toString).orNull).toArray ) .build() } def generate_schema( base_name: String, num_attributes: Int = 1, append_num: Boolean = true ): Schema = { val attrs: Iterable[Attribute] = Range .inclusive(1, num_attributes) .map(num => new Attribute(base_name + (if (append_num) "#@" + num else ""), AttributeType.STRING) ) Schema().add(attrs) } before { opDesc = new CartesianProductOpDesc() } it should "work with basic two input streams with no duplicate attribute names" in { val numLeftSchemaAttributes: Int = 3 val numRightSchemaAttributes: Int = 3 val numLeftTuples: Int = 5 val numRightTuples: Int = 5 val leftSchema = generate_schema("left", numLeftSchemaAttributes) val rightSchema = generate_schema("right", numRightSchemaAttributes) opExec = new CartesianProductOpExec() opExec.open() // process 5 left tuples (1 to numLeftTuples).map(value => { assert( opExec .processTuple(generate_tuple(leftSchema, Some(value)), leftPort) .isEmpty ) }) assert(opExec.onFinish(leftPort).isEmpty) // process 5 right tuples val outputTuples: List[TupleLike] = (numLeftTuples + 1 to numLeftTuples + numRightTuples) .map(value => opExec .processTuple(generate_tuple(rightSchema, Some(value)), rightPort) ) .foldLeft(Iterator[TupleLike]())(_ ++ _) .toList assert(opExec.onFinish(rightPort).isEmpty) // verify correct output size assert(outputTuples.size == numLeftTuples * numRightTuples) assert( outputTuples.head.getFields.length == numLeftSchemaAttributes + numRightSchemaAttributes ) opExec.close() } it should "work with basic two input streams with duplicate attribute names" in { val numLeftSchemaAttributes: Int = 5 val numRightSchemaAttributes: Int = 7 val numLeftTuples: Int = 4 val numRightTuples: Int = 3 val duplicateAttribute: Attribute = new Attribute("left", AttributeType.STRING) val leftSchema = generate_schema("left", numLeftSchemaAttributes - 1) .add(duplicateAttribute) val rightSchema = generate_schema("right", numRightSchemaAttributes - 1) .add(duplicateAttribute) val inputSchemas = Map(PortIdentity() -> leftSchema, PortIdentity(1) -> rightSchema) val outputSchema = opDesc.getExternalOutputSchemas(inputSchemas).values.head // verify output schema is as expected & has no duplicates assert( outputSchema.getAttributeNames.toSet.size == outputSchema.getAttributeNames.size ) // no duplicates in output Schema // check left tuple attributes name remain same (0 until numLeftSchemaAttributes).map(index => assert( leftSchema.getAttributeNames .apply(index) .equals(outputSchema.getAttributeNames.apply(index)) ) ) // check right tuple attributes without duplicate names are handled (0 until numRightSchemaAttributes - 1).map(index => assert( rightSchema.getAttributeNames .apply(index) .equals(outputSchema.getAttributeNames.apply(numLeftSchemaAttributes + index)) ) ) // check right tuple attribute with duplicate name is handled val expectedAttrName: String = "left#@1#@1" assert( expectedAttrName.equals( outputSchema.getAttributeNames.apply( numLeftSchemaAttributes + numRightSchemaAttributes - 1 ) ) ) opExec = new CartesianProductOpExec() opExec.open() // process 4 left tuples (1 to numLeftTuples).map(value => { assert( opExec .processTuple(generate_tuple(leftSchema, Some(value)), leftPort) .isEmpty ) }) assert(opExec.onFinish(leftPort).isEmpty) // process 3 right tuples val outputTuples: List[TupleLike] = (numLeftTuples + 1 to numLeftTuples + numRightTuples) .map(value => opExec .processTuple(generate_tuple(rightSchema, Some(value)), rightPort) ) .foldLeft(Iterator[TupleLike]())(_ ++ _) .toList assert(opExec.onFinish(rightPort).isEmpty) // verify correct output size assert(outputTuples.size == numLeftTuples * numRightTuples) // verify output tuple like matches schema outputTuples.foreach(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema) ) opExec.close() } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/dictionary/DictionaryMatcherOpExecSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.dictionary import org.apache.texera.amber.core.tuple._ import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.util.JSONUtils.objectMapper import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class DictionaryMatcherOpExecSpec extends AnyFlatSpec with BeforeAndAfter { val tupleSchema: Schema = Schema() .add(new Attribute("field1", AttributeType.STRING)) .add(new Attribute("field2", AttributeType.INTEGER)) .add(new Attribute("field3", AttributeType.BOOLEAN)) val tuple: Tuple = Tuple .builder(tupleSchema) .add(new Attribute("field1", AttributeType.STRING), "nice a a person") .add(new Attribute("field2", AttributeType.INTEGER), 1) .add( new Attribute("field3", AttributeType.BOOLEAN), true ) .build() var opExec: DictionaryMatcherOpExec = _ val opDesc: DictionaryMatcherOpDesc = new DictionaryMatcherOpDesc() var outputSchema: Schema = _ val dictionaryScan = "nice a a person" val dictionarySubstring = "nice a a person and good" val dictionaryConjunction = "a person is nice" before { opDesc.attribute = "field1" opDesc.dictionary = dictionaryScan opDesc.resultAttribute = "matched" opDesc.matchingType = MatchingType.SCANBASED outputSchema = opDesc.getExternalOutputSchemas(Map(PortIdentity() -> tupleSchema)).values.head } it should "open" in { opExec = new DictionaryMatcherOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() assert(opExec.dictionaryEntries != null) } /** * Test cases that all Matching Types should match the query */ it should "match a tuple if present in the given dictionary entry when matching type is SCANBASED" in { opDesc.matchingType = MatchingType.SCANBASED opExec = new DictionaryMatcherOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val processedTuple = opExec.processTuple(tuple, 0).next() assert( processedTuple.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema).getField("matched") ) opExec.close() } it should "match a tuple if present in the given dictionary entry when matching type is SUBSTRING" in { opDesc.matchingType = MatchingType.SUBSTRING opExec = new DictionaryMatcherOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val processedTuple = opExec.processTuple(tuple, 0).next() assert( processedTuple.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema).getField("matched") ) opExec.close() } it should "match a tuple if present in the given dictionary entry when matching type is CONJUNCTION_INDEXBASED" in { opDesc.matchingType = MatchingType.CONJUNCTION_INDEXBASED opExec = new DictionaryMatcherOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val processedTuple = opExec.processTuple(tuple, 0).next() assert( processedTuple.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema).getField("matched") ) opExec.close() } /** * Test cases that SCANBASED and SUBSTRING Matching Types should fail to match a query */ it should "not match a tuple if not present in the given dictionary entry when matching type is SCANBASED and not exact match" in { opDesc.dictionary = dictionaryConjunction opDesc.matchingType = MatchingType.SCANBASED opExec = new DictionaryMatcherOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val processedTuple = opExec.processTuple(tuple, 0).next() assert( !processedTuple .asInstanceOf[SchemaEnforceable] .enforceSchema(outputSchema) .getField[Boolean]("matched") ) opExec.close() } it should "not match a tuple if the given dictionary entry doesn't contain all the tuple when the matching type is SUBSTRING" in { opDesc.dictionary = dictionaryConjunction opDesc.matchingType = MatchingType.SUBSTRING opExec = new DictionaryMatcherOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val processedTuple = opExec.processTuple(tuple, 0).next() assert( !processedTuple .asInstanceOf[SchemaEnforceable] .enforceSchema(outputSchema) .getField[Boolean]("matched") ) opExec.close() } it should "match a tuple if present in the given dictionary entry when matching type is CONJUNCTION_INDEXBASED even with different order" in { opDesc.dictionary = dictionaryConjunction opDesc.matchingType = MatchingType.CONJUNCTION_INDEXBASED opExec = new DictionaryMatcherOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val processedTuple = opExec.processTuple(tuple, 0).next() assert( processedTuple .asInstanceOf[SchemaEnforceable] .enforceSchema(outputSchema) .getField[Boolean]("matched") ) opExec.close() } /** * Test cases that only SUBSTRING Matching Type should match the query */ it should "not match a tuple if not present in the given dictionary entry when matching type is SCANBASED when the entry contains more text" in { opDesc.dictionary = dictionarySubstring opDesc.matchingType = MatchingType.SCANBASED opExec = new DictionaryMatcherOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val processedTuple = opExec.processTuple(tuple, 0).next() assert( !processedTuple .asInstanceOf[SchemaEnforceable] .enforceSchema(outputSchema) .getField[Boolean]("matched") ) opExec.close() } it should "not match a tuple if not present in the given dictionary entry when matching type is CONJUNCTION_INDEXBASED when the entry contains more text" in { opDesc.dictionary = dictionarySubstring opDesc.matchingType = MatchingType.CONJUNCTION_INDEXBASED opExec = new DictionaryMatcherOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val processedTuple = opExec.processTuple(tuple, 0).next() assert( !processedTuple .asInstanceOf[SchemaEnforceable] .enforceSchema(outputSchema) .getField[Boolean]("matched") ) opExec.close() } it should "match a tuple if not present in the given dictionary entry when matching type is SUBSTRING when the entry contains more text" in { opDesc.dictionary = dictionarySubstring opDesc.matchingType = MatchingType.SUBSTRING opExec = new DictionaryMatcherOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val processedTuple = opExec.processTuple(tuple, 0).next() assert( processedTuple .asInstanceOf[SchemaEnforceable] .enforceSchema(outputSchema) .getField[Boolean]("matched") ) opExec.close() } it should "close properly" in { opExec.close() assert(opExec.dictionaryEntries == null) assert(opExec.tokenizedDictionaryEntries == null) assert(opExec.luceneAnalyzer == null) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/difference/DifferenceOpExecSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.difference import org.apache.texera.amber.core.tuple._ import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class DifferenceOpExecSpec extends AnyFlatSpec with BeforeAndAfter { var input1: Int = 0 var input2: Int = 1 var opExec: DifferenceOpExec = _ var counter: Int = 0 val schema: Schema = Schema() .add(new Attribute("field1", AttributeType.STRING)) .add(new Attribute("field2", AttributeType.INTEGER)) .add(new Attribute("field3", AttributeType.BOOLEAN)) def tuple(): Tuple = { counter += 1 Tuple .builder(schema) .addSequentially(Array("hello", Int.box(counter), Boolean.box(true))) .build() } before { opExec = new DifferenceOpExec() } it should "open" in { opExec.open() } it should "work with basic two input streams with no duplicates" in { opExec.open() counter = 0 val commonTuples = (1 to 10).map(_ => tuple()).toList (0 to 7).map(i => { opExec.processTuple(commonTuples(i), input1) }) assert(opExec.onFinish(input1).isEmpty) (5 to 9).map(i => { opExec.processTuple(commonTuples(i), input2) }) val outputTuples: Set[TupleLike] = opExec.onFinish(input2).toSet assert( outputTuples.equals(commonTuples.slice(0, 5).toSet) ) opExec.close() } it should "work with one empty input upstream after a data stream" in { opExec.open() counter = 0 val commonTuples = (1 to 10).map(_ => tuple()).toList (0 to 9).map(i => { opExec.processTuple(commonTuples(i), input1) }) assert(opExec.onFinish(input1).isEmpty) val outputTuples: Set[TupleLike] = opExec.onFinish(input2).toSet assert(outputTuples.equals(commonTuples.toSet)) opExec.close() } it should "work with one empty input upstream after a data stream - other order" in { opExec.open() counter = 0 val commonTuples = (1 to 10).map(_ => tuple()).toList (0 to 9).map(i => { opExec.processTuple(commonTuples(i), input2) }) assert(opExec.onFinish(input2).isEmpty) val outputTuples: Set[TupleLike] = opExec.onFinish(input1).toSet assert(outputTuples.isEmpty) opExec.close() } it should "work with one empty input upstream before a data stream" in { opExec.open() counter = 0 val commonTuples = (1 to 10).map(_ => tuple()).toList assert(opExec.onFinish(input2).isEmpty) (0 to 9).map(i => { opExec.processTuple(commonTuples(i), input1) }) val outputTuples: Set[TupleLike] = opExec.onFinish(input1).toSet assert(outputTuples.equals(commonTuples.toSet)) opExec.close() } it should "work with one empty input upstream during a data stream" in { opExec.open() counter = 0 val commonTuples = (1 to 10).map(_ => tuple()).toList (0 to 5).map(i => { opExec.processTuple(commonTuples(i), input1) }) assert(opExec.onFinish(input2).isEmpty) (6 to 9).map(i => { opExec.processTuple(commonTuples(i), input1) }) val outputTuples: Set[TupleLike] = opExec.onFinish(input1).toSet assert(outputTuples.equals(commonTuples.toSet)) opExec.close() } it should "work with two empty input upstreams" in { opExec.open() assert(opExec.onFinish(input1).isEmpty) assert(opExec.onFinish(input2).isEmpty) opExec.close() } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/distinct/DistinctOpExecSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.distinct import org.apache.texera.amber.core.tuple._ import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class DistinctOpExecSpec extends AnyFlatSpec with BeforeAndAfter { val tupleSchema: Schema = Schema() .add(new Attribute("field1", AttributeType.STRING)) .add(new Attribute("field2", AttributeType.INTEGER)) .add(new Attribute("field3", AttributeType.BOOLEAN)) val tuple: () => Tuple = () => Tuple .builder(tupleSchema) .add(new Attribute("field1", AttributeType.STRING), "hello") .add(new Attribute("field2", AttributeType.INTEGER), 1) .add( new Attribute("field3", AttributeType.BOOLEAN), true ) .build() val tuple2: () => Tuple = () => Tuple .builder(tupleSchema) .add(new Attribute("field1", AttributeType.STRING), "hello") .add(new Attribute("field2", AttributeType.INTEGER), 2) .add( new Attribute("field3", AttributeType.BOOLEAN), false ) .build() var opExec: DistinctOpExec = _ before { opExec = new DistinctOpExec() } it should "open" in { opExec.open() } it should "remove duplicate Tuple with the same content" in { opExec.open() (1 to 1000).map(_ => { opExec.processTuple(tuple(), 0) }) val outputTuples: List[TupleLike] = opExec.onFinish(0).toList assert(outputTuples.size == 1) assert(outputTuples.head.equals(tuple())) opExec.close() } it should "preserve the insertion order" in { opExec.open() (1 to 1000).map(_ => { opExec.processTuple(tuple(), 0) }) (1 to 1000).map(_ => { opExec.processTuple(tuple2(), 0) }) (1 to 1000).map(_ => { opExec.processTuple(tuple(), 0) }) val outputTuples: List[TupleLike] = opExec.onFinish(0).toList assert(outputTuples.size == 2) assert(outputTuples.head.equals(tuple())) assert(outputTuples.apply(1).equals(tuple2())) opExec.close() } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/filter/SpecializedFilterOpExecSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.filter import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple} import org.apache.texera.amber.util.JSONUtils.objectMapper import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class SpecializedFilterOpExecSpec extends AnyFlatSpec with BeforeAndAfter { val inputPort: Int = 0 val opDesc: SpecializedFilterOpDesc = new SpecializedFilterOpDesc() val tuplesWithOneFieldNull: Iterable[Tuple] = AttributeType .values() .map(attributeType => Tuple .builder( Schema().add(new Attribute(attributeType.name(), attributeType)) ) .add(new Attribute(attributeType.name(), attributeType), null) .build() ) val tupleSchema: Schema = Schema() .add(new Attribute("string", AttributeType.STRING)) .add(new Attribute("int", AttributeType.INTEGER)) .add(new Attribute("bool", AttributeType.BOOLEAN)) .add(new Attribute("long", AttributeType.LONG)) val allNullTuple: Tuple = Tuple .builder(tupleSchema) .add(new Attribute("string", AttributeType.STRING), null) .add(new Attribute("int", AttributeType.INTEGER), null) .add(new Attribute("bool", AttributeType.BOOLEAN), null) .add(new Attribute("long", AttributeType.LONG), null) .build() val nonNullTuple: Tuple = Tuple .builder(tupleSchema) .add(new Attribute("string", AttributeType.STRING), "hello") .add(new Attribute("int", AttributeType.INTEGER), 0) .add(new Attribute("bool", AttributeType.BOOLEAN), false) .add(new Attribute("long", AttributeType.LONG), Long.MaxValue) .build() it should "open and close" in { opDesc.predicates = List() val opExec = new SpecializedFilterOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() opExec.close() } it should "do nothing when predicates is an empty list" in { opDesc.predicates = List() val opExec = new SpecializedFilterOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() assert(opExec.processTuple(allNullTuple, inputPort).isEmpty) opExec.close() } it should "not have is_null comparisons be affected by values" in { opDesc.predicates = List(new FilterPredicate("string", ComparisonType.IS_NULL, "value")) val opExec = new SpecializedFilterOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() assert(opExec.processTuple(allNullTuple, inputPort).nonEmpty) opExec.close() } it should "not have is_not_null comparisons be affected by values" in { opDesc.predicates = List(new FilterPredicate("string", ComparisonType.IS_NOT_NULL, "value")) val opExec = new SpecializedFilterOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() assert(opExec.processTuple(allNullTuple, inputPort).isEmpty) opExec.close() } it should "output null tuples when filtering is_null" in { tuplesWithOneFieldNull .map(nullTuple => { val attributes = nullTuple.getSchema.getAttributes assert(attributes.length == 1) opDesc.predicates = List(new FilterPredicate(attributes.head.getName, ComparisonType.IS_NULL, null)) val opExec = new SpecializedFilterOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() assert(opExec.processTuple(nullTuple, inputPort).nonEmpty) opExec.close() }) } it should "filter out non null tuples when filtering is_null" in { opDesc.predicates = List(new FilterPredicate("string", ComparisonType.IS_NULL, "value")) val opExec = new SpecializedFilterOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() assert(opExec.processTuple(nonNullTuple, inputPort).isEmpty) opExec.close() } it should "output non null tuples when filter is_not_null" in { opDesc.predicates = List(new FilterPredicate("string", ComparisonType.IS_NOT_NULL, "value")) val opExec = new SpecializedFilterOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() assert(opExec.processTuple(nonNullTuple, inputPort).nonEmpty) opExec.close() } it should "filter out null tuples when filter is_not_null" in { tuplesWithOneFieldNull .map(nullTuple => { val attributes = nullTuple.getSchema.getAttributes assert(attributes.length == 1) opDesc.predicates = List(new FilterPredicate(attributes.head.getName, ComparisonType.IS_NOT_NULL, null)) val opExec = new SpecializedFilterOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() assert(opExec.processTuple(nullTuple, inputPort).isEmpty) opExec.close() }) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/flatmap/FlatMapOpExecSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.flatmap import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple, TupleLike} import org.scalatest.flatspec.AnyFlatSpec class FlatMapOpExecSpec extends AnyFlatSpec { private val schema: Schema = Schema().add(new Attribute("v", AttributeType.INTEGER)) private def tuple(v: Int): Tuple = Tuple.builder(schema).add(new Attribute("v", AttributeType.INTEGER), Integer.valueOf(v)).build() "FlatMapOpExec.processTuple" should "delegate to the configured flatMapFunc" in { val exec = new FlatMapOpExec() exec.setFlatMapFunc(t => Iterator(t, t)) val out = exec.processTuple(tuple(1), 0).toList assert(out.size == 2) assert(out.forall(_.asInstanceOf[Tuple] == tuple(1))) } it should "apply a duplicating flatMap (1 → 2) across a stream of tuples" in { val exec = new FlatMapOpExec() exec.setFlatMapFunc(t => Iterator(t, t)) val out = (1 to 4).flatMap(v => exec.processTuple(tuple(v), 0).toList) assert(out.size == 8) val expected = (1 to 4).flatMap(v => List(tuple(v), tuple(v))) assert(out.map(_.asInstanceOf[Tuple]) == expected) } it should "apply an expanding flatMap that fans out by the input value" in { val exec = new FlatMapOpExec() exec.setFlatMapFunc { (t: Tuple) => val n = t.getField[Int]("v") (1 to n).map(_ => t).iterator } val out = exec.processTuple(tuple(3), 0).toList assert(out.size == 3) assert(out.forall(_.asInstanceOf[Tuple] == tuple(3))) } it should "apply a filtering flatMap that drops some inputs entirely" in { val exec = new FlatMapOpExec() // Keep only odd values exec.setFlatMapFunc { (t: Tuple) => if (t.getField[Int]("v") % 2 == 1) Iterator.single(t) else Iterator.empty } val out = (1 to 5).flatMap(v => exec.processTuple(tuple(v), 0).toList) assert(out.map(_.asInstanceOf[Tuple]) == List(tuple(1), tuple(3), tuple(5))) } it should "apply a stateful flatMap (closes over an external counter)" in { val exec = new FlatMapOpExec() var counter = 0 exec.setFlatMapFunc { (t: Tuple) => counter += 1 val emit = (1 to counter).map(_ => t) emit.iterator } val out = (0 until 3).flatMap(_ => exec.processTuple(tuple(7), 0).toList) // counter goes 1, 2, 3 → outputs 1+2+3 = 6 tuples assert(out.size == 6) assert(counter == 3) } it should "emit nothing when the flatMapFunc returns an empty iterator" in { val exec = new FlatMapOpExec() exec.setFlatMapFunc(_ => Iterator.empty) assert(exec.processTuple(tuple(1), 0).isEmpty) } it should "preserve the order of tuples emitted by the flatMapFunc" in { val exec = new FlatMapOpExec() exec.setFlatMapFunc(t => Iterator(tuple(99), t, tuple(0))) val out = exec.processTuple(tuple(7), 0).toList.map(_.asInstanceOf[Tuple]) assert(out == List(tuple(99), tuple(7), tuple(0))) } "FlatMapOpExec.setFlatMapFunc" should "overwrite a previously installed function" in { val exec = new FlatMapOpExec() exec.setFlatMapFunc(_ => Iterator.empty) exec.setFlatMapFunc((t: Tuple) => Iterator[TupleLike](t)) val out = exec.processTuple(tuple(5), 0).toList assert(out == List(tuple(5))) } it should "throw NullPointerException when processTuple is invoked before setFlatMapFunc" in { val exec = new FlatMapOpExec() assertThrows[NullPointerException] { // Iterator construction calls flatMapFunc(tuple) eagerly, so the NPE // surfaces here even though processTuple itself returns an iterator. exec.processTuple(tuple(1), 0) } } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/hashJoin/HashJoinOpSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.hashJoin import org.apache.texera.amber.core.tuple._ import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.hashJoin.HashJoinOpDesc.HASH_JOIN_INTERNAL_KEY_NAME import org.apache.texera.amber.util.JSONUtils.objectMapper import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class HashJoinOpSpec extends AnyFlatSpec with BeforeAndAfter { val build: Int = 0 val probe: Int = 1 var buildOpExec: HashJoinBuildOpExec[String] = _ var probeOpExec: HashJoinProbeOpExec[String] = _ var opDesc: HashJoinOpDesc[String] = _ def getInternalHashTableSchema(buildInputSchema: Schema): Schema = { Schema() .add(HASH_JOIN_INTERNAL_KEY_NAME, AttributeType.ANY) .add(buildInputSchema) } def tuple(name: String, n: Int = 1, i: Option[Int]): Tuple = { Tuple .builder(schema(name, n)) .addSequentially(Array[Any](i.map(_.toString).orNull, i.map(_.toString).orNull)) .build() } def schema(name: String, n: Int = 1): Schema = { Schema() .add(new Attribute(name, AttributeType.STRING)) .add(new Attribute(name + "_" + n, AttributeType.STRING)) } it should "work with basic two input streams with different buildAttributeName and probeAttributeName" in { opDesc = new HashJoinOpDesc[String]() opDesc.buildAttributeName = "build_1" opDesc.probeAttributeName = "probe_1" opDesc.joinType = JoinType.INNER val inputSchemas = Map(PortIdentity() -> schema("build"), PortIdentity(1) -> schema("probe")) val outputSchema = opDesc.getExternalOutputSchemas(inputSchemas).values.head buildOpExec = new HashJoinBuildOpExec[String](objectMapper.writeValueAsString(opDesc)) buildOpExec.open() (0 to 7).map(i => { assert( buildOpExec.processTuple(tuple("build", 1, Some(i)), build).isEmpty ) }) val buildOpOutputIterator = buildOpExec.onFinish(build) assert(buildOpOutputIterator.hasNext) probeOpExec = new HashJoinProbeOpExec[String](objectMapper.writeValueAsString(opDesc)) probeOpExec.open() while (buildOpOutputIterator.hasNext) { assert( probeOpExec .processTuple( buildOpOutputIterator .next() .asInstanceOf[SchemaEnforceable] .enforceSchema(getInternalHashTableSchema(inputSchemas.head._2)), build ) .isEmpty ) } assert(probeOpExec.onFinish(build).isEmpty) buildOpExec.close() val outputTuples = (5 to 9) .map(i => probeOpExec.processTuple(tuple("probe", 1, Some(i)), probe)) .foldLeft(Iterator[TupleLike]())(_ ++ _) .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema)) .toList assert(probeOpExec.onFinish(probe).isEmpty) assert(outputTuples.size == 3) assert(outputTuples.head.getFields.length == 3) probeOpExec.close() } it should "work with basic two input streams with the same buildAttributeName and probeAttributeName" in { opDesc = new HashJoinOpDesc[String]() opDesc.buildAttributeName = "same" opDesc.probeAttributeName = "same" opDesc.joinType = JoinType.INNER val inputSchemas = Map(PortIdentity() -> schema("same", 1), PortIdentity(1) -> schema("same", 2)) val outputSchema = opDesc.getExternalOutputSchemas(inputSchemas).values.head buildOpExec = new HashJoinBuildOpExec[String](objectMapper.writeValueAsString(opDesc)) buildOpExec.open() (0 to 7).map(i => { assert( buildOpExec.processTuple(tuple("same", 1, Some(i)), build).isEmpty ) }) val buildOpOutputIterator = buildOpExec.onFinish(build) assert(buildOpOutputIterator.hasNext) probeOpExec = new HashJoinProbeOpExec[String](objectMapper.writeValueAsString(opDesc)) probeOpExec.open() while (buildOpOutputIterator.hasNext) { assert( probeOpExec .processTuple( buildOpOutputIterator .next() .asInstanceOf[SchemaEnforceable] .enforceSchema(getInternalHashTableSchema(inputSchemas.head._2)), build ) .isEmpty ) } assert(probeOpExec.onFinish(build).isEmpty) buildOpExec.close() val outputTuples = (5 to 9) .map(i => probeOpExec.processTuple(tuple("same", n = 2, Some(i)), probe)) .foldLeft(Iterator[TupleLike]())(_ ++ _) .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema)) .toList assert(probeOpExec.onFinish(probe).isEmpty) assert(outputTuples.size == 3) assert(outputTuples.head.getFields.length == 3) probeOpExec.close() } it should "work with basic two input streams with the same buildAttributeName and probeAttributeName with Full Outer Join" in { opDesc = new HashJoinOpDesc[String]() opDesc.buildAttributeName = "same" opDesc.probeAttributeName = "same" opDesc.joinType = JoinType.FULL_OUTER val inputSchemas = Map(PortIdentity() -> schema("same", 1), PortIdentity(1) -> schema("same", 2)) val outputSchema = opDesc.getExternalOutputSchemas(inputSchemas).values.head buildOpExec = new HashJoinBuildOpExec[String](objectMapper.writeValueAsString(opDesc)) buildOpExec.open() (0 to 7).map(i => { assert( buildOpExec.processTuple(tuple("same", 1, Some(i)), build).isEmpty ) }) val buildOpOutputIterator = buildOpExec.onFinish(build) assert(buildOpOutputIterator.hasNext) probeOpExec = new HashJoinProbeOpExec[String](objectMapper.writeValueAsString(opDesc)) probeOpExec.open() while (buildOpOutputIterator.hasNext) { assert( probeOpExec .processTuple( buildOpOutputIterator .next() .asInstanceOf[SchemaEnforceable] .enforceSchema(getInternalHashTableSchema(inputSchemas.head._2)), build ) .isEmpty ) } assert(probeOpExec.onFinish(build).isEmpty) buildOpExec.close() assert( (5 to 9) .map(_ => { probeOpExec.processTuple(tuple("same", n = 2, None), probe) }) .foldLeft(Iterator[TupleLike]())(_ ++ _) .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema)) .size == 5 ) assert(probeOpExec.onFinish(probe).size == 8) probeOpExec.close() } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/ifStatement/IfOpExecSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.ifStatement import org.apache.texera.amber.core.state.State import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple} import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.util.JSONUtils.objectMapper import org.scalatest.flatspec.AnyFlatSpec class IfOpExecSpec extends AnyFlatSpec { private val schema: Schema = Schema().add(new Attribute("v", AttributeType.INTEGER)) private def tuple(v: Int): Tuple = Tuple.builder(schema).add(new Attribute("v", AttributeType.INTEGER), Integer.valueOf(v)).build() // The IfOpDesc requires polymorphic Jackson (operatorType discriminator), // so build a real instance and serialize it. private def desc(conditionName: String): String = { val d = new IfOpDesc() d.conditionName = conditionName objectMapper.writeValueAsString(d) } private val truePortId = PortIdentity(1) private val falsePortId = PortIdentity() "IfOpExec.processState" should "route to the true port when the condition value is true" in { val exec = new IfOpExec(desc("flag")) val result = exec.processState(State(Map[String, Any]("flag" -> true)), 0) assert(result.exists(_.values("flag") == true)) val out = exec.processTupleMultiPort(tuple(1), 0).toList assert(out == List((tuple(1), Some(truePortId)))) } it should "route to the false port when the condition value is false" in { val exec = new IfOpExec(desc("flag")) exec.processState(State(Map[String, Any]("flag" -> false)), 0) val out = exec.processTupleMultiPort(tuple(1), 0).toList assert(out == List((tuple(1), Some(falsePortId)))) } "IfOpExec.processTupleMultiPort" should "default to the true port before any state is observed" in { val exec = new IfOpExec(desc("flag")) val out = exec.processTupleMultiPort(tuple(7), 0).toList assert(out == List((tuple(7), Some(truePortId)))) } it should "reflect the most recent processState routing decision" in { val exec = new IfOpExec(desc("flag")) exec.processState(State(Map[String, Any]("flag" -> true)), 0) exec.processState(State(Map[String, Any]("flag" -> false)), 0) val out = exec.processTupleMultiPort(tuple(1), 0).toList assert(out == List((tuple(1), Some(falsePortId)))) } "IfOpExec.processTuple" should "be unimplemented (multi-port routing is required)" in { val exec = new IfOpExec(desc("flag")) assertThrows[NotImplementedError] { exec.processTuple(tuple(1), 0) } } "IfOpExec.processState" should "throw when the configured conditionName is missing from the state" in { val exec = new IfOpExec(desc("flag")) // `state.values(desc.conditionName)` does an unsafe Map.apply, so a // missing key surfaces as NoSuchElementException rather than a quiet // misroute. assertThrows[NoSuchElementException] { exec.processState(State(Map[String, Any]("other" -> true)), 0) } } it should "throw ClassCastException when the conditionName value is a String" in { val exec = new IfOpExec(desc("flag")) // Current contract is `asInstanceOf[Boolean]`, so a non-Boolean value // must surface as a ClassCastException rather than a silent route. assertThrows[ClassCastException] { exec.processState(State(Map[String, Any]("flag" -> "yes")), 0) } } it should "throw ClassCastException when the conditionName value is an Int" in { val exec = new IfOpExec(desc("flag")) // Scala's `asInstanceOf[Boolean]` does not coerce numerics to booleans // the way Python's `bool(1)` does — it must throw. assertThrows[ClassCastException] { exec.processState(State(Map[String, Any]("flag" -> 1)), 0) } } it should "throw ClassCastException when the conditionName value is zero (Int)" in { val exec = new IfOpExec(desc("flag")) // Likewise, integer 0 is not coerced to false. assertThrows[ClassCastException] { exec.processState(State(Map[String, Any]("flag" -> 0)), 0) } } it should "treat a null condition value as false (default Boolean unbox)" in { val exec = new IfOpExec(desc("flag")) // `null.asInstanceOf[Boolean]` quietly unboxes to `false` in Scala, so // a null condition routes to the FALSE port rather than throwing. This // is a quiet behavior worth pinning so a future change to throw doesn't // silently regress. val result = exec.processState(State(Map[String, Any]("flag" -> null)), 0) assert(result.isDefined) val out = exec.processTupleMultiPort(tuple(1), 0).toList assert(out == List((tuple(1), Some(falsePortId)))) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/intersect/IntersectOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.intersect import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.core.workflow.{HashPartition, SinglePartition, UnknownPartition} import org.apache.texera.amber.operator.metadata.OperatorGroupConstants import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers class IntersectOpDescSpec extends AnyFlatSpec with Matchers { private val workflowId = WorkflowIdentity(1L) private val executionId = ExecutionIdentity(1L) "IntersectOpDesc.operatorInfo" should "advertise the user-friendly name and Set group" in { val info = (new IntersectOpDesc).operatorInfo info.userFriendlyName shouldBe "Intersect" info.operatorGroupName shouldBe OperatorGroupConstants.SET_GROUP info.operatorDescription should include("intersect") } it should "expose two input ports (PortIdentity 0 and 1) and one blocking output" in { val info = (new IntersectOpDesc).operatorInfo info.inputPorts should have length 2 info.inputPorts.map(_.id.id) shouldBe List(0, 1) info.outputPorts should have length 1 info.outputPorts.head.blocking shouldBe true } "IntersectOpDesc.getPhysicalOp" should "require HashPartition on both input ports" in { val op = new IntersectOpDesc val physical = op.getPhysicalOp(workflowId, executionId) physical.partitionRequirement shouldBe List( Option(HashPartition()), Option(HashPartition()) ) } it should "always derive HashPartition for the output regardless of input partitions" in { // The Intersect set semantics require both inputs to be hash-aligned, so // the derived output partition must remain hash even when the upstream // inputs report differing partition kinds. val op = new IntersectOpDesc val physical = op.getPhysicalOp(workflowId, executionId) physical.derivePartition(List(SinglePartition(), UnknownPartition())) shouldBe HashPartition() physical.derivePartition( List(HashPartition(List("a")), HashPartition(List("b"))) ) shouldBe HashPartition() } it should "wire the IntersectOpExec class name into the OpExecInitInfo" in { // Pattern-match on OpExecWithClassName instead of substring-matching the // toString output, which is brittle to scalapb formatting changes. val op = new IntersectOpDesc val physical = op.getPhysicalOp(workflowId, executionId) physical.opExecInitInfo match { case OpExecWithClassName(className, _) => className shouldBe "org.apache.texera.amber.operator.intersect.IntersectOpExec" case other => fail(s"expected OpExecWithClassName, got $other") } } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/intersect/IntersectOpExecSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.intersect import org.apache.texera.amber.core.tuple._ import org.apache.texera.amber.core.virtualidentity.{OperatorIdentity, PhysicalOpIdentity} import org.apache.texera.amber.core.workflow.{PhysicalLink, PortIdentity} import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec import scala.util.Random class IntersectOpExecSpec extends AnyFlatSpec with BeforeAndAfter { var opExec: IntersectOpExec = _ var counter: Int = 0 val tupleSchema: Schema = Schema() .add(new Attribute("field1", AttributeType.STRING)) .add(new Attribute("field2", AttributeType.INTEGER)) .add(new Attribute("field3", AttributeType.BOOLEAN)) def physicalOpId(): PhysicalOpIdentity = { counter += 1 PhysicalOpIdentity(OperatorIdentity("" + counter), "" + counter) } def physicalLink(): PhysicalLink = PhysicalLink( physicalOpId(), fromPortId = PortIdentity(), physicalOpId(), toPortId = PortIdentity() ) def tuple(): Tuple = { counter += 1 Tuple .builder(tupleSchema) .add(new Attribute("field1", AttributeType.STRING), "hello") .add(new Attribute("field2", AttributeType.INTEGER), counter) .add( new Attribute("field3", AttributeType.BOOLEAN), true ) .build() } before { opExec = new IntersectOpExec() } it should "open" in { opExec.open() } it should "work with basic two input streams with no duplicates" in { val input1 = 0 val input2 = 1 opExec.open() counter = 0 val commonTuples = (1 to 10).map(_ => tuple()).toList (0 to 7).map(i => { opExec.processTuple(commonTuples(i), input1) }) assert(opExec.onFinish(input1).isEmpty) (5 to 9).map(i => { opExec.processTuple(commonTuples(i), input2) }) val outputTuples: Set[TupleLike] = opExec.onFinish(input2).toSet assert(outputTuples.equals(commonTuples.slice(5, 8).toSet)) opExec.close() } it should "work with one empty input upstream after a data stream" in { val input0 = 0 val input1 = 1 opExec.open() counter = 0 val commonTuples = (1 to 10).map(_ => tuple()).toList (1 to 100).map(_ => { opExec.processTuple(tuple(), input0) opExec.processTuple(commonTuples(Random.nextInt(commonTuples.size)), input0) }) assert(opExec.onFinish(input0).isEmpty) assert(opExec.onFinish(input1).isEmpty) opExec.close() } it should "work with one empty input upstream after a data stream - other order" in { val input1 = 0 val input2 = 1 opExec.open() counter = 0 val commonTuples = (1 to 10).map(_ => tuple()).toList (1 to 100).map(_ => { opExec.processTuple(tuple(), input1) opExec.processTuple(commonTuples(Random.nextInt(commonTuples.size)), input1) }) assert(opExec.onFinish(input2).isEmpty) assert(opExec.onFinish(input1).isEmpty) opExec.close() } it should "work with one empty input upstream before a data stream" in { val input1 = 0 val input2 = 1 opExec.open() counter = 0 val commonTuples = (1 to 10).map(_ => tuple()).toList assert(opExec.onFinish(input2).isEmpty) (1 to 100).map(_ => { opExec.processTuple(tuple(), input1) opExec.processTuple(commonTuples(Random.nextInt(commonTuples.size)), input1) }) assert(opExec.onFinish(input1).isEmpty) opExec.close() } it should "work with one empty input upstream during a data stream" in { val input1 = 0 val input2 = 1 opExec.open() counter = 0 val commonTuples = (1 to 10).map(_ => tuple()).toList (1 to 100).map(_ => { opExec.processTuple(tuple(), input1) opExec.processTuple(commonTuples(Random.nextInt(commonTuples.size)), input1) }) assert(opExec.onFinish(input2).isEmpty) (1 to 100).map(_ => { opExec.processTuple(tuple(), input1) opExec.processTuple(commonTuples(Random.nextInt(commonTuples.size)), input1) }) assert(opExec.onFinish(input1).isEmpty) opExec.close() } it should "work with two empty input upstreams" in { opExec.open() assert(opExec.onFinish(0).isEmpty) assert(opExec.onFinish(1).isEmpty) opExec.close() } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/intervalJoin/IntervalOpExecSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.intervalJoin import org.apache.texera.amber.core.tuple._ import org.apache.texera.amber.core.virtualidentity.{OperatorIdentity, PhysicalOpIdentity} import org.apache.texera.amber.core.workflow.{PhysicalLink, PortIdentity} import org.apache.texera.amber.util.JSONUtils.objectMapper import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec import java.sql.Timestamp import scala.collection.mutable.ArrayBuffer import scala.util.Random.{nextInt, nextLong} class IntervalOpExecSpec extends AnyFlatSpec with BeforeAndAfter { val left: Int = 0 val right: Int = 1 var opDesc: IntervalJoinOpDesc = _ var counter: Int = 0 def physicalLink(): PhysicalLink = PhysicalLink( physicalOpId(), fromPortId = PortIdentity(), physicalOpId(), toPortId = PortIdentity() ) def physicalOpId(): PhysicalOpIdentity = { counter += 1 PhysicalOpIdentity(OperatorIdentity("" + counter), "" + counter) } def newTuple[T](name: String, n: Int = 1, i: T, attributeType: AttributeType): Tuple = { Tuple .builder(schema(name, attributeType, n)) .add(new Attribute(name, attributeType), i) .add(new Attribute(name + "_" + 1, attributeType), i) .build() } def integerTuple(name: String, n: Int = 1, i: Int): Tuple = { Tuple .builder(schema(name, AttributeType.INTEGER, n)) .add(new Attribute(name, AttributeType.INTEGER), i) .add(new Attribute(name + "_" + 1, AttributeType.INTEGER), i) .build() } def doubleTuple(name: String, n: Int = 1, i: Double): Tuple = { Tuple .builder(schema(name, AttributeType.DOUBLE, n)) .add(new Attribute(name, AttributeType.DOUBLE), i) .add(new Attribute(name + "_" + 1, AttributeType.DOUBLE), i) .build() } def schema(name: String, attributeType: AttributeType, n: Int = 1): Schema = { Schema() .add(new Attribute(name, attributeType)) .add(new Attribute(name + "_" + n, attributeType)) } def longTuple(name: String, n: Int = 1, i: Long): Tuple = { Tuple .builder(schema(name, AttributeType.LONG, n)) .add(new Attribute(name, AttributeType.LONG), i) .add(new Attribute(name + "_" + 1, AttributeType.LONG), i) .build() } def timeStampTuple(name: String, n: Int = 1, i: Timestamp): Tuple = { Tuple .builder(schema(name, AttributeType.TIMESTAMP, n)) .add(new Attribute(name, AttributeType.TIMESTAMP), i) .add(new Attribute(name + "_" + 1, AttributeType.TIMESTAMP), i) .build() } def bruteForceJoin[T]( leftInput: Array[T], rightInput: Array[T], includeLeftBound: Boolean, includeRightBound: Boolean, constant: Long, dataType: AttributeType, timeIntervalType: TimeIntervalType = TimeIntervalType.DAY ): Int = { var resultSize: Int = 0 for (k <- leftInput.indices) { for (i <- rightInput.indices) { dataType match { case AttributeType.INTEGER => if ( compare( leftInput(k).asInstanceOf[Int].toLong, rightInput(i).asInstanceOf[Int].toLong, rightInput(i).asInstanceOf[Int].toLong + constant, includeLeftBound, includeRightBound ) ) { resultSize += 1 } case AttributeType.LONG => if ( compare( leftInput(k).asInstanceOf[Long], rightInput(i).asInstanceOf[Long], rightInput(i).asInstanceOf[Long] + constant, includeLeftBound, includeRightBound ) ) { resultSize += 1 } case AttributeType.TIMESTAMP => val leftBoundValue: Long = rightInput(i).asInstanceOf[Timestamp].getTime val rightBoundValue: Long = timeIntervalType match { case TimeIntervalType.YEAR => Timestamp .valueOf( rightInput(i).asInstanceOf[Timestamp].toLocalDateTime.plusYears(constant) ) .getTime case TimeIntervalType.MONTH => Timestamp .valueOf( rightInput(i).asInstanceOf[Timestamp].toLocalDateTime.plusMonths(constant) ) .getTime case TimeIntervalType.DAY => Timestamp .valueOf( rightInput(i).asInstanceOf[Timestamp].toLocalDateTime.plusDays(constant) ) .getTime case TimeIntervalType.HOUR => Timestamp .valueOf( rightInput(i).asInstanceOf[Timestamp].toLocalDateTime.plusHours(constant) ) .getTime case TimeIntervalType.MINUTE => Timestamp .valueOf( rightInput(i).asInstanceOf[Timestamp].toLocalDateTime.plusMinutes(constant) ) .getTime case TimeIntervalType.SECOND => Timestamp .valueOf( rightInput(i).asInstanceOf[Timestamp].toLocalDateTime.plusSeconds(constant) ) .getTime } if ( compare( leftInput(k).asInstanceOf[Timestamp].getTime, leftBoundValue, rightBoundValue, includeLeftBound, includeRightBound ) ) { resultSize += 1 } case _ => throw new RuntimeException(s"unexpected type $dataType") } } } resultSize } def compare( input1: Long, leftBound: Long, rightBound: Long, includeLeftBound: Boolean, includeRightBound: Boolean ): Boolean = { if (includeLeftBound && includeRightBound) { input1 >= leftBound && input1 <= rightBound } else if (includeLeftBound && !includeRightBound) { input1 >= leftBound && input1 < rightBound } else if (!includeLeftBound && includeRightBound) { input1 > leftBound && input1 <= rightBound } else { input1 > leftBound && input1 < rightBound } } def testJoin[T]( leftKey: String, rightKey: String, includeLeftBound: Boolean, includeRightBound: Boolean, dataType: AttributeType, timeIntervalType: TimeIntervalType, intervalConstant: Long, leftInput: Array[T], rightInput: Array[T] ): Unit = { val inputSchemas = Map( PortIdentity() -> schema(leftKey, dataType), PortIdentity(1) -> schema(rightKey, dataType) ) opDesc = new IntervalJoinOpDesc( leftKey, rightKey, intervalConstant, includeLeftBound, includeRightBound, timeIntervalType ) val outputSchema = opDesc.getExternalOutputSchemas(inputSchemas).values.head val opExec = new IntervalJoinOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() counter = 0 var leftIndex: Int = 0 var rightIndex: Int = 0 val leftOrder = LazyList.continually(nextInt(10)).take(leftInput.length).toList val rightOrder = LazyList.continually(nextInt(10)).take(rightInput.length).toList val outputTuples: ArrayBuffer[Tuple] = new ArrayBuffer[Tuple] while (leftIndex < leftOrder.size || rightIndex < rightOrder.size) { if ( leftIndex < leftOrder.size && (rightIndex >= rightOrder.size || leftOrder( leftIndex ) < rightOrder(rightIndex)) ) { val result = opExec .processTuple(newTuple[T](leftKey, 1, leftInput(leftIndex), dataType), left) .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema)) .toBuffer outputTuples.appendAll( result ) leftIndex += 1 } else if (rightIndex < rightOrder.size) { val result = opExec .processTuple(newTuple(rightKey, 1, rightInput(rightIndex), dataType), right) .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema)) .toBuffer outputTuples.appendAll( result ) rightIndex += 1 } } val bruteForceResult: Int = bruteForceJoin( leftInput, rightInput, includeLeftBound, includeRightBound, intervalConstant, dataType ) assert(outputTuples.size == bruteForceResult) assert(opExec.onFinish(left).isEmpty) assert(opExec.onFinish(right).isEmpty) if (outputTuples.nonEmpty) assert(outputTuples.head.getSchema.getAttributeNames.length == 4) opExec.close() } it should "random order test" in { val pointList: Array[Long] = (1L to 10L).toArray[Long] val rangeList: Array[Long] = Array(1L, 5L, 8L) testJoin[Long]( "point", "range", includeLeftBound = true, includeRightBound = true, AttributeType.LONG, TimeIntervalType.DAY, 3, pointList, rangeList ) } it should "work with Integer value int [] interval, simple test" in { val pointList: Array[Int] = (1 to 10).toArray[Int] val rangeList: Array[Int] = Array(1, 5, 8) testJoin[Int]( "point", "range", includeLeftBound = true, includeRightBound = true, AttributeType.INTEGER, TimeIntervalType.DAY, 3, pointList, rangeList ) } it should "work with Integer value int [] interval, same key" in { val pointList: Array[Int] = (1 to 10).toArray[Int] val rangeList: Array[Int] = Array(1, 5, 8) testJoin[Int]( "same", "same", includeLeftBound = true, includeRightBound = true, AttributeType.INTEGER, TimeIntervalType.DAY, 3, pointList, rangeList ) } it should "work with Integer value int [) interval" in { val pointList: Array[Int] = (1 to 10).toArray[Int] val rangeList: Array[Int] = Array(1, 5, 8) testJoin[Int]( "point", "range", includeLeftBound = true, includeRightBound = false, AttributeType.INTEGER, TimeIntervalType.DAY, 3, pointList, rangeList ) } it should "work with Integer value int (] interval" in { val pointList: Array[Int] = (1 to 10).toArray[Int] val rangeList: Array[Int] = Array(1, 5, 8) testJoin[Int]( "point", "range", includeLeftBound = false, includeRightBound = true, AttributeType.INTEGER, TimeIntervalType.DAY, 3, pointList, rangeList ) } it should "work with Integer value int () interval" in { val pointList: Array[Int] = (1 to 10).toArray[Int] val rangeList: Array[Int] = Array(1, 5, 8) testJoin[Int]( "point", "range", includeLeftBound = false, includeRightBound = false, AttributeType.INTEGER, TimeIntervalType.DAY, 3, pointList, rangeList ) } it should "work with Timestamp value int [] interval" in { val pointList: Array[Timestamp] = (1L to 10L) .map(i => { new Timestamp(i) }) .toArray[Timestamp] val rangeList: Array[Timestamp] = Array(1, 5, 8).map(i => { new Timestamp(i) }) testJoin[Timestamp]( "point", "range", includeLeftBound = false, includeRightBound = true, AttributeType.TIMESTAMP, TimeIntervalType.DAY, 3, pointList, rangeList ) } it should "work with Double value int [] interval" in { opDesc.leftAttributeName = "point_1" opDesc.rightAttributeName = "range_1" opDesc.includeLeftBound = true opDesc.includeRightBound = true opDesc.constant = 3 opDesc.timeIntervalType = Option(TimeIntervalType.DAY) val opExec = new IntervalJoinOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() counter = 0 val pointList: Array[Double] = Array(1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 8.1, 9.1, 10.1) pointList.foreach(i => { assert( opExec.processTuple(doubleTuple("point", 1, i), left).isEmpty ) }) assert(opExec.onFinish(left).isEmpty) val rangeList: Array[Double] = Array(1.1, 5.1, 8.1) val outputTuples = rangeList .map(i => opExec.processTuple(doubleTuple("range", 1, i), right)) .foldLeft(Iterator[TupleLike]())(_ ++ _) .toList assert(outputTuples.size == 11) assert(outputTuples.head.getFields.length == 4) opExec.close() } it should "work with Long value int [] interval" in { val pointList: Array[Long] = (1L to 10L).toArray val rangeList: Array[Long] = Array(1L, 5L, 8L) testJoin[Long]( "point", "range", includeLeftBound = true, includeRightBound = true, AttributeType.LONG, TimeIntervalType.DAY, 3, pointList, rangeList ) } it should "work with basic two input streams with left empty table" in { val pointList: Array[Long] = Array() val rangeList: Array[Long] = Array(1L, 5L, 8L) testJoin[Long]( "point", "range", includeLeftBound = true, includeRightBound = true, AttributeType.LONG, TimeIntervalType.DAY, 3, pointList, rangeList ) } it should "work with basic two input streams with right empty table" in { val pointList: Array[Long] = (1L to 10L).toArray val rangeList: Array[Long] = Array() testJoin[Long]( "point", "range", includeLeftBound = true, includeRightBound = true, AttributeType.LONG, TimeIntervalType.DAY, 3, pointList, rangeList ) } it should "test larger dataset(1k)" in { val pointList: Array[Long] = LazyList.continually(nextLong()).take(1000).toArray val rangeList: Array[Long] = LazyList.continually(nextLong()).take(1000).toArray testJoin[Long]( "point", "range", includeLeftBound = true, includeRightBound = true, AttributeType.LONG, TimeIntervalType.DAY, nextInt(1000).toLong, pointList, rangeList ) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/keywordSearch/KeywordSearchOpExecSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.keywordSearch import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple} import org.apache.texera.amber.util.JSONUtils.objectMapper import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class KeywordSearchOpExecSpec extends AnyFlatSpec with BeforeAndAfter { val inputPort: Int = 0 val opDesc: KeywordSearchOpDesc = new KeywordSearchOpDesc() val schema: Schema = Schema() .add(new Attribute("text", AttributeType.STRING)) def createTuple(text: String): Tuple = { Tuple .builder(schema) .add(new Attribute("text", AttributeType.STRING), text) .build() } val testData: List[Tuple] = List( createTuple("3 stars"), createTuple("4 stars"), createTuple("Trump"), createTuple("Trump Biden"), createTuple("hello"), createTuple("the name"), createTuple("an eye"), createTuple("to you"), createTuple("Twitter"), createTuple("안녕하세요"), createTuple("你好"), createTuple("_!@,-") ) it should "find exact match with single number" in { opDesc.attribute = "text" opDesc.keyword = "3" val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val results = testData.filter(t => opExec.processTuple(t, inputPort).nonEmpty) assert(results.length == 1) assert(results.head.getField[String]("text") == "3 stars") opExec.close() } it should "find exact phrase match" in { opDesc.attribute = "text" opDesc.keyword = "\"3 stars\"" val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val results = testData.filter(t => opExec.processTuple(t, inputPort).nonEmpty) assert(results.length == 1) assert(results.head.getField[String]("text") == "3 stars") opExec.close() } it should "find all occurrences of Trump" in { opDesc.attribute = "text" opDesc.keyword = "Trump" val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val results = testData.filter(t => opExec.processTuple(t, inputPort).nonEmpty) assert(results.length == 2) assert(results.map(_.getField[String]("text")).toSet == Set("Trump Biden", "Trump")) opExec.close() } it should "find all occurrences of Biden" in { opDesc.attribute = "text" opDesc.keyword = "Biden" val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val results = testData.filter(t => opExec.processTuple(t, inputPort).nonEmpty) assert(results.length == 1) assert(results.head.getField[String]("text") == "Trump Biden") opExec.close() } it should "find records containing both Trump AND Biden" in { opDesc.attribute = "text" opDesc.keyword = "Trump AND Biden" val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val results = testData.filter(t => opExec.processTuple(t, inputPort).nonEmpty) assert(results.length == 1) assert(results.head.getField[String]("text") == "Trump Biden") opExec.close() } it should "find no matches for exact phrase 'Trump AND Biden'" in { opDesc.attribute = "text" opDesc.keyword = "\"Trump AND Biden\"" val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val results = testData.filter(t => opExec.processTuple(t, inputPort).hasNext) assert(results.isEmpty) opExec.close() } it should "find no matches for partial word 'ell'" in { opDesc.attribute = "text" opDesc.keyword = "ell" val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val results = testData.filter(t => opExec.processTuple(t, inputPort).hasNext) assert(results.isEmpty) opExec.close() } it should "find exact match for word 'the'" in { opDesc.attribute = "text" opDesc.keyword = "the" val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val results = testData.filter(t => opExec.processTuple(t, inputPort).hasNext) assert(results.length == 1) assert(results.head.getField[String]("text") == "the name") opExec.close() } it should "find exact match for word 'an'" in { opDesc.attribute = "text" opDesc.keyword = "an" val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val results = testData.filter(t => opExec.processTuple(t, inputPort).hasNext) assert(results.length == 1) assert(results.head.getField[String]("text") == "an eye") opExec.close() } it should "find exact match for word 'to'" in { opDesc.attribute = "text" opDesc.keyword = "to" val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val results = testData.filter(t => opExec.processTuple(t, inputPort).hasNext) assert(results.length == 1) assert(results.head.getField[String]("text") == "to you") opExec.close() } it should "find case-insensitive match for 'twitter'" in { opDesc.attribute = "text" opDesc.keyword = "twitter" val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val results = testData.filter(t => opExec.processTuple(t, inputPort).hasNext) assert(results.length == 1) assert(results.head.getField[String]("text") == "Twitter") opExec.close() } it should "find exact match for Korean text '안녕하세요'" in { opDesc.attribute = "text" opDesc.keyword = "안녕하세요" val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val results = testData.filter(t => opExec.processTuple(t, inputPort).hasNext) assert(results.length == 1) assert(results.head.getField[String]("text") == "안녕하세요") opExec.close() } it should "find exact match for Chinese text '你好'" in { opDesc.attribute = "text" opDesc.keyword = "你好" val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val results = testData.filter(t => opExec.processTuple(t, inputPort).hasNext) assert(results.length == 1) assert(results.head.getField[String]("text") == "你好") opExec.close() } it should "find no matches for special character '@'" in { opDesc.attribute = "text" opDesc.keyword = "@" val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val results = testData.filter(t => opExec.processTuple(t, inputPort).hasNext) assert(results.isEmpty) opExec.close() } it should "find exact match for special characters '_!@,-'" in { opDesc.attribute = "text" opDesc.keyword = "_!@,-" val opExec = new KeywordSearchOpExec(objectMapper.writeValueAsString(opDesc)) opExec.open() val results = testData.filter(t => opExec.processTuple(t, inputPort).hasNext) assert(results.isEmpty) opExec.close() } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/limit/LimitOpExecSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.limit import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple} import org.apache.texera.amber.util.JSONUtils.objectMapper import org.scalatest.flatspec.AnyFlatSpec class LimitOpExecSpec extends AnyFlatSpec { private val schema: Schema = Schema().add(new Attribute("v", AttributeType.INTEGER)) private def tuple(v: Int): Tuple = Tuple.builder(schema).add(new Attribute("v", AttributeType.INTEGER), Integer.valueOf(v)).build() // LogicalOp is registered for polymorphic Jackson deserialization via the // `operatorType` discriminator, so a hand-rolled `{"limit":N}` string would // fail to bind. Serialize a real `LimitOpDesc` instance to get the proper // discriminator embedded. private def desc(limit: Int): String = { val d = new LimitOpDesc() d.limit = limit objectMapper.writeValueAsString(d) } private def newExec(limit: Int): LimitOpExec = { val exec = new LimitOpExec(desc(limit)) exec.open() exec } "LimitOpExec.processTuple" should "emit each input tuple while under the configured limit" in { val exec = newExec(3) val emitted = (0 until 3).flatMap(i => exec.processTuple(tuple(i), 0).toList).toList assert(emitted.map(_.asInstanceOf[Tuple]) == List(tuple(0), tuple(1), tuple(2))) } it should "emit nothing once the limit is reached" in { val exec = newExec(2) exec.processTuple(tuple(0), 0).toList exec.processTuple(tuple(1), 0).toList val third = exec.processTuple(tuple(2), 0).toList val fourth = exec.processTuple(tuple(3), 0).toList assert(third.isEmpty) assert(fourth.isEmpty) } it should "track the count cumulatively across processTuple invocations" in { val exec = newExec(5) val emitted = (0 until 7).flatMap(i => exec.processTuple(tuple(i), 0).toList) assert(emitted.size == 5) assert(exec.count == 5) } it should "emit nothing for limit = 0" in { val exec = newExec(0) val emitted = (0 until 4).flatMap(i => exec.processTuple(tuple(i), 0).toList) assert(emitted.isEmpty) } "LimitOpExec.open" should "reset the count to 0" in { val exec = new LimitOpExec(desc(3)) exec.count = 99 exec.open() assert(exec.count == 0) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/map/MapOpExecSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.map import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple} import org.scalatest.flatspec.AnyFlatSpec class MapOpExecSpec extends AnyFlatSpec { private val vAttr = new Attribute("v", AttributeType.INTEGER) private val schema: Schema = Schema().add(vAttr) // Use the schema's Attribute when adding fields so the helper stays // consistent with the schema under test. private def tuple(v: Int): Tuple = Tuple.builder(schema).add(schema.getAttribute("v"), Integer.valueOf(v)).build() private class TestMap extends MapOpExec "MapOpExec.processTuple" should "emit exactly one tuple per input by applying the configured mapFunc" in { val exec = new TestMap() exec.setMapFunc((t: Tuple) => tuple(t.getField[Int]("v") * 2)) val out = exec.processTuple(tuple(3), 0).toList assert(out == List(tuple(6))) } it should "apply a doubling map function to a stream of tuples" in { val exec = new TestMap() exec.setMapFunc((t: Tuple) => tuple(t.getField[Int]("v") * 2)) val out = (1 to 5).flatMap(v => exec.processTuple(tuple(v), 0).toList) assert(out.map(_.asInstanceOf[Tuple]) == (1 to 5).map(v => tuple(v * 2))) } it should "apply a constant map function regardless of input" in { val exec = new TestMap() exec.setMapFunc((_: Tuple) => tuple(99)) val out = Seq(1, 2, 3).map(v => exec.processTuple(tuple(v), 0).toList.head.asInstanceOf[Tuple]) assert(out.forall(_ == tuple(99))) } it should "apply a stateful map function (closes over an external counter)" in { val exec = new TestMap() var counter = 0 exec.setMapFunc { (t: Tuple) => counter += 1 tuple(t.getField[Int]("v") + counter) } val out = (1 to 3).map(v => exec.processTuple(tuple(v), 0).toList.head.asInstanceOf[Tuple]) // counter goes 1, 2, 3 → outputs 1+1, 2+2, 3+3 assert(out == List(tuple(2), tuple(4), tuple(6))) assert(counter == 3) } it should "support a map function that produces a tuple with a different schema" in { val outSchema = Schema().add(new Attribute("name", AttributeType.STRING)) val exec = new TestMap() exec.setMapFunc { (t: Tuple) => Tuple .builder(outSchema) .add(outSchema.getAttribute("name"), s"v=${t.getField[Int]("v")}") .build() } val out = exec.processTuple(tuple(7), 0).toList assert(out.size == 1) val mapped = out.head.asInstanceOf[Tuple] assert(mapped.getField[String]("name") == "v=7") } it should "return the same instance when mapFunc returns the input tuple" in { val exec = new TestMap() exec.setMapFunc((t: Tuple) => t) val input = tuple(7) val out = exec.processTuple(input, 0).toList assert(out.size == 1) // Reference identity: processTuple should not copy or rebuild the tuple // when mapFunc returns the same instance. assert(out.head eq input) } it should "always wrap the result in a single-element iterator" in { val exec = new TestMap() exec.setMapFunc((_: Tuple) => tuple(0)) val it = exec.processTuple(tuple(99), 0) assert(it.hasNext) it.next() assert(!it.hasNext) } "MapOpExec.setMapFunc" should "overwrite a previously installed function" in { val exec = new TestMap() exec.setMapFunc((_: Tuple) => tuple(1)) exec.setMapFunc((_: Tuple) => tuple(2)) val out = exec.processTuple(tuple(0), 0).toList assert(out == List(tuple(2))) } it should "throw NullPointerException when mapFunc is invoked before setMapFunc" in { val exec = new TestMap() assertThrows[NullPointerException] { exec.processTuple(tuple(0), 0).toList } } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/projection/ProjectionOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.projection import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema} import org.apache.texera.amber.core.workflow.PortIdentity import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class ProjectionOpDescSpec extends AnyFlatSpec with BeforeAndAfter { val schema = new Schema( new Attribute("field1", AttributeType.STRING), new Attribute("field2", AttributeType.INTEGER), new Attribute("field3", AttributeType.BOOLEAN) ) var projectionOpDesc: ProjectionOpDesc = _ before { projectionOpDesc = new ProjectionOpDesc() } it should "take in attribute names" in { projectionOpDesc.attributes ++= List( new AttributeUnit("field1", "f1"), new AttributeUnit("fields2", "f2") ) assert(projectionOpDesc.attributes.length == 2) } it should "filter schema correctly" in { projectionOpDesc.attributes ++= List( new AttributeUnit("field1", "f1"), new AttributeUnit("field2", "f2") ) val outputSchema = projectionOpDesc.getExternalOutputSchemas(Map(PortIdentity() -> schema)).values.head assert(outputSchema.getAttributes.length == 2) } it should "reorder schema" in { projectionOpDesc.attributes ++= List( new AttributeUnit("field2", "f2"), new AttributeUnit("field1", "f1") ) val outputSchema = projectionOpDesc.getExternalOutputSchemas(Map(PortIdentity() -> schema)).values.head assert(outputSchema.getAttributes.length == 2) assert(outputSchema.getIndex("f2") == 0) assert(outputSchema.getIndex("f1") == 1) } it should "raise RuntimeException on non-existing fields" in { projectionOpDesc.attributes ++= List( new AttributeUnit("field---5", "f5"), new AttributeUnit("field---6", "f6") ) assertThrows[RuntimeException] { projectionOpDesc.getExternalOutputSchemas(Map(PortIdentity() -> schema)).values.head } } it should "raise IllegalArgumentException on empty attributes" in { assertThrows[IllegalArgumentException] { projectionOpDesc.getExternalOutputSchemas(Map(PortIdentity() -> schema)).values.head } } it should "raise RuntimeException on duplicate alias" in { projectionOpDesc.attributes ++= List( new AttributeUnit("field2", "f"), new AttributeUnit("field1", "f") ) assertThrows[RuntimeException] { projectionOpDesc.getExternalOutputSchemas(Map(PortIdentity() -> schema)).values.head } } it should "allow alias to be optional" in { projectionOpDesc.attributes ++= List( new AttributeUnit("field1", "f1"), new AttributeUnit("field2", "") ) val outputSchema = projectionOpDesc.getExternalOutputSchemas(Map(PortIdentity() -> schema)).values.head assert(outputSchema.getAttributes.length == 2) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/projection/ProjectionOpExecSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.projection import org.apache.texera.amber.core.tuple._ import org.apache.texera.amber.util.JSONUtils.objectMapper import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class ProjectionOpExecSpec extends AnyFlatSpec with BeforeAndAfter { val tupleSchema: Schema = Schema() .add(new Attribute("field1", AttributeType.STRING)) .add(new Attribute("field2", AttributeType.INTEGER)) .add(new Attribute("field3", AttributeType.BOOLEAN)) val tuple: Tuple = Tuple .builder(tupleSchema) .add(new Attribute("field1", AttributeType.STRING), "hello") .add(new Attribute("field2", AttributeType.INTEGER), 1) .add( new Attribute("field3", AttributeType.BOOLEAN), true ) .build() val opDesc: ProjectionOpDesc = new ProjectionOpDesc() it should "open" in { opDesc.attributes = List( new AttributeUnit("field2", "f2"), new AttributeUnit("field1", "f1") ) val projectionOpExec = new ProjectionOpExec(objectMapper.writeValueAsString(opDesc)) projectionOpExec.open() } it should "process Tuple" in { opDesc.attributes = List( new AttributeUnit("field2", "f2"), new AttributeUnit("field1", "f1") ) val outputSchema = Schema() .add(new Attribute("f1", AttributeType.STRING)) .add(new Attribute("f2", AttributeType.INTEGER)) val projectionOpExec = new ProjectionOpExec(objectMapper.writeValueAsString(opDesc)) projectionOpExec.open() val outputTuple = projectionOpExec .processTuple(tuple, 0) .next() .asInstanceOf[SchemaEnforceable] .enforceSchema(outputSchema) assert(outputTuple.length == 2) assert(outputTuple.getField("f1").asInstanceOf[String] == "hello") assert(outputTuple.getField("f2").asInstanceOf[Int] == 1) assert(outputTuple.getField[String](0) == "hello") assert(outputTuple.getField[Int](1) == 1) } it should "process Tuple with different order" in { opDesc.attributes = List( new AttributeUnit("field3", "f3"), new AttributeUnit("field1", "f1") ) val outputSchema = Schema() .add(new Attribute("f3", AttributeType.BOOLEAN)) .add(new Attribute("f1", AttributeType.STRING)) val projectionOpExec = new ProjectionOpExec(objectMapper.writeValueAsString(opDesc)) projectionOpExec.open() val outputTuple = projectionOpExec .processTuple(tuple, 0) .next() .asInstanceOf[SchemaEnforceable] .enforceSchema(outputSchema) assert(outputTuple.length == 2) assert(outputTuple.getField("f3").asInstanceOf[Boolean]) assert(outputTuple.getField("f1").asInstanceOf[String] == "hello") assert(outputTuple.getField[Boolean](0)) assert(outputTuple.getField[String](1) == "hello") } it should "meException on non-existing fields" in { opDesc.attributes = List( new AttributeUnit("field---5", "f5"), new AttributeUnit("field---6", "f6") ) val projectionOpExec = new ProjectionOpExec(objectMapper.writeValueAsString(opDesc)) assertThrows[RuntimeException] { projectionOpExec.processTuple(tuple, 0).next() } } it should "raise IllegalArgumentException on empty attributes" in { opDesc.attributes = List() val projectionOpExec = new ProjectionOpExec(objectMapper.writeValueAsString(opDesc)) assertThrows[IllegalArgumentException] { projectionOpExec.processTuple(tuple, 0).next() } } it should "raise RuntimeException on duplicate alias" in { opDesc.attributes = List( new AttributeUnit("field1", "f"), new AttributeUnit("field2", "f") ) val projectionOpExec = new ProjectionOpExec(objectMapper.writeValueAsString(opDesc)) assertThrows[RuntimeException] { projectionOpExec.processTuple(tuple, 0).next() } } it should "allow empty alias" in { opDesc.attributes = List( new AttributeUnit("field2", "f2"), new AttributeUnit("field1", "") ) val outputSchema = Schema() .add(new Attribute("field1", AttributeType.STRING)) .add(new Attribute("f2", AttributeType.INTEGER)) val projectionOpExec = new ProjectionOpExec(objectMapper.writeValueAsString(opDesc)) projectionOpExec.open() val outputTuple = projectionOpExec .processTuple(tuple, 0) .next() .asInstanceOf[SchemaEnforceable] .enforceSchema(outputSchema) assert(outputTuple.length == 2) assert(outputTuple.getField("field1").asInstanceOf[String] == "hello") assert(outputTuple.getField("f2").asInstanceOf[Int] == 1) assert(outputTuple.getField[String](0) == "hello") assert(outputTuple.getField[Int](1) == 1) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/sink/ProgressiveUtilsSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sink import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple} import org.scalatest.flatspec.AnyFlatSpec class ProgressiveUtilsSpec extends AnyFlatSpec { // --- helpers --------------------------------------------------------------- private val baseSchema: Schema = new Schema( new Attribute("id", AttributeType.INTEGER), new Attribute("name", AttributeType.STRING) ) // outputSchema = flag column prepended to baseSchema private val outputSchema: Schema = new Schema( ProgressiveUtils.insertRetractFlagAttr, new Attribute("id", AttributeType.INTEGER), new Attribute("name", AttributeType.STRING) ) private def baseTuple(id: Int, name: String): Tuple = Tuple .builder(baseSchema) .add(new Attribute("id", AttributeType.INTEGER), Int.box(id)) .add(new Attribute("name", AttributeType.STRING), name) .build() // --- insertRetractFlagAttr ------------------------------------------------- "ProgressiveUtils.insertRetractFlagAttr" should "be a BOOLEAN attribute named __internal_is_insertion" in { val attr = ProgressiveUtils.insertRetractFlagAttr assert(attr.getName == "__internal_is_insertion") assert(attr.getType == AttributeType.BOOLEAN) } // --- addInsertionFlag / addRetractionFlag ---------------------------------- "ProgressiveUtils.addInsertionFlag" should "prepend the flag column with value true" in { val flagged = ProgressiveUtils.addInsertionFlag(baseTuple(1, "alice"), outputSchema) assert(flagged.getSchema == outputSchema) assert(flagged.getField[Boolean](ProgressiveUtils.insertRetractFlagAttr.getName) == true) assert(flagged.getField[Integer]("id") == 1) assert(flagged.getField[String]("name") == "alice") } "ProgressiveUtils.addRetractionFlag" should "prepend the flag column with value false" in { val flagged = ProgressiveUtils.addRetractionFlag(baseTuple(2, "bob"), outputSchema) assert(flagged.getField[Boolean](ProgressiveUtils.insertRetractFlagAttr.getName) == false) assert(flagged.getField[Integer]("id") == 2) assert(flagged.getField[String]("name") == "bob") } it should "fail an assertion if addRetractionFlag is called on an already-flagged tuple" in { val alreadyFlagged = ProgressiveUtils.addInsertionFlag(baseTuple(3, "x"), outputSchema) intercept[AssertionError] { ProgressiveUtils.addRetractionFlag(alreadyFlagged, outputSchema) } } it should "fail an assertion if addInsertionFlag is called on an already-flagged tuple" in { // Symmetric guard: both addInsertionFlag and addRetractionFlag carry the // same `assert(!containsAttribute(flagAttr))` precondition, and either // one may be called on already-flagged data, so each path should fail. val alreadyFlagged = ProgressiveUtils.addRetractionFlag(baseTuple(4, "y"), outputSchema) intercept[AssertionError] { ProgressiveUtils.addInsertionFlag(alreadyFlagged, outputSchema) } } // --- isInsertion ----------------------------------------------------------- "ProgressiveUtils.isInsertion" should "return true for an unflagged tuple" in { // Tuples without the flag column default to insertion (the unflagged // default in the engine is "+"). assert(ProgressiveUtils.isInsertion(baseTuple(1, "x"))) } it should "return true when the flag column is present and true" in { val flagged = ProgressiveUtils.addInsertionFlag(baseTuple(1, "x"), outputSchema) assert(ProgressiveUtils.isInsertion(flagged)) } it should "return false when the flag column is present and false" in { val flagged = ProgressiveUtils.addRetractionFlag(baseTuple(1, "x"), outputSchema) assert(!ProgressiveUtils.isInsertion(flagged)) } // --- getTupleFlagAndValue -------------------------------------------------- "ProgressiveUtils.getTupleFlagAndValue" should "split an insertion-flagged tuple into (true, base tuple)" in { val flagged = ProgressiveUtils.addInsertionFlag(baseTuple(1, "alice"), outputSchema) val (flag, stripped) = ProgressiveUtils.getTupleFlagAndValue(flagged) assert(flag) // Full schema equality (names + types + order) — name-only would let a // type drift on the payload columns slip through. assert(stripped.getSchema == baseSchema) assert(stripped.getField[Integer]("id") == 1) assert(stripped.getField[String]("name") == "alice") } it should "split a retraction-flagged tuple into (false, base tuple)" in { val flagged = ProgressiveUtils.addRetractionFlag(baseTuple(2, "bob"), outputSchema) val (flag, stripped) = ProgressiveUtils.getTupleFlagAndValue(flagged) assert(!flag) assert(stripped.getSchema == baseSchema) assert(stripped.getField[Integer]("id") == 2) assert(stripped.getField[String]("name") == "bob") } it should "treat an unflagged tuple as insertion and pass an equivalent schema through unchanged" in { // For a tuple that doesn't carry the flag column, isInsertion returns // true and getPartialSchema returns an equivalent Schema — same attributes // in the same order (filterNot removes nothing). Note that // Schema.getPartialSchema constructs a new instance every time, so this // is structural equality, not reference identity. val raw = baseTuple(3, "carol") val (flag, stripped) = ProgressiveUtils.getTupleFlagAndValue(raw) assert(flag) assert(stripped.getSchema == raw.getSchema) assert(stripped.getField[Integer]("id") == 3) assert(stripped.getField[String]("name") == "carol") } // --- typed payload round-trips -------------------------------------------- // Nothing in `addInsertionFlag` / `getTupleFlagAndValue` is type-specific — // they only care about the BOOLEAN flag column they prepend / strip — but // it is worth pinning that arbitrary AttributeType payload columns survive // the flag → strip → unflag round-trip across the engine's value types. private def flagRoundTrip(payloadAttr: Attribute, payloadValue: AnyRef): (Boolean, AnyRef) = { val payloadSchema = new Schema(payloadAttr) val flaggedSchema = new Schema(ProgressiveUtils.insertRetractFlagAttr, payloadAttr) val raw = Tuple.builder(payloadSchema).add(payloadAttr, payloadValue).build() val flagged = ProgressiveUtils.addInsertionFlag(raw, flaggedSchema) val (flag, stripped) = ProgressiveUtils.getTupleFlagAndValue(flagged) (flag, stripped.getField[AnyRef](payloadAttr.getName)) } "Flag round-trip" should "preserve INTEGER payload values" in { val (flag, value) = flagRoundTrip(new Attribute("v", AttributeType.INTEGER), Int.box(42)) assert(flag) assert(value == Int.box(42)) } it should "preserve LONG payload values" in { val (flag, value) = flagRoundTrip(new Attribute("v", AttributeType.LONG), Long.box(9876543210L)) assert(flag) assert(value == Long.box(9876543210L)) } it should "preserve DOUBLE payload values" in { val (flag, value) = flagRoundTrip(new Attribute("v", AttributeType.DOUBLE), Double.box(3.14159)) assert(flag) assert(value == Double.box(3.14159)) } it should "preserve BOOLEAN payload values (distinct from the flag column)" in { // The flag column is also BOOLEAN; this verifies the implementation // selects the correct attribute by name, not by type. val (flag, value) = flagRoundTrip(new Attribute("active", AttributeType.BOOLEAN), Boolean.box(false)) assert(flag, "outer flag must still be insertion") assert(value == Boolean.box(false), "inner BOOLEAN payload must be preserved") } it should "preserve TIMESTAMP payload values" in { val ts = new java.sql.Timestamp(1_700_000_000_000L) val (flag, value) = flagRoundTrip(new Attribute("ts", AttributeType.TIMESTAMP), ts) assert(flag) assert(value == ts) } it should "preserve BINARY payload values" in { val bytes = Array[Byte](0, 1, 2, 3, -1) val (flag, value) = flagRoundTrip(new Attribute("blob", AttributeType.BINARY), bytes) assert(flag) // Use value-based equality (the Tuple contract elsewhere uses // `sameElements` for Array[Byte]); requiring the *same* array instance // would over-constrain the flag/strip path against future copy-on-write // changes. assert(value.asInstanceOf[Array[Byte]].sameElements(bytes)) } it should "preserve null payload values for every AttributeType" in { // Cover every member of `AttributeType` (Java enum). Avoid hand-listing — // a future addition to the enum would still be tested. AttributeType.values.foreach { tpe => val attr = new Attribute(s"v_${tpe.name().toLowerCase}", tpe) val (flag, value) = flagRoundTrip(attr, null) assert(flag) assert(value == null, s"null payload must survive round-trip for $tpe") } } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/sklearn/SklearnOpDescRegistrySpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sklearn import org.apache.texera.amber.operator.sklearn.training._ import org.apache.texera.amber.pybuilder.PythonReflectionUtils import org.scalatest.flatspec.AnyFlatSpec /** * Pins the wiring (Python import statement + user-friendly model name) for * every concrete `SklearnClassifierOpDesc` and `SklearnTrainingOpDesc`. A * typo in either string would silently misroute downstream UI labels and * cause breakage in the generated Python pipeline. */ class SklearnOpDescRegistrySpec extends AnyFlatSpec { // --------------------------------------------------------------------------- // Classifier registry (25 concrete SklearnClassifierOpDesc subclasses) // --------------------------------------------------------------------------- private val classifierEntries: List[(SklearnClassifierOpDesc, String, String)] = List( ( new SklearnAdaptiveBoostingOpDesc(), "from sklearn.ensemble import AdaBoostClassifier", "Adaptive Boosting" ), (new SklearnBaggingOpDesc(), "from sklearn.ensemble import BaggingClassifier", "Bagging"), ( new SklearnBernoulliNaiveBayesOpDesc(), "from sklearn.naive_bayes import BernoulliNB", "Bernoulli Naive Bayes" ), ( new SklearnComplementNaiveBayesOpDesc(), "from sklearn.naive_bayes import ComplementNB", "Complement Naive Bayes" ), ( new SklearnDummyClassifierOpDesc(), "from sklearn.dummy import DummyClassifier", "Dummy Classifier" ), ( new SklearnDecisionTreeOpDesc(), "from sklearn.tree import DecisionTreeClassifier", "Decision Tree" ), (new SklearnExtraTreeOpDesc(), "from sklearn.tree import ExtraTreeClassifier", "Extra Tree"), ( new SklearnExtraTreesOpDesc(), "from sklearn.ensemble import ExtraTreesClassifier", "Extra Trees" ), ( new SklearnGaussianNaiveBayesOpDesc(), "from sklearn.naive_bayes import GaussianNB", "Gaussian Naive Bayes" ), ( new SklearnGradientBoostingOpDesc(), "from sklearn.ensemble import GradientBoostingClassifier", "Gradient Boosting" ), ( new SklearnKNNOpDesc(), "from sklearn.neighbors import KNeighborsClassifier", "K-nearest Neighbors" ), ( new SklearnLinearSVMOpDesc(), "from sklearn.svm import LinearSVC", "Linear Support Vector Machine" ), ( new SklearnLogisticRegressionCVOpDesc(), "from sklearn.linear_model import LogisticRegressionCV", "Logistic Regression Cross Validation" ), ( new SklearnLogisticRegressionOpDesc(), "from sklearn.linear_model import LogisticRegression", "Logistic Regression" ), ( new SklearnMultiLayerPerceptronOpDesc(), "from sklearn.neural_network import MLPClassifier", "Multi-layer Perceptron" ), ( new SklearnMultinomialNaiveBayesOpDesc(), "from sklearn.naive_bayes import MultinomialNB", "Multinomial Naive Bayes" ), ( new SklearnNearestCentroidOpDesc(), "from sklearn.neighbors import NearestCentroid", "Nearest Centroid" ), ( new SklearnPassiveAggressiveOpDesc(), "from sklearn.linear_model import PassiveAggressiveClassifier", "Passive Aggressive" ), ( new SklearnPerceptronOpDesc(), "from sklearn.linear_model import Perceptron", "Linear Perceptron" ), ( new SklearnProbabilityCalibrationOpDesc(), "from sklearn.calibration import CalibratedClassifierCV", "Probability Calibration" ), ( new SklearnRandomForestOpDesc(), "from sklearn.ensemble import RandomForestClassifier", "Random Forest" ), ( new SklearnRidgeCVOpDesc(), "from sklearn.linear_model import RidgeClassifierCV", "Ridge Regression Cross Validation" ), ( new SklearnRidgeOpDesc(), "from sklearn.linear_model import RidgeClassifier", "Ridge Regression" ), ( new SklearnSDGOpDesc(), "from sklearn.linear_model import SGDClassifier", "Stochastic Gradient Descent" ), (new SklearnSVMOpDesc(), "from sklearn.svm import SVC", "Support Vector Machine") ) classifierEntries.foreach { case (desc, expectedImport, expectedName) => val cls = desc.getClass.getSimpleName cls should s"return import statement '$expectedImport'" in { assert(desc.getImportStatements == expectedImport) } it should s"return user-friendly model name '$expectedName'" in { assert(desc.getUserFriendlyModelName == expectedName) } } "SklearnClassifierOpDesc" should "embed the import statement into generatePythonCode for a concrete subclass" in { val desc = new SklearnLogisticRegressionOpDesc() desc.target = "y" desc.countVectorizer = false // `tfidfTransformer` is a val on the base class, defaults to false. val code = desc.generatePythonCode() assert(code.contains("from sklearn.linear_model import LogisticRegression")) // Classifier OpDescs emit a UDFTableOperator pipeline. assert(code.contains("ProcessTableOperator")) } // NOTE: the abstract base class's empty-string defaults are NOT tested here. // Instantiating `SklearnClassifierOpDesc` from this spec (e.g. via // `new SklearnClassifierOpDesc {}`) creates an anonymous test-package class // under `org.apache.texera.amber.operator.sklearn`, which the // PythonCodeRawInvalidTextSpec classpath scan then picks up as a descriptor // candidate and fails on (anonymous classes have no accessible no-arg // constructor). Every concrete subclass below overrides both methods, so // the base default is never observable in production anyway. // --------------------------------------------------------------------------- // Training registry (26 concrete SklearnTrainingOpDesc subclasses) // --------------------------------------------------------------------------- private val trainingEntries: List[(SklearnTrainingOpDesc, String, String)] = List( ( new SklearnTrainingAdaptiveBoostingOpDesc(), "from sklearn.ensemble import AdaBoostClassifier", "Training: Adaptive Boosting" ), ( new SklearnTrainingBaggingOpDesc(), "from sklearn.ensemble import BaggingClassifier", "Training: Bagging" ), ( new SklearnTrainingBernoulliNaiveBayesOpDesc(), "from sklearn.naive_bayes import BernoulliNB", "Training: Bernoulli Naive Bayes" ), ( new SklearnTrainingComplementNaiveBayesOpDesc(), "from sklearn.naive_bayes import ComplementNB", "Training: Complement Naive Bayes" ), ( new SklearnTrainingDecisionTreeOpDesc(), "from sklearn.tree import DecisionTreeClassifier", "Training: Decision Tree" ), ( new SklearnTrainingDummyClassifierOpDesc(), "from sklearn.dummy import DummyClassifier", "Training: Dummy Classifier" ), ( new SklearnTrainingExtraTreeOpDesc(), "from sklearn.tree import ExtraTreeClassifier", "Training: Extra Tree" ), ( new SklearnTrainingExtraTreesOpDesc(), "from sklearn.ensemble import ExtraTreesClassifier", "Training: Extra Trees" ), ( new SklearnTrainingGaussianNaiveBayesOpDesc(), "from sklearn.naive_bayes import GaussianNB", "Training: Gaussian Naive Bayes" ), ( new SklearnTrainingGradientBoostingOpDesc(), "from sklearn.ensemble import GradientBoostingClassifier", "Training: Gradient Boosting" ), ( new SklearnTrainingKNNOpDesc(), "from sklearn.neighbors import KNeighborsClassifier", "Training: K-nearest Neighbors" ), ( new SklearnTrainingLinearSVMOpDesc(), "from sklearn.svm import LinearSVC", "Training: Linear Support Vector Machine" ), ( new SklearnTrainingLogisticRegressionCVOpDesc(), "from sklearn.linear_model import LogisticRegressionCV", "Training: Logistic Regression Cross Validation" ), ( new SklearnTrainingLogisticRegressionOpDesc(), "from sklearn.linear_model import LogisticRegression", "Training: Logistic Regression" ), ( new SklearnTrainingMultiLayerPerceptronOpDesc(), "from sklearn.neural_network import MLPClassifier", "Training: Multi-layer Perceptron" ), ( new SklearnTrainingMultinomialNaiveBayesOpDesc(), "from sklearn.naive_bayes import MultinomialNB", "Training: Multinomial Naive Bayes" ), ( new SklearnTrainingNearestCentroidOpDesc(), "from sklearn.neighbors import NearestCentroid", "Training: Nearest Centroid" ), ( new SklearnTrainingPassiveAggressiveOpDesc(), "from sklearn.linear_model import PassiveAggressiveClassifier", "Training: Passive Aggressive" ), ( new SklearnTrainingPerceptronOpDesc(), "from sklearn.linear_model import Perceptron", "Training: Linear Perceptron" ), ( new SklearnTrainingProbabilityCalibrationOpDesc(), "from sklearn.calibration import CalibratedClassifierCV", "Training: Probability Calibration" ), ( new SklearnTrainingRandomForestOpDesc(), "from sklearn.ensemble import RandomForestClassifier", "Training: Random Forest" ), ( new SklearnTrainingRidgeCVOpDesc(), "from sklearn.linear_model import RidgeClassifierCV", "Training: Ridge Regression Cross Validation" ), ( new SklearnTrainingRidgeOpDesc(), "from sklearn.linear_model import RidgeClassifier", "Training: Ridge Regression" ), ( new SklearnTrainingSDGOpDesc(), "from sklearn.linear_model import SGDClassifier", "Training: Stochastic Gradient Descent" ), ( new SklearnTrainingSVMOpDesc(), "from sklearn.svm import SVC", "Training: Support Vector Machine" ), ( new SklearnTrainingLinearRegressionOpDesc(), "from sklearn.linear_model import LinearRegression", "Training: Linear Regression" ) ) trainingEntries.foreach { case (desc, expectedImport, expectedName) => val cls = desc.getClass.getSimpleName cls should s"return import statement '$expectedImport'" in { assert(desc.getImportStatements == expectedImport) } it should s"return user-friendly model name '$expectedName'" in { assert(desc.getUserFriendlyModelName == expectedName) } } "SklearnTrainingOpDesc default" should "use the RandomForest defaults until a subclass overrides" in { val base = new SklearnTrainingOpDesc() assert(base.getImportStatements == "from sklearn.ensemble import RandomForestClassifier") assert(base.getUserFriendlyModelName == "RandomForest Training") } it should "embed the import statement into generatePythonCode for a concrete subclass" in { val desc = new SklearnTrainingLogisticRegressionOpDesc() desc.target = "y" desc.countVectorizer = false desc.tfidfTransformer = false val code = desc.generatePythonCode() assert(code.contains("from sklearn.linear_model import LogisticRegression")) assert(code.contains("ProcessTableOperator")) } // --------------------------------------------------------------------------- // Completeness — guard against a new subclass silently bypassing this spec // --------------------------------------------------------------------------- // // Reuse the same classpath scanner that PythonCodeRawInvalidTextSpec uses, // so the two suites agree on what counts as a "concrete" descriptor. private def scanConcrete[T](base: Class[T], pkg: String): Set[Class[_]] = PythonReflectionUtils .scanCandidates( base = base, acceptPackages = Seq(pkg), classLoader = Thread.currentThread().getContextClassLoader ) .toSet "classifierEntries" should "cover every concrete SklearnClassifierOpDesc subclass on the classpath" in { val scanned = scanConcrete(classOf[SklearnClassifierOpDesc], "org.apache.texera.amber.operator.sklearn") val tested = classifierEntries.map(_._1.getClass).toSet[Class[_]] val missing = scanned.diff(tested) val extra = tested.diff(scanned) assert( missing.isEmpty && extra.isEmpty, s"classifierEntries drift — missing on classpath: ${missing .map(_.getName)}, no longer concrete: ${extra.map(_.getName)}" ) } "trainingEntries" should "cover every concrete SklearnTrainingOpDesc subclass on the classpath" in { val scanned = scanConcrete( classOf[SklearnTrainingOpDesc], "org.apache.texera.amber.operator.sklearn.training" ) // SklearnTrainingOpDesc is itself concrete (used as a default fallback), // so the scan picks it up alongside the real subclasses. Exclude it from // the "concrete subclasses" comparison since it is not part of the // registry being pinned. val concreteSubclasses = scanned - classOf[SklearnTrainingOpDesc] val tested = trainingEntries.map(_._1.getClass).toSet[Class[_]] val missing = concreteSubclasses.diff(tested) val extra = tested.diff(concreteSubclasses) assert( missing.isEmpty && extra.isEmpty, s"trainingEntries drift — missing on classpath: ${missing .map(_.getName)}, no longer concrete: ${extra.map(_.getName)}" ) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/sleep/SleepOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sleep import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.operator.metadata.OperatorGroupConstants import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers class SleepOpDescSpec extends AnyFlatSpec with Matchers { private val workflowId = WorkflowIdentity(1L) private val executionId = ExecutionIdentity(1L) "SleepOpDesc.operatorInfo" should "advertise the user-friendly name and Control group" in { val info = (new SleepOpDesc).operatorInfo info.userFriendlyName shouldBe "Sleep" info.operatorGroupName shouldBe OperatorGroupConstants.CONTROL_GROUP info.operatorDescription should include("Sleep") } it should "expose exactly one input port and one output port" in { val info = (new SleepOpDesc).operatorInfo info.inputPorts should have length 1 info.outputPorts should have length 1 } "SleepOpDesc.getPhysicalOp" should "produce a non-parallelizable PhysicalOp pinned to a single worker" in { // Sleep is non-parallelizable on purpose: tuples must traverse the // sleep path serially so the delay is observable as a back-pressure // signal upstream. The descriptor pins both flags explicitly. val op = new SleepOpDesc op.sleepTime = 5 val physical = op.getPhysicalOp(workflowId, executionId) physical.parallelizable shouldBe false physical.suggestedWorkerNum shouldBe Some(1) } it should "wire the SleepOpExec class name into the OpExecInitInfo" in { // The descriptor's getPhysicalOp encodes a fully-qualified Exec class // name; pin it so a rename of SleepOpExec breaks this spec deliberately. // Pattern-match on OpExecWithClassName instead of substring-matching the // toString output, which is brittle to scalapb formatting changes. val op = new SleepOpDesc op.sleepTime = 1 val physical = op.getPhysicalOp(workflowId, executionId) physical.opExecInitInfo match { case OpExecWithClassName(className, descString) => className shouldBe "org.apache.texera.amber.operator.sleep.SleepOpExec" descString should not be empty case other => fail(s"expected OpExecWithClassName, got $other") } } it should "carry forward the operatorInfo input/output ports onto the PhysicalOp" in { val op = new SleepOpDesc op.sleepTime = 1 val physical = op.getPhysicalOp(workflowId, executionId) physical.inputPorts.size shouldBe op.operatorInfo.inputPorts.size physical.outputPorts.size shouldBe op.operatorInfo.outputPorts.size } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/sort/StableMergeSortOpExecSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sort import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema, Tuple} import org.apache.texera.amber.util.JSONUtils.objectMapper import org.scalatest.flatspec.AnyFlatSpec import java.sql.Timestamp import scala.collection.mutable.{ArrayBuffer, ListBuffer} import scala.jdk.CollectionConverters.IterableHasAsJava /** * Integration and internal-behavior tests for [[StableMergeSortOpExec]]. * * Scope & coverage: * - Single-key semantics across core types (BOOLEAN, INTEGER/LONG, DOUBLE, STRING, TIMESTAMP). * - Multi-key lexicographic ordering (mixed directions/types) with null/NaN handling. * - Stability guarantees (relative order preserved for equal keys) and pass-through when no keys. * - Incremental “bucket stack” invariants (binary-carry sizes; no adjacent equal sizes). * - Operational properties (buffering behavior, idempotent onFinish, scale sanity). * - Test hooks for internal merge logic (mergeSortedBuckets, pushBucketAndCombine). * * Null policy: * - Nulls are always ordered last, regardless of ASC/DESC. * - NaN participates as a non-null floating value per Double comparison semantics. * * Notes: * - Some tests rely on package-visible test hooks to validate internals deterministically. */ class StableMergeSortOpExecSpec extends AnyFlatSpec { // =========================================================================== // Helpers // =========================================================================== /** Build a Schema with (name, type) pairs, in-order. */ private def schemaOf(attributes: (String, AttributeType)*): Schema = { attributes.foldLeft(Schema()) { case (acc, (name, attrType)) => acc.add(new Attribute(name, attrType)) } } /** * Construct a Tuple for the provided schema. * * @param values map-like varargs: "colName" -> value. Must provide every column. * @throws java.util.NoSuchElementException if a provided key is not in the schema. */ private def tupleOf(schema: Schema, values: (String, Any)*): Tuple = { val valueMap = values.toMap val builder = Tuple.builder(schema) schema.getAttributeNames.asJava.forEach { name => builder.add(schema.getAttribute(name), valueMap(name)) } builder.build() } /** Convenience builder for a single sort key with direction (ASC by default). */ private def sortKey( attribute: String, pref: SortPreference = SortPreference.ASC ): SortCriteriaUnit = { val criteria = new SortCriteriaUnit() criteria.attributeName = attribute criteria.sortPreference = pref criteria } /** Convert varargs keys into the operator config buffer. */ private def sortKeysBuffer(keys: SortCriteriaUnit*): ListBuffer[SortCriteriaUnit] = ListBuffer(keys: _*) /** * Run the operator on an in-memory sequence of tuples and capture all output. * Output is only emitted at onFinish to preserve determinism. */ private def runStableMergeSort( schema: Schema, tuples: Seq[Tuple] )(configure: StableMergeSortOpDesc => Unit): List[Tuple] = { val desc = new StableMergeSortOpDesc() configure(desc) val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc)) exec.open() tuples.foreach(t => exec.processTuple(t, 0)) val result = exec.onFinish(0).map(_.asInstanceOf[Tuple]).toList exec.close() result } /** Internal test hook to read the current bucket sizes on the stack. */ private def getBucketSizes(exec: StableMergeSortOpExec): List[Int] = exec.debugBucketSizes /** Decompose an integer into its set-bit powers of two (ascending). * Used to check the binary-carry invariant. */ private def binaryDecomposition(number: Int): List[Int] = { var remaining = number val powers = scala.collection.mutable.ListBuffer[Int]() while (remaining > 0) { val lowestSetBit = Integer.lowestOneBit(remaining) powers += lowestSetBit remaining -= lowestSetBit } powers.toList } // =========================================================================== // A. Single-key semantics // =========================================================================== "StableMergeSortOpExec" should "sort integers ascending and preserve duplicate order" in { val schema = schemaOf("value" -> AttributeType.INTEGER, "label" -> AttributeType.STRING) val tuples = List( tupleOf(schema, "value" -> 3, "label" -> "a"), tupleOf(schema, "value" -> 1, "label" -> "first-1"), tupleOf(schema, "value" -> 2, "label" -> "b"), tupleOf(schema, "value" -> 1, "label" -> "first-2"), tupleOf(schema, "value" -> 3, "label" -> "c") ) val result = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey("value")) } assert(result.map(_.getField[Int]("value")) == List(1, 1, 2, 3, 3)) val labelsForOnes = result.filter(_.getField[Int]("value") == 1).map(_.getField[String]("label")) assert(labelsForOnes == List("first-1", "first-2")) } it should "sort integers descending while preserving stability" in { val schema = schemaOf("value" -> AttributeType.INTEGER, "label" -> AttributeType.STRING) val tuples = List( tupleOf(schema, "value" -> 2, "label" -> "first"), tupleOf(schema, "value" -> 2, "label" -> "second"), tupleOf(schema, "value" -> 1, "label" -> "third"), tupleOf(schema, "value" -> 3, "label" -> "fourth") ) val result = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey("value", SortPreference.DESC)) } assert(result.map(_.getField[Int]("value")) == List(3, 2, 2, 1)) val labelsForTwos = result.filter(_.getField[Int]("value") == 2).map(_.getField[String]("label")) assert(labelsForTwos == List("first", "second")) } it should "handle string ordering (case-sensitive)" in { val schema = schemaOf("name" -> AttributeType.STRING) val tuples = List( tupleOf(schema, "name" -> "apple"), tupleOf(schema, "name" -> "Banana"), tupleOf(schema, "name" -> "banana"), tupleOf(schema, "name" -> "APPLE") ) val sorted = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey("name", SortPreference.ASC)) } assert(sorted.map(_.getField[String]("name")) == List("APPLE", "Banana", "apple", "banana")) } it should "order ASCII strings by Java compareTo (punctuation < digits < uppercase < lowercase)" in { val schema = schemaOf("str" -> AttributeType.STRING) val tuples = List("a", "A", "0", "~", "!").map(s => tupleOf(schema, "str" -> s)) val result = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey("str")) } assert(result.map(_.getField[String]("str")) == List("!", "0", "A", "a", "~")) } it should "sort negatives and zeros correctly" in { val schema = schemaOf("value" -> AttributeType.INTEGER) val tuples = List(0, -1, -10, 5, -3, 2).map(v => tupleOf(schema, "value" -> v)) val result = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey("value")) } assert(result.map(_.getField[Int]("value")) == List(-10, -3, -1, 0, 2, 5)) } it should "sort LONG values ascending" in { val schema = schemaOf("id" -> AttributeType.LONG) val tuples = List(5L, 1L, 3L, 9L, 0L).map(v => tupleOf(schema, "id" -> v)) val result = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey("id")) } assert(result.map(_.getField[Long]("id")) == List(0L, 1L, 3L, 5L, 9L)) } it should "sort TIMESTAMP ascending" in { val schema = schemaOf("timestamp" -> AttributeType.TIMESTAMP) val base = Timestamp.valueOf("2022-01-01 00:00:00") val tuples = List( new Timestamp(base.getTime + 4000), new Timestamp(base.getTime + 1000), new Timestamp(base.getTime + 3000), new Timestamp(base.getTime + 2000) ).map(ts => tupleOf(schema, "timestamp" -> ts)) val result = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey("timestamp", SortPreference.ASC)) } val times = result.map(_.getField[Timestamp]("timestamp").getTime) assert(times == times.sorted) } it should "sort TIMESTAMP descending" in { val schema = schemaOf("timestamp" -> AttributeType.TIMESTAMP) val base = Timestamp.valueOf("2023-01-01 00:00:00") val tuples = List( new Timestamp(base.getTime + 3000), base, new Timestamp(base.getTime + 1000), new Timestamp(base.getTime + 2000) ).map(ts => tupleOf(schema, "timestamp" -> ts)) val result = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey("timestamp", SortPreference.DESC)) } val times = result.map(_.getField[Timestamp]("timestamp").getTime) assert(times == times.sorted(Ordering.Long.reverse)) } it should "treat numeric strings as strings (lexicographic ordering)" in { val schema = schemaOf("str" -> AttributeType.STRING) val tuples = List("2", "10", "1", "11", "20").map(s => tupleOf(schema, "str" -> s)) val result = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey("str")) } assert(result.map(_.getField[String]("str")) == List("1", "10", "11", "2", "20")) } it should "sort BOOLEAN ascending (false < true) and descending" in { val schema = schemaOf("bool" -> AttributeType.BOOLEAN) val tuples = List(true, false, true, false).map(v => tupleOf(schema, "bool" -> v)) val asc = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey("bool", SortPreference.ASC)) } assert(asc.map(_.getField[Boolean]("bool")) == List(false, false, true, true)) val desc = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey("bool", SortPreference.DESC)) } assert(desc.map(_.getField[Boolean]("bool")) == List(true, true, false, false)) } it should "sort BINARY ascending (unsigned lexicographic) incl. empty and high-bit bytes" in { val schema = schemaOf("bin" -> AttributeType.BINARY) val bytesEmpty = Array[Byte]() // [] val bytes00 = Array(0x00.toByte) // [00] val bytes0000 = Array(0x00.toByte, 0x00.toByte) // [00,00] val bytes0001 = Array(0x00.toByte, 0x01.toByte) // [00,01] val bytes7F = Array(0x7f.toByte) // [7F] val bytes80 = Array(0x80.toByte) // [80] (-128) val bytesFF = Array(0xff.toByte) // [FF] (-1) val inputTuples = List(bytes80, bytes0000, bytesEmpty, bytesFF, bytes0001, bytes00, bytes7F) .map(arr => tupleOf(schema, "bin" -> arr)) val sorted = runStableMergeSort(schema, inputTuples) { _.keys = sortKeysBuffer(sortKey("bin")) } val actualUnsigned = sorted.map(_.getField[Array[Byte]]("bin").toSeq.map(b => b & 0xff)) val expectedUnsigned = List(bytesEmpty, bytes00, bytes0000, bytes0001, bytes7F, bytes80, bytesFF) .map(_.toSeq.map(b => b & 0xff)) assert(actualUnsigned == expectedUnsigned) } // =========================================================================== // B. Floating-point & Null/NaN policy // =========================================================================== it should "sort DOUBLE values including -0.0, 0.0, infinities and NaN" in { val schema = schemaOf("x" -> AttributeType.DOUBLE) val tuples = List(Double.NaN, Double.PositiveInfinity, 1.5, -0.0, 0.0, -3.2, Double.NegativeInfinity) .map(v => tupleOf(schema, "x" -> v)) val result = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey("x")) } val values = result.map(_.getField[Double]("x")) assert(values.head == Double.NegativeInfinity) assert(values(1) == -3.2) assert(java.lang.Double.compare(values(2), -0.0) == 0) assert(java.lang.Double.compare(values(3), 0.0) == 0) assert(values(4) == 1.5) assert(values(5) == Double.PositiveInfinity) assert(java.lang.Double.isNaN(values(6))) } it should "place NaN before null when sorting DOUBLE ascending (nulls last policy)" in { val schema = schemaOf("x" -> AttributeType.DOUBLE) val tuples = List( tupleOf(schema, "x" -> null), tupleOf(schema, "x" -> Double.NaN), tupleOf(schema, "x" -> Double.NegativeInfinity), tupleOf(schema, "x" -> 1.0), tupleOf(schema, "x" -> Double.PositiveInfinity), tupleOf(schema, "x" -> null) ) val result = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey("x")) } val values = result.map(_.getField[java.lang.Double]("x")) assert(values.take(4).forall(_ != null)) // first 4 are non-null assert(values(0).isInfinite && values(0) == Double.NegativeInfinity) assert(values(1) == 1.0) assert(values(2).isInfinite && values(2) == Double.PositiveInfinity) assert(java.lang.Double.isNaN(values(3))) assert(values.drop(4).forall(_ == null)) } it should "place nulls last regardless of ascending or descending" in { val schema = schemaOf("value" -> AttributeType.INTEGER, "label" -> AttributeType.STRING) val tuples = List( tupleOf(schema, "value" -> null, "label" -> "null-1"), tupleOf(schema, "value" -> 5, "label" -> "five"), tupleOf(schema, "value" -> null, "label" -> "null-2"), tupleOf(schema, "value" -> 3, "label" -> "three") ) val asc = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey("value", SortPreference.ASC)) } assert(asc.map(_.getField[String]("label")) == List("three", "five", "null-1", "null-2")) val desc = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey("value", SortPreference.DESC)) } assert(desc.map(_.getField[String]("label")) == List("five", "three", "null-1", "null-2")) } it should "order NaN highest on secondary DESC but still place nulls last" in { val schema = schemaOf( "group" -> AttributeType.STRING, "score" -> AttributeType.DOUBLE, "label" -> AttributeType.STRING ) val tuples = List( tupleOf(schema, "group" -> "A", "score" -> java.lang.Double.NaN, "label" -> "nan"), tupleOf(schema, "group" -> "A", "score" -> Double.PositiveInfinity, "label" -> "pinf"), tupleOf(schema, "group" -> "A", "score" -> 1.0, "label" -> "one"), tupleOf(schema, "group" -> "A", "score" -> 0.0, "label" -> "zero"), tupleOf(schema, "group" -> "A", "score" -> -1.0, "label" -> "neg"), tupleOf(schema, "group" -> "A", "score" -> Double.NegativeInfinity, "label" -> "ninf"), tupleOf(schema, "group" -> "A", "score" -> null, "label" -> "null-1"), tupleOf(schema, "group" -> "A", "score" -> null, "label" -> "null-2") ) val result = runStableMergeSort(schema, tuples) { desc => desc.keys = sortKeysBuffer(sortKey("group", SortPreference.ASC), sortKey("score", SortPreference.DESC)) } assert( result.map(_.getField[String]("label")) == List("nan", "pinf", "one", "zero", "neg", "ninf", "null-1", "null-2") ) } it should "sort BINARY descending with nulls last and preserve stability for equal byte arrays" in { val schema = schemaOf("bin" -> AttributeType.BINARY, "id" -> AttributeType.STRING) val key00 = Array(0x00.toByte) val keyFF = Array(0xff.toByte) val inputTuples = List( tupleOf(schema, "bin" -> keyFF, "id" -> "ff-1"), tupleOf(schema, "bin" -> key00, "id" -> "00-1"), tupleOf( schema, "bin" -> key00, "id" -> "00-2" ), // equal to previous; stability should keep order tupleOf(schema, "bin" -> null, "id" -> "null-1") ) val sorted = runStableMergeSort(schema, inputTuples) { _.keys = sortKeysBuffer(sortKey("bin", SortPreference.DESC)) } val idsInOrder = sorted.map(_.getField[String]("id")) assert(idsInOrder == List("ff-1", "00-1", "00-2", "null-1")) } // =========================================================================== // C. Multi-key semantics (lexicographic) // =========================================================================== it should "support multi-key sorting with mixed attribute types" in { val schema = schemaOf( "dept" -> AttributeType.STRING, "score" -> AttributeType.DOUBLE, "name" -> AttributeType.STRING, "hired" -> AttributeType.TIMESTAMP ) val base = new Timestamp(Timestamp.valueOf("2020-01-01 00:00:00").getTime) val tuples = List( tupleOf(schema, "dept" -> "Sales", "score" -> 9.5, "name" -> "Alice", "hired" -> base), tupleOf( schema, "dept" -> "Sales", "score" -> 9.5, "name" -> "Bob", "hired" -> new Timestamp(base.getTime + 1000) ), tupleOf( schema, "dept" -> "Sales", "score" -> 8.0, "name" -> "Carol", "hired" -> new Timestamp(base.getTime + 2000) ), tupleOf( schema, "dept" -> "Engineering", "score" -> 9.5, "name" -> "Dave", "hired" -> new Timestamp(base.getTime + 3000) ), tupleOf( schema, "dept" -> null, "score" -> 9.5, "name" -> "Eve", "hired" -> new Timestamp(base.getTime + 4000) ) ) val result = runStableMergeSort(schema, tuples) { desc => desc.keys = sortKeysBuffer( sortKey("dept", SortPreference.ASC), sortKey("score", SortPreference.DESC), sortKey("name", SortPreference.ASC) ) } assert(result.map(_.getField[String]("name")) == List("Dave", "Alice", "Bob", "Carol", "Eve")) } it should "handle multi-key with descending primary and ascending secondary" in { val schema = schemaOf( "major" -> AttributeType.INTEGER, "minor" -> AttributeType.INTEGER, "idx" -> AttributeType.INTEGER ) val tuples = List( (1, 9, 0), (1, 1, 1), (2, 5, 2), (2, 3, 3), (1, 1, 4), (3, 0, 5), (3, 2, 6) ).map { case (ma, mi, i) => tupleOf(schema, "major" -> ma, "minor" -> mi, "idx" -> i) } val result = runStableMergeSort(schema, tuples) { desc => desc.keys = sortKeysBuffer(sortKey("major", SortPreference.DESC), sortKey("minor", SortPreference.ASC)) } val pairs = result.map(t => (t.getField[Int]("major"), t.getField[Int]("minor"))) assert(pairs == List((3, 0), (3, 2), (2, 3), (2, 5), (1, 1), (1, 1), (1, 9))) val idxFor11 = result .filter(t => t.getField[Int]("major") == 1 && t.getField[Int]("minor") == 1) .map(_.getField[Int]("idx")) assert(idxFor11 == List(1, 4)) } it should "use the third key as a tiebreaker (ASC, ASC, then DESC)" in { val schema = schemaOf( "keyA" -> AttributeType.INTEGER, "keyB" -> AttributeType.INTEGER, "keyC" -> AttributeType.INTEGER, "id" -> AttributeType.STRING ) val tuples = List( (1, 1, 1, "x1"), (1, 1, 3, "x3"), (1, 1, 2, "x2"), (1, 0, 9, "y9") ).map { case (a, b, c, id) => tupleOf(schema, "keyA" -> a, "keyB" -> b, "keyC" -> c, "id" -> id) } val result = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey("keyA"), sortKey("keyB"), sortKey("keyC", SortPreference.DESC)) } assert(result.map(_.getField[String]("id")) == List("y9", "x3", "x2", "x1")) } it should "place nulls last across multiple keys (primary ASC, secondary DESC)" in { val schema = schemaOf("keyA" -> AttributeType.STRING, "keyB" -> AttributeType.INTEGER) val tuples = List( ("x", 2), (null, 1), ("x", 1), (null, 5), ("a", 9), ("a", 2) ).map { case (s, i) => tupleOf(schema, "keyA" -> s, "keyB" -> i) } val result = runStableMergeSort(schema, tuples) { desc => desc.keys = sortKeysBuffer(sortKey("keyA", SortPreference.ASC), sortKey("keyB", SortPreference.DESC)) } val out = result.map(t => (t.getField[String]("keyA"), t.getField[Int]("keyB"))) assert(out == List(("a", 9), ("a", 2), ("x", 2), ("x", 1), (null, 5), (null, 1))) } it should "when primary keys are both null, fall back to secondary ASC (nulls still after non-nulls)" in { val schema = schemaOf( "keyA" -> AttributeType.STRING, "keyB" -> AttributeType.INTEGER, "id" -> AttributeType.STRING ) val tuples = List( tupleOf(schema, "keyA" -> "A", "keyB" -> 2, "id" -> "non-null-a"), tupleOf(schema, "keyA" -> null, "keyB" -> 5, "id" -> "null-a-5"), tupleOf(schema, "keyA" -> null, "keyB" -> 1, "id" -> "null-a-1"), tupleOf(schema, "keyA" -> "B", "keyB" -> 9, "id" -> "non-null-b") ) val result = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey("keyA"), sortKey("keyB")) } assert( result .map(_.getField[String]("id")) == List("non-null-a", "non-null-b", "null-a-1", "null-a-5") ) } it should "use INTEGER secondary key to break ties when primary BINARY keys are equal" in { val schema = schemaOf( "bin" -> AttributeType.BINARY, "score" -> AttributeType.INTEGER, "label" -> AttributeType.STRING ) val key00 = Array(0x00.toByte) val key01 = Array(0x01.toByte) val inputTuples = List( tupleOf(schema, "bin" -> key01, "score" -> 1, "label" -> "01-score1"), tupleOf(schema, "bin" -> key00, "score" -> 9, "label" -> "00-score9"), tupleOf(schema, "bin" -> key01, "score" -> 2, "label" -> "01-score2") ) val sorted = runStableMergeSort(schema, inputTuples) { desc => desc.keys = sortKeysBuffer( sortKey("bin", SortPreference.ASC), // primary: binary ascending sortKey("score", SortPreference.DESC) // secondary: integer descending ) } val labelsInOrder = sorted.map(_.getField[String]("label")) assert(labelsInOrder == List("00-score9", "01-score2", "01-score1")) } // =========================================================================== // D. Stability & operational behaviors // =========================================================================== it should "preserve original order among tuples with equal keys" in { val schema = schemaOf("key" -> AttributeType.INTEGER, "index" -> AttributeType.INTEGER) val tuples = (0 until 100).map(i => tupleOf(schema, "key" -> (i % 5), "index" -> i)) val result = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey("key")) } val grouped = result.groupBy(_.getField[Int]("key")).values grouped.foreach { group => val indices = group.map(_.getField[Int]("index")) assert(indices == indices.sorted) } } it should "act as a stable pass-through when keys are empty" in { val schema = schemaOf("value" -> AttributeType.INTEGER, "label" -> AttributeType.STRING) val tuples = List(3, 1, 4, 1, 5, 9).zipWithIndex .map { case (v, i) => tupleOf(schema, "value" -> v, "label" -> s"row-$i") } val result = runStableMergeSort(schema, tuples) { desc => desc.keys = ListBuffer.empty[SortCriteriaUnit] } assert( result.map(_.getField[String]("label")) == List("row-0", "row-1", "row-2", "row-3", "row-4", "row-5") ) } it should "buffer tuples until onFinish is called" in { val schema = schemaOf("value" -> AttributeType.INTEGER) val tuple = tupleOf(schema, "value" -> 2) val desc = new StableMergeSortOpDesc(); desc.keys = sortKeysBuffer(sortKey("value")) val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc)) exec.open() val immediate = exec.processTuple(tuple, 0) assert(immediate.isEmpty) val result = exec.onFinish(0).map(_.asInstanceOf[Tuple]).toList assert(result.map(_.getField[Int]("value")) == List(2)) exec.close() } it should "return empty for empty input" in { val schema = schemaOf("value" -> AttributeType.INTEGER) val result = runStableMergeSort(schema, Seq.empty) { _.keys = sortKeysBuffer(sortKey("value")) } assert(result.isEmpty) } it should "handle single element input" in { val schema = schemaOf("value" -> AttributeType.INTEGER) val result = runStableMergeSort(schema, Seq(tupleOf(schema, "value" -> 42))) { _.keys = sortKeysBuffer(sortKey("value")) } assert(result.map(_.getField[Int]("value")) == List(42)) } it should "sort large inputs efficiently (sanity on boundaries)" in { val schema = schemaOf("value" -> AttributeType.INTEGER, "label" -> AttributeType.STRING) val tuples = (50000 to 1 by -1).map(i => tupleOf(schema, "value" -> i, "label" -> s"row-$i")) val result = runStableMergeSort(schema, tuples) { _.keys = sortKeysBuffer(sortKey("value")) } assert(result.head.getField[Int]("value") == 1) assert(result(1).getField[Int]("value") == 2) assert(result.takeRight(2).map(_.getField[Int]("value")) == List(49999, 50000)) } // =========================================================================== // E. Incremental bucket invariants (binary-carry & no-adjacent-equal) // =========================================================================== it should "merge incrementally: bucket sizes match binary decomposition after each push" in { val schema = schemaOf("value" -> AttributeType.INTEGER) val desc = new StableMergeSortOpDesc(); desc.keys = sortKeysBuffer(sortKey("value")) val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc)) exec.open() val totalCount = 64 for (index <- (totalCount - 1) to 0 by -1) { exec.processTuple(tupleOf(schema, "value" -> index), 0) val sizes = getBucketSizes(exec).sorted assert(sizes == binaryDecomposition(totalCount - index)) } exec.close() } it should "maintain bucket-stack invariant (no adjacent equal sizes) after each insertion" in { val schema = schemaOf("value" -> AttributeType.INTEGER) val desc = new StableMergeSortOpDesc(); desc.keys = sortKeysBuffer(sortKey("value")) val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc)) exec.open() val totalCount = 200 val stream = (0 until totalCount by 2) ++ (1 until totalCount by 2) stream.foreach { index => exec.processTuple(tupleOf(schema, "value" -> (totalCount - 1 - index)), 0) val sizes = getBucketSizes(exec) sizes.sliding(2).foreach { pair => if (pair.length == 2) assert(pair.head != pair.last) } } exec.close() } it should "form expected bucket sizes at milestones (1,2,3,4,7,8,15,16)" in { val schema = schemaOf("value" -> AttributeType.INTEGER) val desc = new StableMergeSortOpDesc(); desc.keys = sortKeysBuffer(sortKey("value")) val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc)) exec.open() val inputSequence = (100 to 1 by -1).map(i => tupleOf(schema, "value" -> i)) val milestones = Set(1, 2, 3, 4, 7, 8, 15, 16) var pushed = 0 inputSequence.foreach { t => exec.processTuple(t, 0); pushed += 1 if (milestones.contains(pushed)) { val sizes = getBucketSizes(exec).sorted assert(sizes == binaryDecomposition(pushed)) } } exec.close() } // =========================================================================== // F. Internal hooks — merge behavior // =========================================================================== "mergeSortedBuckets" should "be stable: left bucket wins on equal keys" in { val schema = schemaOf("key" -> AttributeType.INTEGER, "id" -> AttributeType.STRING) val desc = new StableMergeSortOpDesc(); desc.keys = sortKeysBuffer(sortKey("key")) val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc)); exec.open() // Seed to resolve schema/keys once. exec.processTuple(tupleOf(schema, "key" -> 0, "id" -> "seed"), 0) val left = ArrayBuffer( tupleOf(schema, "key" -> 1, "id" -> "L1"), tupleOf(schema, "key" -> 2, "id" -> "L2") ) val right = ArrayBuffer( tupleOf(schema, "key" -> 1, "id" -> "R1"), tupleOf(schema, "key" -> 3, "id" -> "R3") ) val merged = exec.mergeSortedBuckets(left, right) val ids = merged.map(_.getField[String]("id")).toList assert(ids == List("L1", "R1", "L2", "R3")) exec.close() } "mergeSortedBuckets" should "handle empty left bucket" in { val schema = schemaOf("key" -> AttributeType.INTEGER, "id" -> AttributeType.STRING) val desc = new StableMergeSortOpDesc(); desc.keys = sortKeysBuffer(sortKey("key")) val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc)); exec.open() exec.processTuple(tupleOf(schema, "key" -> 0, "id" -> "seed"), 0) // seed keys val left = ArrayBuffer.empty[Tuple] val right = ArrayBuffer( tupleOf(schema, "key" -> 1, "id" -> "r1"), tupleOf(schema, "key" -> 2, "id" -> "r2") ) val merged = exec.mergeSortedBuckets(left, right) assert(merged.map(_.getField[String]("id")).toList == List("r1", "r2")) exec.close() } "mergeSortedBuckets" should "handle empty right bucket" in { val schema = schemaOf("key" -> AttributeType.INTEGER, "id" -> AttributeType.STRING) val desc = new StableMergeSortOpDesc(); desc.keys = sortKeysBuffer(sortKey("key")) val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc)); exec.open() exec.processTuple(tupleOf(schema, "key" -> 0, "id" -> "seed"), 0) val left = ArrayBuffer( tupleOf(schema, "key" -> 1, "id" -> "l1"), tupleOf(schema, "key" -> 2, "id" -> "l2") ) val right = ArrayBuffer.empty[Tuple] val merged = exec.mergeSortedBuckets(left, right) assert(merged.map(_.getField[String]("id")).toList == List("l1", "l2")) exec.close() } // =========================================================================== // G. Internal hooks — push/finish/idempotence & schema errors // =========================================================================== "pushBucketAndCombine" should "merge two size-2 buckets into size-4 on push (with existing size-1 seed)" in { val schema = schemaOf("value" -> AttributeType.INTEGER) val desc = new StableMergeSortOpDesc(); desc.keys = sortKeysBuffer(sortKey("value")) val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc)); exec.open() // seed to compile keys -> results in one size-1 bucket in the stack exec.processTuple(tupleOf(schema, "value" -> 0), 0) // two pre-sorted buckets of size 2 val bucket1 = ArrayBuffer(tupleOf(schema, "value" -> 1), tupleOf(schema, "value" -> 3)) val bucket2 = ArrayBuffer(tupleOf(schema, "value" -> 2), tupleOf(schema, "value" -> 4)) exec.pushBucketAndCombine(bucket1) // sizes now [1,2] exec.pushBucketAndCombine(bucket2) // equal top [2,2] => merged to 4; sizes [1,4] val sizes = getBucketSizes(exec) assert(sizes == List(1, 4)) exec.close() } it should "return the same sorted output if onFinish is called twice in a row" in { val schema = schemaOf("value" -> AttributeType.INTEGER) val desc = new StableMergeSortOpDesc(); desc.keys = sortKeysBuffer(sortKey("value")) val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc)); exec.open() List(3, 1, 2).foreach(i => exec.processTuple(tupleOf(schema, "value" -> i), 0)) val first = exec.onFinish(0).map(_.asInstanceOf[Tuple]).toList.map(_.getField[Int]("value")) val second = exec.onFinish(0).map(_.asInstanceOf[Tuple]).toList.map(_.getField[Int]("value")) assert(first == List(1, 2, 3)) assert(second == List(1, 2, 3)) exec.close() } it should "have processTuple always return empty iterators until finish" in { val schema = schemaOf("value" -> AttributeType.INTEGER) val desc = new StableMergeSortOpDesc(); desc.keys = sortKeysBuffer(sortKey("value")) val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc)); exec.open() val immediates = (10 to 1 by -1).map(i => exec.processTuple(tupleOf(schema, "value" -> i), 0)) assert(immediates.forall(_.isEmpty)) val out = exec.onFinish(0).map(_.asInstanceOf[Tuple]).toList.map(_.getField[Int]("value")) assert(out == (1 to 10).toList) exec.close() } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/sortPartitions/SortPartitionsOpExecSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.sortPartitions import org.apache.texera.amber.core.tuple._ import org.apache.texera.amber.util.JSONUtils.objectMapper import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class SortPartitionsOpExecSpec extends AnyFlatSpec with BeforeAndAfter { val tupleSchema: Schema = Schema() .add(new Attribute("field1", AttributeType.STRING)) .add(new Attribute("field2", AttributeType.INTEGER)) .add(new Attribute("field3", AttributeType.BOOLEAN)) val tuple: Int => Tuple = i => Tuple .builder(tupleSchema) .add(new Attribute("field1", AttributeType.STRING), "hello") .add(new Attribute("field2", AttributeType.INTEGER), i) .add( new Attribute("field3", AttributeType.BOOLEAN), true ) .build() val opDesc: SortPartitionsOpDesc = new SortPartitionsOpDesc() opDesc.sortAttributeName = "field2" var opExec: SortPartitionsOpExec = _ before { opExec = new SortPartitionsOpExec(objectMapper.writeValueAsString(opDesc)) } it should "open" in { opExec.open() } it should "output in order" in { opExec.open() opExec.processTuple(tuple(3), 0) opExec.processTuple(tuple(1), 0) opExec.processTuple(tuple(2), 0) opExec.processTuple(tuple(5), 0) val outputTuples: List[Tuple] = opExec .onFinish(0) .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(tupleSchema)) .toList assert(outputTuples.size == 4) assert(outputTuples(0).equals(tuple(1))) assert(outputTuples(1).equals(tuple(2))) assert(outputTuples(2).equals(tuple(3))) assert(outputTuples(3).equals(tuple(5))) opExec.close() } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/dataset/FileListerSourceOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.dataset import org.apache.texera.amber.core.tuple.AttributeType import org.scalatest.flatspec.AnyFlatSpec class FileListerSourceOpDescSpec extends AnyFlatSpec { "FileListerSourceOpDesc" should "expose a filename output column" in { val opDesc = new FileListerSourceOpDesc() val outputSchema = opDesc.getExternalOutputSchemas(Map.empty).values.head assert(outputSchema.getAttributes.length == 1) assert(outputSchema.getAttribute("filename").getType == AttributeType.STRING) } it should "use the expected operator metadata" in { val opDesc = new FileListerSourceOpDesc() assert(opDesc.operatorInfo.userFriendlyName == "File Lister") assert(opDesc.operatorInfo.inputPorts.isEmpty) assert(opDesc.operatorInfo.outputPorts.length == 1) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/fetcher/URLFetcherOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.fetcher import org.apache.texera.amber.core.executor.OpExecWithClassName import org.apache.texera.amber.core.tuple.AttributeType import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} import org.apache.texera.amber.operator.metadata.OperatorGroupConstants import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers class URLFetcherOpDescSpec extends AnyFlatSpec with Matchers { private val workflowId = WorkflowIdentity(1L) private val executionId = ExecutionIdentity(1L) private def configured(decoding: DecodingMethod): URLFetcherOpDesc = { val op = new URLFetcherOpDesc op.url = "https://example.test/data" op.decodingMethod = decoding op } "URLFetcherOpDesc.operatorInfo" should "advertise the user-friendly name and API group" in { val info = (new URLFetcherOpDesc).operatorInfo info.userFriendlyName shouldBe "URL Fetcher" info.operatorGroupName shouldBe OperatorGroupConstants.API_GROUP info.operatorDescription should include("URL") } it should "expose no input ports and one output port (source-shaped)" in { val info = (new URLFetcherOpDesc).operatorInfo info.inputPorts shouldBe empty info.outputPorts should have length 1 } "URLFetcherOpDesc.sourceSchema" should "produce a single STRING column when decoding is UTF-8" in { val op = configured(DecodingMethod.UTF_8) val schema = op.sourceSchema() schema.getAttributes should have length 1 schema.getAttributes.head.getName shouldBe "URL content" schema.getAttributes.head.getType shouldBe AttributeType.STRING } it should "produce an ANY column for raw-bytes decoding" in { val op = configured(DecodingMethod.RAW_BYTES) val schema = op.sourceSchema() schema.getAttributes should have length 1 schema.getAttributes.head.getName shouldBe "URL content" schema.getAttributes.head.getType shouldBe AttributeType.ANY } it should "default to ANY when decodingMethod is left unset (current behavior)" in { // Pin: `var decodingMethod: DecodingMethod = _` defaults to null. // sourceSchema's branch is `if (decodingMethod == DecodingMethod.UTF_8) // STRING else ANY`, so a null comparison falls through to ANY without // raising. Documenting the current behavior so a future explicit-null // check breaks this spec deliberately. val op = new URLFetcherOpDesc op.url = "https://example.test/data" val schema = op.sourceSchema() schema.getAttributes should have length 1 schema.getAttributes.head.getType shouldBe AttributeType.ANY } "URLFetcherOpDesc.getPhysicalOp" should "wire the URLFetcherOpExec class name into the OpExecInitInfo" in { // Pattern-match on OpExecWithClassName instead of substring-matching the // toString output, which is brittle to scalapb formatting changes. val op = configured(DecodingMethod.UTF_8) val physical = op.getPhysicalOp(workflowId, executionId) physical.opExecInitInfo match { case OpExecWithClassName(className, _) => className shouldBe "org.apache.texera.amber.operator.source.fetcher.URLFetcherOpExec" case other => fail(s"expected OpExecWithClassName, got $other") } } it should "propagate sourceSchema onto the single output port" in { // Exercise propagateSchema.func directly so the test actually proves the // sourceSchema gets routed to the output port id, not just that an // output port exists. Inputs are empty (this is a source operator). val op = configured(DecodingMethod.UTF_8) val physical = op.getPhysicalOp(workflowId, executionId) val outputPortId = op.operatorInfo.outputPorts.head.id val propagated = physical.propagateSchema.func(Map.empty) propagated should contain key outputPortId propagated(outputPortId) shouldBe op.sourceSchema() } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/fetcher/URLFetcherOpExecSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.fetcher import org.apache.texera.amber.core.tuple.Schema import org.apache.texera.amber.util.JSONUtils.objectMapper import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class URLFetcherOpExecSpec extends AnyFlatSpec with BeforeAndAfter { val resultSchema: Schema = new URLFetcherOpDesc().sourceSchema() val opDesc: URLFetcherOpDesc = new URLFetcherOpDesc() it should "fetch url and output one tuple with raw bytes" in { opDesc.url = "https://www.google.com" opDesc.decodingMethod = DecodingMethod.RAW_BYTES val fetcherOpExec = new URLFetcherOpExec(objectMapper.writeValueAsString(opDesc)) val iterator = fetcherOpExec.produceTuple() assert(iterator.next().getFields.toList.head.isInstanceOf[Array[Byte]]) assert(!iterator.hasNext) } it should "fetch url and output one tuple with UTF-8 string" in { opDesc.url = "https://www.google.com" opDesc.decodingMethod = DecodingMethod.UTF_8 val fetcherOpExec = new URLFetcherOpExec(objectMapper.writeValueAsString(opDesc)) val iterator = fetcherOpExec.produceTuple() assert(iterator.next().getFields.toList.head.isInstanceOf[String]) assert(!iterator.hasNext) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.csv import org.apache.texera.amber.core.storage.FileResolver import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.core.workflow.WorkflowContext.{ DEFAULT_EXECUTION_ID, DEFAULT_WORKFLOW_ID } import org.apache.texera.amber.operator.TestOperators import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class CSVScanSourceOpDescSpec extends AnyFlatSpec with BeforeAndAfter { var csvScanSourceOpDesc: CSVScanSourceOpDesc = _ var parallelCsvScanSourceOpDesc: ParallelCSVScanSourceOpDesc = _ before { csvScanSourceOpDesc = new CSVScanSourceOpDesc() parallelCsvScanSourceOpDesc = new ParallelCSVScanSourceOpDesc() } it should "infer schema from single-line-data csv" in { parallelCsvScanSourceOpDesc.fileName = Some(TestOperators.CountrySalesSmallCsvPath) parallelCsvScanSourceOpDesc.customDelimiter = Some(",") parallelCsvScanSourceOpDesc.hasHeader = true parallelCsvScanSourceOpDesc.setResolvedFileName( FileResolver.resolve(parallelCsvScanSourceOpDesc.fileName.get) ) val inferredSchema: Schema = parallelCsvScanSourceOpDesc.sourceSchema() assert(inferredSchema.getAttributes.length == 14) assert(inferredSchema.getAttribute("Order ID").getType == AttributeType.INTEGER) assert(inferredSchema.getAttribute("Unit Price").getType == AttributeType.DOUBLE) } it should "infer schema from headerless single-line-data csv" in { parallelCsvScanSourceOpDesc.fileName = Some(TestOperators.CountrySalesHeaderlessSmallCsvPath) parallelCsvScanSourceOpDesc.customDelimiter = Some(",") parallelCsvScanSourceOpDesc.hasHeader = false parallelCsvScanSourceOpDesc.setResolvedFileName( FileResolver.resolve(parallelCsvScanSourceOpDesc.fileName.get) ) val inferredSchema: Schema = parallelCsvScanSourceOpDesc.sourceSchema() assert(inferredSchema.getAttributes.length == 14) assert(inferredSchema.getAttribute("column-10").getType == AttributeType.DOUBLE) assert(inferredSchema.getAttribute("column-7").getType == AttributeType.INTEGER) } it should "infer schema from multi-line-data csv" in { csvScanSourceOpDesc.fileName = Some(TestOperators.CountrySalesSmallMultiLineCsvPath) csvScanSourceOpDesc.customDelimiter = Some(",") csvScanSourceOpDesc.hasHeader = true csvScanSourceOpDesc.setResolvedFileName(FileResolver.resolve(csvScanSourceOpDesc.fileName.get)) val inferredSchema: Schema = csvScanSourceOpDesc.sourceSchema() assert(inferredSchema.getAttributes.length == 14) assert(inferredSchema.getAttribute("Order ID").getType == AttributeType.INTEGER) assert(inferredSchema.getAttribute("Unit Price").getType == AttributeType.DOUBLE) } it should "infer schema from headerless multi-line-data csv" in { csvScanSourceOpDesc.fileName = Some(TestOperators.CountrySalesHeaderlessSmallCsvPath) csvScanSourceOpDesc.customDelimiter = Some(",") csvScanSourceOpDesc.hasHeader = false csvScanSourceOpDesc.setResolvedFileName(FileResolver.resolve(csvScanSourceOpDesc.fileName.get)) val inferredSchema: Schema = csvScanSourceOpDesc.sourceSchema() assert(inferredSchema.getAttributes.length == 14) assert(inferredSchema.getAttribute("column-10").getType == AttributeType.DOUBLE) assert(inferredSchema.getAttribute("column-7").getType == AttributeType.INTEGER) } it should "infer schema from headerless multi-line-data csv with custom delimiter" in { csvScanSourceOpDesc.fileName = Some(TestOperators.CountrySalesSmallMultiLineCustomDelimiterCsvPath) csvScanSourceOpDesc.customDelimiter = Some(";") csvScanSourceOpDesc.hasHeader = false csvScanSourceOpDesc.setResolvedFileName(FileResolver.resolve(csvScanSourceOpDesc.fileName.get)) val inferredSchema: Schema = csvScanSourceOpDesc.sourceSchema() assert(inferredSchema.getAttributes.length == 14) assert(inferredSchema.getAttribute("column-10").getType == AttributeType.DOUBLE) assert(inferredSchema.getAttribute("column-7").getType == AttributeType.INTEGER) } it should "create one worker with multi-line-data csv" in { csvScanSourceOpDesc.fileName = Some(TestOperators.CountrySalesSmallMultiLineCustomDelimiterCsvPath) csvScanSourceOpDesc.customDelimiter = Some(";") csvScanSourceOpDesc.hasHeader = false csvScanSourceOpDesc.setResolvedFileName(FileResolver.resolve(csvScanSourceOpDesc.fileName.get)) assert( !csvScanSourceOpDesc .getPhysicalOp(DEFAULT_WORKFLOW_ID, DEFAULT_EXECUTION_ID) .parallelizable ) } it should "use comma as the default delimiter when customDelimiter is not set for parallel CSV" in { parallelCsvScanSourceOpDesc.customDelimiter = None parallelCsvScanSourceOpDesc.getPhysicalOp(DEFAULT_WORKFLOW_ID, DEFAULT_EXECUTION_ID) assert(parallelCsvScanSourceOpDesc.customDelimiter.contains(",")) } it should "use comma as the default delimiter when customDelimiter is empty string for parallel CSV" in { parallelCsvScanSourceOpDesc.customDelimiter = Some("") parallelCsvScanSourceOpDesc.getPhysicalOp(DEFAULT_WORKFLOW_ID, DEFAULT_EXECUTION_ID) assert(parallelCsvScanSourceOpDesc.customDelimiter.contains(",")) } it should "use comma as the default delimiter when customDelimiter is not set for CSV" in { csvScanSourceOpDesc.customDelimiter = None csvScanSourceOpDesc.getPhysicalOp(DEFAULT_WORKFLOW_ID, DEFAULT_EXECUTION_ID) assert(csvScanSourceOpDesc.customDelimiter.contains(",")) } it should "use comma as the default delimiter when customDelimiter is empty string for CSV" in { csvScanSourceOpDesc.customDelimiter = Some("") csvScanSourceOpDesc.getPhysicalOp(DEFAULT_WORKFLOW_ID, DEFAULT_EXECUTION_ID) assert(csvScanSourceOpDesc.customDelimiter.contains(",")) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpExecSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.csv import com.univocity.parsers.common.TextParsingException import com.univocity.parsers.csv.{CsvParser, CsvParserSettings} import org.scalatest.flatspec.AnyFlatSpec import java.io.StringReader /** * Verifies the column-overflow translation in [[CSVScanSourceOpExec.parseNextRow]] * — the path that turns a deep Univocity stack trace into a single-sentence message * the workflow user can act on. */ class CSVScanSourceOpExecSpec extends AnyFlatSpec { private def parserWithMaxColumns(max: Int): CsvParser = { val settings = new CsvParserSettings() settings.setMaxColumns(max) settings.setMaxCharsPerColumn(-1) new CsvParser(settings) } "parseNextRow" should "return the parsed row when the input is within the column limit" in { val parser = parserWithMaxColumns(10) parser.beginParsing(new StringReader("a,b,c\n")) val row = CSVScanSourceOpExec.parseNextRow(parser, 10) assert(row.toSeq == Seq("a", "b", "c")) } it should "return null at end of input (so the iterator can terminate cleanly)" in { val parser = parserWithMaxColumns(10) parser.beginParsing(new StringReader("")) assert(CSVScanSourceOpExec.parseNextRow(parser, 10) == null) } it should "translate a column-overflow TextParsingException into a clear user message" in { val maxColumns = 2 val parser = parserWithMaxColumns(maxColumns) parser.beginParsing(new StringReader("a,b,c,d,e\n")) val ex = intercept[RuntimeException] { CSVScanSourceOpExec.parseNextRow(parser, maxColumns) } // The message must mention the configured limit so the user knows what was hit. assert(ex.getMessage.contains(maxColumns.toString)) assert(ex.getMessage.toLowerCase.contains("max columns")) assert(ex.getMessage.toLowerCase.contains("exceeded")) // The original Univocity exception is preserved as the cause so developers // can still inspect the underlying parser state if needed. assert(ex.getCause.isInstanceOf[TextParsingException]) } "isColumnOverflow" should "detect AIOOBE causes from Java 8's plain-integer message" in { val cause = new ArrayIndexOutOfBoundsException("5") val ex = new TextParsingException(null, "wrapper", cause) assert(CSVScanSourceOpExec.isColumnOverflow(ex, maxColumns = 5)) assert(!CSVScanSourceOpExec.isColumnOverflow(ex, maxColumns = 6)) } it should "detect AIOOBE causes from Java 9+'s 'Index N out of bounds for length M' message" in { val cause = new ArrayIndexOutOfBoundsException("Index 5 out of bounds for length 5") val ex = new TextParsingException(null, "wrapper", cause) assert(CSVScanSourceOpExec.isColumnOverflow(ex, maxColumns = 5)) assert(!CSVScanSourceOpExec.isColumnOverflow(ex, maxColumns = 6)) } it should "ignore TextParsingExceptions whose cause is unrelated" in { val unrelated = new TextParsingException(null, "Some other parsing problem") val withDifferentCause = new TextParsingException(null, "wrapper", new IllegalStateException("nope")) assert(!CSVScanSourceOpExec.isColumnOverflow(unrelated, maxColumns = 5)) assert(!CSVScanSourceOpExec.isColumnOverflow(withDifferentCause, maxColumns = 5)) } it should "ignore an AIOOBE whose message cannot be parsed as an index" in { val unparseable = new ArrayIndexOutOfBoundsException("something went wrong") val ex = new TextParsingException(null, "wrapper", unparseable) assert(!CSVScanSourceOpExec.isColumnOverflow(ex, maxColumns = 5)) } "columnOverflowMessage" should "include the configured maximum so the user knows the current limit" in { val msg = CSVScanSourceOpExec.columnOverflowMessage(750) assert(msg.contains("750")) assert(msg.toLowerCase.contains("max columns")) assert(msg.toLowerCase.contains("exceeded")) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/file/FileScanOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.file import org.apache.texera.amber.core.tuple.{ Attribute, AttributeType, Schema, SchemaEnforceable, Tuple } import org.apache.texera.amber.operator.TestOperators import org.apache.texera.amber.operator.source.scan.{FileAttributeType, FileDecodingMethod} import org.apache.texera.amber.util.JSONUtils.objectMapper import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class FileScanOpDescSpec extends AnyFlatSpec with BeforeAndAfter { private val inputSchema = new Schema(new Attribute("filename", AttributeType.STRING)) var fileScanOpDesc: FileScanOpDesc = _ before { fileScanOpDesc = new FileScanOpDesc() fileScanOpDesc.fileEncoding = FileDecodingMethod.UTF_8 } it should "infer schema with single column representing each line of text" in { val inferredSchema: Schema = fileScanOpDesc.sourceSchema() assert(inferredSchema.getAttributes.length == 1) assert(inferredSchema.getAttribute("line").getType == AttributeType.STRING) } it should "read first 5 lines from the input file path tuple into output tuples" in { fileScanOpDesc.attributeType = FileAttributeType.STRING fileScanOpDesc.fileScanLimit = Option(5) val inputTuple = Tuple(inputSchema, Array[Any](TestOperators.TestTextFilePath)) val fileScanOpExec = new FileScanOpExec(objectMapper.writeValueAsString(fileScanOpDesc)) fileScanOpExec.open() val processedTuple: Iterator[Tuple] = fileScanOpExec .processTuple(inputTuple, 0) .map(tupleLike => tupleLike .asInstanceOf[SchemaEnforceable] .enforceSchema(fileScanOpDesc.sourceSchema()) ) assert(processedTuple.next().getField("line").equals("line1")) assert(processedTuple.next().getField("line").equals("line2")) assert(processedTuple.next().getField("line").equals("line3")) assert(processedTuple.next().getField("line").equals("line4")) assert(processedTuple.next().getField("line").equals("line5")) assertThrows[java.util.NoSuchElementException](processedTuple.next().getField("line")) fileScanOpExec.close() } it should "preserve the original input filename when include filename is enabled" in { fileScanOpDesc.attributeType = FileAttributeType.SINGLE_STRING fileScanOpDesc.outputFileName = true val inputFilePath = TestOperators.TestTextFilePath val inputTuple = Tuple(inputSchema, Array[Any](inputFilePath)) val fileScanOpExec = new FileScanOpExec(objectMapper.writeValueAsString(fileScanOpDesc)) fileScanOpExec.open() val outputSchema = fileScanOpDesc.sourceSchema() val processedTuple = fileScanOpExec .processTuple(inputTuple, 0) .next() .asInstanceOf[SchemaEnforceable] .enforceSchema(outputSchema) assert(processedTuple.getField[String]("filename") == inputFilePath) fileScanOpExec.close() } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/file/FileScanSourceOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.file import org.apache.texera.amber.core.storage.FileResolver import org.apache.texera.amber.core.tuple.{AttributeType, Schema, SchemaEnforceable, Tuple} import org.apache.texera.amber.operator.TestOperators import org.apache.texera.amber.operator.source.scan.{FileAttributeType, FileDecodingMethod} import org.apache.texera.amber.util.JSONUtils.objectMapper import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class FileScanSourceOpDescSpec extends AnyFlatSpec with BeforeAndAfter { var fileScanSourceOpDesc: FileScanSourceOpDesc = _ before { fileScanSourceOpDesc = new FileScanSourceOpDesc() fileScanSourceOpDesc.setResolvedFileName(FileResolver.resolve(TestOperators.TestTextFilePath)) fileScanSourceOpDesc.fileEncoding = FileDecodingMethod.UTF_8 } it should "infer schema with single column representing each line of text in normal text scan mode" in { val inferredSchema: Schema = fileScanSourceOpDesc.sourceSchema() assert(inferredSchema.getAttributes.length == 1) assert(inferredSchema.getAttribute("line").getType == AttributeType.STRING) } it should "infer schema with single column representing entire file in outputAsSingleTuple mode" in { fileScanSourceOpDesc.attributeType = FileAttributeType.SINGLE_STRING val inferredSchema: Schema = fileScanSourceOpDesc.sourceSchema() assert(inferredSchema.getAttributes.length == 1) assert(inferredSchema.getAttribute("line").getType == AttributeType.STRING) } it should "infer schema with user-specified output schema attribute" in { fileScanSourceOpDesc.attributeType = FileAttributeType.STRING val customOutputAttributeName: String = "testing" fileScanSourceOpDesc.attributeName = customOutputAttributeName val inferredSchema: Schema = fileScanSourceOpDesc.sourceSchema() assert(inferredSchema.getAttributes.length == 1) assert(inferredSchema.getAttribute("testing").getType == AttributeType.STRING) } it should "infer schema with integer attribute type" in { fileScanSourceOpDesc.attributeType = FileAttributeType.INTEGER val inferredSchema: Schema = fileScanSourceOpDesc.sourceSchema() assert(inferredSchema.getAttributes.length == 1) assert(inferredSchema.getAttribute("line").getType == AttributeType.INTEGER) } it should "read first 5 lines of the input text file into corresponding output tuples" in { fileScanSourceOpDesc.attributeType = FileAttributeType.STRING fileScanSourceOpDesc.fileScanLimit = Option(5) val FileScanSourceOpExec = new FileScanSourceOpExec(objectMapper.writeValueAsString(fileScanSourceOpDesc)) FileScanSourceOpExec.open() val processedTuple: Iterator[Tuple] = FileScanSourceOpExec .produceTuple() .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(fileScanSourceOpDesc.sourceSchema()) ) assert(processedTuple.next().getField("line").equals("line1")) assert(processedTuple.next().getField("line").equals("line2")) assert(processedTuple.next().getField("line").equals("line3")) assert(processedTuple.next().getField("line").equals("line4")) assert(processedTuple.next().getField("line").equals("line5")) assertThrows[java.util.NoSuchElementException](processedTuple.next().getField("line")) FileScanSourceOpExec.close() } it should "read first 5 lines of the input text file with CRLF separators into corresponding output tuples" in { fileScanSourceOpDesc.setResolvedFileName( FileResolver.resolve(TestOperators.TestCRLFTextFilePath) ) fileScanSourceOpDesc.attributeType = FileAttributeType.STRING fileScanSourceOpDesc.fileScanLimit = Option(5) val FileScanSourceOpExec = new FileScanSourceOpExec(objectMapper.writeValueAsString(fileScanSourceOpDesc)) FileScanSourceOpExec.open() val processedTuple: Iterator[Tuple] = FileScanSourceOpExec .produceTuple() .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(fileScanSourceOpDesc.sourceSchema()) ) assert(processedTuple.next().getField("line").equals("line1")) assert(processedTuple.next().getField("line").equals("line2")) assert(processedTuple.next().getField("line").equals("line3")) assert(processedTuple.next().getField("line").equals("line4")) assert(processedTuple.next().getField("line").equals("line5")) assertThrows[java.util.NoSuchElementException](processedTuple.next().getField("line")) FileScanSourceOpExec.close() } it should "read first 5 lines of the input text file into a single output tuple" in { fileScanSourceOpDesc.attributeType = FileAttributeType.SINGLE_STRING val FileScanSourceOpExec = new FileScanSourceOpExec(objectMapper.writeValueAsString(fileScanSourceOpDesc)) FileScanSourceOpExec.open() val processedTuple: Iterator[Tuple] = FileScanSourceOpExec .produceTuple() .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(fileScanSourceOpDesc.sourceSchema()) ) assert( processedTuple .next() .getField("line") .equals("line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10") ) assertThrows[java.util.NoSuchElementException](processedTuple.next().getField("line")) FileScanSourceOpExec.close() } it should "read first 5 lines of the input text into corresponding output INTEGER tuples" in { fileScanSourceOpDesc.setResolvedFileName( FileResolver.resolve(TestOperators.TestNumbersFilePath) ) fileScanSourceOpDesc.attributeType = FileAttributeType.INTEGER fileScanSourceOpDesc.fileScanLimit = Option(5) val FileScanSourceOpExec = new FileScanSourceOpExec(objectMapper.writeValueAsString(fileScanSourceOpDesc)) FileScanSourceOpExec.open() val processedTuple: Iterator[Tuple] = FileScanSourceOpExec .produceTuple() .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(fileScanSourceOpDesc.sourceSchema()) ) assert(processedTuple.next().getField[Int]("line") == 1) assert(processedTuple.next().getField[Int]("line") == 2) assert(processedTuple.next().getField[Int]("line") == 3) assert(processedTuple.next().getField[Int]("line") == 4) assert(processedTuple.next().getField[Int]("line") == 5) assertThrows[java.util.NoSuchElementException](processedTuple.next().getField("line")) FileScanSourceOpExec.close() } it should "read first 5 lines of the input text file with US_ASCII encoding" in { fileScanSourceOpDesc.setResolvedFileName( FileResolver.resolve(TestOperators.TestCRLFTextFilePath) ) fileScanSourceOpDesc.fileEncoding = FileDecodingMethod.ASCII fileScanSourceOpDesc.attributeType = FileAttributeType.STRING fileScanSourceOpDesc.fileScanLimit = Option(5) val FileScanSourceOpExec = new FileScanSourceOpExec(objectMapper.writeValueAsString(fileScanSourceOpDesc)) FileScanSourceOpExec.open() val processedTuple: Iterator[Tuple] = FileScanSourceOpExec .produceTuple() .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(fileScanSourceOpDesc.sourceSchema()) ) assert(processedTuple.next().getField("line").equals("line1")) assert(processedTuple.next().getField("line").equals("line2")) assert(processedTuple.next().getField("line").equals("line3")) assert(processedTuple.next().getField("line").equals("line4")) assert(processedTuple.next().getField("line").equals("line5")) assertThrows[java.util.NoSuchElementException](processedTuple.next().getField("line")) FileScanSourceOpExec.close() } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/file/FileScanSourceOpExecSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.file import org.apache.texera.amber.core.tuple.{AttributeType, LargeBinary, Schema, SchemaEnforceable} import org.apache.texera.amber.operator.source.scan.{FileAttributeType, FileDecodingMethod} import org.apache.texera.amber.util.JSONUtils.objectMapper import org.scalatest.BeforeAndAfterAll import org.scalatest.flatspec.AnyFlatSpec import java.io.{BufferedOutputStream, FileOutputStream} import java.net.URI import java.nio.file.{Files, Path} import java.util.zip.{ZipEntry, ZipOutputStream} /** * Unit tests for LARGE_BINARY logic in FileScanSourceOpExec. * Full integration tests with S3 and database are in LargeBinaryManagerSpec. */ class FileScanSourceOpExecSpec extends AnyFlatSpec with BeforeAndAfterAll { private val testDir = Path .of(sys.env.getOrElse("TEXERA_HOME", ".")) .resolve("common/workflow-operator/src/test/resources") .toRealPath() private val testFile = testDir.resolve("test_large_binary.txt") private val testZip = testDir.resolve("test_large_binary.zip") override def beforeAll(): Unit = { super.beforeAll() Files.write(testFile, "Test content\nLine 2\nLine 3".getBytes) createZipFile(testZip, Map("file1.txt" -> "Content 1", "file2.txt" -> "Content 2")) } override def afterAll(): Unit = { Files.deleteIfExists(testFile) Files.deleteIfExists(testZip) super.afterAll() } private def createZipFile(path: Path, entries: Map[String, String]): Unit = { val zipOut = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(path.toFile))) try { entries.foreach { case (name, content) => zipOut.putNextEntry(new ZipEntry(name)) zipOut.write(content.getBytes) zipOut.closeEntry() } } finally { zipOut.close() } } private def createDescriptor( file: Path = testFile, attributeName: String = "line" ): FileScanSourceOpDesc = { val desc = new FileScanSourceOpDesc() desc.fileName = Some(file.toString) desc.attributeType = FileAttributeType.LARGE_BINARY desc.attributeName = attributeName desc.fileEncoding = FileDecodingMethod.UTF_8 desc } private def assertSchema(schema: Schema, attributeName: String): Unit = { assert(schema.getAttributes.length == 1) assert(schema.getAttribute(attributeName).getType == AttributeType.LARGE_BINARY) } // Schema Tests it should "infer LARGE_BINARY schema with default attribute name" in { assertSchema(createDescriptor().sourceSchema(), "line") } it should "infer LARGE_BINARY schema with custom attribute name" in { assertSchema(createDescriptor(attributeName = "custom_field").sourceSchema(), "custom_field") } it should "map LARGE_BINARY to correct AttributeType" in { assert(FileAttributeType.LARGE_BINARY.getType == AttributeType.LARGE_BINARY) } // Type Classification Tests it should "correctly classify LARGE_BINARY as isSingle type" in { val isSingleTypes = List( FileAttributeType.LARGE_BINARY, FileAttributeType.SINGLE_STRING, FileAttributeType.BINARY ) val multiLineTypes = List( FileAttributeType.STRING, FileAttributeType.INTEGER, FileAttributeType.LONG, FileAttributeType.DOUBLE, FileAttributeType.BOOLEAN, FileAttributeType.TIMESTAMP ) isSingleTypes.foreach(t => assert(t.isSingle, s"$t should be isSingle")) multiLineTypes.foreach(t => assert(!t.isSingle, s"$t should not be isSingle")) } // Execution Tests it should "create LargeBinary when reading file with LARGE_BINARY type" in { val desc = createDescriptor() desc.setResolvedFileName(URI.create(testFile.toUri.toString)) val executor = new FileScanSourceOpExec(objectMapper.writeValueAsString(desc)) try { executor.open() val tuples = executor.produceTuple().toSeq executor.close() assert(tuples.size == 1) val field = tuples.head .asInstanceOf[SchemaEnforceable] .enforceSchema(desc.sourceSchema()) .getField[Any]("line") assert(field.isInstanceOf[LargeBinary]) assert(field.asInstanceOf[LargeBinary].getUri.startsWith("s3://")) } catch { case e: Exception => info(s"S3 not configured: ${e.getMessage}") } } // LargeBinary Tests it should "create valid LargeBinary with correct URI parsing" in { val pointer = new LargeBinary("s3://bucket/path/to/object") assert(pointer.getUri == "s3://bucket/path/to/object") assert(pointer.getBucketName == "bucket") assert(pointer.getObjectKey == "path/to/object") } it should "reject invalid LargeBinary URIs" in { assertThrows[IllegalArgumentException](new LargeBinary("http://invalid")) assertThrows[IllegalArgumentException](new LargeBinary("not-a-uri")) assertThrows[IllegalArgumentException](new LargeBinary(null.asInstanceOf[String])) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/text/TextInputSourceOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.source.scan.text import org.apache.texera.amber.core.tuple.{AttributeType, Schema, SchemaEnforceable, Tuple} import org.apache.texera.amber.operator.TestOperators import org.apache.texera.amber.operator.source.scan.FileAttributeType import org.apache.texera.amber.util.JSONUtils.objectMapper import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec import java.nio.charset.StandardCharsets import java.nio.file.{Files, Path, Paths} class TextInputSourceOpDescSpec extends AnyFlatSpec with BeforeAndAfter { var textInputSourceOpDesc: TextInputSourceOpDesc = _ before { textInputSourceOpDesc = new TextInputSourceOpDesc() } it should "infer schema with single column representing each line of text in normal text scan mode" in { val inferredSchema: Schema = textInputSourceOpDesc.sourceSchema() assert(inferredSchema.getAttributes.length == 1) assert(inferredSchema.getAttribute("line").getType == AttributeType.STRING) } it should "infer schema with single column representing entire input in outputAsSingleTuple mode" in { textInputSourceOpDesc.attributeType = FileAttributeType.SINGLE_STRING val inferredSchema: Schema = textInputSourceOpDesc.sourceSchema() assert(inferredSchema.getAttributes.length == 1) assert(inferredSchema.getAttribute("line").getType == AttributeType.STRING) } it should "infer schema with user-specified output schema attribute" in { textInputSourceOpDesc.attributeType = FileAttributeType.STRING val customOutputAttributeName: String = "testing" textInputSourceOpDesc.attributeName = customOutputAttributeName val inferredSchema: Schema = textInputSourceOpDesc.sourceSchema() assert(inferredSchema.getAttributes.length == 1) assert(inferredSchema.getAttribute("testing").getType == AttributeType.STRING) } it should "infer schema with integer attribute type" in { textInputSourceOpDesc.attributeType = FileAttributeType.INTEGER val inferredSchema: Schema = textInputSourceOpDesc.sourceSchema() assert(inferredSchema.getAttributes.length == 1) assert(inferredSchema.getAttribute("line").getType == AttributeType.INTEGER) } it should "read first 5 lines of the input text into corresponding output tuples" in { val inputString: String = readFileIntoString(TestOperators.TestTextFilePath) textInputSourceOpDesc.attributeType = FileAttributeType.STRING textInputSourceOpDesc.textInput = inputString textInputSourceOpDesc.fileScanLimit = Option(5) val textScanSourceOpExec = new TextInputSourceOpExec(objectMapper.writeValueAsString(textInputSourceOpDesc)) textScanSourceOpExec.open() val processedTuple: Iterator[Tuple] = textScanSourceOpExec .produceTuple() .map(tupleLike => tupleLike .asInstanceOf[SchemaEnforceable] .enforceSchema(textInputSourceOpDesc.sourceSchema()) ) assert(processedTuple.next().getField("line").equals("line1")) assert(processedTuple.next().getField("line").equals("line2")) assert(processedTuple.next().getField("line").equals("line3")) assert(processedTuple.next().getField("line").equals("line4")) assert(processedTuple.next().getField("line").equals("line5")) assertThrows[java.util.NoSuchElementException](processedTuple.next().getField("line")) textScanSourceOpExec.close() } it should "read first 5 lines of the input text with CRLF separators into corresponding output tuples" in { val inputString: String = readFileIntoString(TestOperators.TestCRLFTextFilePath) textInputSourceOpDesc.attributeType = FileAttributeType.STRING textInputSourceOpDesc.textInput = inputString textInputSourceOpDesc.fileScanLimit = Option(5) val textScanSourceOpExec = new TextInputSourceOpExec(objectMapper.writeValueAsString(textInputSourceOpDesc)) textScanSourceOpExec.open() val processedTuple: Iterator[Tuple] = textScanSourceOpExec .produceTuple() .map(tupleLike => tupleLike .asInstanceOf[SchemaEnforceable] .enforceSchema(textInputSourceOpDesc.sourceSchema()) ) assert(processedTuple.next().getField("line").equals("line1")) assert(processedTuple.next().getField("line").equals("line2")) assert(processedTuple.next().getField("line").equals("line3")) assert(processedTuple.next().getField("line").equals("line4")) assert(processedTuple.next().getField("line").equals("line5")) assertThrows[java.util.NoSuchElementException](processedTuple.next().getField("line")) textScanSourceOpExec.close() } it should "read first 5 lines of the input text into a single output tuple" in { val inputString: String = readFileIntoString(TestOperators.TestTextFilePath) textInputSourceOpDesc.attributeType = FileAttributeType.SINGLE_STRING textInputSourceOpDesc.textInput = inputString val textScanSourceOpExec = new TextInputSourceOpExec(objectMapper.writeValueAsString(textInputSourceOpDesc)) textScanSourceOpExec.open() val processedTuple: Iterator[Tuple] = textScanSourceOpExec .produceTuple() .map(tupleLike => tupleLike .asInstanceOf[SchemaEnforceable] .enforceSchema(textInputSourceOpDesc.sourceSchema()) ) assert( processedTuple .next() .getField[String]("line") .equals("line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10") ) assertThrows[java.util.NoSuchElementException](processedTuple.next().getField("line")) textScanSourceOpExec.close() } it should "read first 5 lines of the input text into corresponding output INTEGER tuples" in { val inputString: String = readFileIntoString(TestOperators.TestNumbersFilePath) textInputSourceOpDesc.attributeType = FileAttributeType.INTEGER textInputSourceOpDesc.textInput = inputString textInputSourceOpDesc.fileScanLimit = Option(5) val textScanSourceOpExec = new TextInputSourceOpExec(objectMapper.writeValueAsString(textInputSourceOpDesc)) textScanSourceOpExec.open() val processedTuple: Iterator[Tuple] = textScanSourceOpExec .produceTuple() .map(tupleLike => tupleLike .asInstanceOf[SchemaEnforceable] .enforceSchema(textInputSourceOpDesc.sourceSchema()) ) assert(processedTuple.next().getField[Int]("line") == 1) assert(processedTuple.next().getField[Int]("line") == 2) assert(processedTuple.next().getField[Int]("line") == 3) assert(processedTuple.next().getField[Int]("line") == 4) assert(processedTuple.next().getField[Int]("line") == 5) assertThrows[java.util.NoSuchElementException](processedTuple.next().getField("line")) textScanSourceOpExec.close() } /** * Helper function using UTF-8 encoding to read text file * into String * * @param filePath path of input file * @return entire file represented as String */ def readFileIntoString(filePath: String): String = { val path: Path = Paths.get(filePath) new String(Files.readAllBytes(path), StandardCharsets.UTF_8) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/symmetricDifference/SymmetricDifferenceOpExecSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.symmetricDifference import org.apache.texera.amber.core.tuple._ import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class SymmetricDifferenceOpExecSpec extends AnyFlatSpec with BeforeAndAfter { var opExec: SymmetricDifferenceOpExec = _ var counter: Int = 0 val schema: Schema = Schema() .add(new Attribute("field1", AttributeType.STRING)) .add(new Attribute("field2", AttributeType.INTEGER)) .add(new Attribute("field3", AttributeType.BOOLEAN)) def tuple(): Tuple = { counter += 1 Tuple .builder(schema) .addSequentially(Array("hello", Int.box(counter), Boolean.box(true))) .build() } before { opExec = new SymmetricDifferenceOpExec() } it should "open" in { opExec.open() } it should "work with basic two input streams with no duplicates" in { val input1 = 0 val input2 = 1 opExec.open() counter = 0 val commonTuples = (1 to 10).map(_ => tuple()).toList (0 to 7).map(i => { opExec.processTuple(commonTuples(i), input1) }) assert(opExec.onFinish(input1).isEmpty) (5 to 9).map(i => { opExec.processTuple(commonTuples(i), input2) }) val outputTuples: Set[TupleLike] = opExec.onFinish(input2).toSet assert( outputTuples.equals(commonTuples.slice(0, 5).toSet.union(commonTuples.slice(8, 10).toSet)) ) opExec.close() } it should "work with one empty input upstream after a data stream" in { val input1 = 0 val input2 = 1 opExec.open() counter = 0 val commonTuples = (1 to 10).map(_ => tuple()).toList (0 to 9).map(i => { opExec.processTuple(commonTuples(i), input1) }) assert(opExec.onFinish(input1).isEmpty) val outputTuples: Set[Tuple] = opExec .onFinish(input2) .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(schema)) .toSet assert(outputTuples.equals(commonTuples.toSet)) opExec.close() } it should "work with one empty input upstream after a data stream - other order" in { val input1 = 0 val input2 = 1 opExec.open() counter = 0 val commonTuples = (1 to 10).map(_ => tuple()).toList (0 to 9).map(i => { opExec.processTuple(commonTuples(i), input2) }) assert(opExec.onFinish(input2).isEmpty) val outputTuples: Set[Tuple] = opExec .onFinish(input1) .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(schema)) .toSet assert(outputTuples.equals(commonTuples.toSet)) opExec.close() } it should "work with one empty input upstream before a data stream" in { val input1 = 0 val input2 = 1 opExec.open() counter = 0 val commonTuples = (1 to 10).map(_ => tuple()).toList assert(opExec.onFinish(input2).isEmpty) (0 to 9).map(i => { opExec.processTuple(commonTuples(i), input1) }) val outputTuples: Set[Tuple] = opExec .onFinish(input1) .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(schema)) .toSet assert(outputTuples.equals(commonTuples.toSet)) opExec.close() } it should "work with one empty input upstream during a data stream" in { val input1 = 0 val input2 = 1 opExec.open() counter = 0 val commonTuples = (1 to 10).map(_ => tuple()).toList (0 to 5).map(i => { opExec.processTuple(commonTuples(i), input1) }) assert(opExec.onFinish(input2).isEmpty) (6 to 9).map(i => { opExec.processTuple(commonTuples(i), input1) }) val outputTuples: Set[Tuple] = opExec .onFinish(input1) .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(schema)) .toSet assert(outputTuples.equals(commonTuples.toSet)) opExec.close() } it should "work with two empty input upstreams" in { opExec.open() assert(opExec.onFinish(0).isEmpty) assert(opExec.onFinish(1).isEmpty) opExec.close() } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/timeSeriesPlot/TimeSeriesOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.timeSeriesPlot import org.apache.texera.amber.operator.visualization.timeSeriesplot.TimeSeriesOpDesc import org.scalatest.funsuite.AnyFunSuite class TimeSeriesOpDescSpec extends AnyFunSuite { test("generatePythonCode returns non-empty python code") { val op = new TimeSeriesOpDesc // set minimal required fields op.timeColumn = "date" op.valueColumn = "value" op.CategoryColumn = "cat" op.facetColumn = "facet" op.plotType = "line" op.showRangeSlider = false val py = op.generatePythonCode() assert(py.nonEmpty) assert(py.contains("class ProcessTableOperator")) assert(py.contains("def process_table")) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/typecasting/TypeCastingOpExecSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.typecasting import org.apache.texera.amber.core.tuple._ import org.apache.texera.amber.util.JSONUtils.objectMapper import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class TypeCastingOpExecSpec extends AnyFlatSpec with BeforeAndAfter { val tupleSchema: Schema = Schema() .add(new Attribute("field1", AttributeType.STRING)) .add(new Attribute("field2", AttributeType.INTEGER)) .add(new Attribute("field3", AttributeType.BOOLEAN)) .add(new Attribute("field4", AttributeType.LONG)) val castToSchema: Schema = Schema() .add(new Attribute("field1", AttributeType.STRING)) .add(new Attribute("field2", AttributeType.STRING)) .add(new Attribute("field3", AttributeType.STRING)) .add(new Attribute("field4", AttributeType.LONG)) val castingUnit1 = new TypeCastingUnit() castingUnit1.attribute = "field2" castingUnit1.resultType = AttributeType.STRING val castingUnit2 = new TypeCastingUnit() castingUnit2.attribute = "field3" castingUnit2.resultType = AttributeType.STRING val castingUnits: List[TypeCastingUnit] = List(castingUnit1, castingUnit2) val opDesc: TypeCastingOpDesc = new TypeCastingOpDesc() opDesc.typeCastingUnits = castingUnits val tuple: Tuple = Tuple .builder(tupleSchema) .add(new Attribute("field1", AttributeType.STRING), "hello") .add(new Attribute("field2", AttributeType.INTEGER), 1) .add( new Attribute("field3", AttributeType.BOOLEAN), true ) .add( new Attribute("field4", AttributeType.LONG), 3L ) .build() it should "open" in { val typeCastingOpExec = new TypeCastingOpExec(objectMapper.writeValueAsString(opDesc)) typeCastingOpExec.open() } it should "process Tuple" in { val typeCastingOpExec = new TypeCastingOpExec(objectMapper.writeValueAsString(opDesc)) typeCastingOpExec.open() val outputTuple = typeCastingOpExec .processTuple(tuple, 0) .next() .asInstanceOf[SchemaEnforceable] .enforceSchema(castToSchema) assert(outputTuple.length == 4) assert(outputTuple.getField("field1").asInstanceOf[String] == "hello") assert(outputTuple.getField("field2").asInstanceOf[String] == "1") assert(outputTuple.getField("field3").asInstanceOf[String] == "true") assert(outputTuple.getField("field4").asInstanceOf[Long] == 3L) assert("hello" == outputTuple.getField[String](0)) assert(outputTuple.getField[String](1) == "1") assert(outputTuple.getField[String](2) == "true") assert(outputTuple.getField[Long](3) == 3L) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/PythonLambdaFunctionOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.udf.python import org.apache.texera.amber.core.tuple.{Attribute, AttributeType, Schema} import org.apache.texera.amber.core.workflow.PortIdentity import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class PythonLambdaFunctionOpDescSpec extends AnyFlatSpec with BeforeAndAfter { val schema = new Schema( new Attribute("column_str", AttributeType.STRING), new Attribute("column_int", AttributeType.INTEGER), new Attribute("column_bool", AttributeType.BOOLEAN) ) var opDesc: PythonLambdaFunctionOpDesc = _ before { opDesc = new PythonLambdaFunctionOpDesc() } it should "add one new column into schema successfully" in { opDesc.lambdaAttributeUnits ++= List( new LambdaAttributeUnit( "Add New Column", "tuple_['column_str']", "newColumn1", AttributeType.STRING ) ) val outputSchema = opDesc.getExternalOutputSchemas(Map(PortIdentity() -> schema)).values.head assert(outputSchema.getAttributes.length == 4) } it should "add multiple new columns into schema successfully" in { opDesc.lambdaAttributeUnits ++= List( new LambdaAttributeUnit( "Add New Column", "tuple_['column_str']", "newColumn1", AttributeType.STRING ), new LambdaAttributeUnit( "Add New Column", "tuple_['column_int']", "newColumn2", AttributeType.INTEGER ) ) val outputSchema = opDesc.getExternalOutputSchemas(Map(PortIdentity() -> schema)).values.head assert(outputSchema.getAttributes.length == 5) } it should "build successfully when there is no new column but with modifying the existing column" in { opDesc.lambdaAttributeUnits ++= List( new LambdaAttributeUnit( "column_str", "tuple_['column_str'] + hello", "", AttributeType.STRING ) ) val outputSchema = opDesc.getExternalOutputSchemas(Map(PortIdentity() -> schema)).values.head assert(outputSchema.getAttributes.length == 3) } it should "raise exception if the new column name already exists" in { opDesc.lambdaAttributeUnits ++= List( new LambdaAttributeUnit( "Add New Column", "tuple_['column_str']", "column_str", AttributeType.STRING ) ) assertThrows[RuntimeException] { opDesc.getExternalOutputSchemas(Map(PortIdentity() -> schema)).values.head } } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/unneststring/UnnestStringOpExecSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.unneststring import org.apache.texera.amber.core.tuple._ import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.util.JSONUtils.objectMapper import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class UnnestStringOpExecSpec extends AnyFlatSpec with BeforeAndAfter { val tupleSchema: Schema = Schema() .add(new Attribute("field1", AttributeType.STRING)) .add(new Attribute("field2", AttributeType.INTEGER)) .add(new Attribute("field3", AttributeType.STRING)) val tuple: Tuple = Tuple .builder(tupleSchema) .add(new Attribute("field1", AttributeType.STRING), "a-b-c") .add(new Attribute("field2", AttributeType.INTEGER), 1) .add(new Attribute("field3", AttributeType.STRING), "a") .build() var opExec: UnnestStringOpExec = _ var opDesc: UnnestStringOpDesc = _ var outputSchema: Schema = _ before { opDesc = new UnnestStringOpDesc() opDesc.attribute = "field1" opDesc.delimiter = "-" opDesc.resultAttribute = "split" } it should "open" in { opDesc.attribute = "field1" opDesc.delimiter = "-" opExec = new UnnestStringOpExec(objectMapper.writeValueAsString(opDesc)) outputSchema = opDesc.getExternalOutputSchemas(Map(PortIdentity() -> tupleSchema)).values.head opExec.open() assert(opExec.flatMapFunc != null) } it should "split value in the given attribute and output the split result in the result attribute, one for each tuple" in { opDesc.attribute = "field1" opDesc.delimiter = "-" opExec = new UnnestStringOpExec(objectMapper.writeValueAsString(opDesc)) outputSchema = opDesc.getExternalOutputSchemas(Map(PortIdentity() -> tupleSchema)).values.head opExec.open() val processedTuple = opExec .processTuple(tuple, 0) .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema)) assert(processedTuple.next().getField("split").equals("a")) assert(processedTuple.next().getField("split").equals("b")) assert(processedTuple.next().getField("split").equals("c")) assertThrows[java.util.NoSuchElementException](processedTuple.next().getField("split")) opExec.close() } it should "generate the correct tuple when there is no delimiter in the value" in { opDesc.attribute = "field3" opDesc.delimiter = "-" opExec = new UnnestStringOpExec(objectMapper.writeValueAsString(opDesc)) outputSchema = opDesc.getExternalOutputSchemas(Map(PortIdentity() -> tupleSchema)).values.head opExec.open() val processedTuple = opExec .processTuple(tuple, 0) .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema)) assert(processedTuple.next().getField("split").equals("a")) assertThrows[java.util.NoSuchElementException](processedTuple.next().getField("split")) opExec.close() } it should "only contain split results that are not null" in { opDesc.attribute = "field1" opDesc.delimiter = "/" opExec = new UnnestStringOpExec(objectMapper.writeValueAsString(opDesc)) outputSchema = opDesc.getExternalOutputSchemas(Map(PortIdentity() -> tupleSchema)).values.head val tuple: Tuple = Tuple .builder(tupleSchema) .add(new Attribute("field1", AttributeType.STRING), "//a//b/") .add(new Attribute("field2", AttributeType.INTEGER), 1) .add(new Attribute("field3", AttributeType.STRING), "a") .build() opExec.open() val processedTuple = opExec .processTuple(tuple, 0) .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema)) assert(processedTuple.next().getField("split").equals("a")) assert(processedTuple.next().getField("split").equals("b")) assertThrows[java.util.NoSuchElementException](processedTuple.next().getField("split")) opExec.close() } it should "split by regex delimiter" in { opDesc.attribute = "field1" opDesc.delimiter = "<\\d*>" opExec = new UnnestStringOpExec(objectMapper.writeValueAsString(opDesc)) outputSchema = opDesc.getExternalOutputSchemas(Map(PortIdentity() -> tupleSchema)).values.head val tuple: Tuple = Tuple .builder(tupleSchema) .add(new Attribute("field1", AttributeType.STRING), "<>a<1>b<12>") .add(new Attribute("field2", AttributeType.INTEGER), 1) .add(new Attribute("field3", AttributeType.STRING), "a") .build() opExec.open() val processedTuple = opExec .processTuple(tuple, 0) .map(tupleLike => tupleLike.asInstanceOf[SchemaEnforceable].enforceSchema(outputSchema)) assert(processedTuple.next().getField("split").equals("a")) assert(processedTuple.next().getField("split").equals("b")) assertThrows[java.util.NoSuchElementException](processedTuple.next().getField("split")) opExec.close() } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/DotPlot/DotPlotOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.DotPlot import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class DotPlotOpDescSpec extends AnyFlatSpec with BeforeAndAfter { var opDesc: DotPlotOpDesc = _ before { opDesc = new DotPlotOpDesc() } it should "generate a plotly python figure with count aggregation" in { opDesc.countAttribute = "column1" assert( opDesc .createPlotlyFigure() .plain .contains( "table = table.groupby([column1])[column1].count().reset_index(name='counts')" ) ) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/ImageViz/ImageVisualizerOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.ImageViz import org.apache.texera.amber.core.tuple.AttributeType import org.apache.texera.amber.operator.metadata.OperatorGroupConstants import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers class ImageVisualizerOpDescSpec extends AnyFlatSpec with BeforeAndAfter with Matchers { var opDesc: ImageVisualizerOpDesc = _ before { opDesc = new ImageVisualizerOpDesc() } it should "currently throw NullPointerException when binaryContent is uninitialized" in { // Documents the present behavior without claiming it is the contract: // `binaryContent` is declared `var binaryContent: EncodableString = _`, // so an uninitialized reference field defaults to null and the // `assert(binaryContent.nonEmpty)` inside `createBinaryData` reaches // `null.nonEmpty` and throws NPE before the assert message can fire. assertThrows[NullPointerException] { opDesc.createBinaryData() } } it should "eventually reject missing binaryContent with a controlled error (pendingUntilFixed)" in pendingUntilFixed { // Intended contract: because `binaryContent` is declared // `@JsonProperty(required = true)`, an unconfigured operator should // surface a domain error (AssertionError or IllegalArgumentException), // not an NPE from dereferencing null. Using pendingUntilFixed so a // future validation fix flips this test from Pending to a deliberate // failure that forces removal of the marker. val ex = intercept[RuntimeException] { opDesc.createBinaryData() } ex shouldBe a[AssertionError] } "ImageVisualizerOpDesc.operatorInfo" should "advertise the user-friendly name and Media group" in { val info = opDesc.operatorInfo info.userFriendlyName shouldBe "Image Visualizer" info.operatorGroupName shouldBe OperatorGroupConstants.VISUALIZATION_MEDIA_GROUP info.operatorDescription should include("image") } it should "expose exactly one output port wired through forVisualization" in { opDesc.operatorInfo.outputPorts should have length 1 } "ImageVisualizerOpDesc.getOutputSchemas" should "return a single-port schema with an html-content STRING column" in { opDesc.binaryContent = "image_bytes" val schemas = opDesc.getOutputSchemas(Map.empty) schemas should have size 1 val (portId, schema) = schemas.head portId shouldBe opDesc.operatorInfo.outputPorts.head.id schema.getAttributes should have length 1 schema.getAttributes.head.getName shouldBe "html-content" schema.getAttributes.head.getType shouldBe AttributeType.STRING } "ImageVisualizerOpDesc.generatePythonCode" should "render a UDFOperatorV2 source with a runtime column-decode site" in { // EncodableString fields are NOT emitted as literal strings — the pyb // macro wraps them in `self.decode_python_template.decode("")` // calls so the column name resolves at runtime. Verify the structure // (operator class, body helper, decode site) instead of a literal name. opDesc.binaryContent = "image_bytes" val code = opDesc.generatePythonCode() code should include("class ProcessTupleOperator(UDFOperatorV2)") code should include("encode_image_to_html") code should include("decode_python_template") } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/barChart/BarChartOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.barChart import org.apache.texera.amber.core.tuple.AttributeType import org.apache.texera.amber.operator.metadata.OperatorGroupConstants import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import java.nio.charset.StandardCharsets import java.util.Base64 class BarChartOpDescSpec extends AnyFlatSpec with BeforeAndAfter with Matchers { var opDesc: BarChartOpDesc = _ before { opDesc = new BarChartOpDesc() } it should "throw assertion error if value is empty" in { assertThrows[AssertionError] { opDesc.manipulateTable() } } it should "list titles of axes in the python code" in { // The plain (un-encoded) template body still carries the literal column // names; only the encoded `generatePythonCode` output runs them through // base64 + decode_python_template wrapping. opDesc.fields = "geo.state_name" opDesc.value = "person.count" val temp = opDesc.manipulateTable().plain assert(temp.contains("geo.state_name")) assert(temp.contains("person.count")) } it should "throw assertion error if chart is empty" in { assertThrows[AssertionError] { opDesc.manipulateTable() } } "BarChartOpDesc.operatorInfo" should "advertise the user-friendly name and Basic group" in { val info = opDesc.operatorInfo info.userFriendlyName shouldBe "Bar Chart" info.operatorGroupName shouldBe OperatorGroupConstants.VISUALIZATION_BASIC_GROUP info.operatorDescription should include("Bar Chart") } it should "expose exactly one output port wired through forVisualization" in { opDesc.operatorInfo.outputPorts should have length 1 } "BarChartOpDesc.getOutputSchemas" should "return a single-port schema with an html-content STRING column" in { opDesc.value = "v" opDesc.fields = "f" val schemas = opDesc.getOutputSchemas(Map.empty) schemas should have size 1 val (portId, schema) = schemas.head portId shouldBe opDesc.operatorInfo.outputPorts.head.id schema.getAttributes should have length 1 schema.getAttributes.head.getName shouldBe "html-content" schema.getAttributes.head.getType shouldBe AttributeType.STRING } "BarChartOpDesc.generatePythonCode" should "render a UDFTableOperator source with runtime decode sites for value AND fields" in { // Use distinct sentinels and assert on the exact base64-wrapped decode // expressions so the test actually proves both `value` *and* `fields` // were wrapped through wrapWithPythonDecoderExpr. A generic // `decodeOccurrences >= 2` could be satisfied by `value` alone since // both fields appear in multiple template positions. opDesc.value = "VAL_SENT" opDesc.fields = "FIELDS_SENT" val code = opDesc.generatePythonCode() code should include("class ProcessTableOperator(UDFTableOperator)") code should include("plotly.express") def b64(s: String): String = Base64.getEncoder.encodeToString(s.getBytes(StandardCharsets.UTF_8)) code should include(s"self.decode_python_template('${b64("VAL_SENT")}')") code should include(s"self.decode_python_template('${b64("FIELDS_SENT")}')") code should not include "VAL_SENT" code should not include "FIELDS_SENT" } it should "fail-fast when value or fields is unset (asserts inside manipulateTable)" in { // manipulateTable asserts nonEmpty on value AND fields with explicit // messages ("Value column cannot be empty" / "Fields cannot be empty"). val ex = intercept[AssertionError](opDesc.generatePythonCode()) ex.getMessage should (include("Value column") or include("Fields")) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/bubbleChart/BubbleChartOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.bubbleChart import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class BubbleChartOpDescSpec extends AnyFlatSpec with BeforeAndAfter { var opDesc: BubbleChartOpDesc = _ before { opDesc = new BubbleChartOpDesc() } it should "generate a plotly python figure with 3 columns" in { opDesc.xValue = "column1" opDesc.yValue = "column2" opDesc.zValue = "column3" opDesc.enableColor = false assert( opDesc .createPlotlyFigure() .plain .contains( "fig = go.Figure(px.scatter(table, x=column1, y=column2, size=column3, size_max=100))" ) ) } it should "throw assertion error if variable xValue is empty" in { assertThrows[AssertionError] { opDesc.createPlotlyFigure() } } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/bulletChart/BulletChartOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.bulletChart import org.apache.texera.amber.core.tuple.AttributeType import org.apache.texera.amber.operator.metadata.OperatorGroupConstants import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import java.util import java.util.{List => JList} class BulletChartOpDescSpec extends AnyFlatSpec with Matchers { private def configured: BulletChartOpDesc = { val op = new BulletChartOpDesc op.value = "actualValue" op.deltaReference = "100" op } "BulletChartOpDesc.operatorInfo" should "advertise the user-friendly name and Financial group" in { val info = (new BulletChartOpDesc).operatorInfo info.userFriendlyName shouldBe "Bullet Chart" info.operatorGroupName shouldBe OperatorGroupConstants.VISUALIZATION_FINANCIAL_GROUP info.operatorDescription should include("Bullet Chart") } it should "expose exactly one output port wired through forVisualization" in { (new BulletChartOpDesc).operatorInfo.outputPorts should have length 1 } "BulletChartOpDesc.getOutputSchemas" should "return a single-port schema with an html-content STRING column" in { val op = configured val schemas = op.getOutputSchemas(Map.empty) schemas should have size 1 val (portId, schema) = schemas.head portId shouldBe op.operatorInfo.outputPorts.head.id schema.getAttributes should have length 1 schema.getAttributes.head.getName shouldBe "html-content" schema.getAttributes.head.getType shouldBe AttributeType.STRING } "BulletChartOpDesc.generatePythonCode" should "render Python source with a runtime decode site for the value column" in { // EncodableString fields are NOT emitted as literal strings — the pyb // macro wraps them in `self.decode_python_template.decode("")` // calls. The rendered source must reference the decoder symbol at least // for `value` and `deltaReference`. val code = configured.generatePythonCode() code should include("plotly.graph_objects") val decodeOccurrences = "decode_python_template".r.findAllIn(code).length decodeOccurrences should be >= 2 } it should "default to an empty steps list when none are configured" in { // The bullet-chart template ships with several unrelated `[]` literals // (`colors`, `valid_steps`, `step_errors`, `steps_list`, `html_chunks`), // so a bare `code should include("[]")` is too weak. Anchor on the // generated `steps_data = ...` literal directly so a regression that // makes it non-empty would actually fail the assertion. val code = configured.generatePythonCode() code should include regex """steps_data\s*=\s*\[\]""" } it should "include each configured step's start/end JSON keys with extra decode sites" in { val op = configured val steps: JList[BulletChartStepDefinition] = new util.ArrayList[BulletChartStepDefinition]() steps.add(new BulletChartStepDefinition("0", "50")) steps.add(new BulletChartStepDefinition("50", "100")) op.steps = steps val code = op.generatePythonCode() code should include("\"start\":") code should include("\"end\":") // Two steps × 2 EncodableString fields each = 4 extra decode sites on // top of the value/deltaReference decodes from the base configuration. val baseDecodes = "decode_python_template".r.findAllIn(configured.generatePythonCode()).length val withSteps = "decode_python_template".r.findAllIn(code).length withSteps shouldBe baseDecodes + 4 } it should "currently render a code block even with the default empty configuration (no assert guard)" in { // Documents the present behavior: BulletChartOpDesc has no assert // guards inside generatePythonCode, so empty defaults still produce // syntactically valid Python source. The intended contract lives in // the pendingUntilFixed test below. val op = new BulletChartOpDesc val code = op.generatePythonCode() code should include("plotly.graph_objects") } it should "eventually reject empty required value/deltaReference like FunnelPlot/ImageVisualizer (pendingUntilFixed)" in pendingUntilFixed { // Intended contract: `value` and `deltaReference` are marked required // on `BulletChartOpDesc`, so generatePythonCode on a default-constructed // instance should raise instead of rendering empty-string column refs. // Using pendingUntilFixed so a future validation fix flips this test // from Pending to a deliberate failure and forces removal of the marker. val op = new BulletChartOpDesc intercept[RuntimeException] { op.generatePythonCode() } } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/ecdfPlot/ECDFPlotOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.ecdfPlot import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class ECDFPlotOpDescSpec extends AnyFlatSpec with BeforeAndAfter { var opDesc: ECDFPlotOpDesc = _ before { opDesc = new ECDFPlotOpDesc() } it should "throw assertion error if value column is empty" in { assertThrows[AssertionError] { opDesc.manipulateTable() } } it should "generate a plotly ecdf figure with optional parameters" in { opDesc.valueColumn = "score" opDesc.colorColumn = "group" opDesc.separateBy = "category" opDesc.yAxisMode = "count" opDesc.cdfMode = "reversed" opDesc.orientation = "horizontal" opDesc.showMarkers = true opDesc.marginal = "histogram" val plain = opDesc.createPlotlyFigure().plain assert(plain.contains("fig = px.ecdf(table")) assert(plain.contains("ecdfnorm=None")) assert(plain.contains("ecdfmode=self.decode_python_template")) assert(plain.contains("orientation='h'")) assert(plain.contains("markers=True")) assert(plain.contains("marginal=self.decode_python_template")) assert(plain.contains("x=self.decode_python_template")) assert(plain.contains("color=self.decode_python_template")) assert(plain.contains("facet_col=self.decode_python_template")) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/filledAreaPlot/FilledAreaPlotOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.filledAreaPlot import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class FilledAreaPlotOpDescSpec extends AnyFlatSpec with BeforeAndAfter { var opDesc: FilledAreaPlotOpDesc = _ before { opDesc = new FilledAreaPlotOpDesc() } it should "throw error if X is empty" in { val y = "test1" val group = "test2" opDesc.y = y opDesc.lineGroup = group assertThrows[AssertionError] { opDesc.createPlotlyFigure() } } it should "throw error if Y is empty" in { val x = "test1" val group = "test2" opDesc.x = x opDesc.lineGroup = group assertThrows[AssertionError] { opDesc.createPlotlyFigure() } } it should "throw error if LineGroup is not indicated facet column is checked" in { val x = "test1" val y = "test2" opDesc.x = x opDesc.y = y opDesc.facetColumn = true opDesc.color = "color" assertThrows[AssertionError] { opDesc.createPlotlyFigure() } } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/funnelPlot/FunnelPlotOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.funnelPlot import org.apache.texera.amber.core.tuple.AttributeType import org.apache.texera.amber.operator.metadata.OperatorGroupConstants import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers class FunnelPlotOpDescSpec extends AnyFlatSpec with Matchers { private def configured: FunnelPlotOpDesc = { val op = new FunnelPlotOpDesc op.x = "stage" op.y = "count" op } "FunnelPlotOpDesc.operatorInfo" should "advertise the user-friendly name and Financial group" in { val info = (new FunnelPlotOpDesc).operatorInfo info.userFriendlyName shouldBe "Funnel Plot" info.operatorGroupName shouldBe OperatorGroupConstants.VISUALIZATION_FINANCIAL_GROUP info.operatorDescription should include("Funnel") } it should "expose exactly one output port wired through forVisualization" in { (new FunnelPlotOpDesc).operatorInfo.outputPorts should have length 1 } "FunnelPlotOpDesc.getOutputSchemas" should "return a single-port schema with an html-content STRING column" in { val op = configured val schemas = op.getOutputSchemas(Map.empty) schemas should have size 1 val (portId, schema) = schemas.head portId shouldBe op.operatorInfo.outputPorts.head.id schema.getAttributes should have length 1 schema.getAttributes.head.getName shouldBe "html-content" schema.getAttributes.head.getType shouldBe AttributeType.STRING } "FunnelPlotOpDesc.generatePythonCode" should "render a UDFTableOperator source with runtime decode sites for x and y" in { // EncodableString fields are NOT emitted as literal strings — the pyb // macro wraps them in `self.decode_python_template.decode("")` // calls. Each configured column becomes one decode site, so x + y must // produce at least two distinct decodes in the rendered source. val code = configured.generatePythonCode() code should include("class ProcessTableOperator(UDFTableOperator)") code should include("plotly.express") val decodeOccurrences = "decode_python_template".r.findAllIn(code).length decodeOccurrences should be >= 2 } it should "render the optional color argument only when color is configured" in { val without = configured.generatePythonCode() val withColor = { val op = configured op.color = "category" op.generatePythonCode() } without should not include "color=" withColor should include("color=") // With color set, the rendered source has one extra decode site beyond // the two for x and y. val withDecodes = "decode_python_template".r.findAllIn(withColor).length val withoutDecodes = "decode_python_template".r.findAllIn(without).length withDecodes shouldBe withoutDecodes + 1 } it should "fail-fast when required x/y are unset (the assert guards inside createPlotlyFigure)" in { // Pin: createPlotlyFigure asserts nonEmpty on both x and y. The fields // are initialized to "" so the assert path is reached (not the NPE path // that ImageVisualizerOpDesc hits). val op = new FunnelPlotOpDesc op.x = "" op.y = "" assertThrows[AssertionError](op.generatePythonCode()) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/ganttChart/GanttChartOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.ganttChart import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class GanttChartOpDescSpec extends AnyFlatSpec with BeforeAndAfter { var opDesc: GanttChartOpDesc = _ before { opDesc = new GanttChartOpDesc() } it should "generate a plotly python figure with 3 columns and no color" in { opDesc.start = "start" opDesc.finish = "finish" opDesc.task = "task" opDesc.color = "" assert( opDesc .createPlotlyFigure() .plain .contains( "fig = px.timeline(table, x_start=start, x_end=finish, y=task )" ) ) } it should "generate a plotly python figure with 3 columns and color" in { opDesc.start = "start" opDesc.finish = "finish" opDesc.task = "task" opDesc.color = "color" val plain = opDesc .createPlotlyFigure() .plain assert( plain .contains( "fig = px.timeline(table, x_start=start, x_end=finish, y=task , color=color )" ) ) } it should "generate a plotly python figure with 3 columns and color and pattern" in { opDesc.start = "start" opDesc.finish = "finish" opDesc.task = "task" opDesc.color = "color" opDesc.pattern = "task" assert( opDesc .createPlotlyFigure() .plain .contains( "fig = px.timeline(table, x_start=start, x_end=finish, y=task , color=color , pattern_shape=task)" ) ) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/heatMap/HeatMapOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.heatMap import org.apache.texera.amber.core.tuple.AttributeType import org.apache.texera.amber.operator.metadata.OperatorGroupConstants import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers class HeatMapOpDescSpec extends AnyFlatSpec with Matchers { private def configured: HeatMapOpDesc = { val op = new HeatMapOpDesc op.x = "ax" op.y = "ay" op.value = "v" op } "HeatMapOpDesc.operatorInfo" should "advertise the user-friendly name and Scientific group" in { val info = (new HeatMapOpDesc).operatorInfo info.userFriendlyName shouldBe "Heatmap" info.operatorGroupName shouldBe OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP info.operatorDescription should include("HeatMap") } it should "expose exactly one output port wired through forVisualization" in { (new HeatMapOpDesc).operatorInfo.outputPorts should have length 1 } "HeatMapOpDesc.getOutputSchemas" should "return a single-port schema with an html-content STRING column" in { val op = configured val schemas = op.getOutputSchemas(Map.empty) schemas should have size 1 val (portId, schema) = schemas.head portId shouldBe op.operatorInfo.outputPorts.head.id schema.getAttributes should have length 1 schema.getAttributes.head.getName shouldBe "html-content" schema.getAttributes.head.getType shouldBe AttributeType.STRING } "HeatMapOpDesc.generatePythonCode" should "render a UDFTableOperator source with three runtime decode sites for x/y/value" in { // EncodableString fields are wrapped in `self.decode_python_template(...)` // calls by the pyb macro; pin a structural count instead of literal names. val code = configured.generatePythonCode() code should include("class ProcessTableOperator(UDFTableOperator)") code should include("plotly.graph_objects") val decodeOccurrences = "decode_python_template".r.findAllIn(code).length decodeOccurrences should be >= 3 } it should "fail-fast when any required field is unset (asserts inside createHeatMap)" in { // createHeatMap asserts nonEmpty on x, y, AND value. Empty defaults // ("") hit the assert path and surface as AssertionError. val op = new HeatMapOpDesc assertThrows[AssertionError](op.generatePythonCode()) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/hierarchychart/HierarchyChartOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.hierarchychart import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class HierarchyChartOpDescSpec extends AnyFlatSpec with BeforeAndAfter { var opDesc: HierarchyChartOpDesc = _ before { opDesc = new HierarchyChartOpDesc() } it should "generate a list of hierarchy sections in the python code" in { val attributes = Array.fill(3)(new HierarchySection()) attributes(0).attributeName = "column_a" attributes(1).attributeName = "column_b" attributes(2).attributeName = "column_c" opDesc.hierarchy = attributes.toList opDesc.hierarchyChartType = HierarchyChartType.TREEMAP opDesc.hierarchyChartType = HierarchyChartType.SUNBURSTCHART } it should "throw assertion error if hierarchy is empty" in { assertThrows[AssertionError] { opDesc.createPlotlyFigure() } } it should "throw assertion error if value is empty" in { assertThrows[AssertionError] { opDesc.manipulateTable() } } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/htmlviz/HtmlVizOpExecSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.htmlviz import org.apache.texera.amber.core.tuple._ import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.util.JSONUtils.objectMapper import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class HtmlVizOpExecSpec extends AnyFlatSpec with BeforeAndAfter { val schema = new Schema( new Attribute("field1", AttributeType.STRING), new Attribute("field2", AttributeType.STRING) ) val opDesc: HtmlVizOpDesc = new HtmlVizOpDesc() val outputSchema: Schema = opDesc.getExternalOutputSchemas(Map(PortIdentity() -> schema)).values.head def tuple(): Tuple = Tuple .builder(schema) .addSequentially(Array("hello", "")) .build() it should "process a target field" in { opDesc.htmlContentAttrName = "field1" val htmlVizOpExec = new HtmlVizOpExec(objectMapper.writeValueAsString(opDesc)) htmlVizOpExec.open() val processedTuple: Tuple = htmlVizOpExec .processTuple(tuple(), 0) .next() .asInstanceOf[SchemaEnforceable] .enforceSchema(outputSchema) assert(processedTuple.getField("html-content").asInstanceOf[String] == "hello") } it should "process another target field" in { opDesc.htmlContentAttrName = "field2" val htmlVizOpExec = new HtmlVizOpExec(objectMapper.writeValueAsString(opDesc)) htmlVizOpExec.open() val processedTuple: Tuple = htmlVizOpExec .processTuple(tuple(), 0) .next() .asInstanceOf[SchemaEnforceable] .enforceSchema(outputSchema) assert(processedTuple.getField("html-content").asInstanceOf[String] == "") } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/lineChart/LineChartOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.lineChart import org.apache.texera.amber.core.tuple.AttributeType import org.apache.texera.amber.operator.metadata.OperatorGroupConstants import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import java.nio.charset.StandardCharsets import java.util import java.util.Base64 class LineChartOpDescSpec extends AnyFlatSpec with Matchers { private def lineConfig(x: String, y: String): LineConfig = { val c = new LineConfig c.xValue = x c.yValue = y c } private def configured: LineChartOpDesc = { val op = new LineChartOpDesc op.xLabel = "x_col" op.yLabel = "y_col" val ls = new util.ArrayList[LineConfig]() ls.add(lineConfig("x_col", "y_col")) op.lines = ls op } "LineChartOpDesc.operatorInfo" should "advertise the user-friendly name and Basic group" in { val info = (new LineChartOpDesc).operatorInfo info.userFriendlyName shouldBe "Line Chart" info.operatorGroupName shouldBe OperatorGroupConstants.VISUALIZATION_BASIC_GROUP info.operatorDescription should include("line chart") } it should "expose exactly one output port wired through forVisualization" in { (new LineChartOpDesc).operatorInfo.outputPorts should have length 1 } "LineChartOpDesc.getOutputSchemas" should "return a single-port schema with an html-content STRING column" in { val op = configured val schemas = op.getOutputSchemas(Map.empty) schemas should have size 1 val (portId, schema) = schemas.head portId shouldBe op.operatorInfo.outputPorts.head.id schema.getAttributes should have length 1 schema.getAttributes.head.getName shouldBe "html-content" schema.getAttributes.head.getType shouldBe AttributeType.STRING } "LineChartOpDesc.generatePythonCode" should "render Python source with runtime decode sites for both labels" in { // Use distinct sentinels for the two LABELS *and* the LineConfig values // so the spec actually exercises both label fields. Asserting on the // exact base64 payloads proves each field was wrapped through // wrapWithPythonDecoderExpr individually — `decodeOccurrences >= 2` // could otherwise be satisfied by xValue/yValue alone. val op = new LineChartOpDesc op.xLabel = "X_LBL_SENT" op.yLabel = "Y_LBL_SENT" val ls = new util.ArrayList[LineConfig]() ls.add(lineConfig("X_VAL_SENT", "Y_VAL_SENT")) op.lines = ls val code = op.generatePythonCode() code should include("plotly") def b64(s: String): String = Base64.getEncoder.encodeToString(s.getBytes(StandardCharsets.UTF_8)) code should include(s"self.decode_python_template('${b64("X_LBL_SENT")}')") code should include(s"self.decode_python_template('${b64("Y_LBL_SENT")}')") // Raw sentinels must be absent from the encoded output — their presence // would mean the field was never run through the decoder wrapper. code should not include "X_LBL_SENT" code should not include "Y_LBL_SENT" } it should "raise AssertionError when lines is left at its default (empty list)" in { // `var lines: util.List[LineConfig]` defaults to an empty ArrayList. // `createPlotlyFigure` asserts nonEmpty on lines before iterating, so a // default-constructed LineChartOpDesc raises AssertionError with a // descriptive message rather than proceeding with no traces. val op = new LineChartOpDesc val ex = intercept[AssertionError](op.generatePythonCode()) ex.getMessage should include("At least one line must be configured") } it should "raise AssertionError when lines is explicitly set to an empty list" in { // `createPlotlyFigure` guards against an empty lines list with an assertion, // matching the fail-fast pattern used by sibling visualizers // (HeatMapOpDesc, BarChartOpDesc). val op = configured op.lines = new util.ArrayList[LineConfig]() assertThrows[AssertionError](op.generatePythonCode()) } it should "raise AssertionError rather than NullPointerException when lines is set to null" in { // `lines` is a public mutable field; Jackson deserializing an explicit JSON // null or a caller assigning null can set it back to null even after the // non-null default is in place. `createPlotlyFigure` wraps `lines` in // `Option(...).getOrElse(emptyList)` before asserting nonEmpty, so a null // assignment produces the descriptive AssertionError rather than an NPE. val op = configured op.lines = null val ex = intercept[AssertionError](op.generatePythonCode()) ex.getMessage should include("At least one line must be configured") } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/pieChart/PieChartOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.pieChart import org.apache.texera.amber.core.tuple.AttributeType import org.apache.texera.amber.operator.metadata.OperatorGroupConstants import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import java.nio.charset.StandardCharsets import java.util.Base64 class PieChartOpDescSpec extends AnyFlatSpec with BeforeAndAfter with Matchers { var opDesc: PieChartOpDesc = _ before { opDesc = new PieChartOpDesc() } it should "throw assertion error if value is empty" in { assertThrows[AssertionError] { opDesc.manipulateTable() } } "PieChartOpDesc.operatorInfo" should "advertise the user-friendly name and Basic group" in { val info = opDesc.operatorInfo info.userFriendlyName shouldBe "Pie Chart" info.operatorGroupName shouldBe OperatorGroupConstants.VISUALIZATION_BASIC_GROUP info.operatorDescription should include("Pie Chart") } it should "expose exactly one output port wired through forVisualization" in { opDesc.operatorInfo.outputPorts should have length 1 } "PieChartOpDesc.getOutputSchemas" should "return a single-port schema with an html-content STRING column" in { opDesc.value = "amount" opDesc.name = "label" val schemas = opDesc.getOutputSchemas(Map.empty) schemas should have size 1 val (portId, schema) = schemas.head portId shouldBe opDesc.operatorInfo.outputPorts.head.id schema.getAttributes should have length 1 schema.getAttributes.head.getName shouldBe "html-content" schema.getAttributes.head.getType shouldBe AttributeType.STRING } "PieChartOpDesc.generatePythonCode" should "render Python source with runtime decode sites for value and name" in { // Use distinct sentinels and assert on the exact base64-wrapped decode // expressions so a regression that leaves `name` as a raw literal // cannot satisfy a generic `decodeOccurrences >= 2` (since `value` is // referenced multiple times in the generated template anyway). opDesc.value = "VAL_SENT" opDesc.name = "NAME_SENT" val code = opDesc.generatePythonCode() code should include("class ProcessTableOperator(UDFTableOperator)") code should include("plotly.express") def b64(s: String): String = Base64.getEncoder.encodeToString(s.getBytes(StandardCharsets.UTF_8)) code should include(s"self.decode_python_template('${b64("VAL_SENT")}')") code should include(s"self.decode_python_template('${b64("NAME_SENT")}')") code should not include "VAL_SENT" code should not include "NAME_SENT" } it should "fail-fast when value is unset even if name is set (only `value` is asserted)" in { // Pin: PieChartOpDesc.manipulateTable and createPlotlyFigure both assert // nonEmpty on `value`, but neither asserts on `name`. Setting just `name` // is therefore not enough to satisfy the guards. opDesc.name = "label" assertThrows[AssertionError](opDesc.generatePythonCode()) } it should "render successfully when only name is empty (asymmetric guard, current behavior)" in { // Pin: name has no assert guard. With value set and name empty, the // generated Python still renders — only the runtime call site receives // an empty decode. This asymmetry between value (asserted) and name // (not asserted) is documented here. opDesc.value = "amount" opDesc.name = "" val code = opDesc.generatePythonCode() code should include("class ProcessTableOperator(UDFTableOperator)") } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/scatterplot/ScatterPlotOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.scatterplot import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class ScatterPlotOpDescSpec extends AnyFlatSpec with BeforeAndAfter { var opDesc: ScatterplotOpDesc = _ before { opDesc = new ScatterplotOpDesc() } it should "throw assertion error if value is empty" in { assertThrows[AssertionError] { opDesc.manipulateTable() } } it should "throw assertion error if chart is empty" in { assertThrows[AssertionError] { opDesc.manipulateTable() } } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/volcanoPlot/VolcanoPlotOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.volcanoPlot import org.apache.texera.amber.core.tuple.AttributeType import org.apache.texera.amber.operator.metadata.OperatorGroupConstants import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers class VolcanoPlotOpDescSpec extends AnyFlatSpec with Matchers { private def configured: VolcanoPlotOpDesc = { val op = new VolcanoPlotOpDesc op.effectColumn = "log2fc" op.pvalueColumn = "pvalue" op } "VolcanoPlotOpDesc.operatorInfo" should "advertise the user-friendly name and Scientific group" in { val info = (new VolcanoPlotOpDesc).operatorInfo info.userFriendlyName shouldBe "Volcano Plot" info.operatorGroupName shouldBe OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP info.operatorDescription should include("statistical") } it should "expose exactly one output port wired through forVisualization" in { (new VolcanoPlotOpDesc).operatorInfo.outputPorts should have length 1 } "VolcanoPlotOpDesc.getOutputSchemas" should "return a single-port schema with an html-content STRING column" in { val op = configured val schemas = op.getOutputSchemas(Map.empty) schemas should have size 1 val (portId, schema) = schemas.head portId shouldBe op.operatorInfo.outputPorts.head.id schema.getAttributes should have length 1 schema.getAttributes.head.getName shouldBe "html-content" schema.getAttributes.head.getType shouldBe AttributeType.STRING } "VolcanoPlotOpDesc.generatePythonCode" should "render a UDFTableOperator source that decodes both column references" in { // EncodableString fields are NOT emitted as literal column names — the // pyb macro wraps them in `self.decode_python_template.decode("")` // calls so the column name is resolved at runtime. Verify the structure // (class + import + decode site count) instead of substring matches. val code = configured.generatePythonCode() code should include("class ProcessTableOperator(UDFTableOperator)") code should include("plotly.express") code should include("-log10(pvalue)") val decodeOccurrences = "decode_python_template".r.findAllIn(code).length decodeOccurrences should be >= 2 } it should "currently render code even when required fields are empty (no assert guard)" in { // Documents the present behavior: VolcanoPlotOpDesc does not assert on // its required fields inside `generatePythonCode`. An empty // configuration therefore renders syntactically valid Python that // references an empty string. The intended contract is split out into // the pendingUntilFixed test below so this assertion no longer reads // as the contract. val op = new VolcanoPlotOpDesc val code = op.generatePythonCode() code should include("class ProcessTableOperator(UDFTableOperator)") } it should "eventually reject empty required fields like FunnelPlot/ImageVisualizer (pendingUntilFixed)" in pendingUntilFixed { // Intended contract: `effectColumn` and `pvalueColumn` are marked // required on `VolcanoPlotOpDesc`, so generatePythonCode on a // default-constructed instance should raise instead of producing a // string-literal-empty payload. Using pendingUntilFixed so a future // validation fix flips this test from Pending to a deliberate failure // and forces removal of the marker. val op = new VolcanoPlotOpDesc intercept[RuntimeException] { op.generatePythonCode() } } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/wordCloud/WordCloudOpDescSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.operator.visualization.wordCloud import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec class WordCloudOpDescSpec extends AnyFlatSpec with BeforeAndAfter { var opDesc: WordCloudOpDesc = _ before { opDesc = new WordCloudOpDesc() } it should "use correct regex pattern to match word characters" in { opDesc.textColumn = "text_col" val code = opDesc.manipulateTable().plain assert( code.contains("""r'\w'"""), "regex should use single backslash \\w to match word characters" ) assert( !code.contains("""r'\\w'"""), "regex should not use double backslash \\\\w which matches literal backslash+w" ) } it should "include the text column in manipulateTable" in { opDesc.textColumn = "my_text" val code = opDesc.manipulateTable().plain assert(code.contains("my_text")) } it should "include the text column in createWordCloudFigure" in { opDesc.textColumn = "my_text" val code = opDesc.createWordCloudFigure().plain assert(code.contains("my_text")) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/pybuilder/DescriptorChecker.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.pybuilder import com.fasterxml.jackson.annotation.JsonProperty import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.pybuilder.PythonReflectionTextUtils.{ countOccurrences, extractContexts, formatThrowable, truncateBlock } import org.apache.texera.amber.pybuilder.PythonReflectionUtils.{ Finding, RawInvalidTextResult, TypeEnv } import java.lang.reflect._ import java.util import scala.collection.mutable import scala.jdk.CollectionConverters._ import scala.util.Try //IMPORTANT ENABLE EXISTENTIALs import scala.language.existentials object DescriptorChecker { final case class CheckResult(findings: Seq[Finding], code: Option[String]) } /** * Validates a [[PythonOperatorDescriptor]] by instantiating it and attempting to generate Python code. * * What it does (high level): * 1) Instantiates the descriptor (supports Scala object descriptors via MODULE$). * 2) Best-effort initializes @JsonProperty fields using defaults and "required" semantics. * 3) Inject raw invalid string-typed @JsonProperty fields (and string containers) to detect invalid code. * 4) Captures stdout/stderr and exceptions from generatePythonCode() and reports them as findings. * * Generic-awareness: * - Tracks a best-effort TypeEnv (TypeVariable -> Type) per instantiated object (identity-based) so that * defaults/injection can reason about element types for generic collections. * * Note (EN): The idea is to "touch" the object as little as possible, but enough to reveal common problems * (null required fields, missing defaults, raw text leak, prints to stdout/stderr). */ final class DescriptorChecker(private val rawInvalidText: String, private val maxDepth: Int) { // Carry env per instantiated object (Identity semantics) private val envByObj = new util.IdentityHashMap[AnyRef, TypeEnv]() import DescriptorChecker.CheckResult /** Convenience wrapper that only returns findings (drops generated code). */ def check(descriptorClass: Class[_ <: PythonOperatorDescriptor]): Seq[Finding] = checkWithCode(descriptorClass).findings /** * Runs the full validation pipeline and returns both findings and (if generated) Python code. * * Important: This method tries hard to continue even if parts fail (best-effort strategy), * so you can see multiple issues in a single run instead of failing fast on the first problem. */ def checkWithCode(descriptorClass: Class[_ <: PythonOperatorDescriptor]): CheckResult = { instantiateDescriptor(descriptorClass) match { case Left(instantiateFailureReason) => CheckResult( Seq(Finding(descriptorClass.getName, "instantiate", instantiateFailureReason)), None ) case Right(descriptorInstance) => val findingsBuffer = mutable.ArrayBuffer.empty[Finding] // Seed env for the root descriptor instance (used by later generic-aware routines) envByObj.put(descriptorInstance, computeEnvFromConcreteClass(descriptorInstance.getClass)) // 0) Fill required/defaulted props (deep) bestEffortFillJsonPropertyDefaults(descriptorInstance, maxDepth) // 1) Raw Invalid strings (deep) val rawInvalidTextingResult = rawInvalidTextJsonPropertyStringsDeep(descriptorInstance, rawInvalidText, maxDepth) if (rawInvalidTextingResult.failed.nonEmpty) { findingsBuffer += Finding( descriptorClass.getName, "injection-failure", s"Could not rawInvalidText some @JsonProperty members: ${rawInvalidTextingResult.failed.mkString(", ")}" ) } // 2) Capture stdout/stderr + exceptions during codegen val consoleCapture = PythonConsoleCapture.captureOutErr { Try(descriptorInstance.generatePythonCode()) } val generatedCodeTry = consoleCapture.value val generatedCodeOpt = generatedCodeTry.toOption val capturedStdout = consoleCapture.out.trim val capturedStderr = consoleCapture.err.trim if (capturedStdout.nonEmpty) { findingsBuffer += Finding( descriptorClass.getName, "stdout", s"generatePythonCode printed to stdout:\n${truncateBlock(capturedStdout, maxLines = 30, maxChars = 4000)}" ) } if (capturedStderr.nonEmpty) { findingsBuffer += Finding( descriptorClass.getName, "stderr", s"generatePythonCode printed to stderr:\n${truncateBlock(capturedStderr, maxLines = 30, maxChars = 4000)}" ) } generatedCodeTry.failed.toOption.foreach { thrown => findingsBuffer += Finding(descriptorClass.getName, "exception", formatThrowable(thrown)) } // 3) Raw invalid string leakage check: did the rawInvalidText marker appear in generated Python? generatedCodeOpt.foreach { generatedCode => val rawInvalidTextHitCount = countOccurrences(generatedCode, rawInvalidText) if (rawInvalidTextHitCount > 0) { val rawInvalidTextContexts = extractContexts(generatedCode, rawInvalidText, radius = 160, maxContexts = 2) .map(_.replace("\n", "\\n")) .mkString("\n - ...", "...\n - ...", "...") findingsBuffer += Finding( descriptorClass.getName, "raw-invalid-text-leak", s"""Generated Python contains rawInvalidText '$rawInvalidText' ($rawInvalidTextHitCount occurrence(s)) |rawInvalidTexted members: ${if (rawInvalidTextingResult.changed.isEmpty) "(none found)" else rawInvalidTextingResult.changed.mkString(", ")} |contexts: |$rawInvalidTextContexts""".stripMargin ) } } CheckResult(findingsBuffer.toSeq, generatedCodeOpt) } } /** * Instantiates a descriptor: * - Scala object: fetches MODULE$ * - Regular class: uses an accessible no-arg constructor */ private def instantiateDescriptor( descriptorClass: Class[_ <: PythonOperatorDescriptor] ): Either[String, PythonOperatorDescriptor] = { val scalaModuleFieldOpt: Option[Field] = Try(descriptorClass.getField("MODULE$")).toOption .orElse(Try(descriptorClass.getDeclaredField("MODULE$")).toOption) scalaModuleFieldOpt match { case Some(scalaModuleField) => Try { scalaModuleField.setAccessible(true) scalaModuleField.get(null).asInstanceOf[PythonOperatorDescriptor] }.toEither.left.map(thrown => s"cannot access Scala object MODULE $scalaModuleFieldOpt: ${thrown.getClass.getName}: ${Option(thrown.getMessage) .getOrElse("")}" ) case None => Try { val noArgConstructor = descriptorClass.getDeclaredConstructor() noArgConstructor.setAccessible(true) noArgConstructor.newInstance().asInstanceOf[PythonOperatorDescriptor] }.toEither.left.map(_ => "cannot instantiate (needs an accessible no-arg constructor or must be a Scala object)" ) } } // ------------------------------------------------------------ // Generic type resolution (TypeEnv) // ------------------------------------------------------------ private final case class SimpleParameterizedType(raw: Type, args: scala.Array[Type], owner: Type) extends ParameterizedType { override def getRawType: Type = raw override def getActualTypeArguments: scala.Array[Type] = args.clone() override def getOwnerType: Type = owner } /** * Builds a best-effort mapping of type variables to concrete types by walking: * - generic superclass * - generic interfaces * recursively up the inheritance chain. */ private def computeEnvFromConcreteClass(concreteClass: Class[_]): TypeEnv = { val typeVarBindings = mutable.Map.empty[TypeVariable[_], Type] val visitedTypes = mutable.Set.empty[Type] def resolveInCollectedEnv(unresolvedType: Type): Type = resolveType(unresolvedType, typeVarBindings.toMap) def traverseType(nextType: Type): Unit = { if (nextType == null || visitedTypes.contains(nextType)) return visitedTypes += nextType nextType match { case parameterizedType: ParameterizedType => val rawClassOpt = typeToClass(parameterizedType.getRawType) rawClassOpt.foreach { rawClass => val rawTypeVariables = rawClass.getTypeParameters val typeArguments = parameterizedType.getActualTypeArguments rawTypeVariables.zipAll(typeArguments, null, null).foreach { case (typeVar, typeArg) => if (typeVar != null && typeArg != null) typeVarBindings(typeVar) = resolveInCollectedEnv(typeArg) } } rawClassOpt.foreach(traverseClass) case rawClass: Class[_] => traverseClass(rawClass) case _ => () } } def traverseClass(currentClass: Class[_]): Unit = { if (currentClass == null || currentClass == classOf[Object]) return traverseType(currentClass.getGenericSuperclass) currentClass.getGenericInterfaces.foreach(traverseType) traverseClass(currentClass.getSuperclass) } traverseClass(concreteClass) typeVarBindings.toMap } private def resolveType(unresolvedType: Type, typeEnv: TypeEnv): Type = unresolvedType match { case typeVar: TypeVariable[_] => typeEnv.get(typeVar) match { case Some(resolvedBinding) => resolveType(resolvedBinding, typeEnv) case None => typeVar.getBounds.headOption .map(bound => resolveType(bound, typeEnv)) .getOrElse(typeVar) } case wildcardType: WildcardType => wildcardType.getUpperBounds.headOption .map(bound => resolveType(bound, typeEnv)) .getOrElse(wildcardType) case genericArrayType: GenericArrayType => val resolvedComponentType = resolveType(genericArrayType.getGenericComponentType, typeEnv) typeToClass(resolvedComponentType) .map(componentClass => java.lang.reflect.Array.newInstance(componentClass, 0).getClass.asInstanceOf[Type] ) .getOrElse(genericArrayType) case parameterizedType: ParameterizedType => val resolvedRawType = resolveType(parameterizedType.getRawType, typeEnv) val resolvedOwnerType = Option(parameterizedType.getOwnerType).map(owner => resolveType(owner, typeEnv)).orNull val resolvedTypeArguments = parameterizedType.getActualTypeArguments.map(typeArg => resolveType(typeArg, typeEnv)) SimpleParameterizedType(resolvedRawType, resolvedTypeArguments, resolvedOwnerType) case rawClass: Class[_] => rawClass case otherType => otherType } /** Retrieves the best available TypeEnv for a specific object instance. */ private def envFor(instance: AnyRef): TypeEnv = { val storedEnv = Option(envByObj.get(instance)).getOrElse(Map.empty) val classDerivedEnv = computeEnvFromConcreteClass(instance.getClass) classDerivedEnv ++ storedEnv } /** * Extends an existing TypeEnv with (rawClass type params -> resolved type args). * Used when instantiating parameterized types so child object graphs can be reasoned about. */ private def envForParameterizedInstance( rawClass: Class[_], typeArguments: scala.Array[Type], parentTypeEnv: TypeEnv ): TypeEnv = { val rawTypeVariables = rawClass.getTypeParameters val resolvedTypeArguments = typeArguments.map(typeArg => resolveType(typeArg, parentTypeEnv)) val rawTypeVarBindings = rawTypeVariables .zipAll(resolvedTypeArguments, null, null) .collect { case (typeVar, typeArg) if typeVar != null && typeArg != null => typeVar -> typeArg } .toMap parentTypeEnv ++ rawTypeVarBindings } private def typeToClass(typ: Type): Option[Class[_]] = typ match { case rawClass: Class[_] => Some(rawClass) case parameterizedType: ParameterizedType => typeToClass(parameterizedType.getRawType) case _ => None } private def elementTypeOfResolved(resolvedType: Type): Option[Type] = resolvedType match { case parameterizedType: ParameterizedType => parameterizedType.getActualTypeArguments.headOption case arrayClass: Class[_] if arrayClass.isArray => Some(arrayClass.getComponentType) case _ => None } // ------------------------------------------------------------ // Best-effort init (generic-aware) // ------------------------------------------------------------ /** * Best-effort initialization for @JsonProperty fields: * - If @JsonProperty(required = true) or defaultValue is provided, tries to initialize when null. * - Also ensures required collections are non-empty (adds an element when element type can be inferred). * * This is intentionally heuristic: the goal is to create a "usable enough" object graph for codegen * without knowing real business semantics. */ private def bestEffortFillJsonPropertyDefaults( rootDescriptor: AnyRef, recursionDepthLimit: Int ): Unit = { val visitedIdentityHashes = mutable.Set.empty[Int] def fillRecursively(currentObject: AnyRef, remainingDepth: Int): Unit = { if (currentObject == null || remainingDepth < 0) return val objectId = System.identityHashCode(currentObject) if (visitedIdentityHashes.contains(objectId)) return visitedIdentityHashes += objectId val currentTypeEnv = envFor(currentObject) walkHierarchy(currentObject.getClass) { declaringClassInHierarchy => declaringClassInHierarchy.getDeclaredFields.foreach { declaredField => if (!shouldSkipField(declaredField)) { val jsonPropertyOpt = jsonPropertyForFieldOrAccessors(declaringClassInHierarchy, declaredField) jsonPropertyOpt.foreach { jsonPropertyAnn => declaredField.setAccessible(true) val currentFieldValue = Try(declaredField.get(currentObject)).toOption.orNull val defaultValueText = Option(jsonPropertyAnn.defaultValue()).getOrElse("").trim val isRequired = jsonPropertyAnn.required() val resolvedFieldType = resolveType(declaredField.getGenericType, currentTypeEnv) val needsInitialization = (currentFieldValue == null) && (isRequired || defaultValueText.nonEmpty) val ensuredValue: AnyRef = if (needsInitialization) { val defaultValue = defaultValueForResolvedType( targetType = resolvedFieldType, defaultValueText = defaultValueText, remainingDepth = remainingDepth, typeEnvAtParent = currentTypeEnv ) if (defaultValue != null) { trySet(currentObject, declaringClassInHierarchy, declaredField, defaultValue) defaultValue } else currentFieldValue } else currentFieldValue val updatedValue = ensureNonEmptyIfRequired( owningInstance = currentObject, declaringClass = declaringClassInHierarchy, field = declaredField, currentFieldValue = ensuredValue, jsonPropertyAnn = jsonPropertyAnn, resolvedFieldType = resolvedFieldType, typeEnvAtField = currentTypeEnv, remainingDepth = remainingDepth ) recurseIntoValue(updatedValue, remainingDepth - 1, fillRecursively) } } } } } fillRecursively(rootDescriptor, recursionDepthLimit) } private def ensureNonEmptyIfRequired( owningInstance: AnyRef, declaringClass: Class[_], field: Field, currentFieldValue: AnyRef, jsonPropertyAnn: JsonProperty, resolvedFieldType: Type, typeEnvAtField: TypeEnv, remainingDepth: Int ): AnyRef = { if (!jsonPropertyAnn.required() || remainingDepth <= 0) return currentFieldValue // If required and null, try to initialize collection containers too val ensuredNonNullValue: AnyRef = if (currentFieldValue != null) currentFieldValue else { val rawFieldClass = typeToClass(resolvedFieldType).getOrElse(field.getType) val defaultValue = defaultValueForResolvedType( targetType = rawFieldClass, defaultValueText = "", remainingDepth = remainingDepth, typeEnvAtParent = typeEnvAtField ) if (defaultValue != null) trySet(owningInstance, declaringClass, field, defaultValue) defaultValue } if (ensuredNonNullValue == null) return null val runtimeValueClass = ensuredNonNullValue.getClass val elementTypeOpt = elementTypeOfResolved(resolvedFieldType).map(et => resolveType(et, typeEnvAtField)) def makeElementValue(): AnyRef = { val elementType = elementTypeOpt.getOrElse(classOf[String]) defaultValueForResolvedType( targetType = elementType, defaultValueText = "", remainingDepth = remainingDepth - 1, typeEnvAtParent = typeEnvAtField ) } if (isJavaList(runtimeValueClass)) { val javaList = ensuredNonNullValue.asInstanceOf[util.List[AnyRef]] if (javaList.isEmpty) { val elementValue = makeElementValue() if (elementValue != null) javaList.add(elementValue) } } else if (isScalaIterable(runtimeValueClass)) { val scalaIterable = ensuredNonNullValue.asInstanceOf[scala.collection.Iterable[Any]] if (scalaIterable.isEmpty) { val elementValue = makeElementValue() if (elementValue != null) trySet(owningInstance, declaringClass, field, List(elementValue).asInstanceOf[AnyRef]) } } else if (runtimeValueClass.isArray && runtimeValueClass.getComponentType == classOf[String]) { val stringArray = ensuredNonNullValue.asInstanceOf[scala.Array[String]] if (stringArray.isEmpty) trySet(owningInstance, declaringClass, field, scala.Array("x").asInstanceOf[AnyRef]) } Try(field.get(owningInstance)).toOption.orNull } private def defaultValueForResolvedType( targetType: Type, defaultValueText: String, remainingDepth: Int, typeEnvAtParent: TypeEnv ): AnyRef = { val trimmedDefaultValueText = Option(defaultValueText).getOrElse("").trim val resolvedTargetType = resolveType(targetType, typeEnvAtParent) resolvedTargetType match { case rawClass: Class[_] => if (rawClass == classOf[String]) { if (trimmedDefaultValueText.nonEmpty) trimmedDefaultValueText else "x" } else if (rawClass == java.lang.Boolean.TYPE || rawClass == classOf[java.lang.Boolean]) { val booleanValue = trimmedDefaultValueText.toLowerCase match { case "true" => true case "false" => false case _ => false } java.lang.Boolean.valueOf(booleanValue) } else if (rawClass == java.lang.Integer.TYPE || rawClass == classOf[java.lang.Integer]) { java.lang.Integer.valueOf(Try(trimmedDefaultValueText.toInt).getOrElse(1)) } else if (rawClass == java.lang.Long.TYPE || rawClass == classOf[java.lang.Long]) { java.lang.Long.valueOf(Try(trimmedDefaultValueText.toLong).getOrElse(1L)) } else if (rawClass == java.lang.Double.TYPE || rawClass == classOf[java.lang.Double]) { java.lang.Double.valueOf(Try(trimmedDefaultValueText.toDouble).getOrElse(1.0d)) } else if (rawClass == java.lang.Float.TYPE || rawClass == classOf[java.lang.Float]) { java.lang.Float.valueOf(Try(trimmedDefaultValueText.toFloat).getOrElse(1.0f)) } else if (rawClass.isEnum) { chooseEnumConstant(rawClass, trimmedDefaultValueText) } else if (isJavaList(rawClass)) { new util.ArrayList[AnyRef]() } else if (isScalaIterable(rawClass)) { List.empty[Any] } else if (rawClass.isArray && rawClass.getComponentType == classOf[String]) { scala.Array.empty[String] } else if (classOf[scala.Option[_]].isAssignableFrom(rawClass)) { None } else if ( !rawClass.isInterface && !Modifier.isAbstract(rawClass.getModifiers) && remainingDepth > 0 ) { instantiateBestEffort(rawClass).orNull } else null case parameterizedType: ParameterizedType => val rawClass = typeToClass(parameterizedType.getRawType).orNull if (rawClass == null) return null if (rawClass.isEnum) { chooseEnumConstant(rawClass, trimmedDefaultValueText) } else if (isJavaList(rawClass)) { new util.ArrayList[AnyRef]() } else if (isScalaIterable(rawClass)) { List.empty[Any] } else if (classOf[scala.Option[_]].isAssignableFrom(rawClass)) { None } else if ( !rawClass.isInterface && !Modifier.isAbstract(rawClass.getModifiers) && remainingDepth > 0 ) { val instanceOpt = instantiateBestEffort(rawClass) instanceOpt.foreach { newInstance => val newInstanceTypeEnv = envForParameterizedInstance( rawClass, parameterizedType.getActualTypeArguments, typeEnvAtParent ) envByObj.put(newInstance, newInstanceTypeEnv) } instanceOpt.orNull } else null case _ => null } } /** * Attempts to set a value into a field through multiple strategies: * 1) Direct reflective field set * 2) Scala setter: fieldName_$eq * 3) JavaBean setter: setFieldName */ private def trySet( owningInstance: AnyRef, declaringClass: Class[_], field: Field, newValue: AnyRef ): Unit = { // 1) Try direct field set val didSetViaField = Try { field.setAccessible(true); field.set(owningInstance, newValue) }.isSuccess if (didSetViaField) return // 2) Try Scala setter: name_$eq val scalaSetterName = field.getName + "_$eq" val didInvokeScalaSetter = Try { val matchingMethodOpt = declaringClass.getDeclaredMethods.find(m => m.getName == scalaSetterName && m.getParameterCount == 1 ) matchingMethodOpt.foreach { setterMethod => setterMethod.setAccessible(true) setterMethod.invoke(owningInstance, newValue.asInstanceOf[Object]) } matchingMethodOpt.isDefined }.getOrElse(false) if (didInvokeScalaSetter) return // 3) Try JavaBean setter: setX val javaBeanSetterName = "set" + upperFirst(field.getName) Try { val matchingMethodOpt = declaringClass.getDeclaredMethods.find(m => m.getName == javaBeanSetterName && m.getParameterCount == 1 ) matchingMethodOpt.foreach { setterMethod => setterMethod.setAccessible(true) setterMethod.invoke(owningInstance, newValue.asInstanceOf[Object]) } } () } // ------------------------------------------------------------ // Raw Invalid String Detection (generic-aware) // ------------------------------------------------------------ /** * Replaces string values in @JsonProperty fields (and string containers) with the rawInvalidText marker. * * Returns which members were changed and which ones could not be changed. */ private def rawInvalidTextJsonPropertyStringsDeep( rootDescriptor: AnyRef, rawInvalidTextMarker: String, recursionDepthLimit: Int ): RawInvalidTextResult = { val changedMembers = mutable.ArrayBuffer.empty[String] val failedMembers = mutable.ArrayBuffer.empty[String] val visitedIdentityHashes = mutable.Set.empty[Int] def rawInvalidTextRecursively(currentObject: AnyRef, remainingDepth: Int): Unit = { if (currentObject == null || remainingDepth < 0) return val objectId = System.identityHashCode(currentObject) if (visitedIdentityHashes.contains(objectId)) return visitedIdentityHashes += objectId val currentTypeEnv = envFor(currentObject) walkHierarchy(currentObject.getClass) { declaringClassInHierarchy => declaringClassInHierarchy.getDeclaredFields.foreach { declaredField => if (!shouldSkipField(declaredField)) { val jsonPropertyOpt = jsonPropertyForFieldOrAccessors(declaringClassInHierarchy, declaredField) jsonPropertyOpt.foreach { jsonPropertyAnn => declaredField.setAccessible(true) val jsonPropertyName = effectiveJsonPropName(jsonPropertyAnn, fallback = declaredField.getName) val resolvedFieldType = resolveType(declaredField.getGenericType, currentTypeEnv) val rawFieldClass = typeToClass(resolvedFieldType).getOrElse(declaredField.getType) val currentFieldValue = Try(declaredField.get(currentObject)).toOption.orNull if (rawFieldClass == classOf[String]) { val didInjected = Try { trySet( currentObject, declaringClassInHierarchy, declaredField, rawInvalidTextMarker ) }.isSuccess if (didInjected) changedMembers += s"""${declaringClassInHierarchy.getSimpleName}.${declaredField.getName}(@JsonProperty("$jsonPropertyName"))""" else failedMembers += s"${declaringClassInHierarchy.getSimpleName}.${declaredField.getName}" } else if (isJavaList(rawFieldClass)) { val javaListValue = if (currentFieldValue != null) currentFieldValue.asInstanceOf[util.List[AnyRef]] else { val newList = new util.ArrayList[AnyRef]() Try(trySet(currentObject, declaringClassInHierarchy, declaredField, newList)) newList } val isElementTypeString = elementTypeOfResolved(resolvedFieldType) .map(et => resolveType(et, currentTypeEnv)) .flatMap(typeToClass) .contains(classOf[String]) if (isElementTypeString) { Try { javaListValue.clear(); javaListValue.add(rawInvalidTextMarker) } changedMembers += s"""${declaringClassInHierarchy.getSimpleName}.${declaredField.getName}[0](@JsonProperty("$jsonPropertyName"))""" } else { javaListValue.asScala.foreach(elementObj => rawInvalidTextRecursively(elementObj, remainingDepth - 1) ) } } else if (isScalaIterable(rawFieldClass)) { val isElementTypeString = elementTypeOfResolved(resolvedFieldType) .map(et => resolveType(et, currentTypeEnv)) .flatMap(typeToClass) .contains(classOf[String]) if (isElementTypeString) { val didSetList = Try( trySet( currentObject, declaringClassInHierarchy, declaredField, List(rawInvalidTextMarker).asInstanceOf[AnyRef] ) ).isSuccess if (didSetList) changedMembers += s"""${declaringClassInHierarchy.getSimpleName}.${declaredField.getName}[0](@JsonProperty("$jsonPropertyName"))""" else failedMembers += s"${declaringClassInHierarchy.getSimpleName}.${declaredField.getName}" } else { recurseIntoValue(currentFieldValue, remainingDepth - 1, rawInvalidTextRecursively) } } else if ( rawFieldClass.isArray && rawFieldClass.getComponentType == classOf[String] ) { val didInjectedArray = Try( trySet( currentObject, declaringClassInHierarchy, declaredField, scala.Array(rawInvalidTextMarker).asInstanceOf[AnyRef] ) ).isSuccess if (didInjectedArray) changedMembers += s"""${declaringClassInHierarchy.getSimpleName}.${declaredField.getName}[0](@JsonProperty("$jsonPropertyName"))""" else failedMembers += s"${declaringClassInHierarchy.getSimpleName}.${declaredField.getName}" } else { recurseIntoValue(currentFieldValue, remainingDepth - 1, rawInvalidTextRecursively) } } } } } } rawInvalidTextRecursively(rootDescriptor, recursionDepthLimit) RawInvalidTextResult(changedMembers.distinct.toSeq, failedMembers.distinct.toSeq) } // ------------------------------------------------------------ // Reflection utilities // ------------------------------------------------------------ /** Walks the class hierarchy from `startingClass` up to (excluding) java.lang.Object. */ private def walkHierarchy(startingClass: Class[_])(visitFn: Class[_] => Unit): Unit = { var currentClass: Class[_] = startingClass while (currentClass != null && currentClass != classOf[Object]) { visitFn(currentClass) currentClass = currentClass.getSuperclass } } /** Filters out synthetic, compiler-generated, and static fields (things we should not involve with). */ private def shouldSkipField(field: Field): Boolean = { field.isSynthetic || field.getName.contains("$") || Modifier.isStatic(field.getModifiers) } private def upperFirst(text: String): String = if (text.isEmpty) text else s"${text.charAt(0).toUpper}${text.substring(1)}" /** * Finds a @JsonProperty annotation either on: * - The field itself, or * - A getter/setter method that corresponds to the field name (Scala/Java styles). */ private def jsonPropertyForFieldOrAccessors( declaringClass: Class[_], field: Field ): Option[JsonProperty] = { Option(field.getAnnotation(classOf[JsonProperty])).orElse { val fieldName = field.getName val getterMethodNames = Seq(fieldName, "get" + upperFirst(fieldName), "is" + upperFirst(fieldName)) val setterMethodNames = Seq(fieldName + "_$eq", "set" + upperFirst(fieldName)) val declaredMethods = declaringClass.getDeclaredMethods def annotationOn(methodName: String, expectedParamCount: Int): Option[JsonProperty] = declaredMethods .find(m => m.getName == methodName && !m.isSynthetic && m.getParameterCount == expectedParamCount ) .flatMap(m => Option(m.getAnnotation(classOf[JsonProperty]))) getterMethodNames.iterator .map(candidateName => annotationOn(candidateName, 0)) .find(_.nonEmpty) .flatten .orElse( setterMethodNames.iterator .map(candidateName => annotationOn(candidateName, 1)) .find(_.nonEmpty) .flatten ) } } private def effectiveJsonPropName(jsonPropertyAnn: JsonProperty, fallback: String): String = { val explicitName = Option(jsonPropertyAnn.value()).getOrElse("").trim if (explicitName.nonEmpty) explicitName else fallback } private def isJavaList(clazz: Class[_]): Boolean = classOf[util.List[_]].isAssignableFrom(clazz) private def isScalaIterable(clazz: Class[_]): Boolean = classOf[scala.collection.Iterable[_]].isAssignableFrom(clazz) || classOf[scala.collection.Seq[_]].isAssignableFrom(clazz) private def chooseEnumConstant(enumClass: Class[_], desiredValue: String): AnyRef = { val enumConstants = enumClass.getEnumConstants.asInstanceOf[scala.Array[AnyRef]] if (enumConstants == null || enumConstants.isEmpty) return null val desiredLower = Option(desiredValue).getOrElse("").trim.toLowerCase if (desiredLower.isEmpty) return enumConstants.head def getNameViaReflection(enumValue: AnyRef): Option[String] = Try { val getNameMethod = enumValue.getClass.getMethod("getName") getNameMethod.setAccessible(true) getNameMethod.invoke(enumValue).toString }.toOption enumConstants .find { constant => val enumName = Try(constant.asInstanceOf[Enum[_]].name()).toOption.getOrElse("") val stringRepr = constant.toString.toLowerCase val enumNameLower = enumName.toLowerCase val reflectedNameLower = getNameViaReflection(constant).getOrElse("").toLowerCase stringRepr == desiredLower || enumNameLower == desiredLower || reflectedNameLower == desiredLower } .getOrElse(enumConstants.head) } /** * Best-effort instantiation for arbitrary classes: * - Scala object (MODULE$), else * - No-arg constructor. */ private def instantiateBestEffort(clazz: Class[_]): Option[AnyRef] = { val scalaModuleInstanceOpt = Try(clazz.getField("MODULE$")).toOption .orElse(Try(clazz.getDeclaredField("MODULE$")).toOption) .flatMap { moduleField => Try { moduleField.setAccessible(true); moduleField.get(null).asInstanceOf[AnyRef] }.toOption } scalaModuleInstanceOpt.orElse { Try { val noArgConstructor = clazz.getDeclaredConstructor() noArgConstructor.setAccessible(true) noArgConstructor.newInstance().asInstanceOf[AnyRef] }.toOption } } /** * Recurses into: * - Java Lists * - Scala Iterables * - Arrays * - Arbitrary non-leaf objects (excludes primitives, boxed primitives, String, enums, and core java/scala packages) */ private def recurseIntoValue( value: AnyRef, remainingDepth: Int, visitFn: (AnyRef, Int) => Unit ): Unit = { if (value == null || remainingDepth < 0) return value match { case javaList: util.List[_] => javaList.asScala.foreach { case elementRef: AnyRef => visitFn(elementRef, remainingDepth) case _ => () } case scalaIterable: scala.collection.Iterable[_] => scalaIterable.foreach { case elementRef: AnyRef => visitFn(elementRef, remainingDepth) case _ => () } case arrayValue: scala.Array[_] => arrayValue.foreach { case elementRef: AnyRef => visitFn(elementRef, remainingDepth) case _ => () } case otherValue => val runtimeClass = otherValue.getClass val isLeafValue = runtimeClass.isPrimitive || runtimeClass == classOf[String] || classOf[java.lang.Number].isAssignableFrom(runtimeClass) || runtimeClass == classOf[java.lang.Boolean] || runtimeClass.isEnum || runtimeClass.getName.startsWith("java.") || runtimeClass.getName.startsWith("javax.") || runtimeClass.getName.startsWith("scala.") if (!isLeafValue) visitFn(otherValue, remainingDepth) } } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/pybuilder/PythonClassgraphScanner.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.pybuilder import io.github.classgraph.ClassGraph import java.lang.reflect.Modifier import scala.jdk.CollectionConverters._ private[amber] object PythonClassgraphScanner { def scanCandidates( base: Class[_], acceptPackages: Seq[String], classLoader: ClassLoader ): Seq[Class[_]] = { val cg = new ClassGraph() .overrideClassLoaders(classLoader) .enableClassInfo() acceptPackages.foreach(p => cg.acceptPackages(p)) val scanResult = cg.scan() try { val infoList = if (base.isInterface) scanResult.getClassesImplementing(base.getName) else scanResult.getSubclasses(base.getName) infoList .loadClasses() .asScala .toSeq .filterNot(_.isInterface) .filterNot(c => Modifier.isAbstract(c.getModifiers)) } finally { scanResult.close() } } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/pybuilder/PythonConsoleCapture.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.pybuilder import org.apache.texera.amber.pybuilder.PythonReflectionUtils.Captured import java.io.{ByteArrayOutputStream, PrintStream} import java.nio.charset.StandardCharsets private[amber] object PythonConsoleCapture { def captureOutErr[A](thunk: => A): Captured[A] = { val outByteArrayOutStream = new ByteArrayOutputStream() val errByteArrayOutStream = new ByteArrayOutputStream() val outPrintStream = new PrintStream(outByteArrayOutStream) val errorPrintStream = new PrintStream(errByteArrayOutStream) val value = Console.withOut(outPrintStream) { Console.withErr(errorPrintStream) { thunk } } outPrintStream.flush() errorPrintStream.flush() Captured( value, outByteArrayOutStream.toString(StandardCharsets.UTF_8.name()), errByteArrayOutStream.toString(StandardCharsets.UTF_8.name()) ) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/pybuilder/PythonRawTextReportRenderer.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.pybuilder import org.apache.texera.amber.pybuilder.PythonReflectionTextUtils.indent import org.apache.texera.amber.pybuilder.PythonReflectionUtils.Finding private[amber] object PythonRawTextReportRenderer { def render(findings: Seq[Finding], total: Int): String = { val grouped = findings.groupBy(_.kind) val stringBuilder = new StringBuilder stringBuilder.append( s"PythonRawTextReportRendererTest failures: ${findings.size} finding(s) across $total descriptor(s)\n" ) def section(kind: String, title: String): Unit = { grouped.get(kind).foreach { items => stringBuilder.append(s"\n== $title (${items.size}) ==\n") items.sortBy(_.clazz).foreach { f => stringBuilder.append(s"- ${f.clazz}\n${indent(f.message.trim, 4)}\n") } } } section("instantiate", "Instantiation failures") section("injection-failure", "Injection failed") section("exception", "generatePythonCode exceptions") section("raw-invalid-text-leak", "Raw invalid text leaked into generated Python") section("py-compile", "py_compile failures") section("stdout", "Unexpected stdout") section("stderr", "Unexpected stderr") stringBuilder.toString() } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/pybuilder/PythonReflectionTextUtils.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.pybuilder import scala.collection.mutable private[amber] object PythonReflectionTextUtils { def indent(string: String, times: Int): String = { val pad = " " * times string.linesIterator.map(line => pad + line).mkString("\n") } def formatThrowable(throwable: Throwable): String = { val message = Option(throwable.getMessage).getOrElse("No message") val trace = throwable.getStackTrace.filter(_.getClassName.startsWith("org.apache.texera")).take(5) s"${throwable.getClass.getName}: $message\n${trace.mkString("\n")}" } def truncateBlock(string: String, maxLines: Int, maxChars: Int): String = { val lines = string.linesIterator.take(maxLines).toList val combined = lines.mkString("\n") if (combined.length > maxChars) combined.take(maxChars) + "..." else combined } def countOccurrences(targetHay: String, needle: String): Int = { if (needle.isEmpty) 0 else targetHay.split(java.util.regex.Pattern.quote(needle), -1).length - 1 } def extractContexts( string: String, needle: String, radius: Int, maxContexts: Int ): Seq[String] = { val outArrayBuffer = mutable.ArrayBuffer.empty[String] var idx = string.indexOf(needle) while (idx != -1 && outArrayBuffer.size < maxContexts) { val start = math.max(0, idx - radius) val end = math.min(string.length, idx + needle.length + radius) outArrayBuffer += string.substring(start, end) idx = string.indexOf(needle, idx + 1) } outArrayBuffer.toSeq } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/pybuilder/PythonReflectionUtils.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.pybuilder import org.apache.texera.amber.operator.PythonOperatorDescriptor import java.lang.reflect.{Type, TypeVariable} object PythonReflectionUtils { final case class RawInvalidTextResult(changed: Seq[String], failed: Seq[String]) final case class Finding(clazz: String, kind: String, message: String) final case class Captured[A](value: A, out: String, err: String) // Type-variable substitution environment type TypeEnv = Map[TypeVariable[_], Type] /** Scan non-abstract, non-interface candidates under acceptPackages. */ def scanCandidates( base: Class[_], acceptPackages: Seq[String], classLoader: ClassLoader ): Seq[Class[_]] = PythonClassgraphScanner.scanCandidates(base, acceptPackages, classLoader) /** Run the full instantiate -> fill -> inject -> execute -> leak check pipeline for one descriptor class. */ def checkDescriptor( clazz: Class[_ <: PythonOperatorDescriptor], rawInvalidText: String, maxDepth: Int ): Seq[Finding] = new DescriptorChecker(rawInvalidText, maxDepth).check(clazz) /** Same pipeline, but also returns the generated Python code when available. */ def checkDescriptorWithCode( clazz: Class[_ <: PythonOperatorDescriptor], rawInvalidText: String, maxDepth: Int ): DescriptorChecker.CheckResult = new DescriptorChecker(rawInvalidText, maxDepth).checkWithCode(clazz) def renderReport(findings: Seq[Finding], total: Int): String = PythonRawTextReportRenderer.render(findings, total) def captureOutErr[A](thunk: => A): Captured[A] = PythonConsoleCapture.captureOutErr(thunk) } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/util/ArrowUtilsSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.util import org.apache.texera.amber.core.tuple.AttributeTypeUtils.AttributeTypeException import org.apache.texera.amber.core.tuple.{AttributeType, LargeBinary, Schema, Tuple} import org.apache.arrow.memory.{BufferAllocator, RootAllocator} import org.apache.arrow.vector.VectorSchemaRoot import org.apache.arrow.vector.types.pojo.{ArrowType, Field, FieldType} import org.apache.arrow.vector.types.{DateUnit, FloatingPointPrecision, IntervalUnit, TimeUnit} import org.scalatest.flatspec.AnyFlatSpec import java.sql.Timestamp import java.util import scala.jdk.CollectionConverters.IterableHasAsJava class ArrowUtilsSpec extends AnyFlatSpec { val unsignedShortInt = new ArrowType.Int(16, false) val signedShortInt = new ArrowType.Int(16, true) val unsignedInt = new ArrowType.Int(32, false) val signedInt = new ArrowType.Int(32, true) val unsignedLongInt = new ArrowType.Int(64, false) val signedLongInt = new ArrowType.Int(64, true) val boolean: ArrowType.Bool = ArrowType.Bool.INSTANCE val float = new ArrowType.FloatingPoint(FloatingPointPrecision.SINGLE) val double = new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE) val half = new ArrowType.FloatingPoint(FloatingPointPrecision.HALF) val timestamp = new ArrowType.Timestamp(TimeUnit.MILLISECOND, "UTC") val string: ArrowType.Utf8 = ArrowType.Utf8.INSTANCE val texeraSchema: Schema = Schema() .add("test-1", AttributeType.INTEGER) .add("test-2", AttributeType.LONG) .add("test-3", AttributeType.BOOLEAN) .add("test-4", AttributeType.DOUBLE) .add("test-5", AttributeType.TIMESTAMP) .add("test-6", AttributeType.STRING) val arrowSchema: org.apache.arrow.vector.types.pojo.Schema = new org.apache.arrow.vector.types.pojo.Schema( Array( Field.nullablePrimitive("test-1", signedInt), Field.nullablePrimitive("test-2", signedLongInt), Field.nullablePrimitive("test-3", boolean), Field.nullablePrimitive("test-4", double), Field.nullablePrimitive("test-5", timestamp), Field.nullablePrimitive("test-6", string) ).toList.asJava ) behavior of "ArrowUtils" it should "convert to AttributeTypes correctly" in { assert(ArrowUtils.toAttributeType(unsignedShortInt) == AttributeType.INTEGER) assert(ArrowUtils.toAttributeType(signedShortInt) == AttributeType.INTEGER) assert(ArrowUtils.toAttributeType(unsignedInt) == AttributeType.INTEGER) assert(ArrowUtils.toAttributeType(signedInt) == AttributeType.INTEGER) assert(ArrowUtils.toAttributeType(unsignedLongInt) == AttributeType.LONG) assert(ArrowUtils.toAttributeType(signedLongInt) == AttributeType.LONG) assert(ArrowUtils.toAttributeType(boolean) == AttributeType.BOOLEAN) assert(ArrowUtils.toAttributeType(float) == AttributeType.DOUBLE) assert(ArrowUtils.toAttributeType(double) == AttributeType.DOUBLE) assert(ArrowUtils.toAttributeType(half) == AttributeType.DOUBLE) assert(ArrowUtils.toAttributeType(timestamp) == AttributeType.TIMESTAMP) assert(ArrowUtils.toAttributeType(timestamp) == AttributeType.TIMESTAMP) assert(ArrowUtils.toAttributeType(string) == AttributeType.STRING) } it should "convert from AttributeType correctly" in { assert(ArrowUtils.fromAttributeType(AttributeType.INTEGER) == signedInt) assert(ArrowUtils.fromAttributeType(AttributeType.LONG) == signedLongInt) assert(ArrowUtils.fromAttributeType(AttributeType.BOOLEAN) == boolean) assert(ArrowUtils.fromAttributeType(AttributeType.DOUBLE) == double) assert(ArrowUtils.fromAttributeType(AttributeType.TIMESTAMP) == timestamp) assert(ArrowUtils.fromAttributeType(AttributeType.STRING) == string) // a special case that's not symmetric, AttributeType.ANY will be converted to ArrowType.Utf8 // but not the other way around. assert(ArrowUtils.fromAttributeType(AttributeType.ANY) == string) // LARGE_BINARY is converted to ArrowType.Utf8 (same as STRING) assert(ArrowUtils.fromAttributeType(AttributeType.LARGE_BINARY) == string) } it should "raise AttributeTypeException when converting unsupported types" in { assertThrows[AttributeTypeException] { ArrowUtils.toAttributeType(new ArrowType.Null) } assertThrows[AttributeTypeException] { ArrowUtils.toAttributeType(new ArrowType.Date(DateUnit.DAY)) } assertThrows[AttributeTypeException] { ArrowUtils.toAttributeType(new ArrowType.Map(true)) } assertThrows[AttributeTypeException] { ArrowUtils.toAttributeType(new ArrowType.List) } assertThrows[AttributeTypeException] { ArrowUtils.toAttributeType(new ArrowType.Interval(IntervalUnit.DAY_TIME)) } } it should "convert to Texera Schema correctly" in { assert(ArrowUtils.toTexeraSchema(arrowSchema) == texeraSchema) } it should "convert from Texera Schema correctly" in { assert(ArrowUtils.fromTexeraSchema(texeraSchema) == arrowSchema) } it should "set Arrow Fields from Texera Tuple correctly" in { val tuple = Tuple .builder(texeraSchema) .addSequentially( Array( Int.box(2), Long.box(1L), Boolean.box(true), Double.box(1.1), new Timestamp(10000L), "hello world" ) ) .build() val allocator: BufferAllocator = new RootAllocator() val vectorSchemaRoot = VectorSchemaRoot.create(arrowSchema, allocator) vectorSchemaRoot.allocateNew() val rowCount = vectorSchemaRoot.getRowCount val index = 1 // set Tuple into the Vectors ArrowUtils.setTexeraTuple(tuple, index, vectorSchemaRoot) assert(vectorSchemaRoot.getVector(0).getObject(index).asInstanceOf[Int] == 2) assert(vectorSchemaRoot.getVector(1).getObject(index).asInstanceOf[Long] == 1L) assert(vectorSchemaRoot.getVector(2).getObject(index).asInstanceOf[Boolean] == true) assert(vectorSchemaRoot.getVector(3).getObject(index).asInstanceOf[Double] == 1.1) // the arrow storage type of timestamp is Long assert(vectorSchemaRoot.getVector(4).getObject(index).asInstanceOf[Long] == 10000L) // the arrow storage type of string is Text assert( vectorSchemaRoot.getVector(5).getObject(index) == new org.apache.arrow.vector.util.Text("hello world") ) // should not have more vectors created. assertThrows[IllegalArgumentException](vectorSchemaRoot.getVector(7).getObject(index)) // other fields at other untouched indices should be null assert(vectorSchemaRoot.getVector(0).getObject(index + 1) == null) // now the rowCount should be incremented by 1 assert(vectorSchemaRoot.getRowCount == rowCount + 1) } it should "get Texera Tuple from Arrow Fields correctly" in { val tuple = Tuple .builder(texeraSchema) .addSequentially( Array( Int.box(2), Long.box(1L), Boolean.box(true), Double.box(1.1), new Timestamp(10000L), "hello world" ) ) .build() val allocator: BufferAllocator = new RootAllocator() val vectorSchemaRoot = VectorSchemaRoot.create(arrowSchema, allocator) vectorSchemaRoot.allocateNew() // set Tuple into the Vectors ArrowUtils.appendTexeraTuple(tuple, vectorSchemaRoot) // get the Tuple from the Vectors assert(ArrowUtils.getTexeraTuple(0, vectorSchemaRoot) == tuple) } it should "get Texera Tuple from Arrow Fields with null values correctly" in { val tuple = Tuple .builder(texeraSchema) .addSequentially( Array( Int.box(2), null, Boolean.box(true), Double.box(1.1), null, null ) ) .build() val allocator: BufferAllocator = new RootAllocator() val vectorSchemaRoot = VectorSchemaRoot.create(arrowSchema, allocator) vectorSchemaRoot.allocateNew() // set Tuple into the Vectors ArrowUtils.appendTexeraTuple(tuple, vectorSchemaRoot) // get the Tuple from the Vectors assert(ArrowUtils.getTexeraTuple(0, vectorSchemaRoot) == tuple) } it should "convert from AttributeType to ArrowType for LARGE_BINARY correctly" in { // LARGE_BINARY is converted to ArrowType.Utf8 (stored as string) assert(ArrowUtils.fromAttributeType(AttributeType.LARGE_BINARY) == string) } it should "convert Texera Schema with LARGE_BINARY to Arrow Schema with metadata correctly" in { val texeraSchemaWithLargeBinary = Schema() .add("regular_string", AttributeType.STRING) .add("large_binary_field", AttributeType.LARGE_BINARY) val arrowSchema = ArrowUtils.fromTexeraSchema(texeraSchemaWithLargeBinary) // Check that regular string field has no metadata val regularStringField = arrowSchema.getFields.get(0) assert(regularStringField.getName == "regular_string") assert(regularStringField.getType == string) assert( regularStringField.getMetadata == null || !regularStringField.getMetadata.containsKey( "texera_type" ) ) // Check that LARGE_BINARY field has metadata val largeBinaryField = arrowSchema.getFields.get(1) assert(largeBinaryField.getName == "large_binary_field") assert(largeBinaryField.getType == string) // LARGE_BINARY is stored as Utf8 assert(largeBinaryField.getMetadata != null) assert(largeBinaryField.getMetadata.get("texera_type") == "LARGE_BINARY") } it should "convert Arrow Schema with LARGE_BINARY metadata to Texera Schema correctly" in { // Create Arrow schema with LARGE_BINARY metadata val largeBinaryMetadata = new util.HashMap[String, String]() largeBinaryMetadata.put("texera_type", "LARGE_BINARY") val arrowSchemaWithLargeBinary = new org.apache.arrow.vector.types.pojo.Schema( Array( Field.nullablePrimitive("regular_string", string), new Field( "large_binary_field", new FieldType(true, string, null, largeBinaryMetadata), null ) ).toList.asJava ) val texeraSchema = ArrowUtils.toTexeraSchema(arrowSchemaWithLargeBinary) assert(texeraSchema.getAttribute("regular_string").getName == "regular_string") assert(texeraSchema.getAttribute("regular_string").getType == AttributeType.STRING) assert(texeraSchema.getAttribute("large_binary_field").getName == "large_binary_field") assert(texeraSchema.getAttribute("large_binary_field").getType == AttributeType.LARGE_BINARY) } it should "set and get Texera Tuple with LARGE_BINARY correctly" in { val texeraSchemaWithLargeBinary = Schema() .add("large_binary_field", AttributeType.LARGE_BINARY) .add("regular_string", AttributeType.STRING) val largeBinary = new LargeBinary("s3://test-bucket/path/to/object") val tuple = Tuple .builder(texeraSchemaWithLargeBinary) .addSequentially( Array( largeBinary, "regular string value" ) ) .build() val allocator: BufferAllocator = new RootAllocator() val arrowSchema = ArrowUtils.fromTexeraSchema(texeraSchemaWithLargeBinary) val vectorSchemaRoot = VectorSchemaRoot.create(arrowSchema, allocator) vectorSchemaRoot.allocateNew() // Set Tuple into the Vectors ArrowUtils.appendTexeraTuple(tuple, vectorSchemaRoot) // Verify the LARGE_BINARY is stored as string (URI) in Arrow val storedValue = vectorSchemaRoot.getVector(0).getObject(0) assert(storedValue.toString == "s3://test-bucket/path/to/object") // Get the Tuple from the Vectors val retrievedTuple = ArrowUtils.getTexeraTuple(0, vectorSchemaRoot) assert(retrievedTuple.getField[LargeBinary](0) == largeBinary) assert(retrievedTuple.getField[String](1) == "regular string value") } it should "handle null LARGE_BINARY values correctly" in { val texeraSchemaWithLargeBinary = Schema() .add("large_binary_field", AttributeType.LARGE_BINARY) val tuple = Tuple .builder(texeraSchemaWithLargeBinary) .addSequentially( Array( null.asInstanceOf[LargeBinary] ) ) .build() val allocator: BufferAllocator = new RootAllocator() val arrowSchema = ArrowUtils.fromTexeraSchema(texeraSchemaWithLargeBinary) val vectorSchemaRoot = VectorSchemaRoot.create(arrowSchema, allocator) vectorSchemaRoot.allocateNew() // Set Tuple into the Vectors ArrowUtils.appendTexeraTuple(tuple, vectorSchemaRoot) // Verify null is stored correctly assert(vectorSchemaRoot.getVector(0).getObject(0) == null) // Get the Tuple from the Vectors val retrievedTuple = ArrowUtils.getTexeraTuple(0, vectorSchemaRoot) assert(retrievedTuple.getField[LargeBinary](0) == null) } it should "round-trip LARGE_BINARY schema conversion correctly" in { val originalSchema = Schema() .add("field1", AttributeType.STRING) .add("field2", AttributeType.LARGE_BINARY) .add("field3", AttributeType.INTEGER) .add("field4", AttributeType.LARGE_BINARY) // Convert to Arrow and back val arrowSchema = ArrowUtils.fromTexeraSchema(originalSchema) val roundTripSchema = ArrowUtils.toTexeraSchema(arrowSchema) assert(roundTripSchema.getAttribute("field1").getType == AttributeType.STRING) assert(roundTripSchema.getAttribute("field2").getType == AttributeType.LARGE_BINARY) assert(roundTripSchema.getAttribute("field3").getType == AttributeType.INTEGER) assert(roundTripSchema.getAttribute("field4").getType == AttributeType.LARGE_BINARY) assert(roundTripSchema == originalSchema) } } ================================================ FILE: common/workflow-operator/src/test/scala/org/apache/texera/amber/util/PythonCodeRawInvalidTextSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.util import com.typesafe.config.ConfigFactory import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.pybuilder.PythonReflectionTextUtils.truncateBlock import org.apache.texera.amber.pybuilder.PythonReflectionUtils import org.scalatest.funsuite.AnyFunSuite import java.nio.charset.StandardCharsets import java.nio.file.Files import java.util.concurrent import java.util.concurrent.TimeUnit import scala.util.Try /** * Regression tests for validation pipeline used for PythonOperatorDescriptor codegen. * * What this suite checks: * 1) Code generation must not leak raw invalid text from @JsonProperty string values into the emitted Python. * 2) The emitted Python should pass a basic `py_compile` sanity check under an isolated interpreter. * * Notes: * - "RawInvalid" is a marker chosen to be very unlikely to appear in real code. * - We only scan under AcceptPackages to keep the suite fast and avoid pulling in unrelated classes. */ final class PythonCodeRawInvalidTextSpec extends AnyFunSuite { // Scala literal "\\!." is the 3-char string: \!. private val RawInvalid: String = "\\!." private val MaxDepth: Int = 3 private val AcceptPackages: Seq[String] = Seq("org.apache.texera.amber.operator") /** * Runs `python -m py_compile` on the provided source, using an isolated interpreter invocation. * * Isolation flags: * - -I : isolate (ignore user site-packages / env) * - -S : don't import site * - -B : don't write .pyc files * * @return Right(()) on success, Left(errorMessage) on failure (including timeout). */ private def pyCompile(pythonExecutable: String, pythonSource: String): Either[String, Unit] = { val tempFile = Files.createTempFile("texera_py_compile_", ".py") try { Files.write(tempFile, pythonSource.getBytes(StandardCharsets.UTF_8)) val processBuilder = new ProcessBuilder( pythonExecutable, "-I", "-S", "-B", "-m", "py_compile", tempFile.toString ) // Merge stderr into stdout to keep a single combined output stream for easy reporting. processBuilder.redirectErrorStream(true) val processStartEither = Try(processBuilder.start()).toEither.left.map { thrown => s"Could not start python executable '$pythonExecutable': ${thrown.getClass.getName}: ${Option(thrown.getMessage) .getOrElse("")}" } processStartEither.flatMap { process => val didFinish = process.waitFor(30, concurrent.TimeUnit.SECONDS) if (!didFinish) { process.destroyForcibly() Left("py_compile timed out after 30s (process was killed)") } else { val combinedOutput = Try(new String(process.getInputStream.readAllBytes(), StandardCharsets.UTF_8)) .getOrElse("") .trim val exitCode = process.exitValue() if (exitCode == 0) Right(()) else { val clippedOutput = if (combinedOutput.nonEmpty) truncateBlock(combinedOutput, maxLines = 40, maxChars = 8000) else "(no output)" Left(s"py_compile failed (exit=$exitCode)\nOutput:\n$clippedOutput") } } } } finally { Try(Files.deleteIfExists(tempFile)) () } } /** * Loads the Python executable path from configuration, with fallbacks. * * Lookup strategy: * 1) Try parsing udf.conf from resources and resolving it. * 2) Fall back to ConfigFactory.load(). * 3) Read python.path, trim, and ensure it's non-empty. * 4) If missing or invalid, fall back to "python3", then "python", then "py" * (validated by running --version). */ private def loadPythonExeFromUdfConf(): Option[String] = { def fromConfig: Option[String] = { val configOpt = Try(ConfigFactory.parseResources("udf.conf").resolve()).toOption .orElse(Try(ConfigFactory.load()).toOption) configOpt .flatMap(c => Try(c.getConfig("python").getString("path")).toOption) .map(_.trim) .filter(_.nonEmpty) } def isRunnable(exe: String): Boolean = { val pTry = Try(new ProcessBuilder(exe, "--version").redirectErrorStream(true).start()) pTry.toOption.exists { p => val finished = p.waitFor(5, TimeUnit.SECONDS) if (!finished) { p.destroyForcibly(); false } else p.exitValue() == 0 } } val candidates = fromConfig.toList ++ List("python3", "python", "py") candidates.distinct.find(isRunnable) } test( "PythonOperatorDescriptor.generatePythonCode should not contain raw invalid JsonProperty Strings" ) { val classLoader = Thread.currentThread().getContextClassLoader val descriptorCandidates = PythonReflectionUtils .scanCandidates( base = classOf[PythonOperatorDescriptor], acceptPackages = AcceptPackages, classLoader = classLoader ) .map(_.asInstanceOf[Class[_ <: PythonOperatorDescriptor]]) .sortBy(_.getName) if (descriptorCandidates.isEmpty) { fail( s"No implementations of ${classOf[PythonOperatorDescriptor].getName} were found. " + s"Check acceptPackages() / test classpath / module wiring." ) } val total = descriptorCandidates.size var ok = 0 var checked = 0 val allFindings = descriptorCandidates.flatMap { descriptorClass => checked += 1 val findings = PythonReflectionUtils.checkDescriptor( descriptorClass, rawInvalidText = RawInvalid, maxDepth = MaxDepth ) if (findings.isEmpty) { ok += 1 println(s"[raw-invalid OK $ok/$total | checked $checked/$total] ${descriptorClass.getName}") } findings } println(s"[raw-invalid SUMMARY] ok=$ok/$total") if (allFindings.nonEmpty) { fail(PythonReflectionUtils.renderReport(allFindings, total = total)) } } test("PythonOperatorDescriptor.generatePythonCode should py_compile under isolated Python") { val pythonExeOpt = loadPythonExeFromUdfConf() if (pythonExeOpt.isEmpty) { fail( "python.path not found in udf.conf (or application.conf). Configure python.path to enable this test." ) } val pythonExecutable = pythonExeOpt.get val classLoader = Thread.currentThread().getContextClassLoader val descriptorCandidates = PythonReflectionUtils .scanCandidates( base = classOf[PythonOperatorDescriptor], acceptPackages = AcceptPackages, classLoader = classLoader ) .map(_.asInstanceOf[Class[_ <: PythonOperatorDescriptor]]) .sortBy(_.getName) if (descriptorCandidates.isEmpty) { fail( s"No implementations of ${classOf[PythonOperatorDescriptor].getName} were found. " + s"Check acceptPackages() / test classpath / module wiring." ) } val total = descriptorCandidates.size var ok = 0 var checked = 0 val allFindings = descriptorCandidates.flatMap { descriptorClass => checked += 1 val checkResult = PythonReflectionUtils.checkDescriptorWithCode( descriptorClass, rawInvalidText = RawInvalid, maxDepth = MaxDepth ) val pyCompileFindings = checkResult.code.toSeq.flatMap { generatedCode => pyCompile(pythonExecutable, generatedCode) match { case Left(errorMessage) => Seq(PythonReflectionUtils.Finding(descriptorClass.getName, "py-compile", errorMessage)) case Right(()) => Nil } } val findings = checkResult.findings ++ pyCompileFindings if (findings.isEmpty && checkResult.code.nonEmpty) { ok += 1 println(s"[py-compile OK $ok/$total | checked $checked/$total] ${descriptorClass.getName}") } findings } println(s"[py-compile SUMMARY] ok=$ok/$total") if (allFindings.nonEmpty) { fail(PythonReflectionUtils.renderReport(allFindings, total = total)) } } } ================================================ FILE: computing-unit-managing-service/LICENSE-binary ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Support. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or support. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================================ THIRD-PARTY COMPONENTS ================================================================================ Apache Texera's binary distribution of this service includes the following third-party components, grouped by license. Each section references licenses/ for the full text of the applicable license. Components under the Apache License, Version 2.0 are governed by the same license terms as Apache Texera itself and are listed for completeness. Locations within the distribution: - Scala/Java jars listed below ship under the lib/ directory of this service's Universal zip. - Source files derived from third-party projects are compiled into the bundled jars under lib/ and live at the listed paths in the Apache Texera source tree. -------------------------------------------------------------------------------- Dependencies under the Apache License, Version 2.0 -------------------------------------------------------------------------------- Scala/Java jars: - com.fasterxml.classmate-1.7.0.jar - com.fasterxml.jackson.core.jackson-annotations-2.18.6.jar - com.fasterxml.jackson.core.jackson-core-2.18.6.jar - com.fasterxml.jackson.core.jackson-databind-2.18.6.jar - com.fasterxml.jackson.dataformat.jackson-dataformat-yaml-2.17.0.jar - com.fasterxml.jackson.datatype.jackson-datatype-guava-2.16.1.jar - com.fasterxml.jackson.datatype.jackson-datatype-jdk8-2.16.1.jar - com.fasterxml.jackson.datatype.jackson-datatype-jsr310-2.17.0.jar - com.fasterxml.jackson.jakarta.rs.jackson-jakarta-rs-base-2.16.1.jar - com.fasterxml.jackson.jakarta.rs.jackson-jakarta-rs-json-provider-2.16.1.jar - com.fasterxml.jackson.jaxrs.jackson-jaxrs-base-2.10.5.jar - com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider-2.10.5.jar - com.fasterxml.jackson.module.jackson-module-blackbird-2.16.1.jar - com.fasterxml.jackson.module.jackson-module-jakarta-xmlbind-annotations-2.16.1.jar - com.fasterxml.jackson.module.jackson-module-jsonSchema-2.18.6.jar - com.fasterxml.jackson.module.jackson-module-no-ctor-deser-2.18.6.jar - com.fasterxml.jackson.module.jackson-module-parameter-names-2.16.1.jar - com.fasterxml.jackson.module.jackson-module-scala_2.13-2.18.6.jar - com.fasterxml.woodstox.woodstox-core-5.3.0.jar - com.github.ben-manes.caffeine.caffeine-3.1.8.jar - com.github.sisyphsu.dateparser-1.0.11.jar - com.github.sisyphsu.retree-1.0.4.jar - com.github.stephenc.jcip.jcip-annotations-1.0-1.jar - com.google.android.annotations-4.1.1.4.jar - com.google.api.grpc.proto-google-common-protos-2.22.0.jar - com.google.code.findbugs.jsr305-3.0.2.jar - com.google.code.gson.gson-2.11.0.jar - com.google.errorprone.error_prone_annotations-2.27.0.jar - com.google.flatbuffers.flatbuffers-java-23.5.26.jar - com.google.guava.failureaccess-1.0.2.jar - com.google.guava.guava-33.0.0-jre.jar - com.google.guava.listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar - com.google.inject.extensions.guice-servlet-4.0.jar - com.google.inject.guice-4.0.jar - com.google.j2objc.j2objc-annotations-2.8.jar - com.googlecode.javaewah.JavaEWAH-1.1.12.jar - com.helger.profiler-1.1.1.jar - com.nimbusds.nimbus-jose-jwt-9.8.1.jar - com.softwaremill.sttp.client4.core_2.13-4.0.0-M6.jar - com.softwaremill.sttp.model.core_2.13-1.7.2.jar - com.softwaremill.sttp.shared.core_2.13-1.3.16.jar - com.softwaremill.sttp.shared.ws_2.13-1.3.16.jar - com.squareup.okhttp.okhttp-2.7.5.jar - com.squareup.okhttp3.logging-interceptor-4.12.0.jar - com.squareup.okhttp3.okhttp-4.12.0.jar - com.squareup.okio.okio-3.6.0.jar - com.squareup.okio.okio-jvm-3.6.0.jar - com.thesamet.scalapb.lenses_2.13-0.11.20.jar - com.thesamet.scalapb.scalapb-json4s_2.13-0.12.0.jar - com.thesamet.scalapb.scalapb-runtime_2.13-0.11.20.jar - com.typesafe.config-1.4.6.jar - com.typesafe.play.play-functional_2.13-2.10.6.jar - com.typesafe.play.play-json_2.13-2.10.6.jar - com.typesafe.scala-logging.scala-logging_2.13-3.9.5.jar - commons-beanutils.commons-beanutils-1.9.4.jar - commons-cli.commons-cli-1.2.jar - commons-codec.commons-codec-1.17.1.jar - commons-collections.commons-collections-3.2.2.jar - commons-io.commons-io-2.16.1.jar - commons-logging.commons-logging-1.2.jar - commons-net.commons-net-3.6.jar - commons-pool.commons-pool-1.6.jar - dev.failsafe.failsafe-3.3.2.jar - io.airlift.aircompressor-0.27.jar - io.dropwizard.dropwizard-auth-4.0.7.jar - io.dropwizard.dropwizard-configuration-4.0.7.jar - io.dropwizard.dropwizard-core-4.0.7.jar - io.dropwizard.dropwizard-health-4.0.7.jar - io.dropwizard.dropwizard-jackson-4.0.7.jar - io.dropwizard.dropwizard-jersey-4.0.7.jar - io.dropwizard.dropwizard-jetty-4.0.7.jar - io.dropwizard.dropwizard-lifecycle-4.0.7.jar - io.dropwizard.dropwizard-logging-4.0.7.jar - io.dropwizard.dropwizard-metrics-4.0.7.jar - io.dropwizard.dropwizard-request-logging-4.0.7.jar - io.dropwizard.dropwizard-servlets-4.0.7.jar - io.dropwizard.dropwizard-util-4.0.7.jar - io.dropwizard.dropwizard-validation-4.0.7.jar - io.dropwizard.logback.logback-throttling-appender-1.4.2.jar - io.dropwizard.metrics.metrics-annotation-4.2.25.jar - io.dropwizard.metrics.metrics-caffeine-4.2.25.jar - io.dropwizard.metrics.metrics-core-4.2.25.jar - io.dropwizard.metrics.metrics-healthchecks-4.2.25.jar - io.dropwizard.metrics.metrics-jakarta-servlets-4.2.25.jar - io.dropwizard.metrics.metrics-jersey3-4.2.25.jar - io.dropwizard.metrics.metrics-jetty11-4.2.25.jar - io.dropwizard.metrics.metrics-jmx-4.2.25.jar - io.dropwizard.metrics.metrics-json-4.2.25.jar - io.dropwizard.metrics.metrics-jvm-4.2.25.jar - io.dropwizard.metrics.metrics-logback-4.2.25.jar - io.fabric8.kubernetes-client-6.12.1.jar - io.fabric8.kubernetes-client-api-6.12.1.jar - io.fabric8.kubernetes-httpclient-okhttp-6.12.1.jar - io.fabric8.kubernetes-model-admissionregistration-6.12.1.jar - io.fabric8.kubernetes-model-apiextensions-6.12.1.jar - io.fabric8.kubernetes-model-apps-6.12.1.jar - io.fabric8.kubernetes-model-autoscaling-6.12.1.jar - io.fabric8.kubernetes-model-batch-6.12.1.jar - io.fabric8.kubernetes-model-certificates-6.12.1.jar - io.fabric8.kubernetes-model-common-6.12.1.jar - io.fabric8.kubernetes-model-coordination-6.12.1.jar - io.fabric8.kubernetes-model-core-6.12.1.jar - io.fabric8.kubernetes-model-discovery-6.12.1.jar - io.fabric8.kubernetes-model-events-6.12.1.jar - io.fabric8.kubernetes-model-extensions-6.12.1.jar - io.fabric8.kubernetes-model-flowcontrol-6.12.1.jar - io.fabric8.kubernetes-model-gatewayapi-6.12.1.jar - io.fabric8.kubernetes-model-metrics-6.12.1.jar - io.fabric8.kubernetes-model-networking-6.12.1.jar - io.fabric8.kubernetes-model-node-6.12.1.jar - io.fabric8.kubernetes-model-policy-6.12.1.jar - io.fabric8.kubernetes-model-rbac-6.12.1.jar - io.fabric8.kubernetes-model-resource-6.12.1.jar - io.fabric8.kubernetes-model-scheduling-6.12.1.jar - io.fabric8.kubernetes-model-storageclass-6.12.1.jar - io.fabric8.zjsonpatch-0.3.0.jar - io.grpc.grpc-api-1.60.0.jar - io.grpc.grpc-context-1.60.0.jar - io.grpc.grpc-core-1.60.0.jar - io.grpc.grpc-netty-1.60.0.jar - io.grpc.grpc-protobuf-1.60.0.jar - io.grpc.grpc-protobuf-lite-1.60.0.jar - io.grpc.grpc-stub-1.60.0.jar - io.grpc.grpc-util-1.60.0.jar - io.gsonfire.gson-fire-1.9.0.jar - io.kubernetes.client-java-21.0.0.jar - io.kubernetes.client-java-api-21.0.0.jar - io.kubernetes.client-java-proto-21.0.0.jar - io.lakefs.sdk-1.51.0.jar - io.netty.netty-3.10.6.Final.jar - io.netty.netty-buffer-4.1.104.Final.jar - io.netty.netty-codec-4.1.104.Final.jar - io.netty.netty-codec-http-4.1.100.Final.jar - io.netty.netty-codec-http2-4.1.100.Final.jar - io.netty.netty-codec-socks-4.1.100.Final.jar - io.netty.netty-common-4.1.104.Final.jar - io.netty.netty-handler-4.1.104.Final.jar - io.netty.netty-handler-proxy-4.1.100.Final.jar - io.netty.netty-resolver-4.1.104.Final.jar - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-linux-aarch_64.jar - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-linux-x86_64.jar - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-osx-aarch_64.jar - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-osx-x86_64.jar - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-windows-x86_64.jar - io.netty.netty-tcnative-boringssl-static-2.0.61.Final.jar - io.netty.netty-tcnative-classes-2.0.61.Final.jar - io.netty.netty-transport-4.1.104.Final.jar - io.netty.netty-transport-native-unix-common-4.1.104.Final.jar - io.perfmark.perfmark-api-0.26.0.jar - io.r2dbc.r2dbc-spi-0.9.0.RELEASE.jar - io.swagger.swagger-annotations-1.6.14.jar - jakarta.inject.jakarta.inject-api-2.0.1.jar - jakarta.validation.jakarta.validation-api-3.0.2.jar - javax.inject.javax.inject-1.jar - javax.validation.validation-api-2.0.1.Final.jar - log4j.log4j-1.2.17.jar - net.minidev.accessors-smart-2.4.2.jar - net.minidev.json-smart-2.4.2.jar - org.apache.arrow.arrow-format-15.0.2.jar - org.apache.arrow.arrow-memory-core-15.0.2.jar - org.apache.arrow.arrow-memory-netty-15.0.2.jar - org.apache.arrow.arrow-vector-15.0.2.jar - org.apache.arrow.flight-core-15.0.2.jar - org.apache.arrow.flight-grpc-15.0.2.jar - org.apache.avro.avro-1.12.0.jar - org.apache.commons.commons-collections4-4.4.jar - org.apache.commons.commons-compress-1.26.2.jar - org.apache.commons.commons-configuration2-2.1.1.jar - org.apache.commons.commons-jcs3-core-3.2.jar - org.apache.commons.commons-lang3-3.14.0.jar - org.apache.commons.commons-math3-3.1.1.jar - org.apache.commons.commons-text-1.11.0.jar - org.apache.commons.commons-vfs2-2.9.0.jar - org.apache.curator.curator-client-4.2.0.jar - org.apache.curator.curator-framework-4.2.0.jar - org.apache.curator.curator-recipes-4.2.0.jar - org.apache.hadoop.hadoop-annotations-3.3.1.jar - org.apache.hadoop.hadoop-auth-3.3.1.jar - org.apache.hadoop.hadoop-common-3.3.1.jar - org.apache.hadoop.hadoop-hdfs-client-3.3.1.jar - org.apache.hadoop.hadoop-mapreduce-client-core-3.3.1.jar - org.apache.hadoop.hadoop-yarn-api-3.3.1.jar - org.apache.hadoop.hadoop-yarn-client-3.3.1.jar - org.apache.hadoop.hadoop-yarn-common-3.3.1.jar - org.apache.hadoop.thirdparty.hadoop-shaded-guava-1.1.1.jar - org.apache.hadoop.thirdparty.hadoop-shaded-protobuf_3_7-1.1.1.jar - org.apache.htrace.htrace-core4-4.1.0-incubating.jar - org.apache.httpcomponents.client5.httpclient5-5.4.jar - org.apache.httpcomponents.core5.httpcore5-5.3.jar - org.apache.httpcomponents.core5.httpcore5-h2-5.3.jar - org.apache.httpcomponents.httpclient-4.5.13.jar - org.apache.httpcomponents.httpcore-4.4.16.jar - org.apache.iceberg.iceberg-api-1.7.1.jar - org.apache.iceberg.iceberg-aws-1.7.1.jar - org.apache.iceberg.iceberg-bundled-guava-1.7.1.jar - org.apache.iceberg.iceberg-common-1.7.1.jar - org.apache.iceberg.iceberg-core-1.7.1.jar - org.apache.iceberg.iceberg-data-1.7.1.jar - org.apache.iceberg.iceberg-parquet-1.7.1.jar - org.apache.kerby.kerb-admin-1.0.1.jar - org.apache.kerby.kerb-client-1.0.1.jar - org.apache.kerby.kerb-common-1.0.1.jar - org.apache.kerby.kerb-core-1.0.1.jar - org.apache.kerby.kerb-crypto-1.0.1.jar - org.apache.kerby.kerb-identity-1.0.1.jar - org.apache.kerby.kerb-server-1.0.1.jar - org.apache.kerby.kerb-simplekdc-1.0.1.jar - org.apache.kerby.kerb-util-1.0.1.jar - org.apache.kerby.kerby-asn1-1.0.1.jar - org.apache.kerby.kerby-config-1.0.1.jar - org.apache.kerby.kerby-pkix-1.0.1.jar - org.apache.kerby.kerby-util-1.0.1.jar - org.apache.kerby.kerby-xdr-1.0.1.jar - org.apache.kerby.token-provider-1.0.1.jar - org.apache.orc.orc-core-1.9.4-nohive.jar - org.apache.orc.orc-shims-1.9.4.jar - org.apache.parquet.parquet-avro-1.13.1.jar - org.apache.parquet.parquet-column-1.13.1.jar - org.apache.parquet.parquet-common-1.13.1.jar - org.apache.parquet.parquet-encoding-1.13.1.jar - org.apache.parquet.parquet-format-structures-1.13.1.jar - org.apache.parquet.parquet-hadoop-1.13.1.jar - org.apache.parquet.parquet-jackson-1.13.1.jar - org.apache.yetus.audience-annotations-0.13.0.jar - org.apache.zookeeper.zookeeper-3.5.6.jar - org.apache.zookeeper.zookeeper-jute-3.5.6.jar - org.bitbucket.b_c.jose4j-0.9.6.jar - org.eclipse.jetty.jetty-http-11.0.20.jar - org.eclipse.jetty.jetty-io-11.0.20.jar - org.eclipse.jetty.jetty-security-11.0.20.jar - org.eclipse.jetty.jetty-server-11.0.20.jar - org.eclipse.jetty.jetty-servlet-11.0.20.jar - org.eclipse.jetty.jetty-servlets-11.0.20.jar - org.eclipse.jetty.jetty-util-11.0.20.jar - org.eclipse.jetty.toolchain.jetty-jakarta-servlet-api-5.0.2.jar - org.eclipse.jetty.toolchain.setuid.jetty-setuid-java-1.0.4.jar - org.eclipse.jetty.websocket.websocket-api-9.4.40.v20210413.jar - org.eclipse.jetty.websocket.websocket-client-9.4.40.v20210413.jar - org.eclipse.jetty.websocket.websocket-common-9.4.40.v20210413.jar - org.ehcache.sizeof-0.4.3.jar - org.hibernate.validator.hibernate-validator-7.0.5.Final.jar - org.javassist.javassist-3.30.2-GA.jar - org.jboss.logging.jboss-logging-3.5.3.Final.jar - org.jetbrains.annotations-17.0.0.jar - org.jetbrains.kotlin.kotlin-stdlib-1.9.10.jar - org.jetbrains.kotlin.kotlin-stdlib-common-1.9.10.jar - org.jetbrains.kotlin.kotlin-stdlib-jdk7-1.9.10.jar - org.jetbrains.kotlin.kotlin-stdlib-jdk8-1.9.10.jar - org.jheaps.jheaps-0.11.jar - org.jooq.jooq-3.16.23.jar - org.json4s.json4s-ast_2.13-4.0.1.jar - org.json4s.json4s-jackson-core_2.13-4.0.1.jar - org.openapitools.jackson-databind-nullable-0.2.6.jar - org.roaringbitmap.RoaringBitmap-1.3.0.jar - org.scala-lang.modules.scala-collection-compat_2.13-2.13.0.jar - org.scala-lang.scala-library-2.13.18.jar - org.scala-lang.scala-reflect-2.13.18.jar - org.slf4j.jcl-over-slf4j-2.0.12.jar - org.slf4j.log4j-over-slf4j-2.0.12.jar - org.snakeyaml.snakeyaml-engine-2.7.jar - org.xerial.snappy.snappy-java-1.1.8.3.jar - org.yaml.snakeyaml-2.2.jar - software.amazon.awssdk.annotations-2.29.51.jar - software.amazon.awssdk.apache-client-2.29.51.jar - software.amazon.awssdk.arns-2.29.51.jar - software.amazon.awssdk.auth-2.29.51.jar - software.amazon.awssdk.aws-core-2.29.51.jar - software.amazon.awssdk.aws-query-protocol-2.29.51.jar - software.amazon.awssdk.aws-xml-protocol-2.29.51.jar - software.amazon.awssdk.checksums-2.29.51.jar - software.amazon.awssdk.checksums-spi-2.29.51.jar - software.amazon.awssdk.crt-core-2.29.51.jar - software.amazon.awssdk.endpoints-spi-2.29.51.jar - software.amazon.awssdk.http-auth-2.29.51.jar - software.amazon.awssdk.http-auth-aws-2.29.51.jar - software.amazon.awssdk.http-auth-aws-eventstream-2.29.51.jar - software.amazon.awssdk.http-auth-spi-2.29.51.jar - software.amazon.awssdk.http-client-spi-2.29.51.jar - software.amazon.awssdk.identity-spi-2.29.51.jar - software.amazon.awssdk.json-utils-2.29.51.jar - software.amazon.awssdk.metrics-spi-2.29.51.jar - software.amazon.awssdk.netty-nio-client-2.29.51.jar - software.amazon.awssdk.profiles-2.29.51.jar - software.amazon.awssdk.protocol-core-2.29.51.jar - software.amazon.awssdk.regions-2.29.51.jar - software.amazon.awssdk.retries-2.29.51.jar - software.amazon.awssdk.retries-spi-2.29.51.jar - software.amazon.awssdk.s3-2.29.51.jar - software.amazon.awssdk.sdk-core-2.29.51.jar - software.amazon.awssdk.sts-2.29.51.jar - software.amazon.awssdk.third-party-jackson-core-2.29.51.jar - software.amazon.awssdk.utils-2.29.51.jar - software.amazon.eventstream.eventstream-1.0.1.jar -------------------------------------------------------------------------------- Dependencies under the MIT License -------------------------------------------------------------------------------- Source files derived from third-party MIT-licensed projects: - mbknor-jackson-jsonschema common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaDraft.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaArrayWithUniqueItems.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaBool.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDefault.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDescription.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaExamples.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaFormat.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInject.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInt.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaOptions.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaString.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaTitle.java https://github.com/mbknor/mbknor-jackson-jsonschema Scala/Java jars: - net.sourceforge.argparse4j.argparse4j-0.9.0.jar - org.bouncycastle.bcpkix-jdk18on-1.78.1.jar - org.bouncycastle.bcprov-jdk18on-1.78.1.jar - org.bouncycastle.bcutil-jdk18on-1.78.1.jar - org.checkerframework.checker-qual-3.52.0.jar - org.codehaus.mojo.animal-sniffer-annotations-1.23.jar - org.projectlombok.lombok-1.18.24.jar - org.reactivestreams.reactive-streams-1.0.4.jar - org.slf4j.jul-to-slf4j-2.0.12.jar - org.slf4j.slf4j-api-2.0.16.jar -------------------------------------------------------------------------------- Dependencies under the BSD 3-Clause License -------------------------------------------------------------------------------- Scala/Java jars: - com.google.protobuf.protobuf-java-4.27.1.jar - com.google.re2j.re2j-1.1.jar - com.jcraft.jsch-0.1.55.jar - com.thoughtworks.paranamer.paranamer-2.8.jar - org.jline.jline-3.9.0.jar - org.ow2.asm.asm-8.0.1.jar - org.threeten.threeten-extra-1.7.1.jar -------------------------------------------------------------------------------- Dependencies under the BSD 2-Clause License -------------------------------------------------------------------------------- Scala/Java jars: - com.github.luben.zstd-jni-1.5.0-1.jar - dnsjava.dnsjava-2.1.7.jar - org.codehaus.woodstox.stax2-api-4.2.1.jar - org.postgresql.postgresql-42.7.10.jar -------------------------------------------------------------------------------- Dependencies under the Eclipse Public License, Version 2.0 (some are dual licensed with GPL-2.0 with Classpath Exception) -------------------------------------------------------------------------------- Scala/Java jars: - jakarta.annotation.jakarta.annotation-api-3.0.0.jar - jakarta.el.jakarta.el-api-4.0.0.jar - jakarta.servlet.jakarta.servlet-api-5.0.0.jar - jakarta.ws.rs.jakarta.ws.rs-api-3.1.0.jar - javax.ws.rs.javax.ws.rs-api-2.1.1.jar - org.glassfish.hk2.external.aopalliance-repackaged-3.0.6.jar - org.glassfish.hk2.hk2-api-3.0.6.jar - org.glassfish.hk2.hk2-locator-3.0.3.jar - org.glassfish.hk2.hk2-utils-3.0.6.jar - org.glassfish.hk2.osgi-resource-locator-1.0.3.jar - org.glassfish.jakarta.el-4.0.2.jar - org.glassfish.jersey.containers.jersey-container-servlet-3.0.12.jar - org.glassfish.jersey.containers.jersey-container-servlet-core-3.0.12.jar - org.glassfish.jersey.core.jersey-client-3.0.12.jar - org.glassfish.jersey.core.jersey-common-3.0.12.jar - org.glassfish.jersey.core.jersey-server-3.0.12.jar - org.glassfish.jersey.ext.jersey-bean-validation-3.0.12.jar - org.glassfish.jersey.ext.jersey-metainf-services-3.0.12.jar - org.glassfish.jersey.inject.jersey-hk2-3.0.12.jar - org.jgrapht.jgrapht-core-1.4.0.jar -------------------------------------------------------------------------------- Dependencies under the Eclipse Public License, Version 1.0 (Logback is dual licensed with LGPL-2.1) -------------------------------------------------------------------------------- Scala/Java jars: - ch.qos.logback.logback-access-1.4.14.jar - ch.qos.logback.logback-classic-1.4.14.jar - ch.qos.logback.logback-core-1.4.14.jar -------------------------------------------------------------------------------- Dependencies under the Common Development and Distribution License (CDDL) (some are dual licensed with GPL-2.0 with Classpath Exception) -------------------------------------------------------------------------------- CDDL 1.0 ~~~~~~~~ Scala/Java jars: - javax.annotation.javax.annotation-api-1.3.2.jar - javax.servlet.javax.servlet-api-3.1.0.jar - javax.ws.rs.jsr311-api-1.1.1.jar CDDL 1.1 ~~~~~~~~ Scala/Java jars: - com.sun.jersey.contribs.jersey-guice-1.19.jar -------------------------------------------------------------------------------- Dependencies under the Eclipse Distribution License, Version 1.0 -------------------------------------------------------------------------------- Scala/Java jars: - com.sun.activation.jakarta.activation-2.0.1.jar - jakarta.activation.jakarta.activation-api-2.1.0.jar - jakarta.xml.bind.jakarta.xml.bind-api-3.0.1.jar - org.eclipse.collections.eclipse-collections-11.1.0.jar - org.eclipse.collections.eclipse-collections-api-11.1.0.jar - org.eclipse.jgit.org.eclipse.jgit-5.13.0.202109080827-r.jar -------------------------------------------------------------------------------- Dependencies in the Public Domain (CC0) -------------------------------------------------------------------------------- Scala/Java jars: - aopalliance.aopalliance-1.0.jar Individual jars may contain their own META-INF/LICENSE and META-INF/NOTICE files that apply to their specific contents; those files continue to govern the use of those components. ================================================ FILE: computing-unit-managing-service/NOTICE-binary ================================================ Apache Texera (Incubating) Copyright 2025-2026 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). -------------------------------------------------------------------------------- Apache Hadoop -------------------------------------------------------------------------------- Apache Hadoop Copyright 2006 and onwards The Apache Software Foundation. Export Control Notice --------------------- This distribution includes cryptographic software. The country in which you currently reside may have restrictions on the import, possession, use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. See for more information. The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this software as Export Commodity Control Number (ECCN) 5D002.C.1, which includes information security software using or performing cryptographic functions with asymmetric algorithms. The form and manner of this Apache Software Foundation distribution makes it eligible for export under the License Exception ENC Technology Software Unrestricted (TSU) exception (see the BIS Export Administration Regulations, Section 740.13) for both object code and source code. The following provides more details on the included cryptographic software: This software uses the SSL libraries from the Jetty project written by mortbay.org. Hadoop Yarn Server Web Proxy uses the BouncyCastle Java cryptography APIs written by the Legion of the Bouncy Castle Inc. -------------------------------------------------------------------------------- Apache Parquet -------------------------------------------------------------------------------- Apache Parquet MR Copyright 2014-2024 The Apache Software Foundation This product includes code from Apache Avro. Apache Avro Copyright 2010-2024 The Apache Software Foundation -------------------------------------------------------------------------------- Apache Iceberg -------------------------------------------------------------------------------- Apache Iceberg Copyright 2017-2024 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). -------------------------------------------------------------------------------- This project includes code from Kite, developed at Cloudera, Inc. with the following copyright notice: | Copyright 2013 Cloudera Inc. | | Licensed under the Apache License, Version 2.0 (the "License"); | you may not use this file except in compliance with the License. | You may obtain a copy of the License at | | http://www.apache.org/licenses/LICENSE-2.0 | | Unless required by applicable law or agreed to in writing, software | distributed under the License is distributed on an "AS IS" BASIS, | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | See the License for the specific language governing permissions and | limitations under the License. Apache Arrow (arrow-format, arrow-memory-core, arrow-memory-netty, arrow-vector, flight-core, flight-grpc) Copyright 2016-2023 The Apache Software Foundation Apache Avro Copyright 2009-2024 The Apache Software Foundation Apache Commons BeanUtils Copyright 2000-2019 The Apache Software Foundation Apache Commons CLI Copyright 2001-2022 The Apache Software Foundation Apache Commons Codec Copyright 2002-2024 The Apache Software Foundation Apache Commons Collections (3.x and 4.x) Copyright 2001-2024 The Apache Software Foundation Apache Commons Compress Copyright 2002-2024 The Apache Software Foundation Apache Commons Configuration Copyright 2001-2024 The Apache Software Foundation Apache Commons IO Copyright 2002-2024 The Apache Software Foundation Apache Commons JCS Copyright 2002-2024 The Apache Software Foundation Apache Commons Lang (2.x and 3.x) Copyright 2001-2024 The Apache Software Foundation Apache Commons Logging Copyright 2003-2014 The Apache Software Foundation Apache Commons Math Copyright 2001-2016 The Apache Software Foundation Apache Commons Net Copyright 2001-2023 The Apache Software Foundation Apache Commons Pool Copyright 2001-2024 The Apache Software Foundation Apache Commons Text Copyright 2014-2024 The Apache Software Foundation Apache Commons VFS Copyright 2002-2024 The Apache Software Foundation Apache Curator Copyright 2011-2024 The Apache Software Foundation Apache HttpComponents (httpclient, httpcore, httpasyncclient, httpclient5, httpcore5, httpcore5-h2, httpmime) Copyright 1999-2024 The Apache Software Foundation Apache HTrace (Incubating) Copyright 2016-2017 The Apache Software Foundation Apache Iceberg Copyright 2017-2024 The Apache Software Foundation Apache Kerby (kerb-* and kerby-* subprojects) Copyright 2014-2017 The Apache Software Foundation Apache Maven (many subprojects including wagon-*) Copyright 2001-2024 The Apache Software Foundation Apache ORC Copyright 2013-2024 The Apache Software Foundation Apache Yetus Copyright 2015-2023 The Apache Software Foundation Apache ZooKeeper Copyright 2008-2024 The Apache Software Foundation Apache log4j 1.2 / reload4j Copyright 2007 The Apache Software Foundation -------------------------------------------------------------------------------- Netty -------------------------------------------------------------------------------- The Netty Project Copyright 2011-2024 The Netty Project (https://netty.io/). This product contains the extensions to Java Collections Framework derived from the works by JSR-166 EG, Doug Lea, and Jason T. Greene (Public Domain). This product contains a modified version of Robert Harder's Public Domain Base64 Encoder and Decoder. This product contains a modified version of 'JZlib', a re-implementation of zlib in pure Java (BSD-style license, http://www.jcraft.com/jzlib/). This product contains a modified version of 'Webbit' (BSD License, https://github.com/joewalnes/webbit). This product optionally depends on 'Protocol Buffers' (New BSD License, http://code.google.com/p/protobuf/), 'Bouncy Castle Crypto APIs' (MIT License, http://www.bouncycastle.org/), 'SLF4J' (MIT License, http://www.slf4j.org/), 'Apache Commons Logging' (Apache License 2.0), 'Apache Log4J' (Apache License 2.0), 'JBoss Logging' (GNU LGPL 2.1), and 'Apache Felix' (Apache License 2.0). -------------------------------------------------------------------------------- Eclipse Jetty -------------------------------------------------------------------------------- Jetty Web Container Copyright 1995-2018 Mort Bay Consulting Pty Ltd. The Jetty Web Container is Copyright Mort Bay Consulting Pty Ltd unless otherwise noted. Jetty is dual licensed under both the Apache 2.0 License and the Eclipse Public 1.0 License; Texera redistributes it under the Apache 2.0 terms. Jetty bundles select artifacts under secondary licenses: * Eclipse Public License: org.eclipse.jetty.orbit:org.eclipse.jdt.core, javax.security.auth.message (EPL + ASL2), javax.mail.glassfish (EPL + CDDL 1.0) * CDDL + GPLv2 with classpath exception: javax.servlet:javax.servlet-api, javax.annotation:javax.annotation-api, javax.transaction:javax.transaction-api, javax.websocket:javax.websocket-api * OW2 license: org.ow2.asm:asm-commons, org.ow2.asm:asm * MortBay ASL2: org.mortbay.jasper:apache-jsp, apache-el (based on selected classes from Apache Tomcat) The UnixCrypt.java code implements one-way cryptography used by Unix systems for simple password protection. Copyright 1996 Aki Yoshida, modified April 2001 by Iris Van den Broeke, Daniel Deville. -------------------------------------------------------------------------------- Jackson (FasterXML) -------------------------------------------------------------------------------- Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. Copyright 2007- Tatu Saloranta (tatu.saloranta@iki.fi) Jackson 2.x core and extension components are licensed under Apache License 2.0. This attribution applies to jackson-core, jackson-databind, jackson-annotations, and every jackson-datatype-*, jackson-module-*, jackson-dataformat-*, and jackson-jaxrs-* artifact bundled in this distribution. Java ClassMate library (com.fasterxml:classmate) was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), with contributions from Brian Langel. -------------------------------------------------------------------------------- Google Guice -------------------------------------------------------------------------------- Google Guice - Core Library (and guice-servlet extension) Copyright 2006-2015 Google, Inc. -------------------------------------------------------------------------------- AWS SDK for Java 2.0 -------------------------------------------------------------------------------- AWS SDK for Java 2.0 Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. This product includes software developed by Amazon Technologies, Inc (http://www.amazon.com/). The AWS SDK bundles the following third-party works: * XML parsing and utility functions from JetS3t Copyright 2006-2009 James Murty. * PKCS#1 PEM encoded private key parsing and utility functions from oauth.googlecode.com - Copyright 1998-2010 AOL Inc. * Apache Commons Lang (https://github.com/apache/commons-lang) * Netty Reactive Streams (https://github.com/playframework/netty-reactive-streams) * Jackson-core (https://github.com/FasterXML/jackson-core), shaded as software.amazon.awssdk:third-party-jackson-core * Jackson-dataformat-cbor (https://github.com/FasterXML/jackson-dataformats-binary) Required Apache Commons Lang attribution: Apache Commons Lang Copyright 2001-2020 The Apache Software Foundation -------------------------------------------------------------------------------- Jackson core (verbatim upstream NOTICE) -------------------------------------------------------------------------------- # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. ## Copyright Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) ## Licensing Jackson 2.x core and extension components are licensed under Apache License 2.0 To find the details that apply to this artifact see the accompanying LICENSE file. ## Credits A list of contributors may be found from CREDITS(-2.x) file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. ## FastDoubleParser jackson-core bundles a shaded copy of FastDoubleParser . That code is available under an MIT license under the following copyright. Copyright © 2023 Werner Randelshofer, Switzerland. MIT License. See FastDoubleParser-NOTICE for details of other source code included in FastDoubleParser and the licenses and copyrights that apply to that code. # FastDoubleParser This is a Java port of Daniel Lemire's fast_float project. This project provides parsers for double, float, BigDecimal and BigInteger values. ## Copyright Copyright © 2024 Werner Randelshofer, Switzerland. ## Licensing This code is licensed under MIT License. https://github.com/wrandelshofer/FastDoubleParser/blob/522be16e145f43308c43b23094e31d5efcaa580e/LICENSE (The file 'LICENSE' is included in the sources and classes Jar files that are released by this project - as is required by that license.) Some portions of the code have been derived from other projects. All these projects require that we include a copyright notice, and some require that we also include some text of their license file. fast_double_parser, Copyright (c) 2022 Daniel Lemire. BSL License. https://github.com/lemire/fast_double_parser https://github.com/lemire/fast_double_parser/blob/07d9189a8fb815fe800cb15ca022e7a07093236e/LICENSE.BSL (The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project - as is required by that license.) fast_float, Copyright (c) 2021 The fast_float authors. MIT License. https://github.com/fastfloat/fast_float https://github.com/fastfloat/fast_float/blob/cc1e01e9eee74128e48d51488a6b1df4a767a810/LICENSE-MIT (The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project - as is required by that license.) bigint, Copyright 2020 Tim Buktu. 2-clause BSD License. https://github.com/tbuktu/bigint/tree/floatfft https://github.com/tbuktu/bigint/blob/617c8cd8a7c5e4fb4d919c6a4d11e2586107f029/LICENSE https://github.com/wrandelshofer/FastDoubleParser/blob/39e123b15b71f29a38a087d16a0bc620fc879aa6/bigint-LICENSE (We only use those portions of the bigint project that can be licensed under 2-clause BSD License.) (The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project - as is required by that license.) -------------------------------------------------------------------------------- Jackson modules and datatypes -------------------------------------------------------------------------------- # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. ## Copyright Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) ## Licensing Jackson 2.x core and extension components are licensed under Apache License 2.0 To find the details that apply to this artifact see the accompanying LICENSE file. ## Credits A list of contributors may be found from CREDITS(-2.x) file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers, as well as supported commercially by FasterXML.com. ## Licensing Jackson core and extension components (as well their dependencies) may be licensed under different licenses. To find the details that apply to this artifact see the accompanying LICENSE file. For more information, including possible other licensing options, contact FasterXML.com (http://fasterxml.com). ## Credits A list of contributors may be found from CREDITS file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers, as well as supported commercially by FasterXML.com. ## Licensing Jackson core and extension components may be licensed under different licenses. To find the details that apply to this artifact see the accompanying LICENSE file. For more information, including possible other licensing options, contact FasterXML.com (http://fasterxml.com). ## Credits A list of contributors may be found from CREDITS file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. ## Copyright Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) ## Licensing Jackson components are licensed under Apache (Software) License, version 2.0, as per accompanying LICENSE file. ## Credits A list of contributors may be found from CREDITS file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers, as well as supported commercially by FasterXML.com. ## Licensing Jackson core and extension components may licensed under different licenses. To find the details that apply to this artifact see the accompanying LICENSE file. For more information, including possible other licensing options, contact FasterXML.com (http://fasterxml.com). ## Credits A list of contributors may be found from CREDITS file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. ## Licensing Jackson components are licensed under Apache (Software) License, version 2.0, as per accompanying LICENSE file. ## Credits A list of contributors may be found from CREDITS file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. -------------------------------------------------------------------------------- Eclipse Jetty 11.0 -------------------------------------------------------------------------------- Notices for Eclipse Jetty ========================= This content is produced and maintained by the Eclipse Jetty project. Project home: https://eclipse.dev/jetty/ Trademarks ---------- Eclipse Jetty, and Jetty are trademarks of the Eclipse Foundation. Copyright --------- All contributions are the property of the respective authors or of entities to which copyright has been assigned by the authors (eg. employer). Declared Project Licenses ------------------------- This artifacts of this project are made available under the terms of: * the Eclipse Public License v2.0 https://www.eclipse.org/legal/epl-2.0 SPDX-License-Identifier: EPL-2.0 or * the Apache License, Version 2.0 https://www.apache.org/licenses/LICENSE-2.0 SPDX-License-Identifier: Apache-2.0 The following dependencies are EPL. * org.eclipse.jetty.orbit:org.eclipse.jdt.core The following dependencies are EPL and ASL2. * org.eclipse.jetty.orbit:javax.security.auth.message The following dependencies are EPL and CDDL 1.0. * org.eclipse.jetty.orbit:javax.mail.glassfish The following dependencies are CDDL + GPLv2 with classpath exception. https://glassfish.dev.java.net/nonav/public/CDDL+GPL.html * jakarta.servlet:jakarta.servlet-api * javax.annotation:javax.annotation-api * javax.transaction:javax.transaction-api * javax.websocket:javax.websocket-api The following dependencies are licensed by the OW2 Foundation according to the terms of http://asm.ow2.org/license.html * org.ow2.asm:asm-commons * org.ow2.asm:asm The following dependencies are ASL2 licensed. * org.apache.taglibs:taglibs-standard-spec * org.apache.taglibs:taglibs-standard-impl The following dependencies are ASL2 licensed. Based on selected classes from following Apache Tomcat jars, all ASL2 licensed. * org.mortbay.jasper:apache-jsp * org.apache.tomcat:tomcat-jasper * org.apache.tomcat:tomcat-juli * org.apache.tomcat:tomcat-jsp-api * org.apache.tomcat:tomcat-el-api * org.apache.tomcat:tomcat-jasper-el * org.apache.tomcat:tomcat-api * org.apache.tomcat:tomcat-util-scan * org.apache.tomcat:tomcat-util * org.mortbay.jasper:apache-el * org.apache.tomcat:tomcat-jasper-el * org.apache.tomcat:tomcat-el-api The following artifacts are CDDL + GPLv2 with classpath exception. https://glassfish.dev.java.net/nonav/public/CDDL+GPL.html * org.eclipse.jetty.toolchain:jetty-schemas Cryptography ------------ Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. The UnixCrypt.java code implements the one way cryptography used by Unix systems for simple password protection. Copyright 1996 Aki Yoshida, modified April 2001 by Iris Van den Broeke, Daniel Deville. Permission to use, copy, modify and distribute UnixCrypt for non-commercial or commercial purposes and without fee is granted provided that the copyright notice appears in all copies. -------------------------------------------------------------------------------- Apache Parquet (per-component supplementary notices) -------------------------------------------------------------------------------- Apache Parquet MR (Incubating) Copyright 2014-2015 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). -------------------------------------------------------------------------------- This product includes code from Apache Avro, which includes the following in its NOTICE file: Apache Avro Copyright 2010-2015 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. ## Licensing Jackson 2.x core and extension components are licensed under Apache License 2.0 To find the details that apply to this artifact see the accompanying LICENSE file. ## Credits A list of contributors may be found from CREDITS(-2.x) file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. -------------------------------------------------------------------------------- R2DBC SPI -------------------------------------------------------------------------------- Reactive Relational Database Connectivity Copyright 2017-2021 the original author or authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- Eclipse Jersey (jersey-container-servlet, jersey-container-servlet-core, jersey-client, jersey-hk2, jersey-media-jaxb) -------------------------------------------------------------------------------- # Notice for Jersey This content is produced and maintained by the Eclipse Jersey project. * Project home: https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Angular JS, v1.6.6 * License MIT (http://www.opensource.org/licenses/mit-license.php) * Project: http://angularjs.org * Coyright: (c) 2010-2017 Google, Inc. aopalliance Version 1 * License: all the source code provided by AOP Alliance is Public Domain. * Project: http://aopalliance.sourceforge.net * Copyright: Material in the public domain is not protected by copyright Bean Validation API 3.0.2 * License: Apache License, 2.0 * Project: http://beanvalidation.org/1.1/ * Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors * by the @authors tag. Hibernate Validator CDI, 7.0.5.Final * License: Apache License, 2.0 * Project: https://beanvalidation.org/ * Repackaged in org.glassfish.jersey.server.validation.internal.hibernate Bootstrap v3.3.7 * License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE) * Project: http://getbootstrap.com * Copyright: 2011-2016 Twitter, Inc Google Guava Version 18.0 * License: Apache License, 2.0 * Copyright (C) 2009 The Guava Authors jakarta.inject Version: 1 * License: Apache License, 2.0 * Copyright (C) 2009 The JSR-330 Expert Group Javassist Version 3.29.2-GA * License: Apache License, 2.0 * Project: http://www.javassist.org/ * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. Jackson JAX-RS Providers Version 2.15.3 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers * Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. jQuery v1.12.4 * License: jquery.org/license * Project: jquery.org * Copyright: (c) jQuery Foundation jQuery Barcode plugin 0.3 * License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html) * Project: http://www.pasella.it/projects/jQuery/barcode * Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com JSR-166 Extension - JEP 266 * License: CC0 * No copyright * Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/ KineticJS, v4.7.1 * License: MIT license (http://www.opensource.org/licenses/mit-license.php) * Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS * Copyright: Eric Rowell org.objectweb.asm Version 9.6 * License: Modified BSD (https://asm.ow2.io/license.html) * Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved. org.osgi.core version 6.0.0 * License: Apache License, 2.0 * Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved. org.glassfish.jersey.server.internal.monitoring.core * License: Apache License, 2.0 * Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. * Copyright 2010-2013 Coda Hale and Yammer, Inc. W3.org documents * License: W3C License * Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ -------------------------------------------------------------------------------- Eclipse Jersey Core Server -------------------------------------------------------------------------------- # Notice for Jersey Core Server module This content is produced and maintained by the Eclipse Jersey project. * https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content org.glassfish.jersey.server.internal.monitoring.core * License: Apache License, 2.0 * Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. * Copyright 2010-2013 Coda Hale and Yammer, Inc. org.objectweb.asm Version 9.6 * License: Modified BSD (https://asm.ow2.io/license.html) * Copyright: (c) 2000-2011 INRIA, France Telecom. All rights reserved. W3.org documents * License: W3C License * Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ # Notice for Jersey This content is produced and maintained by the Eclipse Jersey project. * Project home: https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Angular JS, v1.6.6 * License MIT (http://www.opensource.org/licenses/mit-license.php) * Project: http://angularjs.org * Coyright: (c) 2010-2017 Google, Inc. aopalliance Version 1 * License: all the source code provided by AOP Alliance is Public Domain. * Project: http://aopalliance.sourceforge.net * Copyright: Material in the public domain is not protected by copyright Bean Validation API 3.0.2 * License: Apache License, 2.0 * Project: http://beanvalidation.org/1.1/ * Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors * by the @authors tag. Hibernate Validator CDI, 7.0.5.Final * License: Apache License, 2.0 * Project: https://beanvalidation.org/ * Repackaged in org.glassfish.jersey.server.validation.internal.hibernate Bootstrap v3.3.7 * License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE) * Project: http://getbootstrap.com * Copyright: 2011-2016 Twitter, Inc Google Guava Version 18.0 * License: Apache License, 2.0 * Copyright (C) 2009 The Guava Authors jakarta.inject Version: 1 * License: Apache License, 2.0 * Copyright (C) 2009 The JSR-330 Expert Group Javassist Version 3.29.2-GA * License: Apache License, 2.0 * Project: http://www.javassist.org/ * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. Jackson JAX-RS Providers Version 2.15.3 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers * Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. jQuery v1.12.4 * License: jquery.org/license * Project: jquery.org * Copyright: (c) jQuery Foundation jQuery Barcode plugin 0.3 * License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html) * Project: http://www.pasella.it/projects/jQuery/barcode * Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com JSR-166 Extension - JEP 266 * License: CC0 * No copyright * Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/ KineticJS, v4.7.1 * License: MIT license (http://www.opensource.org/licenses/mit-license.php) * Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS * Copyright: Eric Rowell org.objectweb.asm Version 9.6 * License: Modified BSD (https://asm.ow2.io/license.html) * Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved. org.osgi.core version 6.0.0 * License: Apache License, 2.0 * Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved. org.glassfish.jersey.server.internal.monitoring.core * License: Apache License, 2.0 * Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. * Copyright 2010-2013 Coda Hale and Yammer, Inc. W3.org documents * License: W3C License * Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ -------------------------------------------------------------------------------- Eclipse Jersey Core Common -------------------------------------------------------------------------------- # Notice for Jersey Core Common module This content is produced and maintained by the Eclipse Jersey project. * https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Google Guava Version 18.0 * License: Apache License, 2.0 * Copyright: (C) 2009 The Guava Authors JSR-166 Extension - JEP 266 * License: Creative Commons 1.0 (CC0) * No copyright * Written by Doug Lea with assistance from members of JCP JSR-166 * Expert Group and released to the public domain, as explained at * http://creativecommons.org/publicdomain/zero/1.0/ # Notice for Jersey This content is produced and maintained by the Eclipse Jersey project. * Project home: https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Angular JS, v1.6.6 * License MIT (http://www.opensource.org/licenses/mit-license.php) * Project: http://angularjs.org * Coyright: (c) 2010-2017 Google, Inc. aopalliance Version 1 * License: all the source code provided by AOP Alliance is Public Domain. * Project: http://aopalliance.sourceforge.net * Copyright: Material in the public domain is not protected by copyright Bean Validation API 3.0.2 * License: Apache License, 2.0 * Project: http://beanvalidation.org/1.1/ * Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors * by the @authors tag. Hibernate Validator CDI, 7.0.5.Final * License: Apache License, 2.0 * Project: https://beanvalidation.org/ * Repackaged in org.glassfish.jersey.server.validation.internal.hibernate Bootstrap v3.3.7 * License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE) * Project: http://getbootstrap.com * Copyright: 2011-2016 Twitter, Inc Google Guava Version 18.0 * License: Apache License, 2.0 * Copyright (C) 2009 The Guava Authors jakarta.inject Version: 1 * License: Apache License, 2.0 * Copyright (C) 2009 The JSR-330 Expert Group Javassist Version 3.29.2-GA * License: Apache License, 2.0 * Project: http://www.javassist.org/ * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. Jackson JAX-RS Providers Version 2.15.3 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers * Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. jQuery v1.12.4 * License: jquery.org/license * Project: jquery.org * Copyright: (c) jQuery Foundation jQuery Barcode plugin 0.3 * License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html) * Project: http://www.pasella.it/projects/jQuery/barcode * Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com JSR-166 Extension - JEP 266 * License: CC0 * No copyright * Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/ KineticJS, v4.7.1 * License: MIT license (http://www.opensource.org/licenses/mit-license.php) * Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS * Copyright: Eric Rowell org.objectweb.asm Version 9.6 * License: Modified BSD (https://asm.ow2.io/license.html) * Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved. org.osgi.core version 6.0.0 * License: Apache License, 2.0 * Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved. org.glassfish.jersey.server.internal.monitoring.core * License: Apache License, 2.0 * Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. * Copyright 2010-2013 Coda Hale and Yammer, Inc. W3.org documents * License: W3C License * Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ -------------------------------------------------------------------------------- Eclipse Jersey Bean Validation -------------------------------------------------------------------------------- # Notice for Jersey Bean Validation module This content is produced and maintained by the Eclipse Jersey project. * https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Hibernate Validator CDI, 7.0.5.Final * License: Apache License, 2.0 * Project: https://beanvalidation.org/ * Repackaged in org.glassfish.jersey.server.validation.internal.hibernate # Notice for Jersey This content is produced and maintained by the Eclipse Jersey project. * Project home: https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Angular JS, v1.6.6 * License MIT (http://www.opensource.org/licenses/mit-license.php) * Project: http://angularjs.org * Coyright: (c) 2010-2017 Google, Inc. aopalliance Version 1 * License: all the source code provided by AOP Alliance is Public Domain. * Project: http://aopalliance.sourceforge.net * Copyright: Material in the public domain is not protected by copyright Bean Validation API 3.0.2 * License: Apache License, 2.0 * Project: http://beanvalidation.org/1.1/ * Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors * by the @authors tag. Hibernate Validator CDI, 7.0.5.Final * License: Apache License, 2.0 * Project: https://beanvalidation.org/ * Repackaged in org.glassfish.jersey.server.validation.internal.hibernate Bootstrap v3.3.7 * License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE) * Project: http://getbootstrap.com * Copyright: 2011-2016 Twitter, Inc Google Guava Version 18.0 * License: Apache License, 2.0 * Copyright (C) 2009 The Guava Authors jakarta.inject Version: 1 * License: Apache License, 2.0 * Copyright (C) 2009 The JSR-330 Expert Group Javassist Version 3.29.2-GA * License: Apache License, 2.0 * Project: http://www.javassist.org/ * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. Jackson JAX-RS Providers Version 2.15.3 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers * Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. jQuery v1.12.4 * License: jquery.org/license * Project: jquery.org * Copyright: (c) jQuery Foundation jQuery Barcode plugin 0.3 * License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html) * Project: http://www.pasella.it/projects/jQuery/barcode * Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com JSR-166 Extension - JEP 266 * License: CC0 * No copyright * Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/ KineticJS, v4.7.1 * License: MIT license (http://www.opensource.org/licenses/mit-license.php) * Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS * Copyright: Eric Rowell org.objectweb.asm Version 9.6 * License: Modified BSD (https://asm.ow2.io/license.html) * Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved. org.osgi.core version 6.0.0 * License: Apache License, 2.0 * Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved. org.glassfish.jersey.server.internal.monitoring.core * License: Apache License, 2.0 * Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. * Copyright 2010-2013 Coda Hale and Yammer, Inc. W3.org documents * License: W3C License * Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ -------------------------------------------------------------------------------- Eclipse GlassFish HK2 (aopalliance-repackaged, hk2-api, hk2-locator, hk2-utils) -------------------------------------------------------------------------------- # Notices for Eclipse GlassFish This content is produced and maintained by the Eclipse GlassFish project. * Project home: https://projects.eclipse.org/projects/ee4j.glassfish ## Trademarks Eclipse GlassFish, and GlassFish are trademarks of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/glassfish-ha-api * https://github.com/eclipse-ee4j/glassfish-logging-annotation-processor * https://github.com/eclipse-ee4j/glassfish-shoal * https://github.com/eclipse-ee4j/glassfish-cdi-porting-tck * https://github.com/eclipse-ee4j/glassfish-jsftemplating * https://github.com/eclipse-ee4j/glassfish-hk2-extra * https://github.com/eclipse-ee4j/glassfish-hk2 * https://github.com/eclipse-ee4j/glassfish-fighterfish ## Third-party Content This project leverages the following third party content. None ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. -------------------------------------------------------------------------------- Eclipse Jetty Servlet API (jakarta-servlet-api 5.0.2) -------------------------------------------------------------------------------- # Notices for Eclipse Project for Servlet This content is produced and maintained by the Eclipse Project for Servlet project. * Project home: https://projects.eclipse.org/projects/ee4j.servlet ## Trademarks Eclipse Project for Servlet is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/servlet-api * https://github.com/eclipse/jetty.toolchain ## Third-party Content ## Jakarta The following artifacts are EPL 2.0 + GPLv2 with classpath exception. https://projects.eclipse.org/projects/ee4j.servlet * jakarta.servlet:jakarta.servlet-api ## GlassFish The following artifacts are CDDL + GPLv2 with classpath exception. https://glassfish.dev.java.net/nonav/public/CDDL+GPL.html * org.eclipse.jetty.toolchain:jetty-schemas -------------------------------------------------------------------------------- Jakarta XML Binding API (jakarta.xml.bind-api 3.0.x) -------------------------------------------------------------------------------- [//]: # " Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. " [//]: # " " [//]: # " This program and the accompanying materials are made available under the " [//]: # " terms of the Eclipse Distribution License v. 1.0, which is available at " [//]: # " http://www.eclipse.org/org/documents/edl-v10.php. " [//]: # " " [//]: # " SPDX-License-Identifier: BSD-3-Clause " # Notices for Jakarta XML Binding This content is produced and maintained by the Jakarta XML Binding project. * Project home: https://projects.eclipse.org/projects/ee4j.jaxb ## Trademarks Jakarta XML Binding is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Distribution License v. 1.0 which is available at http://www.eclipse.org/org/documents/edl-v10.php. SPDX-License-Identifier: BSD-3-Clause ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jaxb-api * https://github.com/eclipse-ee4j/jaxb-tck ## Third-party Content This project leverages the following third party content. Apache River (3.0.0) * License: Apache-2.0 AND BSD-3-Clause ASM 7 (n/a) * License: BSD-3-Clause * Project: https://asm.ow2.io/ * Source: https://repository.ow2.org/nexus/#nexus-search;gav~org.ow2.asm~asm-commons~~~~kw,versionexpand JTHarness (5.0) * License: (GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0) * Project: https://wiki.openjdk.java.net/display/CodeTools/JT+Harness * Source: http://hg.openjdk.java.net/code-tools/jtharness/ normalize.css (3.0.2) * License: MIT SigTest (n/a) * License: GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. -------------------------------------------------------------------------------- Jakarta RESTful Web Services API (jakarta.ws.rs-api 3.0.x / 3.1.0) -------------------------------------------------------------------------------- # Notices for Jakarta RESTful Web Services This content is produced and maintained by the **Jakarta RESTful Web Services** project. * Project home: https://projects.eclipse.org/projects/ee4j.jaxrs ## Trademarks **Jakarta RESTful Web Services** is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jaxrs-api ## Third-party Content This project leverages the following third party content. javaee-api (7.0) * License: Apache-2.0 AND W3C JUnit (4.11) * License: Common Public License 1.0 Mockito (2.16.0) * Project: http://site.mockito.org * Source: https://github.com/mockito/mockito/releases/tag/v2.16.0 ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. -------------------------------------------------------------------------------- Jakarta Expression Language API (jakarta.el-api 4.0.0, glassfish jakarta.el 4.0.2) -------------------------------------------------------------------------------- # Notices for Jakarta Expression Language This content is produced and maintained by the Jakarta Expression Language project. * Project home: https://projects.eclipse.org/projects/ee4j.el ## Trademarks Jakarta Expression Language is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/el-ri ## Third-party Content ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. -------------------------------------------------------------------------------- Jakarta Annotations API (jakarta.annotation-api 2.1.1 and 3.0.0) -------------------------------------------------------------------------------- # Notices for Jakarta Annotations This content is produced and maintained by the Jakarta Annotations project. * Project home: https://projects.eclipse.org/projects/ee4j.ca ## Trademarks Jakarta Annotations is a trademark of the Eclipse Foundation. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/common-annotations-api ## Third-party Content ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Notices for Jakarta Annotations This content is produced and maintained by the Jakarta Annotations project. * Project home: https://projects.eclipse.org/projects/ee4j.ca ## Trademarks Jakarta Annotations™ is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at https://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GPL-2.0 with Classpath-exception-2.0 which is available at https://openjdk.java.net/legal/gplv2+ce.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/jakartaee/common-annotations-api ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. -------------------------------------------------------------------------------- Jakarta Inject API (jakarta.inject-api 2.0.1) -------------------------------------------------------------------------------- # Notices for Eclipse Jakarta Dependency Injection This content is produced and maintained by the Eclipse Jakarta Dependency Injection project. * Project home: https://projects.eclipse.org/projects/cdi.batch ## Trademarks Jakarta Dependency Injection is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Apache License, Version 2.0 which is available at https://www.apache.org/licenses/LICENSE-2.0. SPDX-License-Identifier: Apache-2.0 ## Source Code The project maintains the following source code repositories: https://github.com/eclipse-ee4j/injection-api https://github.com/eclipse-ee4j/injection-spec https://github.com/eclipse-ee4j/injection-tck ## Third-party Content This project leverages the following third party content. None ## Cryptography None -------------------------------------------------------------------------------- Jakarta Activation (jakarta.activation 2.0.0, 2.0.1, jakarta.activation-api 1.2.1, 2.1.0) -------------------------------------------------------------------------------- # Notices for Eclipse Project for JAF This content is produced and maintained by the Eclipse Project for JAF project. * Project home: https://projects.eclipse.org/projects/ee4j.jaf ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Distribution License v. 1.0, which is available at http://www.eclipse.org/org/documents/edl-v10.php. SPDX-License-Identifier: BSD-3-Clause ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jaf ## Third-party Content This project leverages the following third party content. JUnit (4.12) * License: Eclipse Public License - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Notices for Jakarta Activation This content is produced and maintained by Jakarta Activation project. * Project home: https://projects.eclipse.org/projects/ee4j.jaf ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Distribution License v. 1.0, which is available at http://www.eclipse.org/org/documents/edl-v10.php. SPDX-License-Identifier: BSD-3-Clause ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jaf ## Third-party Content This project leverages the following third party content. JUnit (4.12) * License: Eclipse Public License - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Notices for Jakarta Activation This content is produced and maintained by Jakarta Activation project. * Project home: https://projects.eclipse.org/projects/ee4j.jaf ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Distribution License v. 1.0, which is available at http://www.eclipse.org/org/documents/edl-v10.php. SPDX-License-Identifier: BSD-3-Clause ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jaf -------------------------------------------------------------------------------- Bouncy Castle -------------------------------------------------------------------------------- Bouncy Castle Cryptography (org.bouncycastle.*) is distributed under the Bouncy Castle License, whose text reproduces the MIT License with Bouncy Castle's copyright line: Copyright (c) 2000-2024 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org) The full MIT-equivalent text is reproduced in licenses/LICENSE-MIT.txt; the Bouncy Castle copyright line above must be preserved alongside it. ================================================ FILE: computing-unit-managing-service/build.sbt ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. import scala.collection.Seq name := "computing-unit-managing-service" enablePlugins(JavaAppPackaging) // Ship LICENSE-binary, NOTICE-binary, DISCLAIMER, and the licenses/ // directory at the top of the Universal dist zip. // See project/AddMetaInfLicenseFiles.scala. Universal / mappings := AddMetaInfLicenseFiles.distMappings( (Universal / mappings).value, (ThisBuild / baseDirectory).value, baseDirectory.value / "LICENSE-binary", baseDirectory.value / "NOTICE-binary" ) // Dependency Versions val dropwizardVersion = "4.0.7" // Dependencies libraryDependencies ++= Seq( "io.dropwizard" % "dropwizard-core" % dropwizardVersion, "io.dropwizard" % "dropwizard-auth" % dropwizardVersion, // Dropwizard Authentication module "io.kubernetes" % "client-java" % "21.0.0", "org.jooq" % "jooq" % "3.14.16", "com.typesafe" % "config" % "1.4.6", "com.softwaremill.sttp.client4" %% "core" % "4.0.0-M6", "com.typesafe.play" %% "play-json" % "2.10.6", "io.fabric8" % "kubernetes-client" % "6.12.1" ) // Compiler Options Compile / scalacOptions ++= Seq( "-Xelide-below", "WARNING", "-feature", "-deprecation", "-Ywarn-unused:imports" ) ================================================ FILE: computing-unit-managing-service/project/build.properties ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. sbt.version = 1.12.9 ================================================ FILE: computing-unit-managing-service/project/plugins.sbt ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.16") ================================================ FILE: computing-unit-managing-service/src/main/resources/computing-unit-managing-service-config.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. server: applicationConnectors: - type: http port: 8888 adminConnectors: - type: http port: 8082 requestLog: type: classic appenders: [] logging: level: ${TEXERA_SERVICE_LOG_LEVEL:-INFO} loggers: "com.example": ${TEXERA_SERVICE_LOG_LEVEL:-DEBUG} ================================================ FILE: computing-unit-managing-service/src/main/scala/org/apache/texera/service/ComputingUnitManagingService.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service import com.fasterxml.jackson.module.scala.DefaultScalaModule import io.dropwizard.auth.AuthDynamicFeature import io.dropwizard.configuration.{EnvironmentVariableSubstitutor, SubstitutingSourceProvider} import io.dropwizard.core.Application import io.dropwizard.core.setup.{Bootstrap, Environment} import org.apache.texera.amber.config.StorageConfig import org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser} import org.apache.texera.dao.SqlServer import org.apache.texera.service.resource.{ ComputingUnitAccessResource, ComputingUnitManagingResource, HealthCheckResource } import java.nio.file.Path class ComputingUnitManagingService extends Application[ComputingUnitManagingServiceConfiguration] { override def initialize( bootstrap: Bootstrap[ComputingUnitManagingServiceConfiguration] ): Unit = { // enable environment variable substitution in YAML config bootstrap.setConfigurationSourceProvider( new SubstitutingSourceProvider( bootstrap.getConfigurationSourceProvider, new EnvironmentVariableSubstitutor(false) ) ) // register scala module to dropwizard default object mapper bootstrap.getObjectMapper.registerModule(DefaultScalaModule) } override def run( configuration: ComputingUnitManagingServiceConfiguration, environment: Environment ): Unit = { SqlServer.initConnection( StorageConfig.jdbcUrl, StorageConfig.jdbcUsername, StorageConfig.jdbcPassword ) // Register http resources environment.jersey.setUrlPattern("/api/*") environment.jersey.register(classOf[HealthCheckResource]) // Register JWT authentication filter environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter])) // Enable @Auth annotation for injecting SessionUser environment.jersey.register( new io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser]) ) environment.jersey().register(new ComputingUnitManagingResource) environment.jersey().register(new ComputingUnitAccessResource) // Route request logs through SLF4J, controlled by TEXERA_SERVICE_LOG_LEVEL RequestLoggingFilter.register(environment.getApplicationContext) } } object ComputingUnitManagingService { def main(args: Array[String]): Unit = { val configFilePath = Path .of(sys.env.getOrElse("TEXERA_HOME", ".")) .resolve("computing-unit-managing-service") .resolve("src") .resolve("main") .resolve("resources") .resolve("computing-unit-managing-service-config.yaml") .toAbsolutePath .toString new ComputingUnitManagingService().run("server", configFilePath) } } ================================================ FILE: computing-unit-managing-service/src/main/scala/org/apache/texera/service/ComputingUnitManagingServiceConfiguration.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service import io.dropwizard.core.Configuration class ComputingUnitManagingServiceConfiguration extends Configuration {} ================================================ FILE: computing-unit-managing-service/src/main/scala/org/apache/texera/service/resource/ComputingUnitAccessResource.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.service.resource import io.dropwizard.auth.Auth import jakarta.annotation.security.RolesAllowed import jakarta.ws.rs.core.MediaType import jakarta.ws.rs._ import org.apache.texera.auth.SessionUser import org.apache.texera.config.ComputingUnitConfig import org.apache.texera.dao.SqlServer import org.apache.texera.dao.SqlServer.withTransaction import org.apache.texera.dao.jooq.generated.Tables.COMPUTING_UNIT_USER_ACCESS import org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum import org.apache.texera.dao.jooq.generated.tables.daos.{ ComputingUnitUserAccessDao, UserDao, WorkflowComputingUnitDao } import org.apache.texera.dao.jooq.generated.tables.pojos.ComputingUnitUserAccess import org.apache.texera.service.resource.ComputingUnitAccessResource._ import org.jooq.{DSLContext, EnumType} import scala.jdk.CollectionConverters._ object ComputingUnitAccessResource { private def context: DSLContext = SqlServer .getInstance() .createDSLContext() /** * Identifies whether the given user has read-only access over the given computing unit * * @param cuid computing unit id * @param uid user id * @return boolean value indicating yes/no */ def hasReadAccess(cuid: Integer, uid: Integer): Boolean = { isOwner(cuid, uid) || getPrivilege(cuid, uid).eq(PrivilegeEnum.READ) || hasWriteAccess( cuid, uid ) } /** * Identifies whether the given user has write access over the given computing unit * * @param cuid computing unit id * @param uid user id * @return boolean value indicating yes/no */ def hasWriteAccess(cuid: Integer, uid: Integer): Boolean = { isOwner(cuid, uid) || getPrivilege(cuid, uid).eq(PrivilegeEnum.WRITE) } /** * Identifies whether the given user is the owner of the given computing unit * * @param cuid computing unit id * @param uid user id * @return boolean value indicating yes/no */ def isOwner(cuid: Integer, uid: Integer): Boolean = { val workflowComputingUnitDao = new WorkflowComputingUnitDao(context.configuration()) val unit = workflowComputingUnitDao.fetchOneByCuid(cuid) unit != null && unit.getUid.equals(uid) } def getPrivilege(cuid: Integer, uid: Integer): PrivilegeEnum = { val computingUnitUserAccessDao = new ComputingUnitUserAccessDao(context.configuration()) val accessList = computingUnitUserAccessDao .fetchByUid(uid) .asScala .find(_.getCuid.equals(cuid)) accessList match { case Some(access) => access.getPrivilege case None => null } } case class AccessEntry(email: String, name: String, privilege: EnumType) {} } @Produces(Array(MediaType.APPLICATION_JSON)) @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/access") class ComputingUnitAccessResource { private def ensureSharingIsEnabled(): Unit = { if (!ComputingUnitConfig.sharingComputingUnitEnabled) { throw new ForbiddenException( "The computing unit sharing feature is disabled by the administrator." ) } } final private val userDao = new UserDao(context.configuration()) @GET @Produces(Array(MediaType.APPLICATION_JSON)) @Path("/computing-unit/list/{cuid}") def getComputingUnitAccessList( @Auth user: SessionUser, @PathParam("cuid") cuid: Integer ): List[AccessEntry] = { ensureSharingIsEnabled() withTransaction(context) { ctx => val computingUnitUserAccessDao = new ComputingUnitUserAccessDao(ctx.configuration()) computingUnitUserAccessDao .fetchByCuid(cuid) .asScala .map(access => { val user = userDao.fetchOneByUid(access.getUid) AccessEntry( email = user.getEmail, name = user.getName, privilege = access.getPrivilege ) }) .toList } } @PUT @Path("/computing-unit/grant/{cuid}/{email}/{privilege}") def grantAccess( @Auth user: SessionUser, @PathParam("cuid") cuid: Integer, @PathParam("email") email: String, @PathParam("privilege") privilege: PrivilegeEnum ): Unit = { ensureSharingIsEnabled() if (!hasWriteAccess(cuid, user.getUid)) { throw new IllegalArgumentException("User does not have permission to grant access") } // TODO: add try except and check how to display error message in the frontend val granteeId = userDao.fetchOneByEmail(email).getUid if (granteeId == null) { throw new IllegalArgumentException("User with the given email does not exist") } withTransaction(context) { ctx => val computingUnitUserAccessDao = new ComputingUnitUserAccessDao(ctx.configuration()) val access = new ComputingUnitUserAccess access.setCuid(cuid) access.setUid(granteeId) access.setPrivilege(privilege) computingUnitUserAccessDao.insert(access) } } @DELETE @Path("/computing-unit/revoke/{cuid}/{email}") def revokeAccess( @Auth user: SessionUser, @PathParam("cuid") cuid: Integer, @PathParam("email") email: String ): Unit = { ensureSharingIsEnabled() if (!hasWriteAccess(cuid, user.getUid)) { throw new IllegalArgumentException("User does not have permission to revoke access") } val granteeId = userDao.fetchOneByEmail(email).getUid if (granteeId == null) { throw new IllegalArgumentException("User with the given email does not exist") } withTransaction(context) { ctx => ctx .delete(COMPUTING_UNIT_USER_ACCESS) .where(COMPUTING_UNIT_USER_ACCESS.CUID.eq(cuid)) .and(COMPUTING_UNIT_USER_ACCESS.UID.eq(granteeId)) .execute() } } @GET @Path("/computing-unit/owner/{cuid}") def getOwner( @Auth user: SessionUser, @PathParam("cuid") cuid: Integer ): String = { ensureSharingIsEnabled() withTransaction(context) { ctx => val workflowComputingUnitDao = new WorkflowComputingUnitDao(ctx.configuration()) val unit = workflowComputingUnitDao.fetchOneByCuid(cuid) if (unit == null) { throw new IllegalArgumentException("Computing unit does not exist") } val uid = unit.getUid val owner = userDao.fetchOneByUid(uid) owner.getEmail } } } ================================================ FILE: computing-unit-managing-service/src/main/scala/org/apache/texera/service/resource/ComputingUnitManagingResource.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.resource import io.dropwizard.auth.Auth import io.fabric8.kubernetes.api.model.Quantity import io.fabric8.kubernetes.client.KubernetesClientException import jakarta.annotation.security.RolesAllowed import jakarta.ws.rs._ import jakarta.ws.rs.core.{MediaType, Response} import org.apache.texera.amber.config.{EnvironmentalVariable, StorageConfig} import org.apache.commons.lang3.StringUtils import org.apache.texera.auth.JwtAuth.{TOKEN_EXPIRE_TIME_IN_MINUTES, jwtClaims} import org.apache.texera.auth.{JwtAuth, SessionUser} import org.apache.texera.config.KubernetesConfig.{ cpuLimitOptions, gpuLimitOptions, maxNumOfRunningComputingUnitsPerUser, memoryLimitOptions } import org.apache.texera.config.{ComputingUnitConfig, KubernetesConfig} import org.apache.texera.dao.SqlServer import org.apache.texera.dao.SqlServer.withTransaction import org.apache.texera.dao.jooq.generated.enums.{PrivilegeEnum, WorkflowComputingUnitTypeEnum} import org.apache.texera.dao.jooq.generated.tables.daos.{ ComputingUnitUserAccessDao, UserDao, WorkflowComputingUnitDao } import org.apache.texera.dao.jooq.generated.tables.pojos.WorkflowComputingUnit import org.apache.texera.service.resource.ComputingUnitManagingResource._ import org.apache.texera.service.resource.ComputingUnitState._ import org.apache.texera.service.util.{ ComputingUnitManagingServiceException, InsufficientComputingUnitQuota, KubernetesClient } import org.jooq.{DSLContext, EnumType} import play.api.libs.json._ import java.sql.Timestamp import scala.annotation.unused import scala.jdk.CollectionConverters.CollectionHasAsScala object ComputingUnitManagingResource { private def context: DSLContext = SqlServer .getInstance() .createDSLContext() private def icebergEnvironmentVariables: Map[String, Any] = { val base = Map[String, Any]( EnvironmentalVariable.ENV_ICEBERG_CATALOG_TYPE -> StorageConfig.icebergCatalogType ) StorageConfig.icebergCatalogType match { case "rest" => base ++ Map( EnvironmentalVariable.ENV_ICEBERG_CATALOG_REST_URI -> StorageConfig.icebergRESTCatalogUri, EnvironmentalVariable.ENV_ICEBERG_CATALOG_REST_WAREHOUSE_NAME -> StorageConfig.icebergRESTCatalogWarehouseName ) case "postgres" => base ++ Map( EnvironmentalVariable.ENV_ICEBERG_CATALOG_POSTGRES_URI_WITHOUT_SCHEME -> StorageConfig.icebergPostgresCatalogUriWithoutScheme, EnvironmentalVariable.ENV_ICEBERG_CATALOG_POSTGRES_USERNAME -> StorageConfig.icebergPostgresCatalogUsername, EnvironmentalVariable.ENV_ICEBERG_CATALOG_POSTGRES_PASSWORD -> StorageConfig.icebergPostgresCatalogPassword ) case _ => base } } // Environment variables passed to the created computing unit(pod) private lazy val computingUnitEnvironmentVariables: Map[String, Any] = icebergEnvironmentVariables ++ Map( // Variables for saving the metadata of the results, i.e. URIs of results/stats EnvironmentalVariable.ENV_JDBC_URL -> StorageConfig.jdbcUrl, EnvironmentalVariable.ENV_JDBC_USERNAME -> StorageConfig.jdbcUsername, EnvironmentalVariable.ENV_JDBC_PASSWORD -> StorageConfig.jdbcPassword, // Variables for reading files & exporting results // LakeFS endpoint is passed to CU to make CU work in dev mode(using localhost & using default LakeFS credentials) // LakeFS credentials should NOT be passed to CU EnvironmentalVariable.ENV_LAKEFS_ENDPOINT -> StorageConfig.lakefsEndpoint, // S3 variables are passed to CU for R UDF large binary support EnvironmentalVariable.ENV_S3_ENDPOINT -> StorageConfig.s3Endpoint, EnvironmentalVariable.ENV_S3_REGION -> StorageConfig.s3Region, EnvironmentalVariable.ENV_S3_AUTH_USERNAME -> StorageConfig.s3Username, EnvironmentalVariable.ENV_S3_AUTH_PASSWORD -> StorageConfig.s3Password, EnvironmentalVariable.ENV_FILE_SERVICE_GET_PRESIGNED_URL_ENDPOINT -> EnvironmentalVariable .get(EnvironmentalVariable.ENV_FILE_SERVICE_GET_PRESIGNED_URL_ENDPOINT) .get, EnvironmentalVariable.ENV_FILE_SERVICE_UPLOAD_ONE_FILE_TO_DATASET_ENDPOINT -> EnvironmentalVariable .get(EnvironmentalVariable.ENV_FILE_SERVICE_UPLOAD_ONE_FILE_TO_DATASET_ENDPOINT) .get, // Variables for amber setting // TODO: use AmberConfig for the following items. Currently AmberConfig is only accessible in workflow-executing-service EnvironmentalVariable.ENV_SCHEDULE_GENERATOR_ENABLE_COST_BASED_SCHEDULE_GENERATOR -> EnvironmentalVariable .get(EnvironmentalVariable.ENV_SCHEDULE_GENERATOR_ENABLE_COST_BASED_SCHEDULE_GENERATOR) .get, EnvironmentalVariable.ENV_USER_SYS_ENABLED -> EnvironmentalVariable .get(EnvironmentalVariable.ENV_USER_SYS_ENABLED) .get, EnvironmentalVariable.ENV_MAX_WORKFLOW_WEBSOCKET_REQUEST_PAYLOAD_SIZE_KB -> EnvironmentalVariable .get(EnvironmentalVariable.ENV_MAX_WORKFLOW_WEBSOCKET_REQUEST_PAYLOAD_SIZE_KB) .get, EnvironmentalVariable.ENV_AUTH_JWT_SECRET -> EnvironmentalVariable .get(EnvironmentalVariable.ENV_AUTH_JWT_SECRET) .get ) case class WorkflowComputingUnitCreationParams( name: String, unitType: String, cpuLimit: String, memoryLimit: String, gpuLimit: String, jvmMemorySize: String, shmSize: String, uri: Option[String] = None ) case class WorkflowComputingUnitResourceLimit( cpuLimit: String, memoryLimit: String, gpuLimit: String ) case class WorkflowComputingUnitMetrics( cpuUsage: String, memoryUsage: String ) case class DashboardWorkflowComputingUnit( computingUnit: WorkflowComputingUnit, status: String, metrics: WorkflowComputingUnitMetrics, isOwner: Boolean, accessPrivilege: EnumType, ownerGoogleAvatar: String, ownerName: String ) case class ComputingUnitLimitOptionsResponse( cpuLimitOptions: List[String], memoryLimitOptions: List[String], gpuLimitOptions: List[String] ) case class ComputingUnitTypesResponse( typeOptions: List[String] ) } @Produces(Array(MediaType.APPLICATION_JSON)) @Path("/computing-unit") class ComputingUnitManagingResource { private def getComputingUnitByCuid(ctx: DSLContext, cuid: Int): WorkflowComputingUnit = { val wcDao = new WorkflowComputingUnitDao(ctx.configuration()) val unit = wcDao.fetchOneByCuid(cuid) if (unit == null) { throw new NotFoundException(s"Computing unit with cuid=$cuid does not exist.") } unit } private def userOwnComputingUnit(ctx: DSLContext, cuid: Integer, uid: Integer): Boolean = { getComputingUnitByCuid(ctx, cuid).getUid == uid } private def getSupportedComputingUnitTypes: List[String] = { val allTypes = WorkflowComputingUnitTypeEnum.values().map(_.getLiteral).toList allTypes.filter { case "local" => ComputingUnitConfig.localComputingUnitEnabled case "kubernetes" => KubernetesConfig.kubernetesComputingUnitEnabled case _ => false // Any unknown types are disabled by default } } private def getComputingUnitStatus(unit: WorkflowComputingUnit): ComputingUnitState = { unit.getType match { // ── Local CUs are always "running" ────────────────────────────── case WorkflowComputingUnitTypeEnum.local => Running // ── Kubernetes CUs – only explicit "Running" counts as running ─ case WorkflowComputingUnitTypeEnum.kubernetes => val phaseOpt = KubernetesClient .getPodByName(KubernetesClient.generatePodName(unit.getCuid)) .map(_.getStatus.getPhase) if (phaseOpt.contains("Running")) Running else Pending // ── Any other (unknown) type is treated as pending ────────────── case _ => Pending } } private def getComputingUnitMetrics(unit: WorkflowComputingUnit): WorkflowComputingUnitMetrics = { unit.getType match { case WorkflowComputingUnitTypeEnum.local => WorkflowComputingUnitMetrics("NaN", "NaN") case WorkflowComputingUnitTypeEnum.kubernetes => val metrics = KubernetesClient.getPodMetrics(unit.getCuid) WorkflowComputingUnitMetrics( metrics.getOrElse("cpu", ""), metrics.getOrElse("memory", "") ) case _ => WorkflowComputingUnitMetrics("NaN", "NaN") } } private def getComputingUnitResourceLimit( unit: WorkflowComputingUnit ): WorkflowComputingUnitResourceLimit = { unit.getType match { case WorkflowComputingUnitTypeEnum.local => WorkflowComputingUnitResourceLimit("NaN", "NaN", "NaN") case WorkflowComputingUnitTypeEnum.kubernetes => val podLimits: Map[String, String] = KubernetesClient.getPodLimits(unit.getCuid) // Get GPU value by finding the exact configured resource key val gpuValue = podLimits.getOrElse(KubernetesConfig.gpuResourceKey, "0") WorkflowComputingUnitResourceLimit( podLimits("cpu"), podLimits("memory"), gpuValue ) } } @GET @RolesAllowed(Array("REGULAR", "ADMIN")) @Produces(Array(MediaType.APPLICATION_JSON)) @Path("/limits") def getComputingUnitLimitOptions( @Auth @unused user: SessionUser ): ComputingUnitLimitOptionsResponse = { ComputingUnitLimitOptionsResponse(cpuLimitOptions, memoryLimitOptions, gpuLimitOptions) } @GET @RolesAllowed(Array("REGULAR", "ADMIN")) @Produces(Array(MediaType.APPLICATION_JSON)) @Path("/types") def getComputingUnitTypes( @Auth @unused user: SessionUser ): ComputingUnitTypesResponse = ComputingUnitTypesResponse(getSupportedComputingUnitTypes) /** * Create a new pod for the given user ID. * * @param param The parameters containing the user ID. * @return The created pod or an error response. */ @POST @RolesAllowed(Array("REGULAR", "ADMIN")) @Consumes(Array(MediaType.APPLICATION_JSON)) @Produces(Array(MediaType.APPLICATION_JSON)) @Path("/create") def createWorkflowComputingUnit( param: WorkflowComputingUnitCreationParams, @Auth user: SessionUser ): DashboardWorkflowComputingUnit = { if (param.name.trim.isEmpty) { throw new ForbiddenException("Computing unit name cannot be empty.") } // Validate the unit type val cuType: WorkflowComputingUnitTypeEnum = WorkflowComputingUnitTypeEnum.lookupLiteral(param.unitType) // Validate that the type itself is supported if (!getSupportedComputingUnitTypes.contains(param.unitType)) throw new ForbiddenException( s"Unit type '${param.unitType}' is not allowed. Valid options: " + getSupportedComputingUnitTypes.mkString(", ") ) // For Kubernetes computing units, validate resource limits cuType match { // Kubernetes-specific checks case WorkflowComputingUnitTypeEnum.kubernetes => if (!cpuLimitOptions.contains(param.cpuLimit)) throw new ForbiddenException( s"CPU quantity '${param.cpuLimit}' is not allowed. " + s"Valid options: ${cpuLimitOptions.mkString(", ")}" ) if (!memoryLimitOptions.contains(param.memoryLimit)) throw new ForbiddenException( s"Memory quantity '${param.memoryLimit}' is not allowed. " + s"Valid options: ${memoryLimitOptions.mkString(", ")}" ) if (!gpuLimitOptions.contains(param.gpuLimit)) throw new ForbiddenException( s"GPU quantity '${param.gpuLimit}' is not allowed. " + s"Valid options: ${gpuLimitOptions.mkString(", ")}" ) // Check if the shared-memory size is the valid size representation val shmQuantity = try { Quantity.parse(param.shmSize) } catch { case _: IllegalArgumentException => throw new ForbiddenException( s"Shared-memory size '${param.shmSize}' is not a valid Kubernetes quantity " + s"(examples: 64Mi, 2Gi)." ) } val memQuantity = Quantity.parse(param.memoryLimit) // ensure /dev/shm upper bound ≤ container memory limit if (shmQuantity.compareTo(memQuantity) > 0) throw new ForbiddenException( s"Shared-memory size (${param.shmSize}) cannot exceed the total memory limit " + s"(${param.memoryLimit})." ) // JVM heap ≤ total memory val jvmGB = param.jvmMemorySize.replaceAll("[^0-9]", "").toInt val memGB = if (param.memoryLimit.endsWith("Gi")) param.memoryLimit.replaceAll("[^0-9]", "").toInt else if (param.memoryLimit.endsWith("Mi")) param.memoryLimit.replaceAll("[^0-9]", "").toInt / 1024 else param.memoryLimit.replaceAll("[^0-9]", "").toInt if (jvmGB > memGB) throw new ForbiddenException( s"JVM memory size (${param.jvmMemorySize}) cannot exceed the " + s"total memory limit (${param.memoryLimit})." ) // Local-specific checks case WorkflowComputingUnitTypeEnum.local => if (param.uri.forall(_.trim.isEmpty)) throw new ForbiddenException("URI is required for local computing units") // Anything else (shouldn't happen if you keep supported types in sync) case _ => throw new ForbiddenException(s"Unsupported computing-unit type: ${param.unitType}") } withTransaction(context) { ctx => val wcDao = new WorkflowComputingUnitDao(ctx.configuration()) val units = wcDao .fetchByUid(user.getUid) .asScala .filter(_.getTerminateTime == null) // Filter out terminated units if ( units.size >= maxNumOfRunningComputingUnitsPerUser && cuType == WorkflowComputingUnitTypeEnum.kubernetes ) { throw InsufficientComputingUnitQuota(maxNumOfRunningComputingUnitsPerUser) } val resourceJson: String = cuType match { // ── Kubernetes CU ─────────────────────────────────────── case WorkflowComputingUnitTypeEnum.kubernetes => Json.stringify( Json.obj( "cpuLimit" -> param.cpuLimit, "memoryLimit" -> param.memoryLimit, "gpuLimit" -> param.gpuLimit, "jvmMemorySize" -> param.jvmMemorySize, "shmSize" -> param.shmSize, "nodeAddresses" -> Json.arr() // filled in later ) ) // ── Local CU ───────────────────────────────────────────── case WorkflowComputingUnitTypeEnum.local => Json.stringify( Json.obj( "cpuLimit" -> "NaN", "memoryLimit" -> "NaN", "gpuLimit" -> "NaN", "jvmMemorySize" -> "NaN", "shmSize" -> "NaN", // user-supplied URI goes straight in "nodeAddresses" -> Json.arr(param.uri.get) ) ) case _ => "{}" } val computingUnit = new WorkflowComputingUnit() val userToken = JwtAuth.jwtToken(jwtClaims(user.user, TOKEN_EXPIRE_TIME_IN_MINUTES)) computingUnit.setUid(user.getUid) computingUnit.setName(param.name) computingUnit.setCreationTime(new Timestamp(System.currentTimeMillis())) computingUnit.setType(WorkflowComputingUnitTypeEnum.lookupLiteral(param.unitType)) computingUnit.setResource(resourceJson) // Set URI during initial insert for local only if (cuType == WorkflowComputingUnitTypeEnum.local) { computingUnit.setUri(param.uri.get) } else { computingUnit.setUri("") // placeholder for kubernetes } wcDao.insert(computingUnit) val userDao = new UserDao(ctx.configuration()) val ownerUser = Option(userDao.fetchOneByUid(user.getUid)) val ownerGoogleAvatar: String = ownerUser.flatMap(u => Option(u.getGoogleAvatar).filter(_.nonEmpty)).orNull val ownerUsername: String = ownerUser.flatMap(u => Option(u.getName).filter(_.nonEmpty)).orNull // Retrieve generated cuid val cuid = ctx.lastID().intValue() val insertedUnit = wcDao.fetchOneByCuid(cuid) if (cuType == WorkflowComputingUnitTypeEnum.kubernetes && insertedUnit != null) { // 1. Update the DB with the URI insertedUnit.setUri(KubernetesClient.generatePodURI(cuid)) val updatedResource: JsObject = Json .parse(insertedUnit.getResource) .as[JsObject] ++ Json.obj("nodeAddresses" -> Json.arr(insertedUnit.getUri)) insertedUnit.setResource(Json.stringify(updatedResource)) wcDao.update(insertedUnit) // 2. Launch the pod as CU try { KubernetesClient.createPod( cuid, param.cpuLimit, param.memoryLimit, param.gpuLimit, computingUnitEnvironmentVariables ++ Map( EnvironmentalVariable.ENV_USER_JWT_TOKEN -> userToken, EnvironmentalVariable.ENV_JAVA_OPTS -> s"-Xmx${param.jvmMemorySize}" ), Some(param.shmSize) ) } catch { case e: KubernetesClientException => throw ComputingUnitManagingServiceException.fromKubernetes(e) case t: Throwable => throw t } } DashboardWorkflowComputingUnit( insertedUnit, getComputingUnitStatus(insertedUnit).toString, getComputingUnitMetrics(insertedUnit), isOwner = true, accessPrivilege = PrivilegeEnum.WRITE, ownerGoogleAvatar, ownerUsername ) } } /** * List all computing units created by the current user. * * @return A list of computing units that are not terminated. */ @GET @RolesAllowed(Array("REGULAR", "ADMIN")) @Consumes(Array(MediaType.APPLICATION_JSON)) @Produces(Array(MediaType.APPLICATION_JSON)) @Path("") def listComputingUnits( @Auth user: SessionUser ): List[DashboardWorkflowComputingUnit] = { withTransaction(context) { ctx => val computingUnitDao = new WorkflowComputingUnitDao(ctx.configuration()) val uid = user.getUid // Always fetch units owned by the user val ownedUnits = computingUnitDao.fetchByUid(uid).asScala.toList // Conditionally fetch shared units based on the config flag val (sharedUnits, sharedUnitInfo) = if (ComputingUnitConfig.sharingComputingUnitEnabled) { val computingUnitUserAccessDao = new ComputingUnitUserAccessDao(ctx.configuration()) val info = computingUnitUserAccessDao .fetchByUid(uid) .asScala .map(access => access.getCuid -> access.getPrivilege) .toMap val sharedCuids = info.keys.toList.map(Integer.valueOf(_)) val units = if (sharedCuids.isEmpty) { List() } else { computingUnitDao.fetchByCuid(sharedCuids: _*).asScala.toList } (units, info) } else { // If sharing is disabled, return empty collections (List.empty[WorkflowComputingUnit], Map.empty[Integer, PrivilegeEnum]) } val allUnits = ownedUnits ++ sharedUnits val ownerUids: List[Integer] = allUnits.map(_.getUid).distinct val userDao = new UserDao(ctx.configuration()) val ownerInfoMap: Map[Integer, (String, String)] = userDao .fetchByUid(ownerUids: _*) .asScala .map { u => val avatar = Option(u.getGoogleAvatar).filter(_.nonEmpty).orNull val name = Option(u.getName).filter(_.nonEmpty).orNull u.getUid -> (avatar, name) } .toMap // If a Kubernetes pod has already disappeared (e.g., manually deleted or TTL // GC-ed by the cluster), we treat the corresponding computing unit as // terminated from the system's point of view. Here we eagerly update its // terminateTime in the database **before** we build the response list so // that subsequent API calls will no longer return this unit. allUnits.foreach { unit => if ( unit.getType == WorkflowComputingUnitTypeEnum.kubernetes && !KubernetesClient.podExists(unit.getCuid) ) { unit.setTerminateTime(new Timestamp(System.currentTimeMillis())) computingUnitDao.update(unit) } } // For shared units, we need to check the access privilege which are saved in different table // to streamline the process, we combine owned units with default WRITE privilege and use sharedUnitInfo // to get the privilege for shared units. (ownedUnits.map(u => (u, PrivilegeEnum.WRITE)) ++ sharedUnits.map(u => (u, sharedUnitInfo(u.getCuid)) )) .distinctBy { case (unit, _) => unit.getCuid } .filter { case (unit, _) => unit.getTerminateTime == null } .filter { case (unit, _) => unit.getType match { case WorkflowComputingUnitTypeEnum.kubernetes => KubernetesClient.podExists(unit.getCuid) case _ => true } } .map { case (unit, privilege) => DashboardWorkflowComputingUnit( computingUnit = unit, isOwner = unit.getUid.equals(uid), accessPrivilege = privilege, status = getComputingUnitStatus(unit).toString, metrics = getComputingUnitMetrics(unit), ownerGoogleAvatar = ownerInfoMap.getOrElse(unit.getUid, (null, null))._1, ownerName = ownerInfoMap.getOrElse(unit.getUid, (null, null))._2 ) } } } /** * Return a fully populated [[org.apache.texera.service.resource.ComputingUnitManagingResource.DashboardWorkflowComputingUnit]] for the * specified `cuid`, identical to one row produced by /list. * * @param cuid the ID of the computing-unit to fetch */ @GET @RolesAllowed(Array("REGULAR", "ADMIN")) @Produces(Array(MediaType.APPLICATION_JSON)) @Path("/{cuid}") def getComputingUnitInfo( @PathParam("cuid") cuid: Integer, @Auth user: SessionUser ): DashboardWorkflowComputingUnit = { val unit = getComputingUnitByCuid(context, cuid) val userDao = new UserDao(context.configuration()) val ownerUser = Option(userDao.fetchOneByUid(unit.getUid)) val ownerGoogleAvatar: String = ownerUser.flatMap(u => Option(u.getGoogleAvatar).filter(_.nonEmpty)).orNull val ownerUsername: String = ownerUser.flatMap(u => Option(u.getName).filter(_.nonEmpty)).orNull DashboardWorkflowComputingUnit( computingUnit = unit, status = getComputingUnitStatus(unit).toString, metrics = getComputingUnitMetrics(unit), isOwner = unit.getUid.equals(user.getUid), accessPrivilege = { val cuAccessDao = new ComputingUnitUserAccessDao(context.configuration()) val access = cuAccessDao .fetchByUid(user.getUid) .asScala .find(access => access.getCuid.equals(cuid)) if (access.isDefined) { access.get.getPrivilege } else if (unit.getUid.equals(user.getUid)) { PrivilegeEnum.WRITE } else { // Default privilege for non-owners without explicit access PrivilegeEnum.NONE } }, ownerGoogleAvatar, ownerUsername ) } /** * Terminate the computing unit's pod based on the pod URI. * * @return A response indicating success or failure. */ @DELETE @RolesAllowed(Array("REGULAR", "ADMIN")) @Consumes(Array(MediaType.APPLICATION_JSON)) @Produces(Array(MediaType.APPLICATION_JSON)) @Path("/{cuid}/terminate") def terminateComputingUnit( @PathParam("cuid") cuid: Integer, @Auth user: SessionUser ): Response = { if (!userOwnComputingUnit(context, cuid, user.getUid)) { return Response .status(Response.Status.BAD_REQUEST) .entity(s"User has no access to the computing unit") .build() } // If successful, update the database withTransaction(context) { ctx => val cuDao = new WorkflowComputingUnitDao(ctx.configuration()) val unit = getComputingUnitByCuid(ctx, cuid) // if the computing unit is kubernetes pod, then kill the pod if (unit.getType == WorkflowComputingUnitTypeEnum.kubernetes) { KubernetesClient.deletePod(cuid) } unit.setTerminateTime(new Timestamp(System.currentTimeMillis())) cuDao.update(unit) } Response.ok().build() } /** * Rename a computing unit. * * @param cuid The computing unit ID. * @param name The new name for the computing unit. * @param user The authenticated user. * @return A response indicating success or failure. */ @PUT @RolesAllowed(Array("REGULAR", "ADMIN")) @Consumes(Array(MediaType.APPLICATION_JSON)) @Produces(Array(MediaType.APPLICATION_JSON)) @Path("/{cuid}/rename/{name}") def renameComputingUnit( @PathParam("cuid") cuid: Integer, @PathParam("name") name: String, @Auth user: SessionUser ): Response = { // Verify ownership or write access if ( !userOwnComputingUnit(context, cuid, user.getUid) && !ComputingUnitAccessResource.hasWriteAccess(cuid, user.getUid) ) { return Response .status(Response.Status.FORBIDDEN) .entity("User does not have permission to rename this computing unit") .build() } // Validate name if (StringUtils.isBlank(name)) { return Response .status(Response.Status.BAD_REQUEST) .entity("Computing unit name cannot be empty or blank") .build() } withTransaction(context) { ctx => val cuDao = new WorkflowComputingUnitDao(ctx.configuration()) val unit = getComputingUnitByCuid(ctx, cuid) try { unit.setName(name) cuDao.update(unit) } catch { case e: Exception => return Response .status(Response.Status.INTERNAL_SERVER_ERROR) .entity(e.getMessage) .build() } } Response.ok().build() } /** * Retrieves the CPU and memory metrics for a computing unit identified by its `cuid`. * * @param cuid The computing unit ID. * @return A `WorkflowComputingUnitMetrics` object with CPU and memory usage data. */ @GET @RolesAllowed(Array("REGULAR", "ADMIN")) @Produces(Array(MediaType.APPLICATION_JSON)) @Path("/{cuid}/metrics") def getComputingUnitMetricsEndpoint( @PathParam("cuid") cuid: String, @Auth user: SessionUser ): WorkflowComputingUnitMetrics = { if (!userOwnComputingUnit(context, cuid.toInt, user.getUid)) { throw new BadRequestException("User has no access to the computing unit") } val computingUnit = getComputingUnitByCuid(context, cuid.toInt) getComputingUnitMetrics(computingUnit) } @GET @RolesAllowed(Array("REGULAR", "ADMIN")) @Produces(Array(MediaType.APPLICATION_JSON)) @Path("/{cuid}/limits") def getComputingUnitResourceLimit( @PathParam("cuid") cuid: String, @Auth user: SessionUser ): WorkflowComputingUnitResourceLimit = { if (!userOwnComputingUnit(context, cuid.toInt, user.getUid)) { throw new BadRequestException("User has no access to the computing unit") } val computingUnit = getComputingUnitByCuid(context, cuid.toInt) getComputingUnitResourceLimit(computingUnit) } } ================================================ FILE: computing-unit-managing-service/src/main/scala/org/apache/texera/service/resource/ComputingUnitState.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.service.resource object ComputingUnitState extends Enumeration { type ComputingUnitState = Value val Running, Pending = Value } ================================================ FILE: computing-unit-managing-service/src/main/scala/org/apache/texera/service/resource/HealthCheckResource.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.resource import jakarta.ws.rs.core.MediaType import jakarta.ws.rs.{GET, Path, Produces} @Path("/healthcheck") @Produces(Array(MediaType.APPLICATION_JSON)) class HealthCheckResource { @GET def healthCheck: Map[String, String] = Map("status" -> "ok") } ================================================ FILE: computing-unit-managing-service/src/main/scala/org/apache/texera/service/util/ComputingUnitHelpers.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.texera.service.util import org.apache.texera.dao.jooq.generated.enums.WorkflowComputingUnitTypeEnum import org.apache.texera.dao.jooq.generated.tables.pojos.WorkflowComputingUnit import org.apache.texera.service.resource.ComputingUnitManagingResource.WorkflowComputingUnitMetrics import org.apache.texera.service.resource.ComputingUnitState.{ComputingUnitState, Pending, Running} object ComputingUnitHelpers { def getComputingUnitStatus(unit: WorkflowComputingUnit): ComputingUnitState = { unit.getType match { // Local CUs are always “running” case WorkflowComputingUnitTypeEnum.local => Running // Kubernetes CUs – only explicit “Running” counts as running case WorkflowComputingUnitTypeEnum.kubernetes => val phaseOpt = KubernetesClient .getPodByName(KubernetesClient.generatePodName(unit.getCuid)) .map(_.getStatus.getPhase) if (phaseOpt.contains("Running")) Running else Pending // Any other (unknown) type is treated as pending case _ => Pending } } def getComputingUnitMetrics(unit: WorkflowComputingUnit): WorkflowComputingUnitMetrics = { unit.getType match { case WorkflowComputingUnitTypeEnum.local => WorkflowComputingUnitMetrics("NaN", "NaN") case WorkflowComputingUnitTypeEnum.kubernetes => val metrics = KubernetesClient.getPodMetrics(unit.getCuid) WorkflowComputingUnitMetrics( metrics.getOrElse("cpu", ""), metrics.getOrElse("memory", "") ) case _ => WorkflowComputingUnitMetrics("NaN", "NaN") } } } ================================================ FILE: computing-unit-managing-service/src/main/scala/org/apache/texera/service/util/ComputingUnitManagingServiceException.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.util import io.fabric8.kubernetes.client.KubernetesClientException import jakarta.ws.rs.WebApplicationException /** * Parent type for every error the CU-managing service can raise. */ sealed abstract class ComputingUnitManagingServiceException(msg: String) extends WebApplicationException(msg) // Not enough cluster resources for this CU request (CPU / memory / GPU) final case class InsufficientComputingResource(resourceType: String) extends ComputingUnitManagingServiceException( s"Insufficient $resourceType available in the server. Please decrease the requested amount or try again later." ) // User has reached the per-user CU-quota (number of running units) final case class InsufficientComputingUnitQuota(maxNumberOfComputingUnit: Int) extends ComputingUnitManagingServiceException( s"You may only have $maxNumberOfComputingUnit computing-unit(s) running at the same time" ) // default exception fallback final case class InternalError( override val getMessage: String = "The server encountered an internal error while processing your request. " + "Please try again later." ) extends ComputingUnitManagingServiceException(getMessage) /** * Companion object with helpers. */ object ComputingUnitManagingServiceException { /** * Translate a KubernetesClientException to one of the service-level exceptions */ def fromKubernetes(e: KubernetesClientException): ComputingUnitManagingServiceException = { val message = Option(e.getMessage).map(_.toLowerCase).getOrElse("") if (message.contains("exceeded quota")) { if (message.contains("cpu")) InsufficientComputingResource("CPU") else if (message.contains("memory")) InsufficientComputingResource("memory") else if (message.contains("gpu")) InsufficientComputingResource("GPU") else InternalError(e.getMessage) } else { InternalError(e.getMessage) } } } ================================================ FILE: computing-unit-managing-service/src/main/scala/org/apache/texera/service/util/KubernetesClient.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.util import io.fabric8.kubernetes.api.model._ import io.fabric8.kubernetes.api.model.metrics.v1beta1.PodMetricsList import io.fabric8.kubernetes.client.KubernetesClientBuilder import org.apache.texera.config.KubernetesConfig import scala.jdk.CollectionConverters._ object KubernetesClient { // Initialize the Kubernetes client private val client: io.fabric8.kubernetes.client.KubernetesClient = new KubernetesClientBuilder().build() private val namespace: String = KubernetesConfig.computeUnitPoolNamespace private val podNamePrefix = "computing-unit" def generatePodURI(cuid: Int): String = { s"${generatePodName(cuid)}.${KubernetesConfig.computeUnitServiceName}.$namespace.svc.cluster.local" } def generatePodName(cuid: Int): String = s"$podNamePrefix-$cuid" def podExists(cuid: Int): Boolean = { getPodByName(generatePodName(cuid)).isDefined } def getPodByName(podName: String): Option[Pod] = { Option(client.pods().inNamespace(namespace).withName(podName).get()) } def getPodMetrics(cuid: Int): Map[String, String] = { val podMetricsList: PodMetricsList = client.top().pods().metrics(namespace) val targetPodName = generatePodName(cuid) podMetricsList.getItems.asScala .collectFirst { case podMetrics if podMetrics.getMetadata.getName == targetPodName => podMetrics.getContainers.asScala.flatMap { container => container.getUsage.asScala.map { case (metric, value) => metric -> value.toString } }.toMap } .getOrElse(Map.empty[String, String]) } def getPodLimits(cuid: Int): Map[String, String] = { getPodByName(generatePodName(cuid)) .flatMap { pod => pod.getSpec.getContainers.asScala.headOption.map { container => val limitsMap = container.getResources.getLimits.asScala.map { case (key, value) => key -> value.toString }.toMap limitsMap } } .getOrElse(Map.empty[String, String]) } def createPod( cuid: Int, cpuLimit: String, memoryLimit: String, gpuLimit: String, envVars: Map[String, Any], shmSize: Option[String] = None ): Pod = { val podName = generatePodName(cuid) if (getPodByName(podName).isDefined) { throw new Exception(s"Pod with cuid $cuid already exists") } val envList = envVars .map { case (key, value) => new EnvVarBuilder() .withName(key) .withValue(value.toString) .build() } .toList .asJava // Setup the resource requirements val resourceBuilder = new ResourceRequirementsBuilder() .addToLimits("cpu", new Quantity(cpuLimit)) .addToLimits("memory", new Quantity(memoryLimit)) // Only add GPU resources if the requested amount is greater than 0 if (gpuLimit != "0") { // Use the configured GPU resource key directly resourceBuilder.addToLimits(KubernetesConfig.gpuResourceKey, new Quantity(gpuLimit)) } // Build the pod with metadata val podBuilder = new PodBuilder() .withNewMetadata() .withName(podName) .withNamespace(namespace) .addToLabels("type", "computing-unit") .addToLabels("cuid", cuid.toString) .addToLabels("name", podName) // Start building the pod spec val specBuilder = podBuilder .endMetadata() .withNewSpec() // Only add runtimeClassName when using NVIDIA GPU if (gpuLimit != "0" && KubernetesConfig.gpuResourceKey.contains("nvidia")) { specBuilder.withRuntimeClassName("nvidia") } val containerBuilder = specBuilder .addNewContainer() .withName("computing-unit-master") .withImage(KubernetesConfig.computeUnitImageName) .withImagePullPolicy(KubernetesConfig.computingUnitImagePullPolicy) .addNewPort() .withContainerPort(KubernetesConfig.computeUnitPortNumber) .endPort() .withEnv(envList) .withResources(resourceBuilder.build()) // If shmSize requested, mount /dev/shm shmSize.foreach { _ => containerBuilder .addNewVolumeMount() .withName("dshm") .withMountPath("/dev/shm") .endVolumeMount() } containerBuilder.endContainer() // Add tmpfs volume if needed shmSize.foreach { size => specBuilder .addNewVolume() .withName("dshm") .withEmptyDir( new EmptyDirVolumeSourceBuilder() .withMedium("Memory") .withSizeLimit(new Quantity(size)) .build() ) .endVolume() } val pod = specBuilder .withHostname(podName) .withSubdomain(KubernetesConfig.computeUnitServiceName) .endSpec() .build() client.resource(pod).inNamespace(namespace).create() } def deletePod(cuid: Int): Unit = { client.pods().inNamespace(namespace).withName(generatePodName(cuid)).delete() } } ================================================ FILE: config-service/LICENSE-binary ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Support. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or support. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================================ THIRD-PARTY COMPONENTS ================================================================================ Apache Texera's binary distribution of this service includes the following third-party components, grouped by license. Each section references licenses/ for the full text of the applicable license. Components under the Apache License, Version 2.0 are governed by the same license terms as Apache Texera itself and are listed for completeness. Locations within the distribution: - Scala/Java jars listed below ship under the lib/ directory of this service's Universal zip. - Source files derived from third-party projects are compiled into the bundled jars under lib/ and live at the listed paths in the Apache Texera source tree. -------------------------------------------------------------------------------- Dependencies under the Apache License, Version 2.0 -------------------------------------------------------------------------------- Scala/Java jars: - com.fasterxml.classmate-1.7.0.jar - com.fasterxml.jackson.core.jackson-annotations-2.18.6.jar - com.fasterxml.jackson.core.jackson-core-2.18.6.jar - com.fasterxml.jackson.core.jackson-databind-2.18.6.jar - com.fasterxml.jackson.dataformat.jackson-dataformat-yaml-2.16.1.jar - com.fasterxml.jackson.datatype.jackson-datatype-guava-2.16.1.jar - com.fasterxml.jackson.datatype.jackson-datatype-jdk8-2.16.1.jar - com.fasterxml.jackson.datatype.jackson-datatype-jsr310-2.16.1.jar - com.fasterxml.jackson.jakarta.rs.jackson-jakarta-rs-base-2.16.1.jar - com.fasterxml.jackson.jakarta.rs.jackson-jakarta-rs-json-provider-2.16.1.jar - com.fasterxml.jackson.module.jackson-module-blackbird-2.16.1.jar - com.fasterxml.jackson.module.jackson-module-jakarta-xmlbind-annotations-2.16.1.jar - com.fasterxml.jackson.module.jackson-module-parameter-names-2.16.1.jar - com.fasterxml.jackson.module.jackson-module-scala_2.13-2.18.6.jar - com.github.ben-manes.caffeine.caffeine-3.1.8.jar - com.google.code.findbugs.jsr305-3.0.2.jar - com.google.errorprone.error_prone_annotations-2.25.0.jar - com.google.guava.failureaccess-1.0.2.jar - com.google.guava.guava-33.0.0-jre.jar - com.google.guava.listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar - com.google.j2objc.j2objc-annotations-2.8.jar - com.helger.profiler-1.1.1.jar - com.thesamet.scalapb.lenses_2.13-0.11.20.jar - com.thesamet.scalapb.scalapb-json4s_2.13-0.12.0.jar - com.thesamet.scalapb.scalapb-runtime_2.13-0.11.20.jar - com.typesafe.config-1.4.6.jar - com.typesafe.scala-logging.scala-logging_2.13-3.9.5.jar - io.dropwizard.dropwizard-auth-4.0.7.jar - io.dropwizard.dropwizard-configuration-4.0.7.jar - io.dropwizard.dropwizard-core-4.0.7.jar - io.dropwizard.dropwizard-health-4.0.7.jar - io.dropwizard.dropwizard-jackson-4.0.7.jar - io.dropwizard.dropwizard-jersey-4.0.7.jar - io.dropwizard.dropwizard-jetty-4.0.7.jar - io.dropwizard.dropwizard-lifecycle-4.0.7.jar - io.dropwizard.dropwizard-logging-4.0.7.jar - io.dropwizard.dropwizard-metrics-4.0.7.jar - io.dropwizard.dropwizard-request-logging-4.0.7.jar - io.dropwizard.dropwizard-servlets-4.0.7.jar - io.dropwizard.dropwizard-util-4.0.7.jar - io.dropwizard.dropwizard-validation-4.0.7.jar - io.dropwizard.logback.logback-throttling-appender-1.4.2.jar - io.dropwizard.metrics.metrics-annotation-4.2.25.jar - io.dropwizard.metrics.metrics-caffeine-4.2.25.jar - io.dropwizard.metrics.metrics-core-4.2.25.jar - io.dropwizard.metrics.metrics-healthchecks-4.2.25.jar - io.dropwizard.metrics.metrics-jakarta-servlets-4.2.25.jar - io.dropwizard.metrics.metrics-jersey3-4.2.25.jar - io.dropwizard.metrics.metrics-jetty11-4.2.25.jar - io.dropwizard.metrics.metrics-jmx-4.2.25.jar - io.dropwizard.metrics.metrics-json-4.2.25.jar - io.dropwizard.metrics.metrics-jvm-4.2.25.jar - io.dropwizard.metrics.metrics-logback-4.2.25.jar - io.r2dbc.r2dbc-spi-0.9.0.RELEASE.jar - jakarta.inject.jakarta.inject-api-2.0.1.jar - jakarta.validation.jakarta.validation-api-3.0.2.jar - org.apache.commons.commons-lang3-3.13.0.jar - org.apache.commons.commons-text-1.11.0.jar - org.bitbucket.b_c.jose4j-0.9.6.jar - org.eclipse.jetty.jetty-http-11.0.20.jar - org.eclipse.jetty.jetty-io-11.0.20.jar - org.eclipse.jetty.jetty-security-11.0.20.jar - org.eclipse.jetty.jetty-server-11.0.20.jar - org.eclipse.jetty.jetty-servlet-11.0.20.jar - org.eclipse.jetty.jetty-servlets-11.0.20.jar - org.eclipse.jetty.jetty-util-11.0.20.jar - org.eclipse.jetty.toolchain.jetty-jakarta-servlet-api-5.0.2.jar - org.eclipse.jetty.toolchain.setuid.jetty-setuid-java-1.0.4.jar - org.hibernate.validator.hibernate-validator-7.0.5.Final.jar - org.javassist.javassist-3.30.2-GA.jar - org.jboss.logging.jboss-logging-3.5.3.Final.jar - org.jooq.jooq-3.16.23.jar - org.json4s.json4s-ast_2.13-4.0.1.jar - org.json4s.json4s-jackson-core_2.13-4.0.1.jar - org.playframework.play-functional_2.13-3.1.0-M1.jar - org.playframework.play-json_2.13-3.1.0-M1.jar - org.scala-lang.modules.scala-collection-compat_2.13-2.13.0.jar - org.scala-lang.scala-library-2.13.18.jar - org.scala-lang.scala-reflect-2.13.18.jar - org.slf4j.jcl-over-slf4j-2.0.12.jar - org.slf4j.log4j-over-slf4j-2.0.12.jar - org.yaml.snakeyaml-2.2.jar -------------------------------------------------------------------------------- Dependencies under the MIT License -------------------------------------------------------------------------------- Source files derived from third-party MIT-licensed projects: - mbknor-jackson-jsonschema common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaDraft.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaArrayWithUniqueItems.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaBool.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDefault.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDescription.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaExamples.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaFormat.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInject.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInt.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaOptions.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaString.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaTitle.java https://github.com/mbknor/mbknor-jackson-jsonschema Scala/Java jars: - net.sourceforge.argparse4j.argparse4j-0.9.0.jar - org.checkerframework.checker-qual-3.52.0.jar - org.slf4j.jul-to-slf4j-2.0.12.jar - org.slf4j.slf4j-api-2.0.12.jar -------------------------------------------------------------------------------- Dependencies under the BSD 3-Clause License -------------------------------------------------------------------------------- Scala/Java jars: - com.google.protobuf.protobuf-java-3.25.8.jar - com.thoughtworks.paranamer.paranamer-2.8.jar -------------------------------------------------------------------------------- Dependencies under the BSD 2-Clause License -------------------------------------------------------------------------------- Scala/Java jars: - org.postgresql.postgresql-42.7.10.jar -------------------------------------------------------------------------------- Dependencies under the Eclipse Public License, Version 2.0 (some are dual licensed with GPL-2.0 with Classpath Exception) -------------------------------------------------------------------------------- Scala/Java jars: - jakarta.annotation.jakarta.annotation-api-2.1.1.jar - jakarta.el.jakarta.el-api-4.0.0.jar - jakarta.servlet.jakarta.servlet-api-5.0.0.jar - jakarta.ws.rs.jakarta.ws.rs-api-3.1.0.jar - org.glassfish.hk2.external.aopalliance-repackaged-3.0.6.jar - org.glassfish.hk2.hk2-api-3.0.6.jar - org.glassfish.hk2.hk2-locator-3.0.3.jar - org.glassfish.hk2.hk2-utils-3.0.6.jar - org.glassfish.hk2.osgi-resource-locator-1.0.3.jar - org.glassfish.jakarta.el-4.0.2.jar - org.glassfish.jersey.containers.jersey-container-servlet-3.0.12.jar - org.glassfish.jersey.containers.jersey-container-servlet-core-3.0.12.jar - org.glassfish.jersey.core.jersey-client-3.0.12.jar - org.glassfish.jersey.core.jersey-common-3.0.12.jar - org.glassfish.jersey.core.jersey-server-3.0.12.jar - org.glassfish.jersey.ext.jersey-bean-validation-3.0.12.jar - org.glassfish.jersey.ext.jersey-metainf-services-3.0.12.jar - org.glassfish.jersey.inject.jersey-hk2-3.0.12.jar -------------------------------------------------------------------------------- Dependencies under the Eclipse Public License, Version 1.0 (Logback is dual licensed with LGPL-2.1) -------------------------------------------------------------------------------- Scala/Java jars: - ch.qos.logback.logback-access-1.4.14.jar - ch.qos.logback.logback-classic-1.4.14.jar - ch.qos.logback.logback-core-1.4.14.jar -------------------------------------------------------------------------------- Dependencies under the Eclipse Distribution License, Version 1.0 -------------------------------------------------------------------------------- Scala/Java jars: - com.sun.activation.jakarta.activation-2.0.1.jar - jakarta.activation.jakarta.activation-api-2.1.0.jar - jakarta.xml.bind.jakarta.xml.bind-api-3.0.1.jar -------------------------------------------------------------------------------- Dependencies in the Public Domain (CC0) -------------------------------------------------------------------------------- Scala/Java jars: - org.reactivestreams.reactive-streams-1.0.3.jar Individual jars may contain their own META-INF/LICENSE and META-INF/NOTICE files that apply to their specific contents; those files continue to govern the use of those components. ================================================ FILE: config-service/NOTICE-binary ================================================ Apache Texera (Incubating) Copyright 2025-2026 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). -------------------------------------------------------------------------------- Eclipse Jetty -------------------------------------------------------------------------------- Jetty Web Container Copyright 1995-2018 Mort Bay Consulting Pty Ltd. The Jetty Web Container is Copyright Mort Bay Consulting Pty Ltd unless otherwise noted. Jetty is dual licensed under both the Apache 2.0 License and the Eclipse Public 1.0 License; Texera redistributes it under the Apache 2.0 terms. Jetty bundles select artifacts under secondary licenses: * Eclipse Public License: org.eclipse.jetty.orbit:org.eclipse.jdt.core, javax.security.auth.message (EPL + ASL2), javax.mail.glassfish (EPL + CDDL 1.0) * CDDL + GPLv2 with classpath exception: javax.servlet:javax.servlet-api, javax.annotation:javax.annotation-api, javax.transaction:javax.transaction-api, javax.websocket:javax.websocket-api * OW2 license: org.ow2.asm:asm-commons, org.ow2.asm:asm * MortBay ASL2: org.mortbay.jasper:apache-jsp, apache-el (based on selected classes from Apache Tomcat) The UnixCrypt.java code implements one-way cryptography used by Unix systems for simple password protection. Copyright 1996 Aki Yoshida, modified April 2001 by Iris Van den Broeke, Daniel Deville. -------------------------------------------------------------------------------- Jackson (FasterXML) -------------------------------------------------------------------------------- Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. Copyright 2007- Tatu Saloranta (tatu.saloranta@iki.fi) Jackson 2.x core and extension components are licensed under Apache License 2.0. This attribution applies to jackson-core, jackson-databind, jackson-annotations, and every jackson-datatype-*, jackson-module-*, jackson-dataformat-*, and jackson-jaxrs-* artifact bundled in this distribution. Java ClassMate library (com.fasterxml:classmate) was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), with contributions from Brian Langel. -------------------------------------------------------------------------------- Jackson core (verbatim upstream NOTICE) -------------------------------------------------------------------------------- # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. ## Copyright Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) ## Licensing Jackson 2.x core and extension components are licensed under Apache License 2.0 To find the details that apply to this artifact see the accompanying LICENSE file. ## Credits A list of contributors may be found from CREDITS(-2.x) file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. ## FastDoubleParser jackson-core bundles a shaded copy of FastDoubleParser . That code is available under an MIT license under the following copyright. Copyright © 2023 Werner Randelshofer, Switzerland. MIT License. See FastDoubleParser-NOTICE for details of other source code included in FastDoubleParser and the licenses and copyrights that apply to that code. # FastDoubleParser This is a Java port of Daniel Lemire's fast_float project. This project provides parsers for double, float, BigDecimal and BigInteger values. ## Copyright Copyright © 2024 Werner Randelshofer, Switzerland. ## Licensing This code is licensed under MIT License. https://github.com/wrandelshofer/FastDoubleParser/blob/522be16e145f43308c43b23094e31d5efcaa580e/LICENSE (The file 'LICENSE' is included in the sources and classes Jar files that are released by this project - as is required by that license.) Some portions of the code have been derived from other projects. All these projects require that we include a copyright notice, and some require that we also include some text of their license file. fast_double_parser, Copyright (c) 2022 Daniel Lemire. BSL License. https://github.com/lemire/fast_double_parser https://github.com/lemire/fast_double_parser/blob/07d9189a8fb815fe800cb15ca022e7a07093236e/LICENSE.BSL (The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project - as is required by that license.) fast_float, Copyright (c) 2021 The fast_float authors. MIT License. https://github.com/fastfloat/fast_float https://github.com/fastfloat/fast_float/blob/cc1e01e9eee74128e48d51488a6b1df4a767a810/LICENSE-MIT (The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project - as is required by that license.) bigint, Copyright 2020 Tim Buktu. 2-clause BSD License. https://github.com/tbuktu/bigint/tree/floatfft https://github.com/tbuktu/bigint/blob/617c8cd8a7c5e4fb4d919c6a4d11e2586107f029/LICENSE https://github.com/wrandelshofer/FastDoubleParser/blob/39e123b15b71f29a38a087d16a0bc620fc879aa6/bigint-LICENSE (We only use those portions of the bigint project that can be licensed under 2-clause BSD License.) (The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project - as is required by that license.) -------------------------------------------------------------------------------- Jackson modules and datatypes -------------------------------------------------------------------------------- # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. ## Copyright Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) ## Licensing Jackson 2.x core and extension components are licensed under Apache License 2.0 To find the details that apply to this artifact see the accompanying LICENSE file. ## Credits A list of contributors may be found from CREDITS(-2.x) file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers, as well as supported commercially by FasterXML.com. ## Licensing Jackson core and extension components (as well their dependencies) may be licensed under different licenses. To find the details that apply to this artifact see the accompanying LICENSE file. For more information, including possible other licensing options, contact FasterXML.com (http://fasterxml.com). ## Credits A list of contributors may be found from CREDITS file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers, as well as supported commercially by FasterXML.com. ## Licensing Jackson core and extension components may be licensed under different licenses. To find the details that apply to this artifact see the accompanying LICENSE file. For more information, including possible other licensing options, contact FasterXML.com (http://fasterxml.com). ## Credits A list of contributors may be found from CREDITS file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. ## Copyright Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) ## Licensing Jackson components are licensed under Apache (Software) License, version 2.0, as per accompanying LICENSE file. ## Credits A list of contributors may be found from CREDITS file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers, as well as supported commercially by FasterXML.com. ## Licensing Jackson core and extension components may licensed under different licenses. To find the details that apply to this artifact see the accompanying LICENSE file. For more information, including possible other licensing options, contact FasterXML.com (http://fasterxml.com). ## Credits A list of contributors may be found from CREDITS file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. ## Licensing Jackson components are licensed under Apache (Software) License, version 2.0, as per accompanying LICENSE file. ## Credits A list of contributors may be found from CREDITS file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. -------------------------------------------------------------------------------- Eclipse Jetty 11.0 -------------------------------------------------------------------------------- Notices for Eclipse Jetty ========================= This content is produced and maintained by the Eclipse Jetty project. Project home: https://eclipse.dev/jetty/ Trademarks ---------- Eclipse Jetty, and Jetty are trademarks of the Eclipse Foundation. Copyright --------- All contributions are the property of the respective authors or of entities to which copyright has been assigned by the authors (eg. employer). Declared Project Licenses ------------------------- This artifacts of this project are made available under the terms of: * the Eclipse Public License v2.0 https://www.eclipse.org/legal/epl-2.0 SPDX-License-Identifier: EPL-2.0 or * the Apache License, Version 2.0 https://www.apache.org/licenses/LICENSE-2.0 SPDX-License-Identifier: Apache-2.0 The following dependencies are EPL. * org.eclipse.jetty.orbit:org.eclipse.jdt.core The following dependencies are EPL and ASL2. * org.eclipse.jetty.orbit:javax.security.auth.message The following dependencies are EPL and CDDL 1.0. * org.eclipse.jetty.orbit:javax.mail.glassfish The following dependencies are CDDL + GPLv2 with classpath exception. https://glassfish.dev.java.net/nonav/public/CDDL+GPL.html * jakarta.servlet:jakarta.servlet-api * javax.annotation:javax.annotation-api * javax.transaction:javax.transaction-api * javax.websocket:javax.websocket-api The following dependencies are licensed by the OW2 Foundation according to the terms of http://asm.ow2.org/license.html * org.ow2.asm:asm-commons * org.ow2.asm:asm The following dependencies are ASL2 licensed. * org.apache.taglibs:taglibs-standard-spec * org.apache.taglibs:taglibs-standard-impl The following dependencies are ASL2 licensed. Based on selected classes from following Apache Tomcat jars, all ASL2 licensed. * org.mortbay.jasper:apache-jsp * org.apache.tomcat:tomcat-jasper * org.apache.tomcat:tomcat-juli * org.apache.tomcat:tomcat-jsp-api * org.apache.tomcat:tomcat-el-api * org.apache.tomcat:tomcat-jasper-el * org.apache.tomcat:tomcat-api * org.apache.tomcat:tomcat-util-scan * org.apache.tomcat:tomcat-util * org.mortbay.jasper:apache-el * org.apache.tomcat:tomcat-jasper-el * org.apache.tomcat:tomcat-el-api The following artifacts are CDDL + GPLv2 with classpath exception. https://glassfish.dev.java.net/nonav/public/CDDL+GPL.html * org.eclipse.jetty.toolchain:jetty-schemas Cryptography ------------ Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. The UnixCrypt.java code implements the one way cryptography used by Unix systems for simple password protection. Copyright 1996 Aki Yoshida, modified April 2001 by Iris Van den Broeke, Daniel Deville. Permission to use, copy, modify and distribute UnixCrypt for non-commercial or commercial purposes and without fee is granted provided that the copyright notice appears in all copies. -------------------------------------------------------------------------------- R2DBC SPI -------------------------------------------------------------------------------- Reactive Relational Database Connectivity Copyright 2017-2021 the original author or authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- Eclipse Jersey (jersey-container-servlet, jersey-container-servlet-core, jersey-client, jersey-hk2, jersey-media-jaxb) -------------------------------------------------------------------------------- # Notice for Jersey This content is produced and maintained by the Eclipse Jersey project. * Project home: https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Angular JS, v1.6.6 * License MIT (http://www.opensource.org/licenses/mit-license.php) * Project: http://angularjs.org * Coyright: (c) 2010-2017 Google, Inc. aopalliance Version 1 * License: all the source code provided by AOP Alliance is Public Domain. * Project: http://aopalliance.sourceforge.net * Copyright: Material in the public domain is not protected by copyright Bean Validation API 3.0.2 * License: Apache License, 2.0 * Project: http://beanvalidation.org/1.1/ * Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors * by the @authors tag. Hibernate Validator CDI, 7.0.5.Final * License: Apache License, 2.0 * Project: https://beanvalidation.org/ * Repackaged in org.glassfish.jersey.server.validation.internal.hibernate Bootstrap v3.3.7 * License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE) * Project: http://getbootstrap.com * Copyright: 2011-2016 Twitter, Inc Google Guava Version 18.0 * License: Apache License, 2.0 * Copyright (C) 2009 The Guava Authors jakarta.inject Version: 1 * License: Apache License, 2.0 * Copyright (C) 2009 The JSR-330 Expert Group Javassist Version 3.29.2-GA * License: Apache License, 2.0 * Project: http://www.javassist.org/ * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. Jackson JAX-RS Providers Version 2.15.3 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers * Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. jQuery v1.12.4 * License: jquery.org/license * Project: jquery.org * Copyright: (c) jQuery Foundation jQuery Barcode plugin 0.3 * License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html) * Project: http://www.pasella.it/projects/jQuery/barcode * Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com JSR-166 Extension - JEP 266 * License: CC0 * No copyright * Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/ KineticJS, v4.7.1 * License: MIT license (http://www.opensource.org/licenses/mit-license.php) * Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS * Copyright: Eric Rowell org.objectweb.asm Version 9.6 * License: Modified BSD (https://asm.ow2.io/license.html) * Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved. org.osgi.core version 6.0.0 * License: Apache License, 2.0 * Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved. org.glassfish.jersey.server.internal.monitoring.core * License: Apache License, 2.0 * Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. * Copyright 2010-2013 Coda Hale and Yammer, Inc. W3.org documents * License: W3C License * Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ -------------------------------------------------------------------------------- Eclipse Jersey Core Server -------------------------------------------------------------------------------- # Notice for Jersey Core Server module This content is produced and maintained by the Eclipse Jersey project. * https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content org.glassfish.jersey.server.internal.monitoring.core * License: Apache License, 2.0 * Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. * Copyright 2010-2013 Coda Hale and Yammer, Inc. org.objectweb.asm Version 9.6 * License: Modified BSD (https://asm.ow2.io/license.html) * Copyright: (c) 2000-2011 INRIA, France Telecom. All rights reserved. W3.org documents * License: W3C License * Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ # Notice for Jersey This content is produced and maintained by the Eclipse Jersey project. * Project home: https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Angular JS, v1.6.6 * License MIT (http://www.opensource.org/licenses/mit-license.php) * Project: http://angularjs.org * Coyright: (c) 2010-2017 Google, Inc. aopalliance Version 1 * License: all the source code provided by AOP Alliance is Public Domain. * Project: http://aopalliance.sourceforge.net * Copyright: Material in the public domain is not protected by copyright Bean Validation API 3.0.2 * License: Apache License, 2.0 * Project: http://beanvalidation.org/1.1/ * Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors * by the @authors tag. Hibernate Validator CDI, 7.0.5.Final * License: Apache License, 2.0 * Project: https://beanvalidation.org/ * Repackaged in org.glassfish.jersey.server.validation.internal.hibernate Bootstrap v3.3.7 * License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE) * Project: http://getbootstrap.com * Copyright: 2011-2016 Twitter, Inc Google Guava Version 18.0 * License: Apache License, 2.0 * Copyright (C) 2009 The Guava Authors jakarta.inject Version: 1 * License: Apache License, 2.0 * Copyright (C) 2009 The JSR-330 Expert Group Javassist Version 3.29.2-GA * License: Apache License, 2.0 * Project: http://www.javassist.org/ * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. Jackson JAX-RS Providers Version 2.15.3 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers * Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. jQuery v1.12.4 * License: jquery.org/license * Project: jquery.org * Copyright: (c) jQuery Foundation jQuery Barcode plugin 0.3 * License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html) * Project: http://www.pasella.it/projects/jQuery/barcode * Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com JSR-166 Extension - JEP 266 * License: CC0 * No copyright * Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/ KineticJS, v4.7.1 * License: MIT license (http://www.opensource.org/licenses/mit-license.php) * Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS * Copyright: Eric Rowell org.objectweb.asm Version 9.6 * License: Modified BSD (https://asm.ow2.io/license.html) * Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved. org.osgi.core version 6.0.0 * License: Apache License, 2.0 * Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved. org.glassfish.jersey.server.internal.monitoring.core * License: Apache License, 2.0 * Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. * Copyright 2010-2013 Coda Hale and Yammer, Inc. W3.org documents * License: W3C License * Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ -------------------------------------------------------------------------------- Eclipse Jersey Core Common -------------------------------------------------------------------------------- # Notice for Jersey Core Common module This content is produced and maintained by the Eclipse Jersey project. * https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Google Guava Version 18.0 * License: Apache License, 2.0 * Copyright: (C) 2009 The Guava Authors JSR-166 Extension - JEP 266 * License: Creative Commons 1.0 (CC0) * No copyright * Written by Doug Lea with assistance from members of JCP JSR-166 * Expert Group and released to the public domain, as explained at * http://creativecommons.org/publicdomain/zero/1.0/ # Notice for Jersey This content is produced and maintained by the Eclipse Jersey project. * Project home: https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Angular JS, v1.6.6 * License MIT (http://www.opensource.org/licenses/mit-license.php) * Project: http://angularjs.org * Coyright: (c) 2010-2017 Google, Inc. aopalliance Version 1 * License: all the source code provided by AOP Alliance is Public Domain. * Project: http://aopalliance.sourceforge.net * Copyright: Material in the public domain is not protected by copyright Bean Validation API 3.0.2 * License: Apache License, 2.0 * Project: http://beanvalidation.org/1.1/ * Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors * by the @authors tag. Hibernate Validator CDI, 7.0.5.Final * License: Apache License, 2.0 * Project: https://beanvalidation.org/ * Repackaged in org.glassfish.jersey.server.validation.internal.hibernate Bootstrap v3.3.7 * License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE) * Project: http://getbootstrap.com * Copyright: 2011-2016 Twitter, Inc Google Guava Version 18.0 * License: Apache License, 2.0 * Copyright (C) 2009 The Guava Authors jakarta.inject Version: 1 * License: Apache License, 2.0 * Copyright (C) 2009 The JSR-330 Expert Group Javassist Version 3.29.2-GA * License: Apache License, 2.0 * Project: http://www.javassist.org/ * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. Jackson JAX-RS Providers Version 2.15.3 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers * Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. jQuery v1.12.4 * License: jquery.org/license * Project: jquery.org * Copyright: (c) jQuery Foundation jQuery Barcode plugin 0.3 * License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html) * Project: http://www.pasella.it/projects/jQuery/barcode * Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com JSR-166 Extension - JEP 266 * License: CC0 * No copyright * Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/ KineticJS, v4.7.1 * License: MIT license (http://www.opensource.org/licenses/mit-license.php) * Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS * Copyright: Eric Rowell org.objectweb.asm Version 9.6 * License: Modified BSD (https://asm.ow2.io/license.html) * Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved. org.osgi.core version 6.0.0 * License: Apache License, 2.0 * Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved. org.glassfish.jersey.server.internal.monitoring.core * License: Apache License, 2.0 * Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. * Copyright 2010-2013 Coda Hale and Yammer, Inc. W3.org documents * License: W3C License * Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ -------------------------------------------------------------------------------- Eclipse Jersey Bean Validation -------------------------------------------------------------------------------- # Notice for Jersey Bean Validation module This content is produced and maintained by the Eclipse Jersey project. * https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Hibernate Validator CDI, 7.0.5.Final * License: Apache License, 2.0 * Project: https://beanvalidation.org/ * Repackaged in org.glassfish.jersey.server.validation.internal.hibernate # Notice for Jersey This content is produced and maintained by the Eclipse Jersey project. * Project home: https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Angular JS, v1.6.6 * License MIT (http://www.opensource.org/licenses/mit-license.php) * Project: http://angularjs.org * Coyright: (c) 2010-2017 Google, Inc. aopalliance Version 1 * License: all the source code provided by AOP Alliance is Public Domain. * Project: http://aopalliance.sourceforge.net * Copyright: Material in the public domain is not protected by copyright Bean Validation API 3.0.2 * License: Apache License, 2.0 * Project: http://beanvalidation.org/1.1/ * Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors * by the @authors tag. Hibernate Validator CDI, 7.0.5.Final * License: Apache License, 2.0 * Project: https://beanvalidation.org/ * Repackaged in org.glassfish.jersey.server.validation.internal.hibernate Bootstrap v3.3.7 * License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE) * Project: http://getbootstrap.com * Copyright: 2011-2016 Twitter, Inc Google Guava Version 18.0 * License: Apache License, 2.0 * Copyright (C) 2009 The Guava Authors jakarta.inject Version: 1 * License: Apache License, 2.0 * Copyright (C) 2009 The JSR-330 Expert Group Javassist Version 3.29.2-GA * License: Apache License, 2.0 * Project: http://www.javassist.org/ * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. Jackson JAX-RS Providers Version 2.15.3 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers * Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. jQuery v1.12.4 * License: jquery.org/license * Project: jquery.org * Copyright: (c) jQuery Foundation jQuery Barcode plugin 0.3 * License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html) * Project: http://www.pasella.it/projects/jQuery/barcode * Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com JSR-166 Extension - JEP 266 * License: CC0 * No copyright * Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/ KineticJS, v4.7.1 * License: MIT license (http://www.opensource.org/licenses/mit-license.php) * Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS * Copyright: Eric Rowell org.objectweb.asm Version 9.6 * License: Modified BSD (https://asm.ow2.io/license.html) * Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved. org.osgi.core version 6.0.0 * License: Apache License, 2.0 * Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved. org.glassfish.jersey.server.internal.monitoring.core * License: Apache License, 2.0 * Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. * Copyright 2010-2013 Coda Hale and Yammer, Inc. W3.org documents * License: W3C License * Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ -------------------------------------------------------------------------------- Eclipse GlassFish HK2 (aopalliance-repackaged, hk2-api, hk2-locator, hk2-utils) -------------------------------------------------------------------------------- # Notices for Eclipse GlassFish This content is produced and maintained by the Eclipse GlassFish project. * Project home: https://projects.eclipse.org/projects/ee4j.glassfish ## Trademarks Eclipse GlassFish, and GlassFish are trademarks of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/glassfish-ha-api * https://github.com/eclipse-ee4j/glassfish-logging-annotation-processor * https://github.com/eclipse-ee4j/glassfish-shoal * https://github.com/eclipse-ee4j/glassfish-cdi-porting-tck * https://github.com/eclipse-ee4j/glassfish-jsftemplating * https://github.com/eclipse-ee4j/glassfish-hk2-extra * https://github.com/eclipse-ee4j/glassfish-hk2 * https://github.com/eclipse-ee4j/glassfish-fighterfish ## Third-party Content This project leverages the following third party content. None ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. -------------------------------------------------------------------------------- Eclipse Jetty Servlet API (jakarta-servlet-api 5.0.2) -------------------------------------------------------------------------------- # Notices for Eclipse Project for Servlet This content is produced and maintained by the Eclipse Project for Servlet project. * Project home: https://projects.eclipse.org/projects/ee4j.servlet ## Trademarks Eclipse Project for Servlet is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/servlet-api * https://github.com/eclipse/jetty.toolchain ## Third-party Content ## Jakarta The following artifacts are EPL 2.0 + GPLv2 with classpath exception. https://projects.eclipse.org/projects/ee4j.servlet * jakarta.servlet:jakarta.servlet-api ## GlassFish The following artifacts are CDDL + GPLv2 with classpath exception. https://glassfish.dev.java.net/nonav/public/CDDL+GPL.html * org.eclipse.jetty.toolchain:jetty-schemas -------------------------------------------------------------------------------- Jakarta XML Binding API (jakarta.xml.bind-api 3.0.x) -------------------------------------------------------------------------------- [//]: # " Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. " [//]: # " " [//]: # " This program and the accompanying materials are made available under the " [//]: # " terms of the Eclipse Distribution License v. 1.0, which is available at " [//]: # " http://www.eclipse.org/org/documents/edl-v10.php. " [//]: # " " [//]: # " SPDX-License-Identifier: BSD-3-Clause " # Notices for Jakarta XML Binding This content is produced and maintained by the Jakarta XML Binding project. * Project home: https://projects.eclipse.org/projects/ee4j.jaxb ## Trademarks Jakarta XML Binding is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Distribution License v. 1.0 which is available at http://www.eclipse.org/org/documents/edl-v10.php. SPDX-License-Identifier: BSD-3-Clause ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jaxb-api * https://github.com/eclipse-ee4j/jaxb-tck ## Third-party Content This project leverages the following third party content. Apache River (3.0.0) * License: Apache-2.0 AND BSD-3-Clause ASM 7 (n/a) * License: BSD-3-Clause * Project: https://asm.ow2.io/ * Source: https://repository.ow2.org/nexus/#nexus-search;gav~org.ow2.asm~asm-commons~~~~kw,versionexpand JTHarness (5.0) * License: (GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0) * Project: https://wiki.openjdk.java.net/display/CodeTools/JT+Harness * Source: http://hg.openjdk.java.net/code-tools/jtharness/ normalize.css (3.0.2) * License: MIT SigTest (n/a) * License: GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. -------------------------------------------------------------------------------- Jakarta RESTful Web Services API (jakarta.ws.rs-api 3.0.x / 3.1.0) -------------------------------------------------------------------------------- # Notices for Jakarta RESTful Web Services This content is produced and maintained by the **Jakarta RESTful Web Services** project. * Project home: https://projects.eclipse.org/projects/ee4j.jaxrs ## Trademarks **Jakarta RESTful Web Services** is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jaxrs-api ## Third-party Content This project leverages the following third party content. javaee-api (7.0) * License: Apache-2.0 AND W3C JUnit (4.11) * License: Common Public License 1.0 Mockito (2.16.0) * Project: http://site.mockito.org * Source: https://github.com/mockito/mockito/releases/tag/v2.16.0 ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. -------------------------------------------------------------------------------- Jakarta Expression Language API (jakarta.el-api 4.0.0, glassfish jakarta.el 4.0.2) -------------------------------------------------------------------------------- # Notices for Jakarta Expression Language This content is produced and maintained by the Jakarta Expression Language project. * Project home: https://projects.eclipse.org/projects/ee4j.el ## Trademarks Jakarta Expression Language is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/el-ri ## Third-party Content ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. -------------------------------------------------------------------------------- Jakarta Annotations API (jakarta.annotation-api 2.1.1 and 3.0.0) -------------------------------------------------------------------------------- # Notices for Jakarta Annotations This content is produced and maintained by the Jakarta Annotations project. * Project home: https://projects.eclipse.org/projects/ee4j.ca ## Trademarks Jakarta Annotations is a trademark of the Eclipse Foundation. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/common-annotations-api ## Third-party Content ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Notices for Jakarta Annotations This content is produced and maintained by the Jakarta Annotations project. * Project home: https://projects.eclipse.org/projects/ee4j.ca ## Trademarks Jakarta Annotations™ is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at https://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GPL-2.0 with Classpath-exception-2.0 which is available at https://openjdk.java.net/legal/gplv2+ce.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/jakartaee/common-annotations-api ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. -------------------------------------------------------------------------------- Jakarta Inject API (jakarta.inject-api 2.0.1) -------------------------------------------------------------------------------- # Notices for Eclipse Jakarta Dependency Injection This content is produced and maintained by the Eclipse Jakarta Dependency Injection project. * Project home: https://projects.eclipse.org/projects/cdi.batch ## Trademarks Jakarta Dependency Injection is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Apache License, Version 2.0 which is available at https://www.apache.org/licenses/LICENSE-2.0. SPDX-License-Identifier: Apache-2.0 ## Source Code The project maintains the following source code repositories: https://github.com/eclipse-ee4j/injection-api https://github.com/eclipse-ee4j/injection-spec https://github.com/eclipse-ee4j/injection-tck ## Third-party Content This project leverages the following third party content. None ## Cryptography None -------------------------------------------------------------------------------- Jakarta Activation (jakarta.activation 2.0.0, 2.0.1, jakarta.activation-api 1.2.1, 2.1.0) -------------------------------------------------------------------------------- # Notices for Eclipse Project for JAF This content is produced and maintained by the Eclipse Project for JAF project. * Project home: https://projects.eclipse.org/projects/ee4j.jaf ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Distribution License v. 1.0, which is available at http://www.eclipse.org/org/documents/edl-v10.php. SPDX-License-Identifier: BSD-3-Clause ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jaf ## Third-party Content This project leverages the following third party content. JUnit (4.12) * License: Eclipse Public License - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Notices for Jakarta Activation This content is produced and maintained by Jakarta Activation project. * Project home: https://projects.eclipse.org/projects/ee4j.jaf ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Distribution License v. 1.0, which is available at http://www.eclipse.org/org/documents/edl-v10.php. SPDX-License-Identifier: BSD-3-Clause ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jaf ## Third-party Content This project leverages the following third party content. JUnit (4.12) * License: Eclipse Public License - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Notices for Jakarta Activation This content is produced and maintained by Jakarta Activation project. * Project home: https://projects.eclipse.org/projects/ee4j.jaf ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Distribution License v. 1.0, which is available at http://www.eclipse.org/org/documents/edl-v10.php. SPDX-License-Identifier: BSD-3-Clause ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jaf ================================================ FILE: config-service/build.sbt ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. import scala.collection.Seq name := "config-service" enablePlugins(JavaAppPackaging) // Ship LICENSE-binary, NOTICE-binary, DISCLAIMER, and the licenses/ // directory at the top of the Universal dist zip. // See project/AddMetaInfLicenseFiles.scala. Universal / mappings := AddMetaInfLicenseFiles.distMappings( (Universal / mappings).value, (ThisBuild / baseDirectory).value, baseDirectory.value / "LICENSE-binary", baseDirectory.value / "NOTICE-binary" ) // Enable semanticdb for Scalafix ThisBuild / semanticdbEnabled := true ThisBuild / semanticdbVersion := scalafixSemanticdb.revision // Manage dependency conflicts by always using the latest revision ThisBuild / conflictManager := ConflictManager.latestRevision // Restrict parallel execution of tests to avoid conflicts Global / concurrentRestrictions += Tags.limit(Tags.Test, 1) ///////////////////////////////////////////////////////////////////////////// // Compiler Options ///////////////////////////////////////////////////////////////////////////// // Scala compiler options Compile / scalacOptions ++= Seq( "-Xelide-below", "WARNING", // Turn on optimizations with "WARNING" as the threshold "-feature", // Check feature warnings "-deprecation", // Check deprecation warnings "-Ywarn-unused:imports" // Check for unused imports ) ///////////////////////////////////////////////////////////////////////////// // Version Variables ///////////////////////////////////////////////////////////////////////////// val dropwizardVersion = "4.0.7" val mockitoVersion = "5.4.0" val assertjVersion = "3.24.2" ///////////////////////////////////////////////////////////////////////////// // Test-related Dependencies ///////////////////////////////////////////////////////////////////////////// libraryDependencies ++= Seq( "org.scalamock" %% "scalamock" % "5.2.0" % Test, // ScalaMock "org.scalatest" %% "scalatest" % "3.2.17" % Test, // ScalaTest "io.dropwizard" % "dropwizard-testing" % dropwizardVersion % Test, // Dropwizard Testing "org.mockito" % "mockito-core" % mockitoVersion % Test, // Mockito for mocking "org.assertj" % "assertj-core" % assertjVersion % Test, // AssertJ for assertions "com.novocode" % "junit-interface" % "0.11" % Test // SBT interface for JUnit ) ///////////////////////////////////////////////////////////////////////////// // Dependencies ///////////////////////////////////////////////////////////////////////////// // Core Dependencies libraryDependencies ++= Seq( "io.dropwizard" % "dropwizard-core" % dropwizardVersion, "io.dropwizard" % "dropwizard-auth" % dropwizardVersion, // Dropwizard Authentication module "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.18.6", "jakarta.ws.rs" % "jakarta.ws.rs-api" % "3.1.0", // Ensure Jakarta JAX-RS API is available "org.bitbucket.b_c" % "jose4j" % "0.9.6", "org.playframework" %% "play-json" % "3.1.0-M1", "com.typesafe" % "config" % "1.4.6" // For configuration management ) ================================================ FILE: config-service/src/main/resources/config-service-web-config.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. server: applicationConnectors: - type: http port: 9094 adminConnectors: [] requestLog: type: classic appenders: [] logging: level: ${TEXERA_SERVICE_LOG_LEVEL:-INFO} appenders: - type: console threshold: ${TEXERA_SERVICE_LOG_LEVEL:-INFO} - type: file currentLogFilename: logs/config-service.log archive: true archivedLogFilenamePattern: logs/config-service-%d.log.gz archivedFileCount: 5 ================================================ FILE: config-service/src/main/scala/org/apache/texera/service/ConfigService.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service import com.fasterxml.jackson.module.scala.DefaultScalaModule import com.typesafe.scalalogging.LazyLogging import io.dropwizard.auth.AuthDynamicFeature import io.dropwizard.configuration.{EnvironmentVariableSubstitutor, SubstitutingSourceProvider} import io.dropwizard.core.Application import io.dropwizard.core.setup.{Bootstrap, Environment} import org.apache.texera.amber.config.StorageConfig import org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser} import org.apache.texera.config.DefaultsConfig import org.apache.texera.dao.SqlServer import org.apache.texera.service.resource.{ConfigResource, HealthCheckResource} import org.eclipse.jetty.server.session.SessionHandler import org.jooq.impl.DSL import java.nio.file.Path class ConfigService extends Application[ConfigServiceConfiguration] with LazyLogging { override def initialize(bootstrap: Bootstrap[ConfigServiceConfiguration]): Unit = { // enable environment variable substitution in YAML config bootstrap.setConfigurationSourceProvider( new SubstitutingSourceProvider( bootstrap.getConfigurationSourceProvider, new EnvironmentVariableSubstitutor(false) ) ) // Register Scala module to Dropwizard default object mapper bootstrap.getObjectMapper.registerModule(DefaultScalaModule) SqlServer.initConnection( StorageConfig.jdbcUrl, StorageConfig.jdbcUsername, StorageConfig.jdbcPassword ) } override def run(configuration: ConfigServiceConfiguration, environment: Environment): Unit = { // Serve backend at /api environment.jersey.setUrlPattern("/api/*") environment.jersey.register(classOf[SessionHandler]) environment.servlets.setSessionHandler(new SessionHandler) environment.jersey.register(classOf[HealthCheckResource]) // Register JWT authentication filter environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter])) // Enable @Auth annotation for injecting SessionUser environment.jersey.register( new io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser]) ) environment.jersey.register(new ConfigResource) // Preload default.conf into site_setting tables try { val ctx = SqlServer.getInstance().createDSLContext() SqlServer.withTransaction(ctx) { tx => if (DefaultsConfig.reinit) { tx.deleteFrom(DSL.table("site_settings")).execute() } DefaultsConfig.allDefaults.foreach { case (key, value) => tx .insertInto(DSL.table("site_settings")) .columns( DSL.field("key"), DSL.field("value"), DSL.field("updated_by"), DSL.field("updated_at") ) .values(key, value, "texera", DSL.currentTimestamp()) .onDuplicateKeyIgnore() .execute() } } } catch { case ex: Exception => logger.error("Failed to preload default settings", ex) throw ex } // Route request logs through SLF4J, controlled by TEXERA_SERVICE_LOG_LEVEL RequestLoggingFilter.register(environment.getApplicationContext) } } object ConfigService { def main(args: Array[String]): Unit = { val configFilePath = Path .of(sys.env.getOrElse("TEXERA_HOME", ".")) .resolve("config-service") .resolve("src") .resolve("main") .resolve("resources") .resolve("config-service-web-config.yaml") .toAbsolutePath .toString // Start the Dropwizard application new ConfigService().run("server", configFilePath) } } ================================================ FILE: config-service/src/main/scala/org/apache/texera/service/ConfigServiceConfiguration.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service import io.dropwizard.core.Configuration class ConfigServiceConfiguration extends Configuration {} ================================================ FILE: config-service/src/main/scala/org/apache/texera/service/resource/ConfigResource.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.resource import jakarta.annotation.security.RolesAllowed import jakarta.ws.rs.core.MediaType import jakarta.ws.rs.{GET, Path, Produces} import org.apache.texera.config.{AuthConfig, ComputingUnitConfig, GuiConfig, UserSystemConfig} @Path("/config") @Produces(Array(MediaType.APPLICATION_JSON)) class ConfigResource { @GET @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/gui") def getGuiConfig: Map[String, Any] = Map( // flags from the gui.conf "exportExecutionResultEnabled" -> GuiConfig.guiWorkflowWorkspaceExportExecutionResultEnabled, "autoAttributeCorrectionEnabled" -> GuiConfig.guiWorkflowWorkspaceAutoAttributeCorrectionEnabled, "selectingFilesFromDatasetsEnabled" -> GuiConfig.guiWorkflowWorkspaceSelectingFilesFromDatasetsEnabled, "localLogin" -> GuiConfig.guiLoginLocalLogin, "googleLogin" -> GuiConfig.guiLoginGoogleLogin, "userPresetEnabled" -> GuiConfig.guiWorkflowWorkspaceUserPresetEnabled, "workflowExecutionsTrackingEnabled" -> GuiConfig.guiWorkflowWorkspaceWorkflowExecutionsTrackingEnabled, "linkBreakpointEnabled" -> GuiConfig.guiWorkflowWorkspaceLinkBreakpointEnabled, "asyncRenderingEnabled" -> GuiConfig.guiWorkflowWorkspaceAsyncRenderingEnabled, "timetravelEnabled" -> GuiConfig.guiWorkflowWorkspaceTimetravelEnabled, "productionSharedEditingServer" -> GuiConfig.guiWorkflowWorkspaceProductionSharedEditingServer, "defaultDataTransferBatchSize" -> GuiConfig.guiWorkflowWorkspaceDefaultDataTransferBatchSize, "defaultExecutionMode" -> GuiConfig.guiWorkflowWorkspaceDefaultExecutionMode, "workflowEmailNotificationEnabled" -> GuiConfig.guiWorkflowWorkspaceWorkflowEmailNotificationEnabled, "sharingComputingUnitEnabled" -> ComputingUnitConfig.sharingComputingUnitEnabled, "operatorConsoleMessageBufferSize" -> GuiConfig.guiWorkflowWorkspaceOperatorConsoleMessageBufferSize, "pythonLanguageServerPort" -> GuiConfig.guiWorkflowWorkspacePythonLanguageServerPort, "defaultLocalUser" -> Map( "username" -> GuiConfig.guiLoginDefaultLocalUserUsername, "password" -> GuiConfig.guiLoginDefaultLocalUserPassword ), "activeTimeInMinutes" -> GuiConfig.guiWorkflowWorkspaceActiveTimeInMinutes, "copilotEnabled" -> GuiConfig.guiWorkflowWorkspaceCopilotEnabled, "limitColumns" -> GuiConfig.guiWorkflowWorkspaceLimitColumns, // flags from the auth.conf if needed "expirationTimeInMinutes" -> AuthConfig.jwtExpirationMinutes ) @GET @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/user-system") def getUserSystemConfig: Map[String, Any] = Map( // flags from the user-system.conf "inviteOnly" -> UserSystemConfig.inviteOnly ) } ================================================ FILE: config-service/src/main/scala/org/apache/texera/service/resource/HealthCheckResource.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.resource import jakarta.ws.rs.core.MediaType import jakarta.ws.rs.{GET, Path, Produces} @Path("/healthcheck") @Produces(Array(MediaType.APPLICATION_JSON)) class HealthCheckResource { @GET def healthCheck: Map[String, String] = Map("status" -> "ok") } ================================================ FILE: file-service/LICENSE-binary ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Support. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or support. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================================ THIRD-PARTY COMPONENTS ================================================================================ Apache Texera's binary distribution of this service includes the following third-party components, grouped by license. Each section references licenses/ for the full text of the applicable license. Components under the Apache License, Version 2.0 are governed by the same license terms as Apache Texera itself and are listed for completeness. Locations within the distribution: - Scala/Java jars listed below ship under the lib/ directory of this service's Universal zip. - Source files derived from third-party projects are compiled into the bundled jars under lib/ and live at the listed paths in the Apache Texera source tree. -------------------------------------------------------------------------------- Dependencies under the Apache License, Version 2.0 -------------------------------------------------------------------------------- Scala/Java jars: - com.fasterxml.classmate-1.7.0.jar - com.fasterxml.jackson.core.jackson-annotations-2.18.6.jar - com.fasterxml.jackson.core.jackson-core-2.18.6.jar - com.fasterxml.jackson.core.jackson-databind-2.18.6.jar - com.fasterxml.jackson.dataformat.jackson-dataformat-yaml-2.16.1.jar - com.fasterxml.jackson.datatype.jackson-datatype-guava-2.16.1.jar - com.fasterxml.jackson.datatype.jackson-datatype-jdk8-2.16.1.jar - com.fasterxml.jackson.datatype.jackson-datatype-jsr310-2.16.1.jar - com.fasterxml.jackson.jakarta.rs.jackson-jakarta-rs-base-2.16.1.jar - com.fasterxml.jackson.jakarta.rs.jackson-jakarta-rs-json-provider-2.16.1.jar - com.fasterxml.jackson.jaxrs.jackson-jaxrs-base-2.10.5.jar - com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider-2.10.5.jar - com.fasterxml.jackson.module.jackson-module-blackbird-2.16.1.jar - com.fasterxml.jackson.module.jackson-module-jakarta-xmlbind-annotations-2.16.1.jar - com.fasterxml.jackson.module.jackson-module-jsonSchema-2.18.6.jar - com.fasterxml.jackson.module.jackson-module-no-ctor-deser-2.18.6.jar - com.fasterxml.jackson.module.jackson-module-parameter-names-2.16.1.jar - com.fasterxml.jackson.module.jackson-module-scala_2.13-2.18.6.jar - com.fasterxml.woodstox.woodstox-core-5.3.0.jar - com.github.ben-manes.caffeine.caffeine-3.1.8.jar - com.github.sisyphsu.dateparser-1.0.11.jar - com.github.sisyphsu.retree-1.0.4.jar - com.github.stephenc.jcip.jcip-annotations-1.0-1.jar - com.google.android.annotations-4.1.1.4.jar - com.google.api.grpc.proto-google-common-protos-2.22.0.jar - com.google.code.findbugs.jsr305-3.0.2.jar - com.google.code.gson.gson-2.10.1.jar - com.google.errorprone.error_prone_annotations-2.25.0.jar - com.google.flatbuffers.flatbuffers-java-23.5.26.jar - com.google.guava.failureaccess-1.0.2.jar - com.google.guava.guava-33.0.0-jre.jar - com.google.guava.listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar - com.google.inject.extensions.guice-servlet-4.0.jar - com.google.inject.guice-4.0.jar - com.google.j2objc.j2objc-annotations-2.8.jar - com.googlecode.javaewah.JavaEWAH-1.1.12.jar - com.helger.profiler-1.1.1.jar - com.nimbusds.nimbus-jose-jwt-9.8.1.jar - com.squareup.okhttp.okhttp-2.7.5.jar - com.squareup.okhttp3.logging-interceptor-4.12.0.jar - com.squareup.okhttp3.okhttp-4.12.0.jar - com.squareup.okio.okio-3.6.0.jar - com.squareup.okio.okio-jvm-3.6.0.jar - com.thesamet.scalapb.lenses_2.13-0.11.20.jar - com.thesamet.scalapb.scalapb-json4s_2.13-0.12.0.jar - com.thesamet.scalapb.scalapb-runtime_2.13-0.11.20.jar - com.typesafe.config-1.4.6.jar - com.typesafe.scala-logging.scala-logging_2.13-3.9.5.jar - commons-beanutils.commons-beanutils-1.9.4.jar - commons-cli.commons-cli-1.2.jar - commons-codec.commons-codec-1.17.1.jar - commons-collections.commons-collections-3.2.2.jar - commons-io.commons-io-2.16.1.jar - commons-logging.commons-logging-1.2.jar - commons-net.commons-net-3.6.jar - commons-pool.commons-pool-1.6.jar - dev.failsafe.failsafe-3.3.2.jar - io.airlift.aircompressor-0.27.jar - io.dropwizard.dropwizard-auth-4.0.7.jar - io.dropwizard.dropwizard-configuration-4.0.7.jar - io.dropwizard.dropwizard-core-4.0.7.jar - io.dropwizard.dropwizard-health-4.0.7.jar - io.dropwizard.dropwizard-jackson-4.0.7.jar - io.dropwizard.dropwizard-jersey-4.0.7.jar - io.dropwizard.dropwizard-jetty-4.0.7.jar - io.dropwizard.dropwizard-lifecycle-4.0.7.jar - io.dropwizard.dropwizard-logging-4.0.7.jar - io.dropwizard.dropwizard-metrics-4.0.7.jar - io.dropwizard.dropwizard-request-logging-4.0.7.jar - io.dropwizard.dropwizard-servlets-4.0.7.jar - io.dropwizard.dropwizard-util-4.0.7.jar - io.dropwizard.dropwizard-validation-4.0.7.jar - io.dropwizard.logback.logback-throttling-appender-1.4.2.jar - io.dropwizard.metrics.metrics-annotation-4.2.25.jar - io.dropwizard.metrics.metrics-caffeine-4.2.25.jar - io.dropwizard.metrics.metrics-core-4.2.25.jar - io.dropwizard.metrics.metrics-healthchecks-4.2.25.jar - io.dropwizard.metrics.metrics-jakarta-servlets-4.2.25.jar - io.dropwizard.metrics.metrics-jersey3-4.2.25.jar - io.dropwizard.metrics.metrics-jetty11-4.2.25.jar - io.dropwizard.metrics.metrics-jmx-4.2.25.jar - io.dropwizard.metrics.metrics-json-4.2.25.jar - io.dropwizard.metrics.metrics-jvm-4.2.25.jar - io.dropwizard.metrics.metrics-logback-4.2.25.jar - io.grpc.grpc-api-1.60.0.jar - io.grpc.grpc-context-1.60.0.jar - io.grpc.grpc-core-1.60.0.jar - io.grpc.grpc-netty-1.60.0.jar - io.grpc.grpc-protobuf-1.60.0.jar - io.grpc.grpc-protobuf-lite-1.60.0.jar - io.grpc.grpc-stub-1.60.0.jar - io.grpc.grpc-util-1.60.0.jar - io.gsonfire.gson-fire-1.8.5.jar - io.lakefs.sdk-1.51.0.jar - io.netty.netty-3.10.6.Final.jar - io.netty.netty-buffer-4.1.104.Final.jar - io.netty.netty-codec-4.1.104.Final.jar - io.netty.netty-codec-http-4.1.100.Final.jar - io.netty.netty-codec-http2-4.1.100.Final.jar - io.netty.netty-codec-socks-4.1.100.Final.jar - io.netty.netty-common-4.1.104.Final.jar - io.netty.netty-handler-4.1.104.Final.jar - io.netty.netty-handler-proxy-4.1.100.Final.jar - io.netty.netty-resolver-4.1.104.Final.jar - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-linux-aarch_64.jar - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-linux-x86_64.jar - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-osx-aarch_64.jar - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-osx-x86_64.jar - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-windows-x86_64.jar - io.netty.netty-tcnative-boringssl-static-2.0.61.Final.jar - io.netty.netty-tcnative-classes-2.0.61.Final.jar - io.netty.netty-transport-4.1.104.Final.jar - io.netty.netty-transport-native-unix-common-4.1.104.Final.jar - io.perfmark.perfmark-api-0.26.0.jar - io.r2dbc.r2dbc-spi-0.9.0.RELEASE.jar - jakarta.inject.jakarta.inject-api-2.0.1.jar - jakarta.validation.jakarta.validation-api-3.0.2.jar - javax.inject.javax.inject-1.jar - javax.validation.validation-api-2.0.1.Final.jar - log4j.log4j-1.2.17.jar - net.minidev.accessors-smart-2.4.2.jar - net.minidev.json-smart-2.4.2.jar - org.apache.arrow.arrow-format-15.0.2.jar - org.apache.arrow.arrow-memory-core-15.0.2.jar - org.apache.arrow.arrow-memory-netty-15.0.2.jar - org.apache.arrow.arrow-vector-15.0.2.jar - org.apache.arrow.flight-core-15.0.2.jar - org.apache.arrow.flight-grpc-15.0.2.jar - org.apache.avro.avro-1.12.0.jar - org.apache.commons.commons-compress-1.26.2.jar - org.apache.commons.commons-configuration2-2.1.1.jar - org.apache.commons.commons-jcs3-core-3.2.jar - org.apache.commons.commons-lang3-3.14.0.jar - org.apache.commons.commons-math3-3.1.1.jar - org.apache.commons.commons-text-1.11.0.jar - org.apache.commons.commons-vfs2-2.9.0.jar - org.apache.curator.curator-client-4.2.0.jar - org.apache.curator.curator-framework-4.2.0.jar - org.apache.curator.curator-recipes-4.2.0.jar - org.apache.hadoop.hadoop-annotations-3.3.1.jar - org.apache.hadoop.hadoop-auth-3.3.1.jar - org.apache.hadoop.hadoop-common-3.3.1.jar - org.apache.hadoop.hadoop-hdfs-client-3.3.1.jar - org.apache.hadoop.hadoop-mapreduce-client-core-3.3.1.jar - org.apache.hadoop.hadoop-yarn-api-3.3.1.jar - org.apache.hadoop.hadoop-yarn-client-3.3.1.jar - org.apache.hadoop.hadoop-yarn-common-3.3.1.jar - org.apache.hadoop.thirdparty.hadoop-shaded-guava-1.1.1.jar - org.apache.hadoop.thirdparty.hadoop-shaded-protobuf_3_7-1.1.1.jar - org.apache.htrace.htrace-core4-4.1.0-incubating.jar - org.apache.httpcomponents.client5.httpclient5-5.4.jar - org.apache.httpcomponents.core5.httpcore5-5.3.jar - org.apache.httpcomponents.core5.httpcore5-h2-5.3.jar - org.apache.httpcomponents.httpclient-4.5.13.jar - org.apache.httpcomponents.httpcore-4.4.16.jar - org.apache.iceberg.iceberg-api-1.7.1.jar - org.apache.iceberg.iceberg-aws-1.7.1.jar - org.apache.iceberg.iceberg-bundled-guava-1.7.1.jar - org.apache.iceberg.iceberg-common-1.7.1.jar - org.apache.iceberg.iceberg-core-1.7.1.jar - org.apache.iceberg.iceberg-data-1.7.1.jar - org.apache.iceberg.iceberg-parquet-1.7.1.jar - org.apache.kerby.kerb-admin-1.0.1.jar - org.apache.kerby.kerb-client-1.0.1.jar - org.apache.kerby.kerb-common-1.0.1.jar - org.apache.kerby.kerb-core-1.0.1.jar - org.apache.kerby.kerb-crypto-1.0.1.jar - org.apache.kerby.kerb-identity-1.0.1.jar - org.apache.kerby.kerb-server-1.0.1.jar - org.apache.kerby.kerb-simplekdc-1.0.1.jar - org.apache.kerby.kerb-util-1.0.1.jar - org.apache.kerby.kerby-asn1-1.0.1.jar - org.apache.kerby.kerby-config-1.0.1.jar - org.apache.kerby.kerby-pkix-1.0.1.jar - org.apache.kerby.kerby-util-1.0.1.jar - org.apache.kerby.kerby-xdr-1.0.1.jar - org.apache.kerby.token-provider-1.0.1.jar - org.apache.orc.orc-core-1.9.4-nohive.jar - org.apache.orc.orc-shims-1.9.4.jar - org.apache.parquet.parquet-avro-1.13.1.jar - org.apache.parquet.parquet-column-1.13.1.jar - org.apache.parquet.parquet-common-1.13.1.jar - org.apache.parquet.parquet-encoding-1.13.1.jar - org.apache.parquet.parquet-format-structures-1.13.1.jar - org.apache.parquet.parquet-hadoop-1.13.1.jar - org.apache.parquet.parquet-jackson-1.13.1.jar - org.apache.yetus.audience-annotations-0.13.0.jar - org.apache.zookeeper.zookeeper-3.5.6.jar - org.apache.zookeeper.zookeeper-jute-3.5.6.jar - org.bitbucket.b_c.jose4j-0.9.6.jar - org.eclipse.jetty.jetty-http-11.0.20.jar - org.eclipse.jetty.jetty-io-11.0.20.jar - org.eclipse.jetty.jetty-security-11.0.20.jar - org.eclipse.jetty.jetty-server-11.0.20.jar - org.eclipse.jetty.jetty-servlet-11.0.20.jar - org.eclipse.jetty.jetty-servlets-11.0.20.jar - org.eclipse.jetty.jetty-util-11.0.20.jar - org.eclipse.jetty.toolchain.jetty-jakarta-servlet-api-5.0.2.jar - org.eclipse.jetty.toolchain.setuid.jetty-setuid-java-1.0.4.jar - org.eclipse.jetty.websocket.websocket-api-9.4.40.v20210413.jar - org.eclipse.jetty.websocket.websocket-client-9.4.40.v20210413.jar - org.eclipse.jetty.websocket.websocket-common-9.4.40.v20210413.jar - org.ehcache.sizeof-0.4.3.jar - org.hibernate.validator.hibernate-validator-7.0.5.Final.jar - org.javassist.javassist-3.30.2-GA.jar - org.jboss.logging.jboss-logging-3.5.3.Final.jar - org.jetbrains.annotations-17.0.0.jar - org.jetbrains.kotlin.kotlin-stdlib-1.9.10.jar - org.jetbrains.kotlin.kotlin-stdlib-common-1.9.10.jar - org.jetbrains.kotlin.kotlin-stdlib-jdk7-1.9.10.jar - org.jetbrains.kotlin.kotlin-stdlib-jdk8-1.9.10.jar - org.jheaps.jheaps-0.11.jar - org.jooq.jooq-3.16.23.jar - org.json4s.json4s-ast_2.13-4.0.1.jar - org.json4s.json4s-jackson-core_2.13-4.0.1.jar - org.openapitools.jackson-databind-nullable-0.2.6.jar - org.playframework.play-functional_2.13-3.1.0-M1.jar - org.playframework.play-json_2.13-3.1.0-M1.jar - org.roaringbitmap.RoaringBitmap-1.3.0.jar - org.scala-lang.modules.scala-collection-compat_2.13-2.13.0.jar - org.scala-lang.scala-library-2.13.18.jar - org.scala-lang.scala-reflect-2.13.18.jar - org.slf4j.jcl-over-slf4j-2.0.12.jar - org.slf4j.log4j-over-slf4j-2.0.12.jar - org.xerial.snappy.snappy-java-1.1.8.3.jar - org.yaml.snakeyaml-2.2.jar - software.amazon.awssdk.annotations-2.29.51.jar - software.amazon.awssdk.apache-client-2.29.51.jar - software.amazon.awssdk.arns-2.29.51.jar - software.amazon.awssdk.auth-2.29.51.jar - software.amazon.awssdk.aws-core-2.29.51.jar - software.amazon.awssdk.aws-query-protocol-2.29.51.jar - software.amazon.awssdk.aws-xml-protocol-2.29.51.jar - software.amazon.awssdk.checksums-2.29.51.jar - software.amazon.awssdk.checksums-spi-2.29.51.jar - software.amazon.awssdk.crt-core-2.29.51.jar - software.amazon.awssdk.endpoints-spi-2.29.51.jar - software.amazon.awssdk.http-auth-2.29.51.jar - software.amazon.awssdk.http-auth-aws-2.29.51.jar - software.amazon.awssdk.http-auth-aws-eventstream-2.29.51.jar - software.amazon.awssdk.http-auth-spi-2.29.51.jar - software.amazon.awssdk.http-client-spi-2.29.51.jar - software.amazon.awssdk.identity-spi-2.29.51.jar - software.amazon.awssdk.json-utils-2.29.51.jar - software.amazon.awssdk.metrics-spi-2.29.51.jar - software.amazon.awssdk.netty-nio-client-2.29.51.jar - software.amazon.awssdk.profiles-2.29.51.jar - software.amazon.awssdk.protocol-core-2.29.51.jar - software.amazon.awssdk.regions-2.29.51.jar - software.amazon.awssdk.retries-2.29.51.jar - software.amazon.awssdk.retries-spi-2.29.51.jar - software.amazon.awssdk.s3-2.29.51.jar - software.amazon.awssdk.sdk-core-2.29.51.jar - software.amazon.awssdk.sts-2.29.51.jar - software.amazon.awssdk.third-party-jackson-core-2.29.51.jar - software.amazon.awssdk.utils-2.29.51.jar - software.amazon.eventstream.eventstream-1.0.1.jar -------------------------------------------------------------------------------- Dependencies under the MIT License -------------------------------------------------------------------------------- Source files derived from third-party MIT-licensed projects: - mbknor-jackson-jsonschema common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaDraft.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaArrayWithUniqueItems.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaBool.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDefault.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDescription.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaExamples.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaFormat.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInject.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInt.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaOptions.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaString.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaTitle.java https://github.com/mbknor/mbknor-jackson-jsonschema Scala/Java jars: - net.sourceforge.argparse4j.argparse4j-0.9.0.jar - org.checkerframework.checker-qual-3.52.0.jar - org.codehaus.mojo.animal-sniffer-annotations-1.23.jar - org.projectlombok.lombok-1.18.24.jar - org.reactivestreams.reactive-streams-1.0.4.jar - org.slf4j.jul-to-slf4j-2.0.12.jar - org.slf4j.slf4j-api-2.0.16.jar -------------------------------------------------------------------------------- Dependencies under the BSD 3-Clause License -------------------------------------------------------------------------------- Scala/Java jars: - com.google.protobuf.protobuf-java-3.25.8.jar - com.google.re2j.re2j-1.1.jar - com.jcraft.jsch-0.1.55.jar - com.thoughtworks.paranamer.paranamer-2.8.jar - org.jline.jline-3.9.0.jar - org.ow2.asm.asm-8.0.1.jar - org.threeten.threeten-extra-1.7.1.jar -------------------------------------------------------------------------------- Dependencies under the BSD 2-Clause License -------------------------------------------------------------------------------- Scala/Java jars: - com.github.luben.zstd-jni-1.5.0-1.jar - dnsjava.dnsjava-2.1.7.jar - org.codehaus.woodstox.stax2-api-4.2.1.jar - org.postgresql.postgresql-42.7.10.jar -------------------------------------------------------------------------------- Dependencies under the Eclipse Public License, Version 2.0 (some are dual licensed with GPL-2.0 with Classpath Exception) -------------------------------------------------------------------------------- Scala/Java jars: - jakarta.annotation.jakarta.annotation-api-2.1.1.jar - jakarta.el.jakarta.el-api-4.0.0.jar - jakarta.servlet.jakarta.servlet-api-5.0.0.jar - jakarta.ws.rs.jakarta.ws.rs-api-3.1.0.jar - javax.ws.rs.javax.ws.rs-api-2.1.1.jar - org.glassfish.hk2.external.aopalliance-repackaged-3.0.6.jar - org.glassfish.hk2.hk2-api-3.0.6.jar - org.glassfish.hk2.hk2-locator-3.0.3.jar - org.glassfish.hk2.hk2-utils-3.0.6.jar - org.glassfish.hk2.osgi-resource-locator-1.0.3.jar - org.glassfish.jakarta.el-4.0.2.jar - org.glassfish.jersey.containers.jersey-container-servlet-3.0.12.jar - org.glassfish.jersey.containers.jersey-container-servlet-core-3.0.12.jar - org.glassfish.jersey.core.jersey-client-3.0.12.jar - org.glassfish.jersey.core.jersey-common-3.0.12.jar - org.glassfish.jersey.core.jersey-server-3.0.12.jar - org.glassfish.jersey.ext.jersey-bean-validation-3.0.12.jar - org.glassfish.jersey.ext.jersey-metainf-services-3.0.12.jar - org.glassfish.jersey.inject.jersey-hk2-3.0.12.jar - org.jgrapht.jgrapht-core-1.4.0.jar -------------------------------------------------------------------------------- Dependencies under the Eclipse Public License, Version 1.0 (Logback is dual licensed with LGPL-2.1) -------------------------------------------------------------------------------- Scala/Java jars: - ch.qos.logback.logback-access-1.4.14.jar - ch.qos.logback.logback-classic-1.4.14.jar - ch.qos.logback.logback-core-1.4.14.jar -------------------------------------------------------------------------------- Dependencies under the Common Development and Distribution License (CDDL) (some are dual licensed with GPL-2.0 with Classpath Exception) -------------------------------------------------------------------------------- CDDL 1.0 ~~~~~~~~ Scala/Java jars: - javax.annotation.javax.annotation-api-1.3.2.jar - javax.servlet.javax.servlet-api-3.1.0.jar - javax.ws.rs.jsr311-api-1.1.1.jar CDDL 1.1 ~~~~~~~~ Scala/Java jars: - com.sun.jersey.contribs.jersey-guice-1.19.jar -------------------------------------------------------------------------------- Dependencies under the Eclipse Distribution License, Version 1.0 -------------------------------------------------------------------------------- Scala/Java jars: - com.sun.activation.jakarta.activation-2.0.1.jar - jakarta.activation.jakarta.activation-api-2.1.0.jar - jakarta.xml.bind.jakarta.xml.bind-api-3.0.1.jar - org.eclipse.collections.eclipse-collections-11.1.0.jar - org.eclipse.collections.eclipse-collections-api-11.1.0.jar - org.eclipse.jgit.org.eclipse.jgit-5.13.0.202109080827-r.jar -------------------------------------------------------------------------------- Dependencies in the Public Domain (CC0) -------------------------------------------------------------------------------- Scala/Java jars: - aopalliance.aopalliance-1.0.jar Individual jars may contain their own META-INF/LICENSE and META-INF/NOTICE files that apply to their specific contents; those files continue to govern the use of those components. ================================================ FILE: file-service/NOTICE-binary ================================================ Apache Texera (Incubating) Copyright 2025-2026 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). -------------------------------------------------------------------------------- Apache Hadoop -------------------------------------------------------------------------------- Apache Hadoop Copyright 2006 and onwards The Apache Software Foundation. Export Control Notice --------------------- This distribution includes cryptographic software. The country in which you currently reside may have restrictions on the import, possession, use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. See for more information. The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this software as Export Commodity Control Number (ECCN) 5D002.C.1, which includes information security software using or performing cryptographic functions with asymmetric algorithms. The form and manner of this Apache Software Foundation distribution makes it eligible for export under the License Exception ENC Technology Software Unrestricted (TSU) exception (see the BIS Export Administration Regulations, Section 740.13) for both object code and source code. The following provides more details on the included cryptographic software: This software uses the SSL libraries from the Jetty project written by mortbay.org. Hadoop Yarn Server Web Proxy uses the BouncyCastle Java cryptography APIs written by the Legion of the Bouncy Castle Inc. -------------------------------------------------------------------------------- Apache Parquet -------------------------------------------------------------------------------- Apache Parquet MR Copyright 2014-2024 The Apache Software Foundation This product includes code from Apache Avro. Apache Avro Copyright 2010-2024 The Apache Software Foundation -------------------------------------------------------------------------------- Apache Iceberg -------------------------------------------------------------------------------- Apache Iceberg Copyright 2017-2024 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). -------------------------------------------------------------------------------- This project includes code from Kite, developed at Cloudera, Inc. with the following copyright notice: | Copyright 2013 Cloudera Inc. | | Licensed under the Apache License, Version 2.0 (the "License"); | you may not use this file except in compliance with the License. | You may obtain a copy of the License at | | http://www.apache.org/licenses/LICENSE-2.0 | | Unless required by applicable law or agreed to in writing, software | distributed under the License is distributed on an "AS IS" BASIS, | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | See the License for the specific language governing permissions and | limitations under the License. Apache Arrow (arrow-format, arrow-memory-core, arrow-memory-netty, arrow-vector, flight-core, flight-grpc) Copyright 2016-2023 The Apache Software Foundation Apache Avro Copyright 2009-2024 The Apache Software Foundation Apache Commons BeanUtils Copyright 2000-2019 The Apache Software Foundation Apache Commons CLI Copyright 2001-2022 The Apache Software Foundation Apache Commons Codec Copyright 2002-2024 The Apache Software Foundation Apache Commons Collections (3.x and 4.x) Copyright 2001-2024 The Apache Software Foundation Apache Commons Compress Copyright 2002-2024 The Apache Software Foundation Apache Commons Configuration Copyright 2001-2024 The Apache Software Foundation Apache Commons IO Copyright 2002-2024 The Apache Software Foundation Apache Commons JCS Copyright 2002-2024 The Apache Software Foundation Apache Commons Lang (2.x and 3.x) Copyright 2001-2024 The Apache Software Foundation Apache Commons Logging Copyright 2003-2014 The Apache Software Foundation Apache Commons Math Copyright 2001-2016 The Apache Software Foundation Apache Commons Net Copyright 2001-2023 The Apache Software Foundation Apache Commons Pool Copyright 2001-2024 The Apache Software Foundation Apache Commons Text Copyright 2014-2024 The Apache Software Foundation Apache Commons VFS Copyright 2002-2024 The Apache Software Foundation Apache Curator Copyright 2011-2024 The Apache Software Foundation Apache HttpComponents (httpclient, httpcore, httpasyncclient, httpclient5, httpcore5, httpcore5-h2, httpmime) Copyright 1999-2024 The Apache Software Foundation Apache HTrace (Incubating) Copyright 2016-2017 The Apache Software Foundation Apache Iceberg Copyright 2017-2024 The Apache Software Foundation Apache Kerby (kerb-* and kerby-* subprojects) Copyright 2014-2017 The Apache Software Foundation Apache Maven (many subprojects including wagon-*) Copyright 2001-2024 The Apache Software Foundation Apache ORC Copyright 2013-2024 The Apache Software Foundation Apache Yetus Copyright 2015-2023 The Apache Software Foundation Apache ZooKeeper Copyright 2008-2024 The Apache Software Foundation Apache log4j 1.2 / reload4j Copyright 2007 The Apache Software Foundation -------------------------------------------------------------------------------- Netty -------------------------------------------------------------------------------- The Netty Project Copyright 2011-2024 The Netty Project (https://netty.io/). This product contains the extensions to Java Collections Framework derived from the works by JSR-166 EG, Doug Lea, and Jason T. Greene (Public Domain). This product contains a modified version of Robert Harder's Public Domain Base64 Encoder and Decoder. This product contains a modified version of 'JZlib', a re-implementation of zlib in pure Java (BSD-style license, http://www.jcraft.com/jzlib/). This product contains a modified version of 'Webbit' (BSD License, https://github.com/joewalnes/webbit). This product optionally depends on 'Protocol Buffers' (New BSD License, http://code.google.com/p/protobuf/), 'Bouncy Castle Crypto APIs' (MIT License, http://www.bouncycastle.org/), 'SLF4J' (MIT License, http://www.slf4j.org/), 'Apache Commons Logging' (Apache License 2.0), 'Apache Log4J' (Apache License 2.0), 'JBoss Logging' (GNU LGPL 2.1), and 'Apache Felix' (Apache License 2.0). -------------------------------------------------------------------------------- Eclipse Jetty -------------------------------------------------------------------------------- Jetty Web Container Copyright 1995-2018 Mort Bay Consulting Pty Ltd. The Jetty Web Container is Copyright Mort Bay Consulting Pty Ltd unless otherwise noted. Jetty is dual licensed under both the Apache 2.0 License and the Eclipse Public 1.0 License; Texera redistributes it under the Apache 2.0 terms. Jetty bundles select artifacts under secondary licenses: * Eclipse Public License: org.eclipse.jetty.orbit:org.eclipse.jdt.core, javax.security.auth.message (EPL + ASL2), javax.mail.glassfish (EPL + CDDL 1.0) * CDDL + GPLv2 with classpath exception: javax.servlet:javax.servlet-api, javax.annotation:javax.annotation-api, javax.transaction:javax.transaction-api, javax.websocket:javax.websocket-api * OW2 license: org.ow2.asm:asm-commons, org.ow2.asm:asm * MortBay ASL2: org.mortbay.jasper:apache-jsp, apache-el (based on selected classes from Apache Tomcat) The UnixCrypt.java code implements one-way cryptography used by Unix systems for simple password protection. Copyright 1996 Aki Yoshida, modified April 2001 by Iris Van den Broeke, Daniel Deville. -------------------------------------------------------------------------------- Jackson (FasterXML) -------------------------------------------------------------------------------- Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. Copyright 2007- Tatu Saloranta (tatu.saloranta@iki.fi) Jackson 2.x core and extension components are licensed under Apache License 2.0. This attribution applies to jackson-core, jackson-databind, jackson-annotations, and every jackson-datatype-*, jackson-module-*, jackson-dataformat-*, and jackson-jaxrs-* artifact bundled in this distribution. Java ClassMate library (com.fasterxml:classmate) was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), with contributions from Brian Langel. -------------------------------------------------------------------------------- Google Guice -------------------------------------------------------------------------------- Google Guice - Core Library (and guice-servlet extension) Copyright 2006-2015 Google, Inc. -------------------------------------------------------------------------------- AWS SDK for Java 2.0 -------------------------------------------------------------------------------- AWS SDK for Java 2.0 Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. This product includes software developed by Amazon Technologies, Inc (http://www.amazon.com/). The AWS SDK bundles the following third-party works: * XML parsing and utility functions from JetS3t Copyright 2006-2009 James Murty. * PKCS#1 PEM encoded private key parsing and utility functions from oauth.googlecode.com - Copyright 1998-2010 AOL Inc. * Apache Commons Lang (https://github.com/apache/commons-lang) * Netty Reactive Streams (https://github.com/playframework/netty-reactive-streams) * Jackson-core (https://github.com/FasterXML/jackson-core), shaded as software.amazon.awssdk:third-party-jackson-core * Jackson-dataformat-cbor (https://github.com/FasterXML/jackson-dataformats-binary) Required Apache Commons Lang attribution: Apache Commons Lang Copyright 2001-2020 The Apache Software Foundation -------------------------------------------------------------------------------- Jackson core (verbatim upstream NOTICE) -------------------------------------------------------------------------------- # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. ## Copyright Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) ## Licensing Jackson 2.x core and extension components are licensed under Apache License 2.0 To find the details that apply to this artifact see the accompanying LICENSE file. ## Credits A list of contributors may be found from CREDITS(-2.x) file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. ## FastDoubleParser jackson-core bundles a shaded copy of FastDoubleParser . That code is available under an MIT license under the following copyright. Copyright © 2023 Werner Randelshofer, Switzerland. MIT License. See FastDoubleParser-NOTICE for details of other source code included in FastDoubleParser and the licenses and copyrights that apply to that code. # FastDoubleParser This is a Java port of Daniel Lemire's fast_float project. This project provides parsers for double, float, BigDecimal and BigInteger values. ## Copyright Copyright © 2024 Werner Randelshofer, Switzerland. ## Licensing This code is licensed under MIT License. https://github.com/wrandelshofer/FastDoubleParser/blob/522be16e145f43308c43b23094e31d5efcaa580e/LICENSE (The file 'LICENSE' is included in the sources and classes Jar files that are released by this project - as is required by that license.) Some portions of the code have been derived from other projects. All these projects require that we include a copyright notice, and some require that we also include some text of their license file. fast_double_parser, Copyright (c) 2022 Daniel Lemire. BSL License. https://github.com/lemire/fast_double_parser https://github.com/lemire/fast_double_parser/blob/07d9189a8fb815fe800cb15ca022e7a07093236e/LICENSE.BSL (The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project - as is required by that license.) fast_float, Copyright (c) 2021 The fast_float authors. MIT License. https://github.com/fastfloat/fast_float https://github.com/fastfloat/fast_float/blob/cc1e01e9eee74128e48d51488a6b1df4a767a810/LICENSE-MIT (The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project - as is required by that license.) bigint, Copyright 2020 Tim Buktu. 2-clause BSD License. https://github.com/tbuktu/bigint/tree/floatfft https://github.com/tbuktu/bigint/blob/617c8cd8a7c5e4fb4d919c6a4d11e2586107f029/LICENSE https://github.com/wrandelshofer/FastDoubleParser/blob/39e123b15b71f29a38a087d16a0bc620fc879aa6/bigint-LICENSE (We only use those portions of the bigint project that can be licensed under 2-clause BSD License.) (The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project - as is required by that license.) -------------------------------------------------------------------------------- Jackson modules and datatypes -------------------------------------------------------------------------------- # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. ## Copyright Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) ## Licensing Jackson 2.x core and extension components are licensed under Apache License 2.0 To find the details that apply to this artifact see the accompanying LICENSE file. ## Credits A list of contributors may be found from CREDITS(-2.x) file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers, as well as supported commercially by FasterXML.com. ## Licensing Jackson core and extension components (as well their dependencies) may be licensed under different licenses. To find the details that apply to this artifact see the accompanying LICENSE file. For more information, including possible other licensing options, contact FasterXML.com (http://fasterxml.com). ## Credits A list of contributors may be found from CREDITS file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers, as well as supported commercially by FasterXML.com. ## Licensing Jackson core and extension components may be licensed under different licenses. To find the details that apply to this artifact see the accompanying LICENSE file. For more information, including possible other licensing options, contact FasterXML.com (http://fasterxml.com). ## Credits A list of contributors may be found from CREDITS file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. ## Copyright Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) ## Licensing Jackson components are licensed under Apache (Software) License, version 2.0, as per accompanying LICENSE file. ## Credits A list of contributors may be found from CREDITS file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers, as well as supported commercially by FasterXML.com. ## Licensing Jackson core and extension components may licensed under different licenses. To find the details that apply to this artifact see the accompanying LICENSE file. For more information, including possible other licensing options, contact FasterXML.com (http://fasterxml.com). ## Credits A list of contributors may be found from CREDITS file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. ## Licensing Jackson components are licensed under Apache (Software) License, version 2.0, as per accompanying LICENSE file. ## Credits A list of contributors may be found from CREDITS file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. -------------------------------------------------------------------------------- Eclipse Jetty 11.0 -------------------------------------------------------------------------------- Notices for Eclipse Jetty ========================= This content is produced and maintained by the Eclipse Jetty project. Project home: https://eclipse.dev/jetty/ Trademarks ---------- Eclipse Jetty, and Jetty are trademarks of the Eclipse Foundation. Copyright --------- All contributions are the property of the respective authors or of entities to which copyright has been assigned by the authors (eg. employer). Declared Project Licenses ------------------------- This artifacts of this project are made available under the terms of: * the Eclipse Public License v2.0 https://www.eclipse.org/legal/epl-2.0 SPDX-License-Identifier: EPL-2.0 or * the Apache License, Version 2.0 https://www.apache.org/licenses/LICENSE-2.0 SPDX-License-Identifier: Apache-2.0 The following dependencies are EPL. * org.eclipse.jetty.orbit:org.eclipse.jdt.core The following dependencies are EPL and ASL2. * org.eclipse.jetty.orbit:javax.security.auth.message The following dependencies are EPL and CDDL 1.0. * org.eclipse.jetty.orbit:javax.mail.glassfish The following dependencies are CDDL + GPLv2 with classpath exception. https://glassfish.dev.java.net/nonav/public/CDDL+GPL.html * jakarta.servlet:jakarta.servlet-api * javax.annotation:javax.annotation-api * javax.transaction:javax.transaction-api * javax.websocket:javax.websocket-api The following dependencies are licensed by the OW2 Foundation according to the terms of http://asm.ow2.org/license.html * org.ow2.asm:asm-commons * org.ow2.asm:asm The following dependencies are ASL2 licensed. * org.apache.taglibs:taglibs-standard-spec * org.apache.taglibs:taglibs-standard-impl The following dependencies are ASL2 licensed. Based on selected classes from following Apache Tomcat jars, all ASL2 licensed. * org.mortbay.jasper:apache-jsp * org.apache.tomcat:tomcat-jasper * org.apache.tomcat:tomcat-juli * org.apache.tomcat:tomcat-jsp-api * org.apache.tomcat:tomcat-el-api * org.apache.tomcat:tomcat-jasper-el * org.apache.tomcat:tomcat-api * org.apache.tomcat:tomcat-util-scan * org.apache.tomcat:tomcat-util * org.mortbay.jasper:apache-el * org.apache.tomcat:tomcat-jasper-el * org.apache.tomcat:tomcat-el-api The following artifacts are CDDL + GPLv2 with classpath exception. https://glassfish.dev.java.net/nonav/public/CDDL+GPL.html * org.eclipse.jetty.toolchain:jetty-schemas Cryptography ------------ Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. The UnixCrypt.java code implements the one way cryptography used by Unix systems for simple password protection. Copyright 1996 Aki Yoshida, modified April 2001 by Iris Van den Broeke, Daniel Deville. Permission to use, copy, modify and distribute UnixCrypt for non-commercial or commercial purposes and without fee is granted provided that the copyright notice appears in all copies. -------------------------------------------------------------------------------- Apache Parquet (per-component supplementary notices) -------------------------------------------------------------------------------- Apache Parquet MR (Incubating) Copyright 2014-2015 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). -------------------------------------------------------------------------------- This product includes code from Apache Avro, which includes the following in its NOTICE file: Apache Avro Copyright 2010-2015 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. ## Licensing Jackson 2.x core and extension components are licensed under Apache License 2.0 To find the details that apply to this artifact see the accompanying LICENSE file. ## Credits A list of contributors may be found from CREDITS(-2.x) file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. -------------------------------------------------------------------------------- R2DBC SPI -------------------------------------------------------------------------------- Reactive Relational Database Connectivity Copyright 2017-2021 the original author or authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- Eclipse Jersey (jersey-container-servlet, jersey-container-servlet-core, jersey-client, jersey-hk2, jersey-media-jaxb) -------------------------------------------------------------------------------- # Notice for Jersey This content is produced and maintained by the Eclipse Jersey project. * Project home: https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Angular JS, v1.6.6 * License MIT (http://www.opensource.org/licenses/mit-license.php) * Project: http://angularjs.org * Coyright: (c) 2010-2017 Google, Inc. aopalliance Version 1 * License: all the source code provided by AOP Alliance is Public Domain. * Project: http://aopalliance.sourceforge.net * Copyright: Material in the public domain is not protected by copyright Bean Validation API 3.0.2 * License: Apache License, 2.0 * Project: http://beanvalidation.org/1.1/ * Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors * by the @authors tag. Hibernate Validator CDI, 7.0.5.Final * License: Apache License, 2.0 * Project: https://beanvalidation.org/ * Repackaged in org.glassfish.jersey.server.validation.internal.hibernate Bootstrap v3.3.7 * License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE) * Project: http://getbootstrap.com * Copyright: 2011-2016 Twitter, Inc Google Guava Version 18.0 * License: Apache License, 2.0 * Copyright (C) 2009 The Guava Authors jakarta.inject Version: 1 * License: Apache License, 2.0 * Copyright (C) 2009 The JSR-330 Expert Group Javassist Version 3.29.2-GA * License: Apache License, 2.0 * Project: http://www.javassist.org/ * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. Jackson JAX-RS Providers Version 2.15.3 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers * Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. jQuery v1.12.4 * License: jquery.org/license * Project: jquery.org * Copyright: (c) jQuery Foundation jQuery Barcode plugin 0.3 * License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html) * Project: http://www.pasella.it/projects/jQuery/barcode * Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com JSR-166 Extension - JEP 266 * License: CC0 * No copyright * Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/ KineticJS, v4.7.1 * License: MIT license (http://www.opensource.org/licenses/mit-license.php) * Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS * Copyright: Eric Rowell org.objectweb.asm Version 9.6 * License: Modified BSD (https://asm.ow2.io/license.html) * Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved. org.osgi.core version 6.0.0 * License: Apache License, 2.0 * Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved. org.glassfish.jersey.server.internal.monitoring.core * License: Apache License, 2.0 * Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. * Copyright 2010-2013 Coda Hale and Yammer, Inc. W3.org documents * License: W3C License * Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ -------------------------------------------------------------------------------- Eclipse Jersey Core Server -------------------------------------------------------------------------------- # Notice for Jersey Core Server module This content is produced and maintained by the Eclipse Jersey project. * https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content org.glassfish.jersey.server.internal.monitoring.core * License: Apache License, 2.0 * Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. * Copyright 2010-2013 Coda Hale and Yammer, Inc. org.objectweb.asm Version 9.6 * License: Modified BSD (https://asm.ow2.io/license.html) * Copyright: (c) 2000-2011 INRIA, France Telecom. All rights reserved. W3.org documents * License: W3C License * Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ # Notice for Jersey This content is produced and maintained by the Eclipse Jersey project. * Project home: https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Angular JS, v1.6.6 * License MIT (http://www.opensource.org/licenses/mit-license.php) * Project: http://angularjs.org * Coyright: (c) 2010-2017 Google, Inc. aopalliance Version 1 * License: all the source code provided by AOP Alliance is Public Domain. * Project: http://aopalliance.sourceforge.net * Copyright: Material in the public domain is not protected by copyright Bean Validation API 3.0.2 * License: Apache License, 2.0 * Project: http://beanvalidation.org/1.1/ * Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors * by the @authors tag. Hibernate Validator CDI, 7.0.5.Final * License: Apache License, 2.0 * Project: https://beanvalidation.org/ * Repackaged in org.glassfish.jersey.server.validation.internal.hibernate Bootstrap v3.3.7 * License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE) * Project: http://getbootstrap.com * Copyright: 2011-2016 Twitter, Inc Google Guava Version 18.0 * License: Apache License, 2.0 * Copyright (C) 2009 The Guava Authors jakarta.inject Version: 1 * License: Apache License, 2.0 * Copyright (C) 2009 The JSR-330 Expert Group Javassist Version 3.29.2-GA * License: Apache License, 2.0 * Project: http://www.javassist.org/ * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. Jackson JAX-RS Providers Version 2.15.3 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers * Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. jQuery v1.12.4 * License: jquery.org/license * Project: jquery.org * Copyright: (c) jQuery Foundation jQuery Barcode plugin 0.3 * License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html) * Project: http://www.pasella.it/projects/jQuery/barcode * Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com JSR-166 Extension - JEP 266 * License: CC0 * No copyright * Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/ KineticJS, v4.7.1 * License: MIT license (http://www.opensource.org/licenses/mit-license.php) * Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS * Copyright: Eric Rowell org.objectweb.asm Version 9.6 * License: Modified BSD (https://asm.ow2.io/license.html) * Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved. org.osgi.core version 6.0.0 * License: Apache License, 2.0 * Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved. org.glassfish.jersey.server.internal.monitoring.core * License: Apache License, 2.0 * Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. * Copyright 2010-2013 Coda Hale and Yammer, Inc. W3.org documents * License: W3C License * Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ -------------------------------------------------------------------------------- Eclipse Jersey Core Common -------------------------------------------------------------------------------- # Notice for Jersey Core Common module This content is produced and maintained by the Eclipse Jersey project. * https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Google Guava Version 18.0 * License: Apache License, 2.0 * Copyright: (C) 2009 The Guava Authors JSR-166 Extension - JEP 266 * License: Creative Commons 1.0 (CC0) * No copyright * Written by Doug Lea with assistance from members of JCP JSR-166 * Expert Group and released to the public domain, as explained at * http://creativecommons.org/publicdomain/zero/1.0/ # Notice for Jersey This content is produced and maintained by the Eclipse Jersey project. * Project home: https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Angular JS, v1.6.6 * License MIT (http://www.opensource.org/licenses/mit-license.php) * Project: http://angularjs.org * Coyright: (c) 2010-2017 Google, Inc. aopalliance Version 1 * License: all the source code provided by AOP Alliance is Public Domain. * Project: http://aopalliance.sourceforge.net * Copyright: Material in the public domain is not protected by copyright Bean Validation API 3.0.2 * License: Apache License, 2.0 * Project: http://beanvalidation.org/1.1/ * Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors * by the @authors tag. Hibernate Validator CDI, 7.0.5.Final * License: Apache License, 2.0 * Project: https://beanvalidation.org/ * Repackaged in org.glassfish.jersey.server.validation.internal.hibernate Bootstrap v3.3.7 * License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE) * Project: http://getbootstrap.com * Copyright: 2011-2016 Twitter, Inc Google Guava Version 18.0 * License: Apache License, 2.0 * Copyright (C) 2009 The Guava Authors jakarta.inject Version: 1 * License: Apache License, 2.0 * Copyright (C) 2009 The JSR-330 Expert Group Javassist Version 3.29.2-GA * License: Apache License, 2.0 * Project: http://www.javassist.org/ * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. Jackson JAX-RS Providers Version 2.15.3 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers * Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. jQuery v1.12.4 * License: jquery.org/license * Project: jquery.org * Copyright: (c) jQuery Foundation jQuery Barcode plugin 0.3 * License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html) * Project: http://www.pasella.it/projects/jQuery/barcode * Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com JSR-166 Extension - JEP 266 * License: CC0 * No copyright * Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/ KineticJS, v4.7.1 * License: MIT license (http://www.opensource.org/licenses/mit-license.php) * Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS * Copyright: Eric Rowell org.objectweb.asm Version 9.6 * License: Modified BSD (https://asm.ow2.io/license.html) * Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved. org.osgi.core version 6.0.0 * License: Apache License, 2.0 * Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved. org.glassfish.jersey.server.internal.monitoring.core * License: Apache License, 2.0 * Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. * Copyright 2010-2013 Coda Hale and Yammer, Inc. W3.org documents * License: W3C License * Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ -------------------------------------------------------------------------------- Eclipse Jersey Bean Validation -------------------------------------------------------------------------------- # Notice for Jersey Bean Validation module This content is produced and maintained by the Eclipse Jersey project. * https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Hibernate Validator CDI, 7.0.5.Final * License: Apache License, 2.0 * Project: https://beanvalidation.org/ * Repackaged in org.glassfish.jersey.server.validation.internal.hibernate # Notice for Jersey This content is produced and maintained by the Eclipse Jersey project. * Project home: https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Angular JS, v1.6.6 * License MIT (http://www.opensource.org/licenses/mit-license.php) * Project: http://angularjs.org * Coyright: (c) 2010-2017 Google, Inc. aopalliance Version 1 * License: all the source code provided by AOP Alliance is Public Domain. * Project: http://aopalliance.sourceforge.net * Copyright: Material in the public domain is not protected by copyright Bean Validation API 3.0.2 * License: Apache License, 2.0 * Project: http://beanvalidation.org/1.1/ * Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors * by the @authors tag. Hibernate Validator CDI, 7.0.5.Final * License: Apache License, 2.0 * Project: https://beanvalidation.org/ * Repackaged in org.glassfish.jersey.server.validation.internal.hibernate Bootstrap v3.3.7 * License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE) * Project: http://getbootstrap.com * Copyright: 2011-2016 Twitter, Inc Google Guava Version 18.0 * License: Apache License, 2.0 * Copyright (C) 2009 The Guava Authors jakarta.inject Version: 1 * License: Apache License, 2.0 * Copyright (C) 2009 The JSR-330 Expert Group Javassist Version 3.29.2-GA * License: Apache License, 2.0 * Project: http://www.javassist.org/ * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. Jackson JAX-RS Providers Version 2.15.3 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers * Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. jQuery v1.12.4 * License: jquery.org/license * Project: jquery.org * Copyright: (c) jQuery Foundation jQuery Barcode plugin 0.3 * License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html) * Project: http://www.pasella.it/projects/jQuery/barcode * Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com JSR-166 Extension - JEP 266 * License: CC0 * No copyright * Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/ KineticJS, v4.7.1 * License: MIT license (http://www.opensource.org/licenses/mit-license.php) * Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS * Copyright: Eric Rowell org.objectweb.asm Version 9.6 * License: Modified BSD (https://asm.ow2.io/license.html) * Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved. org.osgi.core version 6.0.0 * License: Apache License, 2.0 * Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved. org.glassfish.jersey.server.internal.monitoring.core * License: Apache License, 2.0 * Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. * Copyright 2010-2013 Coda Hale and Yammer, Inc. W3.org documents * License: W3C License * Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ -------------------------------------------------------------------------------- Eclipse GlassFish HK2 (aopalliance-repackaged, hk2-api, hk2-locator, hk2-utils) -------------------------------------------------------------------------------- # Notices for Eclipse GlassFish This content is produced and maintained by the Eclipse GlassFish project. * Project home: https://projects.eclipse.org/projects/ee4j.glassfish ## Trademarks Eclipse GlassFish, and GlassFish are trademarks of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/glassfish-ha-api * https://github.com/eclipse-ee4j/glassfish-logging-annotation-processor * https://github.com/eclipse-ee4j/glassfish-shoal * https://github.com/eclipse-ee4j/glassfish-cdi-porting-tck * https://github.com/eclipse-ee4j/glassfish-jsftemplating * https://github.com/eclipse-ee4j/glassfish-hk2-extra * https://github.com/eclipse-ee4j/glassfish-hk2 * https://github.com/eclipse-ee4j/glassfish-fighterfish ## Third-party Content This project leverages the following third party content. None ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. -------------------------------------------------------------------------------- Eclipse Jetty Servlet API (jakarta-servlet-api 5.0.2) -------------------------------------------------------------------------------- # Notices for Eclipse Project for Servlet This content is produced and maintained by the Eclipse Project for Servlet project. * Project home: https://projects.eclipse.org/projects/ee4j.servlet ## Trademarks Eclipse Project for Servlet is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/servlet-api * https://github.com/eclipse/jetty.toolchain ## Third-party Content ## Jakarta The following artifacts are EPL 2.0 + GPLv2 with classpath exception. https://projects.eclipse.org/projects/ee4j.servlet * jakarta.servlet:jakarta.servlet-api ## GlassFish The following artifacts are CDDL + GPLv2 with classpath exception. https://glassfish.dev.java.net/nonav/public/CDDL+GPL.html * org.eclipse.jetty.toolchain:jetty-schemas -------------------------------------------------------------------------------- Jakarta XML Binding API (jakarta.xml.bind-api 3.0.x) -------------------------------------------------------------------------------- [//]: # " Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. " [//]: # " " [//]: # " This program and the accompanying materials are made available under the " [//]: # " terms of the Eclipse Distribution License v. 1.0, which is available at " [//]: # " http://www.eclipse.org/org/documents/edl-v10.php. " [//]: # " " [//]: # " SPDX-License-Identifier: BSD-3-Clause " # Notices for Jakarta XML Binding This content is produced and maintained by the Jakarta XML Binding project. * Project home: https://projects.eclipse.org/projects/ee4j.jaxb ## Trademarks Jakarta XML Binding is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Distribution License v. 1.0 which is available at http://www.eclipse.org/org/documents/edl-v10.php. SPDX-License-Identifier: BSD-3-Clause ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jaxb-api * https://github.com/eclipse-ee4j/jaxb-tck ## Third-party Content This project leverages the following third party content. Apache River (3.0.0) * License: Apache-2.0 AND BSD-3-Clause ASM 7 (n/a) * License: BSD-3-Clause * Project: https://asm.ow2.io/ * Source: https://repository.ow2.org/nexus/#nexus-search;gav~org.ow2.asm~asm-commons~~~~kw,versionexpand JTHarness (5.0) * License: (GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0) * Project: https://wiki.openjdk.java.net/display/CodeTools/JT+Harness * Source: http://hg.openjdk.java.net/code-tools/jtharness/ normalize.css (3.0.2) * License: MIT SigTest (n/a) * License: GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. -------------------------------------------------------------------------------- Jakarta RESTful Web Services API (jakarta.ws.rs-api 3.0.x / 3.1.0) -------------------------------------------------------------------------------- # Notices for Jakarta RESTful Web Services This content is produced and maintained by the **Jakarta RESTful Web Services** project. * Project home: https://projects.eclipse.org/projects/ee4j.jaxrs ## Trademarks **Jakarta RESTful Web Services** is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jaxrs-api ## Third-party Content This project leverages the following third party content. javaee-api (7.0) * License: Apache-2.0 AND W3C JUnit (4.11) * License: Common Public License 1.0 Mockito (2.16.0) * Project: http://site.mockito.org * Source: https://github.com/mockito/mockito/releases/tag/v2.16.0 ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. -------------------------------------------------------------------------------- Jakarta Expression Language API (jakarta.el-api 4.0.0, glassfish jakarta.el 4.0.2) -------------------------------------------------------------------------------- # Notices for Jakarta Expression Language This content is produced and maintained by the Jakarta Expression Language project. * Project home: https://projects.eclipse.org/projects/ee4j.el ## Trademarks Jakarta Expression Language is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/el-ri ## Third-party Content ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. -------------------------------------------------------------------------------- Jakarta Annotations API (jakarta.annotation-api 2.1.1 and 3.0.0) -------------------------------------------------------------------------------- # Notices for Jakarta Annotations This content is produced and maintained by the Jakarta Annotations project. * Project home: https://projects.eclipse.org/projects/ee4j.ca ## Trademarks Jakarta Annotations is a trademark of the Eclipse Foundation. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/common-annotations-api ## Third-party Content ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Notices for Jakarta Annotations This content is produced and maintained by the Jakarta Annotations project. * Project home: https://projects.eclipse.org/projects/ee4j.ca ## Trademarks Jakarta Annotations™ is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at https://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GPL-2.0 with Classpath-exception-2.0 which is available at https://openjdk.java.net/legal/gplv2+ce.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/jakartaee/common-annotations-api ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. -------------------------------------------------------------------------------- Jakarta Inject API (jakarta.inject-api 2.0.1) -------------------------------------------------------------------------------- # Notices for Eclipse Jakarta Dependency Injection This content is produced and maintained by the Eclipse Jakarta Dependency Injection project. * Project home: https://projects.eclipse.org/projects/cdi.batch ## Trademarks Jakarta Dependency Injection is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Apache License, Version 2.0 which is available at https://www.apache.org/licenses/LICENSE-2.0. SPDX-License-Identifier: Apache-2.0 ## Source Code The project maintains the following source code repositories: https://github.com/eclipse-ee4j/injection-api https://github.com/eclipse-ee4j/injection-spec https://github.com/eclipse-ee4j/injection-tck ## Third-party Content This project leverages the following third party content. None ## Cryptography None -------------------------------------------------------------------------------- Jakarta Activation (jakarta.activation 2.0.0, 2.0.1, jakarta.activation-api 1.2.1, 2.1.0) -------------------------------------------------------------------------------- # Notices for Eclipse Project for JAF This content is produced and maintained by the Eclipse Project for JAF project. * Project home: https://projects.eclipse.org/projects/ee4j.jaf ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Distribution License v. 1.0, which is available at http://www.eclipse.org/org/documents/edl-v10.php. SPDX-License-Identifier: BSD-3-Clause ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jaf ## Third-party Content This project leverages the following third party content. JUnit (4.12) * License: Eclipse Public License - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Notices for Jakarta Activation This content is produced and maintained by Jakarta Activation project. * Project home: https://projects.eclipse.org/projects/ee4j.jaf ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Distribution License v. 1.0, which is available at http://www.eclipse.org/org/documents/edl-v10.php. SPDX-License-Identifier: BSD-3-Clause ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jaf ## Third-party Content This project leverages the following third party content. JUnit (4.12) * License: Eclipse Public License - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Notices for Jakarta Activation This content is produced and maintained by Jakarta Activation project. * Project home: https://projects.eclipse.org/projects/ee4j.jaf ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Distribution License v. 1.0, which is available at http://www.eclipse.org/org/documents/edl-v10.php. SPDX-License-Identifier: BSD-3-Clause ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jaf ================================================ FILE: file-service/build.sbt ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. import scala.collection.Seq name := "file-service" enablePlugins(JavaAppPackaging) // Ship LICENSE-binary, NOTICE-binary, DISCLAIMER, and the licenses/ // directory at the top of the Universal dist zip. // See project/AddMetaInfLicenseFiles.scala. Universal / mappings := AddMetaInfLicenseFiles.distMappings( (Universal / mappings).value, (ThisBuild / baseDirectory).value, baseDirectory.value / "LICENSE-binary", baseDirectory.value / "NOTICE-binary" ) // Enable semanticdb for Scalafix ThisBuild / semanticdbEnabled := true ThisBuild / semanticdbVersion := scalafixSemanticdb.revision // Manage dependency conflicts by always using the latest revision ThisBuild / conflictManager := ConflictManager.latestRevision // Restrict parallel execution of tests to avoid conflicts Global / concurrentRestrictions += Tags.limit(Tags.Test, 1) ///////////////////////////////////////////////////////////////////////////// // Compiler Options ///////////////////////////////////////////////////////////////////////////// // Scala compiler options Compile / scalacOptions ++= Seq( "-Xelide-below", "WARNING", // Turn on optimizations with "WARNING" as the threshold "-feature", // Check feature warnings "-deprecation", // Check deprecation warnings "-Ywarn-unused:imports" // Check for unused imports ) ///////////////////////////////////////////////////////////////////////////// // Version Variables ///////////////////////////////////////////////////////////////////////////// val dropwizardVersion = "4.0.7" val mockitoVersion = "5.4.0" val assertjVersion = "3.24.2" val testcontainersVersion = "0.44.1" ///////////////////////////////////////////////////////////////////////////// // Test-related Dependencies ///////////////////////////////////////////////////////////////////////////// libraryDependencies ++= Seq( "org.scalamock" %% "scalamock" % "5.2.0" % Test, // ScalaMock "org.scalatest" %% "scalatest" % "3.2.17" % Test, // ScalaTest "io.dropwizard" % "dropwizard-testing" % dropwizardVersion % Test, // Dropwizard Testing "org.mockito" % "mockito-core" % mockitoVersion % Test, // Mockito for mocking "org.assertj" % "assertj-core" % assertjVersion % Test, // AssertJ for assertions "com.novocode" % "junit-interface" % "0.11" % Test, // SBT interface for JUnit "com.dimafeng" %% "testcontainers-scala-scalatest" % testcontainersVersion % Test, // Testcontainers ScalaTest integration "com.dimafeng" %% "testcontainers-scala-postgresql" % testcontainersVersion % Test, // PostgreSQL Testcontainer Scala integration "com.dimafeng" %% "testcontainers-scala-minio" % testcontainersVersion % Test, // MinIO Testcontainer Scala integration ) ///////////////////////////////////////////////////////////////////////////// // Dependencies ///////////////////////////////////////////////////////////////////////////// // Core Dependencies libraryDependencies ++= Seq( "io.dropwizard" % "dropwizard-core" % dropwizardVersion, "io.dropwizard" % "dropwizard-auth" % dropwizardVersion, // Dropwizard Authentication module "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.18.6", "jakarta.ws.rs" % "jakarta.ws.rs-api" % "3.1.0", // Ensure Jakarta JAX-RS API is available "org.bitbucket.b_c" % "jose4j" % "0.9.6", "org.playframework" %% "play-json" % "3.1.0-M1", ) ================================================ FILE: file-service/src/main/resources/docker-compose.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. name: texera-lakefs services: minio: image: minio/minio:RELEASE.2025-02-28T09-55-16Z container_name: texera-lakefs-minio restart: unless-stopped ports: - "9000:9000" - "9001:9001" environment: - MINIO_ROOT_USER=texera_minio - MINIO_ROOT_PASSWORD=password command: server --console-address ":9001" /data # The lines below are recommended to mount a host path in order to persist your data even if the container is removed. volumes: - minio_data:/data postgres: image: postgres:15 container_name: texera-lakefs-postgres restart: unless-stopped environment: - POSTGRES_DB=texera_lakefs - POSTGRES_USER=texera_lakefs_admin - POSTGRES_PASSWORD=password healthcheck: test: ["CMD", "pg_isready", "-U", "texera_lakefs_admin", "-d", "texera_lakefs"] interval: 10s retries: 5 start_period: 5s # Ditto volumes: - postgres_data:/var/lib/postgresql/data lakefs: image: treeverse/lakefs:1.51 container_name: texera-lakefs-lakefs restart: unless-stopped depends_on: postgres: condition: service_healthy minio: condition: service_started ports: - "8000:8000" environment: - LAKEFS_BLOCKSTORE_TYPE=s3 - LAKEFS_BLOCKSTORE_S3_FORCE_PATH_STYLE=true - LAKEFS_BLOCKSTORE_S3_ENDPOINT=http://minio:9000 - LAKEFS_BLOCKSTORE_S3_PRE_SIGNED_ENDPOINT=http://localhost:9000 - LAKEFS_BLOCKSTORE_S3_CREDENTIALS_ACCESS_KEY_ID=texera_minio - LAKEFS_BLOCKSTORE_S3_CREDENTIALS_SECRET_ACCESS_KEY=password - LAKEFS_AUTH_ENCRYPT_SECRET_KEY=random_string_for_lakefs - LAKEFS_LOGGING_LEVEL=INFO - LAKEFS_STATS_ENABLED=1 - LAKEFS_DATABASE_TYPE=postgres - LAKEFS_DATABASE_POSTGRES_CONNECTION_STRING=postgres://texera_lakefs_admin:password@postgres:5432/texera_lakefs?sslmode=disable - LAKEFS_INSTALLATION_USER_NAME=texera-admin - LAKEFS_INSTALLATION_ACCESS_KEY_ID=AKIAIOSFOLKFSSAMPLES - LAKEFS_INSTALLATION_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY entrypoint: ["/bin/sh", "-c"] command: - | lakefs setup --user-name "$$LAKEFS_INSTALLATION_USER_NAME" --access-key-id "$$LAKEFS_INSTALLATION_ACCESS_KEY_ID" --secret-access-key "$$LAKEFS_INSTALLATION_SECRET_ACCESS_KEY" || true lakefs run & echo "---- lakeFS Web UI ----" echo "http://127.0.0.1:8000/" echo "" echo "Access Key ID : $$LAKEFS_INSTALLATION_ACCESS_KEY_ID" echo "Secret Access Key: $$LAKEFS_INSTALLATION_SECRET_ACCESS_KEY" echo "" wait networks: default: name: texera-lakefs # Named Docker volumes — uncomment the following lines if you mounted host paths above. volumes: minio_data: postgres_data: ================================================ FILE: file-service/src/main/resources/file-service-web-config.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. server: applicationConnectors: - type: http port: 9092 adminConnectors: [] requestLog: type: classic appenders: [] logging: level: ${TEXERA_SERVICE_LOG_LEVEL:-INFO} loggers: "io.dropwizard": ${TEXERA_SERVICE_LOG_LEVEL:-INFO} appenders: - type: console - type: file currentLogFilename: log/file-service.log threshold: ALL queueSize: 512 discardingThreshold: 0 archive: true archivedLogFilenamePattern: log/file-service-%d{yyyy-MM-dd}.log.gz archivedFileCount: 7 bufferSize: 8KiB immediateFlush: true ================================================ FILE: file-service/src/main/resources/minio-config.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. version: '3.8' services: minio: image: minio/minio:latest container_name: minio ports: - "9500:9000" # MinIO API - "9501:9001" # MinIO Console UI environment: - MINIO_ROOT_USER=texera_minio - MINIO_ROOT_PASSWORD=password volumes: - /Users/baijiadong/Desktop/chenlab/texera/core/file-service/src/main/user-resources/minio:/data command: server --console-address ":9001" /data ================================================ FILE: file-service/src/main/scala/org/apache/texera/service/FileService.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.module.scala.DefaultScalaModule import com.typesafe.scalalogging.LazyLogging import io.dropwizard.auth.AuthDynamicFeature import io.dropwizard.configuration.{EnvironmentVariableSubstitutor, SubstitutingSourceProvider} import io.dropwizard.core.Application import io.dropwizard.core.setup.{Bootstrap, Environment} import org.apache.texera.amber.config.StorageConfig import org.apache.texera.amber.core.storage.util.LakeFSStorageClient import org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser} import org.apache.texera.dao.SqlServer import org.apache.texera.service.`type`.DatasetFileNode import org.apache.texera.service.`type`.serde.DatasetFileNodeSerializer import org.apache.texera.service.resource.{ DatasetAccessResource, DatasetResource, HealthCheckResource } import org.apache.texera.service.util.S3StorageClient import org.apache.texera.service.util.LargeBinaryManager import org.eclipse.jetty.server.session.SessionHandler import java.nio.file.Path class FileService extends Application[FileServiceConfiguration] with LazyLogging { override def initialize(bootstrap: Bootstrap[FileServiceConfiguration]): Unit = { // enable environment variable substitution in YAML config bootstrap.setConfigurationSourceProvider( new SubstitutingSourceProvider( bootstrap.getConfigurationSourceProvider, new EnvironmentVariableSubstitutor(false) ) ) // Register Scala module to Dropwizard default object mapper bootstrap.getObjectMapper.registerModule(DefaultScalaModule) // register a new custom module just for DatasetFileNode serde/deserde val customSerializerModule = new SimpleModule("CustomSerializers") customSerializerModule.addSerializer(classOf[DatasetFileNode], new DatasetFileNodeSerializer()) bootstrap.getObjectMapper.registerModule(customSerializerModule) } override def run(configuration: FileServiceConfiguration, environment: Environment): Unit = { // Serve backend at /api environment.jersey.setUrlPattern("/api/*") SqlServer.initConnection( StorageConfig.jdbcUrl, StorageConfig.jdbcUsername, StorageConfig.jdbcPassword ) // check if the texera dataset bucket exists, if not create it S3StorageClient.createBucketIfNotExist(StorageConfig.lakefsBucketName) // ensure the large-binary S3 bucket exists before any workflow execution attempts to use it S3StorageClient.createBucketIfNotExist(LargeBinaryManager.DEFAULT_BUCKET) // check if we can connect to the lakeFS service LakeFSStorageClient.healthCheck() environment.jersey.register(classOf[SessionHandler]) environment.servlets.setSessionHandler(new SessionHandler) environment.jersey.register(classOf[HealthCheckResource]) // Register JWT authentication filter environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter])) // Enable @Auth annotation for injecting SessionUser environment.jersey.register( new io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser]) ) environment.jersey.register(classOf[DatasetResource]) environment.jersey.register(classOf[DatasetAccessResource]) // Route request logs through SLF4J, controlled by TEXERA_SERVICE_LOG_LEVEL RequestLoggingFilter.register(environment.getApplicationContext) } } object FileService { def main(args: Array[String]): Unit = { // Set the configuration file's path val configFilePath = Path .of(sys.env.getOrElse("TEXERA_HOME", ".")) .resolve("file-service") .resolve("src") .resolve("main") .resolve("resources") .resolve("file-service-web-config.yaml") .toAbsolutePath .toString // Start the Dropwizard application new FileService().run("server", configFilePath) } } ================================================ FILE: file-service/src/main/scala/org/apache/texera/service/FileServiceConfiguration.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service import io.dropwizard.core.Configuration class FileServiceConfiguration extends Configuration {} ================================================ FILE: file-service/src/main/scala/org/apache/texera/service/resource/DatasetAccessResource.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.resource import io.dropwizard.auth.Auth import jakarta.annotation.security.RolesAllowed import jakarta.ws.rs.core.{MediaType, Response} import jakarta.ws.rs._ import org.apache.texera.auth.SessionUser import org.apache.texera.dao.SqlServer import org.apache.texera.dao.SqlServer.withTransaction import org.apache.texera.dao.jooq.generated.Tables.USER import org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum import org.apache.texera.dao.jooq.generated.tables.DatasetUserAccess.DATASET_USER_ACCESS import org.apache.texera.dao.jooq.generated.tables.daos.{DatasetDao, DatasetUserAccessDao, UserDao} import org.apache.texera.dao.jooq.generated.tables.pojos.{DatasetUserAccess, User} import org.apache.texera.service.resource.DatasetAccessResource.{ AccessEntry, context, getOwner, userHasWriteAccess } import org.jooq.{DSLContext, EnumType} import javax.ws.rs.ForbiddenException object DatasetAccessResource { private def context: DSLContext = SqlServer .getInstance() .createDSLContext() def isDatasetPublic(ctx: DSLContext, did: Integer): Boolean = { val datasetDao = new DatasetDao(ctx.configuration()) Option(datasetDao.fetchOneByDid(did)) .flatMap(dataset => Option(dataset.getIsPublic)) .contains(true) } def userHasReadAccess(ctx: DSLContext, did: Integer, uid: Integer): Boolean = { isDatasetPublic(ctx, did) || userHasWriteAccess(ctx, did, uid) || getDatasetUserAccessPrivilege(ctx, did, uid) == PrivilegeEnum.READ } def userOwnDataset(ctx: DSLContext, did: Integer, uid: Integer): Boolean = { val datasetDao = new DatasetDao(ctx.configuration()) Option(datasetDao.fetchOneByDid(did)) .exists(_.getOwnerUid == uid) } def userHasWriteAccess(ctx: DSLContext, did: Integer, uid: Integer): Boolean = { userOwnDataset(ctx, did, uid) || getDatasetUserAccessPrivilege(ctx, did, uid) == PrivilegeEnum.WRITE } def getDatasetUserAccessPrivilege( ctx: DSLContext, did: Integer, uid: Integer ): PrivilegeEnum = { Option( ctx .select(DATASET_USER_ACCESS.PRIVILEGE) .from(DATASET_USER_ACCESS) .where( DATASET_USER_ACCESS.DID .eq(did) .and(DATASET_USER_ACCESS.UID.eq(uid)) ) .fetchOneInto(classOf[PrivilegeEnum]) ).getOrElse(PrivilegeEnum.NONE) } def getOwner(ctx: DSLContext, did: Integer): User = { val datasetDao = new DatasetDao(ctx.configuration()) val userDao = new UserDao(ctx.configuration()) Option(datasetDao.fetchOneByDid(did)) .flatMap(dataset => Option(dataset.getOwnerUid)) .map(ownerUid => userDao.fetchOneByUid(ownerUid)) .orNull } case class AccessEntry(email: String, name: String, privilege: EnumType) {} } @Produces(Array(MediaType.APPLICATION_JSON)) @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/access/dataset") class DatasetAccessResource { /** * This method returns the owner of a dataset * * @param did , dataset id * @return ownerEmail, the owner's email */ @GET @Path("/owner/{did}") def getOwnerEmailOfDataset(@PathParam("did") did: Integer): String = { var email = "" withTransaction(context) { ctx => val owner = getOwner(ctx, did) if (owner != null) { email = owner.getEmail } } email } /** * Returns information about all current shared access of the given dataset * * @param did dataset id * @return a List of email/name/permission */ @GET @Path("/list/{did}") def getAccessList( @PathParam("did") did: Integer ): java.util.List[AccessEntry] = { withTransaction(context) { ctx => val datasetDao = new DatasetDao(ctx.configuration()) ctx .select( USER.EMAIL, USER.NAME, DATASET_USER_ACCESS.PRIVILEGE ) .from(DATASET_USER_ACCESS) .join(USER) .on(USER.UID.eq(DATASET_USER_ACCESS.UID)) .where( DATASET_USER_ACCESS.DID .eq(did) .and(DATASET_USER_ACCESS.UID.notEqual(datasetDao.fetchOneByDid(did).getOwnerUid)) ) .fetchInto(classOf[AccessEntry]) } } /** * This method shares a dataset to a user with a specific access type * * @param did the given dataset * @param email the email which the access is given to * @param privilege the type of Access given to the target user * @return rejection if user not permitted to share the workflow or Success Message */ @PUT @Path("/grant/{did}/{email}/{privilege}") def grantAccess( @PathParam("did") did: Integer, @PathParam("email") email: String, @PathParam("privilege") privilege: String, @Auth user: SessionUser ): Response = { withTransaction(context) { ctx => if (!userHasWriteAccess(ctx, did, user.getUid)) { throw new ForbiddenException(s"You do not have permission to modify dataset $did") } val datasetUserAccessDao = new DatasetUserAccessDao(ctx.configuration()) val userDao = new UserDao(ctx.configuration()) datasetUserAccessDao.merge( new DatasetUserAccess( did, userDao.fetchOneByEmail(email).getUid, PrivilegeEnum.valueOf(privilege) ) ) Response.ok().build() } } /** * This method revoke the user's access of the given dataset * * @param did the given dataset * @param email the email of the use whose access is about to be removed * @return message indicating a success message */ @DELETE @Path("/revoke/{did}/{email}") def revokeAccess( @PathParam("did") did: Integer, @PathParam("email") email: String, @Auth user: SessionUser ): Response = { withTransaction(context) { ctx => if (!userHasWriteAccess(ctx, did, user.getUid)) { throw new ForbiddenException(s"You do not have permission to modify dataset $did") } val userDao = new UserDao(ctx.configuration()) ctx .delete(DATASET_USER_ACCESS) .where( DATASET_USER_ACCESS.UID .eq(userDao.fetchOneByEmail(email).getUid) .and(DATASET_USER_ACCESS.DID.eq(did)) ) .execute() Response.ok().build() } } } ================================================ FILE: file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.resource import io.dropwizard.auth.Auth import jakarta.annotation.security.RolesAllowed import jakarta.ws.rs._ import jakarta.ws.rs.core._ import org.apache.texera.amber.config.StorageConfig import org.apache.texera.amber.core.storage.model.OnDataset import org.apache.texera.amber.core.storage.util.LakeFSStorageClient import org.apache.texera.amber.core.storage.{DocumentFactory, FileResolver} import org.apache.texera.auth.SessionUser import org.apache.texera.dao.SiteSettings import org.apache.texera.dao.SqlServer import org.apache.texera.dao.SqlServer.withTransaction import org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum import org.apache.texera.dao.jooq.generated.tables.Dataset.DATASET import org.apache.texera.dao.jooq.generated.tables.DatasetUserAccess.DATASET_USER_ACCESS import org.apache.texera.dao.jooq.generated.tables.DatasetVersion.DATASET_VERSION import org.apache.texera.dao.jooq.generated.tables.User.USER import org.apache.texera.dao.jooq.generated.tables.daos.{ DatasetDao, DatasetUserAccessDao, DatasetVersionDao } import org.apache.texera.dao.jooq.generated.tables.pojos.{ Dataset, DatasetUserAccess, DatasetVersion } import org.apache.texera.service.`type`.DatasetFileNode import org.apache.texera.service.resource.DatasetAccessResource._ import org.apache.texera.service.resource.DatasetResource.{context, _} import org.apache.texera.service.util.S3StorageClient import org.apache.texera.service.util.S3StorageClient.{ MAXIMUM_NUM_OF_MULTIPART_S3_PARTS, MINIMUM_NUM_OF_MULTIPART_S3_PART, PHYSICAL_ADDRESS_EXPIRATION_TIME_HRS } import org.jooq.impl.DSL import org.jooq.impl.DSL.{inline => inl} import org.jooq.{DSLContext, EnumType, Record2, Result} import java.io.{InputStream, OutputStream} import java.net.{HttpURLConnection, URI, URL, URLDecoder} import java.nio.charset.StandardCharsets import java.nio.file.{Files, Paths} import java.util import java.util.Optional import java.util.zip.{ZipEntry, ZipOutputStream} import scala.collection.mutable.ListBuffer import scala.jdk.CollectionConverters._ import scala.jdk.OptionConverters._ import org.apache.texera.dao.jooq.generated.tables.DatasetUploadSession.DATASET_UPLOAD_SESSION import org.apache.texera.dao.jooq.generated.tables.DatasetUploadSessionPart.DATASET_UPLOAD_SESSION_PART import org.jooq.exception.DataAccessException import software.amazon.awssdk.services.s3.model.UploadPartResponse import org.apache.commons.io.FilenameUtils import org.apache.texera.service.util.LakeFSExceptionHandler.withLakeFSErrorHandling import org.apache.texera.dao.jooq.generated.tables.records.DatasetUploadSessionRecord import java.sql.SQLException import java.time.OffsetDateTime import scala.util.Try object DatasetResource { private def context = SqlServer .getInstance() .createDSLContext() private def singleFileUploadMaxBytes(defaultMiB: Long = 20L): Long = SiteSettings.getLong("single_file_upload_max_size_mib", defaultMiB) * 1024L * 1024L /** * Helper function to get the dataset from DB using did */ private def getDatasetByID(ctx: DSLContext, did: Integer): Dataset = { val datasetDao = new DatasetDao(ctx.configuration()) val dataset = datasetDao.fetchOneByDid(did) if (dataset == null) { throw new NotFoundException(f"Dataset $did not found") } dataset } /** * Helper function to PUT exactly len bytes from buf to presigned URL, return the ETag */ private def put(buf: Array[Byte], len: Int, url: String, partNum: Int): String = { val conn = new URL(url).openConnection().asInstanceOf[HttpURLConnection] conn.setDoOutput(true) conn.setRequestMethod("PUT") conn.setFixedLengthStreamingMode(len) val out = conn.getOutputStream out.write(buf, 0, len) out.close() val code = conn.getResponseCode if (code != HttpURLConnection.HTTP_OK && code != HttpURLConnection.HTTP_CREATED) throw new RuntimeException(s"Part $partNum upload failed (HTTP $code)") val etag = conn.getHeaderField("ETag").replace("\"", "") conn.disconnect() etag } /** * Helper function to get the dataset version from DB using dvid */ private def getDatasetVersionByID( ctx: DSLContext, dvid: Integer ): DatasetVersion = { val datasetVersionDao = new DatasetVersionDao(ctx.configuration()) val version = datasetVersionDao.fetchOneByDvid(dvid) if (version == null) { throw new NotFoundException("Dataset Version not found") } version } /** * Helper function to get the latest dataset version from the DB */ private def getLatestDatasetVersion( ctx: DSLContext, did: Integer ): Option[DatasetVersion] = { ctx .selectFrom(DATASET_VERSION) .where(DATASET_VERSION.DID.eq(did)) .orderBy(DATASET_VERSION.CREATION_TIME.desc()) .limit(1) .fetchOptionalInto(classOf[DatasetVersion]) .toScala } /** * Validates a file path using Apache Commons IO. */ def validateAndNormalizeFilePathOrThrow(path: String): String = { if (path == null || path.trim.isEmpty) { throw new BadRequestException("Path cannot be empty") } val normalized = FilenameUtils.normalize(path, true) if (normalized == null) { throw new BadRequestException("Invalid path") } if (FilenameUtils.getPrefixLength(normalized) > 0) { throw new BadRequestException("Absolute paths not allowed") } normalized } case class DashboardDataset( dataset: Dataset, ownerEmail: String, accessPrivilege: EnumType, isOwner: Boolean, size: Long ) case class DashboardDatasetVersion( datasetVersion: DatasetVersion, fileNodes: List[DatasetFileNode] ) case class CreateDatasetRequest( datasetName: String, datasetDescription: String, isDatasetPublic: Boolean, isDatasetDownloadable: Boolean ) case class Diff( path: String, pathType: String, diffType: String, // "added", "removed", "changed", etc. sizeBytes: Option[Long] // Size of the changed file (None for directories) ) case class DatasetDescriptionModification(did: Integer, description: String) case class DatasetNameModification(did: Integer, name: String) case class DatasetVersionRootFileNodesResponse( fileNodes: List[DatasetFileNode], size: Long ) case class CoverImageRequest(coverImage: String) } @Produces(Array(MediaType.APPLICATION_JSON, "image/jpeg", "application/pdf")) @Path("/dataset") class DatasetResource { private val ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE = "User has no access to this dataset" private val ERR_DATASET_VERSION_NOT_FOUND_MESSAGE = "The version of the dataset not found" private val EXPIRATION_MINUTES = 5 private val COVER_IMAGE_SIZE_LIMIT_BYTES: Long = 10 * 1024 * 1024 // 10 MB private val ALLOWED_IMAGE_EXTENSIONS: Set[String] = Set(".jpg", ".jpeg", ".png", ".gif", ".webp") /** * Helper function to get the dataset from DB with additional information including user access privilege and owner email */ private def getDashboardDataset( ctx: DSLContext, did: Integer, requesterUid: Option[Integer] ): DashboardDataset = { val targetDataset = getDatasetByID(ctx, did) if (requesterUid.isEmpty && !targetDataset.getIsPublic) { throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) } else if (requesterUid.exists(uid => !userHasReadAccess(ctx, did, uid))) { throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) } val userAccessPrivilege = requesterUid .map(uid => getDatasetUserAccessPrivilege(ctx, did, uid)) .getOrElse(PrivilegeEnum.READ) val isOwner = requesterUid.contains(targetDataset.getOwnerUid) DashboardDataset( targetDataset, getOwner(ctx, did).getEmail, userAccessPrivilege, isOwner, LakeFSStorageClient.retrieveRepositorySize(targetDataset.getRepositoryName) ) } @POST @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/create") @Consumes(Array(MediaType.APPLICATION_JSON)) def createDataset( request: CreateDatasetRequest, @Auth user: SessionUser ): DashboardDataset = { withTransaction(context) { ctx => val uid = user.getUid val datasetUserAccessDao: DatasetUserAccessDao = new DatasetUserAccessDao(ctx.configuration()) val datasetName = request.datasetName val datasetDescription = request.datasetDescription val isDatasetPublic = request.isDatasetPublic val isDatasetDownloadable = request.isDatasetDownloadable // validate dataset name try { validateDatasetName(datasetName) } catch { case e: IllegalArgumentException => throw new BadRequestException(e.getMessage) } // Check if a dataset with the same name already exists val existingDatasets = context .selectFrom(DATASET) .where(DATASET.OWNER_UID.eq(uid)) .and(DATASET.NAME.eq(datasetName)) .fetch() if (!existingDatasets.isEmpty) { throw new BadRequestException("Dataset with the same name already exists") } // insert the dataset into the database val dataset = new Dataset() dataset.setName(datasetName) dataset.setDescription(datasetDescription) dataset.setIsPublic(isDatasetPublic) dataset.setIsDownloadable(isDatasetDownloadable) dataset.setOwnerUid(uid) // insert record and get created dataset with did val createdDataset = ctx .insertInto(DATASET) .set(ctx.newRecord(DATASET, dataset)) .returning() .fetchOne() // Initialize the repository in LakeFS val repositoryName = s"dataset-${createdDataset.getDid}" try { LakeFSStorageClient.initRepo(repositoryName) } catch { case e: Exception => ctx .deleteFrom(DATASET) .where(DATASET.DID.eq(createdDataset.getDid)) .execute() throw new WebApplicationException( s"Failed to create the dataset: ${e.getMessage}" ) } // update repository name of the created dataset createdDataset.setRepositoryName(repositoryName) createdDataset.update() // Insert the requester as the WRITE access user for this dataset val datasetUserAccess = new DatasetUserAccess() datasetUserAccess.setDid(createdDataset.getDid) datasetUserAccess.setUid(uid) datasetUserAccess.setPrivilege(PrivilegeEnum.WRITE) datasetUserAccessDao.insert(datasetUserAccess) DashboardDataset( createdDataset.into(classOf[Dataset]), user.getEmail, PrivilegeEnum.WRITE, isOwner = true, 0 ) } } @POST @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/{did}/version/create") @Consumes(Array(MediaType.TEXT_PLAIN)) def createDatasetVersion( versionName: String, @PathParam("did") did: Integer, @Auth user: SessionUser ): DashboardDatasetVersion = { val uid = user.getUid withTransaction(context) { ctx => if (!userHasWriteAccess(ctx, did, uid)) { throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) } val dataset = getDatasetByID(ctx, did) val datasetName = dataset.getName val repositoryName = dataset.getRepositoryName // Check if there are any changes in LakeFS before creating a new version val diffs = withLakeFSErrorHandling { LakeFSStorageClient.retrieveUncommittedObjects(repoName = repositoryName) } if (diffs.isEmpty) { throw new WebApplicationException( "No changes detected in dataset. Version creation aborted.", Response.Status.BAD_REQUEST ) } // Generate a new version name val versionCount = ctx .selectCount() .from(DATASET_VERSION) .where(DATASET_VERSION.DID.eq(did)) .fetchOne(0, classOf[Int]) val sanitizedVersionName = Option(versionName).filter(_.nonEmpty).getOrElse("") val newVersionName = if (sanitizedVersionName.isEmpty) { s"v${versionCount + 1}" } else { s"v${versionCount + 1} - $sanitizedVersionName" } // Create a commit in LakeFS val commit = withLakeFSErrorHandling { LakeFSStorageClient.createCommit( repoName = repositoryName, branch = "main", commitMessage = s"Created dataset version: $newVersionName" ) } if (commit == null || commit.getId == null) { throw new WebApplicationException( "Failed to create commit in LakeFS. Version creation aborted.", Response.Status.INTERNAL_SERVER_ERROR ) } // Create a new dataset version entry in the database val datasetVersion = new DatasetVersion() datasetVersion.setDid(did) datasetVersion.setCreatorUid(uid) datasetVersion.setName(newVersionName) datasetVersion.setVersionHash(commit.getId) // Store LakeFS version hash val insertedVersion = ctx .insertInto(DATASET_VERSION) .set(ctx.newRecord(DATASET_VERSION, datasetVersion)) .returning() .fetchOne() .into(classOf[DatasetVersion]) // Retrieve committed file structure val fileNodes = withLakeFSErrorHandling { LakeFSStorageClient.retrieveObjectsOfVersion(repositoryName, commit.getId) } DashboardDatasetVersion( insertedVersion, DatasetFileNode .fromLakeFSRepositoryCommittedObjects( Map((user.getEmail, datasetName, newVersionName) -> fileNodes) ) ) } } @DELETE @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/{did}") def deleteDataset(@PathParam("did") did: Integer, @Auth user: SessionUser): Response = { val uid = user.getUid withTransaction(context) { ctx => val datasetDao = new DatasetDao(ctx.configuration()) val dataset = getDatasetByID(ctx, did) if (!userOwnDataset(ctx, dataset.getDid, uid)) { // throw the exception that user has no access to certain dataset throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) } try { LakeFSStorageClient.deleteRepo(dataset.getRepositoryName) } catch { case e: Exception => throw new WebApplicationException( s"Failed to delete a repository in LakeFS: ${e.getMessage}", e ) } // delete the directory on S3 if ( S3StorageClient.directoryExists(StorageConfig.lakefsBucketName, dataset.getRepositoryName) ) { S3StorageClient.deleteDirectory(StorageConfig.lakefsBucketName, dataset.getRepositoryName) } // delete the dataset from the DB datasetDao.deleteById(dataset.getDid) Response.ok().build() } } @POST @Consumes(Array(MediaType.APPLICATION_JSON)) @Produces(Array(MediaType.APPLICATION_JSON)) @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/update/description") def updateDatasetDescription( modificator: DatasetDescriptionModification, @Auth sessionUser: SessionUser ): Response = { withTransaction(context) { ctx => val uid = sessionUser.getUid val datasetDao = new DatasetDao(ctx.configuration()) val dataset = getDatasetByID(ctx, modificator.did) if (!userHasWriteAccess(ctx, modificator.did, uid)) { throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) } dataset.setDescription(modificator.description) datasetDao.update(dataset) Response.ok().build() } } @POST @Consumes(Array(MediaType.APPLICATION_JSON)) @Produces(Array(MediaType.APPLICATION_JSON)) @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/update/name") def updateDatasetName( modificator: DatasetNameModification, @Auth sessionUser: SessionUser ): Response = { withTransaction(context) { ctx => val uid = sessionUser.getUid val datasetDao = new DatasetDao(ctx.configuration()) val dataset = getDatasetByID(ctx, modificator.did) if (!userHasWriteAccess(ctx, modificator.did, uid)) { throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) } dataset.setName(modificator.name) datasetDao.update(dataset) Response.ok().build() } } @POST @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/{did}/upload") @Consumes(Array(MediaType.APPLICATION_OCTET_STREAM)) def uploadOneFileToDataset( @PathParam("did") did: Integer, @QueryParam("filePath") encodedFilePath: String, @QueryParam("message") message: String, fileStream: InputStream, @Context headers: HttpHeaders, @Auth user: SessionUser ): Response = { // These variables are defined at the top so catch block can access them val uid = user.getUid var repoName: String = null var filePath: String = null var uploadId: String = null var physicalAddress: String = null try { withTransaction(context) { ctx => if (!userHasWriteAccess(ctx, did, uid)) throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) val dataset = getDatasetByID(ctx, did) repoName = dataset.getRepositoryName filePath = URLDecoder.decode(encodedFilePath, StandardCharsets.UTF_8.name) // ---------- decide part-size & number-of-parts ---------- val declaredLen = Option(headers.getHeaderString(HttpHeaders.CONTENT_LENGTH)).map(_.toLong) var partSize = StorageConfig.s3MultipartUploadPartSize declaredLen.foreach { ln => val needed = ((ln + partSize - 1) / partSize).toInt if (needed > MAXIMUM_NUM_OF_MULTIPART_S3_PARTS) partSize = math.max( MINIMUM_NUM_OF_MULTIPART_S3_PART, ln / (MAXIMUM_NUM_OF_MULTIPART_S3_PARTS - 1) ) } val expectedParts = declaredLen .map(ln => ((ln + partSize - 1) / partSize).toInt + 1 ) // “+1” for the last (possibly small) part .getOrElse(MAXIMUM_NUM_OF_MULTIPART_S3_PARTS) // ---------- ask LakeFS for presigned URLs ---------- val presign = LakeFSStorageClient .initiatePresignedMultipartUploads(repoName, filePath, expectedParts) uploadId = presign.getUploadId val presignedUrls = presign.getPresignedUrls.asScala.iterator physicalAddress = presign.getPhysicalAddress // ---------- stream & upload parts ---------- /* 1. Reads the input stream in chunks of 'partSize' bytes by stacking them in a buffer 2. Uploads each chunk (part) using a presigned URL 3. Tracks each part number and ETag returned from S3 4. After all parts are uploaded, completes the multipart upload */ val buf = new Array[Byte](partSize.toInt) var buffered = 0 var partNumber = 1 val completedParts = ListBuffer[(Int, String)]() @inline def flush(): Unit = { if (buffered == 0) return if (!presignedUrls.hasNext) throw new WebApplicationException("Ran out of presigned part URLs – ask for more parts") val etag = put(buf, buffered, presignedUrls.next(), partNumber) completedParts += ((partNumber, etag)) partNumber += 1 buffered = 0 } var read = fileStream.read(buf, buffered, buf.length - buffered) while (read != -1) { buffered += read if (buffered == buf.length) flush() // buffer full read = fileStream.read(buf, buffered, buf.length - buffered) } fileStream.close() flush() // ---------- complete upload ---------- LakeFSStorageClient.completePresignedMultipartUploads( repoName, filePath, uploadId, completedParts.toList, physicalAddress ) Response.ok(Map("message" -> s"Uploaded $filePath in ${completedParts.size} parts")).build() } } catch { case e: Exception => if (repoName != null && filePath != null && uploadId != null && physicalAddress != null) { LakeFSStorageClient.abortPresignedMultipartUploads( repoName, filePath, uploadId, physicalAddress ) } throw new WebApplicationException( s"Failed to upload file to dataset: ${e.getMessage}", e ) } } @GET @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/presign-download") def getPresignedUrl( @QueryParam("filePath") encodedUrl: String, @QueryParam("repositoryName") repositoryName: String, @QueryParam("commitHash") commitHash: String, @Auth user: SessionUser ): Response = { val uid = user.getUid generatePresignedResponse(encodedUrl, repositoryName, commitHash, uid) } @GET @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/presign-download-s3") def getPresignedUrlWithS3( @QueryParam("filePath") encodedUrl: String, @QueryParam("repositoryName") repositoryName: String, @QueryParam("commitHash") commitHash: String, @Auth user: SessionUser ): Response = { val uid = user.getUid generatePresignedResponse(encodedUrl, repositoryName, commitHash, uid) } @GET @Path("/public-presign-download") def getPublicPresignedUrl( @QueryParam("filePath") encodedUrl: String, @QueryParam("repositoryName") repositoryName: String, @QueryParam("commitHash") commitHash: String ): Response = { generatePresignedResponse(encodedUrl, repositoryName, commitHash, null) } @GET @Path("/public-presign-download-s3") def getPublicPresignedUrlWithS3( @QueryParam("filePath") encodedUrl: String, @QueryParam("repositoryName") repositoryName: String, @QueryParam("commitHash") commitHash: String ): Response = { generatePresignedResponse(encodedUrl, repositoryName, commitHash, null) } @DELETE @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/{did}/file") @Consumes(Array(MediaType.APPLICATION_JSON)) def deleteDatasetFile( @PathParam("did") did: Integer, @QueryParam("filePath") encodedFilePath: String, @Auth user: SessionUser ): Response = { val uid = user.getUid withTransaction(context) { ctx => if (!userHasWriteAccess(ctx, did, uid)) { throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) } val repositoryName = getDatasetByID(ctx, did).getRepositoryName // Decode the file path val filePath = URLDecoder.decode(encodedFilePath, StandardCharsets.UTF_8.name()) // Try to initialize the repository in LakeFS try { LakeFSStorageClient.deleteObject(repositoryName, filePath) } catch { case e: Exception => throw new WebApplicationException( s"Failed to delete the file from repo in LakeFS: ${e.getMessage}" ) } Response.ok().build() } } @POST @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/multipart-upload") @Consumes(Array(MediaType.APPLICATION_JSON)) def multipartUpload( @QueryParam("type") operationType: String, @QueryParam("ownerEmail") ownerEmail: String, @QueryParam("datasetName") datasetName: String, @QueryParam("filePath") filePath: String, @QueryParam("fileSizeBytes") fileSizeBytes: Optional[java.lang.Long], @QueryParam("partSizeBytes") partSizeBytes: Optional[java.lang.Long], @QueryParam("restart") restart: Optional[java.lang.Boolean], @Auth user: SessionUser ): Response = { val uid = user.getUid val dataset: Dataset = getDatasetBy(ownerEmail, datasetName) operationType.toLowerCase match { case "list" => listMultipartUploads(dataset.getDid, uid) case "init" => initMultipartUpload(dataset.getDid, filePath, fileSizeBytes, partSizeBytes, restart, uid) case "finish" => finishMultipartUpload(dataset.getDid, filePath, uid) case "abort" => abortMultipartUpload(dataset.getDid, filePath, uid) case _ => throw new BadRequestException("Invalid type parameter. Use 'init', 'finish', or 'abort'.") } } @POST @RolesAllowed(Array("REGULAR", "ADMIN")) @Consumes(Array(MediaType.APPLICATION_OCTET_STREAM)) @Path("/multipart-upload/part") def uploadPart( @QueryParam("ownerEmail") datasetOwnerEmail: String, @QueryParam("datasetName") datasetName: String, @QueryParam("filePath") encodedFilePath: String, @QueryParam("partNumber") partNumber: Int, partStream: InputStream, @Context headers: HttpHeaders, @Auth user: SessionUser ): Response = { val uid = user.getUid val dataset: Dataset = getDatasetBy(datasetOwnerEmail, datasetName) val did = dataset.getDid if (encodedFilePath == null || encodedFilePath.isEmpty) throw new BadRequestException("filePath is required") if (partNumber < 1) throw new BadRequestException("partNumber must be >= 1") val filePath = validateAndNormalizeFilePathOrThrow( URLDecoder.decode(encodedFilePath, StandardCharsets.UTF_8.name()) ) val contentLength = Option(headers.getHeaderString(HttpHeaders.CONTENT_LENGTH)) .map(_.trim) .flatMap(s => Try(s.toLong).toOption) .filter(_ > 0) .getOrElse { throw new BadRequestException("Invalid/Missing Content-Length") } withTransaction(context) { ctx => if (!userHasWriteAccess(ctx, did, uid)) throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) val session = ctx .selectFrom(DATASET_UPLOAD_SESSION) .where( DATASET_UPLOAD_SESSION.UID .eq(uid) .and(DATASET_UPLOAD_SESSION.DID.eq(did)) .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath)) ) .fetchOne() if (session == null) throw new NotFoundException("Upload session not found. Call type=init first.") val expectedParts: Int = session.getNumPartsRequested val fileSizeBytesValue: Long = session.getFileSizeBytes val partSizeBytesValue: Long = session.getPartSizeBytes if (fileSizeBytesValue <= 0L) { throw new WebApplicationException( s"Upload session has an invalid file size of $fileSizeBytesValue. Restart the upload.", Response.Status.INTERNAL_SERVER_ERROR ) } if (partSizeBytesValue <= 0L) { throw new WebApplicationException( s"Upload session has an invalid part size of $partSizeBytesValue. Restart the upload.", Response.Status.INTERNAL_SERVER_ERROR ) } // lastPartSize = fileSize - partSize*(expectedParts-1) val nMinus1: Long = expectedParts.toLong - 1L if (nMinus1 < 0L) { throw new WebApplicationException( s"Upload session has an invalid number of requested parts of $expectedParts. Restart the upload.", Response.Status.INTERNAL_SERVER_ERROR ) } if (nMinus1 > 0L && partSizeBytesValue > Long.MaxValue / nMinus1) { throw new WebApplicationException( "Overflow while computing last part size", Response.Status.INTERNAL_SERVER_ERROR ) } val prefixBytes: Long = partSizeBytesValue * nMinus1 if (prefixBytes > fileSizeBytesValue) { throw new WebApplicationException( s"Upload session is invalid: computed bytes before last part ($prefixBytes) exceed declared file size ($fileSizeBytesValue). Restart the upload.", Response.Status.INTERNAL_SERVER_ERROR ) } val lastPartSize: Long = fileSizeBytesValue - prefixBytes if (lastPartSize <= 0L || lastPartSize > partSizeBytesValue) { throw new WebApplicationException( s"Upload session is invalid: computed last part size ($lastPartSize bytes) must be within 1..$partSizeBytesValue bytes. Restart the upload.", Response.Status.INTERNAL_SERVER_ERROR ) } val allowedSize: Long = if (partNumber < expectedParts) partSizeBytesValue else lastPartSize if (partNumber > expectedParts) { throw new BadRequestException( s"$partNumber exceeds the requested parts on init: $expectedParts" ) } if (partNumber < expectedParts && contentLength < MINIMUM_NUM_OF_MULTIPART_S3_PART) { throw new BadRequestException( s"Part $partNumber is too small ($contentLength bytes). " + s"All non-final parts must be >= $MINIMUM_NUM_OF_MULTIPART_S3_PART bytes." ) } if (contentLength != allowedSize) { throw new BadRequestException( s"Invalid part size for partNumber=$partNumber. " + s"Expected Content-Length=$allowedSize, got $contentLength." ) } val physicalAddr = Option(session.getPhysicalAddress).map(_.trim).getOrElse("") if (physicalAddr.isEmpty) { throw new WebApplicationException( "Upload session is missing physicalAddress. Restart the upload.", Response.Status.INTERNAL_SERVER_ERROR ) } val uploadId = session.getUploadId val (bucket, key) = try LakeFSStorageClient.parsePhysicalAddress(physicalAddr) catch { case e: IllegalArgumentException => throw new WebApplicationException( s"Upload session has invalid physicalAddress. Restart the upload. (${e.getMessage})", Response.Status.INTERNAL_SERVER_ERROR ) } // Per-part lock: if another request is streaming the same part, fail fast. val partRow = try { ctx .selectFrom(DATASET_UPLOAD_SESSION_PART) .where( DATASET_UPLOAD_SESSION_PART.UPLOAD_ID .eq(uploadId) .and(DATASET_UPLOAD_SESSION_PART.PART_NUMBER.eq(partNumber)) ) .forUpdate() .noWait() .fetchOne() } catch { case e: DataAccessException if Option(e.getCause) .collect { case s: SQLException => s.getSQLState } .contains("55P03") => throw new WebApplicationException( s"Part $partNumber is already being uploaded", Response.Status.CONFLICT ) } if (partRow == null) { // Should not happen if init pre-created rows throw new WebApplicationException( s"Part row not initialized for part $partNumber. Restart the upload.", Response.Status.INTERNAL_SERVER_ERROR ) } // Idempotency: if ETag already set, accept the retry quickly. val existing = Option(partRow.getEtag).map(_.trim).getOrElse("") if (existing.isEmpty) { // Stream to S3 while holding the part lock (prevents concurrent streams for same part) val response: UploadPartResponse = S3StorageClient.uploadPartWithRequest( bucket = bucket, key = key, uploadId = uploadId, partNumber = partNumber, inputStream = partStream, contentLength = Some(contentLength) ) val etagClean = Option(response.eTag()).map(_.replace("\"", "")).map(_.trim).getOrElse("") if (etagClean.isEmpty) { throw new WebApplicationException( s"Missing ETag returned from S3 for part $partNumber", Response.Status.INTERNAL_SERVER_ERROR ) } ctx .update(DATASET_UPLOAD_SESSION_PART) .set(DATASET_UPLOAD_SESSION_PART.ETAG, etagClean) .where( DATASET_UPLOAD_SESSION_PART.UPLOAD_ID .eq(uploadId) .and(DATASET_UPLOAD_SESSION_PART.PART_NUMBER.eq(partNumber)) ) .execute() } Response.ok().build() } } @POST @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/{did}/update/publicity") def toggleDatasetPublicity( @PathParam("did") did: Integer, @Auth sessionUser: SessionUser ): Response = { withTransaction(context) { ctx => val datasetDao = new DatasetDao(ctx.configuration()) val uid = sessionUser.getUid if (!userHasWriteAccess(ctx, did, uid)) { throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) } val existedDataset = getDatasetByID(ctx, did) val newPublicStatus = !existedDataset.getIsPublic existedDataset.setIsPublic(newPublicStatus) datasetDao.update(existedDataset) Response.ok().build() } } @POST @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/{did}/update/downloadable") def toggleDatasetDownloadable( @PathParam("did") did: Integer, @Auth sessionUser: SessionUser ): Response = { withTransaction(context) { ctx => val datasetDao = new DatasetDao(ctx.configuration()) val uid = sessionUser.getUid if (!userOwnDataset(ctx, did, uid)) { throw new ForbiddenException("Only dataset owners can modify download permissions") } val existedDataset = getDatasetByID(ctx, did) val newDownloadableStatus = !existedDataset.getIsDownloadable existedDataset.setIsDownloadable(newDownloadableStatus) datasetDao.update(existedDataset) Response.ok().build() } } @GET @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/{did}/diff") def getDatasetDiff( @PathParam("did") did: Integer, @Auth user: SessionUser ): List[Diff] = { val uid = user.getUid withTransaction(context) { ctx => if (!userHasReadAccess(ctx, did, uid)) { throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) } // Retrieve staged (uncommitted) changes from LakeFS val dataset = getDatasetByID(ctx, did) val lakefsDiffs = withLakeFSErrorHandling { LakeFSStorageClient.retrieveUncommittedObjects(dataset.getRepositoryName) } // Convert LakeFS Diff objects to our custom Diff case class lakefsDiffs.map(d => new Diff( d.getPath, d.getPathType.getValue, d.getType.getValue, Option(d.getSizeBytes).map(_.longValue()) ) ) } } @PUT @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/{did}/diff") @Consumes(Array(MediaType.APPLICATION_JSON)) def resetDatasetFileDiff( @PathParam("did") did: Integer, @QueryParam("filePath") encodedFilePath: String, @Auth user: SessionUser ): Response = { val uid = user.getUid withTransaction(context) { ctx => if (!userHasWriteAccess(ctx, did, uid)) { throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) } val repositoryName = getDatasetByID(ctx, did).getRepositoryName // Decode the file path val filePath = URLDecoder.decode(encodedFilePath, StandardCharsets.UTF_8.name()) // Try to reset the file change in LakeFS try { LakeFSStorageClient.resetObjectUploadOrDeletion(repositoryName, filePath) } catch { case e: Exception => throw new WebApplicationException( s"Failed to reset the changes from repo in LakeFS: ${e.getMessage}" ) } Response.ok().build() } } /** * This method returns a list of DashboardDatasets objects that are accessible by current user. * * @param user the session user * @return list of user accessible DashboardDataset objects */ @GET @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/list") def listDatasets( @Auth user: SessionUser ): List[DashboardDataset] = { val uid = user.getUid withTransaction(context)(ctx => { var accessibleDatasets: ListBuffer[DashboardDataset] = ListBuffer() // first fetch all datasets user have explicit access to accessibleDatasets = ListBuffer.from( ctx .select() .from( DATASET .leftJoin(DATASET_USER_ACCESS) .on(DATASET_USER_ACCESS.DID.eq(DATASET.DID)) .leftJoin(USER) .on(USER.UID.eq(DATASET.OWNER_UID)) ) .where(DATASET_USER_ACCESS.UID.eq(uid)) .fetch() .map(record => { val dataset = record.into(DATASET).into(classOf[Dataset]) val datasetAccess = record.into(DATASET_USER_ACCESS).into(classOf[DatasetUserAccess]) val ownerEmail = record.into(USER).getEmail DashboardDataset( isOwner = dataset.getOwnerUid == uid, dataset = dataset, accessPrivilege = datasetAccess.getPrivilege, ownerEmail = ownerEmail, size = 0 ) }) .asScala ) // then we fetch the public datasets and merge it as a part of the result if not exist val publicDatasets = ctx .select() .from( DATASET .leftJoin(USER) .on(USER.UID.eq(DATASET.OWNER_UID)) ) .where(DATASET.IS_PUBLIC.eq(true)) .fetch() .map(record => { val dataset = record.into(DATASET).into(classOf[Dataset]) val ownerEmail = record.into(USER).getEmail DashboardDataset( isOwner = false, dataset = dataset, accessPrivilege = PrivilegeEnum.READ, ownerEmail = ownerEmail, size = LakeFSStorageClient.retrieveRepositorySize(dataset.getRepositoryName) ) }) publicDatasets.forEach { publicDataset => if (!accessibleDatasets.exists(_.dataset.getDid == publicDataset.dataset.getDid)) { val dashboardDataset = DashboardDataset( isOwner = false, dataset = publicDataset.dataset, ownerEmail = publicDataset.ownerEmail, accessPrivilege = PrivilegeEnum.READ, size = LakeFSStorageClient.retrieveRepositorySize(publicDataset.dataset.getRepositoryName) ) accessibleDatasets = accessibleDatasets :+ dashboardDataset } } accessibleDatasets.toList }) } @GET @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/{did}/version/list") def getDatasetVersionList( @PathParam("did") did: Integer, @Auth user: SessionUser ): List[DatasetVersion] = { val uid = user.getUid withTransaction(context)(ctx => { val dataset = getDatasetByID(ctx, did) if (!userHasReadAccess(ctx, dataset.getDid, uid)) { throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) } fetchDatasetVersions(ctx, dataset.getDid) }) } @GET @Path("/{name}/publicVersion/list") def getPublicDatasetVersionList( @PathParam("name") did: Integer ): List[DatasetVersion] = { withTransaction(context)(ctx => { if (!isDatasetPublic(ctx, did)) { throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) } fetchDatasetVersions(ctx, did) }) } @GET @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/{did}/version/latest") def retrieveLatestDatasetVersion( @PathParam("did") did: Integer, @Auth user: SessionUser ): DashboardDatasetVersion = { val uid = user.getUid withTransaction(context)(ctx => { if (!userHasReadAccess(ctx, did, uid)) { throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) } val dataset = getDatasetByID(ctx, did) val latestVersion = getLatestDatasetVersion(ctx, did).getOrElse( throw new NotFoundException(ERR_DATASET_VERSION_NOT_FOUND_MESSAGE) ) val ownerNode = DatasetFileNode .fromLakeFSRepositoryCommittedObjects( Map( (user.getEmail, dataset.getName, latestVersion.getName) -> LakeFSStorageClient .retrieveObjectsOfVersion(dataset.getRepositoryName, latestVersion.getVersionHash) ) ) .head DashboardDatasetVersion( latestVersion, ownerNode.children.get .find(_.getName == dataset.getName) .head .children .get .find(_.getName == latestVersion.getName) .head .children .get ) }) } @GET @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/{did}/versionZip") def getDatasetVersionZip( @PathParam("did") did: Integer, @QueryParam("dvid") dvid: Integer, // Dataset version ID, nullable @QueryParam("latest") latest: java.lang.Boolean, // Flag to get latest version, nullable @Auth user: SessionUser ): Response = { withTransaction(context) { ctx => if ((dvid != null && latest != null) || (dvid == null && latest == null)) { throw new BadRequestException("Specify exactly one: dvid= OR latest=true") } // Check read access and download permission val uid = user.getUid if (!userHasReadAccess(ctx, did, uid)) { throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) } // Retrieve dataset and check download permission val dataset = getDatasetByID(ctx, did) // Non-owners can download if dataset is downloadable and they have read access if (!userOwnDataset(ctx, did, uid) && !dataset.getIsDownloadable) { throw new ForbiddenException("Dataset download is not allowed") } // Determine which version to retrieve val datasetVersion = if (dvid != null) { getDatasetVersionByID(ctx, dvid) } else if (java.lang.Boolean.TRUE.equals(latest)) { getLatestDatasetVersion(ctx, did).getOrElse( throw new NotFoundException(ERR_DATASET_VERSION_NOT_FOUND_MESSAGE) ) } else { throw new BadRequestException("Invalid parameters") } // Retrieve dataset and version details val datasetName = dataset.getName val repositoryName = dataset.getRepositoryName val versionHash = datasetVersion.getVersionHash val objects = LakeFSStorageClient.retrieveObjectsOfVersion(repositoryName, versionHash) if (objects.isEmpty) { return Response .status(Response.Status.NOT_FOUND) .entity(s"No objects found in version $versionHash of repository $repositoryName") .build() } // StreamingOutput for ZIP download val streamingOutput = new StreamingOutput { override def write(outputStream: OutputStream): Unit = { val zipOut = new ZipOutputStream(outputStream) try { objects.foreach { obj => val filePath = obj.getPath val file = LakeFSStorageClient.getFileFromRepo(repositoryName, versionHash, filePath) zipOut.putNextEntry(new ZipEntry(filePath)) Files.copy(Paths.get(file.toURI), zipOut) zipOut.closeEntry() } } finally { zipOut.close() } } } val zipFilename = s"""attachment; filename="$datasetName-${datasetVersion.getName}.zip"""" Response .ok(streamingOutput, "application/zip") .header("Content-Disposition", zipFilename) .build() } } @GET @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/{did}/version/{dvid}/rootFileNodes") def retrieveDatasetVersionRootFileNodes( @PathParam("did") did: Integer, @PathParam("dvid") dvid: Integer, @Auth user: SessionUser ): DatasetVersionRootFileNodesResponse = { val uid = user.getUid withTransaction(context)(ctx => fetchDatasetVersionRootFileNodes(ctx, did, dvid, Some(uid))) } @GET @Path("/{did}/publicVersion/{dvid}/rootFileNodes") def retrievePublicDatasetVersionRootFileNodes( @PathParam("did") did: Integer, @PathParam("dvid") dvid: Integer ): DatasetVersionRootFileNodesResponse = { withTransaction(context)(ctx => fetchDatasetVersionRootFileNodes(ctx, did, dvid, None)) } @GET @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/{did}") def getDataset( @PathParam("did") did: Integer, @Auth user: SessionUser ): DashboardDataset = { val uid = user.getUid withTransaction(context)(ctx => getDashboardDataset(ctx, did, Some(uid))) } @GET @Path("/public/{did}") def getPublicDataset( @PathParam("did") did: Integer ): DashboardDataset = { withTransaction(context)(ctx => getDashboardDataset(ctx, did, None)) } /** * This method returns all owner user names of the dataset that the user has access to * * @return OwnerName[] */ @GET @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/user-dataset-owners") def retrieveOwners(@Auth user: SessionUser): util.List[String] = { context .selectDistinct(USER.EMAIL) .from(USER) .join(DATASET) .on(DATASET.OWNER_UID.eq(USER.UID)) .join(DATASET_USER_ACCESS) .on(DATASET_USER_ACCESS.DID.eq(DATASET.DID)) .where(DATASET_USER_ACCESS.UID.eq(user.getUid)) .fetchInto(classOf[String]) } /** * Validates the dataset name. * * Rules: * - Must be at least 1 character long. * - Only lowercase letters, numbers, underscores, and hyphens are allowed. * - Cannot start with a hyphen. * * @param name The dataset name to validate. * @throws java.lang.IllegalArgumentException if the name is invalid. */ private def validateDatasetName(name: String): Unit = { val datasetNamePattern = "^[A-Za-z0-9_-]+$".r if (!datasetNamePattern.matches(name)) { throw new IllegalArgumentException( s"Invalid dataset name: '$name'. " + "Dataset names must be at least 1 character long and " + "contain only lowercase letters, numbers, underscores, and hyphens, " + "and cannot start with a hyphen." ) } } private def fetchDatasetVersions(ctx: DSLContext, did: Integer): List[DatasetVersion] = { ctx .selectFrom(DATASET_VERSION) .where(DATASET_VERSION.DID.eq(did)) .orderBy(DATASET_VERSION.CREATION_TIME.desc()) // Change to .asc() for ascending order .fetchInto(classOf[DatasetVersion]) .asScala .toList } private def fetchDatasetVersionRootFileNodes( ctx: DSLContext, did: Integer, dvid: Integer, uid: Option[Integer] ): DatasetVersionRootFileNodesResponse = { val dataset = getDashboardDataset(ctx, did, uid) val datasetVersion = getDatasetVersionByID(ctx, dvid) val datasetName = dataset.dataset.getName val repositoryName = dataset.dataset.getRepositoryName val ownerFileNode = DatasetFileNode .fromLakeFSRepositoryCommittedObjects( Map( (dataset.ownerEmail, datasetName, datasetVersion.getName) -> LakeFSStorageClient .retrieveObjectsOfVersion(repositoryName, datasetVersion.getVersionHash) ) ) .head DatasetVersionRootFileNodesResponse( ownerFileNode.children.get .find(_.getName == datasetName) .head .children .get .find(_.getName == datasetVersion.getName) .head .children .get, DatasetFileNode.calculateTotalSize(List(ownerFileNode)) ) } private def generatePresignedResponse( encodedUrl: String, repositoryName: String, commitHash: String, uid: Integer ): Response = { resolveDatasetAndPath(encodedUrl, repositoryName, commitHash, uid) match { case Left(errorResponse) => errorResponse case Right((resolvedRepositoryName, resolvedCommitHash, resolvedFilePath)) => val url = LakeFSStorageClient.getFilePresignedUrl( resolvedRepositoryName, resolvedCommitHash, resolvedFilePath ) Response.ok(Map("presignedUrl" -> url)).build() } } private def resolveDatasetAndPath( encodedUrl: String, repositoryName: String, commitHash: String, uid: Integer ): Either[Response, (String, String, String)] = { val decodedPathStr = URLDecoder.decode(encodedUrl, StandardCharsets.UTF_8.name()) (Option(repositoryName), Option(commitHash)) match { case (Some(_), None) | (None, Some(_)) => // Case 1: Only one parameter is provided (error case) Left( Response .status(Response.Status.BAD_REQUEST) .entity( "Both repositoryName and commitHash must be provided together, or neither should be provided." ) .build() ) case (Some(repositoryName), Some(commit)) => // Case 2: repositoryName and commitHash are provided, validate access val response = withTransaction(context) { ctx => val datasetDao = new DatasetDao(ctx.configuration()) val datasets = datasetDao.fetchByRepositoryName(repositoryName).asScala.toList if (datasets.isEmpty || !userHasReadAccess(ctx, datasets.head.getDid, uid)) throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) val dataset = datasets.head // Standard read access check only - download restrictions handled per endpoint // Non-download operations (viewing) should work for all public datasets (repositoryName, commit, decodedPathStr) } Right(response) case (None, None) => // Case 3: Neither repositoryName nor commitHash are provided, resolve normally val response = withTransaction(context) { ctx => val fileUri = FileResolver.resolve(decodedPathStr) val document = DocumentFactory.openReadonlyDocument(fileUri).asInstanceOf[OnDataset] val datasetDao = new DatasetDao(ctx.configuration()) val datasets = datasetDao.fetchByRepositoryName(document.getRepositoryName()).asScala.toList if (datasets.isEmpty || !userHasReadAccess(ctx, datasets.head.getDid, uid)) throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) val dataset = datasets.head // Standard read access check only - download restrictions handled per endpoint // Non-download operations (viewing) should work for all public datasets ( document.getRepositoryName(), document.getVersionHash(), document.getFileRelativePath() ) } Right(response) } } // === Multipart helpers === private def getDatasetBy(ownerEmail: String, datasetName: String) = { val dataset = context .select(DATASET.fields: _*) .from(DATASET) .leftJoin(USER) .on(USER.UID.eq(DATASET.OWNER_UID)) .where(USER.EMAIL.eq(ownerEmail)) .and(DATASET.NAME.eq(datasetName)) .fetchOneInto(classOf[Dataset]) if (dataset == null) { throw new BadRequestException("Dataset not found") } dataset } private def listMultipartUploads(did: Integer, requesterUid: Int): Response = { withTransaction(context) { ctx => if (!userHasWriteAccess(ctx, did, requesterUid)) { throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) } val filePaths = ctx .selectDistinct(DATASET_UPLOAD_SESSION.FILE_PATH) .from(DATASET_UPLOAD_SESSION) .where(DATASET_UPLOAD_SESSION.DID.eq(did)) .and( DSL.condition( "created_at > current_timestamp - (? * interval '1 hour')", PHYSICAL_ADDRESS_EXPIRATION_TIME_HRS ) ) .orderBy(DATASET_UPLOAD_SESSION.FILE_PATH.asc()) .fetch(DATASET_UPLOAD_SESSION.FILE_PATH) .asScala .toList Response.ok(Map("filePaths" -> filePaths.asJava)).build() } } private def initMultipartUpload( did: Integer, encodedFilePath: String, fileSizeBytes: Optional[java.lang.Long], partSizeBytes: Optional[java.lang.Long], restart: Optional[java.lang.Boolean], uid: Integer ): Response = { withTransaction(context) { ctx => if (!userHasWriteAccess(ctx, did, uid)) { throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) } val dataset = getDatasetByID(ctx, did) val repositoryName = dataset.getRepositoryName val filePath = validateAndNormalizeFilePathOrThrow( URLDecoder.decode(encodedFilePath, StandardCharsets.UTF_8.name()) ) if (fileSizeBytes == null || !fileSizeBytes.isPresent) throw new BadRequestException("fileSizeBytes is required for initialization") if (partSizeBytes == null || !partSizeBytes.isPresent) throw new BadRequestException("partSizeBytes is required for initialization") val fileSizeBytesValue: Long = fileSizeBytes.get.longValue() val partSizeBytesValue: Long = partSizeBytes.get.longValue() if (fileSizeBytesValue <= 0L) throw new BadRequestException("fileSizeBytes must be > 0") if (partSizeBytesValue <= 0L) throw new BadRequestException("partSizeBytes must be > 0") val totalMaxBytes: Long = singleFileUploadMaxBytes() if (totalMaxBytes <= 0L) { throw new WebApplicationException( "singleFileUploadMaxBytes must be > 0", Response.Status.INTERNAL_SERVER_ERROR ) } if (fileSizeBytesValue > totalMaxBytes) { throw new BadRequestException( s"fileSizeBytes=$fileSizeBytesValue exceeds singleFileUploadMaxBytes=$totalMaxBytes" ) } val addend: Long = partSizeBytesValue - 1L if (addend < 0L || fileSizeBytesValue > Long.MaxValue - addend) { throw new WebApplicationException( "Overflow while computing numParts", Response.Status.INTERNAL_SERVER_ERROR ) } val numPartsLong: Long = (fileSizeBytesValue + addend) / partSizeBytesValue if (numPartsLong < 1L || numPartsLong > MAXIMUM_NUM_OF_MULTIPART_S3_PARTS.toLong) { throw new BadRequestException( s"Computed numParts=$numPartsLong is out of range 1..$MAXIMUM_NUM_OF_MULTIPART_S3_PARTS" ) } val computedNumParts: Int = numPartsLong.toInt if (computedNumParts > 1 && partSizeBytesValue < MINIMUM_NUM_OF_MULTIPART_S3_PART) { throw new BadRequestException( s"partSizeBytes=$partSizeBytesValue is too small. " + s"All non-final parts must be >= $MINIMUM_NUM_OF_MULTIPART_S3_PART bytes." ) } var session: DatasetUploadSessionRecord = null var rows: Result[Record2[Integer, String]] = null try { session = ctx .selectFrom(DATASET_UPLOAD_SESSION) .where( DATASET_UPLOAD_SESSION.UID .eq(uid) .and(DATASET_UPLOAD_SESSION.DID.eq(did)) .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath)) ) .forUpdate() .noWait() .fetchOne() if (session != null) { //Gain parts lock rows = ctx .select(DATASET_UPLOAD_SESSION_PART.PART_NUMBER, DATASET_UPLOAD_SESSION_PART.ETAG) .from(DATASET_UPLOAD_SESSION_PART) .where(DATASET_UPLOAD_SESSION_PART.UPLOAD_ID.eq(session.getUploadId)) .forUpdate() .noWait() .fetch() val dbFileSize = session.getFileSizeBytes val dbPartSize = session.getPartSizeBytes val dbNumParts = session.getNumPartsRequested val createdAt: OffsetDateTime = session.getCreatedAt val isExpired = createdAt .plusHours(PHYSICAL_ADDRESS_EXPIRATION_TIME_HRS.toLong) .isBefore(OffsetDateTime.now(createdAt.getOffset)) // or OffsetDateTime.now() val conflictConfig = dbFileSize != fileSizeBytesValue || dbPartSize != partSizeBytesValue || dbNumParts != computedNumParts || isExpired || Option(restart).exists(_.orElse(false)) if (conflictConfig) { // Parts will be deleted automatically (ON DELETE CASCADE) ctx .deleteFrom(DATASET_UPLOAD_SESSION) .where(DATASET_UPLOAD_SESSION.UPLOAD_ID.eq(session.getUploadId)) .execute() try { LakeFSStorageClient.abortPresignedMultipartUploads( repositoryName, filePath, session.getUploadId, session.getPhysicalAddress ) } catch { case _: Throwable => () } session = null rows = null } } } catch { case e: DataAccessException if Option(e.getCause) .collect { case s: SQLException => s.getSQLState } .contains("55P03") => throw new WebApplicationException( "Another client is uploading this file", Response.Status.CONFLICT ) } if (session == null) { val presign = withLakeFSErrorHandling { LakeFSStorageClient.initiatePresignedMultipartUploads( repositoryName, filePath, computedNumParts ) } val uploadIdStr = presign.getUploadId val physicalAddr = presign.getPhysicalAddress try { val rowsInserted = ctx .insertInto(DATASET_UPLOAD_SESSION) .set(DATASET_UPLOAD_SESSION.FILE_PATH, filePath) .set(DATASET_UPLOAD_SESSION.DID, did) .set(DATASET_UPLOAD_SESSION.UID, uid) .set(DATASET_UPLOAD_SESSION.UPLOAD_ID, uploadIdStr) .set(DATASET_UPLOAD_SESSION.PHYSICAL_ADDRESS, physicalAddr) .set(DATASET_UPLOAD_SESSION.NUM_PARTS_REQUESTED, Integer.valueOf(computedNumParts)) .set(DATASET_UPLOAD_SESSION.FILE_SIZE_BYTES, java.lang.Long.valueOf(fileSizeBytesValue)) .set(DATASET_UPLOAD_SESSION.PART_SIZE_BYTES, java.lang.Long.valueOf(partSizeBytesValue)) .onDuplicateKeyIgnore() .execute() if (rowsInserted == 1) { val partNumberSeries = DSL.generateSeries(1, computedNumParts).asTable("gs", "partNumberField") val partNumberField = partNumberSeries.field("partNumberField", classOf[Integer]) ctx .insertInto( DATASET_UPLOAD_SESSION_PART, DATASET_UPLOAD_SESSION_PART.UPLOAD_ID, DATASET_UPLOAD_SESSION_PART.PART_NUMBER, DATASET_UPLOAD_SESSION_PART.ETAG ) .select( ctx .select( inl(uploadIdStr), partNumberField, inl("") ) .from(partNumberSeries) ) .execute() session = ctx .selectFrom(DATASET_UPLOAD_SESSION) .where( DATASET_UPLOAD_SESSION.UID .eq(uid) .and(DATASET_UPLOAD_SESSION.DID.eq(did)) .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath)) ) .fetchOne() } else { try { LakeFSStorageClient.abortPresignedMultipartUploads( repositoryName, filePath, uploadIdStr, physicalAddr ) } catch { case _: Throwable => () } session = ctx .selectFrom(DATASET_UPLOAD_SESSION) .where( DATASET_UPLOAD_SESSION.UID .eq(uid) .and(DATASET_UPLOAD_SESSION.DID.eq(did)) .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath)) ) .fetchOne() } } catch { case e: Exception => try { LakeFSStorageClient.abortPresignedMultipartUploads( repositoryName, filePath, uploadIdStr, physicalAddr ) } catch { case _: Throwable => () } throw e } } if (session == null) { throw new WebApplicationException( "Failed to create or locate upload session", Response.Status.INTERNAL_SERVER_ERROR ) } val dbNumParts = session.getNumPartsRequested val uploadId = session.getUploadId val nParts = dbNumParts // CHANGED: lock rows with NOWAIT; if any row is locked by another uploader -> 409 if (rows == null) { rows = try { ctx .select(DATASET_UPLOAD_SESSION_PART.PART_NUMBER, DATASET_UPLOAD_SESSION_PART.ETAG) .from(DATASET_UPLOAD_SESSION_PART) .where(DATASET_UPLOAD_SESSION_PART.UPLOAD_ID.eq(uploadId)) .forUpdate() .noWait() .fetch() } catch { case e: DataAccessException if Option(e.getCause) .collect { case s: SQLException => s.getSQLState } .contains("55P03") => throw new WebApplicationException( "Another client is uploading parts for this file", Response.Status.CONFLICT ) } } // CHANGED: compute missingParts + completedPartsCount from the SAME query result val missingParts = rows.asScala .filter(r => Option(r.get(DATASET_UPLOAD_SESSION_PART.ETAG)).map(_.trim).getOrElse("").isEmpty ) .map(r => r.get(DATASET_UPLOAD_SESSION_PART.PART_NUMBER).intValue()) .toList val completedPartsCount = nParts - missingParts.size Response .ok( Map( "missingParts" -> missingParts.asJava, "completedPartsCount" -> Integer.valueOf(completedPartsCount) ) ) .build() } } private def finishMultipartUpload( did: Integer, encodedFilePath: String, uid: Int ): Response = { val filePath = validateAndNormalizeFilePathOrThrow( URLDecoder.decode(encodedFilePath, StandardCharsets.UTF_8.name()) ) withTransaction(context) { ctx => if (!userHasWriteAccess(ctx, did, uid)) { throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) } val dataset = getDatasetByID(ctx, did) // Lock the session so abort/finish don't race each other val session = try { ctx .selectFrom(DATASET_UPLOAD_SESSION) .where( DATASET_UPLOAD_SESSION.UID .eq(uid) .and(DATASET_UPLOAD_SESSION.DID.eq(did)) .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath)) ) .forUpdate() .noWait() .fetchOne() } catch { case e: DataAccessException if Option(e.getCause) .collect { case s: SQLException => s.getSQLState } .contains("55P03") => throw new WebApplicationException( "Upload is already being finalized/aborted", Response.Status.CONFLICT ) } if (session == null) { throw new NotFoundException("Upload session not found or already finalized") } val uploadId = session.getUploadId val expectedParts = session.getNumPartsRequested val physicalAddr = Option(session.getPhysicalAddress).map(_.trim).getOrElse("") if (physicalAddr.isEmpty) { throw new WebApplicationException( "Upload session is missing physicalAddress. Restart the upload.", Response.Status.INTERNAL_SERVER_ERROR ) } val total = DSL.count() val done = DSL .count() .filterWhere(DATASET_UPLOAD_SESSION_PART.ETAG.ne("")) .as("done") val agg = ctx .select(total.as("total"), done) .from(DATASET_UPLOAD_SESSION_PART) .where(DATASET_UPLOAD_SESSION_PART.UPLOAD_ID.eq(uploadId)) .fetchOne() val totalCnt = agg.get("total", classOf[java.lang.Integer]).intValue() val doneCnt = agg.get("done", classOf[java.lang.Integer]).intValue() if (totalCnt != expectedParts) { throw new WebApplicationException( s"Part table mismatch: expected $expectedParts rows but found $totalCnt. Restart the upload.", Response.Status.INTERNAL_SERVER_ERROR ) } if (doneCnt != expectedParts) { val missing = ctx .select(DATASET_UPLOAD_SESSION_PART.PART_NUMBER) .from(DATASET_UPLOAD_SESSION_PART) .where( DATASET_UPLOAD_SESSION_PART.UPLOAD_ID .eq(uploadId) .and(DATASET_UPLOAD_SESSION_PART.ETAG.eq("")) ) .orderBy(DATASET_UPLOAD_SESSION_PART.PART_NUMBER.asc()) .limit(50) .fetch(DATASET_UPLOAD_SESSION_PART.PART_NUMBER) .asScala .toList throw new WebApplicationException( s"Upload incomplete. Some missing ETags for parts are: ${missing.mkString(",")}", Response.Status.CONFLICT ) } // Build partsList in order val partsList: List[(Int, String)] = ctx .select(DATASET_UPLOAD_SESSION_PART.PART_NUMBER, DATASET_UPLOAD_SESSION_PART.ETAG) .from(DATASET_UPLOAD_SESSION_PART) .where(DATASET_UPLOAD_SESSION_PART.UPLOAD_ID.eq(uploadId)) .orderBy(DATASET_UPLOAD_SESSION_PART.PART_NUMBER.asc()) .fetch() .asScala .map(r => ( r.get(DATASET_UPLOAD_SESSION_PART.PART_NUMBER).intValue(), r.get(DATASET_UPLOAD_SESSION_PART.ETAG) ) ) .toList val objectStats = withLakeFSErrorHandling { LakeFSStorageClient.completePresignedMultipartUploads( dataset.getRepositoryName, filePath, uploadId, partsList, physicalAddr ) } // FINAL SERVER-SIDE SIZE CHECK (do not rely on init) val actualSizeBytes = Option(objectStats.getSizeBytes).map(_.longValue()).getOrElse(-1L) if (actualSizeBytes <= 0L) { throw new WebApplicationException( "lakeFS did not return sizeBytes for completed multipart upload", Response.Status.INTERNAL_SERVER_ERROR ) } val maxBytes = singleFileUploadMaxBytes() val tooLarge = actualSizeBytes > maxBytes if (tooLarge) { try { LakeFSStorageClient.resetObjectUploadOrDeletion(dataset.getRepositoryName, filePath) } catch { case _: Throwable => () } } // always cleanup session ctx .deleteFrom(DATASET_UPLOAD_SESSION) .where( DATASET_UPLOAD_SESSION.UID .eq(uid) .and(DATASET_UPLOAD_SESSION.DID.eq(did)) .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath)) ) .execute() if (tooLarge) { throw new WebApplicationException( s"Upload exceeded max size: actualSizeBytes=$actualSizeBytes maxBytes=$maxBytes", Response.Status.REQUEST_ENTITY_TOO_LARGE ) } Response .ok( Map( "message" -> "Multipart upload completed successfully", "filePath" -> objectStats.getPath ) ) .build() } } private def abortMultipartUpload( did: Integer, encodedFilePath: String, uid: Int ): Response = { val filePath = validateAndNormalizeFilePathOrThrow( URLDecoder.decode(encodedFilePath, StandardCharsets.UTF_8.name()) ) val (repoName, uploadId, physicalAddr) = withTransaction(context) { ctx => if (!userHasWriteAccess(ctx, did, uid)) { throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) } val dataset = getDatasetByID(ctx, did) val session = try { ctx .selectFrom(DATASET_UPLOAD_SESSION) .where( DATASET_UPLOAD_SESSION.UID .eq(uid) .and(DATASET_UPLOAD_SESSION.DID.eq(did)) .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath)) ) .forUpdate() .noWait() .fetchOne() } catch { case e: DataAccessException if Option(e.getCause) .collect { case s: SQLException => s.getSQLState } .contains("55P03") => throw new WebApplicationException( "Upload is already being finalized/aborted", Response.Status.CONFLICT ) } if (session == null) { throw new NotFoundException("Upload session not found or already finalized") } val physicalAddr = Option(session.getPhysicalAddress).map(_.trim).getOrElse("") // Delete session; parts removed via ON DELETE CASCADE ctx .deleteFrom(DATASET_UPLOAD_SESSION) .where( DATASET_UPLOAD_SESSION.UID .eq(uid) .and(DATASET_UPLOAD_SESSION.DID.eq(did)) .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath)) ) .execute() (dataset.getRepositoryName, session.getUploadId, physicalAddr) } withLakeFSErrorHandling { LakeFSStorageClient.abortPresignedMultipartUploads(repoName, filePath, uploadId, physicalAddr) } Response.ok(Map("message" -> "Multipart upload aborted successfully")).build() } /** * Updates the cover image for a dataset. * * @param did Dataset ID * @param request Cover image request containing the relative file path * @param sessionUser Authenticated user session * @return Response with updated cover image path * * Expected coverImage format: "version/folder/image.jpg" (relative to dataset root) */ @POST @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/{did}/update/cover") @Consumes(Array(MediaType.APPLICATION_JSON)) def updateDatasetCoverImage( @PathParam("did") did: Integer, request: CoverImageRequest, @Auth sessionUser: SessionUser ): Response = { withTransaction(context) { ctx => val uid = sessionUser.getUid val dataset = getDatasetByID(ctx, did) if (!userHasWriteAccess(ctx, did, uid)) { throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) } if (request.coverImage == null || request.coverImage.trim.isEmpty) { throw new BadRequestException("Cover image path is required") } val normalized = DatasetResource.validateAndNormalizeFilePathOrThrow(request.coverImage) val extension = FilenameUtils.getExtension(normalized) if (extension == null || !ALLOWED_IMAGE_EXTENSIONS.contains(s".$extension".toLowerCase)) { throw new BadRequestException("Invalid file type") } val owner = getOwner(ctx, did) val document = DocumentFactory .openReadonlyDocument( FileResolver.resolve(s"${owner.getEmail}/${dataset.getName}/$normalized") ) .asInstanceOf[OnDataset] val fileSize = LakeFSStorageClient.getFileSize( document.getRepositoryName(), document.getVersionHash(), document.getFileRelativePath() ) if (fileSize > COVER_IMAGE_SIZE_LIMIT_BYTES) { throw new BadRequestException( s"Cover image must be less than ${COVER_IMAGE_SIZE_LIMIT_BYTES / (1024 * 1024)} MB" ) } dataset.setCoverImage(normalized) new DatasetDao(ctx.configuration()).update(dataset) Response.ok(Map("coverImage" -> normalized)).build() } } /** * Get the cover image for a dataset. * Returns a 307 redirect to the presigned S3 URL. * * @param did Dataset ID * @return 307 Temporary Redirect to cover image */ @GET @Path("/{did}/cover") def getDatasetCover( @PathParam("did") did: Integer, @Auth sessionUser: Optional[SessionUser] ): Response = { withTransaction(context) { ctx => val dataset = getDatasetByID(ctx, did) val requesterUid = if (sessionUser.isPresent) Some(sessionUser.get().getUid) else None if (requesterUid.isEmpty && !dataset.getIsPublic) { throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) } else if (requesterUid.exists(uid => !userHasReadAccess(ctx, did, uid))) { throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE) } val coverImage = Option(dataset.getCoverImage).getOrElse( throw new NotFoundException("No cover image") ) val owner = getOwner(ctx, did) val fullPath = s"${owner.getEmail}/${dataset.getName}/$coverImage" val document = DocumentFactory .openReadonlyDocument(FileResolver.resolve(fullPath)) .asInstanceOf[OnDataset] val presignedUrl = LakeFSStorageClient.getFilePresignedUrl( document.getRepositoryName(), document.getVersionHash(), document.getFileRelativePath() ) Response.temporaryRedirect(new URI(presignedUrl)).build() } } } ================================================ FILE: file-service/src/main/scala/org/apache/texera/service/resource/HealthCheckResource.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.resource import jakarta.ws.rs.core.MediaType import jakarta.ws.rs.{GET, Path, Produces} @Path("/healthcheck") @Produces(Array(MediaType.APPLICATION_JSON)) class HealthCheckResource { @GET def healthCheck: Map[String, String] = Map("status" -> "ok") } ================================================ FILE: file-service/src/main/scala/org/apache/texera/service/type/dataset/DatasetFileNode.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.`type` import io.lakefs.clients.sdk.model.ObjectStats import org.apache.texera.amber.core.storage.util.dataset.PhysicalFileNode import java.util import scala.collection.mutable // DatasetFileNode represents a unique file in dataset, its full path is in the format of: // /ownerEmail/datasetName/versionName/fileRelativePath // e.g. /bob@texera.com/twitterDataset/v1/california/irvine/tw1.csv // ownerName is bob@texera.com; datasetName is twitterDataset, versionName is v1, fileRelativePath is california/irvine/tw1.csv class DatasetFileNode( val name: String, // direct name of this node val nodeType: String, // "file" or "directory" val parent: DatasetFileNode, // the parent node val ownerEmail: String, val size: Option[Long] = None, // size of the file in bytes, None if directory var children: Option[List[DatasetFileNode]] = None // Only populated if 'type' is 'directory' ) { // Ensure the type is either "file" or "directory" require(nodeType == "file" || nodeType == "directory", "type must be 'file' or 'directory'") // Getters for the parameters def getName: String = name def getNodeType: String = nodeType def getParent: DatasetFileNode = parent def getOwnerEmail: String = ownerEmail def getSize: Option[Long] = size def getChildren: List[DatasetFileNode] = children.getOrElse(List()) // Method to get the full file path def getFilePath: String = { val pathComponents = new mutable.ArrayBuffer[String]() var currentNode: DatasetFileNode = this while (currentNode != null) { if (currentNode.parent != null) { // Skip the root node to avoid double slashes pathComponents.prepend(currentNode.name) } currentNode = currentNode.parent } "/" + pathComponents.mkString("/") } } object DatasetFileNode { /** * Converts a map of LakeFS committed objects into a structured dataset file node tree. * * @param map A mapping from `(ownerEmail, datasetName, versionName)` to a list of committed objects. * @return A list of root-level dataset file nodes. */ def fromLakeFSRepositoryCommittedObjects( map: Map[(String, String, String), List[ObjectStats]] ): List[DatasetFileNode] = { val rootNode = new DatasetFileNode("/", "directory", null, "") // Owner level nodes map val ownerNodes = mutable.Map[String, DatasetFileNode]() map.foreach { case ((ownerEmail, datasetName, versionName), objects) => val ownerNode = ownerNodes.getOrElseUpdate( ownerEmail, { val newNode = new DatasetFileNode(ownerEmail, "directory", rootNode, ownerEmail) rootNode.children = Some(rootNode.getChildren :+ newNode) newNode } ) val datasetNode = ownerNode.getChildren.find(_.getName == datasetName).getOrElse { val newNode = new DatasetFileNode(datasetName, "directory", ownerNode, ownerEmail) ownerNode.children = Some(ownerNode.getChildren :+ newNode) newNode } val versionNode = datasetNode.getChildren.find(_.getName == versionName).getOrElse { val newNode = new DatasetFileNode(versionName, "directory", datasetNode, ownerEmail) datasetNode.children = Some(datasetNode.getChildren :+ newNode) newNode } // Directory map for efficient lookups val directoryMap = mutable.Map[String, DatasetFileNode]() directoryMap("") = versionNode // Root of the dataset version // Process each object (file or directory) from LakeFS objects.foreach { obj => val pathParts = obj.getPath.split("/").toList var currentPath = "" var parentNode: DatasetFileNode = versionNode pathParts.foreach { part => currentPath = if (currentPath.isEmpty) part else s"$currentPath/$part" val isFile = pathParts.last == part val nodeType = if (isFile) "file" else "directory" val fileSize = if (isFile) Some(obj.getSizeBytes.longValue()) else None val existingNode = directoryMap.get(currentPath) val node = existingNode.getOrElse { val newNode = new DatasetFileNode(part, nodeType, parentNode, ownerEmail, fileSize) parentNode.children = Some(parentNode.getChildren :+ newNode) if (!isFile) directoryMap(currentPath) = newNode newNode } parentNode = node // Move parent reference deeper for next iteration } } } // Sorting function to sort children of a node alphabetically in descending order def sortChildren(node: DatasetFileNode): Unit = { node.children = Some(node.getChildren.sortBy(_.getName)(Ordering.String.reverse)) node.getChildren.foreach(sortChildren) } // Apply the sorting to the root node sortChildren(rootNode) rootNode.getChildren } def fromPhysicalFileNodes( map: Map[(String, String, String), List[PhysicalFileNode]] ): List[DatasetFileNode] = { val rootNode = new DatasetFileNode("/", "directory", null, "") val ownerNodes = mutable.Map[String, DatasetFileNode]() map.foreach { case ((ownerEmail, datasetName, versionName), physicalNodes) => val ownerNode = ownerNodes.getOrElseUpdate( ownerEmail, { val newNode = new DatasetFileNode(ownerEmail, "directory", rootNode, ownerEmail) rootNode.children = Some(rootNode.getChildren :+ newNode) newNode } ) val datasetNode = ownerNode.getChildren.find(_.getName == datasetName).getOrElse { val newNode = new DatasetFileNode(datasetName, "directory", ownerNode, ownerEmail) ownerNode.children = Some(ownerNode.getChildren :+ newNode) newNode } val versionNode = datasetNode.getChildren.find(_.getName == versionName).getOrElse { val newNode = new DatasetFileNode(versionName, "directory", datasetNode, ownerEmail) datasetNode.children = Some(datasetNode.getChildren :+ newNode) newNode } physicalNodes.foreach(node => addNodeToTree(versionNode, node, ownerEmail)) } // Sorting function to sort children of a node alphabetically in descending order def sortChildren(node: DatasetFileNode): Unit = { node.children = Some(node.getChildren.sortBy(_.getName)(Ordering.String.reverse)) node.getChildren.foreach(sortChildren) } // Apply the sorting to the root node sortChildren(rootNode) rootNode.getChildren } private def addNodeToTree( parentNode: DatasetFileNode, physicalNode: PhysicalFileNode, ownerEmail: String ): Unit = { val queue = new util.LinkedList[(DatasetFileNode, PhysicalFileNode)]() queue.add((parentNode, physicalNode)) while (!queue.isEmpty) { val (currentParent, currentPhysicalNode) = queue.poll() val relativePath = currentPhysicalNode.getRelativePath.toString.split("/").toList val nodeName = relativePath.last val fileType = if (currentPhysicalNode.isDirectory) "directory" else "file" val fileSize = if (fileType == "file") Some(currentPhysicalNode.getSize) else None val existingNode = currentParent.getChildren.find(child => child.getName == nodeName && child.getNodeType == fileType ) val fileNode = existingNode.getOrElse { val newNode = new DatasetFileNode( nodeName, fileType, currentParent, ownerEmail, fileSize ) currentParent.children = Some(currentParent.getChildren :+ newNode) newNode } // Add children of the current physical node to the queue currentPhysicalNode.getChildren.forEach(child => queue.add((fileNode, child))) } } /** * Traverses a given list of DatasetFileNode and returns the total size of all files. * * @param nodes List of root-level DatasetFileNode. * @return Total size in bytes. */ def calculateTotalSize(nodes: List[DatasetFileNode]): Long = { def traverse(node: DatasetFileNode): Long = { val fileSize = node.getSize.getOrElse(0L) val childrenSize = node.getChildren.map(traverse).sum fileSize + childrenSize } nodes.map(traverse).sum } } ================================================ FILE: file-service/src/main/scala/org/apache/texera/service/type/serde/DatasetFileNodeSerializer.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.type.serde; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.std.StdSerializer; import org.apache.texera.service.type.DatasetFileNode; import scala.collection.JavaConverters; import scala.collection.immutable.List; import java.io.IOException; // this class is used to serialize the FileNode as JSON. So that FileNodes can be inspected by the frontend through JSON. public class DatasetFileNodeSerializer extends StdSerializer { public DatasetFileNodeSerializer() { this(null); } public DatasetFileNodeSerializer(Class t) { super(t); } @Override public void serialize(DatasetFileNode value, JsonGenerator gen, SerializerProvider provider) throws IOException { gen.writeStartObject(); gen.writeStringField("name", value.getName()); gen.writeStringField("type", value.getNodeType()); gen.writeStringField("parentDir", value.getParent().getFilePath()); gen.writeStringField("ownerEmail", value.getOwnerEmail()); if (value.getNodeType().equals("file")) { gen.writeObjectField("size", value.getSize()); } if (value.getNodeType().equals("directory")) { gen.writeFieldName("children"); gen.writeStartArray(); List children = value.getChildren(); for (DatasetFileNode child : JavaConverters.seqAsJavaList(children)) { serialize(child, gen, provider); // Recursively serialize children } gen.writeEndArray(); } gen.writeEndObject(); } } ================================================ FILE: file-service/src/main/scala/org/apache/texera/service/util/LakeFSExceptionHandler.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.util import jakarta.ws.rs._ import jakarta.ws.rs.core.{MediaType, Response} import org.slf4j.LoggerFactory import scala.jdk.CollectionConverters._ object LakeFSExceptionHandler { private val logger = LoggerFactory.getLogger(getClass) private val fallbackMessages = Map( 400 -> "LakeFS rejected the request. Please verify the parameters (repository/branch/path) and try again.", 401 -> "Authentication with LakeFS failed.", 403 -> "Permission denied by LakeFS.", 404 -> "LakeFS resource not found. The repository/branch/object may not exist.", 409 -> "LakeFS reported a conflict. Another operation may be in progress.", 420 -> "Too many requests to LakeFS." ).withDefaultValue( "LakeFS request failed due to an unexpected server error." ) /** * Wraps a LakeFS call with centralized error handling. */ def withLakeFSErrorHandling[T](call: => T): T = { try { call } catch { case e: io.lakefs.clients.sdk.ApiException => handleException(e) } } /** * Converts LakeFS ApiException to appropriate HTTP exception */ private def handleException(e: io.lakefs.clients.sdk.ApiException): Nothing = { val code = e.getCode val rawBody = Option(e.getResponseBody).filter(_.nonEmpty) val message = s"${fallbackMessages(code)}" logger.warn(s"LakeFS error $code, ${e.getMessage}, body: ${rawBody.getOrElse("N/A")}") def errorResponse(status: Int): Response = Response .status(status) .entity(Map("message" -> message).asJava) .`type`(MediaType.APPLICATION_JSON) .build() throw (code match { case 400 => new BadRequestException(errorResponse(400)) case 401 => new NotAuthorizedException(errorResponse(401)) case 403 => new ForbiddenException(errorResponse(403)) case 404 => new NotFoundException(errorResponse(404)) case c if c >= 400 && c < 500 => new WebApplicationException(errorResponse(c)) case _ => new InternalServerErrorException(errorResponse(500)) }) } } ================================================ FILE: file-service/src/test/scala/org/apache/texera/service/MockLakeFS.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service import com.dimafeng.testcontainers._ import io.lakefs.clients.sdk.{ApiClient, RepositoriesApi} import org.apache.texera.amber.config.StorageConfig import org.apache.texera.service.util.S3StorageClient import org.scalatest.{BeforeAndAfterAll, Suite} import org.testcontainers.containers.Network import org.testcontainers.utility.DockerImageName import software.amazon.awssdk.auth.credentials.{AwsBasicCredentials, StaticCredentialsProvider} import software.amazon.awssdk.regions.Region import software.amazon.awssdk.services.s3.S3Client import software.amazon.awssdk.services.s3.S3Configuration import java.net.URI /** * Trait to spin up a LakeFS + MinIO + Postgres stack using Testcontainers, * similar to how MockTexeraDB uses EmbeddedPostgres. */ trait MockLakeFS extends ForAllTestContainer with BeforeAndAfterAll { self: Suite => // network for containers to communicate val network: Network = Network.newNetwork() // Postgres for LakeFS metadata val postgres: PostgreSQLContainer = PostgreSQLContainer .Def( dockerImageName = DockerImageName.parse("postgres:15"), databaseName = "texera_lakefs", username = "texera_lakefs_admin", password = "password" ) .createContainer() postgres.container.withNetwork(network) // MinIO for object storage val minio = MinIOContainer( dockerImageName = DockerImageName.parse("minio/minio:RELEASE.2025-02-28T09-55-16Z"), userName = "texera_minio", password = "password" ) minio.container.withNetwork(network) // LakeFS val lakefsDatabaseURL: String = s"postgresql://${postgres.username}:${postgres.password}" + s"@${postgres.container.getNetworkAliases.get(0)}:5432/${postgres.databaseName}" + s"?sslmode=disable" val lakefsUsername = "texera-admin" // These are the API credentials created/used during setup. // In lakeFS, the access key + secret key are used as basic-auth username/password for the API. val lakefsAccessKeyID = "AKIAIOSFOLKFSSAMPLES" val lakefsSecretAccessKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" val lakefs = GenericContainer( dockerImage = "treeverse/lakefs:1.51", exposedPorts = Seq(8000), env = Map( "LAKEFS_BLOCKSTORE_TYPE" -> "s3", "LAKEFS_BLOCKSTORE_S3_FORCE_PATH_STYLE" -> "true", "LAKEFS_BLOCKSTORE_S3_ENDPOINT" -> s"http://${minio.container.getNetworkAliases.get(0)}:9000", "LAKEFS_BLOCKSTORE_S3_PRE_SIGNED_ENDPOINT" -> "http://localhost:9000", "LAKEFS_BLOCKSTORE_S3_CREDENTIALS_ACCESS_KEY_ID" -> "texera_minio", "LAKEFS_BLOCKSTORE_S3_CREDENTIALS_SECRET_ACCESS_KEY" -> "password", "LAKEFS_AUTH_ENCRYPT_SECRET_KEY" -> "random_string_for_lakefs", "LAKEFS_LOGGING_LEVEL" -> "INFO", "LAKEFS_STATS_ENABLED" -> "1", "LAKEFS_DATABASE_TYPE" -> "postgres", "LAKEFS_DATABASE_POSTGRES_CONNECTION_STRING" -> lakefsDatabaseURL, "LAKEFS_INSTALLATION_USER_NAME" -> lakefsUsername, "LAKEFS_INSTALLATION_ACCESS_KEY_ID" -> lakefsAccessKeyID, "LAKEFS_INSTALLATION_SECRET_ACCESS_KEY" -> lakefsSecretAccessKey ) ) lakefs.container.withNetwork(network) override val container = MultipleContainers(postgres, minio, lakefs) def lakefsBaseUrl: String = s"http://${lakefs.host}:${lakefs.mappedPort(8000)}" def minioEndpoint: String = s"http://${minio.host}:${minio.mappedPort(9000)}" def lakefsApiBasePath: String = s"$lakefsBaseUrl/api/v1" // ---- Clients (lazy so they initialize after containers are started) ---- lazy val lakefsApiClient: ApiClient = { val apiClient = new ApiClient() apiClient.setBasePath(lakefsApiBasePath) // basic-auth for lakeFS API uses accessKey as username, secretKey as password apiClient.setUsername(lakefsAccessKeyID) apiClient.setPassword(lakefsSecretAccessKey) apiClient } lazy val repositoriesApi: RepositoriesApi = new RepositoriesApi(lakefsApiClient) /** * S3 client instance for testing pointed at MinIO. * * Notes: * - Region can be any value for MinIO, but MUST match what your signing expects. * so we use that. * - Path-style is important: http://host:port/bucket/key */ lazy val s3Client: S3Client = { //Temporal credentials for testing purposes only val creds = AwsBasicCredentials.create("texera_minio", "password") S3Client .builder() .endpointOverride(URI.create(StorageConfig.s3Endpoint)) // set in afterStart() .region(Region.US_WEST_2) // Required for `.build()`; not important in this test config. .credentialsProvider(StaticCredentialsProvider.create(creds)) .serviceConfiguration(S3Configuration.builder().pathStyleAccessEnabled(true).build()) .build() } override def afterStart(): Unit = { super.afterStart() // setup LakeFS (idempotent-ish, but will fail if it truly cannot run) val lakefsSetupResult = lakefs.container.execInContainer( "lakefs", "setup", "--user-name", lakefsUsername, "--access-key-id", lakefsAccessKeyID, "--secret-access-key", lakefsSecretAccessKey ) if (lakefsSetupResult.getExitCode != 0) { throw new RuntimeException(s"Failed to setup LakeFS: ${lakefsSetupResult.getStderr}") } // replace storage endpoints in StorageConfig StorageConfig.s3Endpoint = minioEndpoint StorageConfig.lakefsEndpoint = lakefsApiBasePath // create S3 bucket used by lakeFS in tests S3StorageClient.createBucketIfNotExist(StorageConfig.lakefsBucketName) } } ================================================ FILE: file-service/src/test/scala/org/apache/texera/service/resource/DatasetResourceSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.resource import ch.qos.logback.classic.{Level, Logger} import io.lakefs.clients.sdk.ApiException import jakarta.ws.rs._ import jakarta.ws.rs.core._ import org.apache.texera.amber.core.storage.util.LakeFSStorageClient import org.apache.texera.auth.SessionUser import org.apache.texera.dao.MockTexeraDB import org.apache.texera.dao.jooq.generated.enums.{PrivilegeEnum, UserRoleEnum} import org.apache.texera.dao.jooq.generated.tables.DatasetUploadSession.DATASET_UPLOAD_SESSION import org.apache.texera.dao.jooq.generated.tables.DatasetUploadSessionPart.DATASET_UPLOAD_SESSION_PART import org.apache.texera.dao.jooq.generated.tables.daos.{DatasetDao, DatasetVersionDao, UserDao} import org.apache.texera.dao.jooq.generated.tables.pojos.{Dataset, DatasetVersion, User} import org.apache.texera.service.MockLakeFS import org.apache.texera.service.util.S3StorageClient import org.jooq.SQLDialect import org.jooq.impl.DSL import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import org.scalatest.tagobjects.Slow import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, Tag} import org.slf4j.LoggerFactory import java.io.{ByteArrayInputStream, IOException, InputStream} import java.net.URLEncoder import java.nio.charset.StandardCharsets import java.nio.file.{Files, Paths} import java.security.MessageDigest import java.util.concurrent.CyclicBarrier import java.util.{Collections, Date, Locale, Optional} import scala.concurrent.duration._ import scala.concurrent.{Await, ExecutionContext, Future} import scala.jdk.CollectionConverters._ import scala.util.Random object StressMultipart extends Tag("org.apache.texera.stress.multipart") class DatasetResourceSpec extends AnyFlatSpec with Matchers with MockTexeraDB with MockLakeFS with BeforeAndAfterAll with BeforeAndAfterEach { // ---------- Response entity helpers ---------- private def entityAsScalaMap(resp: Response): Map[String, Any] = { resp.getEntity match { case m: scala.collection.Map[_, _] => m.asInstanceOf[scala.collection.Map[String, Any]].toMap case m: java.util.Map[_, _] => m.asScala.toMap.asInstanceOf[Map[String, Any]] case null => Map.empty case other => fail(s"Unexpected response entity type: ${other.getClass}") } } private def mapListOfInts(x: Any): List[Int] = x match { case l: java.util.List[_] => l.asScala.map(_.toString.toInt).toList case l: scala.collection.Seq[_] => l.map(_.toString.toInt).toList case other => fail(s"Expected list, got: ${other.getClass}") } private def mapListOfStrings(x: Any): List[String] = x match { case l: java.util.List[_] => l.asScala.map(_.toString).toList case l: scala.collection.Seq[_] => l.map(_.toString).toList case other => fail(s"Expected list, got: ${other.getClass}") } private def listUploads( user: SessionUser = multipartOwnerSessionUser ): List[String] = { val resp = datasetResource.multipartUpload( "list", ownerUser.getEmail, multipartDataset.getName, urlEnc("ignored"), Optional.empty(), Optional.empty(), Optional.empty(), user ) resp.getStatus shouldEqual 200 val m = entityAsScalaMap(resp) mapListOfStrings(m("filePaths")) } // ---------- logging (multipart tests can be noisy) ---------- private var savedLevels: Map[String, Level] = Map.empty private def setLoggerLevel(loggerName: String, newLevel: Level): Level = { val logger = LoggerFactory.getLogger(loggerName).asInstanceOf[Logger] val prev = logger.getLevel logger.setLevel(newLevel) prev } // ---------- execution context (multipart race tests) ---------- private implicit val ec: ExecutionContext = ExecutionContext.global // --------------------------------------------------------------------------- // Shared fixtures (DatasetResource basic tests) // --------------------------------------------------------------------------- private val ownerUser: User = { val user = new User user.setName("test_user") user.setPassword("123") user.setEmail("test_user@test.com") user.setRole(UserRoleEnum.ADMIN) user } private val otherAdminUser: User = { val user = new User user.setName("test_user2") user.setPassword("123") user.setEmail("test_user2@test.com") user.setRole(UserRoleEnum.ADMIN) user } // REGULAR user used specifically for multipart "no WRITE access" tests. private val multipartNoWriteUser: User = { val user = new User user.setName("multipart_user2") user.setPassword("123") user.setEmail("multipart_user2@test.com") user.setRole(UserRoleEnum.REGULAR) user } private val baseDataset: Dataset = { val dataset = new Dataset dataset.setName("test-dataset") dataset.setRepositoryName("test-dataset") dataset.setIsPublic(true) dataset.setIsDownloadable(true) dataset.setDescription("dataset for test") dataset } // --------------------------------------------------------------------------- // Multipart fixtures // --------------------------------------------------------------------------- private val multipartRepoName: String = s"multipart-ds-${System.nanoTime()}-${Random.alphanumeric.take(6).mkString.toLowerCase}" private val multipartDataset: Dataset = { val dataset = new Dataset dataset.setName("multipart-ds") dataset.setRepositoryName(multipartRepoName) dataset.setIsPublic(true) dataset.setIsDownloadable(true) dataset.setDescription("dataset for multipart upload tests") dataset } // Test fixtures for cover image tests. Creates file in LakeFS and DatasetVersion record. private val testCoverImagePath = "v1/test-cover.jpg" private val testImageBytes: Array[Byte] = Array.fill[Byte](1024)(0xff.toByte) private lazy val testDatasetVersion: DatasetVersion = { try { LakeFSStorageClient.initRepo(baseDataset.getRepositoryName) } catch { case e: ApiException if e.getCode == 409 => } LakeFSStorageClient.writeFileToRepo( baseDataset.getRepositoryName, "test-cover.jpg", new ByteArrayInputStream(testImageBytes) ) val version = new DatasetVersion() version.setDid(baseDataset.getDid) version.setCreatorUid(ownerUser.getUid) version.setName("v1") version.setVersionHash("main") new DatasetVersionDao(getDSLContext.configuration()).insert(version) version } // ---------- DAOs / resource ---------- lazy val datasetDao = new DatasetDao(getDSLContext.configuration()) lazy val datasetResource = new DatasetResource() // ---------- session users ---------- lazy val sessionUser = new SessionUser(ownerUser) lazy val sessionUser2 = new SessionUser(otherAdminUser) // Multipart callers lazy val multipartOwnerSessionUser = sessionUser lazy val multipartNoWriteSessionUser = new SessionUser(multipartNoWriteUser) // --------------------------------------------------------------------------- // Lifecycle // --------------------------------------------------------------------------- override protected def beforeAll(): Unit = { super.beforeAll() // init db initializeDBAndReplaceDSLContext() // insert users val userDao = new UserDao(getDSLContext.configuration()) userDao.insert(ownerUser) userDao.insert(otherAdminUser) userDao.insert(multipartNoWriteUser) // insert datasets (owned by ownerUser) baseDataset.setOwnerUid(ownerUser.getUid) multipartDataset.setOwnerUid(ownerUser.getUid) datasetDao.insert(baseDataset) datasetDao.insert(multipartDataset) savedLevels = Map( "org.apache.http.wire" -> setLoggerLevel("org.apache.http.wire", Level.WARN), "org.apache.http.headers" -> setLoggerLevel("org.apache.http.headers", Level.WARN) ) } override protected def beforeEach(): Unit = { super.beforeEach() // Multipart repo must exist for presigned multipart init to succeed. // If it already exists, ignore 409. try LakeFSStorageClient.initRepo(multipartDataset.getRepositoryName) catch { case e: ApiException if e.getCode == 409 => // ok } // Ensure max upload size setting does not leak between tests clearMaxUploadMiB() } override protected def afterAll(): Unit = { try shutdownDB() finally { try savedLevels.foreach { case (name, prev) => setLoggerLevel(name, prev) } finally super .afterAll() } } // =========================================================================== // DatasetResourceSpec (original basic tests) // =========================================================================== "createDataset" should "create dataset successfully if user does not have a dataset with the same name" in { val createDatasetRequest = DatasetResource.CreateDatasetRequest( datasetName = "new-dataset", datasetDescription = "description for new dataset", isDatasetPublic = false, isDatasetDownloadable = true ) val createdDataset = datasetResource.createDataset(createDatasetRequest, sessionUser) createdDataset.dataset.getName shouldEqual "new-dataset" createdDataset.dataset.getDescription shouldEqual "description for new dataset" createdDataset.dataset.getIsPublic shouldBe false createdDataset.dataset.getIsDownloadable shouldBe true } it should "refuse to create dataset if user already has a dataset with the same name" in { val createDatasetRequest = DatasetResource.CreateDatasetRequest( datasetName = "test-dataset", datasetDescription = "description for new dataset", isDatasetPublic = false, isDatasetDownloadable = true ) assertThrows[BadRequestException] { datasetResource.createDataset(createDatasetRequest, sessionUser) } } it should "create dataset successfully if another user has a dataset with the same name" in { val createDatasetRequest = DatasetResource.CreateDatasetRequest( datasetName = "test-dataset", datasetDescription = "description for new dataset", isDatasetPublic = false, isDatasetDownloadable = true ) val createdDataset = datasetResource.createDataset(createDatasetRequest, sessionUser2) createdDataset.dataset.getName shouldEqual "test-dataset" createdDataset.dataset.getDescription shouldEqual "description for new dataset" createdDataset.dataset.getIsPublic shouldBe false createdDataset.dataset.getIsDownloadable shouldBe true } it should "return DashboardDataset with correct owner email, WRITE privilege, and isOwner=true" in { val createDatasetRequest = DatasetResource.CreateDatasetRequest( datasetName = "dashboard-dataset-test", datasetDescription = "test for DashboardDataset properties", isDatasetPublic = true, isDatasetDownloadable = false ) val dashboardDataset = datasetResource.createDataset(createDatasetRequest, sessionUser) dashboardDataset.ownerEmail shouldEqual ownerUser.getEmail dashboardDataset.accessPrivilege shouldEqual PrivilegeEnum.WRITE dashboardDataset.isOwner shouldBe true dashboardDataset.size shouldEqual 0 dashboardDataset.dataset.getName shouldEqual "dashboard-dataset-test" dashboardDataset.dataset.getDescription shouldEqual "test for DashboardDataset properties" dashboardDataset.dataset.getIsPublic shouldBe true dashboardDataset.dataset.getIsDownloadable shouldBe false } it should "delete dataset successfully if user owns it" in { val dataset = new Dataset dataset.setName("delete-ds") dataset.setRepositoryName("delete-ds") dataset.setDescription("for delete test") dataset.setOwnerUid(ownerUser.getUid) dataset.setIsPublic(true) dataset.setIsDownloadable(true) datasetDao.insert(dataset) LakeFSStorageClient.initRepo(dataset.getRepositoryName) val response = datasetResource.deleteDataset(dataset.getDid, sessionUser) response.getStatus shouldEqual 200 datasetDao.fetchOneByDid(dataset.getDid) shouldBe null } it should "refuse to delete dataset if not owned by user" in { val dataset = new Dataset dataset.setName("user1-ds") dataset.setRepositoryName("user1-ds") dataset.setDescription("for forbidden test") dataset.setOwnerUid(ownerUser.getUid) dataset.setIsPublic(true) dataset.setIsDownloadable(true) datasetDao.insert(dataset) LakeFSStorageClient.initRepo(dataset.getRepositoryName) assertThrows[ForbiddenException] { datasetResource.deleteDataset(dataset.getDid, sessionUser2) } datasetDao.fetchOneByDid(dataset.getDid) should not be null } "updateDatasetName" should "rename dataset successfully if user has write access" in { val dataset = new Dataset dataset.setName("rename-before") dataset.setRepositoryName("rename-before-repo") dataset.setDescription("for rename happy path") dataset.setOwnerUid(ownerUser.getUid) dataset.setIsPublic(true) dataset.setIsDownloadable(true) datasetDao.insert(dataset) val response = datasetResource.updateDatasetName( DatasetResource.DatasetNameModification(dataset.getDid, "rename-after"), sessionUser ) response.getStatus shouldEqual 200 datasetDao.fetchOneByDid(dataset.getDid).getName shouldEqual "rename-after" } it should "refuse to rename dataset if user lacks write access" in { val dataset = new Dataset dataset.setName("rename-forbidden") dataset.setRepositoryName("rename-forbidden-repo") dataset.setDescription("for rename forbidden test") dataset.setOwnerUid(ownerUser.getUid) dataset.setIsPublic(true) dataset.setIsDownloadable(true) datasetDao.insert(dataset) assertThrows[ForbiddenException] { datasetResource.updateDatasetName( DatasetResource.DatasetNameModification(dataset.getDid, "hijacked"), sessionUser2 ) } datasetDao.fetchOneByDid(dataset.getDid).getName shouldEqual "rename-forbidden" } it should "throw NotFoundException when renaming a non-existent dataset" in { val nonExistentDid: Integer = Int.box(Int.MaxValue) assertThrows[NotFoundException] { datasetResource.updateDatasetName( DatasetResource.DatasetNameModification(nonExistentDid, "ghost"), sessionUser ) } } it should "leave repository_name unchanged after rename" in { val dataset = new Dataset dataset.setName("rename-keeps-repo") dataset.setRepositoryName("rename-keeps-repo-stable") dataset.setDescription("for repo-name invariance test") dataset.setOwnerUid(ownerUser.getUid) dataset.setIsPublic(true) dataset.setIsDownloadable(true) datasetDao.insert(dataset) datasetResource.updateDatasetName( DatasetResource.DatasetNameModification(dataset.getDid, "rename-keeps-repo-renamed"), sessionUser ) val reloaded = datasetDao.fetchOneByDid(dataset.getDid) reloaded.getName shouldEqual "rename-keeps-repo-renamed" reloaded.getRepositoryName shouldEqual "rename-keeps-repo-stable" } // =========================================================================== // Multipart upload tests (merged in) // =========================================================================== // ---------- SHA-256 Utils ---------- private def sha256OfChunks(chunks: Seq[Array[Byte]]): Array[Byte] = { val messageDigest = MessageDigest.getInstance("SHA-256") chunks.foreach(messageDigest.update) messageDigest.digest() } private def sha256OfFile(path: java.nio.file.Path): Array[Byte] = { val messageDigest = MessageDigest.getInstance("SHA-256") val inputStream = Files.newInputStream(path) try { val buffer = new Array[Byte](8192) var bytesRead = inputStream.read(buffer) while (bytesRead != -1) { messageDigest.update(buffer, 0, bytesRead) bytesRead = inputStream.read(buffer) } messageDigest.digest() } finally inputStream.close() } // ---------- helpers ---------- private def urlEnc(raw: String): String = URLEncoder.encode(raw, StandardCharsets.UTF_8.name()) /** Minimum part-size rule (S3-style): every part except the LAST must be >= 5 MiB. */ private val MinNonFinalPartBytes: Int = 5 * 1024 * 1024 private def minPartBytes(fillByte: Byte): Array[Byte] = Array.fill[Byte](MinNonFinalPartBytes)(fillByte) private def tinyBytes(fillByte: Byte, n: Int = 1): Array[Byte] = Array.fill[Byte](n)(fillByte) /** InputStream that behaves like a mid-flight network drop after N bytes. */ private def flakyStream( payload: Array[Byte], failAfterBytes: Int, msg: String = "simulated network drop" ): InputStream = new InputStream { private var pos = 0 override def read(): Int = { if (pos >= failAfterBytes) throw new IOException(msg) if (pos >= payload.length) return -1 val nextByte = payload(pos) & 0xff pos += 1 nextByte } } /** Minimal HttpHeaders impl needed by DatasetResource.uploadPart */ private def mkHeaders(contentLength: Long): HttpHeaders = new HttpHeaders { private val headers = new MultivaluedHashMap[String, String]() headers.putSingle(HttpHeaders.CONTENT_LENGTH, contentLength.toString) override def getHeaderString(name: String): String = headers.getFirst(name) override def getRequestHeaders = headers override def getRequestHeader(name: String) = Option(headers.get(name)).getOrElse(Collections.emptyList[String]()) override def getAcceptableMediaTypes = Collections.emptyList[MediaType]() override def getAcceptableLanguages = Collections.emptyList[Locale]() override def getMediaType: MediaType = null override def getLanguage: Locale = null override def getCookies = Collections.emptyMap[String, Cookie]() override def getDate: Date = null override def getLength: Int = contentLength.toInt } private def mkHeadersMissingContentLength: HttpHeaders = new HttpHeaders { private val headers = new MultivaluedHashMap[String, String]() override def getHeaderString(name: String): String = null override def getRequestHeaders = headers override def getRequestHeader(name: String) = Collections.emptyList[String]() override def getAcceptableMediaTypes = Collections.emptyList[MediaType]() override def getAcceptableLanguages = Collections.emptyList[Locale]() override def getMediaType: MediaType = null override def getLanguage: Locale = null override def getCookies = Collections.emptyMap[String, Cookie]() override def getDate: Date = null override def getLength: Int = -1 } private def mkHeadersRawContentLength(raw: String): HttpHeaders = new HttpHeaders { override def getRequestHeader(name: String): java.util.List[String] = if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) Collections.singletonList(raw) else Collections.emptyList() override def getHeaderString(name: String): String = if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) raw else null override def getRequestHeaders: MultivaluedMap[String, String] = { val map = new MultivaluedHashMap[String, String]() map.putSingle(HttpHeaders.CONTENT_LENGTH, raw) map } override def getAcceptableMediaTypes: java.util.List[MediaType] = Collections.emptyList() override def getAcceptableLanguages: java.util.List[Locale] = Collections.emptyList() override def getMediaType: MediaType = null override def getLanguage: Locale = null override def getCookies: java.util.Map[String, Cookie] = Collections.emptyMap() // Not used by the resource (it reads getHeaderString), but keep it safe. override def getLength: Int = -1 override def getDate: Date = ??? } private def uniqueFilePath(prefix: String): String = s"$prefix/${System.nanoTime()}-${Random.alphanumeric.take(8).mkString}.bin" // ---------- site_settings helpers (max upload size) ---------- private val MaxUploadKey = "single_file_upload_max_size_mib" private def upsertSiteSetting(key: String, value: String): Unit = { val table = DSL.table(DSL.name("texera_db", "site_settings")) val keyField = DSL.field(DSL.name("key"), classOf[String]) val valField = DSL.field(DSL.name("value"), classOf[String]) // Keep it simple + compatible across jOOQ versions: delete then insert. val ctx = getDSLContext ctx.deleteFrom(table).where(keyField.eq(key)).execute() ctx.insertInto(table).columns(keyField, valField).values(key, value).execute() } private def deleteSiteSetting(key: String): Boolean = { val table = DSL.table(DSL.name("texera_db", "site_settings")) val keyField = DSL.field(DSL.name("key"), classOf[String]) getDSLContext.deleteFrom(table).where(keyField.eq(key)).execute() > 0 } private def setMaxUploadMiB(mib: Long): Unit = upsertSiteSetting(MaxUploadKey, mib.toString) private def clearMaxUploadMiB(): Unit = deleteSiteSetting(MaxUploadKey) /** * Convenience helper that adapts legacy "numParts" tests to the new init API: * init now takes (fileSizeBytes, partSizeBytes) and computes numParts internally. * * - Non-final parts are exactly partSizeBytes. * - Final part is exactly lastPartBytes. */ private def initUpload( filePath: String, numParts: Int, lastPartBytes: Int = 1, partSizeBytes: Int = MinNonFinalPartBytes, user: SessionUser = multipartOwnerSessionUser, restart: Optional[java.lang.Boolean] = Optional.empty() ): Response = { require(numParts >= 1, "numParts must be >= 1") require(lastPartBytes > 0, "lastPartBytes must be > 0") require(partSizeBytes > 0, "partSizeBytes must be > 0") if (numParts > 1) require( lastPartBytes <= partSizeBytes, "lastPartBytes must be <= partSizeBytes for multipart" ) val fileSizeBytes: Long = if (numParts == 1) lastPartBytes.toLong else partSizeBytes.toLong * (numParts.toLong - 1L) + lastPartBytes.toLong // For numParts == 1, allow partSizeBytes >= fileSizeBytes (still computes 1 part). val maxPartSizeBytes: Long = if (numParts == 1) Math.max(partSizeBytes.toLong, fileSizeBytes) else partSizeBytes.toLong datasetResource.multipartUpload( "init", ownerUser.getEmail, multipartDataset.getName, urlEnc(filePath), Optional.of(java.lang.Long.valueOf(fileSizeBytes)), Optional.of(java.lang.Long.valueOf(maxPartSizeBytes)), restart, user ) } private def initRaw( filePath: String, fileSizeBytes: Long, partSizeBytes: Long, user: SessionUser = multipartOwnerSessionUser ): Response = { datasetResource.multipartUpload( "init", ownerUser.getEmail, multipartDataset.getName, urlEnc(filePath), Optional.of(java.lang.Long.valueOf(fileSizeBytes)), Optional.of(java.lang.Long.valueOf(partSizeBytes)), Optional.empty(), user ) } private def finishUpload( filePath: String, user: SessionUser = multipartOwnerSessionUser ): Response = datasetResource.multipartUpload( "finish", ownerUser.getEmail, multipartDataset.getName, urlEnc(filePath), Optional.empty(), Optional.empty(), Optional.empty(), user ) private def abortUpload( filePath: String, user: SessionUser = multipartOwnerSessionUser ): Response = datasetResource.multipartUpload( "abort", ownerUser.getEmail, multipartDataset.getName, urlEnc(filePath), Optional.empty(), Optional.empty(), Optional.empty(), user ) private def uploadPart( filePath: String, partNumber: Int, bytes: Array[Byte], user: SessionUser = multipartOwnerSessionUser, contentLengthOverride: Option[Long] = None, missingContentLength: Boolean = false, rawContentLengthOverride: Option[String] = None ): Response = { val contentLength = contentLengthOverride.getOrElse(bytes.length.toLong) val headers = if (missingContentLength) mkHeadersMissingContentLength else rawContentLengthOverride.map(mkHeadersRawContentLength).getOrElse(mkHeaders(contentLength)) datasetResource.uploadPart( ownerUser.getEmail, multipartDataset.getName, urlEnc(filePath), partNumber, new ByteArrayInputStream(bytes), headers, user ) } private def uploadPartWithStream( filePath: String, partNumber: Int, stream: InputStream, contentLength: Long, user: SessionUser = multipartOwnerSessionUser, rawContentLengthOverride: Option[String] = None ): Response = { val headers = rawContentLengthOverride.map(mkHeadersRawContentLength).getOrElse(mkHeaders(contentLength)) datasetResource.uploadPart( ownerUser.getEmail, multipartDataset.getName, urlEnc(filePath), partNumber, stream, headers, user ) } private def fetchSession(filePath: String) = getDSLContext .selectFrom(DATASET_UPLOAD_SESSION) .where( DATASET_UPLOAD_SESSION.UID .eq(ownerUser.getUid) .and(DATASET_UPLOAD_SESSION.DID.eq(multipartDataset.getDid)) .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath)) ) .fetchOne() private def fetchPartRows(uploadId: String) = getDSLContext .selectFrom(DATASET_UPLOAD_SESSION_PART) .where(DATASET_UPLOAD_SESSION_PART.UPLOAD_ID.eq(uploadId)) .fetch() .asScala .toList private def fetchUploadIdOrFail(filePath: String): String = { val sessionRecord = fetchSession(filePath) sessionRecord should not be null sessionRecord.getUploadId } private def expireUploadSession(uploadId: String): Unit = { val expiredHoursAgo = S3StorageClient.PHYSICAL_ADDRESS_EXPIRATION_TIME_HRS + 1 getDSLContext .update(DATASET_UPLOAD_SESSION) .set( DATASET_UPLOAD_SESSION.CREATED_AT, DSL .field(s"current_timestamp - interval '${expiredHoursAgo} hours'") .cast(classOf[java.time.OffsetDateTime]) ) .where(DATASET_UPLOAD_SESSION.UPLOAD_ID.eq(uploadId)) .execute() } private def assertPlaceholdersCreated(uploadId: String, expectedParts: Int): Unit = { val rows = fetchPartRows(uploadId).sortBy(_.getPartNumber) rows.size shouldEqual expectedParts rows.head.getPartNumber shouldEqual 1 rows.last.getPartNumber shouldEqual expectedParts rows.foreach { r => r.getEtag should not be null r.getEtag shouldEqual "" // placeholder convention } } private def assertStatus(ex: WebApplicationException, status: Int): Unit = ex.getResponse.getStatus shouldEqual status // --------------------------------------------------------------------------- // LIST TESTS (type=list) // --------------------------------------------------------------------------- "multipart-upload?type=list" should "return empty when no active sessions exist for the dataset" in { // Make deterministic: remove any leftover sessions from other tests. getDSLContext .deleteFrom(DATASET_UPLOAD_SESSION) .where(DATASET_UPLOAD_SESSION.DID.eq(multipartDataset.getDid)) .execute() listUploads() shouldBe empty } it should "reject list when caller lacks WRITE access" in { val ex = intercept[ForbiddenException] { listUploads(user = multipartNoWriteSessionUser) } ex.getResponse.getStatus shouldEqual 403 } it should "return only non-expired sessions, sorted by filePath (and exclude expired ones)" in { // Clean slate getDSLContext .deleteFrom(DATASET_UPLOAD_SESSION) .where(DATASET_UPLOAD_SESSION.DID.eq(multipartDataset.getDid)) .execute() val fpA = uniqueFilePath("list-a") val fpB = uniqueFilePath("list-b") initUpload(fpB, numParts = 2).getStatus shouldEqual 200 initUpload(fpA, numParts = 2).getStatus shouldEqual 200 // Expire fpB by pushing created_at back beyond the real session expiration window. val uploadIdB = fetchUploadIdOrFail(fpB) expireUploadSession(uploadIdB) val listed = listUploads() listed shouldEqual listed.sorted listed should contain(fpA) listed should not contain fpB } it should "not list sessions after abort (cleanup works end-to-end)" in { // Clean slate getDSLContext .deleteFrom(DATASET_UPLOAD_SESSION) .where(DATASET_UPLOAD_SESSION.DID.eq(multipartDataset.getDid)) .execute() val fp = uniqueFilePath("list-after-abort") initUpload(fp, numParts = 2).getStatus shouldEqual 200 listUploads() should contain(fp) abortUpload(fp).getStatus shouldEqual 200 listUploads() should not contain fp } // --------------------------------------------------------------------------- // INIT TESTS // --------------------------------------------------------------------------- "multipart-upload?type=init" should "create an upload session row + precreate part placeholders (happy path)" in { val filePath = uniqueFilePath("init-happy") val resp = initUpload(filePath, numParts = 3) resp.getStatus shouldEqual 200 val sessionRecord = fetchSession(filePath) sessionRecord should not be null sessionRecord.getNumPartsRequested shouldEqual 3 sessionRecord.getUploadId should not be null sessionRecord.getPhysicalAddress should not be null assertPlaceholdersCreated(sessionRecord.getUploadId, expectedParts = 3) } it should "restart session when restart=true is explicitly requested (even if config is unchanged) and reset progress" in { val filePath = uniqueFilePath("init-restart-true") // Initial init initUpload(filePath, numParts = 2, lastPartBytes = 123).getStatus shouldEqual 200 val oldUploadId = fetchUploadIdOrFail(filePath) // Make progress in old session uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200 fetchPartRows(oldUploadId).find(_.getPartNumber == 1).get.getEtag.trim should not be "" // Re-init with same config but restart=true => must restart val r2 = initUpload( filePath, numParts = 2, lastPartBytes = 123, restart = Optional.of(java.lang.Boolean.TRUE) ) r2.getStatus shouldEqual 200 val newUploadId = fetchUploadIdOrFail(filePath) newUploadId should not equal oldUploadId // Old part rows gone, new placeholders empty fetchPartRows(oldUploadId) shouldBe empty assertPlaceholdersCreated(newUploadId, expectedParts = 2) // Response should look like a fresh session val m = entityAsScalaMap(r2) mapListOfInts(m("missingParts")) shouldEqual List(1, 2) m("completedPartsCount").toString.toInt shouldEqual 0 } it should "not restart session when restart=false (same config) and preserve uploadId + progress" in { val filePath = uniqueFilePath("init-restart-false") initUpload(filePath, numParts = 3, lastPartBytes = 123).getStatus shouldEqual 200 val uploadId1 = fetchUploadIdOrFail(filePath) uploadPart(filePath, 1, minPartBytes(7.toByte)).getStatus shouldEqual 200 val r2 = initUpload( filePath, numParts = 3, lastPartBytes = 123, restart = Optional.of(java.lang.Boolean.FALSE) ) r2.getStatus shouldEqual 200 val uploadId2 = fetchUploadIdOrFail(filePath) uploadId2 shouldEqual uploadId1 val m = entityAsScalaMap(r2) mapListOfInts(m("missingParts")) shouldEqual List(2, 3) m("completedPartsCount").toString.toInt shouldEqual 1 } it should "restart even when all parts were already uploaded (restart=true makes missingParts full again)" in { val filePath = uniqueFilePath("init-restart-after-all-parts") initUpload(filePath, numParts = 2, lastPartBytes = 123).getStatus shouldEqual 200 val oldUploadId = fetchUploadIdOrFail(filePath) // Upload everything (but don't finish) uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200 uploadPart(filePath, 2, tinyBytes(2.toByte, n = 123)).getStatus shouldEqual 200 // Confirm "all done" without restart val rNoRestart = initUpload(filePath, numParts = 2, lastPartBytes = 123) rNoRestart.getStatus shouldEqual 200 val mNoRestart = entityAsScalaMap(rNoRestart) mapListOfInts(mNoRestart("missingParts")) shouldEqual Nil mNoRestart("completedPartsCount").toString.toInt shouldEqual 2 // Now force restart => must reset val rRestart = initUpload( filePath, numParts = 2, lastPartBytes = 123, restart = Optional.of(java.lang.Boolean.TRUE) ) rRestart.getStatus shouldEqual 200 val newUploadId = fetchUploadIdOrFail(filePath) newUploadId should not equal oldUploadId fetchPartRows(oldUploadId) shouldBe empty assertPlaceholdersCreated(newUploadId, expectedParts = 2) val m = entityAsScalaMap(rRestart) mapListOfInts(m("missingParts")) shouldEqual List(1, 2) m("completedPartsCount").toString.toInt shouldEqual 0 } "multipart-upload?type=init" should "restart session when init config changes (fileSize/partSize/numParts) and recreate placeholders" in { val filePath = uniqueFilePath("init-conflict-restart") // First init => 2 parts initUpload(filePath, numParts = 2, lastPartBytes = 123).getStatus shouldEqual 200 val oldUploadId = fetchUploadIdOrFail(filePath) // Upload part 1 so old session isn't empty uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200 fetchPartRows(oldUploadId).find(_.getPartNumber == 1).get.getEtag.trim should not be "" // Second init with DIFFERENT config => 3 parts now val resp2 = initUpload(filePath, numParts = 3, lastPartBytes = 50) resp2.getStatus shouldEqual 200 val newUploadId = fetchUploadIdOrFail(filePath) newUploadId should not equal oldUploadId // Old part rows should have been deleted via ON DELETE CASCADE fetchPartRows(oldUploadId) shouldBe empty // New placeholders should exist and be empty assertPlaceholdersCreated(newUploadId, expectedParts = 3) val m2 = entityAsScalaMap(resp2) mapListOfInts(m2("missingParts")) shouldEqual List(1, 2, 3) m2("completedPartsCount").toString.toInt shouldEqual 0 } it should "restart session when physicalAddress has expired (created_at too old), even if config is unchanged" in { val filePath = uniqueFilePath("init-expired-restart") // First init (2 parts) val r1 = initUpload(filePath, numParts = 2, lastPartBytes = 123) r1.getStatus shouldEqual 200 val oldUploadId = fetchUploadIdOrFail(filePath) oldUploadId should not be null // Optional: create some progress so we know it truly resets uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200 fetchPartRows(oldUploadId).find(_.getPartNumber == 1).get.getEtag.trim should not be "" // Age the session so it is definitely expired (> PHYSICAL_ADDRESS_EXPIRATION_TIME_HRS) expireUploadSession(oldUploadId) // Same init config again -> should restart because it's expired val r2 = initUpload(filePath, numParts = 2, lastPartBytes = 123) r2.getStatus shouldEqual 200 val newUploadId = fetchUploadIdOrFail(filePath) newUploadId should not equal oldUploadId // Old part rows should have been deleted (ON DELETE CASCADE) fetchPartRows(oldUploadId) shouldBe empty // New placeholders should exist, empty assertPlaceholdersCreated(newUploadId, expectedParts = 2) // Response should reflect a fresh session val m2 = entityAsScalaMap(r2) mapListOfInts(m2("missingParts")) shouldEqual List(1, 2) m2("completedPartsCount").toString.toInt shouldEqual 0 } it should "be resumable: repeated init with same config keeps uploadId and returns missingParts + completedPartsCount" in { val filePath = uniqueFilePath("init-resume-same-config") val resp1 = initUpload(filePath, numParts = 3, lastPartBytes = 123) resp1.getStatus shouldEqual 200 val uploadId1 = fetchUploadIdOrFail(filePath) uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200 val resp2 = initUpload(filePath, numParts = 3, lastPartBytes = 123) resp2.getStatus shouldEqual 200 val uploadId2 = fetchUploadIdOrFail(filePath) uploadId2 shouldEqual uploadId1 val m2 = entityAsScalaMap(resp2) val missing = mapListOfInts(m2("missingParts")) missing shouldEqual List(2, 3) m2("completedPartsCount").toString.toInt shouldEqual 1 } it should "return missingParts=[] when all parts are already uploaded (completedPartsCount == numParts)" in { val filePath = uniqueFilePath("init-all-done") initUpload(filePath, numParts = 2, lastPartBytes = 123).getStatus shouldEqual 200 uploadPart(filePath, 1, minPartBytes(7.toByte)).getStatus shouldEqual 200 uploadPart(filePath, 2, tinyBytes(8.toByte, n = 123)).getStatus shouldEqual 200 val resp2 = initUpload(filePath, numParts = 2, lastPartBytes = 123) resp2.getStatus shouldEqual 200 val m2 = entityAsScalaMap(resp2) mapListOfInts(m2("missingParts")) shouldEqual Nil m2("completedPartsCount").toString.toInt shouldEqual 2 } it should "return 409 CONFLICT if the upload session row is locked by another transaction" in { val filePath = uniqueFilePath("init-session-row-locked") initUpload(filePath, numParts = 2).getStatus shouldEqual 200 val connectionProvider = getDSLContext.configuration().connectionProvider() val connection = connectionProvider.acquire() connection.setAutoCommit(false) try { val locking = DSL.using(connection, SQLDialect.POSTGRES) locking .selectFrom(DATASET_UPLOAD_SESSION) .where( DATASET_UPLOAD_SESSION.UID .eq(ownerUser.getUid) .and(DATASET_UPLOAD_SESSION.DID.eq(multipartDataset.getDid)) .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath)) ) .forUpdate() .fetchOne() val ex = intercept[WebApplicationException] { initUpload(filePath, numParts = 2) } ex.getResponse.getStatus shouldEqual 409 } finally { connection.rollback() connectionProvider.release(connection) } // lock released => init works again initUpload(filePath, numParts = 2).getStatus shouldEqual 200 } it should "treat normalized-equivalent paths as the same session (no duplicate sessions)" in { val base = s"norm-${System.nanoTime()}.bin" val raw = s"a/../$base" // normalizes to base // init using traversal-ish but normalizable path initUpload(raw, numParts = 1, lastPartBytes = 16, partSizeBytes = 16).getStatus shouldEqual 200 val uploadId1 = fetchUploadIdOrFail(base) // stored path should be normalized // init using normalized path should hit the same session (resume) val resp2 = initUpload(base, numParts = 1, lastPartBytes = 16, partSizeBytes = 16) resp2.getStatus shouldEqual 200 val uploadId2 = fetchUploadIdOrFail(base) uploadId2 shouldEqual uploadId1 val m2 = entityAsScalaMap(resp2) mapListOfInts(m2("missingParts")) shouldEqual List(1) m2("completedPartsCount").toString.toInt shouldEqual 0 } it should "restart session when fileSizeBytes differs (single-part; computedNumParts unchanged)" in { val filePath = uniqueFilePath("init-conflict-filesize") val declared = 16 val r1 = initRaw(filePath, fileSizeBytes = declared, partSizeBytes = 32L) // numParts=1 r1.getStatus shouldEqual 200 val oldUploadId = fetchUploadIdOrFail(filePath) // Add progress in old session uploadPart(filePath, 1, Array.fill[Byte](declared)(1.toByte)).getStatus shouldEqual 200 fetchPartRows(oldUploadId).find(_.getPartNumber == 1).get.getEtag.trim should not be "" val r2 = initRaw(filePath, fileSizeBytes = 17L, partSizeBytes = 32L) // numParts=1 still r2.getStatus shouldEqual 200 val newUploadId = fetchUploadIdOrFail(filePath) newUploadId should not equal oldUploadId fetchPartRows(oldUploadId) shouldBe empty // old placeholders removed val session = fetchSession(filePath) session.getFileSizeBytes shouldEqual 17L session.getPartSizeBytes shouldEqual 32L session.getNumPartsRequested shouldEqual 1 val m = entityAsScalaMap(r2) mapListOfInts(m("missingParts")) shouldEqual List(1) m("completedPartsCount").toString.toInt shouldEqual 0 // progress reset } it should "restart session when partSizeBytes differs (single-part; computedNumParts unchanged)" in { val filePath = uniqueFilePath("init-conflict-partsize") initRaw(filePath, fileSizeBytes = 16L, partSizeBytes = 32L).getStatus shouldEqual 200 val oldUploadId = fetchUploadIdOrFail(filePath) // Second init, same fileSize, different partSize, still 1 part val r2 = initRaw(filePath, fileSizeBytes = 16L, partSizeBytes = 64L) r2.getStatus shouldEqual 200 val newUploadId = fetchUploadIdOrFail(filePath) newUploadId should not equal oldUploadId fetchPartRows(oldUploadId) shouldBe empty val session = fetchSession(filePath) session.getFileSizeBytes shouldEqual 16L session.getPartSizeBytes shouldEqual 64L session.getNumPartsRequested shouldEqual 1 val m = entityAsScalaMap(r2) mapListOfInts(m("missingParts")) shouldEqual List(1) m("completedPartsCount").toString.toInt shouldEqual 0 } it should "restart session when computed numParts differs (multipart -> single-part)" in { val filePath = uniqueFilePath("init-conflict-numparts") val partSize = MinNonFinalPartBytes.toLong // 5 MiB val fileSize = partSize * 2L + 123L // => computedNumParts = 3 val r1 = initRaw(filePath, fileSizeBytes = fileSize, partSizeBytes = partSize) r1.getStatus shouldEqual 200 val oldUploadId = fetchUploadIdOrFail(filePath) fetchSession(filePath).getNumPartsRequested shouldEqual 3 // Create progress uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200 // Re-init with a partSize >= fileSize => computedNumParts becomes 1 val r2 = initRaw(filePath, fileSizeBytes = fileSize, partSizeBytes = fileSize) r2.getStatus shouldEqual 200 val newUploadId = fetchUploadIdOrFail(filePath) newUploadId should not equal oldUploadId fetchPartRows(oldUploadId) shouldBe empty val session = fetchSession(filePath) session.getNumPartsRequested shouldEqual 1 session.getFileSizeBytes shouldEqual fileSize session.getPartSizeBytes shouldEqual fileSize val m = entityAsScalaMap(r2) mapListOfInts(m("missingParts")) shouldEqual List(1) m("completedPartsCount").toString.toInt shouldEqual 0 } it should "reject missing fileSizeBytes / partSizeBytes" in { val filePath1 = uniqueFilePath("init-missing-filesize") val ex1 = intercept[BadRequestException] { datasetResource.multipartUpload( "init", ownerUser.getEmail, multipartDataset.getName, urlEnc(filePath1), Optional.empty(), Optional.of(java.lang.Long.valueOf(MinNonFinalPartBytes.toLong)), Optional.empty(), multipartOwnerSessionUser ) } assertStatus(ex1, 400) val filePath2 = uniqueFilePath("init-missing-partsize") val ex2 = intercept[BadRequestException] { datasetResource.multipartUpload( "init", ownerUser.getEmail, multipartDataset.getName, urlEnc(filePath2), Optional.of(java.lang.Long.valueOf(1L)), Optional.empty(), Optional.empty(), multipartOwnerSessionUser ) } assertStatus(ex2, 400) } it should "reject invalid fileSizeBytes / partSizeBytes (<= 0)" in { val filePath = uniqueFilePath("init-bad-sizes") assertStatus( intercept[BadRequestException] { datasetResource.multipartUpload( "init", ownerUser.getEmail, multipartDataset.getName, urlEnc(filePath), Optional.of(java.lang.Long.valueOf(0L)), Optional.of(java.lang.Long.valueOf(1L)), Optional.empty(), multipartOwnerSessionUser ) }, 400 ) assertStatus( intercept[BadRequestException] { datasetResource.multipartUpload( "init", ownerUser.getEmail, multipartDataset.getName, urlEnc(filePath), Optional.of(java.lang.Long.valueOf(1L)), Optional.of(java.lang.Long.valueOf(0L)), Optional.empty(), multipartOwnerSessionUser ) }, 400 ) } it should "enforce max upload size at init (>, == boundary)" in { // Use a tiny limit so the test doesn't allocate big buffers. setMaxUploadMiB(1) // 1 MiB val oneMiB: Long = 1024L * 1024L val filePathOver = uniqueFilePath("init-max-over") assertStatus( intercept[BadRequestException] { datasetResource.multipartUpload( "init", ownerUser.getEmail, multipartDataset.getName, urlEnc(filePathOver), Optional.of(java.lang.Long.valueOf(oneMiB + 1L)), Optional.of(java.lang.Long.valueOf(oneMiB + 1L)), // single-part Optional.empty(), multipartOwnerSessionUser ) }, 400 ) val filePathEq = uniqueFilePath("init-max-eq") val resp = datasetResource.multipartUpload( "init", ownerUser.getEmail, multipartDataset.getName, urlEnc(filePathEq), Optional.of(java.lang.Long.valueOf(oneMiB)), Optional.of(java.lang.Long.valueOf(oneMiB)), // single-part Optional.empty(), multipartOwnerSessionUser ) resp.getStatus shouldEqual 200 fetchSession(filePathEq) should not be null } it should "enforce max upload size for multipart (2-part boundary)" in { setMaxUploadMiB(6) // 6 MiB val max6MiB: Long = 6L * 1024L * 1024L val partSize: Long = MinNonFinalPartBytes.toLong // 5 MiB val filePathEq = uniqueFilePath("init-max-multipart-eq") val respEq = datasetResource.multipartUpload( "init", ownerUser.getEmail, multipartDataset.getName, urlEnc(filePathEq), Optional.of(java.lang.Long.valueOf(max6MiB)), Optional.of(java.lang.Long.valueOf(partSize)), Optional.empty(), multipartOwnerSessionUser ) respEq.getStatus shouldEqual 200 fetchSession(filePathEq).getNumPartsRequested shouldEqual 2 val filePathOver = uniqueFilePath("init-max-multipart-over") assertStatus( intercept[BadRequestException] { datasetResource.multipartUpload( "init", ownerUser.getEmail, multipartDataset.getName, urlEnc(filePathOver), Optional.of(java.lang.Long.valueOf(max6MiB + 1L)), Optional.of(java.lang.Long.valueOf(partSize)), Optional.empty(), multipartOwnerSessionUser ) }, 400 ) } it should "reject init when fileSizeBytes/partSizeBytes would overflow numParts computation (malicious huge inputs)" in { // Make max big enough to get past the max-size gate without overflowing maxBytes itself. val maxMiB: Long = Long.MaxValue / (1024L * 1024L) setMaxUploadMiB(maxMiB) val totalMaxBytes: Long = maxMiB * 1024L * 1024L val filePath = uniqueFilePath("init-overflow-numParts") val ex = intercept[WebApplicationException] { datasetResource.multipartUpload( "init", ownerUser.getEmail, multipartDataset.getName, urlEnc(filePath), Optional.of(java.lang.Long.valueOf(totalMaxBytes)), Optional.of(java.lang.Long.valueOf(MinNonFinalPartBytes.toLong)), Optional.empty(), multipartOwnerSessionUser ) } assertStatus(ex, 500) } it should "reject invalid filePath (empty, absolute, '..', control chars)" in { // failures (must throw) assertStatus(intercept[BadRequestException] { initUpload("/absolute.bin", 2) }, 400) assertStatus(intercept[BadRequestException] { initUpload("../escape.bin", 2) }, 400) // control chars rejected intercept[IllegalArgumentException] { initUpload(s"a/${0.toChar}b.bin", 2) } // now succeed (no intercept, because no throw) assert(initUpload("./nope.bin", 2).getStatus == 200) assert(initUpload("a/./b.bin", 2).getStatus == 200) assert(initUpload("a/../escape.bin", 2).getStatus == 200) } it should "reject invalid type parameter" in { val filePath = uniqueFilePath("init-bad-type") val ex = intercept[BadRequestException] { datasetResource.multipartUpload( "not-a-real-type", ownerUser.getEmail, multipartDataset.getName, urlEnc(filePath), Optional.empty(), Optional.empty(), Optional.empty(), multipartOwnerSessionUser ) } assertStatus(ex, 400) } it should "reject init when caller lacks WRITE access" in { val filePath = uniqueFilePath("init-forbidden") val ex = intercept[ForbiddenException] { initUpload(filePath, numParts = 2, user = multipartNoWriteSessionUser) } assertStatus(ex, 403) } it should "handle init race: concurrent init calls converge to a single session (both return 200)" in { val filePath = uniqueFilePath("init-race") val barrier = new CyclicBarrier(2) def callInit(): Either[Throwable, Response] = try { barrier.await() Right(initUpload(filePath, numParts = 2)) } catch { case t: Throwable => Left(t) } val future1 = Future(callInit()) val future2 = Future(callInit()) val results = Await.result(Future.sequence(Seq(future1, future2)), 30.seconds) // No unexpected failures val fails = results.collect { case Left(t) => t } withClue(s"init race failures: ${fails.map(_.getMessage).mkString(", ")}") { fails shouldBe empty } // Both should be OK val oks = results.collect { case Right(r) => r } oks.size shouldEqual 2 oks.foreach(_.getStatus shouldEqual 200) // Exactly one session row exists for this file path val sessionRecord = fetchSession(filePath) sessionRecord should not be null // Placeholders created for expected parts assertPlaceholdersCreated(sessionRecord.getUploadId, expectedParts = 2) //Both responses should report missingParts [1,2] and completedPartsCount 0 oks.foreach { r => val m = entityAsScalaMap(r) mapListOfInts(m("missingParts")) shouldEqual List(1, 2) m("completedPartsCount").toString.toInt shouldEqual 0 } } it should "return 409 if init cannot acquire the session row lock (NOWAIT)" in { val filePath = uniqueFilePath("init-lock-409") initUpload(filePath, numParts = 2).getStatus shouldEqual 200 val connectionProvider = getDSLContext.configuration().connectionProvider() val connection = connectionProvider.acquire() connection.setAutoCommit(false) try { val locking = DSL.using(connection, SQLDialect.POSTGRES) locking .selectFrom(DATASET_UPLOAD_SESSION) .where( DATASET_UPLOAD_SESSION.UID .eq(ownerUser.getUid) .and(DATASET_UPLOAD_SESSION.DID.eq(multipartDataset.getDid)) .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath)) ) .forUpdate() .fetchOne() val ex = intercept[WebApplicationException] { initUpload(filePath, numParts = 2) } ex.getResponse.getStatus shouldEqual 409 } finally { connection.rollback() connectionProvider.release(connection) } } // --------------------------------------------------------------------------- // PART UPLOAD TESTS // --------------------------------------------------------------------------- "multipart-upload/part" should "reject uploadPart if init was not called" in { val filePath = uniqueFilePath("part-no-init") val ex = intercept[NotFoundException] { uploadPart(filePath, partNumber = 1, bytes = Array[Byte](1, 2, 3)) } assertStatus(ex, 404) } it should "reject missing/invalid Content-Length" in { val filePath = uniqueFilePath("part-bad-cl") initUpload(filePath, numParts = 2) assertStatus( intercept[BadRequestException] { uploadPart( filePath, partNumber = 1, bytes = Array[Byte](1, 2, 3), missingContentLength = true ) }, 400 ) assertStatus( intercept[BadRequestException] { uploadPart( filePath, partNumber = 1, bytes = Array[Byte](1, 2, 3), contentLengthOverride = Some(0L) ) }, 400 ) assertStatus( intercept[BadRequestException] { uploadPart( filePath, partNumber = 1, bytes = Array[Byte](1, 2, 3), contentLengthOverride = Some(-5L) ) }, 400 ) } it should "reject non-numeric Content-Length (header poisoning)" in { val filePath = uniqueFilePath("part-cl-nonnumeric") initUpload(filePath, numParts = 1) val ex = intercept[BadRequestException] { uploadPart( filePath, partNumber = 1, bytes = tinyBytes(1.toByte), rawContentLengthOverride = Some("not-a-number") ) } assertStatus(ex, 400) } it should "reject Content-Length that overflows Long (header poisoning)" in { val filePath = uniqueFilePath("part-cl-overflow") initUpload(filePath, numParts = 1) val ex = intercept[BadRequestException] { uploadPart( filePath, partNumber = 1, bytes = tinyBytes(1.toByte), rawContentLengthOverride = Some("999999999999999999999999999999999999999") ) } assertStatus(ex, 400) } it should "reject when Content-Length does not equal the expected part size (attempted size-bypass)" in { val filePath = uniqueFilePath("part-cl-mismatch-expected") initUpload(filePath, numParts = 2) val uploadId = fetchUploadIdOrFail(filePath) val bytes = minPartBytes(1.toByte) // exactly MinNonFinalPartBytes val ex = intercept[BadRequestException] { uploadPart( filePath, partNumber = 1, bytes = bytes, contentLengthOverride = Some(bytes.length.toLong - 1L) // lie by 1 byte ) } assertStatus(ex, 400) // Ensure we didn't accidentally persist an ETag for a rejected upload. fetchPartRows(uploadId).find(_.getPartNumber == 1).get.getEtag shouldEqual "" } it should "not store more bytes than declared Content-Length (send 2x bytes, claim x)" in { val filePath = uniqueFilePath("part-body-gt-cl") val declared: Int = 1024 initUpload(filePath, numParts = 1, lastPartBytes = declared, partSizeBytes = declared) val first = Array.fill[Byte](declared)(1.toByte) val extra = Array.fill[Byte](declared)(2.toByte) val sent = first ++ extra // 2x bytes sent uploadPart( filePath, partNumber = 1, bytes = sent, contentLengthOverride = Some(declared.toLong) // claim only x ).getStatus shouldEqual 200 finishUpload(filePath).getStatus shouldEqual 200 // If anything "accepted" the extra bytes, the committed object would exceed declared size. val repoName = multipartDataset.getRepositoryName val downloaded = LakeFSStorageClient.getFileFromRepo(repoName, "main", filePath) Files.size(Paths.get(downloaded.toURI)) shouldEqual declared.toLong val expected = sha256OfChunks(Seq(first)) val got = sha256OfFile(Paths.get(downloaded.toURI)) got.toSeq shouldEqual expected } it should "reject null/empty filePath param early without depending on error text" in { val httpHeaders = mkHeaders(1L) val ex1 = intercept[BadRequestException] { datasetResource.uploadPart( ownerUser.getEmail, multipartDataset.getName, null, 1, new ByteArrayInputStream(Array.emptyByteArray), httpHeaders, multipartOwnerSessionUser ) } assertStatus(ex1, 400) val ex2 = intercept[BadRequestException] { datasetResource.uploadPart( ownerUser.getEmail, multipartDataset.getName, "", 1, new ByteArrayInputStream(Array.emptyByteArray), httpHeaders, multipartOwnerSessionUser ) } assertStatus(ex2, 400) } it should "reject invalid partNumber (< 1) and partNumber > requested" in { val filePath = uniqueFilePath("part-bad-pn") initUpload(filePath, numParts = 2) assertStatus( intercept[BadRequestException] { uploadPart(filePath, partNumber = 0, bytes = tinyBytes(1.toByte)) }, 400 ) assertStatus( intercept[BadRequestException] { uploadPart(filePath, partNumber = 3, bytes = minPartBytes(2.toByte)) }, 400 ) } it should "reject a non-final part smaller than the minimum size (without checking message)" in { val filePath = uniqueFilePath("part-too-small-nonfinal") initUpload(filePath, numParts = 2) val ex = intercept[BadRequestException] { uploadPart(filePath, partNumber = 1, bytes = tinyBytes(1.toByte)) } assertStatus(ex, 400) val uploadId = fetchUploadIdOrFail(filePath) fetchPartRows(uploadId).find(_.getPartNumber == 1).get.getEtag shouldEqual "" } it should "upload a part successfully and persist its ETag into DATASET_UPLOAD_SESSION_PART" in { val filePath = uniqueFilePath("part-happy-db") initUpload(filePath, numParts = 2) val uploadId = fetchUploadIdOrFail(filePath) fetchPartRows(uploadId).find(_.getPartNumber == 1).get.getEtag shouldEqual "" val bytes = minPartBytes(7.toByte) uploadPart(filePath, partNumber = 1, bytes = bytes).getStatus shouldEqual 200 val after = fetchPartRows(uploadId).find(_.getPartNumber == 1).get after.getEtag should not equal "" } it should "allow retrying the same part sequentially (no duplicates, etag ends non-empty)" in { val filePath = uniqueFilePath("part-retry") initUpload(filePath, numParts = 2) val uploadId = fetchUploadIdOrFail(filePath) uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200 uploadPart(filePath, 1, minPartBytes(2.toByte)).getStatus shouldEqual 200 val rows = fetchPartRows(uploadId).filter(_.getPartNumber == 1) rows.size shouldEqual 1 rows.head.getEtag should not equal "" } it should "apply per-part locking: return 409 if that part row is locked by another uploader" in { val filePath = uniqueFilePath("part-lock") initUpload(filePath, numParts = 2) val uploadId = fetchUploadIdOrFail(filePath) val connectionProvider = getDSLContext.configuration().connectionProvider() val connection = connectionProvider.acquire() connection.setAutoCommit(false) try { val locking = DSL.using(connection, SQLDialect.POSTGRES) locking .selectFrom(DATASET_UPLOAD_SESSION_PART) .where( DATASET_UPLOAD_SESSION_PART.UPLOAD_ID .eq(uploadId) .and(DATASET_UPLOAD_SESSION_PART.PART_NUMBER.eq(1)) ) .forUpdate() .fetchOne() val ex = intercept[WebApplicationException] { uploadPart(filePath, 1, minPartBytes(1.toByte)) } assertStatus(ex, 409) } finally { connection.rollback() connectionProvider.release(connection) } uploadPart(filePath, 1, minPartBytes(3.toByte)).getStatus shouldEqual 200 } it should "not block other parts: locking part 1 does not prevent uploading part 2" in { val filePath = uniqueFilePath("part-lock-other-part") initUpload(filePath, numParts = 2) val uploadId = fetchUploadIdOrFail(filePath) val connectionProvider = getDSLContext.configuration().connectionProvider() val connection = connectionProvider.acquire() connection.setAutoCommit(false) try { val locking = DSL.using(connection, SQLDialect.POSTGRES) locking .selectFrom(DATASET_UPLOAD_SESSION_PART) .where( DATASET_UPLOAD_SESSION_PART.UPLOAD_ID .eq(uploadId) .and(DATASET_UPLOAD_SESSION_PART.PART_NUMBER.eq(1)) ) .forUpdate() .fetchOne() uploadPart(filePath, 2, tinyBytes(9.toByte)).getStatus shouldEqual 200 } finally { connection.rollback() connectionProvider.release(connection) } } it should "reject uploadPart when caller lacks WRITE access" in { val filePath = uniqueFilePath("part-forbidden") initUpload(filePath, numParts = 2) val ex = intercept[ForbiddenException] { uploadPart(filePath, 1, minPartBytes(1.toByte), user = multipartNoWriteSessionUser) } assertStatus(ex, 403) } "multipart-upload/part" should "treat retries as idempotent once ETag is set (no overwrite on second call)" in { val filePath = uniqueFilePath("part-idempotent") initUpload( filePath, numParts = 1, lastPartBytes = 16, partSizeBytes = 16 ).getStatus shouldEqual 200 val uploadId = fetchUploadIdOrFail(filePath) val n = 16 val bytes1: Array[Byte] = Array.tabulate[Byte](n)(i => (i + 1).toByte) val bytes2: Array[Byte] = Array.tabulate[Byte](n)(i => (i + 1).toByte) uploadPart(filePath, 1, bytes1).getStatus shouldEqual 200 val etag1 = fetchPartRows(uploadId).find(_.getPartNumber == 1).get.getEtag uploadPart(filePath, 1, bytes2).getStatus shouldEqual 200 val etag2 = fetchPartRows(uploadId).find(_.getPartNumber == 1).get.getEtag etag2 shouldEqual etag1 finishUpload(filePath).getStatus shouldEqual 200 val repoName = multipartDataset.getRepositoryName val downloaded = LakeFSStorageClient.getFileFromRepo(repoName, "main", filePath) val gotBytes = Files.readAllBytes(Paths.get(downloaded.toURI)) gotBytes.toSeq shouldEqual bytes1.toSeq } // --------------------------------------------------------------------------- // FINISH TESTS // --------------------------------------------------------------------------- "multipart-upload?type=finish" should "reject finish if init was not called" in { val filePath = uniqueFilePath("finish-no-init") val ex = intercept[NotFoundException] { finishUpload(filePath) } assertStatus(ex, 404) } it should "not commit an oversized upload if the max upload size is tightened before finish (server-side rollback)" in { val filePath = uniqueFilePath("finish-max-tightened") val twoMiB: Long = 2L * 1024L * 1024L // Allow init + part upload under a higher limit. setMaxUploadMiB(3) // 3 MiB datasetResource .multipartUpload( "init", ownerUser.getEmail, multipartDataset.getName, urlEnc(filePath), Optional.of(java.lang.Long.valueOf(twoMiB)), Optional.of(java.lang.Long.valueOf(twoMiB)), Optional.empty(), multipartOwnerSessionUser ) .getStatus shouldEqual 200 uploadPart(filePath, 1, Array.fill[Byte](twoMiB.toInt)(7.toByte)).getStatus shouldEqual 200 // Tighten the limit just before finish. setMaxUploadMiB(1) // 1 MiB val ex = intercept[WebApplicationException] { finishUpload(filePath) // this now THROWS 413 (doesn't return Response) } ex.getResponse.getStatus shouldEqual 413 // Oversized objects must not remain accessible after finish (rollback happened). val repoName = multipartDataset.getRepositoryName val notFound = intercept[ApiException] { LakeFSStorageClient.getFileFromRepo(repoName, "main", filePath) } notFound.getCode shouldEqual 404 // Session still available. fetchSession(filePath) should not be null } it should "reject finish when no parts were uploaded (all placeholders empty) without checking messages" in { val filePath = uniqueFilePath("finish-no-parts") initUpload(filePath, numParts = 2) val ex = intercept[WebApplicationException] { finishUpload(filePath) } assertStatus(ex, 409) fetchSession(filePath) should not be null } it should "reject finish when some parts are missing (etag empty treated as missing)" in { val filePath = uniqueFilePath("finish-missing") initUpload(filePath, numParts = 3) uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200 val ex = intercept[WebApplicationException] { finishUpload(filePath) } assertStatus(ex, 409) val uploadId = fetchUploadIdOrFail(filePath) fetchPartRows(uploadId).find(_.getPartNumber == 2).get.getEtag shouldEqual "" fetchPartRows(uploadId).find(_.getPartNumber == 3).get.getEtag shouldEqual "" } it should "reject finish when extra part rows exist in DB (bypass endpoint) without checking messages" in { val filePath = uniqueFilePath("finish-extra-db") initUpload(filePath, numParts = 2) uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200 uploadPart(filePath, 2, tinyBytes(2.toByte)).getStatus shouldEqual 200 val sessionRecord = fetchSession(filePath) val uploadId = sessionRecord.getUploadId getDSLContext .insertInto(DATASET_UPLOAD_SESSION_PART) .set(DATASET_UPLOAD_SESSION_PART.UPLOAD_ID, uploadId) .set(DATASET_UPLOAD_SESSION_PART.PART_NUMBER, Integer.valueOf(3)) .set(DATASET_UPLOAD_SESSION_PART.ETAG, "bogus-etag") .execute() val ex = intercept[WebApplicationException] { finishUpload(filePath) } assertStatus(ex, 500) fetchSession(filePath) should not be null fetchPartRows(uploadId).nonEmpty shouldEqual true } it should "finish successfully when all parts have non-empty etags; delete session + part rows" in { val filePath = uniqueFilePath("finish-happy") initUpload(filePath, numParts = 3) uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200 uploadPart(filePath, 2, minPartBytes(2.toByte)).getStatus shouldEqual 200 uploadPart(filePath, 3, tinyBytes(3.toByte)).getStatus shouldEqual 200 val uploadId = fetchUploadIdOrFail(filePath) val resp = finishUpload(filePath) resp.getStatus shouldEqual 200 fetchSession(filePath) shouldBe null fetchPartRows(uploadId) shouldBe empty } it should "be idempotent-ish: second finish should return NotFound after successful finish" in { val filePath = uniqueFilePath("finish-twice") initUpload(filePath, numParts = 1) uploadPart(filePath, 1, tinyBytes(1.toByte)).getStatus shouldEqual 200 finishUpload(filePath).getStatus shouldEqual 200 val ex = intercept[NotFoundException] { finishUpload(filePath) } assertStatus(ex, 404) } it should "reject finish when caller lacks WRITE access" in { val filePath = uniqueFilePath("finish-forbidden") initUpload(filePath, numParts = 1) uploadPart(filePath, 1, tinyBytes(1.toByte)).getStatus shouldEqual 200 val ex = intercept[ForbiddenException] { finishUpload(filePath, user = multipartNoWriteSessionUser) } assertStatus(ex, 403) } it should "return 409 CONFLICT if the session row is locked by another finalizer/aborter" in { val filePath = uniqueFilePath("finish-lock-race") initUpload(filePath, numParts = 1) uploadPart(filePath, 1, tinyBytes(1.toByte)).getStatus shouldEqual 200 val connectionProvider = getDSLContext.configuration().connectionProvider() val connection = connectionProvider.acquire() connection.setAutoCommit(false) try { val locking = DSL.using(connection, SQLDialect.POSTGRES) locking .selectFrom(DATASET_UPLOAD_SESSION) .where( DATASET_UPLOAD_SESSION.UID .eq(ownerUser.getUid) .and(DATASET_UPLOAD_SESSION.DID.eq(multipartDataset.getDid)) .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath)) ) .forUpdate() .fetchOne() val ex = intercept[WebApplicationException] { finishUpload(filePath) } assertStatus(ex, 409) } finally { connection.rollback() connectionProvider.release(connection) } } // --------------------------------------------------------------------------- // ABORT TESTS // --------------------------------------------------------------------------- "multipart-upload?type=abort" should "reject abort if init was not called" in { val filePath = uniqueFilePath("abort-no-init") val ex = intercept[NotFoundException] { abortUpload(filePath) } assertStatus(ex, 404) } it should "abort successfully; delete session + part rows" in { val filePath = uniqueFilePath("abort-happy") initUpload(filePath, numParts = 2) uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200 val uploadId = fetchUploadIdOrFail(filePath) abortUpload(filePath).getStatus shouldEqual 200 fetchSession(filePath) shouldBe null fetchPartRows(uploadId) shouldBe empty } it should "reject abort when caller lacks WRITE access" in { val filePath = uniqueFilePath("abort-forbidden") initUpload(filePath, numParts = 1) val ex = intercept[ForbiddenException] { abortUpload(filePath, user = multipartNoWriteSessionUser) } assertStatus(ex, 403) } it should "return 409 CONFLICT if the session row is locked by another finalizer/aborter" in { val filePath = uniqueFilePath("abort-lock-race") initUpload(filePath, numParts = 1) val connectionProvider = getDSLContext.configuration().connectionProvider() val connection = connectionProvider.acquire() connection.setAutoCommit(false) try { val locking = DSL.using(connection, SQLDialect.POSTGRES) locking .selectFrom(DATASET_UPLOAD_SESSION) .where( DATASET_UPLOAD_SESSION.UID .eq(ownerUser.getUid) .and(DATASET_UPLOAD_SESSION.DID.eq(multipartDataset.getDid)) .and(DATASET_UPLOAD_SESSION.FILE_PATH.eq(filePath)) ) .forUpdate() .fetchOne() val ex = intercept[WebApplicationException] { abortUpload(filePath) } assertStatus(ex, 409) } finally { connection.rollback() connectionProvider.release(connection) } } it should "be consistent: abort after finish should return NotFound" in { val filePath = uniqueFilePath("abort-after-finish") initUpload(filePath, numParts = 1) uploadPart(filePath, 1, tinyBytes(1.toByte)).getStatus shouldEqual 200 finishUpload(filePath).getStatus shouldEqual 200 val ex = intercept[NotFoundException] { abortUpload(filePath) } assertStatus(ex, 404) } // --------------------------------------------------------------------------- // FAILURE / RESILIENCE (still unit tests; simulated failures) // --------------------------------------------------------------------------- "multipart upload implementation" should "release locks and keep DB consistent if the incoming stream fails mid-upload (simulated network drop)" in { val filePath = uniqueFilePath("netfail-upload-stream") initUpload(filePath, numParts = 2).getStatus shouldEqual 200 val uploadId = fetchUploadIdOrFail(filePath) val payload = minPartBytes(5.toByte) val flaky = new InputStream { private var pos = 0 override def read(): Int = { if (pos >= 1024) throw new IOException("simulated network drop") val b = payload(pos) & 0xff pos += 1 b } } intercept[Throwable] { uploadPartWithStream( filePath, partNumber = 1, stream = flaky, contentLength = payload.length.toLong ) } fetchPartRows(uploadId).find(_.getPartNumber == 1).get.getEtag shouldEqual "" uploadPart(filePath, 1, payload).getStatus shouldEqual 200 fetchPartRows(uploadId).find(_.getPartNumber == 1).get.getEtag should not equal "" } it should "not delete session/parts if finalize fails downstream (simulate by corrupting an ETag)" in { val filePath = uniqueFilePath("netfail-finish") initUpload(filePath, numParts = 2).getStatus shouldEqual 200 uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200 uploadPart(filePath, 2, tinyBytes(2.toByte)).getStatus shouldEqual 200 val uploadId = fetchUploadIdOrFail(filePath) getDSLContext .update(DATASET_UPLOAD_SESSION_PART) .set(DATASET_UPLOAD_SESSION_PART.ETAG, "definitely-not-a-real-etag") .where( DATASET_UPLOAD_SESSION_PART.UPLOAD_ID .eq(uploadId) .and(DATASET_UPLOAD_SESSION_PART.PART_NUMBER.eq(1)) ) .execute() intercept[Throwable] { finishUpload(filePath) } fetchSession(filePath) should not be null fetchPartRows(uploadId).nonEmpty shouldEqual true } it should "allow abort + re-init after part 1 succeeded but part 2 drops mid-flight; then complete successfully" in { val filePath = uniqueFilePath("reinit-after-part2-drop") initUpload(filePath, numParts = 2, lastPartBytes = 1024 * 1024).getStatus shouldEqual 200 val uploadId1 = fetchUploadIdOrFail(filePath) uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200 val bytesPart2 = Array.fill[Byte](1024 * 1024)(2.toByte) intercept[Throwable] { uploadPartWithStream( filePath, partNumber = 2, stream = flakyStream(bytesPart2, failAfterBytes = 4096), contentLength = bytesPart2.length.toLong ) } abortUpload(filePath).getStatus shouldEqual 200 fetchSession(filePath) shouldBe null fetchPartRows(uploadId1) shouldBe empty initUpload(filePath, numParts = 2, lastPartBytes = 123).getStatus shouldEqual 200 uploadPart(filePath, 1, minPartBytes(3.toByte)).getStatus shouldEqual 200 uploadPart(filePath, 2, tinyBytes(4.toByte, n = 123)).getStatus shouldEqual 200 finishUpload(filePath).getStatus shouldEqual 200 fetchSession(filePath) shouldBe null } it should "allow re-upload after failures: (1) part1 drop, (2) part2 drop, (3) finalize failure; each followed by abort + re-init + success" in { def abortAndAssertClean(filePath: String, uploadId: String): Unit = { abortUpload(filePath).getStatus shouldEqual 200 fetchSession(filePath) shouldBe null fetchPartRows(uploadId) shouldBe empty } def reinitAndFinishHappy(filePath: String): Unit = { initUpload(filePath, numParts = 2, lastPartBytes = 321).getStatus shouldEqual 200 uploadPart(filePath, 1, minPartBytes(7.toByte)).getStatus shouldEqual 200 uploadPart(filePath, 2, tinyBytes(8.toByte, n = 321)).getStatus shouldEqual 200 finishUpload(filePath).getStatus shouldEqual 200 fetchSession(filePath) shouldBe null } withClue("scenario (1): part1 mid-flight drop") { val filePath = uniqueFilePath("reupload-part1-drop") initUpload(filePath, numParts = 2).getStatus shouldEqual 200 val uploadId = fetchUploadIdOrFail(filePath) val p1 = minPartBytes(5.toByte) intercept[Throwable] { uploadPartWithStream( filePath, partNumber = 1, stream = flakyStream(p1, failAfterBytes = 4096), contentLength = p1.length.toLong ) } fetchPartRows(uploadId).find(_.getPartNumber == 1).get.getEtag shouldEqual "" abortAndAssertClean(filePath, uploadId) reinitAndFinishHappy(filePath) } withClue("scenario (2): part2 mid-flight drop") { val filePath = uniqueFilePath("reupload-part2-drop") initUpload(filePath, numParts = 2, lastPartBytes = 1024 * 1024).getStatus shouldEqual 200 val uploadId = fetchUploadIdOrFail(filePath) uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200 val bytesPart2 = Array.fill[Byte](1024 * 1024)(2.toByte) intercept[Throwable] { uploadPartWithStream( filePath, partNumber = 2, stream = flakyStream(bytesPart2, failAfterBytes = 4096), contentLength = bytesPart2.length.toLong ) } abortAndAssertClean(filePath, uploadId) reinitAndFinishHappy(filePath) } withClue("scenario (3): finalize failure then re-upload") { val filePath = uniqueFilePath("reupload-finalize-fail") initUpload(filePath, numParts = 2).getStatus shouldEqual 200 uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200 uploadPart(filePath, 2, tinyBytes(2.toByte)).getStatus shouldEqual 200 val uploadId = fetchUploadIdOrFail(filePath) getDSLContext .update(DATASET_UPLOAD_SESSION_PART) .set(DATASET_UPLOAD_SESSION_PART.ETAG, "definitely-not-a-real-etag") .where( DATASET_UPLOAD_SESSION_PART.UPLOAD_ID .eq(uploadId) .and(DATASET_UPLOAD_SESSION_PART.PART_NUMBER.eq(1)) ) .execute() intercept[Throwable] { finishUpload(filePath) } fetchSession(filePath) should not be null fetchPartRows(uploadId).nonEmpty shouldEqual true abortAndAssertClean(filePath, uploadId) reinitAndFinishHappy(filePath) } } // --------------------------------------------------------------------------- // CORRUPTION CHECKS // --------------------------------------------------------------------------- it should "upload without corruption (sha256 matches final object)" in { val filePath = uniqueFilePath("sha256-positive") initUpload(filePath, numParts = 3, lastPartBytes = 123).getStatus shouldEqual 200 val part1 = minPartBytes(1.toByte) val part2 = minPartBytes(2.toByte) val part3 = Array.fill[Byte](123)(3.toByte) uploadPart(filePath, 1, part1).getStatus shouldEqual 200 uploadPart(filePath, 2, part2).getStatus shouldEqual 200 uploadPart(filePath, 3, part3).getStatus shouldEqual 200 finishUpload(filePath).getStatus shouldEqual 200 val expected = sha256OfChunks(Seq(part1, part2, part3)) val repoName = multipartDataset.getRepositoryName val ref = "main" val downloaded = LakeFSStorageClient.getFileFromRepo(repoName, ref, filePath) val got = sha256OfFile(Paths.get(downloaded.toURI)) got.toSeq shouldEqual expected.toSeq } it should "detect corruption (sha256 mismatch when a part is altered)" in { val filePath = uniqueFilePath("sha256-negative") initUpload(filePath, numParts = 3, lastPartBytes = 123).getStatus shouldEqual 200 val part1 = minPartBytes(1.toByte) val part2 = minPartBytes(2.toByte) val part3 = Array.fill[Byte](123)(3.toByte) val intendedHash = sha256OfChunks(Seq(part1, part2, part3)) val part2corrupt = part2.clone() part2corrupt(0) = (part2corrupt(0) ^ 0x01).toByte uploadPart(filePath, 1, part1).getStatus shouldEqual 200 uploadPart(filePath, 2, part2corrupt).getStatus shouldEqual 200 uploadPart(filePath, 3, part3).getStatus shouldEqual 200 finishUpload(filePath).getStatus shouldEqual 200 val repoName = multipartDataset.getRepositoryName val ref = "main" val downloaded = LakeFSStorageClient.getFileFromRepo(repoName, ref, filePath) val gotHash = sha256OfFile(Paths.get(downloaded.toURI)) gotHash.toSeq should not equal intendedHash.toSeq val corruptHash = sha256OfChunks(Seq(part1, part2corrupt, part3)) gotHash.toSeq shouldEqual corruptHash.toSeq } // --------------------------------------------------------------------------- // STRESS / SOAK TESTS (tagged) // --------------------------------------------------------------------------- it should "survive 2 concurrent multipart uploads (fan-out)" taggedAs (StressMultipart, Slow) in { val parallelUploads = 2 val maxParts = 2 def oneUpload(i: Int): Future[Unit] = Future { val filePath = uniqueFilePath(s"stress-$i") val numParts = 2 + Random.nextInt(maxParts - 1) initUpload(filePath, numParts, lastPartBytes = 1024).getStatus shouldEqual 200 val sharedMin = minPartBytes((i % 127).toByte) val partFuts = (1 to numParts).map { partN => Future { val bytes = if (partN < numParts) sharedMin else tinyBytes((partN % 127).toByte, n = 1024) uploadPart(filePath, partN, bytes).getStatus shouldEqual 200 } } Await.result(Future.sequence(partFuts), 60.seconds) finishUpload(filePath).getStatus shouldEqual 200 fetchSession(filePath) shouldBe null } val all = Future.sequence((1 to parallelUploads).map(oneUpload)) Await.result(all, 180.seconds) } it should "throttle concurrent uploads of the SAME part via per-part locks" taggedAs (StressMultipart, Slow) in { val filePath = uniqueFilePath("stress-same-part") initUpload(filePath, numParts = 2).getStatus shouldEqual 200 val contenders = 2 val barrier = new CyclicBarrier(contenders) def tryUploadStatus(): Future[Int] = Future { barrier.await() try { uploadPart(filePath, 1, minPartBytes(7.toByte)).getStatus } catch { case e: WebApplicationException => e.getResponse.getStatus } } val statuses = Await.result(Future.sequence((1 to contenders).map(_ => tryUploadStatus())), 60.seconds) statuses.foreach { s => s should (be(200) or be(409)) } statuses.count(_ == 200) should be >= 1 val uploadId = fetchUploadIdOrFail(filePath) val part1 = fetchPartRows(uploadId).find(_.getPartNumber == 1).get part1.getEtag.trim should not be "" } // =========================================================================== // Cover Image Tests // =========================================================================== "updateDatasetCoverImage" should "reject path traversal attempts" in { val maliciousPaths = Seq( "../../../etc/passwd", "v1/../../secret.txt", "../escape.jpg" ) maliciousPaths.foreach { path => val request = DatasetResource.CoverImageRequest(path) assertThrows[BadRequestException] { datasetResource.updateDatasetCoverImage( baseDataset.getDid, request, sessionUser ) } } } it should "reject absolute paths" in { val absolutePaths = Seq( "/etc/passwd", "/var/log/system.log" ) absolutePaths.foreach { path => val request = DatasetResource.CoverImageRequest(path) assertThrows[BadRequestException] { datasetResource.updateDatasetCoverImage( baseDataset.getDid, request, sessionUser ) } } } it should "reject invalid file types" in { val invalidPaths = Seq( "v1/script.js", "v1/document.pdf", "v1/data.csv" ) invalidPaths.foreach { path => val request = DatasetResource.CoverImageRequest(path) assertThrows[BadRequestException] { datasetResource.updateDatasetCoverImage( baseDataset.getDid, request, sessionUser ) } } } it should "reject empty or null cover image path" in { assertThrows[BadRequestException] { datasetResource.updateDatasetCoverImage( baseDataset.getDid, DatasetResource.CoverImageRequest(""), sessionUser ) } assertThrows[BadRequestException] { datasetResource.updateDatasetCoverImage( baseDataset.getDid, DatasetResource.CoverImageRequest(null), sessionUser ) } } it should "reject when user lacks WRITE access" in { val request = DatasetResource.CoverImageRequest("v1/cover.jpg") assertThrows[ForbiddenException] { datasetResource.updateDatasetCoverImage( baseDataset.getDid, request, sessionUser2 ) } } it should "set cover image successfully" in { testDatasetVersion val request = DatasetResource.CoverImageRequest(testCoverImagePath) val response = datasetResource.updateDatasetCoverImage( baseDataset.getDid, request, sessionUser ) response.getStatus shouldEqual 200 val updated = datasetDao.fetchOneByDid(baseDataset.getDid) updated.getCoverImage shouldEqual testCoverImagePath } "getDatasetCover" should "reject private dataset cover for anonymous users" in { val dataset = datasetDao.fetchOneByDid(baseDataset.getDid) dataset.setIsPublic(false) dataset.setCoverImage("v1/cover.jpg") datasetDao.update(dataset) assertThrows[ForbiddenException] { datasetResource.getDatasetCover(baseDataset.getDid, Optional.empty()) } } it should "reject private dataset cover for users without access" in { val dataset = datasetDao.fetchOneByDid(baseDataset.getDid) dataset.setOwnerUid(ownerUser.getUid) dataset.setIsPublic(false) dataset.setCoverImage("v1/cover.jpg") datasetDao.update(dataset) assertThrows[ForbiddenException] { datasetResource.getDatasetCover(baseDataset.getDid, Optional.of(sessionUser2)) } } it should "return 404 when no cover image is set" in { val dataset = datasetDao.fetchOneByDid(baseDataset.getDid) dataset.setCoverImage(null) dataset.setIsPublic(true) datasetDao.update(dataset) assertThrows[NotFoundException] { datasetResource.getDatasetCover(baseDataset.getDid, Optional.of(sessionUser)) } } it should "get cover image successfully with 307 redirect" in { testDatasetVersion val dataset = datasetDao.fetchOneByDid(baseDataset.getDid) dataset.setIsPublic(true) dataset.setCoverImage(testCoverImagePath) datasetDao.update(dataset) val response = datasetResource.getDatasetCover( baseDataset.getDid, Optional.empty() ) response.getStatus shouldEqual 307 response.getHeaderString("Location") should not be null } "LakeFS error handling" should "return 500 when ETag is invalid, with the message included in the error response body" in { val filePath = uniqueFilePath("error-body") initUpload(filePath, 2).getStatus shouldEqual 200 uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200 uploadPart(filePath, 2, tinyBytes(2.toByte)).getStatus shouldEqual 200 val uploadId = fetchUploadIdOrFail(filePath) getDSLContext .update(DATASET_UPLOAD_SESSION_PART) .set(DATASET_UPLOAD_SESSION_PART.ETAG, "BAD") .where(DATASET_UPLOAD_SESSION_PART.UPLOAD_ID.eq(uploadId)) .execute() val ex = intercept[WebApplicationException] { finishUpload(filePath) } ex.getResponse.getStatus shouldEqual 500 Option(ex.getResponse.getEntity).map(_.toString).getOrElse("") should include( "LakeFS request failed due to an unexpected server error." ) abortUpload(filePath) } it should "return 400 when physicalAddress is invalid" in { val filePath = uniqueFilePath("missing-physical-address") initUpload(filePath, 2).getStatus shouldEqual 200 uploadPart(filePath, 1, minPartBytes(1.toByte)).getStatus shouldEqual 200 uploadPart(filePath, 2, tinyBytes(2.toByte)).getStatus shouldEqual 200 val uploadId = fetchUploadIdOrFail(filePath) getDSLContext .update(DATASET_UPLOAD_SESSION) .set(DATASET_UPLOAD_SESSION.PHYSICAL_ADDRESS, "BAD") .where(DATASET_UPLOAD_SESSION.UPLOAD_ID.eq(uploadId)) .execute() val ex = intercept[WebApplicationException] { finishUpload(filePath) } ex.getResponse.getStatus shouldEqual 400 Option(ex.getResponse.getEntity).map(_.toString).getOrElse("") should include( "LakeFS rejected the request" ) intercept[WebApplicationException] { abortUpload(filePath) }.getResponse.getStatus shouldEqual 400 // DB session is cleaned up fetchSession(filePath) shouldBe null fetchPartRows(uploadId) shouldBe empty } // =========================================================================== // Pagination test – verify that listing APIs return more than the default (100 items) // =========================================================================== "LakeFS pagination" should "return all files when count exceeds one page for both uncommitted and committed objects" taggedAs Slow in { val repoName = s"pagination-${System.nanoTime()}-${Random.alphanumeric.take(6).mkString.toLowerCase}" LakeFSStorageClient.initRepo(repoName) val totalFiles = 110 (1 to totalFiles).foreach { i => LakeFSStorageClient.writeFileToRepo( repoName, s"file-$i.txt", new ByteArrayInputStream(s"content-$i".getBytes(StandardCharsets.UTF_8)) ) } // before commit: 110 files should appear as uncommitted diffs LakeFSStorageClient.retrieveUncommittedObjects(repoName).size shouldEqual totalFiles // after commit: 110 files should appear as committed objects val commit = LakeFSStorageClient.withCreateVersion(repoName, "commit all files") {} LakeFSStorageClient.retrieveObjectsOfVersion(repoName, commit.getId).size shouldEqual totalFiles } } ================================================ FILE: frontend/.editorconfig ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Editor configuration, see http://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: frontend/.eslintrc.json ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. { "root": true, "ignorePatterns": ["projects/**/*"], "overrides": [ { "files": ["*.ts"], "parser": "@typescript-eslint/parser", "parserOptions": { "project": ["tsconfig.json"], "createDefaultProgram": true }, "extends": [ "plugin:@angular-eslint/recommended", "plugin:@angular-eslint/template/process-inline-templates", "plugin:rxjs/recommended" ], "rules": { "@angular-eslint/component-selector": [ "error", { "type": "element", "prefix": "texera", "style": "kebab-case" } ], "@angular-eslint/directive-selector": [ "error", { "type": "attribute", "prefix": "texera", "style": "camelCase" } ], "@angular-eslint/prefer-standalone": "off", "@angular-eslint/prefer-inject": "off", "@typescript-eslint/consistent-type-definitions": "off", "@typescript-eslint/dot-notation": "off", "@typescript-eslint/explicit-member-accessibility": [ "off", { "accessibility": "explicit" } ], "brace-style": ["error", "1tbs"], "dot-notation": "off", "id-blacklist": "off", "id-match": "off", "indent": "off", "no-empty-function": "off", "no-shadow": "off", "no-underscore-dangle": "off", "no-unused-expressions": "error", "quotes": ["error", "double", { "avoidEscape": true }], "rxjs-angular/prefer-takeuntil": [ "error", { "alias": ["untilDestroyed"], "checkComplete": true, "checkDecorators": ["Component"], "checkDestroy": false } ], "rxjs/no-unsafe-takeuntil": [ "error", { "alias": ["untilDestroyed"] } ], "rxjs/no-nested-subscribe": "off", "rxjs/no-sharereplay": "off", "rxjs/no-unsafe-subject-next": "off", "rxjs/no-index": "error", "rxjs/no-internal": "error", "rxjs/no-compat": "error" }, "plugins": ["rxjs-angular"] }, { "files": ["*.html"], "extends": ["plugin:@angular-eslint/template/recommended"], "rules": { "@angular-eslint/template/prefer-control-flow": "off" } }, { "files": ["*.html"], "excludedFiles": ["*inline-template-*.component.html"], "extends": [], "rules": { "@angular-eslint/template/prefer-control-flow": "off" } } ] } ================================================ FILE: frontend/.gitignore ================================================ /.angular/cache /.nx # See http://help.github.com/ignore-files/ for more about ignoring files. # compiled output /dist /dist-server /tmp /out-tsc src/environments/version.ts # test coverage /coverage # vitest browser-mode snapshot baselines **/__screenshots__/ # dependencies /node_modules .angular # yarn v4 related .yarn/* !.yarn/cache !.yarn/patches !.yarn/plugins !.yarn/releases !.yarn/sdks !.yarn/versions # nx migration /migrations.json ================================================ FILE: frontend/.nvmrc ================================================ lts/* engine-strict=true save-exact=true ================================================ FILE: frontend/.prettierignore ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Ignore artifacts: dist build coverage node_modules **/environments/version.ts .angular /.nx/cache /.nx/workspace-data src/app/common/type/proto ================================================ FILE: frontend/.prettierrc.json ================================================ { "printWidth": 120, "tabWidth": 2, "useTabs": false, "semi": true, "singleQuote": false, "quoteProps": "as-needed", "trailingComma": "es5", "bracketSameLine": true, "bracketSpacing": true, "arrowParens": "avoid", "endOfLine": "lf", "singleAttributePerLine": true, "overrides": [ { "files": "*.component.html", "options": { "parser": "angular" } }, { "files": "*.html", "options": { "parser": "html" } } ] } ================================================ FILE: frontend/.yarn/releases/yarn-4.14.1.cjs ================================================ #!/usr/bin/env node /* eslint-disable */ //prettier-ignore (()=>{var gje=Object.create;var tU=Object.defineProperty;var mje=Object.getOwnPropertyDescriptor;var yje=Object.getOwnPropertyNames;var Eje=Object.getPrototypeOf,Ije=Object.prototype.hasOwnProperty;var Ie=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,r)=>(typeof require<"u"?require:t)[r]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});var Xe=(e,t)=>()=>(e&&(t=e(e=0)),t);var G=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),Vt=(e,t)=>{for(var r in t)tU(e,r,{get:t[r],enumerable:!0})},Cje=(e,t,r,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of yje(t))!Ije.call(e,a)&&a!==r&&tU(e,a,{get:()=>t[a],enumerable:!(s=mje(t,a))||s.enumerable});return e};var et=(e,t,r)=>(r=e!=null?gje(Eje(e)):{},Cje(t||!e||!e.__esModule?tU(r,"default",{value:e,enumerable:!0}):r,e));var Ai={};Vt(Ai,{SAFE_TIME:()=>fZ,S_IFDIR:()=>JP,S_IFLNK:()=>KP,S_IFMT:()=>Hf,S_IFREG:()=>b2});var Hf,JP,b2,KP,fZ,AZ=Xe(()=>{Hf=61440,JP=16384,b2=32768,KP=40960,fZ=456789e3});var or={};Vt(or,{EBADF:()=>qo,EBUSY:()=>wje,EEXIST:()=>Pje,EINVAL:()=>vje,EISDIR:()=>bje,ENOENT:()=>Sje,ENOSYS:()=>Bje,ENOTDIR:()=>Dje,ENOTEMPTY:()=>kje,EOPNOTSUPP:()=>Qje,EROFS:()=>xje,ERR_DIR_CLOSED:()=>rU});function Bc(e,t){return Object.assign(new Error(`${e}: ${t}`),{code:e})}function wje(e){return Bc("EBUSY",e)}function Bje(e,t){return Bc("ENOSYS",`${e}, ${t}`)}function vje(e){return Bc("EINVAL",`invalid argument, ${e}`)}function qo(e){return Bc("EBADF",`bad file descriptor, ${e}`)}function Sje(e){return Bc("ENOENT",`no such file or directory, ${e}`)}function Dje(e){return Bc("ENOTDIR",`not a directory, ${e}`)}function bje(e){return Bc("EISDIR",`illegal operation on a directory, ${e}`)}function Pje(e){return Bc("EEXIST",`file already exists, ${e}`)}function xje(e){return Bc("EROFS",`read-only filesystem, ${e}`)}function kje(e){return Bc("ENOTEMPTY",`directory not empty, ${e}`)}function Qje(e){return Bc("EOPNOTSUPP",`operation not supported, ${e}`)}function rU(){return Bc("ERR_DIR_CLOSED","Directory handle was closed")}var zP=Xe(()=>{});var al={};Vt(al,{BigIntStatsEntry:()=>aE,DEFAULT_MODE:()=>sU,DirEntry:()=>nU,StatEntry:()=>oE,areStatsEqual:()=>oU,clearStats:()=>XP,convertToBigIntStats:()=>Tje,makeDefaultStats:()=>pZ,makeEmptyStats:()=>Rje});function pZ(){return new oE}function Rje(){return XP(pZ())}function XP(e){for(let t in e)if(Object.hasOwn(e,t)){let r=e[t];typeof r=="number"?e[t]=0:typeof r=="bigint"?e[t]=BigInt(0):iU.types.isDate(r)&&(e[t]=new Date(0))}return e}function Tje(e){let t=new aE;for(let r in e)if(Object.hasOwn(e,r)){let s=e[r];typeof s=="number"?t[r]=BigInt(Math.floor(s)):iU.types.isDate(s)&&(t[r]=new Date(s))}return t.atimeNs=t.atimeMs*BigInt(1e6)+BigInt(Math.floor(e.atimeMs%1*1e3))*BigInt(1e3),t.mtimeNs=t.mtimeMs*BigInt(1e6)+BigInt(Math.floor(e.mtimeMs%1*1e3))*BigInt(1e3),t.ctimeNs=t.ctimeMs*BigInt(1e6)+BigInt(Math.floor(e.ctimeMs%1*1e3))*BigInt(1e3),t.birthtimeNs=t.birthtimeMs*BigInt(1e6)+BigInt(Math.floor(e.birthtimeMs%1*1e3))*BigInt(1e3),t}function oU(e,t){if(e.atimeMs!==t.atimeMs||e.birthtimeMs!==t.birthtimeMs||e.blksize!==t.blksize||e.blocks!==t.blocks||e.ctimeMs!==t.ctimeMs||e.dev!==t.dev||e.gid!==t.gid||e.ino!==t.ino||e.isBlockDevice()!==t.isBlockDevice()||e.isCharacterDevice()!==t.isCharacterDevice()||e.isDirectory()!==t.isDirectory()||e.isFIFO()!==t.isFIFO()||e.isFile()!==t.isFile()||e.isSocket()!==t.isSocket()||e.isSymbolicLink()!==t.isSymbolicLink()||e.mode!==t.mode||e.mtimeMs!==t.mtimeMs||e.nlink!==t.nlink||e.rdev!==t.rdev||e.size!==t.size||e.uid!==t.uid)return!1;let r=e,s=t;return!(r.atimeNs!==s.atimeNs||r.mtimeNs!==s.mtimeNs||r.ctimeNs!==s.ctimeNs||r.birthtimeNs!==s.birthtimeNs)}var iU,sU,nU,oE,aE,aU=Xe(()=>{iU=et(Ie("util")),sU=33188,nU=class{constructor(){this.name="";this.path="";this.mode=0}isBlockDevice(){return!1}isCharacterDevice(){return!1}isDirectory(){return(this.mode&61440)===16384}isFIFO(){return!1}isFile(){return(this.mode&61440)===32768}isSocket(){return!1}isSymbolicLink(){return(this.mode&61440)===40960}},oE=class{constructor(){this.uid=0;this.gid=0;this.size=0;this.blksize=0;this.atimeMs=0;this.mtimeMs=0;this.ctimeMs=0;this.birthtimeMs=0;this.atime=new Date(0);this.mtime=new Date(0);this.ctime=new Date(0);this.birthtime=new Date(0);this.dev=0;this.ino=0;this.mode=sU;this.nlink=1;this.rdev=0;this.blocks=1}isBlockDevice(){return!1}isCharacterDevice(){return!1}isDirectory(){return(this.mode&61440)===16384}isFIFO(){return!1}isFile(){return(this.mode&61440)===32768}isSocket(){return!1}isSymbolicLink(){return(this.mode&61440)===40960}},aE=class{constructor(){this.uid=BigInt(0);this.gid=BigInt(0);this.size=BigInt(0);this.blksize=BigInt(0);this.atimeMs=BigInt(0);this.mtimeMs=BigInt(0);this.ctimeMs=BigInt(0);this.birthtimeMs=BigInt(0);this.atimeNs=BigInt(0);this.mtimeNs=BigInt(0);this.ctimeNs=BigInt(0);this.birthtimeNs=BigInt(0);this.atime=new Date(0);this.mtime=new Date(0);this.ctime=new Date(0);this.birthtime=new Date(0);this.dev=BigInt(0);this.ino=BigInt(0);this.mode=BigInt(sU);this.nlink=BigInt(1);this.rdev=BigInt(0);this.blocks=BigInt(1)}isBlockDevice(){return!1}isCharacterDevice(){return!1}isDirectory(){return(this.mode&BigInt(61440))===BigInt(16384)}isFIFO(){return!1}isFile(){return(this.mode&BigInt(61440))===BigInt(32768)}isSocket(){return!1}isSymbolicLink(){return(this.mode&BigInt(61440))===BigInt(40960)}}});function Mje(e){let t,r;if(t=e.match(Oje))e=t[1];else if(r=e.match(Lje))e=`\\\\${r[1]?".\\":""}${r[2]}`;else return e;return e.replace(/\//g,"\\")}function Uje(e){e=e.replace(/\\/g,"/");let t,r;return(t=e.match(Fje))?e=`/${t[1]}`:(r=e.match(Nje))&&(e=`/unc/${r[1]?".dot/":""}${r[2]}`),e}function ZP(e,t){return e===fe?dZ(t):lU(t)}var P2,vt,Er,fe,J,hZ,Fje,Nje,Oje,Lje,lU,dZ,ll=Xe(()=>{P2=et(Ie("path")),vt={root:"/",dot:".",parent:".."},Er={home:"~",nodeModules:"node_modules",manifest:"package.json",lockfile:"yarn.lock",virtual:"__virtual__",pnpJs:".pnp.js",pnpCjs:".pnp.cjs",pnpData:".pnp.data.json",pnpEsmLoader:".pnp.loader.mjs",rc:".yarnrc.yml",env:".env"},fe=Object.create(P2.default),J=Object.create(P2.default.posix);fe.cwd=()=>process.cwd();J.cwd=process.platform==="win32"?()=>lU(process.cwd()):process.cwd;process.platform==="win32"&&(J.resolve=(...e)=>e.length>0&&J.isAbsolute(e[0])?P2.default.posix.resolve(...e):P2.default.posix.resolve(J.cwd(),...e));hZ=function(e,t,r){return t=e.normalize(t),r=e.normalize(r),t===r?".":(t.endsWith(e.sep)||(t=t+e.sep),r.startsWith(t)?r.slice(t.length):null)};fe.contains=(e,t)=>hZ(fe,e,t);J.contains=(e,t)=>hZ(J,e,t);Fje=/^([a-zA-Z]:.*)$/,Nje=/^\/\/(\.\/)?(.*)$/,Oje=/^\/([a-zA-Z]:.*)$/,Lje=/^\/unc\/(\.dot\/)?(.*)$/;lU=process.platform==="win32"?Uje:e=>e,dZ=process.platform==="win32"?Mje:e=>e;fe.fromPortablePath=dZ;fe.toPortablePath=lU});async function $P(e,t){let r="0123456789abcdef";await e.mkdirPromise(t.indexPath,{recursive:!0});let s=[];for(let a of r)for(let n of r)s.push(e.mkdirPromise(e.pathUtils.join(t.indexPath,`${a}${n}`),{recursive:!0}));return await Promise.all(s),t.indexPath}async function gZ(e,t,r,s,a){let n=e.pathUtils.normalize(t),c=r.pathUtils.normalize(s),f=[],p=[],{atime:h,mtime:E}=a.stableTime?{atime:hg,mtime:hg}:await r.lstatPromise(c);await e.mkdirpPromise(e.pathUtils.dirname(t),{utimes:[h,E]}),await cU(f,p,e,n,r,c,{...a,didParentExist:!0});for(let C of f)await C();await Promise.all(p.map(C=>C()))}async function cU(e,t,r,s,a,n,c){let f=c.didParentExist?await mZ(r,s):null,p=await a.lstatPromise(n),{atime:h,mtime:E}=c.stableTime?{atime:hg,mtime:hg}:p,C;switch(!0){case p.isDirectory():C=await Hje(e,t,r,s,f,a,n,p,c);break;case p.isFile():C=await qje(e,t,r,s,f,a,n,p,c);break;case p.isSymbolicLink():C=await Wje(e,t,r,s,f,a,n,p,c);break;default:throw new Error(`Unsupported file type (${p.mode})`)}return(c.linkStrategy?.type!=="HardlinkFromIndex"||!p.isFile())&&((C||f?.mtime?.getTime()!==E.getTime()||f?.atime?.getTime()!==h.getTime())&&(t.push(()=>r.lutimesPromise(s,h,E)),C=!0),(f===null||(f.mode&511)!==(p.mode&511))&&(t.push(()=>r.chmodPromise(s,p.mode&511)),C=!0)),C}async function mZ(e,t){try{return await e.lstatPromise(t)}catch{return null}}async function Hje(e,t,r,s,a,n,c,f,p){if(a!==null&&!a.isDirectory())if(p.overwrite)e.push(async()=>r.removePromise(s)),a=null;else return!1;let h=!1;a===null&&(e.push(async()=>{try{await r.mkdirPromise(s,{mode:f.mode})}catch(S){if(S.code!=="EEXIST")throw S}}),h=!0);let E=await n.readdirPromise(c),C=p.didParentExist&&!a?{...p,didParentExist:!1}:p;if(p.stableSort)for(let S of E.sort())await cU(e,t,r,r.pathUtils.join(s,S),n,n.pathUtils.join(c,S),C)&&(h=!0);else(await Promise.all(E.map(async x=>{await cU(e,t,r,r.pathUtils.join(s,x),n,n.pathUtils.join(c,x),C)}))).some(x=>x)&&(h=!0);return h}async function jje(e,t,r,s,a,n,c,f,p,h){let E=await n.checksumFilePromise(c,{algorithm:"sha1"}),C=420,S=f.mode&511,x=`${E}${S!==C?S.toString(8):""}`,I=r.pathUtils.join(h.indexPath,E.slice(0,2),`${x}.dat`),T;(ae=>(ae[ae.Lock=0]="Lock",ae[ae.Rename=1]="Rename"))(T||={});let O=1,U=await mZ(r,I);if(a){let ie=U&&a.dev===U.dev&&a.ino===U.ino,ue=U?.mtimeMs!==_je;if(ie&&ue&&h.autoRepair&&(O=0,U=null),!ie)if(p.overwrite)e.push(async()=>r.removePromise(s)),a=null;else return!1}let V=!U&&O===1?`${I}.${Math.floor(Math.random()*4294967296).toString(16).padStart(8,"0")}`:null,te=!1;return e.push(async()=>{if(!U&&(O===0&&await r.lockPromise(I,async()=>{let ie=await n.readFilePromise(c);await r.writeFilePromise(I,ie)}),O===1&&V)){let ie=await n.readFilePromise(c);await r.writeFilePromise(V,ie);try{await r.linkPromise(V,I)}catch(ue){if(ue.code==="EEXIST")te=!0,await r.unlinkPromise(V);else throw ue}}a||await r.linkPromise(I,s)}),t.push(async()=>{U||(await r.lutimesPromise(I,hg,hg),S!==C&&await r.chmodPromise(I,S)),V&&!te&&await r.unlinkPromise(V)}),!1}async function Gje(e,t,r,s,a,n,c,f,p){if(a!==null)if(p.overwrite)e.push(async()=>r.removePromise(s)),a=null;else return!1;return e.push(async()=>{let h=await n.readFilePromise(c);await r.writeFilePromise(s,h)}),!0}async function qje(e,t,r,s,a,n,c,f,p){return p.linkStrategy?.type==="HardlinkFromIndex"?jje(e,t,r,s,a,n,c,f,p,p.linkStrategy):Gje(e,t,r,s,a,n,c,f,p)}async function Wje(e,t,r,s,a,n,c,f,p){if(a!==null)if(p.overwrite)e.push(async()=>r.removePromise(s)),a=null;else return!1;return e.push(async()=>{await r.symlinkPromise(ZP(r.pathUtils,await n.readlinkPromise(c)),s)}),!0}var hg,_je,uU=Xe(()=>{ll();hg=new Date(456789e3*1e3),_je=hg.getTime()});function ex(e,t,r,s){let a=()=>{let n=r.shift();if(typeof n>"u")return null;let c=e.pathUtils.join(t,n);return Object.assign(e.statSync(c),{name:n,path:void 0})};return new x2(t,a,s)}var x2,yZ=Xe(()=>{zP();x2=class{constructor(t,r,s={}){this.path=t;this.nextDirent=r;this.opts=s;this.closed=!1}throwIfClosed(){if(this.closed)throw rU()}async*[Symbol.asyncIterator](){try{let t;for(;(t=await this.read())!==null;)yield t}finally{await this.close()}}read(t){let r=this.readSync();return typeof t<"u"?t(null,r):Promise.resolve(r)}readSync(){return this.throwIfClosed(),this.nextDirent()}close(t){return this.closeSync(),typeof t<"u"?t(null):Promise.resolve()}closeSync(){this.throwIfClosed(),this.opts.onClose?.(),this.closed=!0}}});function EZ(e,t){if(e!==t)throw new Error(`Invalid StatWatcher status: expected '${t}', got '${e}'`)}var IZ,tx,CZ=Xe(()=>{IZ=Ie("events");aU();tx=class e extends IZ.EventEmitter{constructor(r,s,{bigint:a=!1}={}){super();this.status="ready";this.changeListeners=new Map;this.startTimeout=null;this.fakeFs=r,this.path=s,this.bigint=a,this.lastStats=this.stat()}static create(r,s,a){let n=new e(r,s,a);return n.start(),n}start(){EZ(this.status,"ready"),this.status="running",this.startTimeout=setTimeout(()=>{this.startTimeout=null,this.fakeFs.existsSync(this.path)||this.emit("change",this.lastStats,this.lastStats)},3)}stop(){EZ(this.status,"running"),this.status="stopped",this.startTimeout!==null&&(clearTimeout(this.startTimeout),this.startTimeout=null),this.emit("stop")}stat(){try{return this.fakeFs.statSync(this.path,{bigint:this.bigint})}catch{let r=this.bigint?new aE:new oE;return XP(r)}}makeInterval(r){let s=setInterval(()=>{let a=this.stat(),n=this.lastStats;oU(a,n)||(this.lastStats=a,this.emit("change",a,n))},r.interval);return r.persistent?s:s.unref()}registerChangeListener(r,s){this.addListener("change",r),this.changeListeners.set(r,this.makeInterval(s))}unregisterChangeListener(r){this.removeListener("change",r);let s=this.changeListeners.get(r);typeof s<"u"&&clearInterval(s),this.changeListeners.delete(r)}unregisterAllChangeListeners(){for(let r of this.changeListeners.keys())this.unregisterChangeListener(r)}hasChangeListeners(){return this.changeListeners.size>0}ref(){for(let r of this.changeListeners.values())r.ref();return this}unref(){for(let r of this.changeListeners.values())r.unref();return this}}});function lE(e,t,r,s){let a,n,c,f;switch(typeof r){case"function":a=!1,n=!0,c=5007,f=r;break;default:({bigint:a=!1,persistent:n=!0,interval:c=5007}=r),f=s;break}let p=rx.get(e);typeof p>"u"&&rx.set(e,p=new Map);let h=p.get(t);return typeof h>"u"&&(h=tx.create(e,t,{bigint:a}),p.set(t,h)),h.registerChangeListener(f,{persistent:n,interval:c}),h}function dg(e,t,r){let s=rx.get(e);if(typeof s>"u")return;let a=s.get(t);typeof a>"u"||(typeof r>"u"?a.unregisterAllChangeListeners():a.unregisterChangeListener(r),a.hasChangeListeners()||(a.stop(),s.delete(t)))}function gg(e){let t=rx.get(e);if(!(typeof t>"u"))for(let r of t.keys())dg(e,r)}var rx,fU=Xe(()=>{CZ();rx=new WeakMap});function Yje(e){let t=e.match(/\r?\n/g);if(t===null)return BZ.EOL;let r=t.filter(a=>a===`\r `).length,s=t.length-r;return r>s?`\r `:` `}function mg(e,t){return t.replace(/\r?\n/g,Yje(e))}var wZ,BZ,Ep,jf,yg=Xe(()=>{wZ=Ie("crypto"),BZ=Ie("os");uU();ll();Ep=class{constructor(t){this.pathUtils=t}async*genTraversePromise(t,{stableSort:r=!1}={}){let s=[t];for(;s.length>0;){let a=s.shift();if((await this.lstatPromise(a)).isDirectory()){let c=await this.readdirPromise(a);if(r)for(let f of c.sort())s.push(this.pathUtils.join(a,f));else throw new Error("Not supported")}else yield a}}async checksumFilePromise(t,{algorithm:r="sha512"}={}){let s=await this.openPromise(t,"r");try{let n=Buffer.allocUnsafeSlow(65536),c=(0,wZ.createHash)(r),f=0;for(;(f=await this.readPromise(s,n,0,65536))!==0;)c.update(f===65536?n:n.slice(0,f));return c.digest("hex")}finally{await this.closePromise(s)}}async removePromise(t,{recursive:r=!0,maxRetries:s=5}={}){let a;try{a=await this.lstatPromise(t)}catch(n){if(n.code==="ENOENT")return;throw n}if(a.isDirectory()){if(r){let n=await this.readdirPromise(t);await Promise.all(n.map(c=>this.removePromise(this.pathUtils.resolve(t,c))))}for(let n=0;n<=s;n++)try{await this.rmdirPromise(t);break}catch(c){if(c.code!=="EBUSY"&&c.code!=="ENOTEMPTY")throw c;nsetTimeout(f,n*100))}}else await this.unlinkPromise(t)}removeSync(t,{recursive:r=!0}={}){let s;try{s=this.lstatSync(t)}catch(a){if(a.code==="ENOENT")return;throw a}if(s.isDirectory()){if(r)for(let a of this.readdirSync(t))this.removeSync(this.pathUtils.resolve(t,a));this.rmdirSync(t)}else this.unlinkSync(t)}async mkdirpPromise(t,{chmod:r,utimes:s}={}){if(t=this.resolve(t),t===this.pathUtils.dirname(t))return;let a=t.split(this.pathUtils.sep),n;for(let c=2;c<=a.length;++c){let f=a.slice(0,c).join(this.pathUtils.sep);if(!this.existsSync(f)){try{await this.mkdirPromise(f)}catch(p){if(p.code==="EEXIST")continue;throw p}if(n??=f,r!=null&&await this.chmodPromise(f,r),s!=null)await this.utimesPromise(f,s[0],s[1]);else{let p=await this.statPromise(this.pathUtils.dirname(f));await this.utimesPromise(f,p.atime,p.mtime)}}}return n}mkdirpSync(t,{chmod:r,utimes:s}={}){if(t=this.resolve(t),t===this.pathUtils.dirname(t))return;let a=t.split(this.pathUtils.sep),n;for(let c=2;c<=a.length;++c){let f=a.slice(0,c).join(this.pathUtils.sep);if(!this.existsSync(f)){try{this.mkdirSync(f)}catch(p){if(p.code==="EEXIST")continue;throw p}if(n??=f,r!=null&&this.chmodSync(f,r),s!=null)this.utimesSync(f,s[0],s[1]);else{let p=this.statSync(this.pathUtils.dirname(f));this.utimesSync(f,p.atime,p.mtime)}}}return n}async copyPromise(t,r,{baseFs:s=this,overwrite:a=!0,stableSort:n=!1,stableTime:c=!1,linkStrategy:f=null}={}){return await gZ(this,t,s,r,{overwrite:a,stableSort:n,stableTime:c,linkStrategy:f})}copySync(t,r,{baseFs:s=this,overwrite:a=!0}={}){let n=s.lstatSync(r),c=this.existsSync(t);if(n.isDirectory()){this.mkdirpSync(t);let p=s.readdirSync(r);for(let h of p)this.copySync(this.pathUtils.join(t,h),s.pathUtils.join(r,h),{baseFs:s,overwrite:a})}else if(n.isFile()){if(!c||a){c&&this.removeSync(t);let p=s.readFileSync(r);this.writeFileSync(t,p)}}else if(n.isSymbolicLink()){if(!c||a){c&&this.removeSync(t);let p=s.readlinkSync(r);this.symlinkSync(ZP(this.pathUtils,p),t)}}else throw new Error(`Unsupported file type (file: ${r}, mode: 0o${n.mode.toString(8).padStart(6,"0")})`);let f=n.mode&511;this.chmodSync(t,f)}async changeFilePromise(t,r,s={}){return Buffer.isBuffer(r)?this.changeFileBufferPromise(t,r,s):this.changeFileTextPromise(t,r,s)}async changeFileBufferPromise(t,r,{mode:s}={}){let a=Buffer.alloc(0);try{a=await this.readFilePromise(t)}catch{}Buffer.compare(a,r)!==0&&await this.writeFilePromise(t,r,{mode:s})}async changeFileTextPromise(t,r,{automaticNewlines:s,mode:a}={}){let n="";try{n=await this.readFilePromise(t,"utf8")}catch{}let c=s?mg(n,r):r;n!==c&&await this.writeFilePromise(t,c,{mode:a})}changeFileSync(t,r,s={}){return Buffer.isBuffer(r)?this.changeFileBufferSync(t,r,s):this.changeFileTextSync(t,r,s)}changeFileBufferSync(t,r,{mode:s}={}){let a=Buffer.alloc(0);try{a=this.readFileSync(t)}catch{}Buffer.compare(a,r)!==0&&this.writeFileSync(t,r,{mode:s})}changeFileTextSync(t,r,{automaticNewlines:s=!1,mode:a}={}){let n="";try{n=this.readFileSync(t,"utf8")}catch{}let c=s?mg(n,r):r;n!==c&&this.writeFileSync(t,c,{mode:a})}async movePromise(t,r){try{await this.renamePromise(t,r)}catch(s){if(s.code==="EXDEV")await this.copyPromise(r,t),await this.removePromise(t);else throw s}}moveSync(t,r){try{this.renameSync(t,r)}catch(s){if(s.code==="EXDEV")this.copySync(r,t),this.removeSync(t);else throw s}}async lockPromise(t,r){let s=`${t}.flock`,a=1e3/60,n=Date.now(),c=null,f=async()=>{let p;try{[p]=await this.readJsonPromise(s)}catch{return Date.now()-n<500}try{return process.kill(p,0),!0}catch{return!1}};for(;c===null;)try{c=await this.openPromise(s,"wx")}catch(p){if(p.code==="EEXIST"){if(!await f())try{await this.unlinkPromise(s);continue}catch{}if(Date.now()-n<60*1e3)await new Promise(h=>setTimeout(h,a));else throw new Error(`Couldn't acquire a lock in a reasonable time (via ${s})`)}else throw p}await this.writePromise(c,JSON.stringify([process.pid]));try{return await r()}finally{try{await this.closePromise(c),await this.unlinkPromise(s)}catch{}}}async readJsonPromise(t){let r=await this.readFilePromise(t,"utf8");try{return JSON.parse(r)}catch(s){throw s.message+=` (in ${t})`,s}}readJsonSync(t){let r=this.readFileSync(t,"utf8");try{return JSON.parse(r)}catch(s){throw s.message+=` (in ${t})`,s}}async writeJsonPromise(t,r,{compact:s=!1}={}){let a=s?0:2;return await this.writeFilePromise(t,`${JSON.stringify(r,null,a)} `)}writeJsonSync(t,r,{compact:s=!1}={}){let a=s?0:2;return this.writeFileSync(t,`${JSON.stringify(r,null,a)} `)}async preserveTimePromise(t,r){let s=await this.lstatPromise(t),a=await r();typeof a<"u"&&(t=a),await this.lutimesPromise(t,s.atime,s.mtime)}async preserveTimeSync(t,r){let s=this.lstatSync(t),a=r();typeof a<"u"&&(t=a),this.lutimesSync(t,s.atime,s.mtime)}},jf=class extends Ep{constructor(){super(J)}}});var Gs,Ip=Xe(()=>{yg();Gs=class extends Ep{getExtractHint(t){return this.baseFs.getExtractHint(t)}resolve(t){return this.mapFromBase(this.baseFs.resolve(this.mapToBase(t)))}getRealPath(){return this.mapFromBase(this.baseFs.getRealPath())}async openPromise(t,r,s){return this.baseFs.openPromise(this.mapToBase(t),r,s)}openSync(t,r,s){return this.baseFs.openSync(this.mapToBase(t),r,s)}async opendirPromise(t,r){return Object.assign(await this.baseFs.opendirPromise(this.mapToBase(t),r),{path:t})}opendirSync(t,r){return Object.assign(this.baseFs.opendirSync(this.mapToBase(t),r),{path:t})}async readPromise(t,r,s,a,n){return await this.baseFs.readPromise(t,r,s,a,n)}readSync(t,r,s,a,n){return this.baseFs.readSync(t,r,s,a,n)}async writePromise(t,r,s,a,n){return typeof r=="string"?await this.baseFs.writePromise(t,r,s):await this.baseFs.writePromise(t,r,s,a,n)}writeSync(t,r,s,a,n){return typeof r=="string"?this.baseFs.writeSync(t,r,s):this.baseFs.writeSync(t,r,s,a,n)}async closePromise(t){return this.baseFs.closePromise(t)}closeSync(t){this.baseFs.closeSync(t)}createReadStream(t,r){return this.baseFs.createReadStream(t!==null?this.mapToBase(t):t,r)}createWriteStream(t,r){return this.baseFs.createWriteStream(t!==null?this.mapToBase(t):t,r)}async realpathPromise(t){return this.mapFromBase(await this.baseFs.realpathPromise(this.mapToBase(t)))}realpathSync(t){return this.mapFromBase(this.baseFs.realpathSync(this.mapToBase(t)))}async existsPromise(t){return this.baseFs.existsPromise(this.mapToBase(t))}existsSync(t){return this.baseFs.existsSync(this.mapToBase(t))}accessSync(t,r){return this.baseFs.accessSync(this.mapToBase(t),r)}async accessPromise(t,r){return this.baseFs.accessPromise(this.mapToBase(t),r)}async statPromise(t,r){return this.baseFs.statPromise(this.mapToBase(t),r)}statSync(t,r){return this.baseFs.statSync(this.mapToBase(t),r)}async fstatPromise(t,r){return this.baseFs.fstatPromise(t,r)}fstatSync(t,r){return this.baseFs.fstatSync(t,r)}lstatPromise(t,r){return this.baseFs.lstatPromise(this.mapToBase(t),r)}lstatSync(t,r){return this.baseFs.lstatSync(this.mapToBase(t),r)}async fchmodPromise(t,r){return this.baseFs.fchmodPromise(t,r)}fchmodSync(t,r){return this.baseFs.fchmodSync(t,r)}async chmodPromise(t,r){return this.baseFs.chmodPromise(this.mapToBase(t),r)}chmodSync(t,r){return this.baseFs.chmodSync(this.mapToBase(t),r)}async fchownPromise(t,r,s){return this.baseFs.fchownPromise(t,r,s)}fchownSync(t,r,s){return this.baseFs.fchownSync(t,r,s)}async chownPromise(t,r,s){return this.baseFs.chownPromise(this.mapToBase(t),r,s)}chownSync(t,r,s){return this.baseFs.chownSync(this.mapToBase(t),r,s)}async renamePromise(t,r){return this.baseFs.renamePromise(this.mapToBase(t),this.mapToBase(r))}renameSync(t,r){return this.baseFs.renameSync(this.mapToBase(t),this.mapToBase(r))}async copyFilePromise(t,r,s=0){return this.baseFs.copyFilePromise(this.mapToBase(t),this.mapToBase(r),s)}copyFileSync(t,r,s=0){return this.baseFs.copyFileSync(this.mapToBase(t),this.mapToBase(r),s)}async appendFilePromise(t,r,s){return this.baseFs.appendFilePromise(this.fsMapToBase(t),r,s)}appendFileSync(t,r,s){return this.baseFs.appendFileSync(this.fsMapToBase(t),r,s)}async writeFilePromise(t,r,s){return this.baseFs.writeFilePromise(this.fsMapToBase(t),r,s)}writeFileSync(t,r,s){return this.baseFs.writeFileSync(this.fsMapToBase(t),r,s)}async unlinkPromise(t){return this.baseFs.unlinkPromise(this.mapToBase(t))}unlinkSync(t){return this.baseFs.unlinkSync(this.mapToBase(t))}async utimesPromise(t,r,s){return this.baseFs.utimesPromise(this.mapToBase(t),r,s)}utimesSync(t,r,s){return this.baseFs.utimesSync(this.mapToBase(t),r,s)}async lutimesPromise(t,r,s){return this.baseFs.lutimesPromise(this.mapToBase(t),r,s)}lutimesSync(t,r,s){return this.baseFs.lutimesSync(this.mapToBase(t),r,s)}async mkdirPromise(t,r){return this.baseFs.mkdirPromise(this.mapToBase(t),r)}mkdirSync(t,r){return this.baseFs.mkdirSync(this.mapToBase(t),r)}async rmdirPromise(t,r){return this.baseFs.rmdirPromise(this.mapToBase(t),r)}rmdirSync(t,r){return this.baseFs.rmdirSync(this.mapToBase(t),r)}async rmPromise(t,r){return this.baseFs.rmPromise(this.mapToBase(t),r)}rmSync(t,r){return this.baseFs.rmSync(this.mapToBase(t),r)}async linkPromise(t,r){return this.baseFs.linkPromise(this.mapToBase(t),this.mapToBase(r))}linkSync(t,r){return this.baseFs.linkSync(this.mapToBase(t),this.mapToBase(r))}async symlinkPromise(t,r,s){let a=this.mapToBase(r);if(this.pathUtils.isAbsolute(t))return this.baseFs.symlinkPromise(this.mapToBase(t),a,s);let n=this.mapToBase(this.pathUtils.join(this.pathUtils.dirname(r),t)),c=this.baseFs.pathUtils.relative(this.baseFs.pathUtils.dirname(a),n);return this.baseFs.symlinkPromise(c,a,s)}symlinkSync(t,r,s){let a=this.mapToBase(r);if(this.pathUtils.isAbsolute(t))return this.baseFs.symlinkSync(this.mapToBase(t),a,s);let n=this.mapToBase(this.pathUtils.join(this.pathUtils.dirname(r),t)),c=this.baseFs.pathUtils.relative(this.baseFs.pathUtils.dirname(a),n);return this.baseFs.symlinkSync(c,a,s)}async readFilePromise(t,r){return this.baseFs.readFilePromise(this.fsMapToBase(t),r)}readFileSync(t,r){return this.baseFs.readFileSync(this.fsMapToBase(t),r)}readdirPromise(t,r){return this.baseFs.readdirPromise(this.mapToBase(t),r)}readdirSync(t,r){return this.baseFs.readdirSync(this.mapToBase(t),r)}async readlinkPromise(t){return this.mapFromBase(await this.baseFs.readlinkPromise(this.mapToBase(t)))}readlinkSync(t){return this.mapFromBase(this.baseFs.readlinkSync(this.mapToBase(t)))}async truncatePromise(t,r){return this.baseFs.truncatePromise(this.mapToBase(t),r)}truncateSync(t,r){return this.baseFs.truncateSync(this.mapToBase(t),r)}async ftruncatePromise(t,r){return this.baseFs.ftruncatePromise(t,r)}ftruncateSync(t,r){return this.baseFs.ftruncateSync(t,r)}watch(t,r,s){return this.baseFs.watch(this.mapToBase(t),r,s)}watchFile(t,r,s){return this.baseFs.watchFile(this.mapToBase(t),r,s)}unwatchFile(t,r){return this.baseFs.unwatchFile(this.mapToBase(t),r)}fsMapToBase(t){return typeof t=="number"?t:this.mapToBase(t)}}});var Gf,vZ=Xe(()=>{Ip();Gf=class extends Gs{constructor(t,{baseFs:r,pathUtils:s}){super(s),this.target=t,this.baseFs=r}getRealPath(){return this.target}getBaseFs(){return this.baseFs}mapFromBase(t){return t}mapToBase(t){return t}}});function SZ(e){let t=e;return typeof e.path=="string"&&(t.path=fe.toPortablePath(e.path)),t}var DZ,Vn,Eg=Xe(()=>{DZ=et(Ie("fs"));yg();ll();Vn=class extends jf{constructor(t=DZ.default){super(),this.realFs=t}getExtractHint(){return!1}getRealPath(){return vt.root}resolve(t){return J.resolve(t)}async openPromise(t,r,s){return await new Promise((a,n)=>{this.realFs.open(fe.fromPortablePath(t),r,s,this.makeCallback(a,n))})}openSync(t,r,s){return this.realFs.openSync(fe.fromPortablePath(t),r,s)}async opendirPromise(t,r){return await new Promise((s,a)=>{typeof r<"u"?this.realFs.opendir(fe.fromPortablePath(t),r,this.makeCallback(s,a)):this.realFs.opendir(fe.fromPortablePath(t),this.makeCallback(s,a))}).then(s=>{let a=s;return Object.defineProperty(a,"path",{value:t,configurable:!0,writable:!0}),a})}opendirSync(t,r){let a=typeof r<"u"?this.realFs.opendirSync(fe.fromPortablePath(t),r):this.realFs.opendirSync(fe.fromPortablePath(t));return Object.defineProperty(a,"path",{value:t,configurable:!0,writable:!0}),a}async readPromise(t,r,s=0,a=0,n=-1){return await new Promise((c,f)=>{this.realFs.read(t,r,s,a,n,(p,h)=>{p?f(p):c(h)})})}readSync(t,r,s,a,n){return this.realFs.readSync(t,r,s,a,n)}async writePromise(t,r,s,a,n){return await new Promise((c,f)=>typeof r=="string"?this.realFs.write(t,r,s,this.makeCallback(c,f)):this.realFs.write(t,r,s,a,n,this.makeCallback(c,f)))}writeSync(t,r,s,a,n){return typeof r=="string"?this.realFs.writeSync(t,r,s):this.realFs.writeSync(t,r,s,a,n)}async closePromise(t){await new Promise((r,s)=>{this.realFs.close(t,this.makeCallback(r,s))})}closeSync(t){this.realFs.closeSync(t)}createReadStream(t,r){let s=t!==null?fe.fromPortablePath(t):t;return this.realFs.createReadStream(s,r)}createWriteStream(t,r){let s=t!==null?fe.fromPortablePath(t):t;return this.realFs.createWriteStream(s,r)}async realpathPromise(t){return await new Promise((r,s)=>{this.realFs.realpath(fe.fromPortablePath(t),{},this.makeCallback(r,s))}).then(r=>fe.toPortablePath(r))}realpathSync(t){return fe.toPortablePath(this.realFs.realpathSync(fe.fromPortablePath(t),{}))}async existsPromise(t){return await new Promise(r=>{this.realFs.exists(fe.fromPortablePath(t),r)})}accessSync(t,r){return this.realFs.accessSync(fe.fromPortablePath(t),r)}async accessPromise(t,r){return await new Promise((s,a)=>{this.realFs.access(fe.fromPortablePath(t),r,this.makeCallback(s,a))})}existsSync(t){return this.realFs.existsSync(fe.fromPortablePath(t))}async statPromise(t,r){return await new Promise((s,a)=>{r?this.realFs.stat(fe.fromPortablePath(t),r,this.makeCallback(s,a)):this.realFs.stat(fe.fromPortablePath(t),this.makeCallback(s,a))})}statSync(t,r){return r?this.realFs.statSync(fe.fromPortablePath(t),r):this.realFs.statSync(fe.fromPortablePath(t))}async fstatPromise(t,r){return await new Promise((s,a)=>{r?this.realFs.fstat(t,r,this.makeCallback(s,a)):this.realFs.fstat(t,this.makeCallback(s,a))})}fstatSync(t,r){return r?this.realFs.fstatSync(t,r):this.realFs.fstatSync(t)}async lstatPromise(t,r){return await new Promise((s,a)=>{r?this.realFs.lstat(fe.fromPortablePath(t),r,this.makeCallback(s,a)):this.realFs.lstat(fe.fromPortablePath(t),this.makeCallback(s,a))})}lstatSync(t,r){return r?this.realFs.lstatSync(fe.fromPortablePath(t),r):this.realFs.lstatSync(fe.fromPortablePath(t))}async fchmodPromise(t,r){return await new Promise((s,a)=>{this.realFs.fchmod(t,r,this.makeCallback(s,a))})}fchmodSync(t,r){return this.realFs.fchmodSync(t,r)}async chmodPromise(t,r){return await new Promise((s,a)=>{this.realFs.chmod(fe.fromPortablePath(t),r,this.makeCallback(s,a))})}chmodSync(t,r){return this.realFs.chmodSync(fe.fromPortablePath(t),r)}async fchownPromise(t,r,s){return await new Promise((a,n)=>{this.realFs.fchown(t,r,s,this.makeCallback(a,n))})}fchownSync(t,r,s){return this.realFs.fchownSync(t,r,s)}async chownPromise(t,r,s){return await new Promise((a,n)=>{this.realFs.chown(fe.fromPortablePath(t),r,s,this.makeCallback(a,n))})}chownSync(t,r,s){return this.realFs.chownSync(fe.fromPortablePath(t),r,s)}async renamePromise(t,r){return await new Promise((s,a)=>{this.realFs.rename(fe.fromPortablePath(t),fe.fromPortablePath(r),this.makeCallback(s,a))})}renameSync(t,r){return this.realFs.renameSync(fe.fromPortablePath(t),fe.fromPortablePath(r))}async copyFilePromise(t,r,s=0){return await new Promise((a,n)=>{this.realFs.copyFile(fe.fromPortablePath(t),fe.fromPortablePath(r),s,this.makeCallback(a,n))})}copyFileSync(t,r,s=0){return this.realFs.copyFileSync(fe.fromPortablePath(t),fe.fromPortablePath(r),s)}async appendFilePromise(t,r,s){return await new Promise((a,n)=>{let c=typeof t=="string"?fe.fromPortablePath(t):t;s?this.realFs.appendFile(c,r,s,this.makeCallback(a,n)):this.realFs.appendFile(c,r,this.makeCallback(a,n))})}appendFileSync(t,r,s){let a=typeof t=="string"?fe.fromPortablePath(t):t;s?this.realFs.appendFileSync(a,r,s):this.realFs.appendFileSync(a,r)}async writeFilePromise(t,r,s){return await new Promise((a,n)=>{let c=typeof t=="string"?fe.fromPortablePath(t):t;s?this.realFs.writeFile(c,r,s,this.makeCallback(a,n)):this.realFs.writeFile(c,r,this.makeCallback(a,n))})}writeFileSync(t,r,s){let a=typeof t=="string"?fe.fromPortablePath(t):t;s?this.realFs.writeFileSync(a,r,s):this.realFs.writeFileSync(a,r)}async unlinkPromise(t){return await new Promise((r,s)=>{this.realFs.unlink(fe.fromPortablePath(t),this.makeCallback(r,s))})}unlinkSync(t){return this.realFs.unlinkSync(fe.fromPortablePath(t))}async utimesPromise(t,r,s){return await new Promise((a,n)=>{this.realFs.utimes(fe.fromPortablePath(t),r,s,this.makeCallback(a,n))})}utimesSync(t,r,s){this.realFs.utimesSync(fe.fromPortablePath(t),r,s)}async lutimesPromise(t,r,s){return await new Promise((a,n)=>{this.realFs.lutimes(fe.fromPortablePath(t),r,s,this.makeCallback(a,n))})}lutimesSync(t,r,s){this.realFs.lutimesSync(fe.fromPortablePath(t),r,s)}async mkdirPromise(t,r){return await new Promise((s,a)=>{this.realFs.mkdir(fe.fromPortablePath(t),r,this.makeCallback(s,a))})}mkdirSync(t,r){return this.realFs.mkdirSync(fe.fromPortablePath(t),r)}async rmdirPromise(t,r){return await new Promise((s,a)=>{r?this.realFs.rmdir(fe.fromPortablePath(t),r,this.makeCallback(s,a)):this.realFs.rmdir(fe.fromPortablePath(t),this.makeCallback(s,a))})}rmdirSync(t,r){return this.realFs.rmdirSync(fe.fromPortablePath(t),r)}async rmPromise(t,r){return await new Promise((s,a)=>{r?this.realFs.rm(fe.fromPortablePath(t),r,this.makeCallback(s,a)):this.realFs.rm(fe.fromPortablePath(t),this.makeCallback(s,a))})}rmSync(t,r){return this.realFs.rmSync(fe.fromPortablePath(t),r)}async linkPromise(t,r){return await new Promise((s,a)=>{this.realFs.link(fe.fromPortablePath(t),fe.fromPortablePath(r),this.makeCallback(s,a))})}linkSync(t,r){return this.realFs.linkSync(fe.fromPortablePath(t),fe.fromPortablePath(r))}async symlinkPromise(t,r,s){return await new Promise((a,n)=>{this.realFs.symlink(fe.fromPortablePath(t.replace(/\/+$/,"")),fe.fromPortablePath(r),s,this.makeCallback(a,n))})}symlinkSync(t,r,s){return this.realFs.symlinkSync(fe.fromPortablePath(t.replace(/\/+$/,"")),fe.fromPortablePath(r),s)}async readFilePromise(t,r){return await new Promise((s,a)=>{let n=typeof t=="string"?fe.fromPortablePath(t):t;this.realFs.readFile(n,r,this.makeCallback(s,a))})}readFileSync(t,r){let s=typeof t=="string"?fe.fromPortablePath(t):t;return this.realFs.readFileSync(s,r)}async readdirPromise(t,r){return await new Promise((s,a)=>{r?r.recursive&&process.platform==="win32"?r.withFileTypes?this.realFs.readdir(fe.fromPortablePath(t),r,this.makeCallback(n=>s(n.map(SZ)),a)):this.realFs.readdir(fe.fromPortablePath(t),r,this.makeCallback(n=>s(n.map(fe.toPortablePath)),a)):this.realFs.readdir(fe.fromPortablePath(t),r,this.makeCallback(s,a)):this.realFs.readdir(fe.fromPortablePath(t),this.makeCallback(s,a))})}readdirSync(t,r){return r?r.recursive&&process.platform==="win32"?r.withFileTypes?this.realFs.readdirSync(fe.fromPortablePath(t),r).map(SZ):this.realFs.readdirSync(fe.fromPortablePath(t),r).map(fe.toPortablePath):this.realFs.readdirSync(fe.fromPortablePath(t),r):this.realFs.readdirSync(fe.fromPortablePath(t))}async readlinkPromise(t){return await new Promise((r,s)=>{this.realFs.readlink(fe.fromPortablePath(t),this.makeCallback(r,s))}).then(r=>fe.toPortablePath(r))}readlinkSync(t){return fe.toPortablePath(this.realFs.readlinkSync(fe.fromPortablePath(t)))}async truncatePromise(t,r){return await new Promise((s,a)=>{this.realFs.truncate(fe.fromPortablePath(t),r,this.makeCallback(s,a))})}truncateSync(t,r){return this.realFs.truncateSync(fe.fromPortablePath(t),r)}async ftruncatePromise(t,r){return await new Promise((s,a)=>{this.realFs.ftruncate(t,r,this.makeCallback(s,a))})}ftruncateSync(t,r){return this.realFs.ftruncateSync(t,r)}watch(t,r,s){return this.realFs.watch(fe.fromPortablePath(t),r,s)}watchFile(t,r,s){return this.realFs.watchFile(fe.fromPortablePath(t),r,s)}unwatchFile(t,r){return this.realFs.unwatchFile(fe.fromPortablePath(t),r)}makeCallback(t,r){return(s,a)=>{s?r(s):t(a)}}}});var bn,bZ=Xe(()=>{Eg();Ip();ll();bn=class extends Gs{constructor(t,{baseFs:r=new Vn}={}){super(J),this.target=this.pathUtils.normalize(t),this.baseFs=r}getRealPath(){return this.pathUtils.resolve(this.baseFs.getRealPath(),this.target)}resolve(t){return this.pathUtils.isAbsolute(t)?J.normalize(t):this.baseFs.resolve(J.join(this.target,t))}mapFromBase(t){return t}mapToBase(t){return this.pathUtils.isAbsolute(t)?t:this.pathUtils.join(this.target,t)}}});var PZ,qf,xZ=Xe(()=>{Eg();Ip();ll();PZ=vt.root,qf=class extends Gs{constructor(t,{baseFs:r=new Vn}={}){super(J),this.target=this.pathUtils.resolve(vt.root,t),this.baseFs=r}getRealPath(){return this.pathUtils.resolve(this.baseFs.getRealPath(),this.pathUtils.relative(vt.root,this.target))}getTarget(){return this.target}getBaseFs(){return this.baseFs}mapToBase(t){let r=this.pathUtils.normalize(t);if(this.pathUtils.isAbsolute(t))return this.pathUtils.resolve(this.target,this.pathUtils.relative(PZ,t));if(r.match(/^\.\.\/?/))throw new Error(`Resolving this path (${t}) would escape the jail`);return this.pathUtils.resolve(this.target,t)}mapFromBase(t){return this.pathUtils.resolve(PZ,this.pathUtils.relative(this.target,t))}}});var cE,kZ=Xe(()=>{Ip();cE=class extends Gs{constructor(r,s){super(s);this.instance=null;this.factory=r}get baseFs(){return this.instance||(this.instance=this.factory()),this.instance}set baseFs(r){this.instance=r}mapFromBase(r){return r}mapToBase(r){return r}}});var Ig,cl,$h,QZ=Xe(()=>{Ig=Ie("fs");yg();Eg();fU();zP();ll();cl=4278190080,$h=class extends jf{constructor({baseFs:r=new Vn,filter:s=null,magicByte:a=42,maxOpenFiles:n=1/0,useCache:c=!0,maxAge:f=5e3,typeCheck:p=Ig.constants.S_IFREG,getMountPoint:h,factoryPromise:E,factorySync:C}){if(Math.floor(a)!==a||!(a>1&&a<=127))throw new Error("The magic byte must be set to a round value between 1 and 127 included");super();this.fdMap=new Map;this.nextFd=3;this.isMount=new Set;this.notMount=new Set;this.realPaths=new Map;this.limitOpenFilesTimeout=null;this.baseFs=r,this.mountInstances=c?new Map:null,this.factoryPromise=E,this.factorySync=C,this.filter=s,this.getMountPoint=h,this.magic=a<<24,this.maxAge=f,this.maxOpenFiles=n,this.typeCheck=p}getExtractHint(r){return this.baseFs.getExtractHint(r)}getRealPath(){return this.baseFs.getRealPath()}saveAndClose(){if(gg(this),this.mountInstances)for(let[r,{childFs:s}]of this.mountInstances.entries())s.saveAndClose?.(),this.mountInstances.delete(r)}discardAndClose(){if(gg(this),this.mountInstances)for(let[r,{childFs:s}]of this.mountInstances.entries())s.discardAndClose?.(),this.mountInstances.delete(r)}resolve(r){return this.baseFs.resolve(r)}remapFd(r,s){let a=this.nextFd++|this.magic;return this.fdMap.set(a,[r,s]),a}async openPromise(r,s,a){return await this.makeCallPromise(r,async()=>await this.baseFs.openPromise(r,s,a),async(n,{subPath:c})=>this.remapFd(n,await n.openPromise(c,s,a)))}openSync(r,s,a){return this.makeCallSync(r,()=>this.baseFs.openSync(r,s,a),(n,{subPath:c})=>this.remapFd(n,n.openSync(c,s,a)))}async opendirPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.opendirPromise(r,s),async(a,{subPath:n})=>await a.opendirPromise(n,s),{requireSubpath:!1})}opendirSync(r,s){return this.makeCallSync(r,()=>this.baseFs.opendirSync(r,s),(a,{subPath:n})=>a.opendirSync(n,s),{requireSubpath:!1})}async readPromise(r,s,a,n,c){if((r&cl)!==this.magic)return await this.baseFs.readPromise(r,s,a,n,c);let f=this.fdMap.get(r);if(typeof f>"u")throw qo("read");let[p,h]=f;return await p.readPromise(h,s,a,n,c)}readSync(r,s,a,n,c){if((r&cl)!==this.magic)return this.baseFs.readSync(r,s,a,n,c);let f=this.fdMap.get(r);if(typeof f>"u")throw qo("readSync");let[p,h]=f;return p.readSync(h,s,a,n,c)}async writePromise(r,s,a,n,c){if((r&cl)!==this.magic)return typeof s=="string"?await this.baseFs.writePromise(r,s,a):await this.baseFs.writePromise(r,s,a,n,c);let f=this.fdMap.get(r);if(typeof f>"u")throw qo("write");let[p,h]=f;return typeof s=="string"?await p.writePromise(h,s,a):await p.writePromise(h,s,a,n,c)}writeSync(r,s,a,n,c){if((r&cl)!==this.magic)return typeof s=="string"?this.baseFs.writeSync(r,s,a):this.baseFs.writeSync(r,s,a,n,c);let f=this.fdMap.get(r);if(typeof f>"u")throw qo("writeSync");let[p,h]=f;return typeof s=="string"?p.writeSync(h,s,a):p.writeSync(h,s,a,n,c)}async closePromise(r){if((r&cl)!==this.magic)return await this.baseFs.closePromise(r);let s=this.fdMap.get(r);if(typeof s>"u")throw qo("close");this.fdMap.delete(r);let[a,n]=s;return await a.closePromise(n)}closeSync(r){if((r&cl)!==this.magic)return this.baseFs.closeSync(r);let s=this.fdMap.get(r);if(typeof s>"u")throw qo("closeSync");this.fdMap.delete(r);let[a,n]=s;return a.closeSync(n)}createReadStream(r,s){return r===null?this.baseFs.createReadStream(r,s):this.makeCallSync(r,()=>this.baseFs.createReadStream(r,s),(a,{archivePath:n,subPath:c})=>{let f=a.createReadStream(c,s);return f.path=fe.fromPortablePath(this.pathUtils.join(n,c)),f})}createWriteStream(r,s){return r===null?this.baseFs.createWriteStream(r,s):this.makeCallSync(r,()=>this.baseFs.createWriteStream(r,s),(a,{subPath:n})=>a.createWriteStream(n,s))}async realpathPromise(r){return await this.makeCallPromise(r,async()=>await this.baseFs.realpathPromise(r),async(s,{archivePath:a,subPath:n})=>{let c=this.realPaths.get(a);return typeof c>"u"&&(c=await this.baseFs.realpathPromise(a),this.realPaths.set(a,c)),this.pathUtils.join(c,this.pathUtils.relative(vt.root,await s.realpathPromise(n)))})}realpathSync(r){return this.makeCallSync(r,()=>this.baseFs.realpathSync(r),(s,{archivePath:a,subPath:n})=>{let c=this.realPaths.get(a);return typeof c>"u"&&(c=this.baseFs.realpathSync(a),this.realPaths.set(a,c)),this.pathUtils.join(c,this.pathUtils.relative(vt.root,s.realpathSync(n)))})}async existsPromise(r){return await this.makeCallPromise(r,async()=>await this.baseFs.existsPromise(r),async(s,{subPath:a})=>await s.existsPromise(a))}existsSync(r){return this.makeCallSync(r,()=>this.baseFs.existsSync(r),(s,{subPath:a})=>s.existsSync(a))}async accessPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.accessPromise(r,s),async(a,{subPath:n})=>await a.accessPromise(n,s))}accessSync(r,s){return this.makeCallSync(r,()=>this.baseFs.accessSync(r,s),(a,{subPath:n})=>a.accessSync(n,s))}async statPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.statPromise(r,s),async(a,{subPath:n})=>await a.statPromise(n,s))}statSync(r,s){return this.makeCallSync(r,()=>this.baseFs.statSync(r,s),(a,{subPath:n})=>a.statSync(n,s))}async fstatPromise(r,s){if((r&cl)!==this.magic)return this.baseFs.fstatPromise(r,s);let a=this.fdMap.get(r);if(typeof a>"u")throw qo("fstat");let[n,c]=a;return n.fstatPromise(c,s)}fstatSync(r,s){if((r&cl)!==this.magic)return this.baseFs.fstatSync(r,s);let a=this.fdMap.get(r);if(typeof a>"u")throw qo("fstatSync");let[n,c]=a;return n.fstatSync(c,s)}async lstatPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.lstatPromise(r,s),async(a,{subPath:n})=>await a.lstatPromise(n,s))}lstatSync(r,s){return this.makeCallSync(r,()=>this.baseFs.lstatSync(r,s),(a,{subPath:n})=>a.lstatSync(n,s))}async fchmodPromise(r,s){if((r&cl)!==this.magic)return this.baseFs.fchmodPromise(r,s);let a=this.fdMap.get(r);if(typeof a>"u")throw qo("fchmod");let[n,c]=a;return n.fchmodPromise(c,s)}fchmodSync(r,s){if((r&cl)!==this.magic)return this.baseFs.fchmodSync(r,s);let a=this.fdMap.get(r);if(typeof a>"u")throw qo("fchmodSync");let[n,c]=a;return n.fchmodSync(c,s)}async chmodPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.chmodPromise(r,s),async(a,{subPath:n})=>await a.chmodPromise(n,s))}chmodSync(r,s){return this.makeCallSync(r,()=>this.baseFs.chmodSync(r,s),(a,{subPath:n})=>a.chmodSync(n,s))}async fchownPromise(r,s,a){if((r&cl)!==this.magic)return this.baseFs.fchownPromise(r,s,a);let n=this.fdMap.get(r);if(typeof n>"u")throw qo("fchown");let[c,f]=n;return c.fchownPromise(f,s,a)}fchownSync(r,s,a){if((r&cl)!==this.magic)return this.baseFs.fchownSync(r,s,a);let n=this.fdMap.get(r);if(typeof n>"u")throw qo("fchownSync");let[c,f]=n;return c.fchownSync(f,s,a)}async chownPromise(r,s,a){return await this.makeCallPromise(r,async()=>await this.baseFs.chownPromise(r,s,a),async(n,{subPath:c})=>await n.chownPromise(c,s,a))}chownSync(r,s,a){return this.makeCallSync(r,()=>this.baseFs.chownSync(r,s,a),(n,{subPath:c})=>n.chownSync(c,s,a))}async renamePromise(r,s){return await this.makeCallPromise(r,async()=>await this.makeCallPromise(s,async()=>await this.baseFs.renamePromise(r,s),async()=>{throw Object.assign(new Error("EEXDEV: cross-device link not permitted"),{code:"EEXDEV"})}),async(a,{subPath:n})=>await this.makeCallPromise(s,async()=>{throw Object.assign(new Error("EEXDEV: cross-device link not permitted"),{code:"EEXDEV"})},async(c,{subPath:f})=>{if(a!==c)throw Object.assign(new Error("EEXDEV: cross-device link not permitted"),{code:"EEXDEV"});return await a.renamePromise(n,f)}))}renameSync(r,s){return this.makeCallSync(r,()=>this.makeCallSync(s,()=>this.baseFs.renameSync(r,s),()=>{throw Object.assign(new Error("EEXDEV: cross-device link not permitted"),{code:"EEXDEV"})}),(a,{subPath:n})=>this.makeCallSync(s,()=>{throw Object.assign(new Error("EEXDEV: cross-device link not permitted"),{code:"EEXDEV"})},(c,{subPath:f})=>{if(a!==c)throw Object.assign(new Error("EEXDEV: cross-device link not permitted"),{code:"EEXDEV"});return a.renameSync(n,f)}))}async copyFilePromise(r,s,a=0){let n=async(c,f,p,h)=>{if(a&Ig.constants.COPYFILE_FICLONE_FORCE)throw Object.assign(new Error(`EXDEV: cross-device clone not permitted, copyfile '${f}' -> ${h}'`),{code:"EXDEV"});if(a&Ig.constants.COPYFILE_EXCL&&await this.existsPromise(f))throw Object.assign(new Error(`EEXIST: file already exists, copyfile '${f}' -> '${h}'`),{code:"EEXIST"});let E;try{E=await c.readFilePromise(f)}catch{throw Object.assign(new Error(`EINVAL: invalid argument, copyfile '${f}' -> '${h}'`),{code:"EINVAL"})}await p.writeFilePromise(h,E)};return await this.makeCallPromise(r,async()=>await this.makeCallPromise(s,async()=>await this.baseFs.copyFilePromise(r,s,a),async(c,{subPath:f})=>await n(this.baseFs,r,c,f)),async(c,{subPath:f})=>await this.makeCallPromise(s,async()=>await n(c,f,this.baseFs,s),async(p,{subPath:h})=>c!==p?await n(c,f,p,h):await c.copyFilePromise(f,h,a)))}copyFileSync(r,s,a=0){let n=(c,f,p,h)=>{if(a&Ig.constants.COPYFILE_FICLONE_FORCE)throw Object.assign(new Error(`EXDEV: cross-device clone not permitted, copyfile '${f}' -> ${h}'`),{code:"EXDEV"});if(a&Ig.constants.COPYFILE_EXCL&&this.existsSync(f))throw Object.assign(new Error(`EEXIST: file already exists, copyfile '${f}' -> '${h}'`),{code:"EEXIST"});let E;try{E=c.readFileSync(f)}catch{throw Object.assign(new Error(`EINVAL: invalid argument, copyfile '${f}' -> '${h}'`),{code:"EINVAL"})}p.writeFileSync(h,E)};return this.makeCallSync(r,()=>this.makeCallSync(s,()=>this.baseFs.copyFileSync(r,s,a),(c,{subPath:f})=>n(this.baseFs,r,c,f)),(c,{subPath:f})=>this.makeCallSync(s,()=>n(c,f,this.baseFs,s),(p,{subPath:h})=>c!==p?n(c,f,p,h):c.copyFileSync(f,h,a)))}async appendFilePromise(r,s,a){return await this.makeCallPromise(r,async()=>await this.baseFs.appendFilePromise(r,s,a),async(n,{subPath:c})=>await n.appendFilePromise(c,s,a))}appendFileSync(r,s,a){return this.makeCallSync(r,()=>this.baseFs.appendFileSync(r,s,a),(n,{subPath:c})=>n.appendFileSync(c,s,a))}async writeFilePromise(r,s,a){return await this.makeCallPromise(r,async()=>await this.baseFs.writeFilePromise(r,s,a),async(n,{subPath:c})=>await n.writeFilePromise(c,s,a))}writeFileSync(r,s,a){return this.makeCallSync(r,()=>this.baseFs.writeFileSync(r,s,a),(n,{subPath:c})=>n.writeFileSync(c,s,a))}async unlinkPromise(r){return await this.makeCallPromise(r,async()=>await this.baseFs.unlinkPromise(r),async(s,{subPath:a})=>await s.unlinkPromise(a))}unlinkSync(r){return this.makeCallSync(r,()=>this.baseFs.unlinkSync(r),(s,{subPath:a})=>s.unlinkSync(a))}async utimesPromise(r,s,a){return await this.makeCallPromise(r,async()=>await this.baseFs.utimesPromise(r,s,a),async(n,{subPath:c})=>await n.utimesPromise(c,s,a))}utimesSync(r,s,a){return this.makeCallSync(r,()=>this.baseFs.utimesSync(r,s,a),(n,{subPath:c})=>n.utimesSync(c,s,a))}async lutimesPromise(r,s,a){return await this.makeCallPromise(r,async()=>await this.baseFs.lutimesPromise(r,s,a),async(n,{subPath:c})=>await n.lutimesPromise(c,s,a))}lutimesSync(r,s,a){return this.makeCallSync(r,()=>this.baseFs.lutimesSync(r,s,a),(n,{subPath:c})=>n.lutimesSync(c,s,a))}async mkdirPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.mkdirPromise(r,s),async(a,{subPath:n})=>await a.mkdirPromise(n,s))}mkdirSync(r,s){return this.makeCallSync(r,()=>this.baseFs.mkdirSync(r,s),(a,{subPath:n})=>a.mkdirSync(n,s))}async rmdirPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.rmdirPromise(r,s),async(a,{subPath:n})=>await a.rmdirPromise(n,s))}rmdirSync(r,s){return this.makeCallSync(r,()=>this.baseFs.rmdirSync(r,s),(a,{subPath:n})=>a.rmdirSync(n,s))}async rmPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.rmPromise(r,s),async(a,{subPath:n})=>await a.rmPromise(n,s))}rmSync(r,s){return this.makeCallSync(r,()=>this.baseFs.rmSync(r,s),(a,{subPath:n})=>a.rmSync(n,s))}async linkPromise(r,s){return await this.makeCallPromise(s,async()=>await this.baseFs.linkPromise(r,s),async(a,{subPath:n})=>await a.linkPromise(r,n))}linkSync(r,s){return this.makeCallSync(s,()=>this.baseFs.linkSync(r,s),(a,{subPath:n})=>a.linkSync(r,n))}async symlinkPromise(r,s,a){return await this.makeCallPromise(s,async()=>await this.baseFs.symlinkPromise(r,s,a),async(n,{subPath:c})=>await n.symlinkPromise(r,c))}symlinkSync(r,s,a){return this.makeCallSync(s,()=>this.baseFs.symlinkSync(r,s,a),(n,{subPath:c})=>n.symlinkSync(r,c))}async readFilePromise(r,s){return this.makeCallPromise(r,async()=>await this.baseFs.readFilePromise(r,s),async(a,{subPath:n})=>await a.readFilePromise(n,s))}readFileSync(r,s){return this.makeCallSync(r,()=>this.baseFs.readFileSync(r,s),(a,{subPath:n})=>a.readFileSync(n,s))}async readdirPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.readdirPromise(r,s),async(a,{subPath:n})=>await a.readdirPromise(n,s),{requireSubpath:!1})}readdirSync(r,s){return this.makeCallSync(r,()=>this.baseFs.readdirSync(r,s),(a,{subPath:n})=>a.readdirSync(n,s),{requireSubpath:!1})}async readlinkPromise(r){return await this.makeCallPromise(r,async()=>await this.baseFs.readlinkPromise(r),async(s,{subPath:a})=>await s.readlinkPromise(a))}readlinkSync(r){return this.makeCallSync(r,()=>this.baseFs.readlinkSync(r),(s,{subPath:a})=>s.readlinkSync(a))}async truncatePromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.truncatePromise(r,s),async(a,{subPath:n})=>await a.truncatePromise(n,s))}truncateSync(r,s){return this.makeCallSync(r,()=>this.baseFs.truncateSync(r,s),(a,{subPath:n})=>a.truncateSync(n,s))}async ftruncatePromise(r,s){if((r&cl)!==this.magic)return this.baseFs.ftruncatePromise(r,s);let a=this.fdMap.get(r);if(typeof a>"u")throw qo("ftruncate");let[n,c]=a;return n.ftruncatePromise(c,s)}ftruncateSync(r,s){if((r&cl)!==this.magic)return this.baseFs.ftruncateSync(r,s);let a=this.fdMap.get(r);if(typeof a>"u")throw qo("ftruncateSync");let[n,c]=a;return n.ftruncateSync(c,s)}watch(r,s,a){return this.makeCallSync(r,()=>this.baseFs.watch(r,s,a),(n,{subPath:c})=>n.watch(c,s,a))}watchFile(r,s,a){return this.makeCallSync(r,()=>this.baseFs.watchFile(r,s,a),()=>lE(this,r,s,a))}unwatchFile(r,s){return this.makeCallSync(r,()=>this.baseFs.unwatchFile(r,s),()=>dg(this,r,s))}async makeCallPromise(r,s,a,{requireSubpath:n=!0}={}){if(typeof r!="string")return await s();let c=this.resolve(r),f=this.findMount(c);return f?n&&f.subPath==="/"?await s():await this.getMountPromise(f.archivePath,async p=>await a(p,f)):await s()}makeCallSync(r,s,a,{requireSubpath:n=!0}={}){if(typeof r!="string")return s();let c=this.resolve(r),f=this.findMount(c);return!f||n&&f.subPath==="/"?s():this.getMountSync(f.archivePath,p=>a(p,f))}findMount(r){if(this.filter&&!this.filter.test(r))return null;let s="";for(;;){let a=r.substring(s.length),n=this.getMountPoint(a,s);if(!n)return null;if(s=this.pathUtils.join(s,n),!this.isMount.has(s)){if(this.notMount.has(s))continue;try{if(this.typeCheck!==null&&(this.baseFs.statSync(s).mode&Ig.constants.S_IFMT)!==this.typeCheck){this.notMount.add(s);continue}}catch{return null}this.isMount.add(s)}return{archivePath:s,subPath:this.pathUtils.join(vt.root,r.substring(s.length))}}}limitOpenFiles(r){if(this.mountInstances===null)return;let s=Date.now(),a=s+this.maxAge,n=r===null?0:this.mountInstances.size-r;for(let[c,{childFs:f,expiresAt:p,refCount:h}]of this.mountInstances.entries())if(!(h!==0||f.hasOpenFileHandles?.())){if(s>=p){f.saveAndClose?.(),this.mountInstances.delete(c),n-=1;continue}else if(r===null||n<=0){a=p;break}f.saveAndClose?.(),this.mountInstances.delete(c),n-=1}this.limitOpenFilesTimeout===null&&(r===null&&this.mountInstances.size>0||r!==null)&&isFinite(a)&&(this.limitOpenFilesTimeout=setTimeout(()=>{this.limitOpenFilesTimeout=null,this.limitOpenFiles(null)},a-s).unref())}async getMountPromise(r,s){if(this.mountInstances){let a=this.mountInstances.get(r);if(!a){let n=await this.factoryPromise(this.baseFs,r);a=this.mountInstances.get(r),a||(a={childFs:n(),expiresAt:0,refCount:0})}this.mountInstances.delete(r),this.limitOpenFiles(this.maxOpenFiles-1),this.mountInstances.set(r,a),a.expiresAt=Date.now()+this.maxAge,a.refCount+=1;try{return await s(a.childFs)}finally{a.refCount-=1}}else{let a=(await this.factoryPromise(this.baseFs,r))();try{return await s(a)}finally{a.saveAndClose?.()}}}getMountSync(r,s){if(this.mountInstances){let a=this.mountInstances.get(r);return a||(a={childFs:this.factorySync(this.baseFs,r),expiresAt:0,refCount:0}),this.mountInstances.delete(r),this.limitOpenFiles(this.maxOpenFiles-1),this.mountInstances.set(r,a),a.expiresAt=Date.now()+this.maxAge,s(a.childFs)}else{let a=this.factorySync(this.baseFs,r);try{return s(a)}finally{a.saveAndClose?.()}}}}});var er,nx,RZ=Xe(()=>{yg();ll();er=()=>Object.assign(new Error("ENOSYS: unsupported filesystem access"),{code:"ENOSYS"}),nx=class e extends Ep{static{this.instance=new e}constructor(){super(J)}getExtractHint(){throw er()}getRealPath(){throw er()}resolve(){throw er()}async openPromise(){throw er()}openSync(){throw er()}async opendirPromise(){throw er()}opendirSync(){throw er()}async readPromise(){throw er()}readSync(){throw er()}async writePromise(){throw er()}writeSync(){throw er()}async closePromise(){throw er()}closeSync(){throw er()}createWriteStream(){throw er()}createReadStream(){throw er()}async realpathPromise(){throw er()}realpathSync(){throw er()}async readdirPromise(){throw er()}readdirSync(){throw er()}async existsPromise(t){throw er()}existsSync(t){throw er()}async accessPromise(){throw er()}accessSync(){throw er()}async statPromise(){throw er()}statSync(){throw er()}async fstatPromise(t){throw er()}fstatSync(t){throw er()}async lstatPromise(t){throw er()}lstatSync(t){throw er()}async fchmodPromise(){throw er()}fchmodSync(){throw er()}async chmodPromise(){throw er()}chmodSync(){throw er()}async fchownPromise(){throw er()}fchownSync(){throw er()}async chownPromise(){throw er()}chownSync(){throw er()}async mkdirPromise(){throw er()}mkdirSync(){throw er()}async rmdirPromise(){throw er()}rmdirSync(){throw er()}async rmPromise(){throw er()}rmSync(){throw er()}async linkPromise(){throw er()}linkSync(){throw er()}async symlinkPromise(){throw er()}symlinkSync(){throw er()}async renamePromise(){throw er()}renameSync(){throw er()}async copyFilePromise(){throw er()}copyFileSync(){throw er()}async appendFilePromise(){throw er()}appendFileSync(){throw er()}async writeFilePromise(){throw er()}writeFileSync(){throw er()}async unlinkPromise(){throw er()}unlinkSync(){throw er()}async utimesPromise(){throw er()}utimesSync(){throw er()}async lutimesPromise(){throw er()}lutimesSync(){throw er()}async readFilePromise(){throw er()}readFileSync(){throw er()}async readlinkPromise(){throw er()}readlinkSync(){throw er()}async truncatePromise(){throw er()}truncateSync(){throw er()}async ftruncatePromise(t,r){throw er()}ftruncateSync(t,r){throw er()}watch(){throw er()}watchFile(){throw er()}unwatchFile(){throw er()}}});var e0,TZ=Xe(()=>{Ip();ll();e0=class extends Gs{constructor(t){super(fe),this.baseFs=t}mapFromBase(t){return fe.fromPortablePath(t)}mapToBase(t){return fe.toPortablePath(t)}}});var Vje,AU,Jje,mo,FZ=Xe(()=>{Eg();Ip();ll();Vje=/^[0-9]+$/,AU=/^(\/(?:[^/]+\/)*?(?:\$\$virtual|__virtual__))((?:\/((?:[^/]+-)?[a-f0-9]+)(?:\/([^/]+))?)?((?:\/.*)?))$/,Jje=/^([^/]+-)?[a-f0-9]+$/,mo=class e extends Gs{static makeVirtualPath(t,r,s){if(J.basename(t)!=="__virtual__")throw new Error('Assertion failed: Virtual folders must be named "__virtual__"');if(!J.basename(r).match(Jje))throw new Error("Assertion failed: Virtual components must be ended by an hexadecimal hash");let n=J.relative(J.dirname(t),s).split("/"),c=0;for(;c{pU=et(Ie("buffer")),NZ=Ie("url"),OZ=Ie("util");Ip();ll();ix=class extends Gs{constructor(t){super(fe),this.baseFs=t}mapFromBase(t){return t}mapToBase(t){if(typeof t=="string")return t;if(t instanceof URL)return(0,NZ.fileURLToPath)(t);if(Buffer.isBuffer(t)){let r=t.toString();if(!Kje(t,r))throw new Error("Non-utf8 buffers are not supported at the moment. Please upvote the following issue if you encounter this error: https://github.com/yarnpkg/berry/issues/4942");return r}throw new Error(`Unsupported path type: ${(0,OZ.inspect)(t)}`)}}});var jZ,Wo,Cp,t0,sx,ox,uE,Qu,Ru,MZ,UZ,_Z,HZ,k2,GZ=Xe(()=>{jZ=Ie("readline"),Wo=Symbol("kBaseFs"),Cp=Symbol("kFd"),t0=Symbol("kClosePromise"),sx=Symbol("kCloseResolve"),ox=Symbol("kCloseReject"),uE=Symbol("kRefs"),Qu=Symbol("kRef"),Ru=Symbol("kUnref"),k2=class{constructor(t,r){this[HZ]=1;this[_Z]=void 0;this[UZ]=void 0;this[MZ]=void 0;this[Wo]=r,this[Cp]=t}get fd(){return this[Cp]}async appendFile(t,r){try{this[Qu](this.appendFile);let s=(typeof r=="string"?r:r?.encoding)??void 0;return await this[Wo].appendFilePromise(this.fd,t,s?{encoding:s}:void 0)}finally{this[Ru]()}}async chown(t,r){try{return this[Qu](this.chown),await this[Wo].fchownPromise(this.fd,t,r)}finally{this[Ru]()}}async chmod(t){try{return this[Qu](this.chmod),await this[Wo].fchmodPromise(this.fd,t)}finally{this[Ru]()}}createReadStream(t){return this[Wo].createReadStream(null,{...t,fd:this.fd})}createWriteStream(t){return this[Wo].createWriteStream(null,{...t,fd:this.fd})}datasync(){throw new Error("Method not implemented.")}sync(){throw new Error("Method not implemented.")}async read(t,r,s,a){try{this[Qu](this.read);let n,c;return ArrayBuffer.isView(t)?typeof r=="object"&&r!==null?(n=t,c=r?.offset??0,s=r?.length??n.byteLength-c,a=r?.position??null):(n=t,c=r??0,s??=0):(n=t?.buffer??Buffer.alloc(16384),c=t?.offset??0,s=t?.length??n.byteLength-c,a=t?.position??null),s===0?{bytesRead:s,buffer:n}:{bytesRead:await this[Wo].readPromise(this.fd,Buffer.isBuffer(n)?n:Buffer.from(n.buffer,n.byteOffset,n.byteLength),c,s,a),buffer:n}}finally{this[Ru]()}}async readFile(t){try{this[Qu](this.readFile);let r=(typeof t=="string"?t:t?.encoding)??void 0;return await this[Wo].readFilePromise(this.fd,r)}finally{this[Ru]()}}readLines(t){return(0,jZ.createInterface)({input:this.createReadStream(t),crlfDelay:1/0})}async stat(t){try{return this[Qu](this.stat),await this[Wo].fstatPromise(this.fd,t)}finally{this[Ru]()}}async truncate(t){try{return this[Qu](this.truncate),await this[Wo].ftruncatePromise(this.fd,t)}finally{this[Ru]()}}utimes(t,r){throw new Error("Method not implemented.")}async writeFile(t,r){try{this[Qu](this.writeFile);let s=(typeof r=="string"?r:r?.encoding)??void 0;await this[Wo].writeFilePromise(this.fd,t,s)}finally{this[Ru]()}}async write(...t){try{if(this[Qu](this.write),ArrayBuffer.isView(t[0])){let[r,s,a,n]=t;return{bytesWritten:await this[Wo].writePromise(this.fd,r,s??void 0,a??void 0,n??void 0),buffer:r}}else{let[r,s,a]=t;return{bytesWritten:await this[Wo].writePromise(this.fd,r,s,a),buffer:r}}}finally{this[Ru]()}}async writev(t,r){try{this[Qu](this.writev);let s=0;if(typeof r<"u")for(let a of t){let n=await this.write(a,void 0,void 0,r);s+=n.bytesWritten,r+=n.bytesWritten}else for(let a of t){let n=await this.write(a);s+=n.bytesWritten}return{buffers:t,bytesWritten:s}}finally{this[Ru]()}}readv(t,r){throw new Error("Method not implemented.")}close(){if(this[Cp]===-1)return Promise.resolve();if(this[t0])return this[t0];if(this[uE]--,this[uE]===0){let t=this[Cp];this[Cp]=-1,this[t0]=this[Wo].closePromise(t).finally(()=>{this[t0]=void 0})}else this[t0]=new Promise((t,r)=>{this[sx]=t,this[ox]=r}).finally(()=>{this[t0]=void 0,this[ox]=void 0,this[sx]=void 0});return this[t0]}[(Wo,Cp,HZ=uE,_Z=t0,UZ=sx,MZ=ox,Qu)](t){if(this[Cp]===-1){let r=new Error("file closed");throw r.code="EBADF",r.syscall=t.name,r}this[uE]++}[Ru](){if(this[uE]--,this[uE]===0){let t=this[Cp];this[Cp]=-1,this[Wo].closePromise(t).then(this[sx],this[ox])}}}});function Q2(e,t){t=new ix(t);let r=(s,a,n)=>{let c=s[a];s[a]=n,typeof c?.[fE.promisify.custom]<"u"&&(n[fE.promisify.custom]=c[fE.promisify.custom])};{r(e,"exists",(s,...a)=>{let c=typeof a[a.length-1]=="function"?a.pop():()=>{};process.nextTick(()=>{t.existsPromise(s).then(f=>{c(f)},()=>{c(!1)})})}),r(e,"read",(...s)=>{let[a,n,c,f,p,h]=s;if(s.length<=3){let E={};s.length<3?h=s[1]:(E=s[1],h=s[2]),{buffer:n=Buffer.alloc(16384),offset:c=0,length:f=n.byteLength,position:p}=E}if(c==null&&(c=0),f|=0,f===0){process.nextTick(()=>{h(null,0,n)});return}p==null&&(p=-1),process.nextTick(()=>{t.readPromise(a,n,c,f,p).then(E=>{h(null,E,n)},E=>{h(E,0,n)})})});for(let s of qZ){let a=s.replace(/Promise$/,"");if(typeof e[a]>"u")continue;let n=t[s];if(typeof n>"u")continue;r(e,a,(...f)=>{let h=typeof f[f.length-1]=="function"?f.pop():()=>{};process.nextTick(()=>{n.apply(t,f).then(E=>{h(null,E)},E=>{h(E)})})})}e.realpath.native=e.realpath}{r(e,"existsSync",s=>{try{return t.existsSync(s)}catch{return!1}}),r(e,"readSync",(...s)=>{let[a,n,c,f,p]=s;return s.length<=3&&({offset:c=0,length:f=n.byteLength,position:p}=s[2]||{}),c==null&&(c=0),f|=0,f===0?0:(p==null&&(p=-1),t.readSync(a,n,c,f,p))});for(let s of zje){let a=s;if(typeof e[a]>"u")continue;let n=t[s];typeof n>"u"||r(e,a,n.bind(t))}e.realpathSync.native=e.realpathSync}{let s=e.promises;for(let a of qZ){let n=a.replace(/Promise$/,"");if(typeof s[n]>"u")continue;let c=t[a];typeof c>"u"||a!=="open"&&r(s,n,(f,...p)=>f instanceof k2?f[n].apply(f,p):c.call(t,f,...p))}r(s,"open",async(...a)=>{let n=await t.openPromise(...a);return new k2(n,t)})}e.read[fE.promisify.custom]=async(s,a,...n)=>({bytesRead:await t.readPromise(s,a,...n),buffer:a}),e.write[fE.promisify.custom]=async(s,a,...n)=>({bytesWritten:await t.writePromise(s,a,...n),buffer:a})}function ax(e,t){let r=Object.create(e);return Q2(r,t),r}var fE,zje,qZ,WZ=Xe(()=>{fE=Ie("util");LZ();GZ();zje=new Set(["accessSync","appendFileSync","createReadStream","createWriteStream","chmodSync","fchmodSync","chownSync","fchownSync","closeSync","copyFileSync","linkSync","lstatSync","fstatSync","lutimesSync","mkdirSync","openSync","opendirSync","readlinkSync","readFileSync","readdirSync","readlinkSync","realpathSync","renameSync","rmdirSync","rmSync","statSync","symlinkSync","truncateSync","ftruncateSync","unlinkSync","unwatchFile","utimesSync","watch","watchFile","writeFileSync","writeSync"]),qZ=new Set(["accessPromise","appendFilePromise","fchmodPromise","chmodPromise","fchownPromise","chownPromise","closePromise","copyFilePromise","linkPromise","fstatPromise","lstatPromise","lutimesPromise","mkdirPromise","openPromise","opendirPromise","readdirPromise","realpathPromise","readFilePromise","readdirPromise","readlinkPromise","renamePromise","rmdirPromise","rmPromise","statPromise","symlinkPromise","truncatePromise","ftruncatePromise","unlinkPromise","utimesPromise","writeFilePromise","writeSync"])});function YZ(e){let t=Math.ceil(Math.random()*4294967296).toString(16).padStart(8,"0");return`${e}${t}`}function VZ(){if(hU)return hU;let e=fe.toPortablePath(JZ.default.tmpdir()),t=le.realpathSync(e);return process.once("exit",()=>{le.rmtempSync()}),hU={tmpdir:e,realTmpdir:t}}var JZ,Tu,hU,le,KZ=Xe(()=>{JZ=et(Ie("os"));Eg();ll();Tu=new Set,hU=null;le=Object.assign(new Vn,{detachTemp(e){Tu.delete(e)},mktempSync(e){let{tmpdir:t,realTmpdir:r}=VZ();for(;;){let s=YZ("xfs-");try{this.mkdirSync(J.join(t,s))}catch(n){if(n.code==="EEXIST")continue;throw n}let a=J.join(r,s);if(Tu.add(a),typeof e>"u")return a;try{return e(a)}finally{if(Tu.has(a)){Tu.delete(a);try{this.removeSync(a)}catch{}}}}},async mktempPromise(e){let{tmpdir:t,realTmpdir:r}=VZ();for(;;){let s=YZ("xfs-");try{await this.mkdirPromise(J.join(t,s))}catch(n){if(n.code==="EEXIST")continue;throw n}let a=J.join(r,s);if(Tu.add(a),typeof e>"u")return a;try{return await e(a)}finally{if(Tu.has(a)){Tu.delete(a);try{await this.removePromise(a)}catch{}}}}},async rmtempPromise(){await Promise.all(Array.from(Tu.values()).map(async e=>{try{await le.removePromise(e,{maxRetries:0}),Tu.delete(e)}catch{}}))},rmtempSync(){for(let e of Tu)try{le.removeSync(e),Tu.delete(e)}catch{}}})});var R2={};Vt(R2,{AliasFS:()=>Gf,BasePortableFakeFS:()=>jf,CustomDir:()=>x2,CwdFS:()=>bn,FakeFS:()=>Ep,Filename:()=>Er,JailFS:()=>qf,LazyFS:()=>cE,MountFS:()=>$h,NoFS:()=>nx,NodeFS:()=>Vn,PortablePath:()=>vt,PosixFS:()=>e0,ProxiedFS:()=>Gs,VirtualFS:()=>mo,constants:()=>Ai,errors:()=>or,extendFs:()=>ax,normalizeLineEndings:()=>mg,npath:()=>fe,opendir:()=>ex,patchFs:()=>Q2,ppath:()=>J,setupCopyIndex:()=>$P,statUtils:()=>al,unwatchAllFiles:()=>gg,unwatchFile:()=>dg,watchFile:()=>lE,xfs:()=>le});var Dt=Xe(()=>{AZ();zP();aU();uU();yZ();fU();yg();ll();ll();vZ();yg();bZ();xZ();kZ();QZ();RZ();Eg();TZ();Ip();FZ();WZ();KZ()});var e$=G((jbt,$Z)=>{$Z.exports=ZZ;ZZ.sync=Zje;var zZ=Ie("fs");function Xje(e,t){var r=t.pathExt!==void 0?t.pathExt:process.env.PATHEXT;if(!r||(r=r.split(";"),r.indexOf("")!==-1))return!0;for(var s=0;s{i$.exports=r$;r$.sync=$je;var t$=Ie("fs");function r$(e,t,r){t$.stat(e,function(s,a){r(s,s?!1:n$(a,t))})}function $je(e,t){return n$(t$.statSync(e),t)}function n$(e,t){return e.isFile()&&e6e(e,t)}function e6e(e,t){var r=e.mode,s=e.uid,a=e.gid,n=t.uid!==void 0?t.uid:process.getuid&&process.getuid(),c=t.gid!==void 0?t.gid:process.getgid&&process.getgid(),f=parseInt("100",8),p=parseInt("010",8),h=parseInt("001",8),E=f|p,C=r&h||r&p&&a===c||r&f&&s===n||r&E&&n===0;return C}});var a$=G((Wbt,o$)=>{var qbt=Ie("fs"),lx;process.platform==="win32"||global.TESTING_WINDOWS?lx=e$():lx=s$();o$.exports=dU;dU.sync=t6e;function dU(e,t,r){if(typeof t=="function"&&(r=t,t={}),!r){if(typeof Promise!="function")throw new TypeError("callback not provided");return new Promise(function(s,a){dU(e,t||{},function(n,c){n?a(n):s(c)})})}lx(e,t||{},function(s,a){s&&(s.code==="EACCES"||t&&t.ignoreErrors)&&(s=null,a=!1),r(s,a)})}function t6e(e,t){try{return lx.sync(e,t||{})}catch(r){if(t&&t.ignoreErrors||r.code==="EACCES")return!1;throw r}}});var h$=G((Ybt,p$)=>{var AE=process.platform==="win32"||process.env.OSTYPE==="cygwin"||process.env.OSTYPE==="msys",l$=Ie("path"),r6e=AE?";":":",c$=a$(),u$=e=>Object.assign(new Error(`not found: ${e}`),{code:"ENOENT"}),f$=(e,t)=>{let r=t.colon||r6e,s=e.match(/\//)||AE&&e.match(/\\/)?[""]:[...AE?[process.cwd()]:[],...(t.path||process.env.PATH||"").split(r)],a=AE?t.pathExt||process.env.PATHEXT||".EXE;.CMD;.BAT;.COM":"",n=AE?a.split(r):[""];return AE&&e.indexOf(".")!==-1&&n[0]!==""&&n.unshift(""),{pathEnv:s,pathExt:n,pathExtExe:a}},A$=(e,t,r)=>{typeof t=="function"&&(r=t,t={}),t||(t={});let{pathEnv:s,pathExt:a,pathExtExe:n}=f$(e,t),c=[],f=h=>new Promise((E,C)=>{if(h===s.length)return t.all&&c.length?E(c):C(u$(e));let S=s[h],x=/^".*"$/.test(S)?S.slice(1,-1):S,I=l$.join(x,e),T=!x&&/^\.[\\\/]/.test(e)?e.slice(0,2)+I:I;E(p(T,h,0))}),p=(h,E,C)=>new Promise((S,x)=>{if(C===a.length)return S(f(E+1));let I=a[C];c$(h+I,{pathExt:n},(T,O)=>{if(!T&&O)if(t.all)c.push(h+I);else return S(h+I);return S(p(h,E,C+1))})});return r?f(0).then(h=>r(null,h),r):f(0)},n6e=(e,t)=>{t=t||{};let{pathEnv:r,pathExt:s,pathExtExe:a}=f$(e,t),n=[];for(let c=0;c{"use strict";var d$=(e={})=>{let t=e.env||process.env;return(e.platform||process.platform)!=="win32"?"PATH":Object.keys(t).reverse().find(s=>s.toUpperCase()==="PATH")||"Path"};gU.exports=d$;gU.exports.default=d$});var I$=G((Jbt,E$)=>{"use strict";var m$=Ie("path"),i6e=h$(),s6e=g$();function y$(e,t){let r=e.options.env||process.env,s=process.cwd(),a=e.options.cwd!=null,n=a&&process.chdir!==void 0&&!process.chdir.disabled;if(n)try{process.chdir(e.options.cwd)}catch{}let c;try{c=i6e.sync(e.command,{path:r[s6e({env:r})],pathExt:t?m$.delimiter:void 0})}catch{}finally{n&&process.chdir(s)}return c&&(c=m$.resolve(a?e.options.cwd:"",c)),c}function o6e(e){return y$(e)||y$(e,!0)}E$.exports=o6e});var C$=G((Kbt,yU)=>{"use strict";var mU=/([()\][%!^"`<>&|;, *?])/g;function a6e(e){return e=e.replace(mU,"^$1"),e}function l6e(e,t){return e=`${e}`,e=e.replace(/(?=(\\+?)?)\1"/g,'$1$1\\"'),e=e.replace(/(?=(\\+?)?)\1$/,"$1$1"),e=`"${e}"`,e=e.replace(mU,"^$1"),t&&(e=e.replace(mU,"^$1")),e}yU.exports.command=a6e;yU.exports.argument=l6e});var B$=G((zbt,w$)=>{"use strict";w$.exports=/^#!(.*)/});var S$=G((Xbt,v$)=>{"use strict";var c6e=B$();v$.exports=(e="")=>{let t=e.match(c6e);if(!t)return null;let[r,s]=t[0].replace(/#! ?/,"").split(" "),a=r.split("/").pop();return a==="env"?s:s?`${a} ${s}`:a}});var b$=G((Zbt,D$)=>{"use strict";var EU=Ie("fs"),u6e=S$();function f6e(e){let r=Buffer.alloc(150),s;try{s=EU.openSync(e,"r"),EU.readSync(s,r,0,150,0),EU.closeSync(s)}catch{}return u6e(r.toString())}D$.exports=f6e});var Q$=G(($bt,k$)=>{"use strict";var A6e=Ie("path"),P$=I$(),x$=C$(),p6e=b$(),h6e=process.platform==="win32",d6e=/\.(?:com|exe)$/i,g6e=/node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i;function m6e(e){e.file=P$(e);let t=e.file&&p6e(e.file);return t?(e.args.unshift(e.file),e.command=t,P$(e)):e.file}function y6e(e){if(!h6e)return e;let t=m6e(e),r=!d6e.test(t);if(e.options.forceShell||r){let s=g6e.test(t);e.command=A6e.normalize(e.command),e.command=x$.command(e.command),e.args=e.args.map(n=>x$.argument(n,s));let a=[e.command].concat(e.args).join(" ");e.args=["/d","/s","/c",`"${a}"`],e.command=process.env.comspec||"cmd.exe",e.options.windowsVerbatimArguments=!0}return e}function E6e(e,t,r){t&&!Array.isArray(t)&&(r=t,t=null),t=t?t.slice(0):[],r=Object.assign({},r);let s={command:e,args:t,options:r,file:void 0,original:{command:e,args:t}};return r.shell?s:y6e(s)}k$.exports=E6e});var F$=G((ePt,T$)=>{"use strict";var IU=process.platform==="win32";function CU(e,t){return Object.assign(new Error(`${t} ${e.command} ENOENT`),{code:"ENOENT",errno:"ENOENT",syscall:`${t} ${e.command}`,path:e.command,spawnargs:e.args})}function I6e(e,t){if(!IU)return;let r=e.emit;e.emit=function(s,a){if(s==="exit"){let n=R$(a,t);if(n)return r.call(e,"error",n)}return r.apply(e,arguments)}}function R$(e,t){return IU&&e===1&&!t.file?CU(t.original,"spawn"):null}function C6e(e,t){return IU&&e===1&&!t.file?CU(t.original,"spawnSync"):null}T$.exports={hookChildProcess:I6e,verifyENOENT:R$,verifyENOENTSync:C6e,notFoundError:CU}});var vU=G((tPt,pE)=>{"use strict";var N$=Ie("child_process"),wU=Q$(),BU=F$();function O$(e,t,r){let s=wU(e,t,r),a=N$.spawn(s.command,s.args,s.options);return BU.hookChildProcess(a,s),a}function w6e(e,t,r){let s=wU(e,t,r),a=N$.spawnSync(s.command,s.args,s.options);return a.error=a.error||BU.verifyENOENTSync(a.status,s),a}pE.exports=O$;pE.exports.spawn=O$;pE.exports.sync=w6e;pE.exports._parse=wU;pE.exports._enoent=BU});var M$=G((rPt,L$)=>{"use strict";function B6e(e,t){function r(){this.constructor=e}r.prototype=t.prototype,e.prototype=new r}function Cg(e,t,r,s){this.message=e,this.expected=t,this.found=r,this.location=s,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,Cg)}B6e(Cg,Error);Cg.buildMessage=function(e,t){var r={literal:function(h){return'"'+a(h.text)+'"'},class:function(h){var E="",C;for(C=0;C0){for(C=1,S=1;C>",b=ur(">>",!1),y=">&",F=ur(">&",!1),z=">",X=ur(">",!1),$="<<<",se=ur("<<<",!1),xe="<&",Fe=ur("<&",!1),ut="<",Ct=ur("<",!1),qt=function(N){return{type:"argument",segments:[].concat(...N)}},ir=function(N){return N},Pt="$'",gn=ur("$'",!1),Pr="'",Cr=ur("'",!1),Or=function(N){return[{type:"text",text:N}]},on='""',li=ur('""',!1),Do=function(){return{type:"text",text:""}},ns='"',so=ur('"',!1),bo=function(N){return N},ji=function(N){return{type:"arithmetic",arithmetic:N,quoted:!0}},oo=function(N){return{type:"shell",shell:N,quoted:!0}},Po=function(N){return{type:"variable",...N,quoted:!0}},TA=function(N){return{type:"text",text:N}},df=function(N){return{type:"arithmetic",arithmetic:N,quoted:!1}},dh=function(N){return{type:"shell",shell:N,quoted:!1}},gh=function(N){return{type:"variable",...N,quoted:!1}},ao=function(N){return{type:"glob",pattern:N}},Gn=/^[^']/,Ns=zi(["'"],!0,!1),lo=function(N){return N.join("")},su=/^[^$"]/,ou=zi(["$",'"'],!0,!1),au=`\\ `,FA=ur(`\\ `,!1),NA=function(){return""},fa="\\",Aa=ur("\\",!1),OA=/^[\\$"`]/,dr=zi(["\\","$",'"',"`"],!1,!1),xo=function(N){return N},Ga="\\a",Ue=ur("\\a",!1),wr=function(){return"a"},gf="\\b",LA=ur("\\b",!1),MA=function(){return"\b"},lu=/^[Ee]/,cu=zi(["E","e"],!1,!1),lc=function(){return"\x1B"},we="\\f",Nt=ur("\\f",!1),cc=function(){return"\f"},Oi="\\n",co=ur("\\n",!1),Tt=function(){return` `},Qn="\\r",pa=ur("\\r",!1),Gi=function(){return"\r"},Li="\\t",qa=ur("\\t",!1),mn=function(){return" "},Xn="\\v",uu=ur("\\v",!1),mh=function(){return"\v"},Wa=/^[\\'"?]/,Ya=zi(["\\","'",'"',"?"],!1,!1),Va=function(N){return String.fromCharCode(parseInt(N,16))},$e="\\x",Ja=ur("\\x",!1),mf="\\u",uc=ur("\\u",!1),vn="\\U",ha=ur("\\U",!1),UA=function(N){return String.fromCodePoint(parseInt(N,16))},_A=/^[0-7]/,da=zi([["0","7"]],!1,!1),kl=/^[0-9a-fA-f]/,Ut=zi([["0","9"],["a","f"],["A","f"]],!1,!1),Rn=Cf(),ga="{}",Ka=ur("{}",!1),is=function(){return"{}"},fc="-",fu=ur("-",!1),Ac="+",za=ur("+",!1),Mi=".",Bs=ur(".",!1),Ql=function(N,K,re){return{type:"number",value:(N==="-"?-1:1)*parseFloat(K.join("")+"."+re.join(""))}},yf=function(N,K){return{type:"number",value:(N==="-"?-1:1)*parseInt(K.join(""))}},pc=function(N){return{type:"variable",...N}},Bi=function(N){return{type:"variable",name:N}},Tn=function(N){return N},hc="*",Ke=ur("*",!1),ot="/",St=ur("/",!1),lr=function(N,K,re){return{type:K==="*"?"multiplication":"division",right:re}},ee=function(N,K){return K.reduce((re,de)=>({left:re,...de}),N)},ye=function(N,K,re){return{type:K==="+"?"addition":"subtraction",right:re}},Oe="$((",mt=ur("$((",!1),Et="))",bt=ur("))",!1),tr=function(N){return N},pn="$(",ci=ur("$(",!1),qi=function(N){return N},Fn="${",Xa=ur("${",!1),Iy=":-",q1=ur(":-",!1),ko=function(N,K){return{name:N,defaultValue:K}},Cy=":-}",yh=ur(":-}",!1),W1=function(N){return{name:N,defaultValue:[]}},Qo=":+",Eh=ur(":+",!1),Ih=function(N,K){return{name:N,alternativeValue:K}},Au=":+}",Ch=ur(":+}",!1),Rd=function(N){return{name:N,alternativeValue:[]}},Td=function(N){return{name:N}},Fd="$",wy=ur("$",!1),Ef=function(N){return t.isGlobPattern(N)},Ro=function(N){return N},Rl=/^[a-zA-Z0-9_]/,wh=zi([["a","z"],["A","Z"],["0","9"],"_"],!1,!1),Nd=function(){return Dy()},Tl=/^[$@*?#a-zA-Z0-9_\-]/,Fl=zi(["$","@","*","?","#",["a","z"],["A","Z"],["0","9"],"_","-"],!1,!1),By=/^[()}<>$|&; \t"']/,HA=zi(["(",")","}","<",">","$","|","&",";"," "," ",'"',"'"],!1,!1),vy=/^[<>&; \t"']/,Sy=zi(["<",">","&",";"," "," ",'"',"'"],!1,!1),jA=/^[ \t]/,GA=zi([" "," "],!1,!1),W=0,xt=0,qA=[{line:1,column:1}],To=0,If=[],yt=0,pu;if("startRule"in t){if(!(t.startRule in s))throw new Error(`Can't start parsing from rule "`+t.startRule+'".');a=s[t.startRule]}function Dy(){return e.substring(xt,W)}function Od(){return wf(xt,W)}function Y1(N,K){throw K=K!==void 0?K:wf(xt,W),WA([Ld(N)],e.substring(xt,W),K)}function Bh(N,K){throw K=K!==void 0?K:wf(xt,W),mi(N,K)}function ur(N,K){return{type:"literal",text:N,ignoreCase:K}}function zi(N,K,re){return{type:"class",parts:N,inverted:K,ignoreCase:re}}function Cf(){return{type:"any"}}function Za(){return{type:"end"}}function Ld(N){return{type:"other",description:N}}function hu(N){var K=qA[N],re;if(K)return K;for(re=N-1;!qA[re];)re--;for(K=qA[re],K={line:K.line,column:K.column};reTo&&(To=W,If=[]),If.push(N))}function mi(N,K){return new Cg(N,null,null,K)}function WA(N,K,re){return new Cg(Cg.buildMessage(N,K),N,K,re)}function $a(){var N,K,re;for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();return K!==r?(re=ma(),re===r&&(re=null),re!==r?(xt=N,K=n(re),N=K):(W=N,N=r)):(W=N,N=r),N}function ma(){var N,K,re,de,Je;if(N=W,K=vh(),K!==r){for(re=[],de=kt();de!==r;)re.push(de),de=kt();re!==r?(de=Md(),de!==r?(Je=el(),Je===r&&(Je=null),Je!==r?(xt=N,K=c(K,de,Je),N=K):(W=N,N=r)):(W=N,N=r)):(W=N,N=r)}else W=N,N=r;if(N===r)if(N=W,K=vh(),K!==r){for(re=[],de=kt();de!==r;)re.push(de),de=kt();re!==r?(de=Md(),de===r&&(de=null),de!==r?(xt=N,K=f(K,de),N=K):(W=N,N=r)):(W=N,N=r)}else W=N,N=r;return N}function el(){var N,K,re,de,Je;for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();if(K!==r)if(re=ma(),re!==r){for(de=[],Je=kt();Je!==r;)de.push(Je),Je=kt();de!==r?(xt=N,K=p(re),N=K):(W=N,N=r)}else W=N,N=r;else W=N,N=r;return N}function Md(){var N;return e.charCodeAt(W)===59?(N=h,W++):(N=r,yt===0&&wt(E)),N===r&&(e.charCodeAt(W)===38?(N=C,W++):(N=r,yt===0&&wt(S))),N}function vh(){var N,K,re;return N=W,K=YA(),K!==r?(re=Ud(),re===r&&(re=null),re!==r?(xt=N,K=x(K,re),N=K):(W=N,N=r)):(W=N,N=r),N}function Ud(){var N,K,re,de,Je,pt,gr;for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();if(K!==r)if(re=by(),re!==r){for(de=[],Je=kt();Je!==r;)de.push(Je),Je=kt();if(de!==r)if(Je=vh(),Je!==r){for(pt=[],gr=kt();gr!==r;)pt.push(gr),gr=kt();pt!==r?(xt=N,K=I(re,Je),N=K):(W=N,N=r)}else W=N,N=r;else W=N,N=r}else W=N,N=r;else W=N,N=r;return N}function by(){var N;return e.substr(W,2)===T?(N=T,W+=2):(N=r,yt===0&&wt(O)),N===r&&(e.substr(W,2)===U?(N=U,W+=2):(N=r,yt===0&&wt(V))),N}function YA(){var N,K,re;return N=W,K=Bf(),K!==r?(re=_d(),re===r&&(re=null),re!==r?(xt=N,K=te(K,re),N=K):(W=N,N=r)):(W=N,N=r),N}function _d(){var N,K,re,de,Je,pt,gr;for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();if(K!==r)if(re=du(),re!==r){for(de=[],Je=kt();Je!==r;)de.push(Je),Je=kt();if(de!==r)if(Je=YA(),Je!==r){for(pt=[],gr=kt();gr!==r;)pt.push(gr),gr=kt();pt!==r?(xt=N,K=ie(re,Je),N=K):(W=N,N=r)}else W=N,N=r;else W=N,N=r}else W=N,N=r;else W=N,N=r;return N}function du(){var N;return e.substr(W,2)===ue?(N=ue,W+=2):(N=r,yt===0&&wt(ae)),N===r&&(e.charCodeAt(W)===124?(N=ge,W++):(N=r,yt===0&&wt(Ae))),N}function gu(){var N,K,re,de,Je,pt;if(N=W,K=bh(),K!==r)if(e.charCodeAt(W)===61?(re=Ce,W++):(re=r,yt===0&&wt(Ee)),re!==r)if(de=VA(),de!==r){for(Je=[],pt=kt();pt!==r;)Je.push(pt),pt=kt();Je!==r?(xt=N,K=d(K,de),N=K):(W=N,N=r)}else W=N,N=r;else W=N,N=r;else W=N,N=r;if(N===r)if(N=W,K=bh(),K!==r)if(e.charCodeAt(W)===61?(re=Ce,W++):(re=r,yt===0&&wt(Ee)),re!==r){for(de=[],Je=kt();Je!==r;)de.push(Je),Je=kt();de!==r?(xt=N,K=Se(K),N=K):(W=N,N=r)}else W=N,N=r;else W=N,N=r;return N}function Bf(){var N,K,re,de,Je,pt,gr,vr,_n,yi,vs;for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();if(K!==r)if(e.charCodeAt(W)===40?(re=Be,W++):(re=r,yt===0&&wt(me)),re!==r){for(de=[],Je=kt();Je!==r;)de.push(Je),Je=kt();if(de!==r)if(Je=ma(),Je!==r){for(pt=[],gr=kt();gr!==r;)pt.push(gr),gr=kt();if(pt!==r)if(e.charCodeAt(W)===41?(gr=ce,W++):(gr=r,yt===0&&wt(Z)),gr!==r){for(vr=[],_n=kt();_n!==r;)vr.push(_n),_n=kt();if(vr!==r){for(_n=[],yi=qn();yi!==r;)_n.push(yi),yi=qn();if(_n!==r){for(yi=[],vs=kt();vs!==r;)yi.push(vs),vs=kt();yi!==r?(xt=N,K=De(Je,_n),N=K):(W=N,N=r)}else W=N,N=r}else W=N,N=r}else W=N,N=r;else W=N,N=r}else W=N,N=r;else W=N,N=r}else W=N,N=r;else W=N,N=r;if(N===r){for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();if(K!==r)if(e.charCodeAt(W)===123?(re=Qe,W++):(re=r,yt===0&&wt(st)),re!==r){for(de=[],Je=kt();Je!==r;)de.push(Je),Je=kt();if(de!==r)if(Je=ma(),Je!==r){for(pt=[],gr=kt();gr!==r;)pt.push(gr),gr=kt();if(pt!==r)if(e.charCodeAt(W)===125?(gr=_,W++):(gr=r,yt===0&&wt(tt)),gr!==r){for(vr=[],_n=kt();_n!==r;)vr.push(_n),_n=kt();if(vr!==r){for(_n=[],yi=qn();yi!==r;)_n.push(yi),yi=qn();if(_n!==r){for(yi=[],vs=kt();vs!==r;)yi.push(vs),vs=kt();yi!==r?(xt=N,K=Ne(Je,_n),N=K):(W=N,N=r)}else W=N,N=r}else W=N,N=r}else W=N,N=r;else W=N,N=r}else W=N,N=r;else W=N,N=r}else W=N,N=r;else W=N,N=r;if(N===r){for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();if(K!==r){for(re=[],de=gu();de!==r;)re.push(de),de=gu();if(re!==r){for(de=[],Je=kt();Je!==r;)de.push(Je),Je=kt();if(de!==r){if(Je=[],pt=mu(),pt!==r)for(;pt!==r;)Je.push(pt),pt=mu();else Je=r;if(Je!==r){for(pt=[],gr=kt();gr!==r;)pt.push(gr),gr=kt();pt!==r?(xt=N,K=ke(re,Je),N=K):(W=N,N=r)}else W=N,N=r}else W=N,N=r}else W=N,N=r}else W=N,N=r;if(N===r){for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();if(K!==r){if(re=[],de=gu(),de!==r)for(;de!==r;)re.push(de),de=gu();else re=r;if(re!==r){for(de=[],Je=kt();Je!==r;)de.push(Je),Je=kt();de!==r?(xt=N,K=be(re),N=K):(W=N,N=r)}else W=N,N=r}else W=N,N=r}}}return N}function Os(){var N,K,re,de,Je;for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();if(K!==r){if(re=[],de=Pi(),de!==r)for(;de!==r;)re.push(de),de=Pi();else re=r;if(re!==r){for(de=[],Je=kt();Je!==r;)de.push(Je),Je=kt();de!==r?(xt=N,K=je(re),N=K):(W=N,N=r)}else W=N,N=r}else W=N,N=r;return N}function mu(){var N,K,re;for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();if(K!==r?(re=qn(),re!==r?(xt=N,K=Re(re),N=K):(W=N,N=r)):(W=N,N=r),N===r){for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();K!==r?(re=Pi(),re!==r?(xt=N,K=Re(re),N=K):(W=N,N=r)):(W=N,N=r)}return N}function qn(){var N,K,re,de,Je;for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();return K!==r?(ct.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(Me)),re===r&&(re=null),re!==r?(de=ss(),de!==r?(Je=Pi(),Je!==r?(xt=N,K=P(re,de,Je),N=K):(W=N,N=r)):(W=N,N=r)):(W=N,N=r)):(W=N,N=r),N}function ss(){var N;return e.substr(W,2)===w?(N=w,W+=2):(N=r,yt===0&&wt(b)),N===r&&(e.substr(W,2)===y?(N=y,W+=2):(N=r,yt===0&&wt(F)),N===r&&(e.charCodeAt(W)===62?(N=z,W++):(N=r,yt===0&&wt(X)),N===r&&(e.substr(W,3)===$?(N=$,W+=3):(N=r,yt===0&&wt(se)),N===r&&(e.substr(W,2)===xe?(N=xe,W+=2):(N=r,yt===0&&wt(Fe)),N===r&&(e.charCodeAt(W)===60?(N=ut,W++):(N=r,yt===0&&wt(Ct))))))),N}function Pi(){var N,K,re;for(N=W,K=[],re=kt();re!==r;)K.push(re),re=kt();return K!==r?(re=VA(),re!==r?(xt=N,K=Re(re),N=K):(W=N,N=r)):(W=N,N=r),N}function VA(){var N,K,re;if(N=W,K=[],re=vf(),re!==r)for(;re!==r;)K.push(re),re=vf();else K=r;return K!==r&&(xt=N,K=qt(K)),N=K,N}function vf(){var N,K;return N=W,K=yn(),K!==r&&(xt=N,K=ir(K)),N=K,N===r&&(N=W,K=Hd(),K!==r&&(xt=N,K=ir(K)),N=K,N===r&&(N=W,K=jd(),K!==r&&(xt=N,K=ir(K)),N=K,N===r&&(N=W,K=os(),K!==r&&(xt=N,K=ir(K)),N=K))),N}function yn(){var N,K,re,de;return N=W,e.substr(W,2)===Pt?(K=Pt,W+=2):(K=r,yt===0&&wt(gn)),K!==r?(re=En(),re!==r?(e.charCodeAt(W)===39?(de=Pr,W++):(de=r,yt===0&&wt(Cr)),de!==r?(xt=N,K=Or(re),N=K):(W=N,N=r)):(W=N,N=r)):(W=N,N=r),N}function Hd(){var N,K,re,de;return N=W,e.charCodeAt(W)===39?(K=Pr,W++):(K=r,yt===0&&wt(Cr)),K!==r?(re=Sf(),re!==r?(e.charCodeAt(W)===39?(de=Pr,W++):(de=r,yt===0&&wt(Cr)),de!==r?(xt=N,K=Or(re),N=K):(W=N,N=r)):(W=N,N=r)):(W=N,N=r),N}function jd(){var N,K,re,de;if(N=W,e.substr(W,2)===on?(K=on,W+=2):(K=r,yt===0&&wt(li)),K!==r&&(xt=N,K=Do()),N=K,N===r)if(N=W,e.charCodeAt(W)===34?(K=ns,W++):(K=r,yt===0&&wt(so)),K!==r){for(re=[],de=Nl();de!==r;)re.push(de),de=Nl();re!==r?(e.charCodeAt(W)===34?(de=ns,W++):(de=r,yt===0&&wt(so)),de!==r?(xt=N,K=bo(re),N=K):(W=N,N=r)):(W=N,N=r)}else W=N,N=r;return N}function os(){var N,K,re;if(N=W,K=[],re=Fo(),re!==r)for(;re!==r;)K.push(re),re=Fo();else K=r;return K!==r&&(xt=N,K=bo(K)),N=K,N}function Nl(){var N,K;return N=W,K=Zr(),K!==r&&(xt=N,K=ji(K)),N=K,N===r&&(N=W,K=Dh(),K!==r&&(xt=N,K=oo(K)),N=K,N===r&&(N=W,K=KA(),K!==r&&(xt=N,K=Po(K)),N=K,N===r&&(N=W,K=Df(),K!==r&&(xt=N,K=TA(K)),N=K))),N}function Fo(){var N,K;return N=W,K=Zr(),K!==r&&(xt=N,K=df(K)),N=K,N===r&&(N=W,K=Dh(),K!==r&&(xt=N,K=dh(K)),N=K,N===r&&(N=W,K=KA(),K!==r&&(xt=N,K=gh(K)),N=K,N===r&&(N=W,K=Py(),K!==r&&(xt=N,K=ao(K)),N=K,N===r&&(N=W,K=Sh(),K!==r&&(xt=N,K=TA(K)),N=K)))),N}function Sf(){var N,K,re;for(N=W,K=[],Gn.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(Ns));re!==r;)K.push(re),Gn.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(Ns));return K!==r&&(xt=N,K=lo(K)),N=K,N}function Df(){var N,K,re;if(N=W,K=[],re=Ol(),re===r&&(su.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(ou))),re!==r)for(;re!==r;)K.push(re),re=Ol(),re===r&&(su.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(ou)));else K=r;return K!==r&&(xt=N,K=lo(K)),N=K,N}function Ol(){var N,K,re;return N=W,e.substr(W,2)===au?(K=au,W+=2):(K=r,yt===0&&wt(FA)),K!==r&&(xt=N,K=NA()),N=K,N===r&&(N=W,e.charCodeAt(W)===92?(K=fa,W++):(K=r,yt===0&&wt(Aa)),K!==r?(OA.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(dr)),re!==r?(xt=N,K=xo(re),N=K):(W=N,N=r)):(W=N,N=r)),N}function En(){var N,K,re;for(N=W,K=[],re=No(),re===r&&(Gn.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(Ns)));re!==r;)K.push(re),re=No(),re===r&&(Gn.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(Ns)));return K!==r&&(xt=N,K=lo(K)),N=K,N}function No(){var N,K,re;return N=W,e.substr(W,2)===Ga?(K=Ga,W+=2):(K=r,yt===0&&wt(Ue)),K!==r&&(xt=N,K=wr()),N=K,N===r&&(N=W,e.substr(W,2)===gf?(K=gf,W+=2):(K=r,yt===0&&wt(LA)),K!==r&&(xt=N,K=MA()),N=K,N===r&&(N=W,e.charCodeAt(W)===92?(K=fa,W++):(K=r,yt===0&&wt(Aa)),K!==r?(lu.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(cu)),re!==r?(xt=N,K=lc(),N=K):(W=N,N=r)):(W=N,N=r),N===r&&(N=W,e.substr(W,2)===we?(K=we,W+=2):(K=r,yt===0&&wt(Nt)),K!==r&&(xt=N,K=cc()),N=K,N===r&&(N=W,e.substr(W,2)===Oi?(K=Oi,W+=2):(K=r,yt===0&&wt(co)),K!==r&&(xt=N,K=Tt()),N=K,N===r&&(N=W,e.substr(W,2)===Qn?(K=Qn,W+=2):(K=r,yt===0&&wt(pa)),K!==r&&(xt=N,K=Gi()),N=K,N===r&&(N=W,e.substr(W,2)===Li?(K=Li,W+=2):(K=r,yt===0&&wt(qa)),K!==r&&(xt=N,K=mn()),N=K,N===r&&(N=W,e.substr(W,2)===Xn?(K=Xn,W+=2):(K=r,yt===0&&wt(uu)),K!==r&&(xt=N,K=mh()),N=K,N===r&&(N=W,e.charCodeAt(W)===92?(K=fa,W++):(K=r,yt===0&&wt(Aa)),K!==r?(Wa.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(Ya)),re!==r?(xt=N,K=xo(re),N=K):(W=N,N=r)):(W=N,N=r),N===r&&(N=yu()))))))))),N}function yu(){var N,K,re,de,Je,pt,gr,vr,_n,yi,vs,zA;return N=W,e.charCodeAt(W)===92?(K=fa,W++):(K=r,yt===0&&wt(Aa)),K!==r?(re=ya(),re!==r?(xt=N,K=Va(re),N=K):(W=N,N=r)):(W=N,N=r),N===r&&(N=W,e.substr(W,2)===$e?(K=$e,W+=2):(K=r,yt===0&&wt(Ja)),K!==r?(re=W,de=W,Je=ya(),Je!==r?(pt=Ls(),pt!==r?(Je=[Je,pt],de=Je):(W=de,de=r)):(W=de,de=r),de===r&&(de=ya()),de!==r?re=e.substring(re,W):re=de,re!==r?(xt=N,K=Va(re),N=K):(W=N,N=r)):(W=N,N=r),N===r&&(N=W,e.substr(W,2)===mf?(K=mf,W+=2):(K=r,yt===0&&wt(uc)),K!==r?(re=W,de=W,Je=Ls(),Je!==r?(pt=Ls(),pt!==r?(gr=Ls(),gr!==r?(vr=Ls(),vr!==r?(Je=[Je,pt,gr,vr],de=Je):(W=de,de=r)):(W=de,de=r)):(W=de,de=r)):(W=de,de=r),de!==r?re=e.substring(re,W):re=de,re!==r?(xt=N,K=Va(re),N=K):(W=N,N=r)):(W=N,N=r),N===r&&(N=W,e.substr(W,2)===vn?(K=vn,W+=2):(K=r,yt===0&&wt(ha)),K!==r?(re=W,de=W,Je=Ls(),Je!==r?(pt=Ls(),pt!==r?(gr=Ls(),gr!==r?(vr=Ls(),vr!==r?(_n=Ls(),_n!==r?(yi=Ls(),yi!==r?(vs=Ls(),vs!==r?(zA=Ls(),zA!==r?(Je=[Je,pt,gr,vr,_n,yi,vs,zA],de=Je):(W=de,de=r)):(W=de,de=r)):(W=de,de=r)):(W=de,de=r)):(W=de,de=r)):(W=de,de=r)):(W=de,de=r)):(W=de,de=r),de!==r?re=e.substring(re,W):re=de,re!==r?(xt=N,K=UA(re),N=K):(W=N,N=r)):(W=N,N=r)))),N}function ya(){var N;return _A.test(e.charAt(W))?(N=e.charAt(W),W++):(N=r,yt===0&&wt(da)),N}function Ls(){var N;return kl.test(e.charAt(W))?(N=e.charAt(W),W++):(N=r,yt===0&&wt(Ut)),N}function Sh(){var N,K,re,de,Je;if(N=W,K=[],re=W,e.charCodeAt(W)===92?(de=fa,W++):(de=r,yt===0&&wt(Aa)),de!==r?(e.length>W?(Je=e.charAt(W),W++):(Je=r,yt===0&&wt(Rn)),Je!==r?(xt=re,de=xo(Je),re=de):(W=re,re=r)):(W=re,re=r),re===r&&(re=W,e.substr(W,2)===ga?(de=ga,W+=2):(de=r,yt===0&&wt(Ka)),de!==r&&(xt=re,de=is()),re=de,re===r&&(re=W,de=W,yt++,Je=xy(),yt--,Je===r?de=void 0:(W=de,de=r),de!==r?(e.length>W?(Je=e.charAt(W),W++):(Je=r,yt===0&&wt(Rn)),Je!==r?(xt=re,de=xo(Je),re=de):(W=re,re=r)):(W=re,re=r))),re!==r)for(;re!==r;)K.push(re),re=W,e.charCodeAt(W)===92?(de=fa,W++):(de=r,yt===0&&wt(Aa)),de!==r?(e.length>W?(Je=e.charAt(W),W++):(Je=r,yt===0&&wt(Rn)),Je!==r?(xt=re,de=xo(Je),re=de):(W=re,re=r)):(W=re,re=r),re===r&&(re=W,e.substr(W,2)===ga?(de=ga,W+=2):(de=r,yt===0&&wt(Ka)),de!==r&&(xt=re,de=is()),re=de,re===r&&(re=W,de=W,yt++,Je=xy(),yt--,Je===r?de=void 0:(W=de,de=r),de!==r?(e.length>W?(Je=e.charAt(W),W++):(Je=r,yt===0&&wt(Rn)),Je!==r?(xt=re,de=xo(Je),re=de):(W=re,re=r)):(W=re,re=r)));else K=r;return K!==r&&(xt=N,K=lo(K)),N=K,N}function JA(){var N,K,re,de,Je,pt;if(N=W,e.charCodeAt(W)===45?(K=fc,W++):(K=r,yt===0&&wt(fu)),K===r&&(e.charCodeAt(W)===43?(K=Ac,W++):(K=r,yt===0&&wt(za))),K===r&&(K=null),K!==r){if(re=[],ct.test(e.charAt(W))?(de=e.charAt(W),W++):(de=r,yt===0&&wt(Me)),de!==r)for(;de!==r;)re.push(de),ct.test(e.charAt(W))?(de=e.charAt(W),W++):(de=r,yt===0&&wt(Me));else re=r;if(re!==r)if(e.charCodeAt(W)===46?(de=Mi,W++):(de=r,yt===0&&wt(Bs)),de!==r){if(Je=[],ct.test(e.charAt(W))?(pt=e.charAt(W),W++):(pt=r,yt===0&&wt(Me)),pt!==r)for(;pt!==r;)Je.push(pt),ct.test(e.charAt(W))?(pt=e.charAt(W),W++):(pt=r,yt===0&&wt(Me));else Je=r;Je!==r?(xt=N,K=Ql(K,re,Je),N=K):(W=N,N=r)}else W=N,N=r;else W=N,N=r}else W=N,N=r;if(N===r){if(N=W,e.charCodeAt(W)===45?(K=fc,W++):(K=r,yt===0&&wt(fu)),K===r&&(e.charCodeAt(W)===43?(K=Ac,W++):(K=r,yt===0&&wt(za))),K===r&&(K=null),K!==r){if(re=[],ct.test(e.charAt(W))?(de=e.charAt(W),W++):(de=r,yt===0&&wt(Me)),de!==r)for(;de!==r;)re.push(de),ct.test(e.charAt(W))?(de=e.charAt(W),W++):(de=r,yt===0&&wt(Me));else re=r;re!==r?(xt=N,K=yf(K,re),N=K):(W=N,N=r)}else W=N,N=r;if(N===r&&(N=W,K=KA(),K!==r&&(xt=N,K=pc(K)),N=K,N===r&&(N=W,K=dc(),K!==r&&(xt=N,K=Bi(K)),N=K,N===r)))if(N=W,e.charCodeAt(W)===40?(K=Be,W++):(K=r,yt===0&&wt(me)),K!==r){for(re=[],de=kt();de!==r;)re.push(de),de=kt();if(re!==r)if(de=uo(),de!==r){for(Je=[],pt=kt();pt!==r;)Je.push(pt),pt=kt();Je!==r?(e.charCodeAt(W)===41?(pt=ce,W++):(pt=r,yt===0&&wt(Z)),pt!==r?(xt=N,K=Tn(de),N=K):(W=N,N=r)):(W=N,N=r)}else W=N,N=r;else W=N,N=r}else W=N,N=r}return N}function bf(){var N,K,re,de,Je,pt,gr,vr;if(N=W,K=JA(),K!==r){for(re=[],de=W,Je=[],pt=kt();pt!==r;)Je.push(pt),pt=kt();if(Je!==r)if(e.charCodeAt(W)===42?(pt=hc,W++):(pt=r,yt===0&&wt(Ke)),pt===r&&(e.charCodeAt(W)===47?(pt=ot,W++):(pt=r,yt===0&&wt(St))),pt!==r){for(gr=[],vr=kt();vr!==r;)gr.push(vr),vr=kt();gr!==r?(vr=JA(),vr!==r?(xt=de,Je=lr(K,pt,vr),de=Je):(W=de,de=r)):(W=de,de=r)}else W=de,de=r;else W=de,de=r;for(;de!==r;){for(re.push(de),de=W,Je=[],pt=kt();pt!==r;)Je.push(pt),pt=kt();if(Je!==r)if(e.charCodeAt(W)===42?(pt=hc,W++):(pt=r,yt===0&&wt(Ke)),pt===r&&(e.charCodeAt(W)===47?(pt=ot,W++):(pt=r,yt===0&&wt(St))),pt!==r){for(gr=[],vr=kt();vr!==r;)gr.push(vr),vr=kt();gr!==r?(vr=JA(),vr!==r?(xt=de,Je=lr(K,pt,vr),de=Je):(W=de,de=r)):(W=de,de=r)}else W=de,de=r;else W=de,de=r}re!==r?(xt=N,K=ee(K,re),N=K):(W=N,N=r)}else W=N,N=r;return N}function uo(){var N,K,re,de,Je,pt,gr,vr;if(N=W,K=bf(),K!==r){for(re=[],de=W,Je=[],pt=kt();pt!==r;)Je.push(pt),pt=kt();if(Je!==r)if(e.charCodeAt(W)===43?(pt=Ac,W++):(pt=r,yt===0&&wt(za)),pt===r&&(e.charCodeAt(W)===45?(pt=fc,W++):(pt=r,yt===0&&wt(fu))),pt!==r){for(gr=[],vr=kt();vr!==r;)gr.push(vr),vr=kt();gr!==r?(vr=bf(),vr!==r?(xt=de,Je=ye(K,pt,vr),de=Je):(W=de,de=r)):(W=de,de=r)}else W=de,de=r;else W=de,de=r;for(;de!==r;){for(re.push(de),de=W,Je=[],pt=kt();pt!==r;)Je.push(pt),pt=kt();if(Je!==r)if(e.charCodeAt(W)===43?(pt=Ac,W++):(pt=r,yt===0&&wt(za)),pt===r&&(e.charCodeAt(W)===45?(pt=fc,W++):(pt=r,yt===0&&wt(fu))),pt!==r){for(gr=[],vr=kt();vr!==r;)gr.push(vr),vr=kt();gr!==r?(vr=bf(),vr!==r?(xt=de,Je=ye(K,pt,vr),de=Je):(W=de,de=r)):(W=de,de=r)}else W=de,de=r;else W=de,de=r}re!==r?(xt=N,K=ee(K,re),N=K):(W=N,N=r)}else W=N,N=r;return N}function Zr(){var N,K,re,de,Je,pt;if(N=W,e.substr(W,3)===Oe?(K=Oe,W+=3):(K=r,yt===0&&wt(mt)),K!==r){for(re=[],de=kt();de!==r;)re.push(de),de=kt();if(re!==r)if(de=uo(),de!==r){for(Je=[],pt=kt();pt!==r;)Je.push(pt),pt=kt();Je!==r?(e.substr(W,2)===Et?(pt=Et,W+=2):(pt=r,yt===0&&wt(bt)),pt!==r?(xt=N,K=tr(de),N=K):(W=N,N=r)):(W=N,N=r)}else W=N,N=r;else W=N,N=r}else W=N,N=r;return N}function Dh(){var N,K,re,de;return N=W,e.substr(W,2)===pn?(K=pn,W+=2):(K=r,yt===0&&wt(ci)),K!==r?(re=ma(),re!==r?(e.charCodeAt(W)===41?(de=ce,W++):(de=r,yt===0&&wt(Z)),de!==r?(xt=N,K=qi(re),N=K):(W=N,N=r)):(W=N,N=r)):(W=N,N=r),N}function KA(){var N,K,re,de,Je,pt;return N=W,e.substr(W,2)===Fn?(K=Fn,W+=2):(K=r,yt===0&&wt(Xa)),K!==r?(re=dc(),re!==r?(e.substr(W,2)===Iy?(de=Iy,W+=2):(de=r,yt===0&&wt(q1)),de!==r?(Je=Os(),Je!==r?(e.charCodeAt(W)===125?(pt=_,W++):(pt=r,yt===0&&wt(tt)),pt!==r?(xt=N,K=ko(re,Je),N=K):(W=N,N=r)):(W=N,N=r)):(W=N,N=r)):(W=N,N=r)):(W=N,N=r),N===r&&(N=W,e.substr(W,2)===Fn?(K=Fn,W+=2):(K=r,yt===0&&wt(Xa)),K!==r?(re=dc(),re!==r?(e.substr(W,3)===Cy?(de=Cy,W+=3):(de=r,yt===0&&wt(yh)),de!==r?(xt=N,K=W1(re),N=K):(W=N,N=r)):(W=N,N=r)):(W=N,N=r),N===r&&(N=W,e.substr(W,2)===Fn?(K=Fn,W+=2):(K=r,yt===0&&wt(Xa)),K!==r?(re=dc(),re!==r?(e.substr(W,2)===Qo?(de=Qo,W+=2):(de=r,yt===0&&wt(Eh)),de!==r?(Je=Os(),Je!==r?(e.charCodeAt(W)===125?(pt=_,W++):(pt=r,yt===0&&wt(tt)),pt!==r?(xt=N,K=Ih(re,Je),N=K):(W=N,N=r)):(W=N,N=r)):(W=N,N=r)):(W=N,N=r)):(W=N,N=r),N===r&&(N=W,e.substr(W,2)===Fn?(K=Fn,W+=2):(K=r,yt===0&&wt(Xa)),K!==r?(re=dc(),re!==r?(e.substr(W,3)===Au?(de=Au,W+=3):(de=r,yt===0&&wt(Ch)),de!==r?(xt=N,K=Rd(re),N=K):(W=N,N=r)):(W=N,N=r)):(W=N,N=r),N===r&&(N=W,e.substr(W,2)===Fn?(K=Fn,W+=2):(K=r,yt===0&&wt(Xa)),K!==r?(re=dc(),re!==r?(e.charCodeAt(W)===125?(de=_,W++):(de=r,yt===0&&wt(tt)),de!==r?(xt=N,K=Td(re),N=K):(W=N,N=r)):(W=N,N=r)):(W=N,N=r),N===r&&(N=W,e.charCodeAt(W)===36?(K=Fd,W++):(K=r,yt===0&&wt(wy)),K!==r?(re=dc(),re!==r?(xt=N,K=Td(re),N=K):(W=N,N=r)):(W=N,N=r)))))),N}function Py(){var N,K,re;return N=W,K=Gd(),K!==r?(xt=W,re=Ef(K),re?re=void 0:re=r,re!==r?(xt=N,K=Ro(K),N=K):(W=N,N=r)):(W=N,N=r),N}function Gd(){var N,K,re,de,Je;if(N=W,K=[],re=W,de=W,yt++,Je=Ph(),yt--,Je===r?de=void 0:(W=de,de=r),de!==r?(e.length>W?(Je=e.charAt(W),W++):(Je=r,yt===0&&wt(Rn)),Je!==r?(xt=re,de=xo(Je),re=de):(W=re,re=r)):(W=re,re=r),re!==r)for(;re!==r;)K.push(re),re=W,de=W,yt++,Je=Ph(),yt--,Je===r?de=void 0:(W=de,de=r),de!==r?(e.length>W?(Je=e.charAt(W),W++):(Je=r,yt===0&&wt(Rn)),Je!==r?(xt=re,de=xo(Je),re=de):(W=re,re=r)):(W=re,re=r);else K=r;return K!==r&&(xt=N,K=lo(K)),N=K,N}function bh(){var N,K,re;if(N=W,K=[],Rl.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(wh)),re!==r)for(;re!==r;)K.push(re),Rl.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(wh));else K=r;return K!==r&&(xt=N,K=Nd()),N=K,N}function dc(){var N,K,re;if(N=W,K=[],Tl.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(Fl)),re!==r)for(;re!==r;)K.push(re),Tl.test(e.charAt(W))?(re=e.charAt(W),W++):(re=r,yt===0&&wt(Fl));else K=r;return K!==r&&(xt=N,K=Nd()),N=K,N}function xy(){var N;return By.test(e.charAt(W))?(N=e.charAt(W),W++):(N=r,yt===0&&wt(HA)),N}function Ph(){var N;return vy.test(e.charAt(W))?(N=e.charAt(W),W++):(N=r,yt===0&&wt(Sy)),N}function kt(){var N,K;if(N=[],jA.test(e.charAt(W))?(K=e.charAt(W),W++):(K=r,yt===0&&wt(GA)),K!==r)for(;K!==r;)N.push(K),jA.test(e.charAt(W))?(K=e.charAt(W),W++):(K=r,yt===0&&wt(GA));else N=r;return N}if(pu=a(),pu!==r&&W===e.length)return pu;throw pu!==r&&W!1}){try{return(0,U$.parse)(e,t)}catch(r){throw r.location&&(r.message=r.message.replace(/(\.)?$/,` (line ${r.location.start.line}, column ${r.location.start.column})$1`)),r}}function hE(e,{endSemicolon:t=!1}={}){return e.map(({command:r,type:s},a)=>`${fx(r)}${s===";"?a!==e.length-1||t?";":"":" &"}`).join(" ")}function fx(e){return`${dE(e.chain)}${e.then?` ${SU(e.then)}`:""}`}function SU(e){return`${e.type} ${fx(e.line)}`}function dE(e){return`${bU(e)}${e.then?` ${DU(e.then)}`:""}`}function DU(e){return`${e.type} ${dE(e.chain)}`}function bU(e){switch(e.type){case"command":return`${e.envs.length>0?`${e.envs.map(t=>cx(t)).join(" ")} `:""}${e.args.map(t=>PU(t)).join(" ")}`;case"subshell":return`(${hE(e.subshell)})${e.args.length>0?` ${e.args.map(t=>T2(t)).join(" ")}`:""}`;case"group":return`{ ${hE(e.group,{endSemicolon:!0})} }${e.args.length>0?` ${e.args.map(t=>T2(t)).join(" ")}`:""}`;case"envs":return e.envs.map(t=>cx(t)).join(" ");default:throw new Error(`Unsupported command type: "${e.type}"`)}}function cx(e){return`${e.name}=${e.args[0]?wg(e.args[0]):""}`}function PU(e){switch(e.type){case"redirection":return T2(e);case"argument":return wg(e);default:throw new Error(`Unsupported argument type: "${e.type}"`)}}function T2(e){return`${e.subtype} ${e.args.map(t=>wg(t)).join(" ")}`}function wg(e){return e.segments.map(t=>xU(t)).join("")}function xU(e){let t=(s,a)=>a?`"${s}"`:s,r=s=>s===""?"''":s.match(/[()}<>$|&;"'\n\t ]/)?s.match(/['\t\p{C}]/u)?s.match(/'/)?`"${s.replace(/["$\t\p{C}]/u,D6e)}"`:`$'${s.replace(/[\t\p{C}]/u,H$)}'`:`'${s}'`:s;switch(e.type){case"text":return r(e.text);case"glob":return e.pattern;case"shell":return t(`$(${hE(e.shell)})`,e.quoted);case"variable":return t(typeof e.defaultValue>"u"?typeof e.alternativeValue>"u"?`\${${e.name}}`:e.alternativeValue.length===0?`\${${e.name}:+}`:`\${${e.name}:+${e.alternativeValue.map(s=>wg(s)).join(" ")}}`:e.defaultValue.length===0?`\${${e.name}:-}`:`\${${e.name}:-${e.defaultValue.map(s=>wg(s)).join(" ")}}`,e.quoted);case"arithmetic":return`$(( ${Ax(e.arithmetic)} ))`;default:throw new Error(`Unsupported argument segment type: "${e.type}"`)}}function Ax(e){let t=a=>{switch(a){case"addition":return"+";case"subtraction":return"-";case"multiplication":return"*";case"division":return"/";default:throw new Error(`Can't extract operator from arithmetic expression of type "${a}"`)}},r=(a,n)=>n?`( ${a} )`:a,s=a=>r(Ax(a),!["number","variable"].includes(a.type));switch(e.type){case"number":return String(e.value);case"variable":return e.name;default:return`${s(e.left)} ${t(e.type)} ${s(e.right)}`}}var U$,_$,S6e,H$,D6e,j$=Xe(()=>{U$=et(M$());_$=new Map([["\f","\\f"],[` `,"\\n"],["\r","\\r"],[" ","\\t"],["\v","\\v"],["\0","\\0"]]),S6e=new Map([["\\","\\\\"],["$","\\$"],['"','\\"'],...Array.from(_$,([e,t])=>[e,`"$'${t}'"`])]),H$=e=>_$.get(e)??`\\x${e.charCodeAt(0).toString(16).padStart(2,"0")}`,D6e=e=>S6e.get(e)??`"$'${H$(e)}'"`});var q$=G((gPt,G$)=>{"use strict";function b6e(e,t){function r(){this.constructor=e}r.prototype=t.prototype,e.prototype=new r}function Bg(e,t,r,s){this.message=e,this.expected=t,this.found=r,this.location=s,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,Bg)}b6e(Bg,Error);Bg.buildMessage=function(e,t){var r={literal:function(h){return'"'+a(h.text)+'"'},class:function(h){var E="",C;for(C=0;C0){for(C=1,S=1;Cue&&(ue=V,ae=[]),ae.push(Me))}function tt(Me,P){return new Bg(Me,null,null,P)}function Ne(Me,P,w){return new Bg(Bg.buildMessage(Me,P),Me,P,w)}function ke(){var Me,P,w,b;return Me=V,P=be(),P!==r?(e.charCodeAt(V)===47?(w=n,V++):(w=r,ge===0&&_(c)),w!==r?(b=be(),b!==r?(te=Me,P=f(P,b),Me=P):(V=Me,Me=r)):(V=Me,Me=r)):(V=Me,Me=r),Me===r&&(Me=V,P=be(),P!==r&&(te=Me,P=p(P)),Me=P),Me}function be(){var Me,P,w,b;return Me=V,P=je(),P!==r?(e.charCodeAt(V)===64?(w=h,V++):(w=r,ge===0&&_(E)),w!==r?(b=ct(),b!==r?(te=Me,P=C(P,b),Me=P):(V=Me,Me=r)):(V=Me,Me=r)):(V=Me,Me=r),Me===r&&(Me=V,P=je(),P!==r&&(te=Me,P=S(P)),Me=P),Me}function je(){var Me,P,w,b,y;return Me=V,e.charCodeAt(V)===64?(P=h,V++):(P=r,ge===0&&_(E)),P!==r?(w=Re(),w!==r?(e.charCodeAt(V)===47?(b=n,V++):(b=r,ge===0&&_(c)),b!==r?(y=Re(),y!==r?(te=Me,P=x(),Me=P):(V=Me,Me=r)):(V=Me,Me=r)):(V=Me,Me=r)):(V=Me,Me=r),Me===r&&(Me=V,P=Re(),P!==r&&(te=Me,P=x()),Me=P),Me}function Re(){var Me,P,w;if(Me=V,P=[],I.test(e.charAt(V))?(w=e.charAt(V),V++):(w=r,ge===0&&_(T)),w!==r)for(;w!==r;)P.push(w),I.test(e.charAt(V))?(w=e.charAt(V),V++):(w=r,ge===0&&_(T));else P=r;return P!==r&&(te=Me,P=x()),Me=P,Me}function ct(){var Me,P,w;if(Me=V,P=[],O.test(e.charAt(V))?(w=e.charAt(V),V++):(w=r,ge===0&&_(U)),w!==r)for(;w!==r;)P.push(w),O.test(e.charAt(V))?(w=e.charAt(V),V++):(w=r,ge===0&&_(U));else P=r;return P!==r&&(te=Me,P=x()),Me=P,Me}if(Ae=a(),Ae!==r&&V===e.length)return Ae;throw Ae!==r&&V{W$=et(q$())});var Sg=G((yPt,vg)=>{"use strict";function V$(e){return typeof e>"u"||e===null}function x6e(e){return typeof e=="object"&&e!==null}function k6e(e){return Array.isArray(e)?e:V$(e)?[]:[e]}function Q6e(e,t){var r,s,a,n;if(t)for(n=Object.keys(t),r=0,s=n.length;r{"use strict";function F2(e,t){Error.call(this),this.name="YAMLException",this.reason=e,this.mark=t,this.message=(this.reason||"(unknown reason)")+(this.mark?" "+this.mark.toString():""),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error().stack||""}F2.prototype=Object.create(Error.prototype);F2.prototype.constructor=F2;F2.prototype.toString=function(t){var r=this.name+": ";return r+=this.reason||"(unknown reason)",!t&&this.mark&&(r+=" "+this.mark.toString()),r};J$.exports=F2});var X$=G((IPt,z$)=>{"use strict";var K$=Sg();function kU(e,t,r,s,a){this.name=e,this.buffer=t,this.position=r,this.line=s,this.column=a}kU.prototype.getSnippet=function(t,r){var s,a,n,c,f;if(!this.buffer)return null;for(t=t||4,r=r||75,s="",a=this.position;a>0&&`\0\r \x85\u2028\u2029`.indexOf(this.buffer.charAt(a-1))===-1;)if(a-=1,this.position-a>r/2-1){s=" ... ",a+=5;break}for(n="",c=this.position;cr/2-1){n=" ... ",c-=5;break}return f=this.buffer.slice(a,c),K$.repeat(" ",t)+s+f+n+` `+K$.repeat(" ",t+this.position-a+s.length)+"^"};kU.prototype.toString=function(t){var r,s="";return this.name&&(s+='in "'+this.name+'" '),s+="at line "+(this.line+1)+", column "+(this.column+1),t||(r=this.getSnippet(),r&&(s+=`: `+r)),s};z$.exports=kU});var Ps=G((CPt,$$)=>{"use strict";var Z$=gE(),F6e=["kind","resolve","construct","instanceOf","predicate","represent","defaultStyle","styleAliases"],N6e=["scalar","sequence","mapping"];function O6e(e){var t={};return e!==null&&Object.keys(e).forEach(function(r){e[r].forEach(function(s){t[String(s)]=r})}),t}function L6e(e,t){if(t=t||{},Object.keys(t).forEach(function(r){if(F6e.indexOf(r)===-1)throw new Z$('Unknown option "'+r+'" is met in definition of "'+e+'" YAML type.')}),this.tag=e,this.kind=t.kind||null,this.resolve=t.resolve||function(){return!0},this.construct=t.construct||function(r){return r},this.instanceOf=t.instanceOf||null,this.predicate=t.predicate||null,this.represent=t.represent||null,this.defaultStyle=t.defaultStyle||null,this.styleAliases=O6e(t.styleAliases||null),N6e.indexOf(this.kind)===-1)throw new Z$('Unknown kind "'+this.kind+'" is specified for "'+e+'" YAML type.')}$$.exports=L6e});var Dg=G((wPt,tee)=>{"use strict";var eee=Sg(),dx=gE(),M6e=Ps();function QU(e,t,r){var s=[];return e.include.forEach(function(a){r=QU(a,t,r)}),e[t].forEach(function(a){r.forEach(function(n,c){n.tag===a.tag&&n.kind===a.kind&&s.push(c)}),r.push(a)}),r.filter(function(a,n){return s.indexOf(n)===-1})}function U6e(){var e={scalar:{},sequence:{},mapping:{},fallback:{}},t,r;function s(a){e[a.kind][a.tag]=e.fallback[a.tag]=a}for(t=0,r=arguments.length;t{"use strict";var _6e=Ps();ree.exports=new _6e("tag:yaml.org,2002:str",{kind:"scalar",construct:function(e){return e!==null?e:""}})});var see=G((vPt,iee)=>{"use strict";var H6e=Ps();iee.exports=new H6e("tag:yaml.org,2002:seq",{kind:"sequence",construct:function(e){return e!==null?e:[]}})});var aee=G((SPt,oee)=>{"use strict";var j6e=Ps();oee.exports=new j6e("tag:yaml.org,2002:map",{kind:"mapping",construct:function(e){return e!==null?e:{}}})});var gx=G((DPt,lee)=>{"use strict";var G6e=Dg();lee.exports=new G6e({explicit:[nee(),see(),aee()]})});var uee=G((bPt,cee)=>{"use strict";var q6e=Ps();function W6e(e){if(e===null)return!0;var t=e.length;return t===1&&e==="~"||t===4&&(e==="null"||e==="Null"||e==="NULL")}function Y6e(){return null}function V6e(e){return e===null}cee.exports=new q6e("tag:yaml.org,2002:null",{kind:"scalar",resolve:W6e,construct:Y6e,predicate:V6e,represent:{canonical:function(){return"~"},lowercase:function(){return"null"},uppercase:function(){return"NULL"},camelcase:function(){return"Null"}},defaultStyle:"lowercase"})});var Aee=G((PPt,fee)=>{"use strict";var J6e=Ps();function K6e(e){if(e===null)return!1;var t=e.length;return t===4&&(e==="true"||e==="True"||e==="TRUE")||t===5&&(e==="false"||e==="False"||e==="FALSE")}function z6e(e){return e==="true"||e==="True"||e==="TRUE"}function X6e(e){return Object.prototype.toString.call(e)==="[object Boolean]"}fee.exports=new J6e("tag:yaml.org,2002:bool",{kind:"scalar",resolve:K6e,construct:z6e,predicate:X6e,represent:{lowercase:function(e){return e?"true":"false"},uppercase:function(e){return e?"TRUE":"FALSE"},camelcase:function(e){return e?"True":"False"}},defaultStyle:"lowercase"})});var hee=G((xPt,pee)=>{"use strict";var Z6e=Sg(),$6e=Ps();function eGe(e){return 48<=e&&e<=57||65<=e&&e<=70||97<=e&&e<=102}function tGe(e){return 48<=e&&e<=55}function rGe(e){return 48<=e&&e<=57}function nGe(e){if(e===null)return!1;var t=e.length,r=0,s=!1,a;if(!t)return!1;if(a=e[r],(a==="-"||a==="+")&&(a=e[++r]),a==="0"){if(r+1===t)return!0;if(a=e[++r],a==="b"){for(r++;r=0?"0b"+e.toString(2):"-0b"+e.toString(2).slice(1)},octal:function(e){return e>=0?"0"+e.toString(8):"-0"+e.toString(8).slice(1)},decimal:function(e){return e.toString(10)},hexadecimal:function(e){return e>=0?"0x"+e.toString(16).toUpperCase():"-0x"+e.toString(16).toUpperCase().slice(1)}},defaultStyle:"decimal",styleAliases:{binary:[2,"bin"],octal:[8,"oct"],decimal:[10,"dec"],hexadecimal:[16,"hex"]}})});var mee=G((kPt,gee)=>{"use strict";var dee=Sg(),oGe=Ps(),aGe=new RegExp("^(?:[-+]?(?:0|[1-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]*|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$");function lGe(e){return!(e===null||!aGe.test(e)||e[e.length-1]==="_")}function cGe(e){var t,r,s,a;return t=e.replace(/_/g,"").toLowerCase(),r=t[0]==="-"?-1:1,a=[],"+-".indexOf(t[0])>=0&&(t=t.slice(1)),t===".inf"?r===1?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY:t===".nan"?NaN:t.indexOf(":")>=0?(t.split(":").forEach(function(n){a.unshift(parseFloat(n,10))}),t=0,s=1,a.forEach(function(n){t+=n*s,s*=60}),r*t):r*parseFloat(t,10)}var uGe=/^[-+]?[0-9]+e/;function fGe(e,t){var r;if(isNaN(e))switch(t){case"lowercase":return".nan";case"uppercase":return".NAN";case"camelcase":return".NaN"}else if(Number.POSITIVE_INFINITY===e)switch(t){case"lowercase":return".inf";case"uppercase":return".INF";case"camelcase":return".Inf"}else if(Number.NEGATIVE_INFINITY===e)switch(t){case"lowercase":return"-.inf";case"uppercase":return"-.INF";case"camelcase":return"-.Inf"}else if(dee.isNegativeZero(e))return"-0.0";return r=e.toString(10),uGe.test(r)?r.replace("e",".e"):r}function AGe(e){return Object.prototype.toString.call(e)==="[object Number]"&&(e%1!==0||dee.isNegativeZero(e))}gee.exports=new oGe("tag:yaml.org,2002:float",{kind:"scalar",resolve:lGe,construct:cGe,predicate:AGe,represent:fGe,defaultStyle:"lowercase"})});var RU=G((QPt,yee)=>{"use strict";var pGe=Dg();yee.exports=new pGe({include:[gx()],implicit:[uee(),Aee(),hee(),mee()]})});var TU=G((RPt,Eee)=>{"use strict";var hGe=Dg();Eee.exports=new hGe({include:[RU()]})});var Bee=G((TPt,wee)=>{"use strict";var dGe=Ps(),Iee=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$"),Cee=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$");function gGe(e){return e===null?!1:Iee.exec(e)!==null||Cee.exec(e)!==null}function mGe(e){var t,r,s,a,n,c,f,p=0,h=null,E,C,S;if(t=Iee.exec(e),t===null&&(t=Cee.exec(e)),t===null)throw new Error("Date resolve error");if(r=+t[1],s=+t[2]-1,a=+t[3],!t[4])return new Date(Date.UTC(r,s,a));if(n=+t[4],c=+t[5],f=+t[6],t[7]){for(p=t[7].slice(0,3);p.length<3;)p+="0";p=+p}return t[9]&&(E=+t[10],C=+(t[11]||0),h=(E*60+C)*6e4,t[9]==="-"&&(h=-h)),S=new Date(Date.UTC(r,s,a,n,c,f,p)),h&&S.setTime(S.getTime()-h),S}function yGe(e){return e.toISOString()}wee.exports=new dGe("tag:yaml.org,2002:timestamp",{kind:"scalar",resolve:gGe,construct:mGe,instanceOf:Date,represent:yGe})});var See=G((FPt,vee)=>{"use strict";var EGe=Ps();function IGe(e){return e==="<<"||e===null}vee.exports=new EGe("tag:yaml.org,2002:merge",{kind:"scalar",resolve:IGe})});var Pee=G((NPt,bee)=>{"use strict";var bg;try{Dee=Ie,bg=Dee("buffer").Buffer}catch{}var Dee,CGe=Ps(),FU=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/= \r`;function wGe(e){if(e===null)return!1;var t,r,s=0,a=e.length,n=FU;for(r=0;r64)){if(t<0)return!1;s+=6}return s%8===0}function BGe(e){var t,r,s=e.replace(/[\r\n=]/g,""),a=s.length,n=FU,c=0,f=[];for(t=0;t>16&255),f.push(c>>8&255),f.push(c&255)),c=c<<6|n.indexOf(s.charAt(t));return r=a%4*6,r===0?(f.push(c>>16&255),f.push(c>>8&255),f.push(c&255)):r===18?(f.push(c>>10&255),f.push(c>>2&255)):r===12&&f.push(c>>4&255),bg?bg.from?bg.from(f):new bg(f):f}function vGe(e){var t="",r=0,s,a,n=e.length,c=FU;for(s=0;s>18&63],t+=c[r>>12&63],t+=c[r>>6&63],t+=c[r&63]),r=(r<<8)+e[s];return a=n%3,a===0?(t+=c[r>>18&63],t+=c[r>>12&63],t+=c[r>>6&63],t+=c[r&63]):a===2?(t+=c[r>>10&63],t+=c[r>>4&63],t+=c[r<<2&63],t+=c[64]):a===1&&(t+=c[r>>2&63],t+=c[r<<4&63],t+=c[64],t+=c[64]),t}function SGe(e){return bg&&bg.isBuffer(e)}bee.exports=new CGe("tag:yaml.org,2002:binary",{kind:"scalar",resolve:wGe,construct:BGe,predicate:SGe,represent:vGe})});var kee=G((LPt,xee)=>{"use strict";var DGe=Ps(),bGe=Object.prototype.hasOwnProperty,PGe=Object.prototype.toString;function xGe(e){if(e===null)return!0;var t=[],r,s,a,n,c,f=e;for(r=0,s=f.length;r{"use strict";var QGe=Ps(),RGe=Object.prototype.toString;function TGe(e){if(e===null)return!0;var t,r,s,a,n,c=e;for(n=new Array(c.length),t=0,r=c.length;t{"use strict";var NGe=Ps(),OGe=Object.prototype.hasOwnProperty;function LGe(e){if(e===null)return!0;var t,r=e;for(t in r)if(OGe.call(r,t)&&r[t]!==null)return!1;return!0}function MGe(e){return e!==null?e:{}}Tee.exports=new NGe("tag:yaml.org,2002:set",{kind:"mapping",resolve:LGe,construct:MGe})});var yE=G((_Pt,Nee)=>{"use strict";var UGe=Dg();Nee.exports=new UGe({include:[TU()],implicit:[Bee(),See()],explicit:[Pee(),kee(),Ree(),Fee()]})});var Lee=G((HPt,Oee)=>{"use strict";var _Ge=Ps();function HGe(){return!0}function jGe(){}function GGe(){return""}function qGe(e){return typeof e>"u"}Oee.exports=new _Ge("tag:yaml.org,2002:js/undefined",{kind:"scalar",resolve:HGe,construct:jGe,predicate:qGe,represent:GGe})});var Uee=G((jPt,Mee)=>{"use strict";var WGe=Ps();function YGe(e){if(e===null||e.length===0)return!1;var t=e,r=/\/([gim]*)$/.exec(e),s="";return!(t[0]==="/"&&(r&&(s=r[1]),s.length>3||t[t.length-s.length-1]!=="/"))}function VGe(e){var t=e,r=/\/([gim]*)$/.exec(e),s="";return t[0]==="/"&&(r&&(s=r[1]),t=t.slice(1,t.length-s.length-1)),new RegExp(t,s)}function JGe(e){var t="/"+e.source+"/";return e.global&&(t+="g"),e.multiline&&(t+="m"),e.ignoreCase&&(t+="i"),t}function KGe(e){return Object.prototype.toString.call(e)==="[object RegExp]"}Mee.exports=new WGe("tag:yaml.org,2002:js/regexp",{kind:"scalar",resolve:YGe,construct:VGe,predicate:KGe,represent:JGe})});var jee=G((GPt,Hee)=>{"use strict";var mx;try{_ee=Ie,mx=_ee("esprima")}catch{typeof window<"u"&&(mx=window.esprima)}var _ee,zGe=Ps();function XGe(e){if(e===null)return!1;try{var t="("+e+")",r=mx.parse(t,{range:!0});return!(r.type!=="Program"||r.body.length!==1||r.body[0].type!=="ExpressionStatement"||r.body[0].expression.type!=="ArrowFunctionExpression"&&r.body[0].expression.type!=="FunctionExpression")}catch{return!1}}function ZGe(e){var t="("+e+")",r=mx.parse(t,{range:!0}),s=[],a;if(r.type!=="Program"||r.body.length!==1||r.body[0].type!=="ExpressionStatement"||r.body[0].expression.type!=="ArrowFunctionExpression"&&r.body[0].expression.type!=="FunctionExpression")throw new Error("Failed to resolve function");return r.body[0].expression.params.forEach(function(n){s.push(n.name)}),a=r.body[0].expression.body.range,r.body[0].expression.body.type==="BlockStatement"?new Function(s,t.slice(a[0]+1,a[1]-1)):new Function(s,"return "+t.slice(a[0],a[1]))}function $Ge(e){return e.toString()}function e5e(e){return Object.prototype.toString.call(e)==="[object Function]"}Hee.exports=new zGe("tag:yaml.org,2002:js/function",{kind:"scalar",resolve:XGe,construct:ZGe,predicate:e5e,represent:$Ge})});var N2=G((WPt,qee)=>{"use strict";var Gee=Dg();qee.exports=Gee.DEFAULT=new Gee({include:[yE()],explicit:[Lee(),Uee(),jee()]})});var cte=G((YPt,O2)=>{"use strict";var wp=Sg(),Xee=gE(),t5e=X$(),Zee=yE(),r5e=N2(),n0=Object.prototype.hasOwnProperty,yx=1,$ee=2,ete=3,Ex=4,NU=1,n5e=2,Wee=3,i5e=/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/,s5e=/[\x85\u2028\u2029]/,o5e=/[,\[\]\{\}]/,tte=/^(?:!|!!|![a-z\-]+!)$/i,rte=/^(?:!|[^,\[\]\{\}])(?:%[0-9a-f]{2}|[0-9a-z\-#;\/\?:@&=\+\$,_\.!~\*'\(\)\[\]])*$/i;function Yee(e){return Object.prototype.toString.call(e)}function Wf(e){return e===10||e===13}function xg(e){return e===9||e===32}function ul(e){return e===9||e===32||e===10||e===13}function EE(e){return e===44||e===91||e===93||e===123||e===125}function a5e(e){var t;return 48<=e&&e<=57?e-48:(t=e|32,97<=t&&t<=102?t-97+10:-1)}function l5e(e){return e===120?2:e===117?4:e===85?8:0}function c5e(e){return 48<=e&&e<=57?e-48:-1}function Vee(e){return e===48?"\0":e===97?"\x07":e===98?"\b":e===116||e===9?" ":e===110?` `:e===118?"\v":e===102?"\f":e===114?"\r":e===101?"\x1B":e===32?" ":e===34?'"':e===47?"/":e===92?"\\":e===78?"\x85":e===95?"\xA0":e===76?"\u2028":e===80?"\u2029":""}function u5e(e){return e<=65535?String.fromCharCode(e):String.fromCharCode((e-65536>>10)+55296,(e-65536&1023)+56320)}var nte=new Array(256),ite=new Array(256);for(Pg=0;Pg<256;Pg++)nte[Pg]=Vee(Pg)?1:0,ite[Pg]=Vee(Pg);var Pg;function f5e(e,t){this.input=e,this.filename=t.filename||null,this.schema=t.schema||r5e,this.onWarning=t.onWarning||null,this.legacy=t.legacy||!1,this.json=t.json||!1,this.listener=t.listener||null,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=e.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.documents=[]}function ste(e,t){return new Xee(t,new t5e(e.filename,e.input,e.position,e.line,e.position-e.lineStart))}function Fr(e,t){throw ste(e,t)}function Ix(e,t){e.onWarning&&e.onWarning.call(null,ste(e,t))}var Jee={YAML:function(t,r,s){var a,n,c;t.version!==null&&Fr(t,"duplication of %YAML directive"),s.length!==1&&Fr(t,"YAML directive accepts exactly one argument"),a=/^([0-9]+)\.([0-9]+)$/.exec(s[0]),a===null&&Fr(t,"ill-formed argument of the YAML directive"),n=parseInt(a[1],10),c=parseInt(a[2],10),n!==1&&Fr(t,"unacceptable YAML version of the document"),t.version=s[0],t.checkLineBreaks=c<2,c!==1&&c!==2&&Ix(t,"unsupported YAML version of the document")},TAG:function(t,r,s){var a,n;s.length!==2&&Fr(t,"TAG directive accepts exactly two arguments"),a=s[0],n=s[1],tte.test(a)||Fr(t,"ill-formed tag handle (first argument) of the TAG directive"),n0.call(t.tagMap,a)&&Fr(t,'there is a previously declared suffix for "'+a+'" tag handle'),rte.test(n)||Fr(t,"ill-formed tag prefix (second argument) of the TAG directive"),t.tagMap[a]=n}};function r0(e,t,r,s){var a,n,c,f;if(t1&&(e.result+=wp.repeat(` `,t-1))}function A5e(e,t,r){var s,a,n,c,f,p,h,E,C=e.kind,S=e.result,x;if(x=e.input.charCodeAt(e.position),ul(x)||EE(x)||x===35||x===38||x===42||x===33||x===124||x===62||x===39||x===34||x===37||x===64||x===96||(x===63||x===45)&&(a=e.input.charCodeAt(e.position+1),ul(a)||r&&EE(a)))return!1;for(e.kind="scalar",e.result="",n=c=e.position,f=!1;x!==0;){if(x===58){if(a=e.input.charCodeAt(e.position+1),ul(a)||r&&EE(a))break}else if(x===35){if(s=e.input.charCodeAt(e.position-1),ul(s))break}else{if(e.position===e.lineStart&&Cx(e)||r&&EE(x))break;if(Wf(x))if(p=e.line,h=e.lineStart,E=e.lineIndent,ls(e,!1,-1),e.lineIndent>=t){f=!0,x=e.input.charCodeAt(e.position);continue}else{e.position=c,e.line=p,e.lineStart=h,e.lineIndent=E;break}}f&&(r0(e,n,c,!1),LU(e,e.line-p),n=c=e.position,f=!1),xg(x)||(c=e.position+1),x=e.input.charCodeAt(++e.position)}return r0(e,n,c,!1),e.result?!0:(e.kind=C,e.result=S,!1)}function p5e(e,t){var r,s,a;if(r=e.input.charCodeAt(e.position),r!==39)return!1;for(e.kind="scalar",e.result="",e.position++,s=a=e.position;(r=e.input.charCodeAt(e.position))!==0;)if(r===39)if(r0(e,s,e.position,!0),r=e.input.charCodeAt(++e.position),r===39)s=e.position,e.position++,a=e.position;else return!0;else Wf(r)?(r0(e,s,a,!0),LU(e,ls(e,!1,t)),s=a=e.position):e.position===e.lineStart&&Cx(e)?Fr(e,"unexpected end of the document within a single quoted scalar"):(e.position++,a=e.position);Fr(e,"unexpected end of the stream within a single quoted scalar")}function h5e(e,t){var r,s,a,n,c,f;if(f=e.input.charCodeAt(e.position),f!==34)return!1;for(e.kind="scalar",e.result="",e.position++,r=s=e.position;(f=e.input.charCodeAt(e.position))!==0;){if(f===34)return r0(e,r,e.position,!0),e.position++,!0;if(f===92){if(r0(e,r,e.position,!0),f=e.input.charCodeAt(++e.position),Wf(f))ls(e,!1,t);else if(f<256&&nte[f])e.result+=ite[f],e.position++;else if((c=l5e(f))>0){for(a=c,n=0;a>0;a--)f=e.input.charCodeAt(++e.position),(c=a5e(f))>=0?n=(n<<4)+c:Fr(e,"expected hexadecimal character");e.result+=u5e(n),e.position++}else Fr(e,"unknown escape sequence");r=s=e.position}else Wf(f)?(r0(e,r,s,!0),LU(e,ls(e,!1,t)),r=s=e.position):e.position===e.lineStart&&Cx(e)?Fr(e,"unexpected end of the document within a double quoted scalar"):(e.position++,s=e.position)}Fr(e,"unexpected end of the stream within a double quoted scalar")}function d5e(e,t){var r=!0,s,a=e.tag,n,c=e.anchor,f,p,h,E,C,S={},x,I,T,O;if(O=e.input.charCodeAt(e.position),O===91)p=93,C=!1,n=[];else if(O===123)p=125,C=!0,n={};else return!1;for(e.anchor!==null&&(e.anchorMap[e.anchor]=n),O=e.input.charCodeAt(++e.position);O!==0;){if(ls(e,!0,t),O=e.input.charCodeAt(e.position),O===p)return e.position++,e.tag=a,e.anchor=c,e.kind=C?"mapping":"sequence",e.result=n,!0;r||Fr(e,"missed comma between flow collection entries"),I=x=T=null,h=E=!1,O===63&&(f=e.input.charCodeAt(e.position+1),ul(f)&&(h=E=!0,e.position++,ls(e,!0,t))),s=e.line,CE(e,t,yx,!1,!0),I=e.tag,x=e.result,ls(e,!0,t),O=e.input.charCodeAt(e.position),(E||e.line===s)&&O===58&&(h=!0,O=e.input.charCodeAt(++e.position),ls(e,!0,t),CE(e,t,yx,!1,!0),T=e.result),C?IE(e,n,S,I,x,T):h?n.push(IE(e,null,S,I,x,T)):n.push(x),ls(e,!0,t),O=e.input.charCodeAt(e.position),O===44?(r=!0,O=e.input.charCodeAt(++e.position)):r=!1}Fr(e,"unexpected end of the stream within a flow collection")}function g5e(e,t){var r,s,a=NU,n=!1,c=!1,f=t,p=0,h=!1,E,C;if(C=e.input.charCodeAt(e.position),C===124)s=!1;else if(C===62)s=!0;else return!1;for(e.kind="scalar",e.result="";C!==0;)if(C=e.input.charCodeAt(++e.position),C===43||C===45)NU===a?a=C===43?Wee:n5e:Fr(e,"repeat of a chomping mode identifier");else if((E=c5e(C))>=0)E===0?Fr(e,"bad explicit indentation width of a block scalar; it cannot be less than one"):c?Fr(e,"repeat of an indentation width identifier"):(f=t+E-1,c=!0);else break;if(xg(C)){do C=e.input.charCodeAt(++e.position);while(xg(C));if(C===35)do C=e.input.charCodeAt(++e.position);while(!Wf(C)&&C!==0)}for(;C!==0;){for(OU(e),e.lineIndent=0,C=e.input.charCodeAt(e.position);(!c||e.lineIndentf&&(f=e.lineIndent),Wf(C)){p++;continue}if(e.lineIndentt)&&p!==0)Fr(e,"bad indentation of a sequence entry");else if(e.lineIndentt)&&(CE(e,t,Ex,!0,a)&&(I?S=e.result:x=e.result),I||(IE(e,h,E,C,S,x,n,c),C=S=x=null),ls(e,!0,-1),O=e.input.charCodeAt(e.position)),e.lineIndent>t&&O!==0)Fr(e,"bad indentation of a mapping entry");else if(e.lineIndentt?p=1:e.lineIndent===t?p=0:e.lineIndentt?p=1:e.lineIndent===t?p=0:e.lineIndent tag; it should be "scalar", not "'+e.kind+'"'),C=0,S=e.implicitTypes.length;C tag; it should be "'+x.kind+'", not "'+e.kind+'"'),x.resolve(e.result)?(e.result=x.construct(e.result),e.anchor!==null&&(e.anchorMap[e.anchor]=e.result)):Fr(e,"cannot resolve a node with !<"+e.tag+"> explicit tag")):Fr(e,"unknown tag !<"+e.tag+">");return e.listener!==null&&e.listener("close",e),e.tag!==null||e.anchor!==null||E}function C5e(e){var t=e.position,r,s,a,n=!1,c;for(e.version=null,e.checkLineBreaks=e.legacy,e.tagMap={},e.anchorMap={};(c=e.input.charCodeAt(e.position))!==0&&(ls(e,!0,-1),c=e.input.charCodeAt(e.position),!(e.lineIndent>0||c!==37));){for(n=!0,c=e.input.charCodeAt(++e.position),r=e.position;c!==0&&!ul(c);)c=e.input.charCodeAt(++e.position);for(s=e.input.slice(r,e.position),a=[],s.length<1&&Fr(e,"directive name must not be less than one character in length");c!==0;){for(;xg(c);)c=e.input.charCodeAt(++e.position);if(c===35){do c=e.input.charCodeAt(++e.position);while(c!==0&&!Wf(c));break}if(Wf(c))break;for(r=e.position;c!==0&&!ul(c);)c=e.input.charCodeAt(++e.position);a.push(e.input.slice(r,e.position))}c!==0&&OU(e),n0.call(Jee,s)?Jee[s](e,s,a):Ix(e,'unknown document directive "'+s+'"')}if(ls(e,!0,-1),e.lineIndent===0&&e.input.charCodeAt(e.position)===45&&e.input.charCodeAt(e.position+1)===45&&e.input.charCodeAt(e.position+2)===45?(e.position+=3,ls(e,!0,-1)):n&&Fr(e,"directives end mark is expected"),CE(e,e.lineIndent-1,Ex,!1,!0),ls(e,!0,-1),e.checkLineBreaks&&s5e.test(e.input.slice(t,e.position))&&Ix(e,"non-ASCII line breaks are interpreted as content"),e.documents.push(e.result),e.position===e.lineStart&&Cx(e)){e.input.charCodeAt(e.position)===46&&(e.position+=3,ls(e,!0,-1));return}if(e.position"u"&&(r=t,t=null);var s=ote(e,r);if(typeof t!="function")return s;for(var a=0,n=s.length;a"u"&&(r=t,t=null),ate(e,t,wp.extend({schema:Zee},r))}function B5e(e,t){return lte(e,wp.extend({schema:Zee},t))}O2.exports.loadAll=ate;O2.exports.load=lte;O2.exports.safeLoadAll=w5e;O2.exports.safeLoad=B5e});var Rte=G((VPt,HU)=>{"use strict";var M2=Sg(),U2=gE(),v5e=N2(),S5e=yE(),mte=Object.prototype.toString,yte=Object.prototype.hasOwnProperty,D5e=9,L2=10,b5e=13,P5e=32,x5e=33,k5e=34,Ete=35,Q5e=37,R5e=38,T5e=39,F5e=42,Ite=44,N5e=45,Cte=58,O5e=61,L5e=62,M5e=63,U5e=64,wte=91,Bte=93,_5e=96,vte=123,H5e=124,Ste=125,Yo={};Yo[0]="\\0";Yo[7]="\\a";Yo[8]="\\b";Yo[9]="\\t";Yo[10]="\\n";Yo[11]="\\v";Yo[12]="\\f";Yo[13]="\\r";Yo[27]="\\e";Yo[34]='\\"';Yo[92]="\\\\";Yo[133]="\\N";Yo[160]="\\_";Yo[8232]="\\L";Yo[8233]="\\P";var j5e=["y","Y","yes","Yes","YES","on","On","ON","n","N","no","No","NO","off","Off","OFF"];function G5e(e,t){var r,s,a,n,c,f,p;if(t===null)return{};for(r={},s=Object.keys(t),a=0,n=s.length;a0?e.charCodeAt(n-1):null,S=S&&Ate(c,f)}else{for(n=0;ns&&e[C+1]!==" ",C=n);else if(!wE(c))return wx;f=n>0?e.charCodeAt(n-1):null,S=S&&Ate(c,f)}h=h||E&&n-C-1>s&&e[C+1]!==" "}return!p&&!h?S&&!a(e)?bte:Pte:r>9&&Dte(e)?wx:h?kte:xte}function K5e(e,t,r,s){e.dump=function(){if(t.length===0)return"''";if(!e.noCompatMode&&j5e.indexOf(t)!==-1)return"'"+t+"'";var a=e.indent*Math.max(1,r),n=e.lineWidth===-1?-1:Math.max(Math.min(e.lineWidth,40),e.lineWidth-a),c=s||e.flowLevel>-1&&r>=e.flowLevel;function f(p){return W5e(e,p)}switch(J5e(t,c,e.indent,n,f)){case bte:return t;case Pte:return"'"+t.replace(/'/g,"''")+"'";case xte:return"|"+pte(t,e.indent)+hte(fte(t,a));case kte:return">"+pte(t,e.indent)+hte(fte(z5e(t,n),a));case wx:return'"'+X5e(t,n)+'"';default:throw new U2("impossible error: invalid scalar style")}}()}function pte(e,t){var r=Dte(e)?String(t):"",s=e[e.length-1]===` `,a=s&&(e[e.length-2]===` `||e===` `),n=a?"+":s?"":"-";return r+n+` `}function hte(e){return e[e.length-1]===` `?e.slice(0,-1):e}function z5e(e,t){for(var r=/(\n+)([^\n]*)/g,s=function(){var h=e.indexOf(` `);return h=h!==-1?h:e.length,r.lastIndex=h,dte(e.slice(0,h),t)}(),a=e[0]===` `||e[0]===" ",n,c;c=r.exec(e);){var f=c[1],p=c[2];n=p[0]===" ",s+=f+(!a&&!n&&p!==""?` `:"")+dte(p,t),a=n}return s}function dte(e,t){if(e===""||e[0]===" ")return e;for(var r=/ [^ ]/g,s,a=0,n,c=0,f=0,p="";s=r.exec(e);)f=s.index,f-a>t&&(n=c>a?c:f,p+=` `+e.slice(a,n),a=n+1),c=f;return p+=` `,e.length-a>t&&c>a?p+=e.slice(a,c)+` `+e.slice(c+1):p+=e.slice(a),p.slice(1)}function X5e(e){for(var t="",r,s,a,n=0;n=55296&&r<=56319&&(s=e.charCodeAt(n+1),s>=56320&&s<=57343)){t+=ute((r-55296)*1024+s-56320+65536),n++;continue}a=Yo[r],t+=!a&&wE(r)?e[n]:a||ute(r)}return t}function Z5e(e,t,r){var s="",a=e.tag,n,c;for(n=0,c=r.length;n1024&&(E+="? "),E+=e.dump+(e.condenseFlow?'"':"")+":"+(e.condenseFlow?"":" "),kg(e,t,h,!1,!1)&&(E+=e.dump,s+=E));e.tag=a,e.dump="{"+s+"}"}function t9e(e,t,r,s){var a="",n=e.tag,c=Object.keys(r),f,p,h,E,C,S;if(e.sortKeys===!0)c.sort();else if(typeof e.sortKeys=="function")c.sort(e.sortKeys);else if(e.sortKeys)throw new U2("sortKeys must be a boolean or a function");for(f=0,p=c.length;f1024,C&&(e.dump&&L2===e.dump.charCodeAt(0)?S+="?":S+="? "),S+=e.dump,C&&(S+=MU(e,t)),kg(e,t+1,E,!0,C)&&(e.dump&&L2===e.dump.charCodeAt(0)?S+=":":S+=": ",S+=e.dump,a+=S));e.tag=n,e.dump=a||"{}"}function gte(e,t,r){var s,a,n,c,f,p;for(a=r?e.explicitTypes:e.implicitTypes,n=0,c=a.length;n tag resolver accepts not "'+p+'" style');e.dump=s}return!0}return!1}function kg(e,t,r,s,a,n){e.tag=null,e.dump=r,gte(e,r,!1)||gte(e,r,!0);var c=mte.call(e.dump);s&&(s=e.flowLevel<0||e.flowLevel>t);var f=c==="[object Object]"||c==="[object Array]",p,h;if(f&&(p=e.duplicates.indexOf(r),h=p!==-1),(e.tag!==null&&e.tag!=="?"||h||e.indent!==2&&t>0)&&(a=!1),h&&e.usedDuplicates[p])e.dump="*ref_"+p;else{if(f&&h&&!e.usedDuplicates[p]&&(e.usedDuplicates[p]=!0),c==="[object Object]")s&&Object.keys(e.dump).length!==0?(t9e(e,t,e.dump,a),h&&(e.dump="&ref_"+p+e.dump)):(e9e(e,t,e.dump),h&&(e.dump="&ref_"+p+" "+e.dump));else if(c==="[object Array]"){var E=e.noArrayIndent&&t>0?t-1:t;s&&e.dump.length!==0?($5e(e,E,e.dump,a),h&&(e.dump="&ref_"+p+e.dump)):(Z5e(e,E,e.dump),h&&(e.dump="&ref_"+p+" "+e.dump))}else if(c==="[object String]")e.tag!=="?"&&K5e(e,e.dump,t,n);else{if(e.skipInvalid)return!1;throw new U2("unacceptable kind of an object to dump "+c)}e.tag!==null&&e.tag!=="?"&&(e.dump="!<"+e.tag+"> "+e.dump)}return!0}function r9e(e,t){var r=[],s=[],a,n;for(UU(e,r,s),a=0,n=s.length;a{"use strict";var Bx=cte(),Tte=Rte();function vx(e){return function(){throw new Error("Function "+e+" is deprecated and cannot be used.")}}Wi.exports.Type=Ps();Wi.exports.Schema=Dg();Wi.exports.FAILSAFE_SCHEMA=gx();Wi.exports.JSON_SCHEMA=RU();Wi.exports.CORE_SCHEMA=TU();Wi.exports.DEFAULT_SAFE_SCHEMA=yE();Wi.exports.DEFAULT_FULL_SCHEMA=N2();Wi.exports.load=Bx.load;Wi.exports.loadAll=Bx.loadAll;Wi.exports.safeLoad=Bx.safeLoad;Wi.exports.safeLoadAll=Bx.safeLoadAll;Wi.exports.dump=Tte.dump;Wi.exports.safeDump=Tte.safeDump;Wi.exports.YAMLException=gE();Wi.exports.MINIMAL_SCHEMA=gx();Wi.exports.SAFE_SCHEMA=yE();Wi.exports.DEFAULT_SCHEMA=N2();Wi.exports.scan=vx("scan");Wi.exports.parse=vx("parse");Wi.exports.compose=vx("compose");Wi.exports.addConstructor=vx("addConstructor")});var Ote=G((KPt,Nte)=>{"use strict";var i9e=Fte();Nte.exports=i9e});var Mte=G((zPt,Lte)=>{"use strict";function s9e(e,t){function r(){this.constructor=e}r.prototype=t.prototype,e.prototype=new r}function Qg(e,t,r,s){this.message=e,this.expected=t,this.found=r,this.location=s,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,Qg)}s9e(Qg,Error);Qg.buildMessage=function(e,t){var r={literal:function(h){return'"'+a(h.text)+'"'},class:function(h){var E="",C;for(C=0;C0){for(C=1,S=1;C({[mt]:Oe})))},ue=function(ee){return ee},ae=function(ee){return ee},ge=Wa("correct indentation"),Ae=" ",Ce=mn(" ",!1),Ee=function(ee){return ee.length===lr*St},d=function(ee){return ee.length===(lr+1)*St},Se=function(){return lr++,!0},Be=function(){return lr--,!0},me=function(){return pa()},ce=Wa("pseudostring"),Z=/^[^\r\n\t ?:,\][{}#&*!|>'"%@`\-]/,De=Xn(["\r",` `," "," ","?",":",",","]","[","{","}","#","&","*","!","|",">","'",'"',"%","@","`","-"],!0,!1),Qe=/^[^\r\n\t ,\][{}:#"']/,st=Xn(["\r",` `," "," ",",","]","[","{","}",":","#",'"',"'"],!0,!1),_=function(){return pa().replace(/^ *| *$/g,"")},tt="--",Ne=mn("--",!1),ke=/^[a-zA-Z\/0-9]/,be=Xn([["a","z"],["A","Z"],"/",["0","9"]],!1,!1),je=/^[^\r\n\t :,]/,Re=Xn(["\r",` `," "," ",":",","],!0,!1),ct="null",Me=mn("null",!1),P=function(){return null},w="true",b=mn("true",!1),y=function(){return!0},F="false",z=mn("false",!1),X=function(){return!1},$=Wa("string"),se='"',xe=mn('"',!1),Fe=function(){return""},ut=function(ee){return ee},Ct=function(ee){return ee.join("")},qt=/^[^"\\\0-\x1F\x7F]/,ir=Xn(['"',"\\",["\0",""],"\x7F"],!0,!1),Pt='\\"',gn=mn('\\"',!1),Pr=function(){return'"'},Cr="\\\\",Or=mn("\\\\",!1),on=function(){return"\\"},li="\\/",Do=mn("\\/",!1),ns=function(){return"/"},so="\\b",bo=mn("\\b",!1),ji=function(){return"\b"},oo="\\f",Po=mn("\\f",!1),TA=function(){return"\f"},df="\\n",dh=mn("\\n",!1),gh=function(){return` `},ao="\\r",Gn=mn("\\r",!1),Ns=function(){return"\r"},lo="\\t",su=mn("\\t",!1),ou=function(){return" "},au="\\u",FA=mn("\\u",!1),NA=function(ee,ye,Oe,mt){return String.fromCharCode(parseInt(`0x${ee}${ye}${Oe}${mt}`))},fa=/^[0-9a-fA-F]/,Aa=Xn([["0","9"],["a","f"],["A","F"]],!1,!1),OA=Wa("blank space"),dr=/^[ \t]/,xo=Xn([" "," "],!1,!1),Ga=Wa("white space"),Ue=/^[ \t\n\r]/,wr=Xn([" "," ",` `,"\r"],!1,!1),gf=`\r `,LA=mn(`\r `,!1),MA=` `,lu=mn(` `,!1),cu="\r",lc=mn("\r",!1),we=0,Nt=0,cc=[{line:1,column:1}],Oi=0,co=[],Tt=0,Qn;if("startRule"in t){if(!(t.startRule in s))throw new Error(`Can't start parsing from rule "`+t.startRule+'".');a=s[t.startRule]}function pa(){return e.substring(Nt,we)}function Gi(){return Va(Nt,we)}function Li(ee,ye){throw ye=ye!==void 0?ye:Va(Nt,we),mf([Wa(ee)],e.substring(Nt,we),ye)}function qa(ee,ye){throw ye=ye!==void 0?ye:Va(Nt,we),Ja(ee,ye)}function mn(ee,ye){return{type:"literal",text:ee,ignoreCase:ye}}function Xn(ee,ye,Oe){return{type:"class",parts:ee,inverted:ye,ignoreCase:Oe}}function uu(){return{type:"any"}}function mh(){return{type:"end"}}function Wa(ee){return{type:"other",description:ee}}function Ya(ee){var ye=cc[ee],Oe;if(ye)return ye;for(Oe=ee-1;!cc[Oe];)Oe--;for(ye=cc[Oe],ye={line:ye.line,column:ye.column};OeOi&&(Oi=we,co=[]),co.push(ee))}function Ja(ee,ye){return new Qg(ee,null,null,ye)}function mf(ee,ye,Oe){return new Qg(Qg.buildMessage(ee,ye),ee,ye,Oe)}function uc(){var ee;return ee=UA(),ee}function vn(){var ee,ye,Oe;for(ee=we,ye=[],Oe=ha();Oe!==r;)ye.push(Oe),Oe=ha();return ye!==r&&(Nt=ee,ye=n(ye)),ee=ye,ee}function ha(){var ee,ye,Oe,mt,Et;return ee=we,ye=kl(),ye!==r?(e.charCodeAt(we)===45?(Oe=c,we++):(Oe=r,Tt===0&&$e(f)),Oe!==r?(mt=Tn(),mt!==r?(Et=da(),Et!==r?(Nt=ee,ye=p(Et),ee=ye):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r),ee}function UA(){var ee,ye,Oe;for(ee=we,ye=[],Oe=_A();Oe!==r;)ye.push(Oe),Oe=_A();return ye!==r&&(Nt=ee,ye=h(ye)),ee=ye,ee}function _A(){var ee,ye,Oe,mt,Et,bt,tr,pn,ci;if(ee=we,ye=Tn(),ye===r&&(ye=null),ye!==r){if(Oe=we,e.charCodeAt(we)===35?(mt=E,we++):(mt=r,Tt===0&&$e(C)),mt!==r){if(Et=[],bt=we,tr=we,Tt++,pn=ot(),Tt--,pn===r?tr=void 0:(we=tr,tr=r),tr!==r?(e.length>we?(pn=e.charAt(we),we++):(pn=r,Tt===0&&$e(S)),pn!==r?(tr=[tr,pn],bt=tr):(we=bt,bt=r)):(we=bt,bt=r),bt!==r)for(;bt!==r;)Et.push(bt),bt=we,tr=we,Tt++,pn=ot(),Tt--,pn===r?tr=void 0:(we=tr,tr=r),tr!==r?(e.length>we?(pn=e.charAt(we),we++):(pn=r,Tt===0&&$e(S)),pn!==r?(tr=[tr,pn],bt=tr):(we=bt,bt=r)):(we=bt,bt=r);else Et=r;Et!==r?(mt=[mt,Et],Oe=mt):(we=Oe,Oe=r)}else we=Oe,Oe=r;if(Oe===r&&(Oe=null),Oe!==r){if(mt=[],Et=Ke(),Et!==r)for(;Et!==r;)mt.push(Et),Et=Ke();else mt=r;mt!==r?(Nt=ee,ye=x(),ee=ye):(we=ee,ee=r)}else we=ee,ee=r}else we=ee,ee=r;if(ee===r&&(ee=we,ye=kl(),ye!==r?(Oe=Ka(),Oe!==r?(mt=Tn(),mt===r&&(mt=null),mt!==r?(e.charCodeAt(we)===58?(Et=I,we++):(Et=r,Tt===0&&$e(T)),Et!==r?(bt=Tn(),bt===r&&(bt=null),bt!==r?(tr=da(),tr!==r?(Nt=ee,ye=O(Oe,tr),ee=ye):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r),ee===r&&(ee=we,ye=kl(),ye!==r?(Oe=is(),Oe!==r?(mt=Tn(),mt===r&&(mt=null),mt!==r?(e.charCodeAt(we)===58?(Et=I,we++):(Et=r,Tt===0&&$e(T)),Et!==r?(bt=Tn(),bt===r&&(bt=null),bt!==r?(tr=da(),tr!==r?(Nt=ee,ye=O(Oe,tr),ee=ye):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r),ee===r))){if(ee=we,ye=kl(),ye!==r)if(Oe=is(),Oe!==r)if(mt=Tn(),mt!==r)if(Et=fu(),Et!==r){if(bt=[],tr=Ke(),tr!==r)for(;tr!==r;)bt.push(tr),tr=Ke();else bt=r;bt!==r?(Nt=ee,ye=O(Oe,Et),ee=ye):(we=ee,ee=r)}else we=ee,ee=r;else we=ee,ee=r;else we=ee,ee=r;else we=ee,ee=r;if(ee===r)if(ee=we,ye=kl(),ye!==r)if(Oe=is(),Oe!==r){if(mt=[],Et=we,bt=Tn(),bt===r&&(bt=null),bt!==r?(e.charCodeAt(we)===44?(tr=U,we++):(tr=r,Tt===0&&$e(V)),tr!==r?(pn=Tn(),pn===r&&(pn=null),pn!==r?(ci=is(),ci!==r?(Nt=Et,bt=te(Oe,ci),Et=bt):(we=Et,Et=r)):(we=Et,Et=r)):(we=Et,Et=r)):(we=Et,Et=r),Et!==r)for(;Et!==r;)mt.push(Et),Et=we,bt=Tn(),bt===r&&(bt=null),bt!==r?(e.charCodeAt(we)===44?(tr=U,we++):(tr=r,Tt===0&&$e(V)),tr!==r?(pn=Tn(),pn===r&&(pn=null),pn!==r?(ci=is(),ci!==r?(Nt=Et,bt=te(Oe,ci),Et=bt):(we=Et,Et=r)):(we=Et,Et=r)):(we=Et,Et=r)):(we=Et,Et=r);else mt=r;mt!==r?(Et=Tn(),Et===r&&(Et=null),Et!==r?(e.charCodeAt(we)===58?(bt=I,we++):(bt=r,Tt===0&&$e(T)),bt!==r?(tr=Tn(),tr===r&&(tr=null),tr!==r?(pn=da(),pn!==r?(Nt=ee,ye=ie(Oe,mt,pn),ee=ye):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)}else we=ee,ee=r;else we=ee,ee=r}return ee}function da(){var ee,ye,Oe,mt,Et,bt,tr;if(ee=we,ye=we,Tt++,Oe=we,mt=ot(),mt!==r?(Et=Ut(),Et!==r?(e.charCodeAt(we)===45?(bt=c,we++):(bt=r,Tt===0&&$e(f)),bt!==r?(tr=Tn(),tr!==r?(mt=[mt,Et,bt,tr],Oe=mt):(we=Oe,Oe=r)):(we=Oe,Oe=r)):(we=Oe,Oe=r)):(we=Oe,Oe=r),Tt--,Oe!==r?(we=ye,ye=void 0):ye=r,ye!==r?(Oe=Ke(),Oe!==r?(mt=Rn(),mt!==r?(Et=vn(),Et!==r?(bt=ga(),bt!==r?(Nt=ee,ye=ue(Et),ee=ye):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r),ee===r&&(ee=we,ye=ot(),ye!==r?(Oe=Rn(),Oe!==r?(mt=UA(),mt!==r?(Et=ga(),Et!==r?(Nt=ee,ye=ue(mt),ee=ye):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r),ee===r))if(ee=we,ye=fc(),ye!==r){if(Oe=[],mt=Ke(),mt!==r)for(;mt!==r;)Oe.push(mt),mt=Ke();else Oe=r;Oe!==r?(Nt=ee,ye=ae(ye),ee=ye):(we=ee,ee=r)}else we=ee,ee=r;return ee}function kl(){var ee,ye,Oe;for(Tt++,ee=we,ye=[],e.charCodeAt(we)===32?(Oe=Ae,we++):(Oe=r,Tt===0&&$e(Ce));Oe!==r;)ye.push(Oe),e.charCodeAt(we)===32?(Oe=Ae,we++):(Oe=r,Tt===0&&$e(Ce));return ye!==r?(Nt=we,Oe=Ee(ye),Oe?Oe=void 0:Oe=r,Oe!==r?(ye=[ye,Oe],ee=ye):(we=ee,ee=r)):(we=ee,ee=r),Tt--,ee===r&&(ye=r,Tt===0&&$e(ge)),ee}function Ut(){var ee,ye,Oe;for(ee=we,ye=[],e.charCodeAt(we)===32?(Oe=Ae,we++):(Oe=r,Tt===0&&$e(Ce));Oe!==r;)ye.push(Oe),e.charCodeAt(we)===32?(Oe=Ae,we++):(Oe=r,Tt===0&&$e(Ce));return ye!==r?(Nt=we,Oe=d(ye),Oe?Oe=void 0:Oe=r,Oe!==r?(ye=[ye,Oe],ee=ye):(we=ee,ee=r)):(we=ee,ee=r),ee}function Rn(){var ee;return Nt=we,ee=Se(),ee?ee=void 0:ee=r,ee}function ga(){var ee;return Nt=we,ee=Be(),ee?ee=void 0:ee=r,ee}function Ka(){var ee;return ee=Ql(),ee===r&&(ee=Ac()),ee}function is(){var ee,ye,Oe;if(ee=Ql(),ee===r){if(ee=we,ye=[],Oe=za(),Oe!==r)for(;Oe!==r;)ye.push(Oe),Oe=za();else ye=r;ye!==r&&(Nt=ee,ye=me()),ee=ye}return ee}function fc(){var ee;return ee=Mi(),ee===r&&(ee=Bs(),ee===r&&(ee=Ql(),ee===r&&(ee=Ac()))),ee}function fu(){var ee;return ee=Mi(),ee===r&&(ee=Ql(),ee===r&&(ee=za())),ee}function Ac(){var ee,ye,Oe,mt,Et,bt;if(Tt++,ee=we,Z.test(e.charAt(we))?(ye=e.charAt(we),we++):(ye=r,Tt===0&&$e(De)),ye!==r){for(Oe=[],mt=we,Et=Tn(),Et===r&&(Et=null),Et!==r?(Qe.test(e.charAt(we))?(bt=e.charAt(we),we++):(bt=r,Tt===0&&$e(st)),bt!==r?(Et=[Et,bt],mt=Et):(we=mt,mt=r)):(we=mt,mt=r);mt!==r;)Oe.push(mt),mt=we,Et=Tn(),Et===r&&(Et=null),Et!==r?(Qe.test(e.charAt(we))?(bt=e.charAt(we),we++):(bt=r,Tt===0&&$e(st)),bt!==r?(Et=[Et,bt],mt=Et):(we=mt,mt=r)):(we=mt,mt=r);Oe!==r?(Nt=ee,ye=_(),ee=ye):(we=ee,ee=r)}else we=ee,ee=r;return Tt--,ee===r&&(ye=r,Tt===0&&$e(ce)),ee}function za(){var ee,ye,Oe,mt,Et;if(ee=we,e.substr(we,2)===tt?(ye=tt,we+=2):(ye=r,Tt===0&&$e(Ne)),ye===r&&(ye=null),ye!==r)if(ke.test(e.charAt(we))?(Oe=e.charAt(we),we++):(Oe=r,Tt===0&&$e(be)),Oe!==r){for(mt=[],je.test(e.charAt(we))?(Et=e.charAt(we),we++):(Et=r,Tt===0&&$e(Re));Et!==r;)mt.push(Et),je.test(e.charAt(we))?(Et=e.charAt(we),we++):(Et=r,Tt===0&&$e(Re));mt!==r?(Nt=ee,ye=_(),ee=ye):(we=ee,ee=r)}else we=ee,ee=r;else we=ee,ee=r;return ee}function Mi(){var ee,ye;return ee=we,e.substr(we,4)===ct?(ye=ct,we+=4):(ye=r,Tt===0&&$e(Me)),ye!==r&&(Nt=ee,ye=P()),ee=ye,ee}function Bs(){var ee,ye;return ee=we,e.substr(we,4)===w?(ye=w,we+=4):(ye=r,Tt===0&&$e(b)),ye!==r&&(Nt=ee,ye=y()),ee=ye,ee===r&&(ee=we,e.substr(we,5)===F?(ye=F,we+=5):(ye=r,Tt===0&&$e(z)),ye!==r&&(Nt=ee,ye=X()),ee=ye),ee}function Ql(){var ee,ye,Oe,mt;return Tt++,ee=we,e.charCodeAt(we)===34?(ye=se,we++):(ye=r,Tt===0&&$e(xe)),ye!==r?(e.charCodeAt(we)===34?(Oe=se,we++):(Oe=r,Tt===0&&$e(xe)),Oe!==r?(Nt=ee,ye=Fe(),ee=ye):(we=ee,ee=r)):(we=ee,ee=r),ee===r&&(ee=we,e.charCodeAt(we)===34?(ye=se,we++):(ye=r,Tt===0&&$e(xe)),ye!==r?(Oe=yf(),Oe!==r?(e.charCodeAt(we)===34?(mt=se,we++):(mt=r,Tt===0&&$e(xe)),mt!==r?(Nt=ee,ye=ut(Oe),ee=ye):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)),Tt--,ee===r&&(ye=r,Tt===0&&$e($)),ee}function yf(){var ee,ye,Oe;if(ee=we,ye=[],Oe=pc(),Oe!==r)for(;Oe!==r;)ye.push(Oe),Oe=pc();else ye=r;return ye!==r&&(Nt=ee,ye=Ct(ye)),ee=ye,ee}function pc(){var ee,ye,Oe,mt,Et,bt;return qt.test(e.charAt(we))?(ee=e.charAt(we),we++):(ee=r,Tt===0&&$e(ir)),ee===r&&(ee=we,e.substr(we,2)===Pt?(ye=Pt,we+=2):(ye=r,Tt===0&&$e(gn)),ye!==r&&(Nt=ee,ye=Pr()),ee=ye,ee===r&&(ee=we,e.substr(we,2)===Cr?(ye=Cr,we+=2):(ye=r,Tt===0&&$e(Or)),ye!==r&&(Nt=ee,ye=on()),ee=ye,ee===r&&(ee=we,e.substr(we,2)===li?(ye=li,we+=2):(ye=r,Tt===0&&$e(Do)),ye!==r&&(Nt=ee,ye=ns()),ee=ye,ee===r&&(ee=we,e.substr(we,2)===so?(ye=so,we+=2):(ye=r,Tt===0&&$e(bo)),ye!==r&&(Nt=ee,ye=ji()),ee=ye,ee===r&&(ee=we,e.substr(we,2)===oo?(ye=oo,we+=2):(ye=r,Tt===0&&$e(Po)),ye!==r&&(Nt=ee,ye=TA()),ee=ye,ee===r&&(ee=we,e.substr(we,2)===df?(ye=df,we+=2):(ye=r,Tt===0&&$e(dh)),ye!==r&&(Nt=ee,ye=gh()),ee=ye,ee===r&&(ee=we,e.substr(we,2)===ao?(ye=ao,we+=2):(ye=r,Tt===0&&$e(Gn)),ye!==r&&(Nt=ee,ye=Ns()),ee=ye,ee===r&&(ee=we,e.substr(we,2)===lo?(ye=lo,we+=2):(ye=r,Tt===0&&$e(su)),ye!==r&&(Nt=ee,ye=ou()),ee=ye,ee===r&&(ee=we,e.substr(we,2)===au?(ye=au,we+=2):(ye=r,Tt===0&&$e(FA)),ye!==r?(Oe=Bi(),Oe!==r?(mt=Bi(),mt!==r?(Et=Bi(),Et!==r?(bt=Bi(),bt!==r?(Nt=ee,ye=NA(Oe,mt,Et,bt),ee=ye):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)):(we=ee,ee=r)))))))))),ee}function Bi(){var ee;return fa.test(e.charAt(we))?(ee=e.charAt(we),we++):(ee=r,Tt===0&&$e(Aa)),ee}function Tn(){var ee,ye;if(Tt++,ee=[],dr.test(e.charAt(we))?(ye=e.charAt(we),we++):(ye=r,Tt===0&&$e(xo)),ye!==r)for(;ye!==r;)ee.push(ye),dr.test(e.charAt(we))?(ye=e.charAt(we),we++):(ye=r,Tt===0&&$e(xo));else ee=r;return Tt--,ee===r&&(ye=r,Tt===0&&$e(OA)),ee}function hc(){var ee,ye;if(Tt++,ee=[],Ue.test(e.charAt(we))?(ye=e.charAt(we),we++):(ye=r,Tt===0&&$e(wr)),ye!==r)for(;ye!==r;)ee.push(ye),Ue.test(e.charAt(we))?(ye=e.charAt(we),we++):(ye=r,Tt===0&&$e(wr));else ee=r;return Tt--,ee===r&&(ye=r,Tt===0&&$e(Ga)),ee}function Ke(){var ee,ye,Oe,mt,Et,bt;if(ee=we,ye=ot(),ye!==r){for(Oe=[],mt=we,Et=Tn(),Et===r&&(Et=null),Et!==r?(bt=ot(),bt!==r?(Et=[Et,bt],mt=Et):(we=mt,mt=r)):(we=mt,mt=r);mt!==r;)Oe.push(mt),mt=we,Et=Tn(),Et===r&&(Et=null),Et!==r?(bt=ot(),bt!==r?(Et=[Et,bt],mt=Et):(we=mt,mt=r)):(we=mt,mt=r);Oe!==r?(ye=[ye,Oe],ee=ye):(we=ee,ee=r)}else we=ee,ee=r;return ee}function ot(){var ee;return e.substr(we,2)===gf?(ee=gf,we+=2):(ee=r,Tt===0&&$e(LA)),ee===r&&(e.charCodeAt(we)===10?(ee=MA,we++):(ee=r,Tt===0&&$e(lu)),ee===r&&(e.charCodeAt(we)===13?(ee=cu,we++):(ee=r,Tt===0&&$e(lc)))),ee}let St=2,lr=0;if(Qn=a(),Qn!==r&&we===e.length)return Qn;throw Qn!==r&&we"u"?!0:typeof e=="object"&&e!==null&&!Array.isArray(e)?Object.keys(e).every(t=>jte(e[t])):!1}function jU(e,t,r){if(e===null)return`null `;if(typeof e=="number"||typeof e=="boolean")return`${e.toString()} `;if(typeof e=="string")return`${_te(e)} `;if(Array.isArray(e)){if(e.length===0)return`[] `;let s=" ".repeat(t);return` ${e.map(n=>`${s}- ${jU(n,t+1,!1)}`).join("")}`}if(typeof e=="object"&&e){let[s,a]=e instanceof Sx?[e.data,!1]:[e,!0],n=" ".repeat(t),c=Object.keys(s);a&&c.sort((p,h)=>{let E=Ute.indexOf(p),C=Ute.indexOf(h);return E===-1&&C===-1?ph?1:0:E!==-1&&C===-1?-1:E===-1&&C!==-1?1:E-C});let f=c.filter(p=>!jte(s[p])).map((p,h)=>{let E=s[p],C=_te(p),S=jU(E,t+1,!0),x=h>0||r?n:"",I=C.length>1024?`? ${C} ${x}:`:`${C}:`,T=S.startsWith(` `)?S:` ${S}`;return`${x}${I}${T}`}).join(t===0?` `:"")||` `;return r?` ${f}`:`${f}`}throw new Error(`Unsupported value type (${e})`)}function fl(e){try{let t=jU(e,0,!1);return t!==` `?t:""}catch(t){throw t.location&&(t.message=t.message.replace(/(\.)?$/,` (line ${t.location.start.line}, column ${t.location.start.column})$1`)),t}}function l9e(e){return e.endsWith(` `)||(e+=` `),(0,Hte.parse)(e)}function u9e(e){if(c9e.test(e))return l9e(e);let t=(0,Dx.safeLoad)(e,{schema:Dx.FAILSAFE_SCHEMA,json:!0});if(t==null)return{};if(typeof t!="object")throw new Error(`Expected an indexed object, got a ${typeof t} instead. Does your file follow Yaml's rules?`);if(Array.isArray(t))throw new Error("Expected an indexed object, got an array instead. Does your file follow Yaml's rules?");return t}function cs(e){return u9e(e)}var Dx,Hte,a9e,Ute,Sx,c9e,Gte=Xe(()=>{Dx=et(Ote()),Hte=et(Mte()),a9e=/^(?![-?:,\][{}#&*!|>'"%@` \t\r\n]).([ \t]*(?![,\][{}:# \t\r\n]).)*$/,Ute=["__metadata","version","resolution","dependencies","peerDependencies","dependenciesMeta","peerDependenciesMeta","binaries"],Sx=class{constructor(t){this.data=t}};fl.PreserveOrdering=Sx;c9e=/^(#.*(\r?\n))*?#\s+yarn\s+lockfile\s+v1\r?\n/i});var _2={};Vt(_2,{parseResolution:()=>px,parseShell:()=>ux,parseSyml:()=>cs,stringifyArgument:()=>PU,stringifyArgumentSegment:()=>xU,stringifyArithmeticExpression:()=>Ax,stringifyCommand:()=>bU,stringifyCommandChain:()=>dE,stringifyCommandChainThen:()=>DU,stringifyCommandLine:()=>fx,stringifyCommandLineThen:()=>SU,stringifyEnvSegment:()=>cx,stringifyRedirectArgument:()=>T2,stringifyResolution:()=>hx,stringifyShell:()=>hE,stringifyShellLine:()=>hE,stringifySyml:()=>fl,stringifyValueArgument:()=>wg});var vc=Xe(()=>{j$();Y$();Gte()});var Wte=G((txt,GU)=>{"use strict";var f9e=e=>{let t=!1,r=!1,s=!1;for(let a=0;a{if(!(typeof e=="string"||Array.isArray(e)))throw new TypeError("Expected the input to be `string | string[]`");t=Object.assign({pascalCase:!1},t);let r=a=>t.pascalCase?a.charAt(0).toUpperCase()+a.slice(1):a;return Array.isArray(e)?e=e.map(a=>a.trim()).filter(a=>a.length).join("-"):e=e.trim(),e.length===0?"":e.length===1?t.pascalCase?e.toUpperCase():e.toLowerCase():(e!==e.toLowerCase()&&(e=f9e(e)),e=e.replace(/^[_.\- ]+/,"").toLowerCase().replace(/[_.\- ]+(\w|$)/g,(a,n)=>n.toUpperCase()).replace(/\d+(\w|$)/g,a=>a.toUpperCase()),r(e))};GU.exports=qte;GU.exports.default=qte});var Yte=G((rxt,A9e)=>{A9e.exports=[{name:"Agola CI",constant:"AGOLA",env:"AGOLA_GIT_REF",pr:"AGOLA_PULL_REQUEST_ID"},{name:"Appcircle",constant:"APPCIRCLE",env:"AC_APPCIRCLE"},{name:"AppVeyor",constant:"APPVEYOR",env:"APPVEYOR",pr:"APPVEYOR_PULL_REQUEST_NUMBER"},{name:"AWS CodeBuild",constant:"CODEBUILD",env:"CODEBUILD_BUILD_ARN"},{name:"Azure Pipelines",constant:"AZURE_PIPELINES",env:"TF_BUILD",pr:{BUILD_REASON:"PullRequest"}},{name:"Bamboo",constant:"BAMBOO",env:"bamboo_planKey"},{name:"Bitbucket Pipelines",constant:"BITBUCKET",env:"BITBUCKET_COMMIT",pr:"BITBUCKET_PR_ID"},{name:"Bitrise",constant:"BITRISE",env:"BITRISE_IO",pr:"BITRISE_PULL_REQUEST"},{name:"Buddy",constant:"BUDDY",env:"BUDDY_WORKSPACE_ID",pr:"BUDDY_EXECUTION_PULL_REQUEST_ID"},{name:"Buildkite",constant:"BUILDKITE",env:"BUILDKITE",pr:{env:"BUILDKITE_PULL_REQUEST",ne:"false"}},{name:"CircleCI",constant:"CIRCLE",env:"CIRCLECI",pr:"CIRCLE_PULL_REQUEST"},{name:"Cirrus CI",constant:"CIRRUS",env:"CIRRUS_CI",pr:"CIRRUS_PR"},{name:"Codefresh",constant:"CODEFRESH",env:"CF_BUILD_ID",pr:{any:["CF_PULL_REQUEST_NUMBER","CF_PULL_REQUEST_ID"]}},{name:"Codemagic",constant:"CODEMAGIC",env:"CM_BUILD_ID",pr:"CM_PULL_REQUEST"},{name:"Codeship",constant:"CODESHIP",env:{CI_NAME:"codeship"}},{name:"Drone",constant:"DRONE",env:"DRONE",pr:{DRONE_BUILD_EVENT:"pull_request"}},{name:"dsari",constant:"DSARI",env:"DSARI"},{name:"Earthly",constant:"EARTHLY",env:"EARTHLY_CI"},{name:"Expo Application Services",constant:"EAS",env:"EAS_BUILD"},{name:"Gerrit",constant:"GERRIT",env:"GERRIT_PROJECT"},{name:"Gitea Actions",constant:"GITEA_ACTIONS",env:"GITEA_ACTIONS"},{name:"GitHub Actions",constant:"GITHUB_ACTIONS",env:"GITHUB_ACTIONS",pr:{GITHUB_EVENT_NAME:"pull_request"}},{name:"GitLab CI",constant:"GITLAB",env:"GITLAB_CI",pr:"CI_MERGE_REQUEST_ID"},{name:"GoCD",constant:"GOCD",env:"GO_PIPELINE_LABEL"},{name:"Google Cloud Build",constant:"GOOGLE_CLOUD_BUILD",env:"BUILDER_OUTPUT"},{name:"Harness CI",constant:"HARNESS",env:"HARNESS_BUILD_ID"},{name:"Heroku",constant:"HEROKU",env:{env:"NODE",includes:"/app/.heroku/node/bin/node"}},{name:"Hudson",constant:"HUDSON",env:"HUDSON_URL"},{name:"Jenkins",constant:"JENKINS",env:["JENKINS_URL","BUILD_ID"],pr:{any:["ghprbPullId","CHANGE_ID"]}},{name:"LayerCI",constant:"LAYERCI",env:"LAYERCI",pr:"LAYERCI_PULL_REQUEST"},{name:"Magnum CI",constant:"MAGNUM",env:"MAGNUM"},{name:"Netlify CI",constant:"NETLIFY",env:"NETLIFY",pr:{env:"PULL_REQUEST",ne:"false"}},{name:"Nevercode",constant:"NEVERCODE",env:"NEVERCODE",pr:{env:"NEVERCODE_PULL_REQUEST",ne:"false"}},{name:"Prow",constant:"PROW",env:"PROW_JOB_ID"},{name:"ReleaseHub",constant:"RELEASEHUB",env:"RELEASE_BUILD_ID"},{name:"Render",constant:"RENDER",env:"RENDER",pr:{IS_PULL_REQUEST:"true"}},{name:"Sail CI",constant:"SAIL",env:"SAILCI",pr:"SAIL_PULL_REQUEST_NUMBER"},{name:"Screwdriver",constant:"SCREWDRIVER",env:"SCREWDRIVER",pr:{env:"SD_PULL_REQUEST",ne:"false"}},{name:"Semaphore",constant:"SEMAPHORE",env:"SEMAPHORE",pr:"PULL_REQUEST_NUMBER"},{name:"Sourcehut",constant:"SOURCEHUT",env:{CI_NAME:"sourcehut"}},{name:"Strider CD",constant:"STRIDER",env:"STRIDER"},{name:"TaskCluster",constant:"TASKCLUSTER",env:["TASK_ID","RUN_ID"]},{name:"TeamCity",constant:"TEAMCITY",env:"TEAMCITY_VERSION"},{name:"Travis CI",constant:"TRAVIS",env:"TRAVIS",pr:{env:"TRAVIS_PULL_REQUEST",ne:"false"}},{name:"Vela",constant:"VELA",env:"VELA",pr:{VELA_PULL_REQUEST:"1"}},{name:"Vercel",constant:"VERCEL",env:{any:["NOW_BUILDER","VERCEL"]},pr:"VERCEL_GIT_PULL_REQUEST_ID"},{name:"Visual Studio App Center",constant:"APPCENTER",env:"APPCENTER_BUILD_ID"},{name:"Woodpecker",constant:"WOODPECKER",env:{CI:"woodpecker"},pr:{CI_BUILD_EVENT:"pull_request"}},{name:"Xcode Cloud",constant:"XCODE_CLOUD",env:"CI_XCODE_PROJECT",pr:"CI_PULL_REQUEST_NUMBER"},{name:"Xcode Server",constant:"XCODE_SERVER",env:"XCS"}]});var Rg=G(Wl=>{"use strict";var Jte=Yte(),xs=process.env;Object.defineProperty(Wl,"_vendors",{value:Jte.map(function(e){return e.constant})});Wl.name=null;Wl.isPR=null;Jte.forEach(function(e){let r=(Array.isArray(e.env)?e.env:[e.env]).every(function(s){return Vte(s)});if(Wl[e.constant]=r,!!r)switch(Wl.name=e.name,typeof e.pr){case"string":Wl.isPR=!!xs[e.pr];break;case"object":"env"in e.pr?Wl.isPR=e.pr.env in xs&&xs[e.pr.env]!==e.pr.ne:"any"in e.pr?Wl.isPR=e.pr.any.some(function(s){return!!xs[s]}):Wl.isPR=Vte(e.pr);break;default:Wl.isPR=null}});Wl.isCI=!!(xs.CI!=="false"&&(xs.BUILD_ID||xs.BUILD_NUMBER||xs.CI||xs.CI_APP_ID||xs.CI_BUILD_ID||xs.CI_BUILD_NUMBER||xs.CI_NAME||xs.CONTINUOUS_INTEGRATION||xs.RUN_ID||Wl.name));function Vte(e){return typeof e=="string"?!!xs[e]:"env"in e?xs[e.env]&&xs[e.env].includes(e.includes):"any"in e?e.any.some(function(t){return!!xs[t]}):Object.keys(e).every(function(t){return xs[t]===e[t]})}});var ni,In,Tg,qU,bx,Kte,WU,YU,Px=Xe(()=>{(function(e){e.StartOfInput="\0",e.EndOfInput="",e.EndOfPartialInput=""})(ni||(ni={}));(function(e){e[e.InitialNode=0]="InitialNode",e[e.SuccessNode=1]="SuccessNode",e[e.ErrorNode=2]="ErrorNode",e[e.CustomNode=3]="CustomNode"})(In||(In={}));Tg=-1,qU=/^(-h|--help)(?:=([0-9]+))?$/,bx=/^(--[a-z]+(?:-[a-z]+)*|-[a-zA-Z]+)$/,Kte=/^-[a-zA-Z]{2,}$/,WU=/^([^=]+)=([\s\S]*)$/,YU=process.env.DEBUG_CLI==="1"});var it,BE,xx,VU,kx=Xe(()=>{Px();it=class extends Error{constructor(t){super(t),this.clipanion={type:"usage"},this.name="UsageError"}},BE=class extends Error{constructor(t,r){if(super(),this.input=t,this.candidates=r,this.clipanion={type:"none"},this.name="UnknownSyntaxError",this.candidates.length===0)this.message="Command not found, but we're not sure what's the alternative.";else if(this.candidates.every(s=>s.reason!==null&&s.reason===r[0].reason)){let[{reason:s}]=this.candidates;this.message=`${s} ${this.candidates.map(({usage:a})=>`$ ${a}`).join(` `)}`}else if(this.candidates.length===1){let[{usage:s}]=this.candidates;this.message=`Command not found; did you mean: $ ${s} ${VU(t)}`}else this.message=`Command not found; did you mean one of: ${this.candidates.map(({usage:s},a)=>`${`${a}.`.padStart(4)} ${s}`).join(` `)} ${VU(t)}`}},xx=class extends Error{constructor(t,r){super(),this.input=t,this.usages=r,this.clipanion={type:"none"},this.name="AmbiguousSyntaxError",this.message=`Cannot find which to pick amongst the following alternatives: ${this.usages.map((s,a)=>`${`${a}.`.padStart(4)} ${s}`).join(` `)} ${VU(t)}`}},VU=e=>`While running ${e.filter(t=>t!==ni.EndOfInput&&t!==ni.EndOfPartialInput).map(t=>{let r=JSON.stringify(t);return t.match(/\s/)||t.length===0||r!==`"${t}"`?r:t}).join(" ")}`});function p9e(e){let t=e.split(` `),r=t.filter(a=>a.match(/\S/)),s=r.length>0?r.reduce((a,n)=>Math.min(a,n.length-n.trimStart().length),Number.MAX_VALUE):0;return t.map(a=>a.slice(s).trimRight()).join(` `)}function Vo(e,{format:t,paragraphs:r}){return e=e.replace(/\r\n?/g,` `),e=p9e(e),e=e.replace(/^\n+|\n+$/g,""),e=e.replace(/^(\s*)-([^\n]*?)\n+/gm,`$1-$2 `),e=e.replace(/\n(\n)?\n*/g,(s,a)=>a||" "),r&&(e=e.split(/\n/).map(s=>{let a=s.match(/^\s*[*-][\t ]+(.*)/);if(!a)return s.match(/(.{1,80})(?: |$)/g).join(` `);let n=s.length-s.trimStart().length;return a[1].match(new RegExp(`(.{1,${78-n}})(?: |$)`,"g")).map((c,f)=>" ".repeat(n)+(f===0?"- ":" ")+c).join(` `)}).join(` `)),e=e.replace(/(`+)((?:.|[\n])*?)\1/g,(s,a,n)=>t.code(a+n+a)),e=e.replace(/(\*\*)((?:.|[\n])*?)\1/g,(s,a,n)=>t.bold(a+n+a)),e?`${e} `:""}var JU,zte,Xte,KU=Xe(()=>{JU=Array(80).fill("\u2501");for(let e=0;e<=24;++e)JU[JU.length-e]=`\x1B[38;5;${232+e}m\u2501`;zte={header:e=>`\x1B[1m\u2501\u2501\u2501 ${e}${e.length<75?` ${JU.slice(e.length+5).join("")}`:":"}\x1B[0m`,bold:e=>`\x1B[1m${e}\x1B[22m`,error:e=>`\x1B[31m\x1B[1m${e}\x1B[22m\x1B[39m`,code:e=>`\x1B[36m${e}\x1B[39m`},Xte={header:e=>e,bold:e=>e,error:e=>e,code:e=>e}});function Ba(e){return{...e,[H2]:!0}}function Yf(e,t){return typeof e>"u"?[e,t]:typeof e=="object"&&e!==null&&!Array.isArray(e)?[void 0,e]:[e,t]}function Qx(e,{mergeName:t=!1}={}){let r=e.match(/^([^:]+): (.*)$/m);if(!r)return"validation failed";let[,s,a]=r;return t&&(a=a[0].toLowerCase()+a.slice(1)),a=s!=="."||!t?`${s.replace(/^\.(\[|$)/,"$1")}: ${a}`:`: ${a}`,a}function j2(e,t){return t.length===1?new it(`${e}${Qx(t[0],{mergeName:!0})}`):new it(`${e}: ${t.map(r=>` - ${Qx(r)}`).join("")}`)}function Fg(e,t,r){if(typeof r>"u")return t;let s=[],a=[],n=f=>{let p=t;return t=f,n.bind(null,p)};if(!r(t,{errors:s,coercions:a,coercion:n}))throw j2(`Invalid value for ${e}`,s);for(let[,f]of a)f();return t}var H2,Bp=Xe(()=>{kx();H2=Symbol("clipanion/isOption")});var Jo={};Vt(Jo,{KeyRelationship:()=>Vf,TypeAssertionError:()=>s0,applyCascade:()=>W2,as:()=>R9e,assert:()=>x9e,assertWithErrors:()=>k9e,cascade:()=>Nx,fn:()=>T9e,hasAtLeastOneKey:()=>r_,hasExactLength:()=>rre,hasForbiddenKeys:()=>Z9e,hasKeyRelationship:()=>V2,hasMaxLength:()=>N9e,hasMinLength:()=>F9e,hasMutuallyExclusiveKeys:()=>$9e,hasRequiredKeys:()=>X9e,hasUniqueItems:()=>O9e,isArray:()=>Rx,isAtLeast:()=>e_,isAtMost:()=>U9e,isBase64:()=>V9e,isBoolean:()=>C9e,isDate:()=>B9e,isDict:()=>D9e,isEnum:()=>ks,isHexColor:()=>Y9e,isISO8601:()=>W9e,isInExclusiveRange:()=>H9e,isInInclusiveRange:()=>_9e,isInstanceOf:()=>P9e,isInteger:()=>t_,isJSON:()=>J9e,isLiteral:()=>$te,isLowerCase:()=>j9e,isMap:()=>S9e,isNegative:()=>L9e,isNullable:()=>z9e,isNumber:()=>ZU,isObject:()=>ere,isOneOf:()=>$U,isOptional:()=>K9e,isPartial:()=>b9e,isPayload:()=>w9e,isPositive:()=>M9e,isRecord:()=>Fx,isSet:()=>v9e,isString:()=>SE,isTuple:()=>Tx,isUUID4:()=>q9e,isUnknown:()=>XU,isUpperCase:()=>G9e,makeTrait:()=>tre,makeValidator:()=>Wr,matchesRegExp:()=>q2,softAssert:()=>Q9e});function ii(e){return e===null?"null":e===void 0?"undefined":e===""?"an empty string":typeof e=="symbol"?`<${e.toString()}>`:Array.isArray(e)?"an array":JSON.stringify(e)}function vE(e,t){if(e.length===0)return"nothing";if(e.length===1)return ii(e[0]);let r=e.slice(0,-1),s=e[e.length-1],a=e.length>2?`, ${t} `:` ${t} `;return`${r.map(n=>ii(n)).join(", ")}${a}${ii(s)}`}function i0(e,t){var r,s,a;return typeof t=="number"?`${(r=e?.p)!==null&&r!==void 0?r:"."}[${t}]`:h9e.test(t)?`${(s=e?.p)!==null&&s!==void 0?s:""}.${t}`:`${(a=e?.p)!==null&&a!==void 0?a:"."}[${JSON.stringify(t)}]`}function zU(e,t,r){return e===1?t:r}function mr({errors:e,p:t}={},r){return e?.push(`${t??"."}: ${r}`),!1}function E9e(e,t){return r=>{e[t]=r}}function Jf(e,t){return r=>{let s=e[t];return e[t]=r,Jf(e,t).bind(null,s)}}function G2(e,t,r){let s=()=>(e(r()),a),a=()=>(e(t),s);return s}function XU(){return Wr({test:(e,t)=>!0})}function $te(e){return Wr({test:(t,r)=>t!==e?mr(r,`Expected ${ii(e)} (got ${ii(t)})`):!0})}function SE(){return Wr({test:(e,t)=>typeof e!="string"?mr(t,`Expected a string (got ${ii(e)})`):!0})}function ks(e){let t=Array.isArray(e)?e:Object.values(e),r=t.every(a=>typeof a=="string"||typeof a=="number"),s=new Set(t);return s.size===1?$te([...s][0]):Wr({test:(a,n)=>s.has(a)?!0:r?mr(n,`Expected one of ${vE(t,"or")} (got ${ii(a)})`):mr(n,`Expected a valid enumeration value (got ${ii(a)})`)})}function C9e(){return Wr({test:(e,t)=>{var r;if(typeof e!="boolean"){if(typeof t?.coercions<"u"){if(typeof t?.coercion>"u")return mr(t,"Unbound coercion result");let s=I9e.get(e);if(typeof s<"u")return t.coercions.push([(r=t.p)!==null&&r!==void 0?r:".",t.coercion.bind(null,s)]),!0}return mr(t,`Expected a boolean (got ${ii(e)})`)}return!0}})}function ZU(){return Wr({test:(e,t)=>{var r;if(typeof e!="number"){if(typeof t?.coercions<"u"){if(typeof t?.coercion>"u")return mr(t,"Unbound coercion result");let s;if(typeof e=="string"){let a;try{a=JSON.parse(e)}catch{}if(typeof a=="number")if(JSON.stringify(a)===e)s=a;else return mr(t,`Received a number that can't be safely represented by the runtime (${e})`)}if(typeof s<"u")return t.coercions.push([(r=t.p)!==null&&r!==void 0?r:".",t.coercion.bind(null,s)]),!0}return mr(t,`Expected a number (got ${ii(e)})`)}return!0}})}function w9e(e){return Wr({test:(t,r)=>{var s;if(typeof r?.coercions>"u")return mr(r,"The isPayload predicate can only be used with coercion enabled");if(typeof r.coercion>"u")return mr(r,"Unbound coercion result");if(typeof t!="string")return mr(r,`Expected a string (got ${ii(t)})`);let a;try{a=JSON.parse(t)}catch{return mr(r,`Expected a JSON string (got ${ii(t)})`)}let n={value:a};return e(a,Object.assign(Object.assign({},r),{coercion:Jf(n,"value")}))?(r.coercions.push([(s=r.p)!==null&&s!==void 0?s:".",r.coercion.bind(null,n.value)]),!0):!1}})}function B9e(){return Wr({test:(e,t)=>{var r;if(!(e instanceof Date)){if(typeof t?.coercions<"u"){if(typeof t?.coercion>"u")return mr(t,"Unbound coercion result");let s;if(typeof e=="string"&&Zte.test(e))s=new Date(e);else{let a;if(typeof e=="string"){let n;try{n=JSON.parse(e)}catch{}typeof n=="number"&&(a=n)}else typeof e=="number"&&(a=e);if(typeof a<"u")if(Number.isSafeInteger(a)||!Number.isSafeInteger(a*1e3))s=new Date(a*1e3);else return mr(t,`Received a timestamp that can't be safely represented by the runtime (${e})`)}if(typeof s<"u")return t.coercions.push([(r=t.p)!==null&&r!==void 0?r:".",t.coercion.bind(null,s)]),!0}return mr(t,`Expected a date (got ${ii(e)})`)}return!0}})}function Rx(e,{delimiter:t}={}){return Wr({test:(r,s)=>{var a;let n=r;if(typeof r=="string"&&typeof t<"u"&&typeof s?.coercions<"u"){if(typeof s?.coercion>"u")return mr(s,"Unbound coercion result");r=r.split(t)}if(!Array.isArray(r))return mr(s,`Expected an array (got ${ii(r)})`);let c=!0;for(let f=0,p=r.length;f{var n,c;if(Object.getPrototypeOf(s).toString()==="[object Set]")if(typeof a?.coercions<"u"){if(typeof a?.coercion>"u")return mr(a,"Unbound coercion result");let f=[...s],p=[...s];if(!r(p,Object.assign(Object.assign({},a),{coercion:void 0})))return!1;let h=()=>p.some((E,C)=>E!==f[C])?new Set(p):s;return a.coercions.push([(n=a.p)!==null&&n!==void 0?n:".",G2(a.coercion,s,h)]),!0}else{let f=!0;for(let p of s)if(f=e(p,Object.assign({},a))&&f,!f&&a?.errors==null)break;return f}if(typeof a?.coercions<"u"){if(typeof a?.coercion>"u")return mr(a,"Unbound coercion result");let f={value:s};return r(s,Object.assign(Object.assign({},a),{coercion:Jf(f,"value")}))?(a.coercions.push([(c=a.p)!==null&&c!==void 0?c:".",G2(a.coercion,s,()=>new Set(f.value))]),!0):!1}return mr(a,`Expected a set (got ${ii(s)})`)}})}function S9e(e,t){let r=Rx(Tx([e,t])),s=Fx(t,{keys:e});return Wr({test:(a,n)=>{var c,f,p;if(Object.getPrototypeOf(a).toString()==="[object Map]")if(typeof n?.coercions<"u"){if(typeof n?.coercion>"u")return mr(n,"Unbound coercion result");let h=[...a],E=[...a];if(!r(E,Object.assign(Object.assign({},n),{coercion:void 0})))return!1;let C=()=>E.some((S,x)=>S[0]!==h[x][0]||S[1]!==h[x][1])?new Map(E):a;return n.coercions.push([(c=n.p)!==null&&c!==void 0?c:".",G2(n.coercion,a,C)]),!0}else{let h=!0;for(let[E,C]of a)if(h=e(E,Object.assign({},n))&&h,!h&&n?.errors==null||(h=t(C,Object.assign(Object.assign({},n),{p:i0(n,E)}))&&h,!h&&n?.errors==null))break;return h}if(typeof n?.coercions<"u"){if(typeof n?.coercion>"u")return mr(n,"Unbound coercion result");let h={value:a};return Array.isArray(a)?r(a,Object.assign(Object.assign({},n),{coercion:void 0}))?(n.coercions.push([(f=n.p)!==null&&f!==void 0?f:".",G2(n.coercion,a,()=>new Map(h.value))]),!0):!1:s(a,Object.assign(Object.assign({},n),{coercion:Jf(h,"value")}))?(n.coercions.push([(p=n.p)!==null&&p!==void 0?p:".",G2(n.coercion,a,()=>new Map(Object.entries(h.value)))]),!0):!1}return mr(n,`Expected a map (got ${ii(a)})`)}})}function Tx(e,{delimiter:t}={}){let r=rre(e.length);return Wr({test:(s,a)=>{var n;if(typeof s=="string"&&typeof t<"u"&&typeof a?.coercions<"u"){if(typeof a?.coercion>"u")return mr(a,"Unbound coercion result");s=s.split(t),a.coercions.push([(n=a.p)!==null&&n!==void 0?n:".",a.coercion.bind(null,s)])}if(!Array.isArray(s))return mr(a,`Expected a tuple (got ${ii(s)})`);let c=r(s,Object.assign({},a));for(let f=0,p=s.length;f{var n;if(Array.isArray(s)&&typeof a?.coercions<"u")return typeof a?.coercion>"u"?mr(a,"Unbound coercion result"):r(s,Object.assign(Object.assign({},a),{coercion:void 0}))?(s=Object.fromEntries(s),a.coercions.push([(n=a.p)!==null&&n!==void 0?n:".",a.coercion.bind(null,s)]),!0):!1;if(typeof s!="object"||s===null)return mr(a,`Expected an object (got ${ii(s)})`);let c=Object.keys(s),f=!0;for(let p=0,h=c.length;p{if(typeof a!="object"||a===null)return mr(n,`Expected an object (got ${ii(a)})`);let c=new Set([...r,...Object.keys(a)]),f={},p=!0;for(let h of c){if(h==="constructor"||h==="__proto__")p=mr(Object.assign(Object.assign({},n),{p:i0(n,h)}),"Unsafe property name");else{let E=Object.prototype.hasOwnProperty.call(e,h)?e[h]:void 0,C=Object.prototype.hasOwnProperty.call(a,h)?a[h]:void 0;typeof E<"u"?p=E(C,Object.assign(Object.assign({},n),{p:i0(n,h),coercion:Jf(a,h)}))&&p:t===null?p=mr(Object.assign(Object.assign({},n),{p:i0(n,h)}),`Extraneous property (got ${ii(C)})`):Object.defineProperty(f,h,{enumerable:!0,get:()=>C,set:E9e(a,h)})}if(!p&&n?.errors==null)break}return t!==null&&(p||n?.errors!=null)&&(p=t(f,n)&&p),p}});return Object.assign(s,{properties:e})}function b9e(e){return ere(e,{extra:Fx(XU())})}function tre(e){return()=>e}function Wr({test:e}){return tre(e)()}function x9e(e,t){if(!t(e))throw new s0}function k9e(e,t){let r=[];if(!t(e,{errors:r}))throw new s0({errors:r})}function Q9e(e,t){}function R9e(e,t,{coerce:r=!1,errors:s,throw:a}={}){let n=s?[]:void 0;if(!r){if(t(e,{errors:n}))return a?e:{value:e,errors:void 0};if(a)throw new s0({errors:n});return{value:void 0,errors:n??!0}}let c={value:e},f=Jf(c,"value"),p=[];if(!t(e,{errors:n,coercion:f,coercions:p})){if(a)throw new s0({errors:n});return{value:void 0,errors:n??!0}}for(let[,h]of p)h();return a?c.value:{value:c.value,errors:void 0}}function T9e(e,t){let r=Tx(e);return(...s)=>{if(!r(s))throw new s0;return t(...s)}}function F9e(e){return Wr({test:(t,r)=>t.length>=e?!0:mr(r,`Expected to have a length of at least ${e} elements (got ${t.length})`)})}function N9e(e){return Wr({test:(t,r)=>t.length<=e?!0:mr(r,`Expected to have a length of at most ${e} elements (got ${t.length})`)})}function rre(e){return Wr({test:(t,r)=>t.length!==e?mr(r,`Expected to have a length of exactly ${e} elements (got ${t.length})`):!0})}function O9e({map:e}={}){return Wr({test:(t,r)=>{let s=new Set,a=new Set;for(let n=0,c=t.length;ne<=0?!0:mr(t,`Expected to be negative (got ${e})`)})}function M9e(){return Wr({test:(e,t)=>e>=0?!0:mr(t,`Expected to be positive (got ${e})`)})}function e_(e){return Wr({test:(t,r)=>t>=e?!0:mr(r,`Expected to be at least ${e} (got ${t})`)})}function U9e(e){return Wr({test:(t,r)=>t<=e?!0:mr(r,`Expected to be at most ${e} (got ${t})`)})}function _9e(e,t){return Wr({test:(r,s)=>r>=e&&r<=t?!0:mr(s,`Expected to be in the [${e}; ${t}] range (got ${r})`)})}function H9e(e,t){return Wr({test:(r,s)=>r>=e&&rt!==Math.round(t)?mr(r,`Expected to be an integer (got ${t})`):!e&&!Number.isSafeInteger(t)?mr(r,`Expected to be a safe integer (got ${t})`):!0})}function q2(e){return Wr({test:(t,r)=>e.test(t)?!0:mr(r,`Expected to match the pattern ${e.toString()} (got ${ii(t)})`)})}function j9e(){return Wr({test:(e,t)=>e!==e.toLowerCase()?mr(t,`Expected to be all-lowercase (got ${e})`):!0})}function G9e(){return Wr({test:(e,t)=>e!==e.toUpperCase()?mr(t,`Expected to be all-uppercase (got ${e})`):!0})}function q9e(){return Wr({test:(e,t)=>y9e.test(e)?!0:mr(t,`Expected to be a valid UUID v4 (got ${ii(e)})`)})}function W9e(){return Wr({test:(e,t)=>Zte.test(e)?!0:mr(t,`Expected to be a valid ISO 8601 date string (got ${ii(e)})`)})}function Y9e({alpha:e=!1}){return Wr({test:(t,r)=>(e?d9e.test(t):g9e.test(t))?!0:mr(r,`Expected to be a valid hexadecimal color string (got ${ii(t)})`)})}function V9e(){return Wr({test:(e,t)=>m9e.test(e)?!0:mr(t,`Expected to be a valid base 64 string (got ${ii(e)})`)})}function J9e(e=XU()){return Wr({test:(t,r)=>{let s;try{s=JSON.parse(t)}catch{return mr(r,`Expected to be a valid JSON string (got ${ii(t)})`)}return e(s,r)}})}function Nx(e,...t){let r=Array.isArray(t[0])?t[0]:t;return Wr({test:(s,a)=>{var n,c;let f={value:s},p=typeof a?.coercions<"u"?Jf(f,"value"):void 0,h=typeof a?.coercions<"u"?[]:void 0;if(!e(s,Object.assign(Object.assign({},a),{coercion:p,coercions:h})))return!1;let E=[];if(typeof h<"u")for(let[,C]of h)E.push(C());try{if(typeof a?.coercions<"u"){if(f.value!==s){if(typeof a?.coercion>"u")return mr(a,"Unbound coercion result");a.coercions.push([(n=a.p)!==null&&n!==void 0?n:".",a.coercion.bind(null,f.value)])}(c=a?.coercions)===null||c===void 0||c.push(...h)}return r.every(C=>C(f.value,a))}finally{for(let C of E)C()}}})}function W2(e,...t){let r=Array.isArray(t[0])?t[0]:t;return Nx(e,r)}function K9e(e){return Wr({test:(t,r)=>typeof t>"u"?!0:e(t,r)})}function z9e(e){return Wr({test:(t,r)=>t===null?!0:e(t,r)})}function X9e(e,t){var r;let s=new Set(e),a=Y2[(r=t?.missingIf)!==null&&r!==void 0?r:"missing"];return Wr({test:(n,c)=>{let f=new Set(Object.keys(n)),p=[];for(let h of s)a(f,h,n)||p.push(h);return p.length>0?mr(c,`Missing required ${zU(p.length,"property","properties")} ${vE(p,"and")}`):!0}})}function r_(e,t){var r;let s=new Set(e),a=Y2[(r=t?.missingIf)!==null&&r!==void 0?r:"missing"];return Wr({test:(n,c)=>Object.keys(n).some(h=>a(s,h,n))?!0:mr(c,`Missing at least one property from ${vE(Array.from(s),"or")}`)})}function Z9e(e,t){var r;let s=new Set(e),a=Y2[(r=t?.missingIf)!==null&&r!==void 0?r:"missing"];return Wr({test:(n,c)=>{let f=new Set(Object.keys(n)),p=[];for(let h of s)a(f,h,n)&&p.push(h);return p.length>0?mr(c,`Forbidden ${zU(p.length,"property","properties")} ${vE(p,"and")}`):!0}})}function $9e(e,t){var r;let s=new Set(e),a=Y2[(r=t?.missingIf)!==null&&r!==void 0?r:"missing"];return Wr({test:(n,c)=>{let f=new Set(Object.keys(n)),p=[];for(let h of s)a(f,h,n)&&p.push(h);return p.length>1?mr(c,`Mutually exclusive properties ${vE(p,"and")}`):!0}})}function V2(e,t,r,s){var a,n;let c=new Set((a=s?.ignore)!==null&&a!==void 0?a:[]),f=Y2[(n=s?.missingIf)!==null&&n!==void 0?n:"missing"],p=new Set(r),h=eqe[t],E=t===Vf.Forbids?"or":"and";return Wr({test:(C,S)=>{let x=new Set(Object.keys(C));if(!f(x,e,C)||c.has(C[e]))return!0;let I=[];for(let T of p)(f(x,T,C)&&!c.has(C[T]))!==h.expect&&I.push(T);return I.length>=1?mr(S,`Property "${e}" ${h.message} ${zU(I.length,"property","properties")} ${vE(I,E)}`):!0}})}var h9e,d9e,g9e,m9e,y9e,Zte,I9e,P9e,$U,s0,Y2,Vf,eqe,Al=Xe(()=>{h9e=/^[a-zA-Z_][a-zA-Z0-9_]*$/;d9e=/^#[0-9a-f]{6}$/i,g9e=/^#[0-9a-f]{6}([0-9a-f]{2})?$/i,m9e=/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/,y9e=/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}$/i,Zte=/^(?:[1-9]\d{3}(-?)(?:(?:0[1-9]|1[0-2])\1(?:0[1-9]|1\d|2[0-8])|(?:0[13-9]|1[0-2])\1(?:29|30)|(?:0[13578]|1[02])(?:\1)31|00[1-9]|0[1-9]\d|[12]\d{2}|3(?:[0-5]\d|6[0-5]))|(?:[1-9]\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)(?:(-?)02(?:\2)29|-?366))T(?:[01]\d|2[0-3])(:?)[0-5]\d(?:\3[0-5]\d)?(?:Z|[+-][01]\d(?:\3[0-5]\d)?)$/;I9e=new Map([["true",!0],["True",!0],["1",!0],[1,!0],["false",!1],["False",!1],["0",!1],[0,!1]]);P9e=e=>Wr({test:(t,r)=>t instanceof e?!0:mr(r,`Expected an instance of ${e.name} (got ${ii(t)})`)}),$U=(e,{exclusive:t=!1}={})=>Wr({test:(r,s)=>{var a,n,c;let f=[],p=typeof s?.errors<"u"?[]:void 0;for(let h=0,E=e.length;h1?mr(s,`Expected to match exactly a single predicate (matched ${f.join(", ")})`):(c=s?.errors)===null||c===void 0||c.push(...p),!1}});s0=class extends Error{constructor({errors:t}={}){let r="Type mismatch";if(t&&t.length>0){r+=` `;for(let s of t)r+=` - ${s}`}super(r)}};Y2={missing:(e,t)=>e.has(t),undefined:(e,t,r)=>e.has(t)&&typeof r[t]<"u",nil:(e,t,r)=>e.has(t)&&r[t]!=null,falsy:(e,t,r)=>e.has(t)&&!!r[t]};(function(e){e.Forbids="Forbids",e.Requires="Requires"})(Vf||(Vf={}));eqe={[Vf.Forbids]:{expect:!1,message:"forbids using"},[Vf.Requires]:{expect:!0,message:"requires using"}}});var at,o0=Xe(()=>{Bp();at=class{constructor(){this.help=!1}static Usage(t){return t}async catch(t){throw t}async validateAndExecute(){let r=this.constructor.schema;if(Array.isArray(r)){let{isDict:a,isUnknown:n,applyCascade:c}=await Promise.resolve().then(()=>(Al(),Jo)),f=c(a(n()),r),p=[],h=[];if(!f(this,{errors:p,coercions:h}))throw j2("Invalid option schema",p);for(let[,C]of h)C()}else if(r!=null)throw new Error("Invalid command schema");let s=await this.execute();return typeof s<"u"?s:0}};at.isOption=H2;at.Default=[]});function pl(e){YU&&console.log(e)}function ire(){let e={nodes:[]};for(let t=0;t{if(t.has(s))return;t.add(s);let a=e.nodes[s];for(let c of Object.values(a.statics))for(let{to:f}of c)r(f);for(let[,{to:c}]of a.dynamics)r(c);for(let{to:c}of a.shortcuts)r(c);let n=new Set(a.shortcuts.map(({to:c})=>c));for(;a.shortcuts.length>0;){let{to:c}=a.shortcuts.shift(),f=e.nodes[c];for(let[p,h]of Object.entries(f.statics)){let E=Object.prototype.hasOwnProperty.call(a.statics,p)?a.statics[p]:a.statics[p]=[];for(let C of h)E.some(({to:S})=>C.to===S)||E.push(C)}for(let[p,h]of f.dynamics)a.dynamics.some(([E,{to:C}])=>p===E&&h.to===C)||a.dynamics.push([p,h]);for(let p of f.shortcuts)n.has(p.to)||(a.shortcuts.push(p),n.add(p.to))}};r(In.InitialNode)}function nqe(e,{prefix:t=""}={}){if(YU){pl(`${t}Nodes are:`);for(let r=0;rE!==In.ErrorNode).map(({state:E})=>({usage:E.candidateUsage,reason:null})));if(h.every(({node:E})=>E===In.ErrorNode))throw new BE(t,h.map(({state:E})=>({usage:E.candidateUsage,reason:E.errorMessage})));s=oqe(h)}if(s.length>0){pl(" Results:");for(let n of s)pl(` - ${n.node} -> ${JSON.stringify(n.state)}`)}else pl(" No results");return s}function sqe(e,t,{endToken:r=ni.EndOfInput}={}){let s=iqe(e,[...t,r]);return aqe(t,s.map(({state:a})=>a))}function oqe(e){let t=0;for(let{state:r}of e)r.path.length>t&&(t=r.path.length);return e.filter(({state:r})=>r.path.length===t)}function aqe(e,t){let r=t.filter(S=>S.selectedIndex!==null),s=r.filter(S=>!S.partial);if(s.length>0&&(r=s),r.length===0)throw new Error;let a=r.filter(S=>S.selectedIndex===Tg||S.requiredOptions.every(x=>x.some(I=>S.options.find(T=>T.name===I))));if(a.length===0)throw new BE(e,r.map(S=>({usage:S.candidateUsage,reason:null})));let n=0;for(let S of a)S.path.length>n&&(n=S.path.length);let c=a.filter(S=>S.path.length===n),f=S=>S.positionals.filter(({extra:x})=>!x).length+S.options.length,p=c.map(S=>({state:S,positionalCount:f(S)})),h=0;for(let{positionalCount:S}of p)S>h&&(h=S);let E=p.filter(({positionalCount:S})=>S===h).map(({state:S})=>S),C=lqe(E);if(C.length>1)throw new xx(e,C.map(S=>S.candidateUsage));return C[0]}function lqe(e){let t=[],r=[];for(let s of e)s.selectedIndex===Tg?r.push(s):t.push(s);return r.length>0&&t.push({...nre,path:sre(...r.map(s=>s.path)),options:r.reduce((s,a)=>s.concat(a.options),[])}),t}function sre(e,t,...r){return t===void 0?Array.from(e):sre(e.filter((s,a)=>s===t[a]),...r)}function Yl(){return{dynamics:[],shortcuts:[],statics:{}}}function ore(e){return e===In.SuccessNode||e===In.ErrorNode}function n_(e,t=0){return{to:ore(e.to)?e.to:e.to>=In.CustomNode?e.to+t-In.CustomNode+1:e.to+t,reducer:e.reducer}}function cqe(e,t=0){let r=Yl();for(let[s,a]of e.dynamics)r.dynamics.push([s,n_(a,t)]);for(let s of e.shortcuts)r.shortcuts.push(n_(s,t));for(let[s,a]of Object.entries(e.statics))r.statics[s]=a.map(n=>n_(n,t));return r}function qs(e,t,r,s,a){e.nodes[t].dynamics.push([r,{to:s,reducer:a}])}function DE(e,t,r,s){e.nodes[t].shortcuts.push({to:r,reducer:s})}function va(e,t,r,s,a){(Object.prototype.hasOwnProperty.call(e.nodes[t].statics,r)?e.nodes[t].statics[r]:e.nodes[t].statics[r]=[]).push({to:s,reducer:a})}function Ox(e,t,r,s,a){if(Array.isArray(t)){let[n,...c]=t;return e[n](r,s,a,...c)}else return e[t](r,s,a)}var nre,uqe,i_,Vl,s_,Lx,Mx=Xe(()=>{Px();kx();nre={candidateUsage:null,requiredOptions:[],errorMessage:null,ignoreOptions:!1,path:[],positionals:[],options:[],remainder:null,selectedIndex:Tg,partial:!1,tokens:[]};uqe={always:()=>!0,isOptionLike:(e,t)=>!e.ignoreOptions&&t!=="-"&&t.startsWith("-"),isNotOptionLike:(e,t)=>e.ignoreOptions||t==="-"||!t.startsWith("-"),isOption:(e,t,r,s)=>!e.ignoreOptions&&t===s,isBatchOption:(e,t,r,s)=>!e.ignoreOptions&&Kte.test(t)&&[...t.slice(1)].every(a=>s.has(`-${a}`)),isBoundOption:(e,t,r,s,a)=>{let n=t.match(WU);return!e.ignoreOptions&&!!n&&bx.test(n[1])&&s.has(n[1])&&a.filter(c=>c.nameSet.includes(n[1])).every(c=>c.allowBinding)},isNegatedOption:(e,t,r,s)=>!e.ignoreOptions&&t===`--no-${s.slice(2)}`,isHelp:(e,t)=>!e.ignoreOptions&&qU.test(t),isUnsupportedOption:(e,t,r,s)=>!e.ignoreOptions&&t.startsWith("-")&&bx.test(t)&&!s.has(t),isInvalidOption:(e,t)=>!e.ignoreOptions&&t.startsWith("-")&&!bx.test(t)},i_={setCandidateState:(e,t,r,s)=>({...e,...s}),setSelectedIndex:(e,t,r,s)=>({...e,selectedIndex:s}),setPartialIndex:(e,t,r,s)=>({...e,selectedIndex:s,partial:!0}),pushBatch:(e,t,r,s)=>{let a=e.options.slice(),n=e.tokens.slice();for(let c=1;c{let[,s,a]=t.match(WU),n=e.options.concat({name:s,value:a}),c=e.tokens.concat([{segmentIndex:r,type:"option",slice:[0,s.length],option:s},{segmentIndex:r,type:"assign",slice:[s.length,s.length+1]},{segmentIndex:r,type:"value",slice:[s.length+1,s.length+a.length+1]}]);return{...e,options:n,tokens:c}},pushPath:(e,t,r)=>{let s=e.path.concat(t),a=e.tokens.concat({segmentIndex:r,type:"path"});return{...e,path:s,tokens:a}},pushPositional:(e,t,r)=>{let s=e.positionals.concat({value:t,extra:!1}),a=e.tokens.concat({segmentIndex:r,type:"positional"});return{...e,positionals:s,tokens:a}},pushExtra:(e,t,r)=>{let s=e.positionals.concat({value:t,extra:!0}),a=e.tokens.concat({segmentIndex:r,type:"positional"});return{...e,positionals:s,tokens:a}},pushExtraNoLimits:(e,t,r)=>{let s=e.positionals.concat({value:t,extra:Vl}),a=e.tokens.concat({segmentIndex:r,type:"positional"});return{...e,positionals:s,tokens:a}},pushTrue:(e,t,r,s)=>{let a=e.options.concat({name:s,value:!0}),n=e.tokens.concat({segmentIndex:r,type:"option",option:s});return{...e,options:a,tokens:n}},pushFalse:(e,t,r,s)=>{let a=e.options.concat({name:s,value:!1}),n=e.tokens.concat({segmentIndex:r,type:"option",option:s});return{...e,options:a,tokens:n}},pushUndefined:(e,t,r,s)=>{let a=e.options.concat({name:t,value:void 0}),n=e.tokens.concat({segmentIndex:r,type:"option",option:t});return{...e,options:a,tokens:n}},pushStringValue:(e,t,r)=>{var s;let a=e.options[e.options.length-1],n=e.options.slice(),c=e.tokens.concat({segmentIndex:r,type:"value"});return a.value=((s=a.value)!==null&&s!==void 0?s:[]).concat([t]),{...e,options:n,tokens:c}},setStringValue:(e,t,r)=>{let s=e.options[e.options.length-1],a=e.options.slice(),n=e.tokens.concat({segmentIndex:r,type:"value"});return s.value=t,{...e,options:a,tokens:n}},inhibateOptions:e=>({...e,ignoreOptions:!0}),useHelp:(e,t,r,s)=>{let[,,a]=t.match(qU);return typeof a<"u"?{...e,options:[{name:"-c",value:String(s)},{name:"-i",value:a}]}:{...e,options:[{name:"-c",value:String(s)}]}},setError:(e,t,r,s)=>t===ni.EndOfInput||t===ni.EndOfPartialInput?{...e,errorMessage:`${s}.`}:{...e,errorMessage:`${s} ("${t}").`},setOptionArityError:(e,t)=>{let r=e.options[e.options.length-1];return{...e,errorMessage:`Not enough arguments to option ${r.name}.`}}},Vl=Symbol(),s_=class{constructor(t,r){this.allOptionNames=new Map,this.arity={leading:[],trailing:[],extra:[],proxy:!1},this.options=[],this.paths=[],this.cliIndex=t,this.cliOpts=r}addPath(t){this.paths.push(t)}setArity({leading:t=this.arity.leading,trailing:r=this.arity.trailing,extra:s=this.arity.extra,proxy:a=this.arity.proxy}){Object.assign(this.arity,{leading:t,trailing:r,extra:s,proxy:a})}addPositional({name:t="arg",required:r=!0}={}){if(!r&&this.arity.extra===Vl)throw new Error("Optional parameters cannot be declared when using .rest() or .proxy()");if(!r&&this.arity.trailing.length>0)throw new Error("Optional parameters cannot be declared after the required trailing positional arguments");!r&&this.arity.extra!==Vl?this.arity.extra.push(t):this.arity.extra!==Vl&&this.arity.extra.length===0?this.arity.leading.push(t):this.arity.trailing.push(t)}addRest({name:t="arg",required:r=0}={}){if(this.arity.extra===Vl)throw new Error("Infinite lists cannot be declared multiple times in the same command");if(this.arity.trailing.length>0)throw new Error("Infinite lists cannot be declared after the required trailing positional arguments");for(let s=0;s1)throw new Error("The arity cannot be higher than 1 when the option only supports the --arg=value syntax");if(!Number.isInteger(s))throw new Error(`The arity must be an integer, got ${s}`);if(s<0)throw new Error(`The arity must be positive, got ${s}`);let f=t.reduce((p,h)=>h.length>p.length?h:p,"");for(let p of t)this.allOptionNames.set(p,f);this.options.push({preferredName:f,nameSet:t,description:r,arity:s,hidden:a,required:n,allowBinding:c})}setContext(t){this.context=t}usage({detailed:t=!0,inlineOptions:r=!0}={}){let s=[this.cliOpts.binaryName],a=[];if(this.paths.length>0&&s.push(...this.paths[0]),t){for(let{preferredName:c,nameSet:f,arity:p,hidden:h,description:E,required:C}of this.options){if(h)continue;let S=[];for(let I=0;I`:`[${x}]`)}s.push(...this.arity.leading.map(c=>`<${c}>`)),this.arity.extra===Vl?s.push("..."):s.push(...this.arity.extra.map(c=>`[${c}]`)),s.push(...this.arity.trailing.map(c=>`<${c}>`))}return{usage:s.join(" "),options:a}}compile(){if(typeof this.context>"u")throw new Error("Assertion failed: No context attached");let t=ire(),r=In.InitialNode,s=this.usage().usage,a=this.options.filter(f=>f.required).map(f=>f.nameSet);r=Fu(t,Yl()),va(t,In.InitialNode,ni.StartOfInput,r,["setCandidateState",{candidateUsage:s,requiredOptions:a}]);let n=this.arity.proxy?"always":"isNotOptionLike",c=this.paths.length>0?this.paths:[[]];for(let f of c){let p=r;if(f.length>0){let S=Fu(t,Yl());DE(t,p,S),this.registerOptions(t,S),p=S}for(let S=0;S0||!this.arity.proxy){let S=Fu(t,Yl());qs(t,p,"isHelp",S,["useHelp",this.cliIndex]),qs(t,S,"always",S,"pushExtra"),va(t,S,ni.EndOfInput,In.SuccessNode,["setSelectedIndex",Tg]),this.registerOptions(t,p)}this.arity.leading.length>0&&(va(t,p,ni.EndOfInput,In.ErrorNode,["setError","Not enough positional arguments"]),va(t,p,ni.EndOfPartialInput,In.SuccessNode,["setPartialIndex",this.cliIndex]));let h=p;for(let S=0;S0||S+1!==this.arity.leading.length)&&(va(t,x,ni.EndOfInput,In.ErrorNode,["setError","Not enough positional arguments"]),va(t,x,ni.EndOfPartialInput,In.SuccessNode,["setPartialIndex",this.cliIndex])),qs(t,h,"isNotOptionLike",x,"pushPositional"),h=x}let E=h;if(this.arity.extra===Vl||this.arity.extra.length>0){let S=Fu(t,Yl());if(DE(t,h,S),this.arity.extra===Vl){let x=Fu(t,Yl());this.arity.proxy||this.registerOptions(t,x),qs(t,h,n,x,"pushExtraNoLimits"),qs(t,x,n,x,"pushExtraNoLimits"),DE(t,x,S)}else for(let x=0;x0)&&this.registerOptions(t,I),qs(t,E,n,I,"pushExtra"),DE(t,I,S),E=I}E=S}this.arity.trailing.length>0&&(va(t,E,ni.EndOfInput,In.ErrorNode,["setError","Not enough positional arguments"]),va(t,E,ni.EndOfPartialInput,In.SuccessNode,["setPartialIndex",this.cliIndex]));let C=E;for(let S=0;S=0&&t{let c=n?ni.EndOfPartialInput:ni.EndOfInput;return sqe(s,a,{endToken:c})}}}}});function lre(){return Ux.default&&"getColorDepth"in Ux.default.WriteStream.prototype?Ux.default.WriteStream.prototype.getColorDepth():process.env.FORCE_COLOR==="0"?1:process.env.FORCE_COLOR==="1"||typeof process.stdout<"u"&&process.stdout.isTTY?8:1}function cre(e){let t=are;if(typeof t>"u"){if(e.stdout===process.stdout&&e.stderr===process.stderr)return null;let{AsyncLocalStorage:r}=Ie("async_hooks");t=are=new r;let s=process.stdout._write;process.stdout._write=function(n,c,f){let p=t.getStore();return typeof p>"u"?s.call(this,n,c,f):p.stdout.write(n,c,f)};let a=process.stderr._write;process.stderr._write=function(n,c,f){let p=t.getStore();return typeof p>"u"?a.call(this,n,c,f):p.stderr.write(n,c,f)}}return r=>t.run(e,r)}var Ux,are,ure=Xe(()=>{Ux=et(Ie("tty"),1)});var _x,fre=Xe(()=>{o0();_x=class e extends at{constructor(t){super(),this.contexts=t,this.commands=[]}static from(t,r){let s=new e(r);s.path=t.path;for(let a of t.options)switch(a.name){case"-c":s.commands.push(Number(a.value));break;case"-i":s.index=Number(a.value);break}return s}async execute(){let t=this.commands;if(typeof this.index<"u"&&this.index>=0&&this.index1){this.context.stdout.write(`Multiple commands match your selection: `),this.context.stdout.write(` `);let r=0;for(let s of this.commands)this.context.stdout.write(this.cli.usage(this.contexts[s].commandClass,{prefix:`${r++}. `.padStart(5)}));this.context.stdout.write(` `),this.context.stdout.write(`Run again with -h= to see the longer details of any of those commands. `)}}}});async function hre(...e){let{resolvedOptions:t,resolvedCommandClasses:r,resolvedArgv:s,resolvedContext:a}=gre(e);return Sa.from(r,t).runExit(s,a)}async function dre(...e){let{resolvedOptions:t,resolvedCommandClasses:r,resolvedArgv:s,resolvedContext:a}=gre(e);return Sa.from(r,t).run(s,a)}function gre(e){let t,r,s,a;switch(typeof process<"u"&&typeof process.argv<"u"&&(s=process.argv.slice(2)),e.length){case 1:r=e[0];break;case 2:e[0]&&e[0].prototype instanceof at||Array.isArray(e[0])?(r=e[0],Array.isArray(e[1])?s=e[1]:a=e[1]):(t=e[0],r=e[1]);break;case 3:Array.isArray(e[2])?(t=e[0],r=e[1],s=e[2]):e[0]&&e[0].prototype instanceof at||Array.isArray(e[0])?(r=e[0],s=e[1],a=e[2]):(t=e[0],r=e[1],a=e[2]);break;default:t=e[0],r=e[1],s=e[2],a=e[3];break}if(typeof s>"u")throw new Error("The argv parameter must be provided when running Clipanion outside of a Node context");return{resolvedOptions:t,resolvedCommandClasses:r,resolvedArgv:s,resolvedContext:a}}function pre(e){return e()}var Are,Sa,mre=Xe(()=>{Px();Mx();KU();ure();o0();fre();Are=Symbol("clipanion/errorCommand");Sa=class e{constructor({binaryLabel:t,binaryName:r="...",binaryVersion:s,enableCapture:a=!1,enableColors:n}={}){this.registrations=new Map,this.builder=new Lx({binaryName:r}),this.binaryLabel=t,this.binaryName=r,this.binaryVersion=s,this.enableCapture=a,this.enableColors=n}static from(t,r={}){let s=new e(r),a=Array.isArray(t)?t:[t];for(let n of a)s.register(n);return s}register(t){var r;let s=new Map,a=new t;for(let p in a){let h=a[p];typeof h=="object"&&h!==null&&h[at.isOption]&&s.set(p,h)}let n=this.builder.command(),c=n.cliIndex,f=(r=t.paths)!==null&&r!==void 0?r:a.paths;if(typeof f<"u")for(let p of f)n.addPath(p);this.registrations.set(t,{specs:s,builder:n,index:c});for(let[p,{definition:h}]of s.entries())h(n,p);n.setContext({commandClass:t})}process(t,r){let{input:s,context:a,partial:n}=typeof t=="object"&&Array.isArray(t)?{input:t,context:r}:t,{contexts:c,process:f}=this.builder.compile(),p=f(s,{partial:n}),h={...e.defaultContext,...a};switch(p.selectedIndex){case Tg:{let E=_x.from(p,c);return E.context=h,E.tokens=p.tokens,E}default:{let{commandClass:E}=c[p.selectedIndex],C=this.registrations.get(E);if(typeof C>"u")throw new Error("Assertion failed: Expected the command class to have been registered.");let S=new E;S.context=h,S.tokens=p.tokens,S.path=p.path;try{for(let[x,{transformer:I}]of C.specs.entries())S[x]=I(C.builder,x,p,h);return S}catch(x){throw x[Are]=S,x}}break}}async run(t,r){var s,a;let n,c={...e.defaultContext,...r},f=(s=this.enableColors)!==null&&s!==void 0?s:c.colorDepth>1;if(!Array.isArray(t))n=t;else try{n=this.process(t,c)}catch(E){return c.stdout.write(this.error(E,{colored:f})),1}if(n.help)return c.stdout.write(this.usage(n,{colored:f,detailed:!0})),0;n.context=c,n.cli={binaryLabel:this.binaryLabel,binaryName:this.binaryName,binaryVersion:this.binaryVersion,enableCapture:this.enableCapture,enableColors:this.enableColors,definitions:()=>this.definitions(),definition:E=>this.definition(E),error:(E,C)=>this.error(E,C),format:E=>this.format(E),process:(E,C)=>this.process(E,{...c,...C}),run:(E,C)=>this.run(E,{...c,...C}),usage:(E,C)=>this.usage(E,C)};let p=this.enableCapture&&(a=cre(c))!==null&&a!==void 0?a:pre,h;try{h=await p(()=>n.validateAndExecute().catch(E=>n.catch(E).then(()=>0)))}catch(E){return c.stdout.write(this.error(E,{colored:f,command:n})),1}return h}async runExit(t,r){process.exitCode=await this.run(t,r)}definition(t,{colored:r=!1}={}){if(!t.usage)return null;let{usage:s}=this.getUsageByRegistration(t,{detailed:!1}),{usage:a,options:n}=this.getUsageByRegistration(t,{detailed:!0,inlineOptions:!1}),c=typeof t.usage.category<"u"?Vo(t.usage.category,{format:this.format(r),paragraphs:!1}):void 0,f=typeof t.usage.description<"u"?Vo(t.usage.description,{format:this.format(r),paragraphs:!1}):void 0,p=typeof t.usage.details<"u"?Vo(t.usage.details,{format:this.format(r),paragraphs:!0}):void 0,h=typeof t.usage.examples<"u"?t.usage.examples.map(([E,C])=>[Vo(E,{format:this.format(r),paragraphs:!1}),C.replace(/\$0/g,this.binaryName)]):void 0;return{path:s,usage:a,category:c,description:f,details:p,examples:h,options:n}}definitions({colored:t=!1}={}){let r=[];for(let s of this.registrations.keys()){let a=this.definition(s,{colored:t});a&&r.push(a)}return r}usage(t=null,{colored:r,detailed:s=!1,prefix:a="$ "}={}){var n;if(t===null){for(let p of this.registrations.keys()){let h=p.paths,E=typeof p.usage<"u";if(!h||h.length===0||h.length===1&&h[0].length===0||((n=h?.some(x=>x.length===0))!==null&&n!==void 0?n:!1))if(t){t=null;break}else t=p;else if(E){t=null;continue}}t&&(s=!0)}let c=t!==null&&t instanceof at?t.constructor:t,f="";if(c)if(s){let{description:p="",details:h="",examples:E=[]}=c.usage||{};p!==""&&(f+=Vo(p,{format:this.format(r),paragraphs:!1}).replace(/^./,x=>x.toUpperCase()),f+=` `),(h!==""||E.length>0)&&(f+=`${this.format(r).header("Usage")} `,f+=` `);let{usage:C,options:S}=this.getUsageByRegistration(c,{inlineOptions:!1});if(f+=`${this.format(r).bold(a)}${C} `,S.length>0){f+=` `,f+=`${this.format(r).header("Options")} `;let x=S.reduce((I,T)=>Math.max(I,T.definition.length),0);f+=` `;for(let{definition:I,description:T}of S)f+=` ${this.format(r).bold(I.padEnd(x))} ${Vo(T,{format:this.format(r),paragraphs:!1})}`}if(h!==""&&(f+=` `,f+=`${this.format(r).header("Details")} `,f+=` `,f+=Vo(h,{format:this.format(r),paragraphs:!0})),E.length>0){f+=` `,f+=`${this.format(r).header("Examples")} `;for(let[x,I]of E)f+=` `,f+=Vo(x,{format:this.format(r),paragraphs:!1}),f+=`${I.replace(/^/m,` ${this.format(r).bold(a)}`).replace(/\$0/g,this.binaryName)} `}}else{let{usage:p}=this.getUsageByRegistration(c);f+=`${this.format(r).bold(a)}${p} `}else{let p=new Map;for(let[S,{index:x}]of this.registrations.entries()){if(typeof S.usage>"u")continue;let I=typeof S.usage.category<"u"?Vo(S.usage.category,{format:this.format(r),paragraphs:!1}):null,T=p.get(I);typeof T>"u"&&p.set(I,T=[]);let{usage:O}=this.getUsageByIndex(x);T.push({commandClass:S,usage:O})}let h=Array.from(p.keys()).sort((S,x)=>S===null?-1:x===null?1:S.localeCompare(x,"en",{usage:"sort",caseFirst:"upper"})),E=typeof this.binaryLabel<"u",C=typeof this.binaryVersion<"u";E||C?(E&&C?f+=`${this.format(r).header(`${this.binaryLabel} - ${this.binaryVersion}`)} `:E?f+=`${this.format(r).header(`${this.binaryLabel}`)} `:f+=`${this.format(r).header(`${this.binaryVersion}`)} `,f+=` ${this.format(r).bold(a)}${this.binaryName} `):f+=`${this.format(r).bold(a)}${this.binaryName} `;for(let S of h){let x=p.get(S).slice().sort((T,O)=>T.usage.localeCompare(O.usage,"en",{usage:"sort",caseFirst:"upper"})),I=S!==null?S.trim():"General commands";f+=` `,f+=`${this.format(r).header(`${I}`)} `;for(let{commandClass:T,usage:O}of x){let U=T.usage.description||"undocumented";f+=` `,f+=` ${this.format(r).bold(O)} `,f+=` ${Vo(U,{format:this.format(r),paragraphs:!1})}`}}f+=` `,f+=Vo("You can also print more details about any of these commands by calling them with the `-h,--help` flag right after the command name.",{format:this.format(r),paragraphs:!0})}return f}error(t,r){var s,{colored:a,command:n=(s=t[Are])!==null&&s!==void 0?s:null}=r===void 0?{}:r;(!t||typeof t!="object"||!("stack"in t))&&(t=new Error(`Execution failed with a non-error rejection (rejected value: ${JSON.stringify(t)})`));let c="",f=t.name.replace(/([a-z])([A-Z])/g,"$1 $2");f==="Error"&&(f="Internal Error"),c+=`${this.format(a).error(f)}: ${t.message} `;let p=t.clipanion;return typeof p<"u"?p.type==="usage"&&(c+=` `,c+=this.usage(n)):t.stack&&(c+=`${t.stack.replace(/^.*\n/,"")} `),c}format(t){var r;return((r=t??this.enableColors)!==null&&r!==void 0?r:e.defaultContext.colorDepth>1)?zte:Xte}getUsageByRegistration(t,r){let s=this.registrations.get(t);if(typeof s>"u")throw new Error("Assertion failed: Unregistered command");return this.getUsageByIndex(s.index,r)}getUsageByIndex(t,r){return this.builder.getBuilderByIndex(t).usage(r)}};Sa.defaultContext={env:process.env,stdin:process.stdin,stdout:process.stdout,stderr:process.stderr,colorDepth:lre()}});var J2,yre=Xe(()=>{o0();J2=class extends at{async execute(){this.context.stdout.write(`${JSON.stringify(this.cli.definitions(),null,2)} `)}};J2.paths=[["--clipanion=definitions"]]});var K2,Ere=Xe(()=>{o0();K2=class extends at{async execute(){this.context.stdout.write(this.cli.usage())}};K2.paths=[["-h"],["--help"]]});function Hx(e={}){return Ba({definition(t,r){var s;t.addProxy({name:(s=e.name)!==null&&s!==void 0?s:r,required:e.required})},transformer(t,r,s){return s.positionals.map(({value:a})=>a)}})}var o_=Xe(()=>{Bp()});var z2,Ire=Xe(()=>{o0();o_();z2=class extends at{constructor(){super(...arguments),this.args=Hx()}async execute(){this.context.stdout.write(`${JSON.stringify(this.cli.process(this.args).tokens,null,2)} `)}};z2.paths=[["--clipanion=tokens"]]});var X2,Cre=Xe(()=>{o0();X2=class extends at{async execute(){var t;this.context.stdout.write(`${(t=this.cli.binaryVersion)!==null&&t!==void 0?t:""} `)}};X2.paths=[["-v"],["--version"]]});var a_={};Vt(a_,{DefinitionsCommand:()=>J2,HelpCommand:()=>K2,TokensCommand:()=>z2,VersionCommand:()=>X2});var wre=Xe(()=>{yre();Ere();Ire();Cre()});function Bre(e,t,r){let[s,a]=Yf(t,r??{}),{arity:n=1}=a,c=e.split(","),f=new Set(c);return Ba({definition(p){p.addOption({names:c,arity:n,hidden:a?.hidden,description:a?.description,required:a.required})},transformer(p,h,E){let C,S=typeof s<"u"?[...s]:void 0;for(let{name:x,value:I}of E.options)f.has(x)&&(C=x,S=S??[],S.push(I));return typeof S<"u"?Fg(C??h,S,a.validator):S}})}var vre=Xe(()=>{Bp()});function Sre(e,t,r){let[s,a]=Yf(t,r??{}),n=e.split(","),c=new Set(n);return Ba({definition(f){f.addOption({names:n,allowBinding:!1,arity:0,hidden:a.hidden,description:a.description,required:a.required})},transformer(f,p,h){let E=s;for(let{name:C,value:S}of h.options)c.has(C)&&(E=S);return E}})}var Dre=Xe(()=>{Bp()});function bre(e,t,r){let[s,a]=Yf(t,r??{}),n=e.split(","),c=new Set(n);return Ba({definition(f){f.addOption({names:n,allowBinding:!1,arity:0,hidden:a.hidden,description:a.description,required:a.required})},transformer(f,p,h){let E=s;for(let{name:C,value:S}of h.options)c.has(C)&&(E??(E=0),S?E+=1:E=0);return E}})}var Pre=Xe(()=>{Bp()});function xre(e={}){return Ba({definition(t,r){var s;t.addRest({name:(s=e.name)!==null&&s!==void 0?s:r,required:e.required})},transformer(t,r,s){let a=c=>{let f=s.positionals[c];return f.extra===Vl||f.extra===!1&&cc)}})}var kre=Xe(()=>{Mx();Bp()});function fqe(e,t,r){let[s,a]=Yf(t,r??{}),{arity:n=1}=a,c=e.split(","),f=new Set(c);return Ba({definition(p){p.addOption({names:c,arity:a.tolerateBoolean?0:n,hidden:a.hidden,description:a.description,required:a.required})},transformer(p,h,E,C){let S,x=s;typeof a.env<"u"&&C.env[a.env]&&(S=a.env,x=C.env[a.env]);for(let{name:I,value:T}of E.options)f.has(I)&&(S=I,x=T);return typeof x=="string"?Fg(S??h,x,a.validator):x}})}function Aqe(e={}){let{required:t=!0}=e;return Ba({definition(r,s){var a;r.addPositional({name:(a=e.name)!==null&&a!==void 0?a:s,required:e.required})},transformer(r,s,a){var n;for(let c=0;c{Mx();Bp()});var he={};Vt(he,{Array:()=>Bre,Boolean:()=>Sre,Counter:()=>bre,Proxy:()=>Hx,Rest:()=>xre,String:()=>Qre,applyValidator:()=>Fg,cleanValidationError:()=>Qx,formatError:()=>j2,isOptionSymbol:()=>H2,makeCommandOption:()=>Ba,rerouteArguments:()=>Yf});var Tre=Xe(()=>{Bp();o_();vre();Dre();Pre();kre();Rre()});var Z2={};Vt(Z2,{Builtins:()=>a_,Cli:()=>Sa,Command:()=>at,Option:()=>he,UsageError:()=>it,formatMarkdownish:()=>Vo,run:()=>dre,runExit:()=>hre});var Yt=Xe(()=>{kx();KU();o0();mre();wre();Tre()});var Fre=G((ckt,pqe)=>{pqe.exports={name:"dotenv",version:"16.3.1",description:"Loads environment variables from .env file",main:"lib/main.js",types:"lib/main.d.ts",exports:{".":{types:"./lib/main.d.ts",require:"./lib/main.js",default:"./lib/main.js"},"./config":"./config.js","./config.js":"./config.js","./lib/env-options":"./lib/env-options.js","./lib/env-options.js":"./lib/env-options.js","./lib/cli-options":"./lib/cli-options.js","./lib/cli-options.js":"./lib/cli-options.js","./package.json":"./package.json"},scripts:{"dts-check":"tsc --project tests/types/tsconfig.json",lint:"standard","lint-readme":"standard-markdown",pretest:"npm run lint && npm run dts-check",test:"tap tests/*.js --100 -Rspec",prerelease:"npm test",release:"standard-version"},repository:{type:"git",url:"git://github.com/motdotla/dotenv.git"},funding:"https://github.com/motdotla/dotenv?sponsor=1",keywords:["dotenv","env",".env","environment","variables","config","settings"],readmeFilename:"README.md",license:"BSD-2-Clause",devDependencies:{"@definitelytyped/dtslint":"^0.0.133","@types/node":"^18.11.3",decache:"^4.6.1",sinon:"^14.0.1",standard:"^17.0.0","standard-markdown":"^7.1.0","standard-version":"^9.5.0",tap:"^16.3.0",tar:"^6.1.11",typescript:"^4.8.4"},engines:{node:">=12"},browser:{fs:!1}}});var Mre=G((ukt,vp)=>{var Nre=Ie("fs"),c_=Ie("path"),hqe=Ie("os"),dqe=Ie("crypto"),gqe=Fre(),u_=gqe.version,mqe=/(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg;function yqe(e){let t={},r=e.toString();r=r.replace(/\r\n?/mg,` `);let s;for(;(s=mqe.exec(r))!=null;){let a=s[1],n=s[2]||"";n=n.trim();let c=n[0];n=n.replace(/^(['"`])([\s\S]*)\1$/mg,"$2"),c==='"'&&(n=n.replace(/\\n/g,` `),n=n.replace(/\\r/g,"\r")),t[a]=n}return t}function Eqe(e){let t=Lre(e),r=Ws.configDotenv({path:t});if(!r.parsed)throw new Error(`MISSING_DATA: Cannot parse ${t} for an unknown reason`);let s=Ore(e).split(","),a=s.length,n;for(let c=0;c=a)throw f}return Ws.parse(n)}function Iqe(e){console.log(`[dotenv@${u_}][INFO] ${e}`)}function Cqe(e){console.log(`[dotenv@${u_}][WARN] ${e}`)}function l_(e){console.log(`[dotenv@${u_}][DEBUG] ${e}`)}function Ore(e){return e&&e.DOTENV_KEY&&e.DOTENV_KEY.length>0?e.DOTENV_KEY:process.env.DOTENV_KEY&&process.env.DOTENV_KEY.length>0?process.env.DOTENV_KEY:""}function wqe(e,t){let r;try{r=new URL(t)}catch(f){throw f.code==="ERR_INVALID_URL"?new Error("INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenv.org/vault/.env.vault?environment=development"):f}let s=r.password;if(!s)throw new Error("INVALID_DOTENV_KEY: Missing key part");let a=r.searchParams.get("environment");if(!a)throw new Error("INVALID_DOTENV_KEY: Missing environment part");let n=`DOTENV_VAULT_${a.toUpperCase()}`,c=e.parsed[n];if(!c)throw new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${n} in your .env.vault file.`);return{ciphertext:c,key:s}}function Lre(e){let t=c_.resolve(process.cwd(),".env");return e&&e.path&&e.path.length>0&&(t=e.path),t.endsWith(".vault")?t:`${t}.vault`}function Bqe(e){return e[0]==="~"?c_.join(hqe.homedir(),e.slice(1)):e}function vqe(e){Iqe("Loading env from encrypted .env.vault");let t=Ws._parseVault(e),r=process.env;return e&&e.processEnv!=null&&(r=e.processEnv),Ws.populate(r,t,e),{parsed:t}}function Sqe(e){let t=c_.resolve(process.cwd(),".env"),r="utf8",s=!!(e&&e.debug);e&&(e.path!=null&&(t=Bqe(e.path)),e.encoding!=null&&(r=e.encoding));try{let a=Ws.parse(Nre.readFileSync(t,{encoding:r})),n=process.env;return e&&e.processEnv!=null&&(n=e.processEnv),Ws.populate(n,a,e),{parsed:a}}catch(a){return s&&l_(`Failed to load ${t} ${a.message}`),{error:a}}}function Dqe(e){let t=Lre(e);return Ore(e).length===0?Ws.configDotenv(e):Nre.existsSync(t)?Ws._configVault(e):(Cqe(`You set DOTENV_KEY but you are missing a .env.vault file at ${t}. Did you forget to build it?`),Ws.configDotenv(e))}function bqe(e,t){let r=Buffer.from(t.slice(-64),"hex"),s=Buffer.from(e,"base64"),a=s.slice(0,12),n=s.slice(-16);s=s.slice(12,-16);try{let c=dqe.createDecipheriv("aes-256-gcm",r,a);return c.setAuthTag(n),`${c.update(s)}${c.final()}`}catch(c){let f=c instanceof RangeError,p=c.message==="Invalid key length",h=c.message==="Unsupported state or unable to authenticate data";if(f||p){let E="INVALID_DOTENV_KEY: It must be 64 characters long (or more)";throw new Error(E)}else if(h){let E="DECRYPTION_FAILED: Please check your DOTENV_KEY";throw new Error(E)}else throw console.error("Error: ",c.code),console.error("Error: ",c.message),c}}function Pqe(e,t,r={}){let s=!!(r&&r.debug),a=!!(r&&r.override);if(typeof t!="object")throw new Error("OBJECT_REQUIRED: Please check the processEnv argument being passed to populate");for(let n of Object.keys(t))Object.prototype.hasOwnProperty.call(e,n)?(a===!0&&(e[n]=t[n]),s&&l_(a===!0?`"${n}" is already defined and WAS overwritten`:`"${n}" is already defined and was NOT overwritten`)):e[n]=t[n]}var Ws={configDotenv:Sqe,_configVault:vqe,_parseVault:Eqe,config:Dqe,decrypt:bqe,parse:yqe,populate:Pqe};vp.exports.configDotenv=Ws.configDotenv;vp.exports._configVault=Ws._configVault;vp.exports._parseVault=Ws._parseVault;vp.exports.config=Ws.config;vp.exports.decrypt=Ws.decrypt;vp.exports.parse=Ws.parse;vp.exports.populate=Ws.populate;vp.exports=Ws});var _re=G((fkt,Ure)=>{"use strict";Ure.exports=(e,...t)=>new Promise(r=>{r(e(...t))})});var Ng=G((Akt,f_)=>{"use strict";var xqe=_re(),Hre=e=>{if(e<1)throw new TypeError("Expected `concurrency` to be a number from 1 and up");let t=[],r=0,s=()=>{r--,t.length>0&&t.shift()()},a=(f,p,...h)=>{r++;let E=xqe(f,...h);p(E),E.then(s,s)},n=(f,p,...h)=>{rnew Promise(h=>n(f,h,...p));return Object.defineProperties(c,{activeCount:{get:()=>r},pendingCount:{get:()=>t.length}}),c};f_.exports=Hre;f_.exports.default=Hre});function Kf(e){return`YN${e.toString(10).padStart(4,"0")}`}function jx(e){let t=Number(e.slice(2));if(typeof Ir[t]>"u")throw new Error(`Unknown message name: "${e}"`);return t}var Ir,Gx=Xe(()=>{Ir=(Ue=>(Ue[Ue.UNNAMED=0]="UNNAMED",Ue[Ue.EXCEPTION=1]="EXCEPTION",Ue[Ue.MISSING_PEER_DEPENDENCY=2]="MISSING_PEER_DEPENDENCY",Ue[Ue.CYCLIC_DEPENDENCIES=3]="CYCLIC_DEPENDENCIES",Ue[Ue.DISABLED_BUILD_SCRIPTS=4]="DISABLED_BUILD_SCRIPTS",Ue[Ue.BUILD_DISABLED=5]="BUILD_DISABLED",Ue[Ue.SOFT_LINK_BUILD=6]="SOFT_LINK_BUILD",Ue[Ue.MUST_BUILD=7]="MUST_BUILD",Ue[Ue.MUST_REBUILD=8]="MUST_REBUILD",Ue[Ue.BUILD_FAILED=9]="BUILD_FAILED",Ue[Ue.RESOLVER_NOT_FOUND=10]="RESOLVER_NOT_FOUND",Ue[Ue.FETCHER_NOT_FOUND=11]="FETCHER_NOT_FOUND",Ue[Ue.LINKER_NOT_FOUND=12]="LINKER_NOT_FOUND",Ue[Ue.FETCH_NOT_CACHED=13]="FETCH_NOT_CACHED",Ue[Ue.YARN_IMPORT_FAILED=14]="YARN_IMPORT_FAILED",Ue[Ue.REMOTE_INVALID=15]="REMOTE_INVALID",Ue[Ue.REMOTE_NOT_FOUND=16]="REMOTE_NOT_FOUND",Ue[Ue.RESOLUTION_PACK=17]="RESOLUTION_PACK",Ue[Ue.CACHE_CHECKSUM_MISMATCH=18]="CACHE_CHECKSUM_MISMATCH",Ue[Ue.UNUSED_CACHE_ENTRY=19]="UNUSED_CACHE_ENTRY",Ue[Ue.MISSING_LOCKFILE_ENTRY=20]="MISSING_LOCKFILE_ENTRY",Ue[Ue.WORKSPACE_NOT_FOUND=21]="WORKSPACE_NOT_FOUND",Ue[Ue.TOO_MANY_MATCHING_WORKSPACES=22]="TOO_MANY_MATCHING_WORKSPACES",Ue[Ue.CONSTRAINTS_MISSING_DEPENDENCY=23]="CONSTRAINTS_MISSING_DEPENDENCY",Ue[Ue.CONSTRAINTS_INCOMPATIBLE_DEPENDENCY=24]="CONSTRAINTS_INCOMPATIBLE_DEPENDENCY",Ue[Ue.CONSTRAINTS_EXTRANEOUS_DEPENDENCY=25]="CONSTRAINTS_EXTRANEOUS_DEPENDENCY",Ue[Ue.CONSTRAINTS_INVALID_DEPENDENCY=26]="CONSTRAINTS_INVALID_DEPENDENCY",Ue[Ue.CANT_SUGGEST_RESOLUTIONS=27]="CANT_SUGGEST_RESOLUTIONS",Ue[Ue.FROZEN_LOCKFILE_EXCEPTION=28]="FROZEN_LOCKFILE_EXCEPTION",Ue[Ue.CROSS_DRIVE_VIRTUAL_LOCAL=29]="CROSS_DRIVE_VIRTUAL_LOCAL",Ue[Ue.FETCH_FAILED=30]="FETCH_FAILED",Ue[Ue.DANGEROUS_NODE_MODULES=31]="DANGEROUS_NODE_MODULES",Ue[Ue.NODE_GYP_INJECTED=32]="NODE_GYP_INJECTED",Ue[Ue.AUTHENTICATION_NOT_FOUND=33]="AUTHENTICATION_NOT_FOUND",Ue[Ue.INVALID_CONFIGURATION_KEY=34]="INVALID_CONFIGURATION_KEY",Ue[Ue.NETWORK_ERROR=35]="NETWORK_ERROR",Ue[Ue.LIFECYCLE_SCRIPT=36]="LIFECYCLE_SCRIPT",Ue[Ue.CONSTRAINTS_MISSING_FIELD=37]="CONSTRAINTS_MISSING_FIELD",Ue[Ue.CONSTRAINTS_INCOMPATIBLE_FIELD=38]="CONSTRAINTS_INCOMPATIBLE_FIELD",Ue[Ue.CONSTRAINTS_EXTRANEOUS_FIELD=39]="CONSTRAINTS_EXTRANEOUS_FIELD",Ue[Ue.CONSTRAINTS_INVALID_FIELD=40]="CONSTRAINTS_INVALID_FIELD",Ue[Ue.AUTHENTICATION_INVALID=41]="AUTHENTICATION_INVALID",Ue[Ue.PROLOG_UNKNOWN_ERROR=42]="PROLOG_UNKNOWN_ERROR",Ue[Ue.PROLOG_SYNTAX_ERROR=43]="PROLOG_SYNTAX_ERROR",Ue[Ue.PROLOG_EXISTENCE_ERROR=44]="PROLOG_EXISTENCE_ERROR",Ue[Ue.STACK_OVERFLOW_RESOLUTION=45]="STACK_OVERFLOW_RESOLUTION",Ue[Ue.AUTOMERGE_FAILED_TO_PARSE=46]="AUTOMERGE_FAILED_TO_PARSE",Ue[Ue.AUTOMERGE_IMMUTABLE=47]="AUTOMERGE_IMMUTABLE",Ue[Ue.AUTOMERGE_SUCCESS=48]="AUTOMERGE_SUCCESS",Ue[Ue.AUTOMERGE_REQUIRED=49]="AUTOMERGE_REQUIRED",Ue[Ue.DEPRECATED_CLI_SETTINGS=50]="DEPRECATED_CLI_SETTINGS",Ue[Ue.PLUGIN_NAME_NOT_FOUND=51]="PLUGIN_NAME_NOT_FOUND",Ue[Ue.INVALID_PLUGIN_REFERENCE=52]="INVALID_PLUGIN_REFERENCE",Ue[Ue.CONSTRAINTS_AMBIGUITY=53]="CONSTRAINTS_AMBIGUITY",Ue[Ue.CACHE_OUTSIDE_PROJECT=54]="CACHE_OUTSIDE_PROJECT",Ue[Ue.IMMUTABLE_INSTALL=55]="IMMUTABLE_INSTALL",Ue[Ue.IMMUTABLE_CACHE=56]="IMMUTABLE_CACHE",Ue[Ue.INVALID_MANIFEST=57]="INVALID_MANIFEST",Ue[Ue.PACKAGE_PREPARATION_FAILED=58]="PACKAGE_PREPARATION_FAILED",Ue[Ue.INVALID_RANGE_PEER_DEPENDENCY=59]="INVALID_RANGE_PEER_DEPENDENCY",Ue[Ue.INCOMPATIBLE_PEER_DEPENDENCY=60]="INCOMPATIBLE_PEER_DEPENDENCY",Ue[Ue.DEPRECATED_PACKAGE=61]="DEPRECATED_PACKAGE",Ue[Ue.INCOMPATIBLE_OS=62]="INCOMPATIBLE_OS",Ue[Ue.INCOMPATIBLE_CPU=63]="INCOMPATIBLE_CPU",Ue[Ue.FROZEN_ARTIFACT_EXCEPTION=64]="FROZEN_ARTIFACT_EXCEPTION",Ue[Ue.TELEMETRY_NOTICE=65]="TELEMETRY_NOTICE",Ue[Ue.PATCH_HUNK_FAILED=66]="PATCH_HUNK_FAILED",Ue[Ue.INVALID_CONFIGURATION_VALUE=67]="INVALID_CONFIGURATION_VALUE",Ue[Ue.UNUSED_PACKAGE_EXTENSION=68]="UNUSED_PACKAGE_EXTENSION",Ue[Ue.REDUNDANT_PACKAGE_EXTENSION=69]="REDUNDANT_PACKAGE_EXTENSION",Ue[Ue.AUTO_NM_SUCCESS=70]="AUTO_NM_SUCCESS",Ue[Ue.NM_CANT_INSTALL_EXTERNAL_SOFT_LINK=71]="NM_CANT_INSTALL_EXTERNAL_SOFT_LINK",Ue[Ue.NM_PRESERVE_SYMLINKS_REQUIRED=72]="NM_PRESERVE_SYMLINKS_REQUIRED",Ue[Ue.UPDATE_LOCKFILE_ONLY_SKIP_LINK=73]="UPDATE_LOCKFILE_ONLY_SKIP_LINK",Ue[Ue.NM_HARDLINKS_MODE_DOWNGRADED=74]="NM_HARDLINKS_MODE_DOWNGRADED",Ue[Ue.PROLOG_INSTANTIATION_ERROR=75]="PROLOG_INSTANTIATION_ERROR",Ue[Ue.INCOMPATIBLE_ARCHITECTURE=76]="INCOMPATIBLE_ARCHITECTURE",Ue[Ue.GHOST_ARCHITECTURE=77]="GHOST_ARCHITECTURE",Ue[Ue.RESOLUTION_MISMATCH=78]="RESOLUTION_MISMATCH",Ue[Ue.PROLOG_LIMIT_EXCEEDED=79]="PROLOG_LIMIT_EXCEEDED",Ue[Ue.NETWORK_DISABLED=80]="NETWORK_DISABLED",Ue[Ue.NETWORK_UNSAFE_HTTP=81]="NETWORK_UNSAFE_HTTP",Ue[Ue.RESOLUTION_FAILED=82]="RESOLUTION_FAILED",Ue[Ue.AUTOMERGE_GIT_ERROR=83]="AUTOMERGE_GIT_ERROR",Ue[Ue.CONSTRAINTS_CHECK_FAILED=84]="CONSTRAINTS_CHECK_FAILED",Ue[Ue.UPDATED_RESOLUTION_RECORD=85]="UPDATED_RESOLUTION_RECORD",Ue[Ue.EXPLAIN_PEER_DEPENDENCIES_CTA=86]="EXPLAIN_PEER_DEPENDENCIES_CTA",Ue[Ue.MIGRATION_SUCCESS=87]="MIGRATION_SUCCESS",Ue[Ue.VERSION_NOTICE=88]="VERSION_NOTICE",Ue[Ue.TIPS_NOTICE=89]="TIPS_NOTICE",Ue[Ue.OFFLINE_MODE_ENABLED=90]="OFFLINE_MODE_ENABLED",Ue[Ue.INVALID_PROVENANCE_ENVIRONMENT=91]="INVALID_PROVENANCE_ENVIRONMENT",Ue[Ue.EXPERIMENTAL=92]="EXPERIMENTAL",Ue))(Ir||{})});var $2=G((hkt,jre)=>{var kqe="2.0.0",Qqe=Number.MAX_SAFE_INTEGER||9007199254740991,Rqe=16,Tqe=250,Fqe=["major","premajor","minor","preminor","patch","prepatch","prerelease"];jre.exports={MAX_LENGTH:256,MAX_SAFE_COMPONENT_LENGTH:Rqe,MAX_SAFE_BUILD_LENGTH:Tqe,MAX_SAFE_INTEGER:Qqe,RELEASE_TYPES:Fqe,SEMVER_SPEC_VERSION:kqe,FLAG_INCLUDE_PRERELEASE:1,FLAG_LOOSE:2}});var eB=G((dkt,Gre)=>{var Nqe=typeof process=="object"&&process.env&&process.env.NODE_DEBUG&&/\bsemver\b/i.test(process.env.NODE_DEBUG)?(...e)=>console.error("SEMVER",...e):()=>{};Gre.exports=Nqe});var bE=G((Sp,qre)=>{var{MAX_SAFE_COMPONENT_LENGTH:A_,MAX_SAFE_BUILD_LENGTH:Oqe,MAX_LENGTH:Lqe}=$2(),Mqe=eB();Sp=qre.exports={};var Uqe=Sp.re=[],_qe=Sp.safeRe=[],rr=Sp.src=[],nr=Sp.t={},Hqe=0,p_="[a-zA-Z0-9-]",jqe=[["\\s",1],["\\d",Lqe],[p_,Oqe]],Gqe=e=>{for(let[t,r]of jqe)e=e.split(`${t}*`).join(`${t}{0,${r}}`).split(`${t}+`).join(`${t}{1,${r}}`);return e},Jr=(e,t,r)=>{let s=Gqe(t),a=Hqe++;Mqe(e,a,t),nr[e]=a,rr[a]=t,Uqe[a]=new RegExp(t,r?"g":void 0),_qe[a]=new RegExp(s,r?"g":void 0)};Jr("NUMERICIDENTIFIER","0|[1-9]\\d*");Jr("NUMERICIDENTIFIERLOOSE","\\d+");Jr("NONNUMERICIDENTIFIER",`\\d*[a-zA-Z-]${p_}*`);Jr("MAINVERSION",`(${rr[nr.NUMERICIDENTIFIER]})\\.(${rr[nr.NUMERICIDENTIFIER]})\\.(${rr[nr.NUMERICIDENTIFIER]})`);Jr("MAINVERSIONLOOSE",`(${rr[nr.NUMERICIDENTIFIERLOOSE]})\\.(${rr[nr.NUMERICIDENTIFIERLOOSE]})\\.(${rr[nr.NUMERICIDENTIFIERLOOSE]})`);Jr("PRERELEASEIDENTIFIER",`(?:${rr[nr.NUMERICIDENTIFIER]}|${rr[nr.NONNUMERICIDENTIFIER]})`);Jr("PRERELEASEIDENTIFIERLOOSE",`(?:${rr[nr.NUMERICIDENTIFIERLOOSE]}|${rr[nr.NONNUMERICIDENTIFIER]})`);Jr("PRERELEASE",`(?:-(${rr[nr.PRERELEASEIDENTIFIER]}(?:\\.${rr[nr.PRERELEASEIDENTIFIER]})*))`);Jr("PRERELEASELOOSE",`(?:-?(${rr[nr.PRERELEASEIDENTIFIERLOOSE]}(?:\\.${rr[nr.PRERELEASEIDENTIFIERLOOSE]})*))`);Jr("BUILDIDENTIFIER",`${p_}+`);Jr("BUILD",`(?:\\+(${rr[nr.BUILDIDENTIFIER]}(?:\\.${rr[nr.BUILDIDENTIFIER]})*))`);Jr("FULLPLAIN",`v?${rr[nr.MAINVERSION]}${rr[nr.PRERELEASE]}?${rr[nr.BUILD]}?`);Jr("FULL",`^${rr[nr.FULLPLAIN]}$`);Jr("LOOSEPLAIN",`[v=\\s]*${rr[nr.MAINVERSIONLOOSE]}${rr[nr.PRERELEASELOOSE]}?${rr[nr.BUILD]}?`);Jr("LOOSE",`^${rr[nr.LOOSEPLAIN]}$`);Jr("GTLT","((?:<|>)?=?)");Jr("XRANGEIDENTIFIERLOOSE",`${rr[nr.NUMERICIDENTIFIERLOOSE]}|x|X|\\*`);Jr("XRANGEIDENTIFIER",`${rr[nr.NUMERICIDENTIFIER]}|x|X|\\*`);Jr("XRANGEPLAIN",`[v=\\s]*(${rr[nr.XRANGEIDENTIFIER]})(?:\\.(${rr[nr.XRANGEIDENTIFIER]})(?:\\.(${rr[nr.XRANGEIDENTIFIER]})(?:${rr[nr.PRERELEASE]})?${rr[nr.BUILD]}?)?)?`);Jr("XRANGEPLAINLOOSE",`[v=\\s]*(${rr[nr.XRANGEIDENTIFIERLOOSE]})(?:\\.(${rr[nr.XRANGEIDENTIFIERLOOSE]})(?:\\.(${rr[nr.XRANGEIDENTIFIERLOOSE]})(?:${rr[nr.PRERELEASELOOSE]})?${rr[nr.BUILD]}?)?)?`);Jr("XRANGE",`^${rr[nr.GTLT]}\\s*${rr[nr.XRANGEPLAIN]}$`);Jr("XRANGELOOSE",`^${rr[nr.GTLT]}\\s*${rr[nr.XRANGEPLAINLOOSE]}$`);Jr("COERCEPLAIN",`(^|[^\\d])(\\d{1,${A_}})(?:\\.(\\d{1,${A_}}))?(?:\\.(\\d{1,${A_}}))?`);Jr("COERCE",`${rr[nr.COERCEPLAIN]}(?:$|[^\\d])`);Jr("COERCEFULL",rr[nr.COERCEPLAIN]+`(?:${rr[nr.PRERELEASE]})?(?:${rr[nr.BUILD]})?(?:$|[^\\d])`);Jr("COERCERTL",rr[nr.COERCE],!0);Jr("COERCERTLFULL",rr[nr.COERCEFULL],!0);Jr("LONETILDE","(?:~>?)");Jr("TILDETRIM",`(\\s*)${rr[nr.LONETILDE]}\\s+`,!0);Sp.tildeTrimReplace="$1~";Jr("TILDE",`^${rr[nr.LONETILDE]}${rr[nr.XRANGEPLAIN]}$`);Jr("TILDELOOSE",`^${rr[nr.LONETILDE]}${rr[nr.XRANGEPLAINLOOSE]}$`);Jr("LONECARET","(?:\\^)");Jr("CARETTRIM",`(\\s*)${rr[nr.LONECARET]}\\s+`,!0);Sp.caretTrimReplace="$1^";Jr("CARET",`^${rr[nr.LONECARET]}${rr[nr.XRANGEPLAIN]}$`);Jr("CARETLOOSE",`^${rr[nr.LONECARET]}${rr[nr.XRANGEPLAINLOOSE]}$`);Jr("COMPARATORLOOSE",`^${rr[nr.GTLT]}\\s*(${rr[nr.LOOSEPLAIN]})$|^$`);Jr("COMPARATOR",`^${rr[nr.GTLT]}\\s*(${rr[nr.FULLPLAIN]})$|^$`);Jr("COMPARATORTRIM",`(\\s*)${rr[nr.GTLT]}\\s*(${rr[nr.LOOSEPLAIN]}|${rr[nr.XRANGEPLAIN]})`,!0);Sp.comparatorTrimReplace="$1$2$3";Jr("HYPHENRANGE",`^\\s*(${rr[nr.XRANGEPLAIN]})\\s+-\\s+(${rr[nr.XRANGEPLAIN]})\\s*$`);Jr("HYPHENRANGELOOSE",`^\\s*(${rr[nr.XRANGEPLAINLOOSE]})\\s+-\\s+(${rr[nr.XRANGEPLAINLOOSE]})\\s*$`);Jr("STAR","(<|>)?=?\\s*\\*");Jr("GTE0","^\\s*>=\\s*0\\.0\\.0\\s*$");Jr("GTE0PRE","^\\s*>=\\s*0\\.0\\.0-0\\s*$")});var qx=G((gkt,Wre)=>{var qqe=Object.freeze({loose:!0}),Wqe=Object.freeze({}),Yqe=e=>e?typeof e!="object"?qqe:e:Wqe;Wre.exports=Yqe});var h_=G((mkt,Jre)=>{var Yre=/^[0-9]+$/,Vre=(e,t)=>{let r=Yre.test(e),s=Yre.test(t);return r&&s&&(e=+e,t=+t),e===t?0:r&&!s?-1:s&&!r?1:eVre(t,e);Jre.exports={compareIdentifiers:Vre,rcompareIdentifiers:Vqe}});var Ko=G((ykt,Zre)=>{var Wx=eB(),{MAX_LENGTH:Kre,MAX_SAFE_INTEGER:Yx}=$2(),{safeRe:zre,t:Xre}=bE(),Jqe=qx(),{compareIdentifiers:PE}=h_(),d_=class e{constructor(t,r){if(r=Jqe(r),t instanceof e){if(t.loose===!!r.loose&&t.includePrerelease===!!r.includePrerelease)return t;t=t.version}else if(typeof t!="string")throw new TypeError(`Invalid version. Must be a string. Got type "${typeof t}".`);if(t.length>Kre)throw new TypeError(`version is longer than ${Kre} characters`);Wx("SemVer",t,r),this.options=r,this.loose=!!r.loose,this.includePrerelease=!!r.includePrerelease;let s=t.trim().match(r.loose?zre[Xre.LOOSE]:zre[Xre.FULL]);if(!s)throw new TypeError(`Invalid Version: ${t}`);if(this.raw=t,this.major=+s[1],this.minor=+s[2],this.patch=+s[3],this.major>Yx||this.major<0)throw new TypeError("Invalid major version");if(this.minor>Yx||this.minor<0)throw new TypeError("Invalid minor version");if(this.patch>Yx||this.patch<0)throw new TypeError("Invalid patch version");s[4]?this.prerelease=s[4].split(".").map(a=>{if(/^[0-9]+$/.test(a)){let n=+a;if(n>=0&&n=0;)typeof this.prerelease[n]=="number"&&(this.prerelease[n]++,n=-2);if(n===-1){if(r===this.prerelease.join(".")&&s===!1)throw new Error("invalid increment argument: identifier already exists");this.prerelease.push(a)}}if(r){let n=[r,a];s===!1&&(n=[r]),PE(this.prerelease[0],r)===0?isNaN(this.prerelease[1])&&(this.prerelease=n):this.prerelease=n}break}default:throw new Error(`invalid increment argument: ${t}`)}return this.raw=this.format(),this.build.length&&(this.raw+=`+${this.build.join(".")}`),this}};Zre.exports=d_});var Og=G((Ekt,ene)=>{var $re=Ko(),Kqe=(e,t,r=!1)=>{if(e instanceof $re)return e;try{return new $re(e,t)}catch(s){if(!r)return null;throw s}};ene.exports=Kqe});var rne=G((Ikt,tne)=>{var zqe=Og(),Xqe=(e,t)=>{let r=zqe(e,t);return r?r.version:null};tne.exports=Xqe});var ine=G((Ckt,nne)=>{var Zqe=Og(),$qe=(e,t)=>{let r=Zqe(e.trim().replace(/^[=v]+/,""),t);return r?r.version:null};nne.exports=$qe});var ane=G((wkt,one)=>{var sne=Ko(),eWe=(e,t,r,s,a)=>{typeof r=="string"&&(a=s,s=r,r=void 0);try{return new sne(e instanceof sne?e.version:e,r).inc(t,s,a).version}catch{return null}};one.exports=eWe});var une=G((Bkt,cne)=>{var lne=Og(),tWe=(e,t)=>{let r=lne(e,null,!0),s=lne(t,null,!0),a=r.compare(s);if(a===0)return null;let n=a>0,c=n?r:s,f=n?s:r,p=!!c.prerelease.length;if(!!f.prerelease.length&&!p)return!f.patch&&!f.minor?"major":c.patch?"patch":c.minor?"minor":"major";let E=p?"pre":"";return r.major!==s.major?E+"major":r.minor!==s.minor?E+"minor":r.patch!==s.patch?E+"patch":"prerelease"};cne.exports=tWe});var Ane=G((vkt,fne)=>{var rWe=Ko(),nWe=(e,t)=>new rWe(e,t).major;fne.exports=nWe});var hne=G((Skt,pne)=>{var iWe=Ko(),sWe=(e,t)=>new iWe(e,t).minor;pne.exports=sWe});var gne=G((Dkt,dne)=>{var oWe=Ko(),aWe=(e,t)=>new oWe(e,t).patch;dne.exports=aWe});var yne=G((bkt,mne)=>{var lWe=Og(),cWe=(e,t)=>{let r=lWe(e,t);return r&&r.prerelease.length?r.prerelease:null};mne.exports=cWe});var Sc=G((Pkt,Ine)=>{var Ene=Ko(),uWe=(e,t,r)=>new Ene(e,r).compare(new Ene(t,r));Ine.exports=uWe});var wne=G((xkt,Cne)=>{var fWe=Sc(),AWe=(e,t,r)=>fWe(t,e,r);Cne.exports=AWe});var vne=G((kkt,Bne)=>{var pWe=Sc(),hWe=(e,t)=>pWe(e,t,!0);Bne.exports=hWe});var Vx=G((Qkt,Dne)=>{var Sne=Ko(),dWe=(e,t,r)=>{let s=new Sne(e,r),a=new Sne(t,r);return s.compare(a)||s.compareBuild(a)};Dne.exports=dWe});var Pne=G((Rkt,bne)=>{var gWe=Vx(),mWe=(e,t)=>e.sort((r,s)=>gWe(r,s,t));bne.exports=mWe});var kne=G((Tkt,xne)=>{var yWe=Vx(),EWe=(e,t)=>e.sort((r,s)=>yWe(s,r,t));xne.exports=EWe});var tB=G((Fkt,Qne)=>{var IWe=Sc(),CWe=(e,t,r)=>IWe(e,t,r)>0;Qne.exports=CWe});var Jx=G((Nkt,Rne)=>{var wWe=Sc(),BWe=(e,t,r)=>wWe(e,t,r)<0;Rne.exports=BWe});var g_=G((Okt,Tne)=>{var vWe=Sc(),SWe=(e,t,r)=>vWe(e,t,r)===0;Tne.exports=SWe});var m_=G((Lkt,Fne)=>{var DWe=Sc(),bWe=(e,t,r)=>DWe(e,t,r)!==0;Fne.exports=bWe});var Kx=G((Mkt,Nne)=>{var PWe=Sc(),xWe=(e,t,r)=>PWe(e,t,r)>=0;Nne.exports=xWe});var zx=G((Ukt,One)=>{var kWe=Sc(),QWe=(e,t,r)=>kWe(e,t,r)<=0;One.exports=QWe});var y_=G((_kt,Lne)=>{var RWe=g_(),TWe=m_(),FWe=tB(),NWe=Kx(),OWe=Jx(),LWe=zx(),MWe=(e,t,r,s)=>{switch(t){case"===":return typeof e=="object"&&(e=e.version),typeof r=="object"&&(r=r.version),e===r;case"!==":return typeof e=="object"&&(e=e.version),typeof r=="object"&&(r=r.version),e!==r;case"":case"=":case"==":return RWe(e,r,s);case"!=":return TWe(e,r,s);case">":return FWe(e,r,s);case">=":return NWe(e,r,s);case"<":return OWe(e,r,s);case"<=":return LWe(e,r,s);default:throw new TypeError(`Invalid operator: ${t}`)}};Lne.exports=MWe});var Une=G((Hkt,Mne)=>{var UWe=Ko(),_We=Og(),{safeRe:Xx,t:Zx}=bE(),HWe=(e,t)=>{if(e instanceof UWe)return e;if(typeof e=="number"&&(e=String(e)),typeof e!="string")return null;t=t||{};let r=null;if(!t.rtl)r=e.match(t.includePrerelease?Xx[Zx.COERCEFULL]:Xx[Zx.COERCE]);else{let p=t.includePrerelease?Xx[Zx.COERCERTLFULL]:Xx[Zx.COERCERTL],h;for(;(h=p.exec(e))&&(!r||r.index+r[0].length!==e.length);)(!r||h.index+h[0].length!==r.index+r[0].length)&&(r=h),p.lastIndex=h.index+h[1].length+h[2].length;p.lastIndex=-1}if(r===null)return null;let s=r[2],a=r[3]||"0",n=r[4]||"0",c=t.includePrerelease&&r[5]?`-${r[5]}`:"",f=t.includePrerelease&&r[6]?`+${r[6]}`:"";return _We(`${s}.${a}.${n}${c}${f}`,t)};Mne.exports=HWe});var Hne=G((jkt,_ne)=>{"use strict";_ne.exports=function(e){e.prototype[Symbol.iterator]=function*(){for(let t=this.head;t;t=t.next)yield t.value}}});var Gne=G((Gkt,jne)=>{"use strict";jne.exports=On;On.Node=Lg;On.create=On;function On(e){var t=this;if(t instanceof On||(t=new On),t.tail=null,t.head=null,t.length=0,e&&typeof e.forEach=="function")e.forEach(function(a){t.push(a)});else if(arguments.length>0)for(var r=0,s=arguments.length;r1)r=t;else if(this.head)s=this.head.next,r=this.head.value;else throw new TypeError("Reduce of empty list with no initial value");for(var a=0;s!==null;a++)r=e(r,s.value,a),s=s.next;return r};On.prototype.reduceReverse=function(e,t){var r,s=this.tail;if(arguments.length>1)r=t;else if(this.tail)s=this.tail.prev,r=this.tail.value;else throw new TypeError("Reduce of empty list with no initial value");for(var a=this.length-1;s!==null;a--)r=e(r,s.value,a),s=s.prev;return r};On.prototype.toArray=function(){for(var e=new Array(this.length),t=0,r=this.head;r!==null;t++)e[t]=r.value,r=r.next;return e};On.prototype.toArrayReverse=function(){for(var e=new Array(this.length),t=0,r=this.tail;r!==null;t++)e[t]=r.value,r=r.prev;return e};On.prototype.slice=function(e,t){t=t||this.length,t<0&&(t+=this.length),e=e||0,e<0&&(e+=this.length);var r=new On;if(tthis.length&&(t=this.length);for(var s=0,a=this.head;a!==null&&sthis.length&&(t=this.length);for(var s=this.length,a=this.tail;a!==null&&s>t;s--)a=a.prev;for(;a!==null&&s>e;s--,a=a.prev)r.push(a.value);return r};On.prototype.splice=function(e,t,...r){e>this.length&&(e=this.length-1),e<0&&(e=this.length+e);for(var s=0,a=this.head;a!==null&&s{"use strict";var WWe=Gne(),Mg=Symbol("max"),bp=Symbol("length"),xE=Symbol("lengthCalculator"),nB=Symbol("allowStale"),Ug=Symbol("maxAge"),Dp=Symbol("dispose"),qne=Symbol("noDisposeOnSet"),Ys=Symbol("lruList"),Nu=Symbol("cache"),Yne=Symbol("updateAgeOnGet"),E_=()=>1,C_=class{constructor(t){if(typeof t=="number"&&(t={max:t}),t||(t={}),t.max&&(typeof t.max!="number"||t.max<0))throw new TypeError("max must be a non-negative number");let r=this[Mg]=t.max||1/0,s=t.length||E_;if(this[xE]=typeof s!="function"?E_:s,this[nB]=t.stale||!1,t.maxAge&&typeof t.maxAge!="number")throw new TypeError("maxAge must be a number");this[Ug]=t.maxAge||0,this[Dp]=t.dispose,this[qne]=t.noDisposeOnSet||!1,this[Yne]=t.updateAgeOnGet||!1,this.reset()}set max(t){if(typeof t!="number"||t<0)throw new TypeError("max must be a non-negative number");this[Mg]=t||1/0,rB(this)}get max(){return this[Mg]}set allowStale(t){this[nB]=!!t}get allowStale(){return this[nB]}set maxAge(t){if(typeof t!="number")throw new TypeError("maxAge must be a non-negative number");this[Ug]=t,rB(this)}get maxAge(){return this[Ug]}set lengthCalculator(t){typeof t!="function"&&(t=E_),t!==this[xE]&&(this[xE]=t,this[bp]=0,this[Ys].forEach(r=>{r.length=this[xE](r.value,r.key),this[bp]+=r.length})),rB(this)}get lengthCalculator(){return this[xE]}get length(){return this[bp]}get itemCount(){return this[Ys].length}rforEach(t,r){r=r||this;for(let s=this[Ys].tail;s!==null;){let a=s.prev;Wne(this,t,s,r),s=a}}forEach(t,r){r=r||this;for(let s=this[Ys].head;s!==null;){let a=s.next;Wne(this,t,s,r),s=a}}keys(){return this[Ys].toArray().map(t=>t.key)}values(){return this[Ys].toArray().map(t=>t.value)}reset(){this[Dp]&&this[Ys]&&this[Ys].length&&this[Ys].forEach(t=>this[Dp](t.key,t.value)),this[Nu]=new Map,this[Ys]=new WWe,this[bp]=0}dump(){return this[Ys].map(t=>$x(this,t)?!1:{k:t.key,v:t.value,e:t.now+(t.maxAge||0)}).toArray().filter(t=>t)}dumpLru(){return this[Ys]}set(t,r,s){if(s=s||this[Ug],s&&typeof s!="number")throw new TypeError("maxAge must be a number");let a=s?Date.now():0,n=this[xE](r,t);if(this[Nu].has(t)){if(n>this[Mg])return kE(this,this[Nu].get(t)),!1;let p=this[Nu].get(t).value;return this[Dp]&&(this[qne]||this[Dp](t,p.value)),p.now=a,p.maxAge=s,p.value=r,this[bp]+=n-p.length,p.length=n,this.get(t),rB(this),!0}let c=new w_(t,r,n,a,s);return c.length>this[Mg]?(this[Dp]&&this[Dp](t,r),!1):(this[bp]+=c.length,this[Ys].unshift(c),this[Nu].set(t,this[Ys].head),rB(this),!0)}has(t){if(!this[Nu].has(t))return!1;let r=this[Nu].get(t).value;return!$x(this,r)}get(t){return I_(this,t,!0)}peek(t){return I_(this,t,!1)}pop(){let t=this[Ys].tail;return t?(kE(this,t),t.value):null}del(t){kE(this,this[Nu].get(t))}load(t){this.reset();let r=Date.now();for(let s=t.length-1;s>=0;s--){let a=t[s],n=a.e||0;if(n===0)this.set(a.k,a.v);else{let c=n-r;c>0&&this.set(a.k,a.v,c)}}}prune(){this[Nu].forEach((t,r)=>I_(this,r,!1))}},I_=(e,t,r)=>{let s=e[Nu].get(t);if(s){let a=s.value;if($x(e,a)){if(kE(e,s),!e[nB])return}else r&&(e[Yne]&&(s.value.now=Date.now()),e[Ys].unshiftNode(s));return a.value}},$x=(e,t)=>{if(!t||!t.maxAge&&!e[Ug])return!1;let r=Date.now()-t.now;return t.maxAge?r>t.maxAge:e[Ug]&&r>e[Ug]},rB=e=>{if(e[bp]>e[Mg])for(let t=e[Ys].tail;e[bp]>e[Mg]&&t!==null;){let r=t.prev;kE(e,t),t=r}},kE=(e,t)=>{if(t){let r=t.value;e[Dp]&&e[Dp](r.key,r.value),e[bp]-=r.length,e[Nu].delete(r.key),e[Ys].removeNode(t)}},w_=class{constructor(t,r,s,a,n){this.key=t,this.value=r,this.length=s,this.now=a,this.maxAge=n||0}},Wne=(e,t,r,s)=>{let a=r.value;$x(e,a)&&(kE(e,r),e[nB]||(a=void 0)),a&&t.call(s,a.value,a.key,e)};Vne.exports=C_});var Dc=G((Wkt,Zne)=>{var B_=class e{constructor(t,r){if(r=VWe(r),t instanceof e)return t.loose===!!r.loose&&t.includePrerelease===!!r.includePrerelease?t:new e(t.raw,r);if(t instanceof v_)return this.raw=t.value,this.set=[[t]],this.format(),this;if(this.options=r,this.loose=!!r.loose,this.includePrerelease=!!r.includePrerelease,this.raw=t.trim().split(/\s+/).join(" "),this.set=this.raw.split("||").map(s=>this.parseRange(s.trim())).filter(s=>s.length),!this.set.length)throw new TypeError(`Invalid SemVer Range: ${this.raw}`);if(this.set.length>1){let s=this.set[0];if(this.set=this.set.filter(a=>!zne(a[0])),this.set.length===0)this.set=[s];else if(this.set.length>1){for(let a of this.set)if(a.length===1&&eYe(a[0])){this.set=[a];break}}}this.format()}format(){return this.range=this.set.map(t=>t.join(" ").trim()).join("||").trim(),this.range}toString(){return this.range}parseRange(t){let s=((this.options.includePrerelease&&ZWe)|(this.options.loose&&$We))+":"+t,a=Kne.get(s);if(a)return a;let n=this.options.loose,c=n?hl[Da.HYPHENRANGELOOSE]:hl[Da.HYPHENRANGE];t=t.replace(c,uYe(this.options.includePrerelease)),Si("hyphen replace",t),t=t.replace(hl[Da.COMPARATORTRIM],KWe),Si("comparator trim",t),t=t.replace(hl[Da.TILDETRIM],zWe),Si("tilde trim",t),t=t.replace(hl[Da.CARETTRIM],XWe),Si("caret trim",t);let f=t.split(" ").map(C=>tYe(C,this.options)).join(" ").split(/\s+/).map(C=>cYe(C,this.options));n&&(f=f.filter(C=>(Si("loose invalid filter",C,this.options),!!C.match(hl[Da.COMPARATORLOOSE])))),Si("range list",f);let p=new Map,h=f.map(C=>new v_(C,this.options));for(let C of h){if(zne(C))return[C];p.set(C.value,C)}p.size>1&&p.has("")&&p.delete("");let E=[...p.values()];return Kne.set(s,E),E}intersects(t,r){if(!(t instanceof e))throw new TypeError("a Range is required");return this.set.some(s=>Xne(s,r)&&t.set.some(a=>Xne(a,r)&&s.every(n=>a.every(c=>n.intersects(c,r)))))}test(t){if(!t)return!1;if(typeof t=="string")try{t=new JWe(t,this.options)}catch{return!1}for(let r=0;re.value==="<0.0.0-0",eYe=e=>e.value==="",Xne=(e,t)=>{let r=!0,s=e.slice(),a=s.pop();for(;r&&s.length;)r=s.every(n=>a.intersects(n,t)),a=s.pop();return r},tYe=(e,t)=>(Si("comp",e,t),e=iYe(e,t),Si("caret",e),e=rYe(e,t),Si("tildes",e),e=oYe(e,t),Si("xrange",e),e=lYe(e,t),Si("stars",e),e),ba=e=>!e||e.toLowerCase()==="x"||e==="*",rYe=(e,t)=>e.trim().split(/\s+/).map(r=>nYe(r,t)).join(" "),nYe=(e,t)=>{let r=t.loose?hl[Da.TILDELOOSE]:hl[Da.TILDE];return e.replace(r,(s,a,n,c,f)=>{Si("tilde",e,s,a,n,c,f);let p;return ba(a)?p="":ba(n)?p=`>=${a}.0.0 <${+a+1}.0.0-0`:ba(c)?p=`>=${a}.${n}.0 <${a}.${+n+1}.0-0`:f?(Si("replaceTilde pr",f),p=`>=${a}.${n}.${c}-${f} <${a}.${+n+1}.0-0`):p=`>=${a}.${n}.${c} <${a}.${+n+1}.0-0`,Si("tilde return",p),p})},iYe=(e,t)=>e.trim().split(/\s+/).map(r=>sYe(r,t)).join(" "),sYe=(e,t)=>{Si("caret",e,t);let r=t.loose?hl[Da.CARETLOOSE]:hl[Da.CARET],s=t.includePrerelease?"-0":"";return e.replace(r,(a,n,c,f,p)=>{Si("caret",e,a,n,c,f,p);let h;return ba(n)?h="":ba(c)?h=`>=${n}.0.0${s} <${+n+1}.0.0-0`:ba(f)?n==="0"?h=`>=${n}.${c}.0${s} <${n}.${+c+1}.0-0`:h=`>=${n}.${c}.0${s} <${+n+1}.0.0-0`:p?(Si("replaceCaret pr",p),n==="0"?c==="0"?h=`>=${n}.${c}.${f}-${p} <${n}.${c}.${+f+1}-0`:h=`>=${n}.${c}.${f}-${p} <${n}.${+c+1}.0-0`:h=`>=${n}.${c}.${f}-${p} <${+n+1}.0.0-0`):(Si("no pr"),n==="0"?c==="0"?h=`>=${n}.${c}.${f}${s} <${n}.${c}.${+f+1}-0`:h=`>=${n}.${c}.${f}${s} <${n}.${+c+1}.0-0`:h=`>=${n}.${c}.${f} <${+n+1}.0.0-0`),Si("caret return",h),h})},oYe=(e,t)=>(Si("replaceXRanges",e,t),e.split(/\s+/).map(r=>aYe(r,t)).join(" ")),aYe=(e,t)=>{e=e.trim();let r=t.loose?hl[Da.XRANGELOOSE]:hl[Da.XRANGE];return e.replace(r,(s,a,n,c,f,p)=>{Si("xRange",e,s,a,n,c,f,p);let h=ba(n),E=h||ba(c),C=E||ba(f),S=C;return a==="="&&S&&(a=""),p=t.includePrerelease?"-0":"",h?a===">"||a==="<"?s="<0.0.0-0":s="*":a&&S?(E&&(c=0),f=0,a===">"?(a=">=",E?(n=+n+1,c=0,f=0):(c=+c+1,f=0)):a==="<="&&(a="<",E?n=+n+1:c=+c+1),a==="<"&&(p="-0"),s=`${a+n}.${c}.${f}${p}`):E?s=`>=${n}.0.0${p} <${+n+1}.0.0-0`:C&&(s=`>=${n}.${c}.0${p} <${n}.${+c+1}.0-0`),Si("xRange return",s),s})},lYe=(e,t)=>(Si("replaceStars",e,t),e.trim().replace(hl[Da.STAR],"")),cYe=(e,t)=>(Si("replaceGTE0",e,t),e.trim().replace(hl[t.includePrerelease?Da.GTE0PRE:Da.GTE0],"")),uYe=e=>(t,r,s,a,n,c,f,p,h,E,C,S,x)=>(ba(s)?r="":ba(a)?r=`>=${s}.0.0${e?"-0":""}`:ba(n)?r=`>=${s}.${a}.0${e?"-0":""}`:c?r=`>=${r}`:r=`>=${r}${e?"-0":""}`,ba(h)?p="":ba(E)?p=`<${+h+1}.0.0-0`:ba(C)?p=`<${h}.${+E+1}.0-0`:S?p=`<=${h}.${E}.${C}-${S}`:e?p=`<${h}.${E}.${+C+1}-0`:p=`<=${p}`,`${r} ${p}`.trim()),fYe=(e,t,r)=>{for(let s=0;s0){let a=e[s].semver;if(a.major===t.major&&a.minor===t.minor&&a.patch===t.patch)return!0}return!1}return!0}});var iB=G((Ykt,iie)=>{var sB=Symbol("SemVer ANY"),b_=class e{static get ANY(){return sB}constructor(t,r){if(r=$ne(r),t instanceof e){if(t.loose===!!r.loose)return t;t=t.value}t=t.trim().split(/\s+/).join(" "),D_("comparator",t,r),this.options=r,this.loose=!!r.loose,this.parse(t),this.semver===sB?this.value="":this.value=this.operator+this.semver.version,D_("comp",this)}parse(t){let r=this.options.loose?eie[tie.COMPARATORLOOSE]:eie[tie.COMPARATOR],s=t.match(r);if(!s)throw new TypeError(`Invalid comparator: ${t}`);this.operator=s[1]!==void 0?s[1]:"",this.operator==="="&&(this.operator=""),s[2]?this.semver=new rie(s[2],this.options.loose):this.semver=sB}toString(){return this.value}test(t){if(D_("Comparator.test",t,this.options.loose),this.semver===sB||t===sB)return!0;if(typeof t=="string")try{t=new rie(t,this.options)}catch{return!1}return S_(t,this.operator,this.semver,this.options)}intersects(t,r){if(!(t instanceof e))throw new TypeError("a Comparator is required");return this.operator===""?this.value===""?!0:new nie(t.value,r).test(this.value):t.operator===""?t.value===""?!0:new nie(this.value,r).test(t.semver):(r=$ne(r),r.includePrerelease&&(this.value==="<0.0.0-0"||t.value==="<0.0.0-0")||!r.includePrerelease&&(this.value.startsWith("<0.0.0")||t.value.startsWith("<0.0.0"))?!1:!!(this.operator.startsWith(">")&&t.operator.startsWith(">")||this.operator.startsWith("<")&&t.operator.startsWith("<")||this.semver.version===t.semver.version&&this.operator.includes("=")&&t.operator.includes("=")||S_(this.semver,"<",t.semver,r)&&this.operator.startsWith(">")&&t.operator.startsWith("<")||S_(this.semver,">",t.semver,r)&&this.operator.startsWith("<")&&t.operator.startsWith(">")))}};iie.exports=b_;var $ne=qx(),{safeRe:eie,t:tie}=bE(),S_=y_(),D_=eB(),rie=Ko(),nie=Dc()});var oB=G((Vkt,sie)=>{var AYe=Dc(),pYe=(e,t,r)=>{try{t=new AYe(t,r)}catch{return!1}return t.test(e)};sie.exports=pYe});var aie=G((Jkt,oie)=>{var hYe=Dc(),dYe=(e,t)=>new hYe(e,t).set.map(r=>r.map(s=>s.value).join(" ").trim().split(" "));oie.exports=dYe});var cie=G((Kkt,lie)=>{var gYe=Ko(),mYe=Dc(),yYe=(e,t,r)=>{let s=null,a=null,n=null;try{n=new mYe(t,r)}catch{return null}return e.forEach(c=>{n.test(c)&&(!s||a.compare(c)===-1)&&(s=c,a=new gYe(s,r))}),s};lie.exports=yYe});var fie=G((zkt,uie)=>{var EYe=Ko(),IYe=Dc(),CYe=(e,t,r)=>{let s=null,a=null,n=null;try{n=new IYe(t,r)}catch{return null}return e.forEach(c=>{n.test(c)&&(!s||a.compare(c)===1)&&(s=c,a=new EYe(s,r))}),s};uie.exports=CYe});var hie=G((Xkt,pie)=>{var P_=Ko(),wYe=Dc(),Aie=tB(),BYe=(e,t)=>{e=new wYe(e,t);let r=new P_("0.0.0");if(e.test(r)||(r=new P_("0.0.0-0"),e.test(r)))return r;r=null;for(let s=0;s{let f=new P_(c.semver.version);switch(c.operator){case">":f.prerelease.length===0?f.patch++:f.prerelease.push(0),f.raw=f.format();case"":case">=":(!n||Aie(f,n))&&(n=f);break;case"<":case"<=":break;default:throw new Error(`Unexpected operation: ${c.operator}`)}}),n&&(!r||Aie(r,n))&&(r=n)}return r&&e.test(r)?r:null};pie.exports=BYe});var gie=G((Zkt,die)=>{var vYe=Dc(),SYe=(e,t)=>{try{return new vYe(e,t).range||"*"}catch{return null}};die.exports=SYe});var ek=G(($kt,Iie)=>{var DYe=Ko(),Eie=iB(),{ANY:bYe}=Eie,PYe=Dc(),xYe=oB(),mie=tB(),yie=Jx(),kYe=zx(),QYe=Kx(),RYe=(e,t,r,s)=>{e=new DYe(e,s),t=new PYe(t,s);let a,n,c,f,p;switch(r){case">":a=mie,n=kYe,c=yie,f=">",p=">=";break;case"<":a=yie,n=QYe,c=mie,f="<",p="<=";break;default:throw new TypeError('Must provide a hilo val of "<" or ">"')}if(xYe(e,t,s))return!1;for(let h=0;h{x.semver===bYe&&(x=new Eie(">=0.0.0")),C=C||x,S=S||x,a(x.semver,C.semver,s)?C=x:c(x.semver,S.semver,s)&&(S=x)}),C.operator===f||C.operator===p||(!S.operator||S.operator===f)&&n(e,S.semver))return!1;if(S.operator===p&&c(e,S.semver))return!1}return!0};Iie.exports=RYe});var wie=G((eQt,Cie)=>{var TYe=ek(),FYe=(e,t,r)=>TYe(e,t,">",r);Cie.exports=FYe});var vie=G((tQt,Bie)=>{var NYe=ek(),OYe=(e,t,r)=>NYe(e,t,"<",r);Bie.exports=OYe});var bie=G((rQt,Die)=>{var Sie=Dc(),LYe=(e,t,r)=>(e=new Sie(e,r),t=new Sie(t,r),e.intersects(t,r));Die.exports=LYe});var xie=G((nQt,Pie)=>{var MYe=oB(),UYe=Sc();Pie.exports=(e,t,r)=>{let s=[],a=null,n=null,c=e.sort((E,C)=>UYe(E,C,r));for(let E of c)MYe(E,t,r)?(n=E,a||(a=E)):(n&&s.push([a,n]),n=null,a=null);a&&s.push([a,null]);let f=[];for(let[E,C]of s)E===C?f.push(E):!C&&E===c[0]?f.push("*"):C?E===c[0]?f.push(`<=${C}`):f.push(`${E} - ${C}`):f.push(`>=${E}`);let p=f.join(" || "),h=typeof t.raw=="string"?t.raw:String(t);return p.length{var kie=Dc(),k_=iB(),{ANY:x_}=k_,aB=oB(),Q_=Sc(),_Ye=(e,t,r={})=>{if(e===t)return!0;e=new kie(e,r),t=new kie(t,r);let s=!1;e:for(let a of e.set){for(let n of t.set){let c=jYe(a,n,r);if(s=s||c!==null,c)continue e}if(s)return!1}return!0},HYe=[new k_(">=0.0.0-0")],Qie=[new k_(">=0.0.0")],jYe=(e,t,r)=>{if(e===t)return!0;if(e.length===1&&e[0].semver===x_){if(t.length===1&&t[0].semver===x_)return!0;r.includePrerelease?e=HYe:e=Qie}if(t.length===1&&t[0].semver===x_){if(r.includePrerelease)return!0;t=Qie}let s=new Set,a,n;for(let x of e)x.operator===">"||x.operator===">="?a=Rie(a,x,r):x.operator==="<"||x.operator==="<="?n=Tie(n,x,r):s.add(x.semver);if(s.size>1)return null;let c;if(a&&n){if(c=Q_(a.semver,n.semver,r),c>0)return null;if(c===0&&(a.operator!==">="||n.operator!=="<="))return null}for(let x of s){if(a&&!aB(x,String(a),r)||n&&!aB(x,String(n),r))return null;for(let I of t)if(!aB(x,String(I),r))return!1;return!0}let f,p,h,E,C=n&&!r.includePrerelease&&n.semver.prerelease.length?n.semver:!1,S=a&&!r.includePrerelease&&a.semver.prerelease.length?a.semver:!1;C&&C.prerelease.length===1&&n.operator==="<"&&C.prerelease[0]===0&&(C=!1);for(let x of t){if(E=E||x.operator===">"||x.operator===">=",h=h||x.operator==="<"||x.operator==="<=",a){if(S&&x.semver.prerelease&&x.semver.prerelease.length&&x.semver.major===S.major&&x.semver.minor===S.minor&&x.semver.patch===S.patch&&(S=!1),x.operator===">"||x.operator===">="){if(f=Rie(a,x,r),f===x&&f!==a)return!1}else if(a.operator===">="&&!aB(a.semver,String(x),r))return!1}if(n){if(C&&x.semver.prerelease&&x.semver.prerelease.length&&x.semver.major===C.major&&x.semver.minor===C.minor&&x.semver.patch===C.patch&&(C=!1),x.operator==="<"||x.operator==="<="){if(p=Tie(n,x,r),p===x&&p!==n)return!1}else if(n.operator==="<="&&!aB(n.semver,String(x),r))return!1}if(!x.operator&&(n||a)&&c!==0)return!1}return!(a&&h&&!n&&c!==0||n&&E&&!a&&c!==0||S||C)},Rie=(e,t,r)=>{if(!e)return t;let s=Q_(e.semver,t.semver,r);return s>0?e:s<0||t.operator===">"&&e.operator===">="?t:e},Tie=(e,t,r)=>{if(!e)return t;let s=Q_(e.semver,t.semver,r);return s<0?e:s>0||t.operator==="<"&&e.operator==="<="?t:e};Fie.exports=_Ye});var pi=G((sQt,Mie)=>{var R_=bE(),Oie=$2(),GYe=Ko(),Lie=h_(),qYe=Og(),WYe=rne(),YYe=ine(),VYe=ane(),JYe=une(),KYe=Ane(),zYe=hne(),XYe=gne(),ZYe=yne(),$Ye=Sc(),eVe=wne(),tVe=vne(),rVe=Vx(),nVe=Pne(),iVe=kne(),sVe=tB(),oVe=Jx(),aVe=g_(),lVe=m_(),cVe=Kx(),uVe=zx(),fVe=y_(),AVe=Une(),pVe=iB(),hVe=Dc(),dVe=oB(),gVe=aie(),mVe=cie(),yVe=fie(),EVe=hie(),IVe=gie(),CVe=ek(),wVe=wie(),BVe=vie(),vVe=bie(),SVe=xie(),DVe=Nie();Mie.exports={parse:qYe,valid:WYe,clean:YYe,inc:VYe,diff:JYe,major:KYe,minor:zYe,patch:XYe,prerelease:ZYe,compare:$Ye,rcompare:eVe,compareLoose:tVe,compareBuild:rVe,sort:nVe,rsort:iVe,gt:sVe,lt:oVe,eq:aVe,neq:lVe,gte:cVe,lte:uVe,cmp:fVe,coerce:AVe,Comparator:pVe,Range:hVe,satisfies:dVe,toComparators:gVe,maxSatisfying:mVe,minSatisfying:yVe,minVersion:EVe,validRange:IVe,outside:CVe,gtr:wVe,ltr:BVe,intersects:vVe,simplifyRange:SVe,subset:DVe,SemVer:GYe,re:R_.re,src:R_.src,tokens:R_.t,SEMVER_SPEC_VERSION:Oie.SEMVER_SPEC_VERSION,RELEASE_TYPES:Oie.RELEASE_TYPES,compareIdentifiers:Lie.compareIdentifiers,rcompareIdentifiers:Lie.rcompareIdentifiers}});var _ie=G((oQt,Uie)=>{"use strict";function bVe(e,t){function r(){this.constructor=e}r.prototype=t.prototype,e.prototype=new r}function _g(e,t,r,s){this.message=e,this.expected=t,this.found=r,this.location=s,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,_g)}bVe(_g,Error);_g.buildMessage=function(e,t){var r={literal:function(h){return'"'+a(h.text)+'"'},class:function(h){var E="",C;for(C=0;C0){for(C=1,S=1;C{switch(Fe[1]){case"|":return xe|Fe[3];case"&":return xe&Fe[3];case"^":return xe^Fe[3]}},$)},S="!",x=Ne("!",!1),I=function($){return!$},T="(",O=Ne("(",!1),U=")",V=Ne(")",!1),te=function($){return $},ie=/^[^ \t\n\r()!|&\^]/,ue=ke([" "," ",` `,"\r","(",")","!","|","&","^"],!0,!1),ae=function($){return t.queryPattern.test($)},ge=function($){return t.checkFn($)},Ae=Re("whitespace"),Ce=/^[ \t\n\r]/,Ee=ke([" "," ",` `,"\r"],!1,!1),d=0,Se=0,Be=[{line:1,column:1}],me=0,ce=[],Z=0,De;if("startRule"in t){if(!(t.startRule in s))throw new Error(`Can't start parsing from rule "`+t.startRule+'".');a=s[t.startRule]}function Qe(){return e.substring(Se,d)}function st(){return Me(Se,d)}function _($,se){throw se=se!==void 0?se:Me(Se,d),b([Re($)],e.substring(Se,d),se)}function tt($,se){throw se=se!==void 0?se:Me(Se,d),w($,se)}function Ne($,se){return{type:"literal",text:$,ignoreCase:se}}function ke($,se,xe){return{type:"class",parts:$,inverted:se,ignoreCase:xe}}function be(){return{type:"any"}}function je(){return{type:"end"}}function Re($){return{type:"other",description:$}}function ct($){var se=Be[$],xe;if(se)return se;for(xe=$-1;!Be[xe];)xe--;for(se=Be[xe],se={line:se.line,column:se.column};xe<$;)e.charCodeAt(xe)===10?(se.line++,se.column=1):se.column++,xe++;return Be[$]=se,se}function Me($,se){var xe=ct($),Fe=ct(se);return{start:{offset:$,line:xe.line,column:xe.column},end:{offset:se,line:Fe.line,column:Fe.column}}}function P($){dme&&(me=d,ce=[]),ce.push($))}function w($,se){return new _g($,null,null,se)}function b($,se,xe){return new _g(_g.buildMessage($,se),$,se,xe)}function y(){var $,se,xe,Fe,ut,Ct,qt,ir;if($=d,se=F(),se!==r){for(xe=[],Fe=d,ut=X(),ut!==r?(e.charCodeAt(d)===124?(Ct=n,d++):(Ct=r,Z===0&&P(c)),Ct===r&&(e.charCodeAt(d)===38?(Ct=f,d++):(Ct=r,Z===0&&P(p)),Ct===r&&(e.charCodeAt(d)===94?(Ct=h,d++):(Ct=r,Z===0&&P(E)))),Ct!==r?(qt=X(),qt!==r?(ir=F(),ir!==r?(ut=[ut,Ct,qt,ir],Fe=ut):(d=Fe,Fe=r)):(d=Fe,Fe=r)):(d=Fe,Fe=r)):(d=Fe,Fe=r);Fe!==r;)xe.push(Fe),Fe=d,ut=X(),ut!==r?(e.charCodeAt(d)===124?(Ct=n,d++):(Ct=r,Z===0&&P(c)),Ct===r&&(e.charCodeAt(d)===38?(Ct=f,d++):(Ct=r,Z===0&&P(p)),Ct===r&&(e.charCodeAt(d)===94?(Ct=h,d++):(Ct=r,Z===0&&P(E)))),Ct!==r?(qt=X(),qt!==r?(ir=F(),ir!==r?(ut=[ut,Ct,qt,ir],Fe=ut):(d=Fe,Fe=r)):(d=Fe,Fe=r)):(d=Fe,Fe=r)):(d=Fe,Fe=r);xe!==r?(Se=$,se=C(se,xe),$=se):(d=$,$=r)}else d=$,$=r;return $}function F(){var $,se,xe,Fe,ut,Ct;return $=d,e.charCodeAt(d)===33?(se=S,d++):(se=r,Z===0&&P(x)),se!==r?(xe=F(),xe!==r?(Se=$,se=I(xe),$=se):(d=$,$=r)):(d=$,$=r),$===r&&($=d,e.charCodeAt(d)===40?(se=T,d++):(se=r,Z===0&&P(O)),se!==r?(xe=X(),xe!==r?(Fe=y(),Fe!==r?(ut=X(),ut!==r?(e.charCodeAt(d)===41?(Ct=U,d++):(Ct=r,Z===0&&P(V)),Ct!==r?(Se=$,se=te(Fe),$=se):(d=$,$=r)):(d=$,$=r)):(d=$,$=r)):(d=$,$=r)):(d=$,$=r),$===r&&($=z())),$}function z(){var $,se,xe,Fe,ut;if($=d,se=X(),se!==r){if(xe=d,Fe=[],ie.test(e.charAt(d))?(ut=e.charAt(d),d++):(ut=r,Z===0&&P(ue)),ut!==r)for(;ut!==r;)Fe.push(ut),ie.test(e.charAt(d))?(ut=e.charAt(d),d++):(ut=r,Z===0&&P(ue));else Fe=r;Fe!==r?xe=e.substring(xe,d):xe=Fe,xe!==r?(Se=d,Fe=ae(xe),Fe?Fe=void 0:Fe=r,Fe!==r?(Se=$,se=ge(xe),$=se):(d=$,$=r)):(d=$,$=r)}else d=$,$=r;return $}function X(){var $,se;for(Z++,$=[],Ce.test(e.charAt(d))?(se=e.charAt(d),d++):(se=r,Z===0&&P(Ee));se!==r;)$.push(se),Ce.test(e.charAt(d))?(se=e.charAt(d),d++):(se=r,Z===0&&P(Ee));return Z--,$===r&&(se=r,Z===0&&P(Ae)),$}if(De=a(),De!==r&&d===e.length)return De;throw De!==r&&d{var{parse:xVe}=_ie();tk.makeParser=(e=/[a-z]+/)=>(t,r)=>xVe(t,{queryPattern:e,checkFn:r});tk.parse=tk.makeParser()});var Gie=G((lQt,jie)=>{"use strict";jie.exports={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]}});var T_=G((cQt,Wie)=>{var lB=Gie(),qie={};for(let e of Object.keys(lB))qie[lB[e]]=e;var hr={rgb:{channels:3,labels:"rgb"},hsl:{channels:3,labels:"hsl"},hsv:{channels:3,labels:"hsv"},hwb:{channels:3,labels:"hwb"},cmyk:{channels:4,labels:"cmyk"},xyz:{channels:3,labels:"xyz"},lab:{channels:3,labels:"lab"},lch:{channels:3,labels:"lch"},hex:{channels:1,labels:["hex"]},keyword:{channels:1,labels:["keyword"]},ansi16:{channels:1,labels:["ansi16"]},ansi256:{channels:1,labels:["ansi256"]},hcg:{channels:3,labels:["h","c","g"]},apple:{channels:3,labels:["r16","g16","b16"]},gray:{channels:1,labels:["gray"]}};Wie.exports=hr;for(let e of Object.keys(hr)){if(!("channels"in hr[e]))throw new Error("missing channels property: "+e);if(!("labels"in hr[e]))throw new Error("missing channel labels property: "+e);if(hr[e].labels.length!==hr[e].channels)throw new Error("channel and label counts mismatch: "+e);let{channels:t,labels:r}=hr[e];delete hr[e].channels,delete hr[e].labels,Object.defineProperty(hr[e],"channels",{value:t}),Object.defineProperty(hr[e],"labels",{value:r})}hr.rgb.hsl=function(e){let t=e[0]/255,r=e[1]/255,s=e[2]/255,a=Math.min(t,r,s),n=Math.max(t,r,s),c=n-a,f,p;n===a?f=0:t===n?f=(r-s)/c:r===n?f=2+(s-t)/c:s===n&&(f=4+(t-r)/c),f=Math.min(f*60,360),f<0&&(f+=360);let h=(a+n)/2;return n===a?p=0:h<=.5?p=c/(n+a):p=c/(2-n-a),[f,p*100,h*100]};hr.rgb.hsv=function(e){let t,r,s,a,n,c=e[0]/255,f=e[1]/255,p=e[2]/255,h=Math.max(c,f,p),E=h-Math.min(c,f,p),C=function(S){return(h-S)/6/E+1/2};return E===0?(a=0,n=0):(n=E/h,t=C(c),r=C(f),s=C(p),c===h?a=s-r:f===h?a=1/3+t-s:p===h&&(a=2/3+r-t),a<0?a+=1:a>1&&(a-=1)),[a*360,n*100,h*100]};hr.rgb.hwb=function(e){let t=e[0],r=e[1],s=e[2],a=hr.rgb.hsl(e)[0],n=1/255*Math.min(t,Math.min(r,s));return s=1-1/255*Math.max(t,Math.max(r,s)),[a,n*100,s*100]};hr.rgb.cmyk=function(e){let t=e[0]/255,r=e[1]/255,s=e[2]/255,a=Math.min(1-t,1-r,1-s),n=(1-t-a)/(1-a)||0,c=(1-r-a)/(1-a)||0,f=(1-s-a)/(1-a)||0;return[n*100,c*100,f*100,a*100]};function kVe(e,t){return(e[0]-t[0])**2+(e[1]-t[1])**2+(e[2]-t[2])**2}hr.rgb.keyword=function(e){let t=qie[e];if(t)return t;let r=1/0,s;for(let a of Object.keys(lB)){let n=lB[a],c=kVe(e,n);c.04045?((t+.055)/1.055)**2.4:t/12.92,r=r>.04045?((r+.055)/1.055)**2.4:r/12.92,s=s>.04045?((s+.055)/1.055)**2.4:s/12.92;let a=t*.4124+r*.3576+s*.1805,n=t*.2126+r*.7152+s*.0722,c=t*.0193+r*.1192+s*.9505;return[a*100,n*100,c*100]};hr.rgb.lab=function(e){let t=hr.rgb.xyz(e),r=t[0],s=t[1],a=t[2];r/=95.047,s/=100,a/=108.883,r=r>.008856?r**(1/3):7.787*r+16/116,s=s>.008856?s**(1/3):7.787*s+16/116,a=a>.008856?a**(1/3):7.787*a+16/116;let n=116*s-16,c=500*(r-s),f=200*(s-a);return[n,c,f]};hr.hsl.rgb=function(e){let t=e[0]/360,r=e[1]/100,s=e[2]/100,a,n,c;if(r===0)return c=s*255,[c,c,c];s<.5?a=s*(1+r):a=s+r-s*r;let f=2*s-a,p=[0,0,0];for(let h=0;h<3;h++)n=t+1/3*-(h-1),n<0&&n++,n>1&&n--,6*n<1?c=f+(a-f)*6*n:2*n<1?c=a:3*n<2?c=f+(a-f)*(2/3-n)*6:c=f,p[h]=c*255;return p};hr.hsl.hsv=function(e){let t=e[0],r=e[1]/100,s=e[2]/100,a=r,n=Math.max(s,.01);s*=2,r*=s<=1?s:2-s,a*=n<=1?n:2-n;let c=(s+r)/2,f=s===0?2*a/(n+a):2*r/(s+r);return[t,f*100,c*100]};hr.hsv.rgb=function(e){let t=e[0]/60,r=e[1]/100,s=e[2]/100,a=Math.floor(t)%6,n=t-Math.floor(t),c=255*s*(1-r),f=255*s*(1-r*n),p=255*s*(1-r*(1-n));switch(s*=255,a){case 0:return[s,p,c];case 1:return[f,s,c];case 2:return[c,s,p];case 3:return[c,f,s];case 4:return[p,c,s];case 5:return[s,c,f]}};hr.hsv.hsl=function(e){let t=e[0],r=e[1]/100,s=e[2]/100,a=Math.max(s,.01),n,c;c=(2-r)*s;let f=(2-r)*a;return n=r*a,n/=f<=1?f:2-f,n=n||0,c/=2,[t,n*100,c*100]};hr.hwb.rgb=function(e){let t=e[0]/360,r=e[1]/100,s=e[2]/100,a=r+s,n;a>1&&(r/=a,s/=a);let c=Math.floor(6*t),f=1-s;n=6*t-c,c&1&&(n=1-n);let p=r+n*(f-r),h,E,C;switch(c){default:case 6:case 0:h=f,E=p,C=r;break;case 1:h=p,E=f,C=r;break;case 2:h=r,E=f,C=p;break;case 3:h=r,E=p,C=f;break;case 4:h=p,E=r,C=f;break;case 5:h=f,E=r,C=p;break}return[h*255,E*255,C*255]};hr.cmyk.rgb=function(e){let t=e[0]/100,r=e[1]/100,s=e[2]/100,a=e[3]/100,n=1-Math.min(1,t*(1-a)+a),c=1-Math.min(1,r*(1-a)+a),f=1-Math.min(1,s*(1-a)+a);return[n*255,c*255,f*255]};hr.xyz.rgb=function(e){let t=e[0]/100,r=e[1]/100,s=e[2]/100,a,n,c;return a=t*3.2406+r*-1.5372+s*-.4986,n=t*-.9689+r*1.8758+s*.0415,c=t*.0557+r*-.204+s*1.057,a=a>.0031308?1.055*a**(1/2.4)-.055:a*12.92,n=n>.0031308?1.055*n**(1/2.4)-.055:n*12.92,c=c>.0031308?1.055*c**(1/2.4)-.055:c*12.92,a=Math.min(Math.max(0,a),1),n=Math.min(Math.max(0,n),1),c=Math.min(Math.max(0,c),1),[a*255,n*255,c*255]};hr.xyz.lab=function(e){let t=e[0],r=e[1],s=e[2];t/=95.047,r/=100,s/=108.883,t=t>.008856?t**(1/3):7.787*t+16/116,r=r>.008856?r**(1/3):7.787*r+16/116,s=s>.008856?s**(1/3):7.787*s+16/116;let a=116*r-16,n=500*(t-r),c=200*(r-s);return[a,n,c]};hr.lab.xyz=function(e){let t=e[0],r=e[1],s=e[2],a,n,c;n=(t+16)/116,a=r/500+n,c=n-s/200;let f=n**3,p=a**3,h=c**3;return n=f>.008856?f:(n-16/116)/7.787,a=p>.008856?p:(a-16/116)/7.787,c=h>.008856?h:(c-16/116)/7.787,a*=95.047,n*=100,c*=108.883,[a,n,c]};hr.lab.lch=function(e){let t=e[0],r=e[1],s=e[2],a;a=Math.atan2(s,r)*360/2/Math.PI,a<0&&(a+=360);let c=Math.sqrt(r*r+s*s);return[t,c,a]};hr.lch.lab=function(e){let t=e[0],r=e[1],a=e[2]/360*2*Math.PI,n=r*Math.cos(a),c=r*Math.sin(a);return[t,n,c]};hr.rgb.ansi16=function(e,t=null){let[r,s,a]=e,n=t===null?hr.rgb.hsv(e)[2]:t;if(n=Math.round(n/50),n===0)return 30;let c=30+(Math.round(a/255)<<2|Math.round(s/255)<<1|Math.round(r/255));return n===2&&(c+=60),c};hr.hsv.ansi16=function(e){return hr.rgb.ansi16(hr.hsv.rgb(e),e[2])};hr.rgb.ansi256=function(e){let t=e[0],r=e[1],s=e[2];return t===r&&r===s?t<8?16:t>248?231:Math.round((t-8)/247*24)+232:16+36*Math.round(t/255*5)+6*Math.round(r/255*5)+Math.round(s/255*5)};hr.ansi16.rgb=function(e){let t=e%10;if(t===0||t===7)return e>50&&(t+=3.5),t=t/10.5*255,[t,t,t];let r=(~~(e>50)+1)*.5,s=(t&1)*r*255,a=(t>>1&1)*r*255,n=(t>>2&1)*r*255;return[s,a,n]};hr.ansi256.rgb=function(e){if(e>=232){let n=(e-232)*10+8;return[n,n,n]}e-=16;let t,r=Math.floor(e/36)/5*255,s=Math.floor((t=e%36)/6)/5*255,a=t%6/5*255;return[r,s,a]};hr.rgb.hex=function(e){let r=(((Math.round(e[0])&255)<<16)+((Math.round(e[1])&255)<<8)+(Math.round(e[2])&255)).toString(16).toUpperCase();return"000000".substring(r.length)+r};hr.hex.rgb=function(e){let t=e.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i);if(!t)return[0,0,0];let r=t[0];t[0].length===3&&(r=r.split("").map(f=>f+f).join(""));let s=parseInt(r,16),a=s>>16&255,n=s>>8&255,c=s&255;return[a,n,c]};hr.rgb.hcg=function(e){let t=e[0]/255,r=e[1]/255,s=e[2]/255,a=Math.max(Math.max(t,r),s),n=Math.min(Math.min(t,r),s),c=a-n,f,p;return c<1?f=n/(1-c):f=0,c<=0?p=0:a===t?p=(r-s)/c%6:a===r?p=2+(s-t)/c:p=4+(t-r)/c,p/=6,p%=1,[p*360,c*100,f*100]};hr.hsl.hcg=function(e){let t=e[1]/100,r=e[2]/100,s=r<.5?2*t*r:2*t*(1-r),a=0;return s<1&&(a=(r-.5*s)/(1-s)),[e[0],s*100,a*100]};hr.hsv.hcg=function(e){let t=e[1]/100,r=e[2]/100,s=t*r,a=0;return s<1&&(a=(r-s)/(1-s)),[e[0],s*100,a*100]};hr.hcg.rgb=function(e){let t=e[0]/360,r=e[1]/100,s=e[2]/100;if(r===0)return[s*255,s*255,s*255];let a=[0,0,0],n=t%1*6,c=n%1,f=1-c,p=0;switch(Math.floor(n)){case 0:a[0]=1,a[1]=c,a[2]=0;break;case 1:a[0]=f,a[1]=1,a[2]=0;break;case 2:a[0]=0,a[1]=1,a[2]=c;break;case 3:a[0]=0,a[1]=f,a[2]=1;break;case 4:a[0]=c,a[1]=0,a[2]=1;break;default:a[0]=1,a[1]=0,a[2]=f}return p=(1-r)*s,[(r*a[0]+p)*255,(r*a[1]+p)*255,(r*a[2]+p)*255]};hr.hcg.hsv=function(e){let t=e[1]/100,r=e[2]/100,s=t+r*(1-t),a=0;return s>0&&(a=t/s),[e[0],a*100,s*100]};hr.hcg.hsl=function(e){let t=e[1]/100,s=e[2]/100*(1-t)+.5*t,a=0;return s>0&&s<.5?a=t/(2*s):s>=.5&&s<1&&(a=t/(2*(1-s))),[e[0],a*100,s*100]};hr.hcg.hwb=function(e){let t=e[1]/100,r=e[2]/100,s=t+r*(1-t);return[e[0],(s-t)*100,(1-s)*100]};hr.hwb.hcg=function(e){let t=e[1]/100,s=1-e[2]/100,a=s-t,n=0;return a<1&&(n=(s-a)/(1-a)),[e[0],a*100,n*100]};hr.apple.rgb=function(e){return[e[0]/65535*255,e[1]/65535*255,e[2]/65535*255]};hr.rgb.apple=function(e){return[e[0]/255*65535,e[1]/255*65535,e[2]/255*65535]};hr.gray.rgb=function(e){return[e[0]/100*255,e[0]/100*255,e[0]/100*255]};hr.gray.hsl=function(e){return[0,0,e[0]]};hr.gray.hsv=hr.gray.hsl;hr.gray.hwb=function(e){return[0,100,e[0]]};hr.gray.cmyk=function(e){return[0,0,0,e[0]]};hr.gray.lab=function(e){return[e[0],0,0]};hr.gray.hex=function(e){let t=Math.round(e[0]/100*255)&255,s=((t<<16)+(t<<8)+t).toString(16).toUpperCase();return"000000".substring(s.length)+s};hr.rgb.gray=function(e){return[(e[0]+e[1]+e[2])/3/255*100]}});var Vie=G((uQt,Yie)=>{var rk=T_();function QVe(){let e={},t=Object.keys(rk);for(let r=t.length,s=0;s{var F_=T_(),NVe=Vie(),QE={},OVe=Object.keys(F_);function LVe(e){let t=function(...r){let s=r[0];return s==null?s:(s.length>1&&(r=s),e(r))};return"conversion"in e&&(t.conversion=e.conversion),t}function MVe(e){let t=function(...r){let s=r[0];if(s==null)return s;s.length>1&&(r=s);let a=e(r);if(typeof a=="object")for(let n=a.length,c=0;c{QE[e]={},Object.defineProperty(QE[e],"channels",{value:F_[e].channels}),Object.defineProperty(QE[e],"labels",{value:F_[e].labels});let t=NVe(e);Object.keys(t).forEach(s=>{let a=t[s];QE[e][s]=MVe(a),QE[e][s].raw=LVe(a)})});Jie.exports=QE});var ik=G((AQt,ese)=>{"use strict";var zie=(e,t)=>(...r)=>`\x1B[${e(...r)+t}m`,Xie=(e,t)=>(...r)=>{let s=e(...r);return`\x1B[${38+t};5;${s}m`},Zie=(e,t)=>(...r)=>{let s=e(...r);return`\x1B[${38+t};2;${s[0]};${s[1]};${s[2]}m`},nk=e=>e,$ie=(e,t,r)=>[e,t,r],RE=(e,t,r)=>{Object.defineProperty(e,t,{get:()=>{let s=r();return Object.defineProperty(e,t,{value:s,enumerable:!0,configurable:!0}),s},enumerable:!0,configurable:!0})},N_,TE=(e,t,r,s)=>{N_===void 0&&(N_=Kie());let a=s?10:0,n={};for(let[c,f]of Object.entries(N_)){let p=c==="ansi16"?"ansi":c;c===t?n[p]=e(r,a):typeof f=="object"&&(n[p]=e(f[t],a))}return n};function UVe(){let e=new Map,t={modifier:{reset:[0,0],bold:[1,22],dim:[2,22],italic:[3,23],underline:[4,24],inverse:[7,27],hidden:[8,28],strikethrough:[9,29]},color:{black:[30,39],red:[31,39],green:[32,39],yellow:[33,39],blue:[34,39],magenta:[35,39],cyan:[36,39],white:[37,39],blackBright:[90,39],redBright:[91,39],greenBright:[92,39],yellowBright:[93,39],blueBright:[94,39],magentaBright:[95,39],cyanBright:[96,39],whiteBright:[97,39]},bgColor:{bgBlack:[40,49],bgRed:[41,49],bgGreen:[42,49],bgYellow:[43,49],bgBlue:[44,49],bgMagenta:[45,49],bgCyan:[46,49],bgWhite:[47,49],bgBlackBright:[100,49],bgRedBright:[101,49],bgGreenBright:[102,49],bgYellowBright:[103,49],bgBlueBright:[104,49],bgMagentaBright:[105,49],bgCyanBright:[106,49],bgWhiteBright:[107,49]}};t.color.gray=t.color.blackBright,t.bgColor.bgGray=t.bgColor.bgBlackBright,t.color.grey=t.color.blackBright,t.bgColor.bgGrey=t.bgColor.bgBlackBright;for(let[r,s]of Object.entries(t)){for(let[a,n]of Object.entries(s))t[a]={open:`\x1B[${n[0]}m`,close:`\x1B[${n[1]}m`},s[a]=t[a],e.set(n[0],n[1]);Object.defineProperty(t,r,{value:s,enumerable:!1})}return Object.defineProperty(t,"codes",{value:e,enumerable:!1}),t.color.close="\x1B[39m",t.bgColor.close="\x1B[49m",RE(t.color,"ansi",()=>TE(zie,"ansi16",nk,!1)),RE(t.color,"ansi256",()=>TE(Xie,"ansi256",nk,!1)),RE(t.color,"ansi16m",()=>TE(Zie,"rgb",$ie,!1)),RE(t.bgColor,"ansi",()=>TE(zie,"ansi16",nk,!0)),RE(t.bgColor,"ansi256",()=>TE(Xie,"ansi256",nk,!0)),RE(t.bgColor,"ansi16m",()=>TE(Zie,"rgb",$ie,!0)),t}Object.defineProperty(ese,"exports",{enumerable:!0,get:UVe})});var rse=G((pQt,tse)=>{"use strict";tse.exports=(e,t=process.argv)=>{let r=e.startsWith("-")?"":e.length===1?"-":"--",s=t.indexOf(r+e),a=t.indexOf("--");return s!==-1&&(a===-1||s{"use strict";var _Ve=Ie("os"),nse=Ie("tty"),bc=rse(),{env:Qs}=process,a0;bc("no-color")||bc("no-colors")||bc("color=false")||bc("color=never")?a0=0:(bc("color")||bc("colors")||bc("color=true")||bc("color=always"))&&(a0=1);"FORCE_COLOR"in Qs&&(Qs.FORCE_COLOR==="true"?a0=1:Qs.FORCE_COLOR==="false"?a0=0:a0=Qs.FORCE_COLOR.length===0?1:Math.min(parseInt(Qs.FORCE_COLOR,10),3));function O_(e){return e===0?!1:{level:e,hasBasic:!0,has256:e>=2,has16m:e>=3}}function L_(e,t){if(a0===0)return 0;if(bc("color=16m")||bc("color=full")||bc("color=truecolor"))return 3;if(bc("color=256"))return 2;if(e&&!t&&a0===void 0)return 0;let r=a0||0;if(Qs.TERM==="dumb")return r;if(process.platform==="win32"){let s=_Ve.release().split(".");return Number(s[0])>=10&&Number(s[2])>=10586?Number(s[2])>=14931?3:2:1}if("CI"in Qs)return["TRAVIS","CIRCLECI","APPVEYOR","GITLAB_CI"].some(s=>s in Qs)||Qs.CI_NAME==="codeship"?1:r;if("TEAMCITY_VERSION"in Qs)return/^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(Qs.TEAMCITY_VERSION)?1:0;if("GITHUB_ACTIONS"in Qs)return 1;if(Qs.COLORTERM==="truecolor")return 3;if("TERM_PROGRAM"in Qs){let s=parseInt((Qs.TERM_PROGRAM_VERSION||"").split(".")[0],10);switch(Qs.TERM_PROGRAM){case"iTerm.app":return s>=3?3:2;case"Apple_Terminal":return 2}}return/-256(color)?$/i.test(Qs.TERM)?2:/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(Qs.TERM)||"COLORTERM"in Qs?1:r}function HVe(e){let t=L_(e,e&&e.isTTY);return O_(t)}ise.exports={supportsColor:HVe,stdout:O_(L_(!0,nse.isatty(1))),stderr:O_(L_(!0,nse.isatty(2)))}});var ase=G((dQt,ose)=>{"use strict";var jVe=(e,t,r)=>{let s=e.indexOf(t);if(s===-1)return e;let a=t.length,n=0,c="";do c+=e.substr(n,s-n)+t+r,n=s+a,s=e.indexOf(t,n);while(s!==-1);return c+=e.substr(n),c},GVe=(e,t,r,s)=>{let a=0,n="";do{let c=e[s-1]==="\r";n+=e.substr(a,(c?s-1:s)-a)+t+(c?`\r `:` `)+r,a=s+1,s=e.indexOf(` `,a)}while(s!==-1);return n+=e.substr(a),n};ose.exports={stringReplaceAll:jVe,stringEncaseCRLFWithFirstIndex:GVe}});var Ase=G((gQt,fse)=>{"use strict";var qVe=/(?:\\(u(?:[a-f\d]{4}|\{[a-f\d]{1,6}\})|x[a-f\d]{2}|.))|(?:\{(~)?(\w+(?:\([^)]*\))?(?:\.\w+(?:\([^)]*\))?)*)(?:[ \t]|(?=\r?\n)))|(\})|((?:.|[\r\n\f])+?)/gi,lse=/(?:^|\.)(\w+)(?:\(([^)]*)\))?/g,WVe=/^(['"])((?:\\.|(?!\1)[^\\])*)\1$/,YVe=/\\(u(?:[a-f\d]{4}|{[a-f\d]{1,6}})|x[a-f\d]{2}|.)|([^\\])/gi,VVe=new Map([["n",` `],["r","\r"],["t"," "],["b","\b"],["f","\f"],["v","\v"],["0","\0"],["\\","\\"],["e","\x1B"],["a","\x07"]]);function use(e){let t=e[0]==="u",r=e[1]==="{";return t&&!r&&e.length===5||e[0]==="x"&&e.length===3?String.fromCharCode(parseInt(e.slice(1),16)):t&&r?String.fromCodePoint(parseInt(e.slice(2,-1),16)):VVe.get(e)||e}function JVe(e,t){let r=[],s=t.trim().split(/\s*,\s*/g),a;for(let n of s){let c=Number(n);if(!Number.isNaN(c))r.push(c);else if(a=n.match(WVe))r.push(a[2].replace(YVe,(f,p,h)=>p?use(p):h));else throw new Error(`Invalid Chalk template style argument: ${n} (in style '${e}')`)}return r}function KVe(e){lse.lastIndex=0;let t=[],r;for(;(r=lse.exec(e))!==null;){let s=r[1];if(r[2]){let a=JVe(s,r[2]);t.push([s].concat(a))}else t.push([s])}return t}function cse(e,t){let r={};for(let a of t)for(let n of a.styles)r[n[0]]=a.inverse?null:n.slice(1);let s=e;for(let[a,n]of Object.entries(r))if(Array.isArray(n)){if(!(a in s))throw new Error(`Unknown Chalk style: ${a}`);s=n.length>0?s[a](...n):s[a]}return s}fse.exports=(e,t)=>{let r=[],s=[],a=[];if(t.replace(qVe,(n,c,f,p,h,E)=>{if(c)a.push(use(c));else if(p){let C=a.join("");a=[],s.push(r.length===0?C:cse(e,r)(C)),r.push({inverse:f,styles:KVe(p)})}else if(h){if(r.length===0)throw new Error("Found extraneous } in Chalk template literal");s.push(cse(e,r)(a.join(""))),a=[],r.pop()}else a.push(E)}),s.push(a.join("")),r.length>0){let n=`Chalk template literal is missing ${r.length} closing bracket${r.length===1?"":"s"} (\`}\`)`;throw new Error(n)}return s.join("")}});var NE=G((mQt,yse)=>{"use strict";var cB=ik(),{stdout:U_,stderr:__}=sse(),{stringReplaceAll:zVe,stringEncaseCRLFWithFirstIndex:XVe}=ase(),{isArray:sk}=Array,hse=["ansi","ansi","ansi256","ansi16m"],FE=Object.create(null),ZVe=(e,t={})=>{if(t.level&&!(Number.isInteger(t.level)&&t.level>=0&&t.level<=3))throw new Error("The `level` option should be an integer from 0 to 3");let r=U_?U_.level:0;e.level=t.level===void 0?r:t.level},H_=class{constructor(t){return dse(t)}},dse=e=>{let t={};return ZVe(t,e),t.template=(...r)=>mse(t.template,...r),Object.setPrototypeOf(t,ok.prototype),Object.setPrototypeOf(t.template,t),t.template.constructor=()=>{throw new Error("`chalk.constructor()` is deprecated. Use `new chalk.Instance()` instead.")},t.template.Instance=H_,t.template};function ok(e){return dse(e)}for(let[e,t]of Object.entries(cB))FE[e]={get(){let r=ak(this,j_(t.open,t.close,this._styler),this._isEmpty);return Object.defineProperty(this,e,{value:r}),r}};FE.visible={get(){let e=ak(this,this._styler,!0);return Object.defineProperty(this,"visible",{value:e}),e}};var gse=["rgb","hex","keyword","hsl","hsv","hwb","ansi","ansi256"];for(let e of gse)FE[e]={get(){let{level:t}=this;return function(...r){let s=j_(cB.color[hse[t]][e](...r),cB.color.close,this._styler);return ak(this,s,this._isEmpty)}}};for(let e of gse){let t="bg"+e[0].toUpperCase()+e.slice(1);FE[t]={get(){let{level:r}=this;return function(...s){let a=j_(cB.bgColor[hse[r]][e](...s),cB.bgColor.close,this._styler);return ak(this,a,this._isEmpty)}}}}var $Ve=Object.defineProperties(()=>{},{...FE,level:{enumerable:!0,get(){return this._generator.level},set(e){this._generator.level=e}}}),j_=(e,t,r)=>{let s,a;return r===void 0?(s=e,a=t):(s=r.openAll+e,a=t+r.closeAll),{open:e,close:t,openAll:s,closeAll:a,parent:r}},ak=(e,t,r)=>{let s=(...a)=>sk(a[0])&&sk(a[0].raw)?pse(s,mse(s,...a)):pse(s,a.length===1?""+a[0]:a.join(" "));return Object.setPrototypeOf(s,$Ve),s._generator=e,s._styler=t,s._isEmpty=r,s},pse=(e,t)=>{if(e.level<=0||!t)return e._isEmpty?"":t;let r=e._styler;if(r===void 0)return t;let{openAll:s,closeAll:a}=r;if(t.indexOf("\x1B")!==-1)for(;r!==void 0;)t=zVe(t,r.close,r.open),r=r.parent;let n=t.indexOf(` `);return n!==-1&&(t=XVe(t,a,s,n)),s+t+a},M_,mse=(e,...t)=>{let[r]=t;if(!sk(r)||!sk(r.raw))return t.join(" ");let s=t.slice(1),a=[r.raw[0]];for(let n=1;n{"use strict";Pc.isInteger=e=>typeof e=="number"?Number.isInteger(e):typeof e=="string"&&e.trim()!==""?Number.isInteger(Number(e)):!1;Pc.find=(e,t)=>e.nodes.find(r=>r.type===t);Pc.exceedsLimit=(e,t,r=1,s)=>s===!1||!Pc.isInteger(e)||!Pc.isInteger(t)?!1:(Number(t)-Number(e))/Number(r)>=s;Pc.escapeNode=(e,t=0,r)=>{let s=e.nodes[t];s&&(r&&s.type===r||s.type==="open"||s.type==="close")&&s.escaped!==!0&&(s.value="\\"+s.value,s.escaped=!0)};Pc.encloseBrace=e=>e.type!=="brace"||e.commas>>0+e.ranges>>0?!1:(e.invalid=!0,!0);Pc.isInvalidBrace=e=>e.type!=="brace"?!1:e.invalid===!0||e.dollar?!0:!(e.commas>>0+e.ranges>>0)||e.open!==!0||e.close!==!0?(e.invalid=!0,!0):!1;Pc.isOpenOrClose=e=>e.type==="open"||e.type==="close"?!0:e.open===!0||e.close===!0;Pc.reduce=e=>e.reduce((t,r)=>(r.type==="text"&&t.push(r.value),r.type==="range"&&(r.type="text"),t),[]);Pc.flatten=(...e)=>{let t=[],r=s=>{for(let a=0;a{"use strict";var Ese=ck();Ise.exports=(e,t={})=>{let r=(s,a={})=>{let n=t.escapeInvalid&&Ese.isInvalidBrace(a),c=s.invalid===!0&&t.escapeInvalid===!0,f="";if(s.value)return(n||c)&&Ese.isOpenOrClose(s)?"\\"+s.value:s.value;if(s.value)return s.value;if(s.nodes)for(let p of s.nodes)f+=r(p);return f};return r(e)}});var wse=G((IQt,Cse)=>{"use strict";Cse.exports=function(e){return typeof e=="number"?e-e===0:typeof e=="string"&&e.trim()!==""?Number.isFinite?Number.isFinite(+e):isFinite(+e):!1}});var Qse=G((CQt,kse)=>{"use strict";var Bse=wse(),Hg=(e,t,r)=>{if(Bse(e)===!1)throw new TypeError("toRegexRange: expected the first argument to be a number");if(t===void 0||e===t)return String(e);if(Bse(t)===!1)throw new TypeError("toRegexRange: expected the second argument to be a number.");let s={relaxZeros:!0,...r};typeof s.strictZeros=="boolean"&&(s.relaxZeros=s.strictZeros===!1);let a=String(s.relaxZeros),n=String(s.shorthand),c=String(s.capture),f=String(s.wrap),p=e+":"+t+"="+a+n+c+f;if(Hg.cache.hasOwnProperty(p))return Hg.cache[p].result;let h=Math.min(e,t),E=Math.max(e,t);if(Math.abs(h-E)===1){let T=e+"|"+t;return s.capture?`(${T})`:s.wrap===!1?T:`(?:${T})`}let C=xse(e)||xse(t),S={min:e,max:t,a:h,b:E},x=[],I=[];if(C&&(S.isPadded=C,S.maxLen=String(S.max).length),h<0){let T=E<0?Math.abs(E):1;I=vse(T,Math.abs(h),S,s),h=S.a=0}return E>=0&&(x=vse(h,E,S,s)),S.negatives=I,S.positives=x,S.result=e7e(I,x,s),s.capture===!0?S.result=`(${S.result})`:s.wrap!==!1&&x.length+I.length>1&&(S.result=`(?:${S.result})`),Hg.cache[p]=S,S.result};function e7e(e,t,r){let s=G_(e,t,"-",!1,r)||[],a=G_(t,e,"",!1,r)||[],n=G_(e,t,"-?",!0,r)||[];return s.concat(n).concat(a).join("|")}function t7e(e,t){let r=1,s=1,a=Dse(e,r),n=new Set([t]);for(;e<=a&&a<=t;)n.add(a),r+=1,a=Dse(e,r);for(a=bse(t+1,s)-1;e1&&f.count.pop(),f.count.push(E.count[0]),f.string=f.pattern+Pse(f.count),c=h+1;continue}r.isPadded&&(C=o7e(h,r,s)),E.string=C+E.pattern+Pse(E.count),n.push(E),c=h+1,f=E}return n}function G_(e,t,r,s,a){let n=[];for(let c of e){let{string:f}=c;!s&&!Sse(t,"string",f)&&n.push(r+f),s&&Sse(t,"string",f)&&n.push(r+f)}return n}function n7e(e,t){let r=[];for(let s=0;st?1:t>e?-1:0}function Sse(e,t,r){return e.some(s=>s[t]===r)}function Dse(e,t){return Number(String(e).slice(0,-t)+"9".repeat(t))}function bse(e,t){return e-e%Math.pow(10,t)}function Pse(e){let[t=0,r=""]=e;return r||t>1?`{${t+(r?","+r:"")}}`:""}function s7e(e,t,r){return`[${e}${t-e===1?"":"-"}${t}]`}function xse(e){return/^-?(0+)\d/.test(e)}function o7e(e,t,r){if(!t.isPadded)return e;let s=Math.abs(t.maxLen-String(e).length),a=r.relaxZeros!==!1;switch(s){case 0:return"";case 1:return a?"0?":"0";case 2:return a?"0{0,2}":"00";default:return a?`0{0,${s}}`:`0{${s}}`}}Hg.cache={};Hg.clearCache=()=>Hg.cache={};kse.exports=Hg});var Y_=G((wQt,Use)=>{"use strict";var a7e=Ie("util"),Fse=Qse(),Rse=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),l7e=e=>t=>e===!0?Number(t):String(t),q_=e=>typeof e=="number"||typeof e=="string"&&e!=="",uB=e=>Number.isInteger(+e),W_=e=>{let t=`${e}`,r=-1;if(t[0]==="-"&&(t=t.slice(1)),t==="0")return!1;for(;t[++r]==="0";);return r>0},c7e=(e,t,r)=>typeof e=="string"||typeof t=="string"?!0:r.stringify===!0,u7e=(e,t,r)=>{if(t>0){let s=e[0]==="-"?"-":"";s&&(e=e.slice(1)),e=s+e.padStart(s?t-1:t,"0")}return r===!1?String(e):e},Tse=(e,t)=>{let r=e[0]==="-"?"-":"";for(r&&(e=e.slice(1),t--);e.length{e.negatives.sort((c,f)=>cf?1:0),e.positives.sort((c,f)=>cf?1:0);let r=t.capture?"":"?:",s="",a="",n;return e.positives.length&&(s=e.positives.join("|")),e.negatives.length&&(a=`-(${r}${e.negatives.join("|")})`),s&&a?n=`${s}|${a}`:n=s||a,t.wrap?`(${r}${n})`:n},Nse=(e,t,r,s)=>{if(r)return Fse(e,t,{wrap:!1,...s});let a=String.fromCharCode(e);if(e===t)return a;let n=String.fromCharCode(t);return`[${a}-${n}]`},Ose=(e,t,r)=>{if(Array.isArray(e)){let s=r.wrap===!0,a=r.capture?"":"?:";return s?`(${a}${e.join("|")})`:e.join("|")}return Fse(e,t,r)},Lse=(...e)=>new RangeError("Invalid range arguments: "+a7e.inspect(...e)),Mse=(e,t,r)=>{if(r.strictRanges===!0)throw Lse([e,t]);return[]},A7e=(e,t)=>{if(t.strictRanges===!0)throw new TypeError(`Expected step "${e}" to be a number`);return[]},p7e=(e,t,r=1,s={})=>{let a=Number(e),n=Number(t);if(!Number.isInteger(a)||!Number.isInteger(n)){if(s.strictRanges===!0)throw Lse([e,t]);return[]}a===0&&(a=0),n===0&&(n=0);let c=a>n,f=String(e),p=String(t),h=String(r);r=Math.max(Math.abs(r),1);let E=W_(f)||W_(p)||W_(h),C=E?Math.max(f.length,p.length,h.length):0,S=E===!1&&c7e(e,t,s)===!1,x=s.transform||l7e(S);if(s.toRegex&&r===1)return Nse(Tse(e,C),Tse(t,C),!0,s);let I={negatives:[],positives:[]},T=V=>I[V<0?"negatives":"positives"].push(Math.abs(V)),O=[],U=0;for(;c?a>=n:a<=n;)s.toRegex===!0&&r>1?T(a):O.push(u7e(x(a,U),C,S)),a=c?a-r:a+r,U++;return s.toRegex===!0?r>1?f7e(I,s):Ose(O,null,{wrap:!1,...s}):O},h7e=(e,t,r=1,s={})=>{if(!uB(e)&&e.length>1||!uB(t)&&t.length>1)return Mse(e,t,s);let a=s.transform||(S=>String.fromCharCode(S)),n=`${e}`.charCodeAt(0),c=`${t}`.charCodeAt(0),f=n>c,p=Math.min(n,c),h=Math.max(n,c);if(s.toRegex&&r===1)return Nse(p,h,!1,s);let E=[],C=0;for(;f?n>=c:n<=c;)E.push(a(n,C)),n=f?n-r:n+r,C++;return s.toRegex===!0?Ose(E,null,{wrap:!1,options:s}):E},fk=(e,t,r,s={})=>{if(t==null&&q_(e))return[e];if(!q_(e)||!q_(t))return Mse(e,t,s);if(typeof r=="function")return fk(e,t,1,{transform:r});if(Rse(r))return fk(e,t,0,r);let a={...s};return a.capture===!0&&(a.wrap=!0),r=r||a.step||1,uB(r)?uB(e)&&uB(t)?p7e(e,t,r,a):h7e(e,t,Math.max(Math.abs(r),1),a):r!=null&&!Rse(r)?A7e(r,a):fk(e,t,1,r)};Use.exports=fk});var jse=G((BQt,Hse)=>{"use strict";var d7e=Y_(),_se=ck(),g7e=(e,t={})=>{let r=(s,a={})=>{let n=_se.isInvalidBrace(a),c=s.invalid===!0&&t.escapeInvalid===!0,f=n===!0||c===!0,p=t.escapeInvalid===!0?"\\":"",h="";if(s.isOpen===!0||s.isClose===!0)return p+s.value;if(s.type==="open")return f?p+s.value:"(";if(s.type==="close")return f?p+s.value:")";if(s.type==="comma")return s.prev.type==="comma"?"":f?s.value:"|";if(s.value)return s.value;if(s.nodes&&s.ranges>0){let E=_se.reduce(s.nodes),C=d7e(...E,{...t,wrap:!1,toRegex:!0});if(C.length!==0)return E.length>1&&C.length>1?`(${C})`:C}if(s.nodes)for(let E of s.nodes)h+=r(E,s);return h};return r(e)};Hse.exports=g7e});var Wse=G((vQt,qse)=>{"use strict";var m7e=Y_(),Gse=uk(),OE=ck(),jg=(e="",t="",r=!1)=>{let s=[];if(e=[].concat(e),t=[].concat(t),!t.length)return e;if(!e.length)return r?OE.flatten(t).map(a=>`{${a}}`):t;for(let a of e)if(Array.isArray(a))for(let n of a)s.push(jg(n,t,r));else for(let n of t)r===!0&&typeof n=="string"&&(n=`{${n}}`),s.push(Array.isArray(n)?jg(a,n,r):a+n);return OE.flatten(s)},y7e=(e,t={})=>{let r=t.rangeLimit===void 0?1e3:t.rangeLimit,s=(a,n={})=>{a.queue=[];let c=n,f=n.queue;for(;c.type!=="brace"&&c.type!=="root"&&c.parent;)c=c.parent,f=c.queue;if(a.invalid||a.dollar){f.push(jg(f.pop(),Gse(a,t)));return}if(a.type==="brace"&&a.invalid!==!0&&a.nodes.length===2){f.push(jg(f.pop(),["{}"]));return}if(a.nodes&&a.ranges>0){let C=OE.reduce(a.nodes);if(OE.exceedsLimit(...C,t.step,r))throw new RangeError("expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.");let S=m7e(...C,t);S.length===0&&(S=Gse(a,t)),f.push(jg(f.pop(),S)),a.nodes=[];return}let p=OE.encloseBrace(a),h=a.queue,E=a;for(;E.type!=="brace"&&E.type!=="root"&&E.parent;)E=E.parent,h=E.queue;for(let C=0;C{"use strict";Yse.exports={MAX_LENGTH:1024*64,CHAR_0:"0",CHAR_9:"9",CHAR_UPPERCASE_A:"A",CHAR_LOWERCASE_A:"a",CHAR_UPPERCASE_Z:"Z",CHAR_LOWERCASE_Z:"z",CHAR_LEFT_PARENTHESES:"(",CHAR_RIGHT_PARENTHESES:")",CHAR_ASTERISK:"*",CHAR_AMPERSAND:"&",CHAR_AT:"@",CHAR_BACKSLASH:"\\",CHAR_BACKTICK:"`",CHAR_CARRIAGE_RETURN:"\r",CHAR_CIRCUMFLEX_ACCENT:"^",CHAR_COLON:":",CHAR_COMMA:",",CHAR_DOLLAR:"$",CHAR_DOT:".",CHAR_DOUBLE_QUOTE:'"',CHAR_EQUAL:"=",CHAR_EXCLAMATION_MARK:"!",CHAR_FORM_FEED:"\f",CHAR_FORWARD_SLASH:"/",CHAR_HASH:"#",CHAR_HYPHEN_MINUS:"-",CHAR_LEFT_ANGLE_BRACKET:"<",CHAR_LEFT_CURLY_BRACE:"{",CHAR_LEFT_SQUARE_BRACKET:"[",CHAR_LINE_FEED:` `,CHAR_NO_BREAK_SPACE:"\xA0",CHAR_PERCENT:"%",CHAR_PLUS:"+",CHAR_QUESTION_MARK:"?",CHAR_RIGHT_ANGLE_BRACKET:">",CHAR_RIGHT_CURLY_BRACE:"}",CHAR_RIGHT_SQUARE_BRACKET:"]",CHAR_SEMICOLON:";",CHAR_SINGLE_QUOTE:"'",CHAR_SPACE:" ",CHAR_TAB:" ",CHAR_UNDERSCORE:"_",CHAR_VERTICAL_LINE:"|",CHAR_ZERO_WIDTH_NOBREAK_SPACE:"\uFEFF"}});var Zse=G((DQt,Xse)=>{"use strict";var E7e=uk(),{MAX_LENGTH:Jse,CHAR_BACKSLASH:V_,CHAR_BACKTICK:I7e,CHAR_COMMA:C7e,CHAR_DOT:w7e,CHAR_LEFT_PARENTHESES:B7e,CHAR_RIGHT_PARENTHESES:v7e,CHAR_LEFT_CURLY_BRACE:S7e,CHAR_RIGHT_CURLY_BRACE:D7e,CHAR_LEFT_SQUARE_BRACKET:Kse,CHAR_RIGHT_SQUARE_BRACKET:zse,CHAR_DOUBLE_QUOTE:b7e,CHAR_SINGLE_QUOTE:P7e,CHAR_NO_BREAK_SPACE:x7e,CHAR_ZERO_WIDTH_NOBREAK_SPACE:k7e}=Vse(),Q7e=(e,t={})=>{if(typeof e!="string")throw new TypeError("Expected a string");let r=t||{},s=typeof r.maxLength=="number"?Math.min(Jse,r.maxLength):Jse;if(e.length>s)throw new SyntaxError(`Input length (${e.length}), exceeds max characters (${s})`);let a={type:"root",input:e,nodes:[]},n=[a],c=a,f=a,p=0,h=e.length,E=0,C=0,S,x={},I=()=>e[E++],T=O=>{if(O.type==="text"&&f.type==="dot"&&(f.type="text"),f&&f.type==="text"&&O.type==="text"){f.value+=O.value;return}return c.nodes.push(O),O.parent=c,O.prev=f,f=O,O};for(T({type:"bos"});E0){if(c.ranges>0){c.ranges=0;let O=c.nodes.shift();c.nodes=[O,{type:"text",value:E7e(c)}]}T({type:"comma",value:S}),c.commas++;continue}if(S===w7e&&C>0&&c.commas===0){let O=c.nodes;if(C===0||O.length===0){T({type:"text",value:S});continue}if(f.type==="dot"){if(c.range=[],f.value+=S,f.type="range",c.nodes.length!==3&&c.nodes.length!==5){c.invalid=!0,c.ranges=0,f.type="text";continue}c.ranges++,c.args=[];continue}if(f.type==="range"){O.pop();let U=O[O.length-1];U.value+=f.value+S,f=U,c.ranges--;continue}T({type:"dot",value:S});continue}T({type:"text",value:S})}do if(c=n.pop(),c.type!=="root"){c.nodes.forEach(V=>{V.nodes||(V.type==="open"&&(V.isOpen=!0),V.type==="close"&&(V.isClose=!0),V.nodes||(V.type="text"),V.invalid=!0)});let O=n[n.length-1],U=O.nodes.indexOf(c);O.nodes.splice(U,1,...c.nodes)}while(n.length>0);return T({type:"eos"}),a};Xse.exports=Q7e});var toe=G((bQt,eoe)=>{"use strict";var $se=uk(),R7e=jse(),T7e=Wse(),F7e=Zse(),Jl=(e,t={})=>{let r=[];if(Array.isArray(e))for(let s of e){let a=Jl.create(s,t);Array.isArray(a)?r.push(...a):r.push(a)}else r=[].concat(Jl.create(e,t));return t&&t.expand===!0&&t.nodupes===!0&&(r=[...new Set(r)]),r};Jl.parse=(e,t={})=>F7e(e,t);Jl.stringify=(e,t={})=>$se(typeof e=="string"?Jl.parse(e,t):e,t);Jl.compile=(e,t={})=>(typeof e=="string"&&(e=Jl.parse(e,t)),R7e(e,t));Jl.expand=(e,t={})=>{typeof e=="string"&&(e=Jl.parse(e,t));let r=T7e(e,t);return t.noempty===!0&&(r=r.filter(Boolean)),t.nodupes===!0&&(r=[...new Set(r)]),r};Jl.create=(e,t={})=>e===""||e.length<3?[e]:t.expand!==!0?Jl.compile(e,t):Jl.expand(e,t);eoe.exports=Jl});var fB=G((PQt,ooe)=>{"use strict";var N7e=Ie("path"),zf="\\\\/",roe=`[^${zf}]`,Pp="\\.",O7e="\\+",L7e="\\?",Ak="\\/",M7e="(?=.)",noe="[^/]",J_=`(?:${Ak}|$)`,ioe=`(?:^|${Ak})`,K_=`${Pp}{1,2}${J_}`,U7e=`(?!${Pp})`,_7e=`(?!${ioe}${K_})`,H7e=`(?!${Pp}{0,1}${J_})`,j7e=`(?!${K_})`,G7e=`[^.${Ak}]`,q7e=`${noe}*?`,soe={DOT_LITERAL:Pp,PLUS_LITERAL:O7e,QMARK_LITERAL:L7e,SLASH_LITERAL:Ak,ONE_CHAR:M7e,QMARK:noe,END_ANCHOR:J_,DOTS_SLASH:K_,NO_DOT:U7e,NO_DOTS:_7e,NO_DOT_SLASH:H7e,NO_DOTS_SLASH:j7e,QMARK_NO_DOT:G7e,STAR:q7e,START_ANCHOR:ioe},W7e={...soe,SLASH_LITERAL:`[${zf}]`,QMARK:roe,STAR:`${roe}*?`,DOTS_SLASH:`${Pp}{1,2}(?:[${zf}]|$)`,NO_DOT:`(?!${Pp})`,NO_DOTS:`(?!(?:^|[${zf}])${Pp}{1,2}(?:[${zf}]|$))`,NO_DOT_SLASH:`(?!${Pp}{0,1}(?:[${zf}]|$))`,NO_DOTS_SLASH:`(?!${Pp}{1,2}(?:[${zf}]|$))`,QMARK_NO_DOT:`[^.${zf}]`,START_ANCHOR:`(?:^|[${zf}])`,END_ANCHOR:`(?:[${zf}]|$)`},Y7e={alnum:"a-zA-Z0-9",alpha:"a-zA-Z",ascii:"\\x00-\\x7F",blank:" \\t",cntrl:"\\x00-\\x1F\\x7F",digit:"0-9",graph:"\\x21-\\x7E",lower:"a-z",print:"\\x20-\\x7E ",punct:"\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~",space:" \\t\\r\\n\\v\\f",upper:"A-Z",word:"A-Za-z0-9_",xdigit:"A-Fa-f0-9"};ooe.exports={MAX_LENGTH:1024*64,POSIX_REGEX_SOURCE:Y7e,REGEX_BACKSLASH:/\\(?![*+?^${}(|)[\]])/g,REGEX_NON_SPECIAL_CHARS:/^[^@![\].,$*+?^{}()|\\/]+/,REGEX_SPECIAL_CHARS:/[-*+?.^${}(|)[\]]/,REGEX_SPECIAL_CHARS_BACKREF:/(\\?)((\W)(\3*))/g,REGEX_SPECIAL_CHARS_GLOBAL:/([-*+?.^${}(|)[\]])/g,REGEX_REMOVE_BACKSLASH:/(?:\[.*?[^\\]\]|\\(?=.))/g,REPLACEMENTS:{"***":"*","**/**":"**","**/**/**":"**"},CHAR_0:48,CHAR_9:57,CHAR_UPPERCASE_A:65,CHAR_LOWERCASE_A:97,CHAR_UPPERCASE_Z:90,CHAR_LOWERCASE_Z:122,CHAR_LEFT_PARENTHESES:40,CHAR_RIGHT_PARENTHESES:41,CHAR_ASTERISK:42,CHAR_AMPERSAND:38,CHAR_AT:64,CHAR_BACKWARD_SLASH:92,CHAR_CARRIAGE_RETURN:13,CHAR_CIRCUMFLEX_ACCENT:94,CHAR_COLON:58,CHAR_COMMA:44,CHAR_DOT:46,CHAR_DOUBLE_QUOTE:34,CHAR_EQUAL:61,CHAR_EXCLAMATION_MARK:33,CHAR_FORM_FEED:12,CHAR_FORWARD_SLASH:47,CHAR_GRAVE_ACCENT:96,CHAR_HASH:35,CHAR_HYPHEN_MINUS:45,CHAR_LEFT_ANGLE_BRACKET:60,CHAR_LEFT_CURLY_BRACE:123,CHAR_LEFT_SQUARE_BRACKET:91,CHAR_LINE_FEED:10,CHAR_NO_BREAK_SPACE:160,CHAR_PERCENT:37,CHAR_PLUS:43,CHAR_QUESTION_MARK:63,CHAR_RIGHT_ANGLE_BRACKET:62,CHAR_RIGHT_CURLY_BRACE:125,CHAR_RIGHT_SQUARE_BRACKET:93,CHAR_SEMICOLON:59,CHAR_SINGLE_QUOTE:39,CHAR_SPACE:32,CHAR_TAB:9,CHAR_UNDERSCORE:95,CHAR_VERTICAL_LINE:124,CHAR_ZERO_WIDTH_NOBREAK_SPACE:65279,SEP:N7e.sep,extglobChars(e){return{"!":{type:"negate",open:"(?:(?!(?:",close:`))${e.STAR})`},"?":{type:"qmark",open:"(?:",close:")?"},"+":{type:"plus",open:"(?:",close:")+"},"*":{type:"star",open:"(?:",close:")*"},"@":{type:"at",open:"(?:",close:")"}}},globChars(e){return e===!0?W7e:soe}}});var AB=G(dl=>{"use strict";var V7e=Ie("path"),J7e=process.platform==="win32",{REGEX_BACKSLASH:K7e,REGEX_REMOVE_BACKSLASH:z7e,REGEX_SPECIAL_CHARS:X7e,REGEX_SPECIAL_CHARS_GLOBAL:Z7e}=fB();dl.isObject=e=>e!==null&&typeof e=="object"&&!Array.isArray(e);dl.hasRegexChars=e=>X7e.test(e);dl.isRegexChar=e=>e.length===1&&dl.hasRegexChars(e);dl.escapeRegex=e=>e.replace(Z7e,"\\$1");dl.toPosixSlashes=e=>e.replace(K7e,"/");dl.removeBackslashes=e=>e.replace(z7e,t=>t==="\\"?"":t);dl.supportsLookbehinds=()=>{let e=process.version.slice(1).split(".").map(Number);return e.length===3&&e[0]>=9||e[0]===8&&e[1]>=10};dl.isWindows=e=>e&&typeof e.windows=="boolean"?e.windows:J7e===!0||V7e.sep==="\\";dl.escapeLast=(e,t,r)=>{let s=e.lastIndexOf(t,r);return s===-1?e:e[s-1]==="\\"?dl.escapeLast(e,t,s-1):`${e.slice(0,s)}\\${e.slice(s)}`};dl.removePrefix=(e,t={})=>{let r=e;return r.startsWith("./")&&(r=r.slice(2),t.prefix="./"),r};dl.wrapOutput=(e,t={},r={})=>{let s=r.contains?"":"^",a=r.contains?"":"$",n=`${s}(?:${e})${a}`;return t.negated===!0&&(n=`(?:^(?!${n}).*$)`),n}});var hoe=G((kQt,poe)=>{"use strict";var aoe=AB(),{CHAR_ASTERISK:z_,CHAR_AT:$7e,CHAR_BACKWARD_SLASH:pB,CHAR_COMMA:eJe,CHAR_DOT:X_,CHAR_EXCLAMATION_MARK:Z_,CHAR_FORWARD_SLASH:Aoe,CHAR_LEFT_CURLY_BRACE:$_,CHAR_LEFT_PARENTHESES:e4,CHAR_LEFT_SQUARE_BRACKET:tJe,CHAR_PLUS:rJe,CHAR_QUESTION_MARK:loe,CHAR_RIGHT_CURLY_BRACE:nJe,CHAR_RIGHT_PARENTHESES:coe,CHAR_RIGHT_SQUARE_BRACKET:iJe}=fB(),uoe=e=>e===Aoe||e===pB,foe=e=>{e.isPrefix!==!0&&(e.depth=e.isGlobstar?1/0:1)},sJe=(e,t)=>{let r=t||{},s=e.length-1,a=r.parts===!0||r.scanToEnd===!0,n=[],c=[],f=[],p=e,h=-1,E=0,C=0,S=!1,x=!1,I=!1,T=!1,O=!1,U=!1,V=!1,te=!1,ie=!1,ue=!1,ae=0,ge,Ae,Ce={value:"",depth:0,isGlob:!1},Ee=()=>h>=s,d=()=>p.charCodeAt(h+1),Se=()=>(ge=Ae,p.charCodeAt(++h));for(;h0&&(me=p.slice(0,E),p=p.slice(E),C-=E),Be&&I===!0&&C>0?(Be=p.slice(0,C),ce=p.slice(C)):I===!0?(Be="",ce=p):Be=p,Be&&Be!==""&&Be!=="/"&&Be!==p&&uoe(Be.charCodeAt(Be.length-1))&&(Be=Be.slice(0,-1)),r.unescape===!0&&(ce&&(ce=aoe.removeBackslashes(ce)),Be&&V===!0&&(Be=aoe.removeBackslashes(Be)));let Z={prefix:me,input:e,start:E,base:Be,glob:ce,isBrace:S,isBracket:x,isGlob:I,isExtglob:T,isGlobstar:O,negated:te,negatedExtglob:ie};if(r.tokens===!0&&(Z.maxDepth=0,uoe(Ae)||c.push(Ce),Z.tokens=c),r.parts===!0||r.tokens===!0){let De;for(let Qe=0;Qe{"use strict";var pk=fB(),Kl=AB(),{MAX_LENGTH:hk,POSIX_REGEX_SOURCE:oJe,REGEX_NON_SPECIAL_CHARS:aJe,REGEX_SPECIAL_CHARS_BACKREF:lJe,REPLACEMENTS:doe}=pk,cJe=(e,t)=>{if(typeof t.expandRange=="function")return t.expandRange(...e,t);e.sort();let r=`[${e.join("-")}]`;try{new RegExp(r)}catch{return e.map(a=>Kl.escapeRegex(a)).join("..")}return r},LE=(e,t)=>`Missing ${e}: "${t}" - use "\\\\${t}" to match literal characters`,t4=(e,t)=>{if(typeof e!="string")throw new TypeError("Expected a string");e=doe[e]||e;let r={...t},s=typeof r.maxLength=="number"?Math.min(hk,r.maxLength):hk,a=e.length;if(a>s)throw new SyntaxError(`Input length: ${a}, exceeds maximum allowed length: ${s}`);let n={type:"bos",value:"",output:r.prepend||""},c=[n],f=r.capture?"":"?:",p=Kl.isWindows(t),h=pk.globChars(p),E=pk.extglobChars(h),{DOT_LITERAL:C,PLUS_LITERAL:S,SLASH_LITERAL:x,ONE_CHAR:I,DOTS_SLASH:T,NO_DOT:O,NO_DOT_SLASH:U,NO_DOTS_SLASH:V,QMARK:te,QMARK_NO_DOT:ie,STAR:ue,START_ANCHOR:ae}=h,ge=P=>`(${f}(?:(?!${ae}${P.dot?T:C}).)*?)`,Ae=r.dot?"":O,Ce=r.dot?te:ie,Ee=r.bash===!0?ge(r):ue;r.capture&&(Ee=`(${Ee})`),typeof r.noext=="boolean"&&(r.noextglob=r.noext);let d={input:e,index:-1,start:0,dot:r.dot===!0,consumed:"",output:"",prefix:"",backtrack:!1,negated:!1,brackets:0,braces:0,parens:0,quotes:0,globstar:!1,tokens:c};e=Kl.removePrefix(e,d),a=e.length;let Se=[],Be=[],me=[],ce=n,Z,De=()=>d.index===a-1,Qe=d.peek=(P=1)=>e[d.index+P],st=d.advance=()=>e[++d.index]||"",_=()=>e.slice(d.index+1),tt=(P="",w=0)=>{d.consumed+=P,d.index+=w},Ne=P=>{d.output+=P.output!=null?P.output:P.value,tt(P.value)},ke=()=>{let P=1;for(;Qe()==="!"&&(Qe(2)!=="("||Qe(3)==="?");)st(),d.start++,P++;return P%2===0?!1:(d.negated=!0,d.start++,!0)},be=P=>{d[P]++,me.push(P)},je=P=>{d[P]--,me.pop()},Re=P=>{if(ce.type==="globstar"){let w=d.braces>0&&(P.type==="comma"||P.type==="brace"),b=P.extglob===!0||Se.length&&(P.type==="pipe"||P.type==="paren");P.type!=="slash"&&P.type!=="paren"&&!w&&!b&&(d.output=d.output.slice(0,-ce.output.length),ce.type="star",ce.value="*",ce.output=Ee,d.output+=ce.output)}if(Se.length&&P.type!=="paren"&&(Se[Se.length-1].inner+=P.value),(P.value||P.output)&&Ne(P),ce&&ce.type==="text"&&P.type==="text"){ce.value+=P.value,ce.output=(ce.output||"")+P.value;return}P.prev=ce,c.push(P),ce=P},ct=(P,w)=>{let b={...E[w],conditions:1,inner:""};b.prev=ce,b.parens=d.parens,b.output=d.output;let y=(r.capture?"(":"")+b.open;be("parens"),Re({type:P,value:w,output:d.output?"":I}),Re({type:"paren",extglob:!0,value:st(),output:y}),Se.push(b)},Me=P=>{let w=P.close+(r.capture?")":""),b;if(P.type==="negate"){let y=Ee;if(P.inner&&P.inner.length>1&&P.inner.includes("/")&&(y=ge(r)),(y!==Ee||De()||/^\)+$/.test(_()))&&(w=P.close=`)$))${y}`),P.inner.includes("*")&&(b=_())&&/^\.[^\\/.]+$/.test(b)){let F=t4(b,{...t,fastpaths:!1}).output;w=P.close=`)${F})${y})`}P.prev.type==="bos"&&(d.negatedExtglob=!0)}Re({type:"paren",extglob:!0,value:Z,output:w}),je("parens")};if(r.fastpaths!==!1&&!/(^[*!]|[/()[\]{}"])/.test(e)){let P=!1,w=e.replace(lJe,(b,y,F,z,X,$)=>z==="\\"?(P=!0,b):z==="?"?y?y+z+(X?te.repeat(X.length):""):$===0?Ce+(X?te.repeat(X.length):""):te.repeat(F.length):z==="."?C.repeat(F.length):z==="*"?y?y+z+(X?Ee:""):Ee:y?b:`\\${b}`);return P===!0&&(r.unescape===!0?w=w.replace(/\\/g,""):w=w.replace(/\\+/g,b=>b.length%2===0?"\\\\":b?"\\":"")),w===e&&r.contains===!0?(d.output=e,d):(d.output=Kl.wrapOutput(w,d,t),d)}for(;!De();){if(Z=st(),Z==="\0")continue;if(Z==="\\"){let b=Qe();if(b==="/"&&r.bash!==!0||b==="."||b===";")continue;if(!b){Z+="\\",Re({type:"text",value:Z});continue}let y=/^\\+/.exec(_()),F=0;if(y&&y[0].length>2&&(F=y[0].length,d.index+=F,F%2!==0&&(Z+="\\")),r.unescape===!0?Z=st():Z+=st(),d.brackets===0){Re({type:"text",value:Z});continue}}if(d.brackets>0&&(Z!=="]"||ce.value==="["||ce.value==="[^")){if(r.posix!==!1&&Z===":"){let b=ce.value.slice(1);if(b.includes("[")&&(ce.posix=!0,b.includes(":"))){let y=ce.value.lastIndexOf("["),F=ce.value.slice(0,y),z=ce.value.slice(y+2),X=oJe[z];if(X){ce.value=F+X,d.backtrack=!0,st(),!n.output&&c.indexOf(ce)===1&&(n.output=I);continue}}}(Z==="["&&Qe()!==":"||Z==="-"&&Qe()==="]")&&(Z=`\\${Z}`),Z==="]"&&(ce.value==="["||ce.value==="[^")&&(Z=`\\${Z}`),r.posix===!0&&Z==="!"&&ce.value==="["&&(Z="^"),ce.value+=Z,Ne({value:Z});continue}if(d.quotes===1&&Z!=='"'){Z=Kl.escapeRegex(Z),ce.value+=Z,Ne({value:Z});continue}if(Z==='"'){d.quotes=d.quotes===1?0:1,r.keepQuotes===!0&&Re({type:"text",value:Z});continue}if(Z==="("){be("parens"),Re({type:"paren",value:Z});continue}if(Z===")"){if(d.parens===0&&r.strictBrackets===!0)throw new SyntaxError(LE("opening","("));let b=Se[Se.length-1];if(b&&d.parens===b.parens+1){Me(Se.pop());continue}Re({type:"paren",value:Z,output:d.parens?")":"\\)"}),je("parens");continue}if(Z==="["){if(r.nobracket===!0||!_().includes("]")){if(r.nobracket!==!0&&r.strictBrackets===!0)throw new SyntaxError(LE("closing","]"));Z=`\\${Z}`}else be("brackets");Re({type:"bracket",value:Z});continue}if(Z==="]"){if(r.nobracket===!0||ce&&ce.type==="bracket"&&ce.value.length===1){Re({type:"text",value:Z,output:`\\${Z}`});continue}if(d.brackets===0){if(r.strictBrackets===!0)throw new SyntaxError(LE("opening","["));Re({type:"text",value:Z,output:`\\${Z}`});continue}je("brackets");let b=ce.value.slice(1);if(ce.posix!==!0&&b[0]==="^"&&!b.includes("/")&&(Z=`/${Z}`),ce.value+=Z,Ne({value:Z}),r.literalBrackets===!1||Kl.hasRegexChars(b))continue;let y=Kl.escapeRegex(ce.value);if(d.output=d.output.slice(0,-ce.value.length),r.literalBrackets===!0){d.output+=y,ce.value=y;continue}ce.value=`(${f}${y}|${ce.value})`,d.output+=ce.value;continue}if(Z==="{"&&r.nobrace!==!0){be("braces");let b={type:"brace",value:Z,output:"(",outputIndex:d.output.length,tokensIndex:d.tokens.length};Be.push(b),Re(b);continue}if(Z==="}"){let b=Be[Be.length-1];if(r.nobrace===!0||!b){Re({type:"text",value:Z,output:Z});continue}let y=")";if(b.dots===!0){let F=c.slice(),z=[];for(let X=F.length-1;X>=0&&(c.pop(),F[X].type!=="brace");X--)F[X].type!=="dots"&&z.unshift(F[X].value);y=cJe(z,r),d.backtrack=!0}if(b.comma!==!0&&b.dots!==!0){let F=d.output.slice(0,b.outputIndex),z=d.tokens.slice(b.tokensIndex);b.value=b.output="\\{",Z=y="\\}",d.output=F;for(let X of z)d.output+=X.output||X.value}Re({type:"brace",value:Z,output:y}),je("braces"),Be.pop();continue}if(Z==="|"){Se.length>0&&Se[Se.length-1].conditions++,Re({type:"text",value:Z});continue}if(Z===","){let b=Z,y=Be[Be.length-1];y&&me[me.length-1]==="braces"&&(y.comma=!0,b="|"),Re({type:"comma",value:Z,output:b});continue}if(Z==="/"){if(ce.type==="dot"&&d.index===d.start+1){d.start=d.index+1,d.consumed="",d.output="",c.pop(),ce=n;continue}Re({type:"slash",value:Z,output:x});continue}if(Z==="."){if(d.braces>0&&ce.type==="dot"){ce.value==="."&&(ce.output=C);let b=Be[Be.length-1];ce.type="dots",ce.output+=Z,ce.value+=Z,b.dots=!0;continue}if(d.braces+d.parens===0&&ce.type!=="bos"&&ce.type!=="slash"){Re({type:"text",value:Z,output:C});continue}Re({type:"dot",value:Z,output:C});continue}if(Z==="?"){if(!(ce&&ce.value==="(")&&r.noextglob!==!0&&Qe()==="("&&Qe(2)!=="?"){ct("qmark",Z);continue}if(ce&&ce.type==="paren"){let y=Qe(),F=Z;if(y==="<"&&!Kl.supportsLookbehinds())throw new Error("Node.js v10 or higher is required for regex lookbehinds");(ce.value==="("&&!/[!=<:]/.test(y)||y==="<"&&!/<([!=]|\w+>)/.test(_()))&&(F=`\\${Z}`),Re({type:"text",value:Z,output:F});continue}if(r.dot!==!0&&(ce.type==="slash"||ce.type==="bos")){Re({type:"qmark",value:Z,output:ie});continue}Re({type:"qmark",value:Z,output:te});continue}if(Z==="!"){if(r.noextglob!==!0&&Qe()==="("&&(Qe(2)!=="?"||!/[!=<:]/.test(Qe(3)))){ct("negate",Z);continue}if(r.nonegate!==!0&&d.index===0){ke();continue}}if(Z==="+"){if(r.noextglob!==!0&&Qe()==="("&&Qe(2)!=="?"){ct("plus",Z);continue}if(ce&&ce.value==="("||r.regex===!1){Re({type:"plus",value:Z,output:S});continue}if(ce&&(ce.type==="bracket"||ce.type==="paren"||ce.type==="brace")||d.parens>0){Re({type:"plus",value:Z});continue}Re({type:"plus",value:S});continue}if(Z==="@"){if(r.noextglob!==!0&&Qe()==="("&&Qe(2)!=="?"){Re({type:"at",extglob:!0,value:Z,output:""});continue}Re({type:"text",value:Z});continue}if(Z!=="*"){(Z==="$"||Z==="^")&&(Z=`\\${Z}`);let b=aJe.exec(_());b&&(Z+=b[0],d.index+=b[0].length),Re({type:"text",value:Z});continue}if(ce&&(ce.type==="globstar"||ce.star===!0)){ce.type="star",ce.star=!0,ce.value+=Z,ce.output=Ee,d.backtrack=!0,d.globstar=!0,tt(Z);continue}let P=_();if(r.noextglob!==!0&&/^\([^?]/.test(P)){ct("star",Z);continue}if(ce.type==="star"){if(r.noglobstar===!0){tt(Z);continue}let b=ce.prev,y=b.prev,F=b.type==="slash"||b.type==="bos",z=y&&(y.type==="star"||y.type==="globstar");if(r.bash===!0&&(!F||P[0]&&P[0]!=="/")){Re({type:"star",value:Z,output:""});continue}let X=d.braces>0&&(b.type==="comma"||b.type==="brace"),$=Se.length&&(b.type==="pipe"||b.type==="paren");if(!F&&b.type!=="paren"&&!X&&!$){Re({type:"star",value:Z,output:""});continue}for(;P.slice(0,3)==="/**";){let se=e[d.index+4];if(se&&se!=="/")break;P=P.slice(3),tt("/**",3)}if(b.type==="bos"&&De()){ce.type="globstar",ce.value+=Z,ce.output=ge(r),d.output=ce.output,d.globstar=!0,tt(Z);continue}if(b.type==="slash"&&b.prev.type!=="bos"&&!z&&De()){d.output=d.output.slice(0,-(b.output+ce.output).length),b.output=`(?:${b.output}`,ce.type="globstar",ce.output=ge(r)+(r.strictSlashes?")":"|$)"),ce.value+=Z,d.globstar=!0,d.output+=b.output+ce.output,tt(Z);continue}if(b.type==="slash"&&b.prev.type!=="bos"&&P[0]==="/"){let se=P[1]!==void 0?"|$":"";d.output=d.output.slice(0,-(b.output+ce.output).length),b.output=`(?:${b.output}`,ce.type="globstar",ce.output=`${ge(r)}${x}|${x}${se})`,ce.value+=Z,d.output+=b.output+ce.output,d.globstar=!0,tt(Z+st()),Re({type:"slash",value:"/",output:""});continue}if(b.type==="bos"&&P[0]==="/"){ce.type="globstar",ce.value+=Z,ce.output=`(?:^|${x}|${ge(r)}${x})`,d.output=ce.output,d.globstar=!0,tt(Z+st()),Re({type:"slash",value:"/",output:""});continue}d.output=d.output.slice(0,-ce.output.length),ce.type="globstar",ce.output=ge(r),ce.value+=Z,d.output+=ce.output,d.globstar=!0,tt(Z);continue}let w={type:"star",value:Z,output:Ee};if(r.bash===!0){w.output=".*?",(ce.type==="bos"||ce.type==="slash")&&(w.output=Ae+w.output),Re(w);continue}if(ce&&(ce.type==="bracket"||ce.type==="paren")&&r.regex===!0){w.output=Z,Re(w);continue}(d.index===d.start||ce.type==="slash"||ce.type==="dot")&&(ce.type==="dot"?(d.output+=U,ce.output+=U):r.dot===!0?(d.output+=V,ce.output+=V):(d.output+=Ae,ce.output+=Ae),Qe()!=="*"&&(d.output+=I,ce.output+=I)),Re(w)}for(;d.brackets>0;){if(r.strictBrackets===!0)throw new SyntaxError(LE("closing","]"));d.output=Kl.escapeLast(d.output,"["),je("brackets")}for(;d.parens>0;){if(r.strictBrackets===!0)throw new SyntaxError(LE("closing",")"));d.output=Kl.escapeLast(d.output,"("),je("parens")}for(;d.braces>0;){if(r.strictBrackets===!0)throw new SyntaxError(LE("closing","}"));d.output=Kl.escapeLast(d.output,"{"),je("braces")}if(r.strictSlashes!==!0&&(ce.type==="star"||ce.type==="bracket")&&Re({type:"maybe_slash",value:"",output:`${x}?`}),d.backtrack===!0){d.output="";for(let P of d.tokens)d.output+=P.output!=null?P.output:P.value,P.suffix&&(d.output+=P.suffix)}return d};t4.fastpaths=(e,t)=>{let r={...t},s=typeof r.maxLength=="number"?Math.min(hk,r.maxLength):hk,a=e.length;if(a>s)throw new SyntaxError(`Input length: ${a}, exceeds maximum allowed length: ${s}`);e=doe[e]||e;let n=Kl.isWindows(t),{DOT_LITERAL:c,SLASH_LITERAL:f,ONE_CHAR:p,DOTS_SLASH:h,NO_DOT:E,NO_DOTS:C,NO_DOTS_SLASH:S,STAR:x,START_ANCHOR:I}=pk.globChars(n),T=r.dot?C:E,O=r.dot?S:E,U=r.capture?"":"?:",V={negated:!1,prefix:""},te=r.bash===!0?".*?":x;r.capture&&(te=`(${te})`);let ie=Ae=>Ae.noglobstar===!0?te:`(${U}(?:(?!${I}${Ae.dot?h:c}).)*?)`,ue=Ae=>{switch(Ae){case"*":return`${T}${p}${te}`;case".*":return`${c}${p}${te}`;case"*.*":return`${T}${te}${c}${p}${te}`;case"*/*":return`${T}${te}${f}${p}${O}${te}`;case"**":return T+ie(r);case"**/*":return`(?:${T}${ie(r)}${f})?${O}${p}${te}`;case"**/*.*":return`(?:${T}${ie(r)}${f})?${O}${te}${c}${p}${te}`;case"**/.*":return`(?:${T}${ie(r)}${f})?${c}${p}${te}`;default:{let Ce=/^(.*?)\.(\w+)$/.exec(Ae);if(!Ce)return;let Ee=ue(Ce[1]);return Ee?Ee+c+Ce[2]:void 0}}},ae=Kl.removePrefix(e,V),ge=ue(ae);return ge&&r.strictSlashes!==!0&&(ge+=`${f}?`),ge};goe.exports=t4});var Eoe=G((RQt,yoe)=>{"use strict";var uJe=Ie("path"),fJe=hoe(),r4=moe(),n4=AB(),AJe=fB(),pJe=e=>e&&typeof e=="object"&&!Array.isArray(e),Zi=(e,t,r=!1)=>{if(Array.isArray(e)){let E=e.map(S=>Zi(S,t,r));return S=>{for(let x of E){let I=x(S);if(I)return I}return!1}}let s=pJe(e)&&e.tokens&&e.input;if(e===""||typeof e!="string"&&!s)throw new TypeError("Expected pattern to be a non-empty string");let a=t||{},n=n4.isWindows(t),c=s?Zi.compileRe(e,t):Zi.makeRe(e,t,!1,!0),f=c.state;delete c.state;let p=()=>!1;if(a.ignore){let E={...t,ignore:null,onMatch:null,onResult:null};p=Zi(a.ignore,E,r)}let h=(E,C=!1)=>{let{isMatch:S,match:x,output:I}=Zi.test(E,c,t,{glob:e,posix:n}),T={glob:e,state:f,regex:c,posix:n,input:E,output:I,match:x,isMatch:S};return typeof a.onResult=="function"&&a.onResult(T),S===!1?(T.isMatch=!1,C?T:!1):p(E)?(typeof a.onIgnore=="function"&&a.onIgnore(T),T.isMatch=!1,C?T:!1):(typeof a.onMatch=="function"&&a.onMatch(T),C?T:!0)};return r&&(h.state=f),h};Zi.test=(e,t,r,{glob:s,posix:a}={})=>{if(typeof e!="string")throw new TypeError("Expected input to be a string");if(e==="")return{isMatch:!1,output:""};let n=r||{},c=n.format||(a?n4.toPosixSlashes:null),f=e===s,p=f&&c?c(e):e;return f===!1&&(p=c?c(e):e,f=p===s),(f===!1||n.capture===!0)&&(n.matchBase===!0||n.basename===!0?f=Zi.matchBase(e,t,r,a):f=t.exec(p)),{isMatch:!!f,match:f,output:p}};Zi.matchBase=(e,t,r,s=n4.isWindows(r))=>(t instanceof RegExp?t:Zi.makeRe(t,r)).test(uJe.basename(e));Zi.isMatch=(e,t,r)=>Zi(t,r)(e);Zi.parse=(e,t)=>Array.isArray(e)?e.map(r=>Zi.parse(r,t)):r4(e,{...t,fastpaths:!1});Zi.scan=(e,t)=>fJe(e,t);Zi.compileRe=(e,t,r=!1,s=!1)=>{if(r===!0)return e.output;let a=t||{},n=a.contains?"":"^",c=a.contains?"":"$",f=`${n}(?:${e.output})${c}`;e&&e.negated===!0&&(f=`^(?!${f}).*$`);let p=Zi.toRegex(f,t);return s===!0&&(p.state=e),p};Zi.makeRe=(e,t={},r=!1,s=!1)=>{if(!e||typeof e!="string")throw new TypeError("Expected a non-empty string");let a={negated:!1,fastpaths:!0};return t.fastpaths!==!1&&(e[0]==="."||e[0]==="*")&&(a.output=r4.fastpaths(e,t)),a.output||(a=r4(e,t)),Zi.compileRe(a,t,r,s)};Zi.toRegex=(e,t)=>{try{let r=t||{};return new RegExp(e,r.flags||(r.nocase?"i":""))}catch(r){if(t&&t.debug===!0)throw r;return/$^/}};Zi.constants=AJe;yoe.exports=Zi});var Coe=G((TQt,Ioe)=>{"use strict";Ioe.exports=Eoe()});var zo=G((FQt,Soe)=>{"use strict";var Boe=Ie("util"),voe=toe(),Xf=Coe(),i4=AB(),woe=e=>e===""||e==="./",xi=(e,t,r)=>{t=[].concat(t),e=[].concat(e);let s=new Set,a=new Set,n=new Set,c=0,f=E=>{n.add(E.output),r&&r.onResult&&r.onResult(E)};for(let E=0;E!s.has(E));if(r&&h.length===0){if(r.failglob===!0)throw new Error(`No matches found for "${t.join(", ")}"`);if(r.nonull===!0||r.nullglob===!0)return r.unescape?t.map(E=>E.replace(/\\/g,"")):t}return h};xi.match=xi;xi.matcher=(e,t)=>Xf(e,t);xi.isMatch=(e,t,r)=>Xf(t,r)(e);xi.any=xi.isMatch;xi.not=(e,t,r={})=>{t=[].concat(t).map(String);let s=new Set,a=[],n=f=>{r.onResult&&r.onResult(f),a.push(f.output)},c=new Set(xi(e,t,{...r,onResult:n}));for(let f of a)c.has(f)||s.add(f);return[...s]};xi.contains=(e,t,r)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${Boe.inspect(e)}"`);if(Array.isArray(t))return t.some(s=>xi.contains(e,s,r));if(typeof t=="string"){if(woe(e)||woe(t))return!1;if(e.includes(t)||e.startsWith("./")&&e.slice(2).includes(t))return!0}return xi.isMatch(e,t,{...r,contains:!0})};xi.matchKeys=(e,t,r)=>{if(!i4.isObject(e))throw new TypeError("Expected the first argument to be an object");let s=xi(Object.keys(e),t,r),a={};for(let n of s)a[n]=e[n];return a};xi.some=(e,t,r)=>{let s=[].concat(e);for(let a of[].concat(t)){let n=Xf(String(a),r);if(s.some(c=>n(c)))return!0}return!1};xi.every=(e,t,r)=>{let s=[].concat(e);for(let a of[].concat(t)){let n=Xf(String(a),r);if(!s.every(c=>n(c)))return!1}return!0};xi.all=(e,t,r)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${Boe.inspect(e)}"`);return[].concat(t).every(s=>Xf(s,r)(e))};xi.capture=(e,t,r)=>{let s=i4.isWindows(r),n=Xf.makeRe(String(e),{...r,capture:!0}).exec(s?i4.toPosixSlashes(t):t);if(n)return n.slice(1).map(c=>c===void 0?"":c)};xi.makeRe=(...e)=>Xf.makeRe(...e);xi.scan=(...e)=>Xf.scan(...e);xi.parse=(e,t)=>{let r=[];for(let s of[].concat(e||[]))for(let a of voe(String(s),t))r.push(Xf.parse(a,t));return r};xi.braces=(e,t)=>{if(typeof e!="string")throw new TypeError("Expected a string");return t&&t.nobrace===!0||!/\{.*\}/.test(e)?[e]:voe(e,t)};xi.braceExpand=(e,t)=>{if(typeof e!="string")throw new TypeError("Expected a string");return xi.braces(e,{...t,expand:!0})};Soe.exports=xi});var boe=G((NQt,Doe)=>{"use strict";Doe.exports=({onlyFirst:e=!1}={})=>{let t=["[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)","(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"].join("|");return new RegExp(t,e?void 0:"g")}});var dk=G((OQt,Poe)=>{"use strict";var hJe=boe();Poe.exports=e=>typeof e=="string"?e.replace(hJe(),""):e});function xoe(e){return Number.isSafeInteger(e)&&e>=0}var koe=Xe(()=>{});function Qoe(e){return e!=null&&typeof e!="function"&&xoe(e.length)}var Roe=Xe(()=>{koe()});function xc(e){return e==="__proto__"}var hB=Xe(()=>{});function ME(e){switch(typeof e){case"number":case"symbol":return!1;case"string":return e.includes(".")||e.includes("[")||e.includes("]")}}var gk=Xe(()=>{});function UE(e){return typeof e=="string"||typeof e=="symbol"?e:Object.is(e?.valueOf?.(),-0)?"-0":String(e)}var mk=Xe(()=>{});function Ou(e){let t=[],r=e.length;if(r===0)return t;let s=0,a="",n="",c=!1;for(e.charCodeAt(0)===46&&(t.push(""),s++);s{});function Pa(e,t,r){if(e==null)return r;switch(typeof t){case"string":{if(xc(t))return r;let s=e[t];return s===void 0?ME(t)?Pa(e,Ou(t),r):r:s}case"number":case"symbol":{typeof t=="number"&&(t=UE(t));let s=e[t];return s===void 0?r:s}default:{if(Array.isArray(t))return dJe(e,t,r);if(Object.is(t?.valueOf(),-0)?t="-0":t=String(t),xc(t))return r;let s=e[t];return s===void 0?r:s}}}function dJe(e,t,r){if(t.length===0)return r;let s=e;for(let a=0;a{hB();gk();mk();_E()});function s4(e){return e!==null&&(typeof e=="object"||typeof e=="function")}var Toe=Xe(()=>{});function HE(e){return e==null||typeof e!="object"&&typeof e!="function"}var Ek=Xe(()=>{});function Ik(e,t){return e===t||Number.isNaN(e)&&Number.isNaN(t)}var o4=Xe(()=>{});function Gg(e){return Object.getOwnPropertySymbols(e).filter(t=>Object.prototype.propertyIsEnumerable.call(e,t))}var Ck=Xe(()=>{});function qg(e){return e==null?e===void 0?"[object Undefined]":"[object Null]":Object.prototype.toString.call(e)}var wk=Xe(()=>{});var Bk,jE,GE,qE,Wg,vk,Sk,Dk,bk,Pk,Foe,xk,WE,Noe,kk,Qk,Rk,Tk,Fk,Ooe,Nk,Ok,Lk,Loe,Mk,Uk,_k=Xe(()=>{Bk="[object RegExp]",jE="[object String]",GE="[object Number]",qE="[object Boolean]",Wg="[object Arguments]",vk="[object Symbol]",Sk="[object Date]",Dk="[object Map]",bk="[object Set]",Pk="[object Array]",Foe="[object Function]",xk="[object ArrayBuffer]",WE="[object Object]",Noe="[object Error]",kk="[object DataView]",Qk="[object Uint8Array]",Rk="[object Uint8ClampedArray]",Tk="[object Uint16Array]",Fk="[object Uint32Array]",Ooe="[object BigUint64Array]",Nk="[object Int8Array]",Ok="[object Int16Array]",Lk="[object Int32Array]",Loe="[object BigInt64Array]",Mk="[object Float32Array]",Uk="[object Float64Array]"});function YE(e){return ArrayBuffer.isView(e)&&!(e instanceof DataView)}var Hk=Xe(()=>{});function Moe(e,t){return c0(e,void 0,e,new Map,t)}function c0(e,t,r,s=new Map,a=void 0){let n=a?.(e,t,r,s);if(n!=null)return n;if(HE(e))return e;if(s.has(e))return s.get(e);if(Array.isArray(e)){let c=new Array(e.length);s.set(e,c);for(let f=0;f{Ck();wk();_k();Ek();Hk()});function Uoe(e){return c0(e,void 0,e,new Map,void 0)}var _oe=Xe(()=>{a4()});function Hoe(e,t){return Moe(e,(r,s,a,n)=>{let c=t?.(r,s,a,n);if(c!=null)return c;if(typeof e=="object")switch(Object.prototype.toString.call(e)){case GE:case jE:case qE:{let f=new e.constructor(e?.valueOf());return l0(f,e),f}case Wg:{let f={};return l0(f,e),f.length=e.length,f[Symbol.iterator]=e[Symbol.iterator],f}default:return}})}var joe=Xe(()=>{a4();_k()});function u0(e){return Hoe(e)}var l4=Xe(()=>{joe()});function jk(e,t=Number.MAX_SAFE_INTEGER){switch(typeof e){case"number":return Number.isInteger(e)&&e>=0&&e{mJe=/^(?:0|[1-9]\d*)$/});function dB(e){return e!==null&&typeof e=="object"&&qg(e)==="[object Arguments]"}var u4=Xe(()=>{wk()});function gB(e,t){let r;if(Array.isArray(t)?r=t:typeof t=="string"&&ME(t)&&e?.[t]==null?r=Ou(t):r=[t],r.length===0)return!1;let s=e;for(let a=0;a{gk();c4();u4();_E()});function A4(e){return typeof e=="object"&&e!==null}var Goe=Xe(()=>{});function qoe(e){return typeof e=="symbol"||e instanceof Symbol}var Woe=Xe(()=>{});function Yoe(e,t){return Array.isArray(e)?!1:typeof e=="number"||typeof e=="boolean"||e==null||qoe(e)?!0:typeof e=="string"&&(EJe.test(e)||!yJe.test(e))||t!=null&&Object.hasOwn(t,e)}var yJe,EJe,Voe=Xe(()=>{Woe();yJe=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,EJe=/^\w*$/});function f0(e,t){if(e==null)return!0;switch(typeof t){case"symbol":case"number":case"object":{if(Array.isArray(t))return Joe(e,t);if(typeof t=="number"?t=UE(t):typeof t=="object"&&(Object.is(t?.valueOf(),-0)?t="-0":t=String(t)),xc(t))return!1;if(e?.[t]===void 0)return!0;try{return delete e[t],!0}catch{return!1}}case"string":{if(e?.[t]===void 0&&ME(t))return Joe(e,Ou(t));if(xc(t))return!1;try{return delete e[t],!0}catch{return!1}}}}function Joe(e,t){let r=Pa(e,t.slice(0,-1),e),s=t[t.length-1];if(r?.[s]===void 0)return!0;if(xc(s))return!1;try{return delete r[s],!0}catch{return!1}}var p4=Xe(()=>{yk();hB();gk();mk();_E()});function Koe(e){return e==null}var zoe=Xe(()=>{});var Xoe,Zoe=Xe(()=>{o4();Xoe=(e,t,r)=>{let s=e[t];(!(Object.hasOwn(e,t)&&Ik(s,r))||r===void 0&&!(t in e))&&(e[t]=r)}});function $oe(e,t,r,s){if(e==null&&!s4(e))return e;let a=Yoe(t,e)?[t]:Array.isArray(t)?t:typeof t=="string"?Ou(t):[t],n=e;for(let c=0;c{hB();Zoe();c4();Voe();mk();Toe();_E()});function Yg(e,t,r){return $oe(e,t,()=>r,()=>{})}var h4=Xe(()=>{eae()});function tae(e,t=0,r={}){typeof r!="object"&&(r={});let s=null,a=null,n=null,c=0,f=null,p,{leading:h=!1,trailing:E=!0,maxWait:C}=r,S="maxWait"in r,x=S?Math.max(Number(C)||0,t):0,I=ue=>(s!==null&&(p=e.apply(a,s)),s=a=null,c=ue,p),T=ue=>(c=ue,f=setTimeout(te,t),h&&s!==null?I(ue):p),O=ue=>(f=null,E&&s!==null?I(ue):p),U=ue=>{if(n===null)return!0;let ae=ue-n,ge=ae>=t||ae<0,Ae=S&&ue-c>=x;return ge||Ae},V=ue=>{let ae=n===null?0:ue-n,ge=t-ae,Ae=x-(ue-c);return S?Math.min(ge,Ae):ge},te=()=>{let ue=Date.now();if(U(ue))return O(ue);f=setTimeout(te,V(ue))},ie=function(...ue){let ae=Date.now(),ge=U(ae);if(s=ue,a=this,n=ae,ge){if(f===null)return T(ae);if(S)return clearTimeout(f),f=setTimeout(te,t),I(ae)}return f===null&&(f=setTimeout(te,t)),p};return ie.cancel=()=>{f!==null&&clearTimeout(f),c=0,n=s=a=f=null},ie.flush=()=>f===null?p:O(Date.now()),ie}var rae=Xe(()=>{});function d4(e,t=0,r={}){let{leading:s=!0,trailing:a=!0}=r;return tae(e,t,{leading:s,maxWait:t,trailing:a})}var nae=Xe(()=>{rae()});function g4(e){if(e==null)return"";if(typeof e=="string")return e;if(Array.isArray(e))return e.map(g4).join(",");let t=String(e);return t==="0"&&Object.is(Number(e),-0)?"-0":t}var iae=Xe(()=>{});function m4(e){if(!e||typeof e!="object")return!1;let t=Object.getPrototypeOf(e);return t===null||t===Object.prototype||Object.getPrototypeOf(t)===null?Object.prototype.toString.call(e)==="[object Object]":!1}var sae=Xe(()=>{});function oae(e,t,r){return mB(e,t,void 0,void 0,void 0,void 0,r)}function mB(e,t,r,s,a,n,c){let f=c(e,t,r,s,a,n);if(f!==void 0)return f;if(typeof e==typeof t)switch(typeof e){case"bigint":case"string":case"boolean":case"symbol":case"undefined":return e===t;case"number":return e===t||Object.is(e,t);case"function":return e===t;case"object":return yB(e,t,n,c)}return yB(e,t,n,c)}function yB(e,t,r,s){if(Object.is(e,t))return!0;let a=qg(e),n=qg(t);if(a===Wg&&(a=WE),n===Wg&&(n=WE),a!==n)return!1;switch(a){case jE:return e.toString()===t.toString();case GE:{let p=e.valueOf(),h=t.valueOf();return Ik(p,h)}case qE:case Sk:case vk:return Object.is(e.valueOf(),t.valueOf());case Bk:return e.source===t.source&&e.flags===t.flags;case Foe:return e===t}r=r??new Map;let c=r.get(e),f=r.get(t);if(c!=null&&f!=null)return c===t;r.set(e,t),r.set(t,e);try{switch(a){case Dk:{if(e.size!==t.size)return!1;for(let[p,h]of e.entries())if(!t.has(p)||!mB(h,t.get(p),p,e,t,r,s))return!1;return!0}case bk:{if(e.size!==t.size)return!1;let p=Array.from(e.values()),h=Array.from(t.values());for(let E=0;EmB(C,x,void 0,e,t,r,s));if(S===-1)return!1;h.splice(S,1)}return!0}case Pk:case Qk:case Rk:case Tk:case Fk:case Ooe:case Nk:case Ok:case Lk:case Loe:case Mk:case Uk:{if(typeof Buffer<"u"&&Buffer.isBuffer(e)!==Buffer.isBuffer(t)||e.length!==t.length)return!1;for(let p=0;p{sae();Ck();wk();_k();o4()});function lae(){}var cae=Xe(()=>{});function y4(e,t){return oae(e,t,lae)}var uae=Xe(()=>{aae();cae()});function fae(e){return YE(e)}var Aae=Xe(()=>{Hk()});function pae(e){if(typeof e!="object"||e==null)return!1;if(Object.getPrototypeOf(e)===null)return!0;if(Object.prototype.toString.call(e)!=="[object Object]"){let r=e[Symbol.toStringTag];return r==null||!Object.getOwnPropertyDescriptor(e,Symbol.toStringTag)?.writable?!1:e.toString()===`[object ${r}]`}let t=e;for(;Object.getPrototypeOf(t)!==null;)t=Object.getPrototypeOf(t);return Object.getPrototypeOf(e)===t}var hae=Xe(()=>{});function dae(e){if(HE(e))return e;if(Array.isArray(e)||YE(e)||e instanceof ArrayBuffer||typeof SharedArrayBuffer<"u"&&e instanceof SharedArrayBuffer)return e.slice(0);let t=Object.getPrototypeOf(e),r=t.constructor;if(e instanceof Date||e instanceof Map||e instanceof Set)return new r(e);if(e instanceof RegExp){let s=new r(e);return s.lastIndex=e.lastIndex,s}if(e instanceof DataView)return new r(e.buffer.slice(0));if(e instanceof Error){let s=new r(e.message);return s.stack=e.stack,s.name=e.name,s.cause=e.cause,s}if(typeof File<"u"&&e instanceof File)return new r([e],e.name,{type:e.type,lastModified:e.lastModified});if(typeof e=="object"){let s=Object.create(t);return Object.assign(s,e)}return e}var gae=Xe(()=>{Ek();Hk()});function E4(e,...t){let r=t.slice(0,-1),s=t[t.length-1],a=e;for(let n=0;n{l4();hB();gae();Ek();Ck();u4();Goe();hae();Aae()});function I4(e,...t){if(e==null)return{};let r=Uoe(e);for(let s=0;s{p4();_oe()});function Vg(e,...t){if(Koe(e))return{};let r={};for(let s=0;s{yk();f4();h4();Roe();zoe()});function Iae(e){return e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()}var Cae=Xe(()=>{});function EB(e){return Iae(g4(e))}var wae=Xe(()=>{Cae();iae()});var zl=Xe(()=>{nae();uae();l4();yk();f4();mae();yae();Eae();h4();p4();wae();_E()});var Ge={};Vt(Ge,{AsyncActions:()=>B4,BufferStream:()=>w4,CachingStrategy:()=>Rae,DefaultStream:()=>v4,allSettledSafe:()=>Lu,assertNever:()=>b4,bufferStream:()=>JE,buildIgnorePattern:()=>DJe,convertMapsToIndexableObjects:()=>Wk,dynamicRequire:()=>kp,escapeRegExp:()=>CJe,getArrayWithDefault:()=>CB,getFactoryWithDefault:()=>Zl,getMapWithDefault:()=>P4,getSetWithDefault:()=>xp,groupBy:()=>xJe,isIndexableObject:()=>C4,isPathLike:()=>bJe,isTaggedYarnVersion:()=>IJe,makeDeferred:()=>xae,mapAndFilter:()=>Xl,mapAndFind:()=>A0,mergeIntoTarget:()=>Fae,overrideType:()=>wJe,parseBoolean:()=>wB,parseDuration:()=>Vk,parseInt:()=>KE,parseOptionalBoolean:()=>Tae,plural:()=>qk,prettifyAsyncErrors:()=>VE,prettifySyncErrors:()=>x4,releaseAfterUseAsync:()=>vJe,replaceEnvVariables:()=>Yk,sortMap:()=>Vs,toMerged:()=>PJe,tryParseOptionalBoolean:()=>k4,validateEnum:()=>BJe});function IJe(e){return!!(Dae.default.valid(e)&&e.match(/^[^-]+(-rc\.[0-9]+)?$/))}function qk(e,{one:t,more:r,zero:s=r}){return e===0?s:e===1?t:r}function CJe(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function wJe(e){}function b4(e){throw new Error(`Assertion failed: Unexpected object '${e}'`)}function BJe(e,t){let r=Object.values(e);if(!r.includes(t))throw new it(`Invalid value for enumeration: ${JSON.stringify(t)} (expected one of ${r.map(s=>JSON.stringify(s)).join(", ")})`);return t}function Xl(e,t){let r=[];for(let s of e){let a=t(s);a!==bae&&r.push(a)}return r}function A0(e,t){for(let r of e){let s=t(r);if(s!==Pae)return s}}function C4(e){return typeof e=="object"&&e!==null}async function Lu(e){let t=await Promise.allSettled(e),r=[];for(let s of t){if(s.status==="rejected")throw s.reason;r.push(s.value)}return r}function Wk(e){if(e instanceof Map&&(e=Object.fromEntries(e)),C4(e))for(let t of Object.keys(e)){let r=e[t];C4(r)&&(e[t]=Wk(r))}return e}function Zl(e,t,r){let s=e.get(t);return typeof s>"u"&&e.set(t,s=r()),s}function CB(e,t){let r=e.get(t);return typeof r>"u"&&e.set(t,r=[]),r}function xp(e,t){let r=e.get(t);return typeof r>"u"&&e.set(t,r=new Set),r}function P4(e,t){let r=e.get(t);return typeof r>"u"&&e.set(t,r=new Map),r}async function vJe(e,t){if(t==null)return await e();try{return await e()}finally{await t()}}async function VE(e,t){try{return await e()}catch(r){throw r.message=t(r.message),r}}function x4(e,t){try{return e()}catch(r){throw r.message=t(r.message),r}}async function JE(e){return await new Promise((t,r)=>{let s=[];e.on("error",a=>{r(a)}),e.on("data",a=>{s.push(a)}),e.on("end",()=>{t(Buffer.concat(s))})})}function xae(){let e,t;return{promise:new Promise((s,a)=>{e=s,t=a}),resolve:e,reject:t}}function kae(e){return IB(fe.fromPortablePath(e))}function Qae(path){let physicalPath=fe.fromPortablePath(path),currentCacheEntry=IB.cache[physicalPath];delete IB.cache[physicalPath];let result;try{result=kae(physicalPath);let freshCacheEntry=IB.cache[physicalPath],dynamicModule=eval("module"),freshCacheIndex=dynamicModule.children.indexOf(freshCacheEntry);freshCacheIndex!==-1&&dynamicModule.children.splice(freshCacheIndex,1)}finally{IB.cache[physicalPath]=currentCacheEntry}return result}function SJe(e){let t=Bae.get(e),r=le.statSync(e);if(t?.mtime===r.mtimeMs)return t.instance;let s=Qae(e);return Bae.set(e,{mtime:r.mtimeMs,instance:s}),s}function kp(e,{cachingStrategy:t=2}={}){switch(t){case 0:return Qae(e);case 1:return SJe(e);case 2:return kae(e);default:throw new Error("Unsupported caching strategy")}}function Vs(e,t){let r=Array.from(e);Array.isArray(t)||(t=[t]);let s=[];for(let n of t)s.push(r.map(c=>n(c)));let a=r.map((n,c)=>c);return a.sort((n,c)=>{for(let f of s){let p=f[n]f[c]?1:0;if(p!==0)return p}return 0}),a.map(n=>r[n])}function DJe(e){return e.length===0?null:e.map(t=>`(${vae.default.makeRe(t,{windows:!1,dot:!0}).source})`).join("|")}function Yk(e,{env:t}){let r="",s=0,a=0,n=e.matchAll(/\\(?[\\$}])|\$\{(?[a-zA-Z]\w*)(?:-|-|(?=\}))|(?\$\{)|\}/g),c=()=>{let f=a;for(let{0:p,index:h,groups:{variable:E}={}}of n)if(E)a++;else if(p==="}"&&--a0)throw new it(`Incomplete variable substitution in input: ${e}`);return r+e.slice(s)}function wB(e){switch(e){case"true":case"1":case 1:case!0:return!0;case"false":case"0":case 0:case!1:return!1;default:throw new Error(`Couldn't parse "${e}" as a boolean`)}}function Tae(e){return typeof e>"u"?e:wB(e)}function k4(e){try{return Tae(e)}catch{return null}}function bJe(e){return!!(fe.isAbsolute(e)||e.match(/^(\.{1,2}|~)\//))}function Fae(e,...t){let r=c=>({value:c}),s=r(e),a=t.map(c=>r(c)),{value:n}=E4(s,...a,(c,f)=>{if(Array.isArray(c)&&Array.isArray(f)){for(let p of f)c.find(h=>y4(h,p))||c.push(p);return c}});return n}function PJe(...e){return Fae({},...e)}function xJe(e,t){let r=Object.create(null);for(let s of e){let a=s[t];r[a]??=[],r[a].push(s)}return r}function KE(e){return typeof e=="string"?Number.parseInt(e,10):e}function Vk(e,t){let r=kJe.exec(e)?.groups;if(!r)throw new Error(`Couldn't parse "${e}" as a duration`);if(r.unit===void 0)return parseFloat(r.num);let s=S4[r.unit];if(!s)throw new Error(`Invalid duration unit "${r.unit}"`);return parseFloat(r.num)*s/S4[t]}var vae,Sae,Dae,D4,bae,Pae,w4,B4,v4,IB,Bae,Rae,S4,kJe,kc=Xe(()=>{Dt();Yt();zl();vae=et(zo()),Sae=et(Ng()),Dae=et(pi()),D4=Ie("stream");bae=Symbol();Xl.skip=bae;Pae=Symbol();A0.skip=Pae;w4=class extends D4.Transform{constructor(){super(...arguments);this.chunks=[]}_transform(r,s,a){if(s!=="buffer"||!Buffer.isBuffer(r))throw new Error("Assertion failed: BufferStream only accept buffers");this.chunks.push(r),a(null,null)}_flush(r){r(null,Buffer.concat(this.chunks))}};B4=class{constructor(t){this.deferred=new Map;this.promises=new Map;this.limit=(0,Sae.default)(t)}set(t,r){let s=this.deferred.get(t);typeof s>"u"&&this.deferred.set(t,s=xae());let a=this.limit(()=>r());return this.promises.set(t,a),a.then(()=>{this.promises.get(t)===a&&s.resolve()},n=>{this.promises.get(t)===a&&s.reject(n)}),s.promise}reduce(t,r){let s=this.promises.get(t)??Promise.resolve();this.set(t,()=>r(s))}async wait(){await Promise.all(this.promises.values())}},v4=class extends D4.Transform{constructor(r=Buffer.alloc(0)){super();this.active=!0;this.ifEmpty=r}_transform(r,s,a){if(s!=="buffer"||!Buffer.isBuffer(r))throw new Error("Assertion failed: DefaultStream only accept buffers");this.active=!1,a(null,r)}_flush(r){this.active&&this.ifEmpty.length>0?r(null,this.ifEmpty):r(null)}},IB=eval("require");Bae=new Map;Rae=(s=>(s[s.NoCache=0]="NoCache",s[s.FsTime=1]="FsTime",s[s.Node=2]="Node",s))(Rae||{});S4={ms:1,s:1e3,m:60*1e3,h:60*60*1e3,d:24*60*60*1e3,w:7*24*60*60*1e3},kJe=new RegExp(`^(?\\d*\\.?\\d+)(?${Object.keys(S4).join("|")})?$`)});var zE,Q4,R4,Nae=Xe(()=>{zE=(r=>(r.HARD="HARD",r.SOFT="SOFT",r))(zE||{}),Q4=(s=>(s.Dependency="Dependency",s.PeerDependency="PeerDependency",s.PeerDependencyMeta="PeerDependencyMeta",s))(Q4||{}),R4=(s=>(s.Inactive="inactive",s.Redundant="redundant",s.Active="active",s))(R4||{})});var pe={};Vt(pe,{LogLevel:()=>$k,Style:()=>zk,Type:()=>dt,addLogFilterSupport:()=>SB,applyColor:()=>si,applyHyperlink:()=>ZE,applyStyle:()=>Jg,json:()=>Kg,jsonOrPretty:()=>TJe,mark:()=>L4,pretty:()=>jt,prettyField:()=>Zf,prettyList:()=>O4,prettyTruncatedLocatorList:()=>Zk,stripAnsi:()=>XE.default,supportsColor:()=>Xk,supportsHyperlinks:()=>N4,tuple:()=>Mu});function Oae(e){let t=["KiB","MiB","GiB","TiB"],r=t.length;for(;r>1&&e<1024**r;)r-=1;let s=1024**r;return`${Math.floor(e*100/s)/100} ${t[r-1]}`}function Jk(e,t){if(Array.isArray(t))return t.length===0?si(e,"[]",dt.CODE):si(e,"[ ",dt.CODE)+t.map(r=>Jk(e,r)).join(", ")+si(e," ]",dt.CODE);if(typeof t=="string")return si(e,JSON.stringify(t),dt.STRING);if(typeof t=="number")return si(e,JSON.stringify(t),dt.NUMBER);if(typeof t=="boolean")return si(e,JSON.stringify(t),dt.BOOLEAN);if(t===null)return si(e,"null",dt.NULL);if(typeof t=="object"&&Object.getPrototypeOf(t)===Object.prototype){let r=Object.entries(t);return r.length===0?si(e,"{}",dt.CODE):si(e,"{ ",dt.CODE)+r.map(([s,a])=>`${Jk(e,s)}: ${Jk(e,a)}`).join(", ")+si(e," }",dt.CODE)}if(typeof t>"u")return si(e,"undefined",dt.NULL);throw new Error("Assertion failed: The value doesn't seem to be a valid JSON object")}function Mu(e,t){return[t,e]}function Jg(e,t,r){return e.get("enableColors")&&r&2&&(t=vB.default.bold(t)),t}function si(e,t,r){if(!e.get("enableColors"))return t;let s=QJe.get(r);if(s===null)return t;let a=typeof s>"u"?r:F4.level>=3?s[0]:s[1],n=typeof a=="number"?T4.ansi256(a):a.startsWith("#")?T4.hex(a):T4[a];if(typeof n!="function")throw new Error(`Invalid format type ${a}`);return n(t)}function ZE(e,t,r){return e.get("enableHyperlinks")?RJe?`\x1B]8;;${r}\x1B\\${t}\x1B]8;;\x1B\\`:`\x1B]8;;${r}\x07${t}\x1B]8;;\x07`:t}function jt(e,t,r){if(t===null)return si(e,"null",dt.NULL);if(Object.hasOwn(Kk,r))return Kk[r].pretty(e,t);if(typeof t!="string")throw new Error(`Assertion failed: Expected the value to be a string, got ${typeof t}`);return si(e,t,r)}function O4(e,t,r,{separator:s=", "}={}){return[...t].map(a=>jt(e,a,r)).join(s)}function Kg(e,t){if(e===null)return null;if(Object.hasOwn(Kk,t))return Kk[t].json(e);if(typeof e!="string")throw new Error(`Assertion failed: Expected the value to be a string, got ${typeof e}`);return e}function TJe(e,t,[r,s]){return e?Kg(r,s):jt(t,r,s)}function L4(e){return{Check:si(e,"\u2713","green"),Cross:si(e,"\u2718","red"),Question:si(e,"?","cyan")}}function Zf(e,{label:t,value:[r,s]}){return`${jt(e,t,dt.CODE)}: ${jt(e,r,s)}`}function Zk(e,t,r){let s=[],a=[...t],n=r;for(;a.length>0;){let h=a[0],E=`${Yr(e,h)}, `,C=M4(h).length+2;if(s.length>0&&nh).join("").slice(0,-2);let c="X".repeat(a.length.toString().length),f=`and ${c} more.`,p=a.length;for(;s.length>1&&nh).join(""),f.replace(c,jt(e,p,dt.NUMBER))].join("")}function SB(e,{configuration:t}){let r=t.get("logFilters"),s=new Map,a=new Map,n=[];for(let C of r){let S=C.get("level");if(typeof S>"u")continue;let x=C.get("code");typeof x<"u"&&s.set(x,S);let I=C.get("text");typeof I<"u"&&a.set(I,S);let T=C.get("pattern");typeof T<"u"&&n.push([Lae.default.matcher(T,{contains:!0}),S])}n.reverse();let c=(C,S,x)=>{if(C===null||C===0)return x;let I=a.size>0||n.length>0?(0,XE.default)(S):S;if(a.size>0){let T=a.get(I);if(typeof T<"u")return T??x}if(n.length>0){for(let[T,O]of n)if(T(I))return O??x}if(s.size>0){let T=s.get(Kf(C));if(typeof T<"u")return T??x}return x},f=e.reportInfo,p=e.reportWarning,h=e.reportError,E=function(C,S,x,I){switch(c(S,x,I)){case"info":f.call(C,S,x);break;case"warning":p.call(C,S??0,x);break;case"error":h.call(C,S??0,x);break}};e.reportInfo=function(...C){return E(this,...C,"info")},e.reportWarning=function(...C){return E(this,...C,"warning")},e.reportError=function(...C){return E(this,...C,"error")}}var vB,BB,Lae,XE,dt,zk,F4,Xk,N4,T4,QJe,Xo,Kk,RJe,$k,Qc=Xe(()=>{Dt();vB=et(NE()),BB=et(Rg());Yt();Lae=et(zo()),XE=et(dk());Gx();Zo();dt={NO_HINT:"NO_HINT",ID:"ID",NULL:"NULL",SCOPE:"SCOPE",NAME:"NAME",RANGE:"RANGE",REFERENCE:"REFERENCE",NUMBER:"NUMBER",STRING:"STRING",BOOLEAN:"BOOLEAN",PATH:"PATH",URL:"URL",ADDED:"ADDED",REMOVED:"REMOVED",CODE:"CODE",INSPECT:"INSPECT",DURATION:"DURATION",SIZE:"SIZE",SIZE_DIFF:"SIZE_DIFF",IDENT:"IDENT",DESCRIPTOR:"DESCRIPTOR",LOCATOR:"LOCATOR",RESOLUTION:"RESOLUTION",DEPENDENT:"DEPENDENT",PACKAGE_EXTENSION:"PACKAGE_EXTENSION",SETTING:"SETTING",MARKDOWN:"MARKDOWN",MARKDOWN_INLINE:"MARKDOWN_INLINE"},zk=(t=>(t[t.BOLD=2]="BOLD",t))(zk||{}),F4=BB.default.GITHUB_ACTIONS?{level:2}:vB.default.supportsColor?{level:vB.default.supportsColor.level}:{level:0},Xk=F4.level!==0,N4=Xk&&!BB.default.GITHUB_ACTIONS&&!BB.default.CIRCLE&&!BB.default.GITLAB,T4=new vB.default.Instance(F4),QJe=new Map([[dt.NO_HINT,null],[dt.NULL,["#a853b5",129]],[dt.SCOPE,["#d75f00",166]],[dt.NAME,["#d7875f",173]],[dt.RANGE,["#00afaf",37]],[dt.REFERENCE,["#87afff",111]],[dt.NUMBER,["#ffd700",220]],[dt.STRING,["#b4bd68",32]],[dt.BOOLEAN,["#faa023",209]],[dt.PATH,["#d75fd7",170]],[dt.URL,["#d75fd7",170]],[dt.ADDED,["#5faf00",70]],[dt.REMOVED,["#ff3131",160]],[dt.CODE,["#87afff",111]],[dt.SIZE,["#ffd700",220]]]),Xo=e=>e;Kk={[dt.ID]:Xo({pretty:(e,t)=>typeof t=="number"?si(e,`${t}`,dt.NUMBER):si(e,t,dt.CODE),json:e=>e}),[dt.INSPECT]:Xo({pretty:(e,t)=>Jk(e,t),json:e=>e}),[dt.NUMBER]:Xo({pretty:(e,t)=>si(e,`${t}`,dt.NUMBER),json:e=>e}),[dt.IDENT]:Xo({pretty:(e,t)=>$i(e,t),json:e=>fn(e)}),[dt.LOCATOR]:Xo({pretty:(e,t)=>Yr(e,t),json:e=>ml(e)}),[dt.DESCRIPTOR]:Xo({pretty:(e,t)=>oi(e,t),json:e=>gl(e)}),[dt.RESOLUTION]:Xo({pretty:(e,{descriptor:t,locator:r})=>DB(e,t,r),json:({descriptor:e,locator:t})=>({descriptor:gl(e),locator:t!==null?ml(t):null})}),[dt.DEPENDENT]:Xo({pretty:(e,{locator:t,descriptor:r})=>U4(e,t,r),json:({locator:e,descriptor:t})=>({locator:ml(e),descriptor:gl(t)})}),[dt.PACKAGE_EXTENSION]:Xo({pretty:(e,t)=>{switch(t.type){case"Dependency":return`${$i(e,t.parentDescriptor)} \u27A4 ${si(e,"dependencies",dt.CODE)} \u27A4 ${$i(e,t.descriptor)}`;case"PeerDependency":return`${$i(e,t.parentDescriptor)} \u27A4 ${si(e,"peerDependencies",dt.CODE)} \u27A4 ${$i(e,t.descriptor)}`;case"PeerDependencyMeta":return`${$i(e,t.parentDescriptor)} \u27A4 ${si(e,"peerDependenciesMeta",dt.CODE)} \u27A4 ${$i(e,xa(t.selector))} \u27A4 ${si(e,t.key,dt.CODE)}`;default:throw new Error(`Assertion failed: Unsupported package extension type: ${t.type}`)}},json:e=>{switch(e.type){case"Dependency":return`${fn(e.parentDescriptor)} > ${fn(e.descriptor)}`;case"PeerDependency":return`${fn(e.parentDescriptor)} >> ${fn(e.descriptor)}`;case"PeerDependencyMeta":return`${fn(e.parentDescriptor)} >> ${e.selector} / ${e.key}`;default:throw new Error(`Assertion failed: Unsupported package extension type: ${e.type}`)}}}),[dt.SETTING]:Xo({pretty:(e,t)=>(e.get(t),ZE(e,si(e,t,dt.CODE),`https://yarnpkg.com/configuration/yarnrc#${t}`)),json:e=>e}),[dt.DURATION]:Xo({pretty:(e,t)=>{if(t>1e3*60){let r=Math.floor(t/1e3/60),s=Math.ceil((t-r*60*1e3)/1e3);return s===0?`${r}m`:`${r}m ${s}s`}else{let r=Math.floor(t/1e3),s=t-r*1e3;return s===0?`${r}s`:`${r}s ${s}ms`}},json:e=>e}),[dt.SIZE]:Xo({pretty:(e,t)=>si(e,Oae(t),dt.NUMBER),json:e=>e}),[dt.SIZE_DIFF]:Xo({pretty:(e,t)=>{let r=t>=0?"+":"-",s=r==="+"?dt.REMOVED:dt.ADDED;return si(e,`${r} ${Oae(Math.max(Math.abs(t),1))}`,s)},json:e=>e}),[dt.PATH]:Xo({pretty:(e,t)=>si(e,fe.fromPortablePath(t),dt.PATH),json:e=>fe.fromPortablePath(e)}),[dt.MARKDOWN]:Xo({pretty:(e,{text:t,format:r,paragraphs:s})=>Vo(t,{format:r,paragraphs:s}),json:({text:e})=>e}),[dt.MARKDOWN_INLINE]:Xo({pretty:(e,t)=>(t=t.replace(/(`+)((?:.|[\n])*?)\1/g,(r,s,a)=>jt(e,s+a+s,dt.CODE)),t=t.replace(/(\*\*)((?:.|[\n])*?)\1/g,(r,s,a)=>Jg(e,a,2)),t),json:e=>e})};RJe=!!process.env.KONSOLE_VERSION;$k=(a=>(a.Error="error",a.Warning="warning",a.Info="info",a.Discard="discard",a))($k||{})});var Mae=G($E=>{"use strict";Object.defineProperty($E,"__esModule",{value:!0});$E.splitWhen=$E.flatten=void 0;function FJe(e){return e.reduce((t,r)=>[].concat(t,r),[])}$E.flatten=FJe;function NJe(e,t){let r=[[]],s=0;for(let a of e)t(a)?(s++,r[s]=[]):r[s].push(a);return r}$E.splitWhen=NJe});var Uae=G(eQ=>{"use strict";Object.defineProperty(eQ,"__esModule",{value:!0});eQ.isEnoentCodeError=void 0;function OJe(e){return e.code==="ENOENT"}eQ.isEnoentCodeError=OJe});var _ae=G(tQ=>{"use strict";Object.defineProperty(tQ,"__esModule",{value:!0});tQ.createDirentFromStats=void 0;var _4=class{constructor(t,r){this.name=t,this.isBlockDevice=r.isBlockDevice.bind(r),this.isCharacterDevice=r.isCharacterDevice.bind(r),this.isDirectory=r.isDirectory.bind(r),this.isFIFO=r.isFIFO.bind(r),this.isFile=r.isFile.bind(r),this.isSocket=r.isSocket.bind(r),this.isSymbolicLink=r.isSymbolicLink.bind(r)}};function LJe(e,t){return new _4(e,t)}tQ.createDirentFromStats=LJe});var qae=G(us=>{"use strict";Object.defineProperty(us,"__esModule",{value:!0});us.convertPosixPathToPattern=us.convertWindowsPathToPattern=us.convertPathToPattern=us.escapePosixPath=us.escapeWindowsPath=us.escape=us.removeLeadingDotSegment=us.makeAbsolute=us.unixify=void 0;var MJe=Ie("os"),UJe=Ie("path"),Hae=MJe.platform()==="win32",_Je=2,HJe=/(\\?)([()*?[\]{|}]|^!|[!+@](?=\()|\\(?![!()*+?@[\]{|}]))/g,jJe=/(\\?)([()[\]{}]|^!|[!+@](?=\())/g,GJe=/^\\\\([.?])/,qJe=/\\(?![!()+@[\]{}])/g;function WJe(e){return e.replace(/\\/g,"/")}us.unixify=WJe;function YJe(e,t){return UJe.resolve(e,t)}us.makeAbsolute=YJe;function VJe(e){if(e.charAt(0)==="."){let t=e.charAt(1);if(t==="/"||t==="\\")return e.slice(_Je)}return e}us.removeLeadingDotSegment=VJe;us.escape=Hae?H4:j4;function H4(e){return e.replace(jJe,"\\$2")}us.escapeWindowsPath=H4;function j4(e){return e.replace(HJe,"\\$2")}us.escapePosixPath=j4;us.convertPathToPattern=Hae?jae:Gae;function jae(e){return H4(e).replace(GJe,"//$1").replace(qJe,"/")}us.convertWindowsPathToPattern=jae;function Gae(e){return j4(e)}us.convertPosixPathToPattern=Gae});var Yae=G((uFt,Wae)=>{Wae.exports=function(t){if(typeof t!="string"||t==="")return!1;for(var r;r=/(\\).|([@?!+*]\(.*\))/g.exec(t);){if(r[2])return!0;t=t.slice(r.index+r[0].length)}return!1}});var Kae=G((fFt,Jae)=>{var JJe=Yae(),Vae={"{":"}","(":")","[":"]"},KJe=function(e){if(e[0]==="!")return!0;for(var t=0,r=-2,s=-2,a=-2,n=-2,c=-2;tt&&(c===-1||c>s||(c=e.indexOf("\\",t),c===-1||c>s)))||a!==-1&&e[t]==="{"&&e[t+1]!=="}"&&(a=e.indexOf("}",t),a>t&&(c=e.indexOf("\\",t),c===-1||c>a))||n!==-1&&e[t]==="("&&e[t+1]==="?"&&/[:!=]/.test(e[t+2])&&e[t+3]!==")"&&(n=e.indexOf(")",t),n>t&&(c=e.indexOf("\\",t),c===-1||c>n))||r!==-1&&e[t]==="("&&e[t+1]!=="|"&&(rr&&(c=e.indexOf("\\",r),c===-1||c>n))))return!0;if(e[t]==="\\"){var f=e[t+1];t+=2;var p=Vae[f];if(p){var h=e.indexOf(p,t);h!==-1&&(t=h+1)}if(e[t]==="!")return!0}else t++}return!1},zJe=function(e){if(e[0]==="!")return!0;for(var t=0;t{"use strict";var XJe=Kae(),ZJe=Ie("path").posix.dirname,$Je=Ie("os").platform()==="win32",G4="/",eKe=/\\/g,tKe=/[\{\[].*[\}\]]$/,rKe=/(^|[^\\])([\{\[]|\([^\)]+$)/,nKe=/\\([\!\*\?\|\[\]\(\)\{\}])/g;zae.exports=function(t,r){var s=Object.assign({flipBackslashes:!0},r);s.flipBackslashes&&$Je&&t.indexOf(G4)<0&&(t=t.replace(eKe,G4)),tKe.test(t)&&(t+=G4),t+="a";do t=ZJe(t);while(XJe(t)||rKe.test(t));return t.replace(nKe,"$1")}});var sle=G(jr=>{"use strict";Object.defineProperty(jr,"__esModule",{value:!0});jr.removeDuplicateSlashes=jr.matchAny=jr.convertPatternsToRe=jr.makeRe=jr.getPatternParts=jr.expandBraceExpansion=jr.expandPatternsWithBraceExpansion=jr.isAffectDepthOfReadingPattern=jr.endsWithSlashGlobStar=jr.hasGlobStar=jr.getBaseDirectory=jr.isPatternRelatedToParentDirectory=jr.getPatternsOutsideCurrentDirectory=jr.getPatternsInsideCurrentDirectory=jr.getPositivePatterns=jr.getNegativePatterns=jr.isPositivePattern=jr.isNegativePattern=jr.convertToNegativePattern=jr.convertToPositivePattern=jr.isDynamicPattern=jr.isStaticPattern=void 0;var iKe=Ie("path"),sKe=Xae(),q4=zo(),Zae="**",oKe="\\",aKe=/[*?]|^!/,lKe=/\[[^[]*]/,cKe=/(?:^|[^!*+?@])\([^(]*\|[^|]*\)/,uKe=/[!*+?@]\([^(]*\)/,fKe=/,|\.\./,AKe=/(?!^)\/{2,}/g;function $ae(e,t={}){return!ele(e,t)}jr.isStaticPattern=$ae;function ele(e,t={}){return e===""?!1:!!(t.caseSensitiveMatch===!1||e.includes(oKe)||aKe.test(e)||lKe.test(e)||cKe.test(e)||t.extglob!==!1&&uKe.test(e)||t.braceExpansion!==!1&&pKe(e))}jr.isDynamicPattern=ele;function pKe(e){let t=e.indexOf("{");if(t===-1)return!1;let r=e.indexOf("}",t+1);if(r===-1)return!1;let s=e.slice(t,r);return fKe.test(s)}function hKe(e){return rQ(e)?e.slice(1):e}jr.convertToPositivePattern=hKe;function dKe(e){return"!"+e}jr.convertToNegativePattern=dKe;function rQ(e){return e.startsWith("!")&&e[1]!=="("}jr.isNegativePattern=rQ;function tle(e){return!rQ(e)}jr.isPositivePattern=tle;function gKe(e){return e.filter(rQ)}jr.getNegativePatterns=gKe;function mKe(e){return e.filter(tle)}jr.getPositivePatterns=mKe;function yKe(e){return e.filter(t=>!W4(t))}jr.getPatternsInsideCurrentDirectory=yKe;function EKe(e){return e.filter(W4)}jr.getPatternsOutsideCurrentDirectory=EKe;function W4(e){return e.startsWith("..")||e.startsWith("./..")}jr.isPatternRelatedToParentDirectory=W4;function IKe(e){return sKe(e,{flipBackslashes:!1})}jr.getBaseDirectory=IKe;function CKe(e){return e.includes(Zae)}jr.hasGlobStar=CKe;function rle(e){return e.endsWith("/"+Zae)}jr.endsWithSlashGlobStar=rle;function wKe(e){let t=iKe.basename(e);return rle(e)||$ae(t)}jr.isAffectDepthOfReadingPattern=wKe;function BKe(e){return e.reduce((t,r)=>t.concat(nle(r)),[])}jr.expandPatternsWithBraceExpansion=BKe;function nle(e){let t=q4.braces(e,{expand:!0,nodupes:!0,keepEscaping:!0});return t.sort((r,s)=>r.length-s.length),t.filter(r=>r!=="")}jr.expandBraceExpansion=nle;function vKe(e,t){let{parts:r}=q4.scan(e,Object.assign(Object.assign({},t),{parts:!0}));return r.length===0&&(r=[e]),r[0].startsWith("/")&&(r[0]=r[0].slice(1),r.unshift("")),r}jr.getPatternParts=vKe;function ile(e,t){return q4.makeRe(e,t)}jr.makeRe=ile;function SKe(e,t){return e.map(r=>ile(r,t))}jr.convertPatternsToRe=SKe;function DKe(e,t){return t.some(r=>r.test(e))}jr.matchAny=DKe;function bKe(e){return e.replace(AKe,"/")}jr.removeDuplicateSlashes=bKe});var cle=G((hFt,lle)=>{"use strict";var PKe=Ie("stream"),ole=PKe.PassThrough,xKe=Array.prototype.slice;lle.exports=kKe;function kKe(){let e=[],t=xKe.call(arguments),r=!1,s=t[t.length-1];s&&!Array.isArray(s)&&s.pipe==null?t.pop():s={};let a=s.end!==!1,n=s.pipeError===!0;s.objectMode==null&&(s.objectMode=!0),s.highWaterMark==null&&(s.highWaterMark=64*1024);let c=ole(s);function f(){for(let E=0,C=arguments.length;E0||(r=!1,p())}function x(I){function T(){I.removeListener("merge2UnpipeEnd",T),I.removeListener("end",T),n&&I.removeListener("error",O),S()}function O(U){c.emit("error",U)}if(I._readableState.endEmitted)return S();I.on("merge2UnpipeEnd",T),I.on("end",T),n&&I.on("error",O),I.pipe(c,{end:!1}),I.resume()}for(let I=0;I{"use strict";Object.defineProperty(nQ,"__esModule",{value:!0});nQ.merge=void 0;var QKe=cle();function RKe(e){let t=QKe(e);return e.forEach(r=>{r.once("error",s=>t.emit("error",s))}),t.once("close",()=>ule(e)),t.once("end",()=>ule(e)),t}nQ.merge=RKe;function ule(e){e.forEach(t=>t.emit("close"))}});var Ale=G(eI=>{"use strict";Object.defineProperty(eI,"__esModule",{value:!0});eI.isEmpty=eI.isString=void 0;function TKe(e){return typeof e=="string"}eI.isString=TKe;function FKe(e){return e===""}eI.isEmpty=FKe});var Qp=G($o=>{"use strict";Object.defineProperty($o,"__esModule",{value:!0});$o.string=$o.stream=$o.pattern=$o.path=$o.fs=$o.errno=$o.array=void 0;var NKe=Mae();$o.array=NKe;var OKe=Uae();$o.errno=OKe;var LKe=_ae();$o.fs=LKe;var MKe=qae();$o.path=MKe;var UKe=sle();$o.pattern=UKe;var _Ke=fle();$o.stream=_Ke;var HKe=Ale();$o.string=HKe});var gle=G(ea=>{"use strict";Object.defineProperty(ea,"__esModule",{value:!0});ea.convertPatternGroupToTask=ea.convertPatternGroupsToTasks=ea.groupPatternsByBaseDirectory=ea.getNegativePatternsAsPositive=ea.getPositivePatterns=ea.convertPatternsToTasks=ea.generate=void 0;var Uu=Qp();function jKe(e,t){let r=ple(e,t),s=ple(t.ignore,t),a=hle(r),n=dle(r,s),c=a.filter(E=>Uu.pattern.isStaticPattern(E,t)),f=a.filter(E=>Uu.pattern.isDynamicPattern(E,t)),p=Y4(c,n,!1),h=Y4(f,n,!0);return p.concat(h)}ea.generate=jKe;function ple(e,t){let r=e;return t.braceExpansion&&(r=Uu.pattern.expandPatternsWithBraceExpansion(r)),t.baseNameMatch&&(r=r.map(s=>s.includes("/")?s:`**/${s}`)),r.map(s=>Uu.pattern.removeDuplicateSlashes(s))}function Y4(e,t,r){let s=[],a=Uu.pattern.getPatternsOutsideCurrentDirectory(e),n=Uu.pattern.getPatternsInsideCurrentDirectory(e),c=V4(a),f=V4(n);return s.push(...J4(c,t,r)),"."in f?s.push(K4(".",n,t,r)):s.push(...J4(f,t,r)),s}ea.convertPatternsToTasks=Y4;function hle(e){return Uu.pattern.getPositivePatterns(e)}ea.getPositivePatterns=hle;function dle(e,t){return Uu.pattern.getNegativePatterns(e).concat(t).map(Uu.pattern.convertToPositivePattern)}ea.getNegativePatternsAsPositive=dle;function V4(e){let t={};return e.reduce((r,s)=>{let a=Uu.pattern.getBaseDirectory(s);return a in r?r[a].push(s):r[a]=[s],r},t)}ea.groupPatternsByBaseDirectory=V4;function J4(e,t,r){return Object.keys(e).map(s=>K4(s,e[s],t,r))}ea.convertPatternGroupsToTasks=J4;function K4(e,t,r,s){return{dynamic:s,positive:t,negative:r,base:e,patterns:[].concat(t,r.map(Uu.pattern.convertToNegativePattern))}}ea.convertPatternGroupToTask=K4});var yle=G(iQ=>{"use strict";Object.defineProperty(iQ,"__esModule",{value:!0});iQ.read=void 0;function GKe(e,t,r){t.fs.lstat(e,(s,a)=>{if(s!==null){mle(r,s);return}if(!a.isSymbolicLink()||!t.followSymbolicLink){z4(r,a);return}t.fs.stat(e,(n,c)=>{if(n!==null){if(t.throwErrorOnBrokenSymbolicLink){mle(r,n);return}z4(r,a);return}t.markSymbolicLink&&(c.isSymbolicLink=()=>!0),z4(r,c)})})}iQ.read=GKe;function mle(e,t){e(t)}function z4(e,t){e(null,t)}});var Ele=G(sQ=>{"use strict";Object.defineProperty(sQ,"__esModule",{value:!0});sQ.read=void 0;function qKe(e,t){let r=t.fs.lstatSync(e);if(!r.isSymbolicLink()||!t.followSymbolicLink)return r;try{let s=t.fs.statSync(e);return t.markSymbolicLink&&(s.isSymbolicLink=()=>!0),s}catch(s){if(!t.throwErrorOnBrokenSymbolicLink)return r;throw s}}sQ.read=qKe});var Ile=G(p0=>{"use strict";Object.defineProperty(p0,"__esModule",{value:!0});p0.createFileSystemAdapter=p0.FILE_SYSTEM_ADAPTER=void 0;var oQ=Ie("fs");p0.FILE_SYSTEM_ADAPTER={lstat:oQ.lstat,stat:oQ.stat,lstatSync:oQ.lstatSync,statSync:oQ.statSync};function WKe(e){return e===void 0?p0.FILE_SYSTEM_ADAPTER:Object.assign(Object.assign({},p0.FILE_SYSTEM_ADAPTER),e)}p0.createFileSystemAdapter=WKe});var Cle=G(Z4=>{"use strict";Object.defineProperty(Z4,"__esModule",{value:!0});var YKe=Ile(),X4=class{constructor(t={}){this._options=t,this.followSymbolicLink=this._getValue(this._options.followSymbolicLink,!0),this.fs=YKe.createFileSystemAdapter(this._options.fs),this.markSymbolicLink=this._getValue(this._options.markSymbolicLink,!1),this.throwErrorOnBrokenSymbolicLink=this._getValue(this._options.throwErrorOnBrokenSymbolicLink,!0)}_getValue(t,r){return t??r}};Z4.default=X4});var zg=G(h0=>{"use strict";Object.defineProperty(h0,"__esModule",{value:!0});h0.statSync=h0.stat=h0.Settings=void 0;var wle=yle(),VKe=Ele(),$4=Cle();h0.Settings=$4.default;function JKe(e,t,r){if(typeof t=="function"){wle.read(e,e3(),t);return}wle.read(e,e3(t),r)}h0.stat=JKe;function KKe(e,t){let r=e3(t);return VKe.read(e,r)}h0.statSync=KKe;function e3(e={}){return e instanceof $4.default?e:new $4.default(e)}});var Sle=G((vFt,vle)=>{var Ble;vle.exports=typeof queueMicrotask=="function"?queueMicrotask.bind(typeof window<"u"?window:global):e=>(Ble||(Ble=Promise.resolve())).then(e).catch(t=>setTimeout(()=>{throw t},0))});var ble=G((SFt,Dle)=>{Dle.exports=XKe;var zKe=Sle();function XKe(e,t){let r,s,a,n=!0;Array.isArray(e)?(r=[],s=e.length):(a=Object.keys(e),r={},s=a.length);function c(p){function h(){t&&t(p,r),t=null}n?zKe(h):h()}function f(p,h,E){r[p]=E,(--s===0||h)&&c(h)}s?a?a.forEach(function(p){e[p](function(h,E){f(p,h,E)})}):e.forEach(function(p,h){p(function(E,C){f(h,E,C)})}):c(null),n=!1}});var t3=G(lQ=>{"use strict";Object.defineProperty(lQ,"__esModule",{value:!0});lQ.IS_SUPPORT_READDIR_WITH_FILE_TYPES=void 0;var aQ=process.versions.node.split(".");if(aQ[0]===void 0||aQ[1]===void 0)throw new Error(`Unexpected behavior. The 'process.versions.node' variable has invalid value: ${process.versions.node}`);var Ple=Number.parseInt(aQ[0],10),ZKe=Number.parseInt(aQ[1],10),xle=10,$Ke=10,eze=Ple>xle,tze=Ple===xle&&ZKe>=$Ke;lQ.IS_SUPPORT_READDIR_WITH_FILE_TYPES=eze||tze});var kle=G(cQ=>{"use strict";Object.defineProperty(cQ,"__esModule",{value:!0});cQ.createDirentFromStats=void 0;var r3=class{constructor(t,r){this.name=t,this.isBlockDevice=r.isBlockDevice.bind(r),this.isCharacterDevice=r.isCharacterDevice.bind(r),this.isDirectory=r.isDirectory.bind(r),this.isFIFO=r.isFIFO.bind(r),this.isFile=r.isFile.bind(r),this.isSocket=r.isSocket.bind(r),this.isSymbolicLink=r.isSymbolicLink.bind(r)}};function rze(e,t){return new r3(e,t)}cQ.createDirentFromStats=rze});var n3=G(uQ=>{"use strict";Object.defineProperty(uQ,"__esModule",{value:!0});uQ.fs=void 0;var nze=kle();uQ.fs=nze});var i3=G(fQ=>{"use strict";Object.defineProperty(fQ,"__esModule",{value:!0});fQ.joinPathSegments=void 0;function ize(e,t,r){return e.endsWith(r)?e+t:e+r+t}fQ.joinPathSegments=ize});var Ole=G(d0=>{"use strict";Object.defineProperty(d0,"__esModule",{value:!0});d0.readdir=d0.readdirWithFileTypes=d0.read=void 0;var sze=zg(),Qle=ble(),oze=t3(),Rle=n3(),Tle=i3();function aze(e,t,r){if(!t.stats&&oze.IS_SUPPORT_READDIR_WITH_FILE_TYPES){Fle(e,t,r);return}Nle(e,t,r)}d0.read=aze;function Fle(e,t,r){t.fs.readdir(e,{withFileTypes:!0},(s,a)=>{if(s!==null){AQ(r,s);return}let n=a.map(f=>({dirent:f,name:f.name,path:Tle.joinPathSegments(e,f.name,t.pathSegmentSeparator)}));if(!t.followSymbolicLinks){s3(r,n);return}let c=n.map(f=>lze(f,t));Qle(c,(f,p)=>{if(f!==null){AQ(r,f);return}s3(r,p)})})}d0.readdirWithFileTypes=Fle;function lze(e,t){return r=>{if(!e.dirent.isSymbolicLink()){r(null,e);return}t.fs.stat(e.path,(s,a)=>{if(s!==null){if(t.throwErrorOnBrokenSymbolicLink){r(s);return}r(null,e);return}e.dirent=Rle.fs.createDirentFromStats(e.name,a),r(null,e)})}}function Nle(e,t,r){t.fs.readdir(e,(s,a)=>{if(s!==null){AQ(r,s);return}let n=a.map(c=>{let f=Tle.joinPathSegments(e,c,t.pathSegmentSeparator);return p=>{sze.stat(f,t.fsStatSettings,(h,E)=>{if(h!==null){p(h);return}let C={name:c,path:f,dirent:Rle.fs.createDirentFromStats(c,E)};t.stats&&(C.stats=E),p(null,C)})}});Qle(n,(c,f)=>{if(c!==null){AQ(r,c);return}s3(r,f)})})}d0.readdir=Nle;function AQ(e,t){e(t)}function s3(e,t){e(null,t)}});var Hle=G(g0=>{"use strict";Object.defineProperty(g0,"__esModule",{value:!0});g0.readdir=g0.readdirWithFileTypes=g0.read=void 0;var cze=zg(),uze=t3(),Lle=n3(),Mle=i3();function fze(e,t){return!t.stats&&uze.IS_SUPPORT_READDIR_WITH_FILE_TYPES?Ule(e,t):_le(e,t)}g0.read=fze;function Ule(e,t){return t.fs.readdirSync(e,{withFileTypes:!0}).map(s=>{let a={dirent:s,name:s.name,path:Mle.joinPathSegments(e,s.name,t.pathSegmentSeparator)};if(a.dirent.isSymbolicLink()&&t.followSymbolicLinks)try{let n=t.fs.statSync(a.path);a.dirent=Lle.fs.createDirentFromStats(a.name,n)}catch(n){if(t.throwErrorOnBrokenSymbolicLink)throw n}return a})}g0.readdirWithFileTypes=Ule;function _le(e,t){return t.fs.readdirSync(e).map(s=>{let a=Mle.joinPathSegments(e,s,t.pathSegmentSeparator),n=cze.statSync(a,t.fsStatSettings),c={name:s,path:a,dirent:Lle.fs.createDirentFromStats(s,n)};return t.stats&&(c.stats=n),c})}g0.readdir=_le});var jle=G(m0=>{"use strict";Object.defineProperty(m0,"__esModule",{value:!0});m0.createFileSystemAdapter=m0.FILE_SYSTEM_ADAPTER=void 0;var tI=Ie("fs");m0.FILE_SYSTEM_ADAPTER={lstat:tI.lstat,stat:tI.stat,lstatSync:tI.lstatSync,statSync:tI.statSync,readdir:tI.readdir,readdirSync:tI.readdirSync};function Aze(e){return e===void 0?m0.FILE_SYSTEM_ADAPTER:Object.assign(Object.assign({},m0.FILE_SYSTEM_ADAPTER),e)}m0.createFileSystemAdapter=Aze});var Gle=G(a3=>{"use strict";Object.defineProperty(a3,"__esModule",{value:!0});var pze=Ie("path"),hze=zg(),dze=jle(),o3=class{constructor(t={}){this._options=t,this.followSymbolicLinks=this._getValue(this._options.followSymbolicLinks,!1),this.fs=dze.createFileSystemAdapter(this._options.fs),this.pathSegmentSeparator=this._getValue(this._options.pathSegmentSeparator,pze.sep),this.stats=this._getValue(this._options.stats,!1),this.throwErrorOnBrokenSymbolicLink=this._getValue(this._options.throwErrorOnBrokenSymbolicLink,!0),this.fsStatSettings=new hze.Settings({followSymbolicLink:this.followSymbolicLinks,fs:this.fs,throwErrorOnBrokenSymbolicLink:this.throwErrorOnBrokenSymbolicLink})}_getValue(t,r){return t??r}};a3.default=o3});var pQ=G(y0=>{"use strict";Object.defineProperty(y0,"__esModule",{value:!0});y0.Settings=y0.scandirSync=y0.scandir=void 0;var qle=Ole(),gze=Hle(),l3=Gle();y0.Settings=l3.default;function mze(e,t,r){if(typeof t=="function"){qle.read(e,c3(),t);return}qle.read(e,c3(t),r)}y0.scandir=mze;function yze(e,t){let r=c3(t);return gze.read(e,r)}y0.scandirSync=yze;function c3(e={}){return e instanceof l3.default?e:new l3.default(e)}});var Yle=G((NFt,Wle)=>{"use strict";function Eze(e){var t=new e,r=t;function s(){var n=t;return n.next?t=n.next:(t=new e,r=t),n.next=null,n}function a(n){r.next=n,r=n}return{get:s,release:a}}Wle.exports=Eze});var Jle=G((OFt,u3)=>{"use strict";var Ize=Yle();function Vle(e,t,r){if(typeof e=="function"&&(r=t,t=e,e=null),!(r>=1))throw new Error("fastqueue concurrency must be equal to or greater than 1");var s=Ize(Cze),a=null,n=null,c=0,f=null,p={push:T,drain:Rc,saturated:Rc,pause:E,paused:!1,get concurrency(){return r},set concurrency(ue){if(!(ue>=1))throw new Error("fastqueue concurrency must be equal to or greater than 1");if(r=ue,!p.paused)for(;a&&c=r||p.paused?n?(n.next=ge,n=ge):(a=ge,n=ge,p.saturated()):(c++,t.call(e,ge.value,ge.worked))}function O(ue,ae){var ge=s.get();ge.context=e,ge.release=U,ge.value=ue,ge.callback=ae||Rc,ge.errorHandler=f,c>=r||p.paused?a?(ge.next=a,a=ge):(a=ge,n=ge,p.saturated()):(c++,t.call(e,ge.value,ge.worked))}function U(ue){ue&&s.release(ue);var ae=a;ae&&c<=r?p.paused?c--:(n===a&&(n=null),a=ae.next,ae.next=null,t.call(e,ae.value,ae.worked),n===null&&p.empty()):--c===0&&p.drain()}function V(){a=null,n=null,p.drain=Rc}function te(){a=null,n=null,p.drain(),p.drain=Rc}function ie(ue){f=ue}}function Rc(){}function Cze(){this.value=null,this.callback=Rc,this.next=null,this.release=Rc,this.context=null,this.errorHandler=null;var e=this;this.worked=function(r,s){var a=e.callback,n=e.errorHandler,c=e.value;e.value=null,e.callback=Rc,e.errorHandler&&n(r,c),a.call(e.context,r,s),e.release(e)}}function wze(e,t,r){typeof e=="function"&&(r=t,t=e,e=null);function s(E,C){t.call(this,E).then(function(S){C(null,S)},C)}var a=Vle(e,s,r),n=a.push,c=a.unshift;return a.push=f,a.unshift=p,a.drained=h,a;function f(E){var C=new Promise(function(S,x){n(E,function(I,T){if(I){x(I);return}S(T)})});return C.catch(Rc),C}function p(E){var C=new Promise(function(S,x){c(E,function(I,T){if(I){x(I);return}S(T)})});return C.catch(Rc),C}function h(){if(a.idle())return new Promise(function(S){S()});var E=a.drain,C=new Promise(function(S){a.drain=function(){E(),S()}});return C}}u3.exports=Vle;u3.exports.promise=wze});var hQ=G($f=>{"use strict";Object.defineProperty($f,"__esModule",{value:!0});$f.joinPathSegments=$f.replacePathSegmentSeparator=$f.isAppliedFilter=$f.isFatalError=void 0;function Bze(e,t){return e.errorFilter===null?!0:!e.errorFilter(t)}$f.isFatalError=Bze;function vze(e,t){return e===null||e(t)}$f.isAppliedFilter=vze;function Sze(e,t){return e.split(/[/\\]/).join(t)}$f.replacePathSegmentSeparator=Sze;function Dze(e,t,r){return e===""?t:e.endsWith(r)?e+t:e+r+t}$f.joinPathSegments=Dze});var p3=G(A3=>{"use strict";Object.defineProperty(A3,"__esModule",{value:!0});var bze=hQ(),f3=class{constructor(t,r){this._root=t,this._settings=r,this._root=bze.replacePathSegmentSeparator(t,r.pathSegmentSeparator)}};A3.default=f3});var g3=G(d3=>{"use strict";Object.defineProperty(d3,"__esModule",{value:!0});var Pze=Ie("events"),xze=pQ(),kze=Jle(),dQ=hQ(),Qze=p3(),h3=class extends Qze.default{constructor(t,r){super(t,r),this._settings=r,this._scandir=xze.scandir,this._emitter=new Pze.EventEmitter,this._queue=kze(this._worker.bind(this),this._settings.concurrency),this._isFatalError=!1,this._isDestroyed=!1,this._queue.drain=()=>{this._isFatalError||this._emitter.emit("end")}}read(){return this._isFatalError=!1,this._isDestroyed=!1,setImmediate(()=>{this._pushToQueue(this._root,this._settings.basePath)}),this._emitter}get isDestroyed(){return this._isDestroyed}destroy(){if(this._isDestroyed)throw new Error("The reader is already destroyed");this._isDestroyed=!0,this._queue.killAndDrain()}onEntry(t){this._emitter.on("entry",t)}onError(t){this._emitter.once("error",t)}onEnd(t){this._emitter.once("end",t)}_pushToQueue(t,r){let s={directory:t,base:r};this._queue.push(s,a=>{a!==null&&this._handleError(a)})}_worker(t,r){this._scandir(t.directory,this._settings.fsScandirSettings,(s,a)=>{if(s!==null){r(s,void 0);return}for(let n of a)this._handleEntry(n,t.base);r(null,void 0)})}_handleError(t){this._isDestroyed||!dQ.isFatalError(this._settings,t)||(this._isFatalError=!0,this._isDestroyed=!0,this._emitter.emit("error",t))}_handleEntry(t,r){if(this._isDestroyed||this._isFatalError)return;let s=t.path;r!==void 0&&(t.path=dQ.joinPathSegments(r,t.name,this._settings.pathSegmentSeparator)),dQ.isAppliedFilter(this._settings.entryFilter,t)&&this._emitEntry(t),t.dirent.isDirectory()&&dQ.isAppliedFilter(this._settings.deepFilter,t)&&this._pushToQueue(s,r===void 0?void 0:t.path)}_emitEntry(t){this._emitter.emit("entry",t)}};d3.default=h3});var Kle=G(y3=>{"use strict";Object.defineProperty(y3,"__esModule",{value:!0});var Rze=g3(),m3=class{constructor(t,r){this._root=t,this._settings=r,this._reader=new Rze.default(this._root,this._settings),this._storage=[]}read(t){this._reader.onError(r=>{Tze(t,r)}),this._reader.onEntry(r=>{this._storage.push(r)}),this._reader.onEnd(()=>{Fze(t,this._storage)}),this._reader.read()}};y3.default=m3;function Tze(e,t){e(t)}function Fze(e,t){e(null,t)}});var zle=G(I3=>{"use strict";Object.defineProperty(I3,"__esModule",{value:!0});var Nze=Ie("stream"),Oze=g3(),E3=class{constructor(t,r){this._root=t,this._settings=r,this._reader=new Oze.default(this._root,this._settings),this._stream=new Nze.Readable({objectMode:!0,read:()=>{},destroy:()=>{this._reader.isDestroyed||this._reader.destroy()}})}read(){return this._reader.onError(t=>{this._stream.emit("error",t)}),this._reader.onEntry(t=>{this._stream.push(t)}),this._reader.onEnd(()=>{this._stream.push(null)}),this._reader.read(),this._stream}};I3.default=E3});var Xle=G(w3=>{"use strict";Object.defineProperty(w3,"__esModule",{value:!0});var Lze=pQ(),gQ=hQ(),Mze=p3(),C3=class extends Mze.default{constructor(){super(...arguments),this._scandir=Lze.scandirSync,this._storage=[],this._queue=new Set}read(){return this._pushToQueue(this._root,this._settings.basePath),this._handleQueue(),this._storage}_pushToQueue(t,r){this._queue.add({directory:t,base:r})}_handleQueue(){for(let t of this._queue.values())this._handleDirectory(t.directory,t.base)}_handleDirectory(t,r){try{let s=this._scandir(t,this._settings.fsScandirSettings);for(let a of s)this._handleEntry(a,r)}catch(s){this._handleError(s)}}_handleError(t){if(gQ.isFatalError(this._settings,t))throw t}_handleEntry(t,r){let s=t.path;r!==void 0&&(t.path=gQ.joinPathSegments(r,t.name,this._settings.pathSegmentSeparator)),gQ.isAppliedFilter(this._settings.entryFilter,t)&&this._pushToStorage(t),t.dirent.isDirectory()&&gQ.isAppliedFilter(this._settings.deepFilter,t)&&this._pushToQueue(s,r===void 0?void 0:t.path)}_pushToStorage(t){this._storage.push(t)}};w3.default=C3});var Zle=G(v3=>{"use strict";Object.defineProperty(v3,"__esModule",{value:!0});var Uze=Xle(),B3=class{constructor(t,r){this._root=t,this._settings=r,this._reader=new Uze.default(this._root,this._settings)}read(){return this._reader.read()}};v3.default=B3});var $le=G(D3=>{"use strict";Object.defineProperty(D3,"__esModule",{value:!0});var _ze=Ie("path"),Hze=pQ(),S3=class{constructor(t={}){this._options=t,this.basePath=this._getValue(this._options.basePath,void 0),this.concurrency=this._getValue(this._options.concurrency,Number.POSITIVE_INFINITY),this.deepFilter=this._getValue(this._options.deepFilter,null),this.entryFilter=this._getValue(this._options.entryFilter,null),this.errorFilter=this._getValue(this._options.errorFilter,null),this.pathSegmentSeparator=this._getValue(this._options.pathSegmentSeparator,_ze.sep),this.fsScandirSettings=new Hze.Settings({followSymbolicLinks:this._options.followSymbolicLinks,fs:this._options.fs,pathSegmentSeparator:this._options.pathSegmentSeparator,stats:this._options.stats,throwErrorOnBrokenSymbolicLink:this._options.throwErrorOnBrokenSymbolicLink})}_getValue(t,r){return t??r}};D3.default=S3});var yQ=G(eA=>{"use strict";Object.defineProperty(eA,"__esModule",{value:!0});eA.Settings=eA.walkStream=eA.walkSync=eA.walk=void 0;var ece=Kle(),jze=zle(),Gze=Zle(),b3=$le();eA.Settings=b3.default;function qze(e,t,r){if(typeof t=="function"){new ece.default(e,mQ()).read(t);return}new ece.default(e,mQ(t)).read(r)}eA.walk=qze;function Wze(e,t){let r=mQ(t);return new Gze.default(e,r).read()}eA.walkSync=Wze;function Yze(e,t){let r=mQ(t);return new jze.default(e,r).read()}eA.walkStream=Yze;function mQ(e={}){return e instanceof b3.default?e:new b3.default(e)}});var EQ=G(x3=>{"use strict";Object.defineProperty(x3,"__esModule",{value:!0});var Vze=Ie("path"),Jze=zg(),tce=Qp(),P3=class{constructor(t){this._settings=t,this._fsStatSettings=new Jze.Settings({followSymbolicLink:this._settings.followSymbolicLinks,fs:this._settings.fs,throwErrorOnBrokenSymbolicLink:this._settings.followSymbolicLinks})}_getFullEntryPath(t){return Vze.resolve(this._settings.cwd,t)}_makeEntry(t,r){let s={name:r,path:r,dirent:tce.fs.createDirentFromStats(r,t)};return this._settings.stats&&(s.stats=t),s}_isFatalError(t){return!tce.errno.isEnoentCodeError(t)&&!this._settings.suppressErrors}};x3.default=P3});var R3=G(Q3=>{"use strict";Object.defineProperty(Q3,"__esModule",{value:!0});var Kze=Ie("stream"),zze=zg(),Xze=yQ(),Zze=EQ(),k3=class extends Zze.default{constructor(){super(...arguments),this._walkStream=Xze.walkStream,this._stat=zze.stat}dynamic(t,r){return this._walkStream(t,r)}static(t,r){let s=t.map(this._getFullEntryPath,this),a=new Kze.PassThrough({objectMode:!0});a._write=(n,c,f)=>this._getEntry(s[n],t[n],r).then(p=>{p!==null&&r.entryFilter(p)&&a.push(p),n===s.length-1&&a.end(),f()}).catch(f);for(let n=0;nthis._makeEntry(a,r)).catch(a=>{if(s.errorFilter(a))return null;throw a})}_getStat(t){return new Promise((r,s)=>{this._stat(t,this._fsStatSettings,(a,n)=>a===null?r(n):s(a))})}};Q3.default=k3});var rce=G(F3=>{"use strict";Object.defineProperty(F3,"__esModule",{value:!0});var $ze=yQ(),eXe=EQ(),tXe=R3(),T3=class extends eXe.default{constructor(){super(...arguments),this._walkAsync=$ze.walk,this._readerStream=new tXe.default(this._settings)}dynamic(t,r){return new Promise((s,a)=>{this._walkAsync(t,r,(n,c)=>{n===null?s(c):a(n)})})}async static(t,r){let s=[],a=this._readerStream.static(t,r);return new Promise((n,c)=>{a.once("error",c),a.on("data",f=>s.push(f)),a.once("end",()=>n(s))})}};F3.default=T3});var nce=G(O3=>{"use strict";Object.defineProperty(O3,"__esModule",{value:!0});var bB=Qp(),N3=class{constructor(t,r,s){this._patterns=t,this._settings=r,this._micromatchOptions=s,this._storage=[],this._fillStorage()}_fillStorage(){for(let t of this._patterns){let r=this._getPatternSegments(t),s=this._splitSegmentsIntoSections(r);this._storage.push({complete:s.length<=1,pattern:t,segments:r,sections:s})}}_getPatternSegments(t){return bB.pattern.getPatternParts(t,this._micromatchOptions).map(s=>bB.pattern.isDynamicPattern(s,this._settings)?{dynamic:!0,pattern:s,patternRe:bB.pattern.makeRe(s,this._micromatchOptions)}:{dynamic:!1,pattern:s})}_splitSegmentsIntoSections(t){return bB.array.splitWhen(t,r=>r.dynamic&&bB.pattern.hasGlobStar(r.pattern))}};O3.default=N3});var ice=G(M3=>{"use strict";Object.defineProperty(M3,"__esModule",{value:!0});var rXe=nce(),L3=class extends rXe.default{match(t){let r=t.split("/"),s=r.length,a=this._storage.filter(n=>!n.complete||n.segments.length>s);for(let n of a){let c=n.sections[0];if(!n.complete&&s>c.length||r.every((p,h)=>{let E=n.segments[h];return!!(E.dynamic&&E.patternRe.test(p)||!E.dynamic&&E.pattern===p)}))return!0}return!1}};M3.default=L3});var sce=G(_3=>{"use strict";Object.defineProperty(_3,"__esModule",{value:!0});var IQ=Qp(),nXe=ice(),U3=class{constructor(t,r){this._settings=t,this._micromatchOptions=r}getFilter(t,r,s){let a=this._getMatcher(r),n=this._getNegativePatternsRe(s);return c=>this._filter(t,c,a,n)}_getMatcher(t){return new nXe.default(t,this._settings,this._micromatchOptions)}_getNegativePatternsRe(t){let r=t.filter(IQ.pattern.isAffectDepthOfReadingPattern);return IQ.pattern.convertPatternsToRe(r,this._micromatchOptions)}_filter(t,r,s,a){if(this._isSkippedByDeep(t,r.path)||this._isSkippedSymbolicLink(r))return!1;let n=IQ.path.removeLeadingDotSegment(r.path);return this._isSkippedByPositivePatterns(n,s)?!1:this._isSkippedByNegativePatterns(n,a)}_isSkippedByDeep(t,r){return this._settings.deep===1/0?!1:this._getEntryLevel(t,r)>=this._settings.deep}_getEntryLevel(t,r){let s=r.split("/").length;if(t==="")return s;let a=t.split("/").length;return s-a}_isSkippedSymbolicLink(t){return!this._settings.followSymbolicLinks&&t.dirent.isSymbolicLink()}_isSkippedByPositivePatterns(t,r){return!this._settings.baseNameMatch&&!r.match(t)}_isSkippedByNegativePatterns(t,r){return!IQ.pattern.matchAny(t,r)}};_3.default=U3});var oce=G(j3=>{"use strict";Object.defineProperty(j3,"__esModule",{value:!0});var Xg=Qp(),H3=class{constructor(t,r){this._settings=t,this._micromatchOptions=r,this.index=new Map}getFilter(t,r){let s=Xg.pattern.convertPatternsToRe(t,this._micromatchOptions),a=Xg.pattern.convertPatternsToRe(r,Object.assign(Object.assign({},this._micromatchOptions),{dot:!0}));return n=>this._filter(n,s,a)}_filter(t,r,s){let a=Xg.path.removeLeadingDotSegment(t.path);if(this._settings.unique&&this._isDuplicateEntry(a)||this._onlyFileFilter(t)||this._onlyDirectoryFilter(t)||this._isSkippedByAbsoluteNegativePatterns(a,s))return!1;let n=t.dirent.isDirectory(),c=this._isMatchToPatterns(a,r,n)&&!this._isMatchToPatterns(a,s,n);return this._settings.unique&&c&&this._createIndexRecord(a),c}_isDuplicateEntry(t){return this.index.has(t)}_createIndexRecord(t){this.index.set(t,void 0)}_onlyFileFilter(t){return this._settings.onlyFiles&&!t.dirent.isFile()}_onlyDirectoryFilter(t){return this._settings.onlyDirectories&&!t.dirent.isDirectory()}_isSkippedByAbsoluteNegativePatterns(t,r){if(!this._settings.absolute)return!1;let s=Xg.path.makeAbsolute(this._settings.cwd,t);return Xg.pattern.matchAny(s,r)}_isMatchToPatterns(t,r,s){let a=Xg.pattern.matchAny(t,r);return!a&&s?Xg.pattern.matchAny(t+"/",r):a}};j3.default=H3});var ace=G(q3=>{"use strict";Object.defineProperty(q3,"__esModule",{value:!0});var iXe=Qp(),G3=class{constructor(t){this._settings=t}getFilter(){return t=>this._isNonFatalError(t)}_isNonFatalError(t){return iXe.errno.isEnoentCodeError(t)||this._settings.suppressErrors}};q3.default=G3});var cce=G(Y3=>{"use strict";Object.defineProperty(Y3,"__esModule",{value:!0});var lce=Qp(),W3=class{constructor(t){this._settings=t}getTransformer(){return t=>this._transform(t)}_transform(t){let r=t.path;return this._settings.absolute&&(r=lce.path.makeAbsolute(this._settings.cwd,r),r=lce.path.unixify(r)),this._settings.markDirectories&&t.dirent.isDirectory()&&(r+="/"),this._settings.objectMode?Object.assign(Object.assign({},t),{path:r}):r}};Y3.default=W3});var CQ=G(J3=>{"use strict";Object.defineProperty(J3,"__esModule",{value:!0});var sXe=Ie("path"),oXe=sce(),aXe=oce(),lXe=ace(),cXe=cce(),V3=class{constructor(t){this._settings=t,this.errorFilter=new lXe.default(this._settings),this.entryFilter=new aXe.default(this._settings,this._getMicromatchOptions()),this.deepFilter=new oXe.default(this._settings,this._getMicromatchOptions()),this.entryTransformer=new cXe.default(this._settings)}_getRootDirectory(t){return sXe.resolve(this._settings.cwd,t.base)}_getReaderOptions(t){let r=t.base==="."?"":t.base;return{basePath:r,pathSegmentSeparator:"/",concurrency:this._settings.concurrency,deepFilter:this.deepFilter.getFilter(r,t.positive,t.negative),entryFilter:this.entryFilter.getFilter(t.positive,t.negative),errorFilter:this.errorFilter.getFilter(),followSymbolicLinks:this._settings.followSymbolicLinks,fs:this._settings.fs,stats:this._settings.stats,throwErrorOnBrokenSymbolicLink:this._settings.throwErrorOnBrokenSymbolicLink,transform:this.entryTransformer.getTransformer()}}_getMicromatchOptions(){return{dot:this._settings.dot,matchBase:this._settings.baseNameMatch,nobrace:!this._settings.braceExpansion,nocase:!this._settings.caseSensitiveMatch,noext:!this._settings.extglob,noglobstar:!this._settings.globstar,posix:!0,strictSlashes:!1}}};J3.default=V3});var uce=G(z3=>{"use strict";Object.defineProperty(z3,"__esModule",{value:!0});var uXe=rce(),fXe=CQ(),K3=class extends fXe.default{constructor(){super(...arguments),this._reader=new uXe.default(this._settings)}async read(t){let r=this._getRootDirectory(t),s=this._getReaderOptions(t);return(await this.api(r,t,s)).map(n=>s.transform(n))}api(t,r,s){return r.dynamic?this._reader.dynamic(t,s):this._reader.static(r.patterns,s)}};z3.default=K3});var fce=G(Z3=>{"use strict";Object.defineProperty(Z3,"__esModule",{value:!0});var AXe=Ie("stream"),pXe=R3(),hXe=CQ(),X3=class extends hXe.default{constructor(){super(...arguments),this._reader=new pXe.default(this._settings)}read(t){let r=this._getRootDirectory(t),s=this._getReaderOptions(t),a=this.api(r,t,s),n=new AXe.Readable({objectMode:!0,read:()=>{}});return a.once("error",c=>n.emit("error",c)).on("data",c=>n.emit("data",s.transform(c))).once("end",()=>n.emit("end")),n.once("close",()=>a.destroy()),n}api(t,r,s){return r.dynamic?this._reader.dynamic(t,s):this._reader.static(r.patterns,s)}};Z3.default=X3});var Ace=G(e8=>{"use strict";Object.defineProperty(e8,"__esModule",{value:!0});var dXe=zg(),gXe=yQ(),mXe=EQ(),$3=class extends mXe.default{constructor(){super(...arguments),this._walkSync=gXe.walkSync,this._statSync=dXe.statSync}dynamic(t,r){return this._walkSync(t,r)}static(t,r){let s=[];for(let a of t){let n=this._getFullEntryPath(a),c=this._getEntry(n,a,r);c===null||!r.entryFilter(c)||s.push(c)}return s}_getEntry(t,r,s){try{let a=this._getStat(t);return this._makeEntry(a,r)}catch(a){if(s.errorFilter(a))return null;throw a}}_getStat(t){return this._statSync(t,this._fsStatSettings)}};e8.default=$3});var pce=G(r8=>{"use strict";Object.defineProperty(r8,"__esModule",{value:!0});var yXe=Ace(),EXe=CQ(),t8=class extends EXe.default{constructor(){super(...arguments),this._reader=new yXe.default(this._settings)}read(t){let r=this._getRootDirectory(t),s=this._getReaderOptions(t);return this.api(r,t,s).map(s.transform)}api(t,r,s){return r.dynamic?this._reader.dynamic(t,s):this._reader.static(r.patterns,s)}};r8.default=t8});var hce=G(nI=>{"use strict";Object.defineProperty(nI,"__esModule",{value:!0});nI.DEFAULT_FILE_SYSTEM_ADAPTER=void 0;var rI=Ie("fs"),IXe=Ie("os"),CXe=Math.max(IXe.cpus().length,1);nI.DEFAULT_FILE_SYSTEM_ADAPTER={lstat:rI.lstat,lstatSync:rI.lstatSync,stat:rI.stat,statSync:rI.statSync,readdir:rI.readdir,readdirSync:rI.readdirSync};var n8=class{constructor(t={}){this._options=t,this.absolute=this._getValue(this._options.absolute,!1),this.baseNameMatch=this._getValue(this._options.baseNameMatch,!1),this.braceExpansion=this._getValue(this._options.braceExpansion,!0),this.caseSensitiveMatch=this._getValue(this._options.caseSensitiveMatch,!0),this.concurrency=this._getValue(this._options.concurrency,CXe),this.cwd=this._getValue(this._options.cwd,process.cwd()),this.deep=this._getValue(this._options.deep,1/0),this.dot=this._getValue(this._options.dot,!1),this.extglob=this._getValue(this._options.extglob,!0),this.followSymbolicLinks=this._getValue(this._options.followSymbolicLinks,!0),this.fs=this._getFileSystemMethods(this._options.fs),this.globstar=this._getValue(this._options.globstar,!0),this.ignore=this._getValue(this._options.ignore,[]),this.markDirectories=this._getValue(this._options.markDirectories,!1),this.objectMode=this._getValue(this._options.objectMode,!1),this.onlyDirectories=this._getValue(this._options.onlyDirectories,!1),this.onlyFiles=this._getValue(this._options.onlyFiles,!0),this.stats=this._getValue(this._options.stats,!1),this.suppressErrors=this._getValue(this._options.suppressErrors,!1),this.throwErrorOnBrokenSymbolicLink=this._getValue(this._options.throwErrorOnBrokenSymbolicLink,!1),this.unique=this._getValue(this._options.unique,!0),this.onlyDirectories&&(this.onlyFiles=!1),this.stats&&(this.objectMode=!0),this.ignore=[].concat(this.ignore)}_getValue(t,r){return t===void 0?r:t}_getFileSystemMethods(t={}){return Object.assign(Object.assign({},nI.DEFAULT_FILE_SYSTEM_ADAPTER),t)}};nI.default=n8});var wQ=G((aNt,gce)=>{"use strict";var dce=gle(),wXe=uce(),BXe=fce(),vXe=pce(),i8=hce(),Tc=Qp();async function s8(e,t){_u(e);let r=o8(e,wXe.default,t),s=await Promise.all(r);return Tc.array.flatten(s)}(function(e){e.glob=e,e.globSync=t,e.globStream=r,e.async=e;function t(h,E){_u(h);let C=o8(h,vXe.default,E);return Tc.array.flatten(C)}e.sync=t;function r(h,E){_u(h);let C=o8(h,BXe.default,E);return Tc.stream.merge(C)}e.stream=r;function s(h,E){_u(h);let C=[].concat(h),S=new i8.default(E);return dce.generate(C,S)}e.generateTasks=s;function a(h,E){_u(h);let C=new i8.default(E);return Tc.pattern.isDynamicPattern(h,C)}e.isDynamicPattern=a;function n(h){return _u(h),Tc.path.escape(h)}e.escapePath=n;function c(h){return _u(h),Tc.path.convertPathToPattern(h)}e.convertPathToPattern=c;let f;(function(h){function E(S){return _u(S),Tc.path.escapePosixPath(S)}h.escapePath=E;function C(S){return _u(S),Tc.path.convertPosixPathToPattern(S)}h.convertPathToPattern=C})(f=e.posix||(e.posix={}));let p;(function(h){function E(S){return _u(S),Tc.path.escapeWindowsPath(S)}h.escapePath=E;function C(S){return _u(S),Tc.path.convertWindowsPathToPattern(S)}h.convertPathToPattern=C})(p=e.win32||(e.win32={}))})(s8||(s8={}));function o8(e,t,r){let s=[].concat(e),a=new i8.default(r),n=dce.generate(s,a),c=new t(a);return n.map(c.read,c)}function _u(e){if(![].concat(e).every(s=>Tc.string.isString(s)&&!Tc.string.isEmpty(s)))throw new TypeError("Patterns must be a string (non empty) or an array of strings")}gce.exports=s8});var Ln={};Vt(Ln,{checksumFile:()=>vQ,checksumPattern:()=>SQ,makeHash:()=>fs});function fs(...e){let t=(0,BQ.createHash)("sha512"),r="";for(let s of e)typeof s=="string"?r+=s:s&&(r&&(t.update(r),r=""),t.update(s));return r&&t.update(r),t.digest("hex")}async function vQ(e,{baseFs:t,algorithm:r}={baseFs:le,algorithm:"sha512"}){let s=await t.openPromise(e,"r");try{let n=Buffer.allocUnsafeSlow(65536),c=(0,BQ.createHash)(r),f=0;for(;(f=await t.readPromise(s,n,0,65536))!==0;)c.update(f===65536?n:n.slice(0,f));return c.digest("hex")}finally{await t.closePromise(s)}}async function SQ(e,{cwd:t}){let s=(await(0,a8.default)(e,{cwd:fe.fromPortablePath(t),onlyDirectories:!0})).map(f=>`${f}/**/*`),a=await(0,a8.default)([e,...s],{cwd:fe.fromPortablePath(t),onlyFiles:!1});a.sort();let n=await Promise.all(a.map(async f=>{let p=[Buffer.from(f)],h=J.join(t,fe.toPortablePath(f)),E=await le.lstatPromise(h);return E.isSymbolicLink()?p.push(Buffer.from(await le.readlinkPromise(h))):E.isFile()&&p.push(await le.readFilePromise(h)),p.join("\0")})),c=(0,BQ.createHash)("sha512");for(let f of n)c.update(f);return c.digest("hex")}var BQ,a8,E0=Xe(()=>{Dt();BQ=Ie("crypto"),a8=et(wQ())});var j={};Vt(j,{allPeerRequests:()=>OB,areDescriptorsEqual:()=>Ice,areIdentsEqual:()=>QB,areLocatorsEqual:()=>RB,areVirtualPackagesEquivalent:()=>TXe,bindDescriptor:()=>QXe,bindLocator:()=>RXe,convertDescriptorToLocator:()=>DQ,convertLocatorToDescriptor:()=>f8,convertPackageToLocator:()=>PXe,convertToIdent:()=>bXe,convertToManifestRange:()=>GXe,copyPackage:()=>xB,devirtualizeDescriptor:()=>kB,devirtualizeLocator:()=>sI,ensureDevirtualizedDescriptor:()=>xXe,ensureDevirtualizedLocator:()=>kXe,getIdentVendorPath:()=>d8,isPackageCompatible:()=>QQ,isPackageInRange:()=>JXe,isVirtualDescriptor:()=>Rp,isVirtualLocator:()=>Hu,makeDescriptor:()=>Mn,makeIdent:()=>ka,makeLocator:()=>Js,makeRange:()=>xQ,parseDescriptor:()=>I0,parseFileStyleRange:()=>HXe,parseIdent:()=>xa,parseLocator:()=>Tp,parseRange:()=>Zg,prettyDependent:()=>U4,prettyDescriptor:()=>oi,prettyIdent:()=>$i,prettyLocator:()=>Yr,prettyLocatorNoColors:()=>M4,prettyRange:()=>aI,prettyReference:()=>FB,prettyResolution:()=>DB,prettyWorkspace:()=>NB,renamePackage:()=>A8,slugifyIdent:()=>c8,slugifyLocator:()=>oI,sortDescriptors:()=>lI,stringifyDescriptor:()=>gl,stringifyIdent:()=>fn,stringifyLocator:()=>ml,tryParseDescriptor:()=>TB,tryParseIdent:()=>Cce,tryParseLocator:()=>PQ,tryParseRange:()=>_Xe,unwrapIdentFromScope:()=>WXe,virtualizeDescriptor:()=>p8,virtualizePackage:()=>h8,wrapIdentIntoScope:()=>qXe});function ka(e,t){if(e?.startsWith("@"))throw new Error("Invalid scope: don't prefix it with '@'");return{identHash:fs(e,t),scope:e,name:t}}function Mn(e,t){return{identHash:e.identHash,scope:e.scope,name:e.name,descriptorHash:fs(e.identHash,t),range:t}}function Js(e,t){return{identHash:e.identHash,scope:e.scope,name:e.name,locatorHash:fs(e.identHash,t),reference:t}}function bXe(e){return{identHash:e.identHash,scope:e.scope,name:e.name}}function DQ(e){return{identHash:e.identHash,scope:e.scope,name:e.name,locatorHash:e.descriptorHash,reference:e.range}}function f8(e){return{identHash:e.identHash,scope:e.scope,name:e.name,descriptorHash:e.locatorHash,range:e.reference}}function PXe(e){return{identHash:e.identHash,scope:e.scope,name:e.name,locatorHash:e.locatorHash,reference:e.reference}}function A8(e,t){return{identHash:t.identHash,scope:t.scope,name:t.name,locatorHash:t.locatorHash,reference:t.reference,version:e.version,languageName:e.languageName,linkType:e.linkType,conditions:e.conditions,dependencies:new Map(e.dependencies),peerDependencies:new Map(e.peerDependencies),dependenciesMeta:new Map(e.dependenciesMeta),peerDependenciesMeta:new Map(e.peerDependenciesMeta),bin:new Map(e.bin)}}function xB(e){return A8(e,e)}function p8(e,t){if(t.includes("#"))throw new Error("Invalid entropy");return Mn(e,`virtual:${t}#${e.range}`)}function h8(e,t){if(t.includes("#"))throw new Error("Invalid entropy");return A8(e,Js(e,`virtual:${t}#${e.reference}`))}function Rp(e){return e.range.startsWith(PB)}function Hu(e){return e.reference.startsWith(PB)}function kB(e){if(!Rp(e))throw new Error("Not a virtual descriptor");return Mn(e,e.range.replace(bQ,""))}function sI(e){if(!Hu(e))throw new Error("Not a virtual descriptor");return Js(e,e.reference.replace(bQ,""))}function xXe(e){return Rp(e)?Mn(e,e.range.replace(bQ,"")):e}function kXe(e){return Hu(e)?Js(e,e.reference.replace(bQ,"")):e}function QXe(e,t){return e.range.includes("::")?e:Mn(e,`${e.range}::${iI.default.stringify(t)}`)}function RXe(e,t){return e.reference.includes("::")?e:Js(e,`${e.reference}::${iI.default.stringify(t)}`)}function QB(e,t){return e.identHash===t.identHash}function Ice(e,t){return e.descriptorHash===t.descriptorHash}function RB(e,t){return e.locatorHash===t.locatorHash}function TXe(e,t){if(!Hu(e))throw new Error("Invalid package type");if(!Hu(t))throw new Error("Invalid package type");if(!QB(e,t)||e.dependencies.size!==t.dependencies.size)return!1;for(let r of e.dependencies.values()){let s=t.dependencies.get(r.identHash);if(!s||!Ice(r,s))return!1}return!0}function xa(e){let t=Cce(e);if(!t)throw new Error(`Invalid ident (${e})`);return t}function Cce(e){let t=e.match(FXe);if(!t)return null;let[,r,s]=t;return ka(typeof r<"u"?r:null,s)}function I0(e,t=!1){let r=TB(e,t);if(!r)throw new Error(`Invalid descriptor (${e})`);return r}function TB(e,t=!1){let r=t?e.match(NXe):e.match(OXe);if(!r)return null;let[,s,a,n]=r;if(n===l8)throw new Error(`Invalid range (${e})`);let c=typeof s<"u"?s:null,f=typeof n<"u"?n:l8;return Mn(ka(c,a),f)}function Tp(e,t=!1){let r=PQ(e,t);if(!r)throw new Error(`Invalid locator (${e})`);return r}function PQ(e,t=!1){let r=t?e.match(LXe):e.match(MXe);if(!r)return null;let[,s,a,n]=r;if(n==="unknown")throw new Error(`Invalid reference (${e})`);let c=typeof s<"u"?s:null,f=typeof n<"u"?n:"unknown";return Js(ka(c,a),f)}function Zg(e,t){let r=e.match(UXe);if(r===null)throw new Error(`Invalid range (${e})`);let s=typeof r[1]<"u"?r[1]:null;if(typeof t?.requireProtocol=="string"&&s!==t.requireProtocol)throw new Error(`Invalid protocol (${s})`);if(t?.requireProtocol&&s===null)throw new Error(`Missing protocol (${s})`);let a=typeof r[3]<"u"?decodeURIComponent(r[2]):null;if(t?.requireSource&&a===null)throw new Error(`Missing source (${e})`);let n=typeof r[3]<"u"?decodeURIComponent(r[3]):decodeURIComponent(r[2]),c=t?.parseSelector?iI.default.parse(n):n,f=typeof r[4]<"u"?iI.default.parse(r[4]):null;return{protocol:s,source:a,selector:c,params:f}}function _Xe(e,t){try{return Zg(e,t)}catch{return null}}function HXe(e,{protocol:t}){let{selector:r,params:s}=Zg(e,{requireProtocol:t,requireBindings:!0});if(typeof s.locator!="string")throw new Error(`Assertion failed: Invalid bindings for ${e}`);return{parentLocator:Tp(s.locator,!0),path:r}}function mce(e){return e=e.replaceAll("%","%25"),e=e.replaceAll(":","%3A"),e=e.replaceAll("#","%23"),e}function jXe(e){return e===null?!1:Object.entries(e).length>0}function xQ({protocol:e,source:t,selector:r,params:s}){let a="";return e!==null&&(a+=`${e}`),t!==null&&(a+=`${mce(t)}#`),a+=mce(r),jXe(s)&&(a+=`::${iI.default.stringify(s)}`),a}function GXe(e){let{params:t,protocol:r,source:s,selector:a}=Zg(e);for(let n in t)n.startsWith("__")&&delete t[n];return xQ({protocol:r,source:s,params:t,selector:a})}function fn(e){return e.scope?`@${e.scope}/${e.name}`:`${e.name}`}function qXe(e,t){return e.scope?ka(t,`${e.scope}__${e.name}`):ka(t,e.name)}function WXe(e,t){if(e.scope!==t)return e;let r=e.name.indexOf("__");if(r===-1)return ka(null,e.name);let s=e.name.slice(0,r),a=e.name.slice(r+2);return ka(s,a)}function gl(e){return e.scope?`@${e.scope}/${e.name}@${e.range}`:`${e.name}@${e.range}`}function ml(e){return e.scope?`@${e.scope}/${e.name}@${e.reference}`:`${e.name}@${e.reference}`}function c8(e){return e.scope!==null?`@${e.scope}-${e.name}`:e.name}function oI(e){let{protocol:t,selector:r}=Zg(e.reference),s=t!==null?t.replace(YXe,""):"exotic",a=u8.default.valid(r),n=a!==null?`${s}-${a}`:`${s}`,c=10;return e.scope?`${c8(e)}-${n}-${e.locatorHash.slice(0,c)}`:`${c8(e)}-${n}-${e.locatorHash.slice(0,c)}`}function $i(e,t){return t.scope?`${jt(e,`@${t.scope}/`,dt.SCOPE)}${jt(e,t.name,dt.NAME)}`:`${jt(e,t.name,dt.NAME)}`}function kQ(e){if(e.startsWith(PB)){let t=kQ(e.substring(e.indexOf("#")+1)),r=e.substring(PB.length,PB.length+SXe);return`${t} [${r}]`}else return e.replace(VXe,"?[...]")}function aI(e,t){return`${jt(e,kQ(t),dt.RANGE)}`}function oi(e,t){return`${$i(e,t)}${jt(e,"@",dt.RANGE)}${aI(e,t.range)}`}function FB(e,t){return`${jt(e,kQ(t),dt.REFERENCE)}`}function Yr(e,t){return`${$i(e,t)}${jt(e,"@",dt.REFERENCE)}${FB(e,t.reference)}`}function M4(e){return`${fn(e)}@${kQ(e.reference)}`}function lI(e){return Vs(e,[t=>fn(t),t=>t.range])}function NB(e,t){return $i(e,t.anchoredLocator)}function DB(e,t,r){let s=Rp(t)?kB(t):t;return r===null?`${oi(e,s)} \u2192 ${L4(e).Cross}`:s.identHash===r.identHash?`${oi(e,s)} \u2192 ${FB(e,r.reference)}`:`${oi(e,s)} \u2192 ${Yr(e,r)}`}function U4(e,t,r){return r===null?`${Yr(e,t)}`:`${Yr(e,t)} (via ${aI(e,r.range)})`}function d8(e){return`node_modules/${fn(e)}`}function JXe(e,t){return t===l8||!e.version?!0:u8.default.satisfies(e.version??"",t)}function QQ(e,t){return e.conditions?DXe(e.conditions,r=>{let[,s,a]=r.match(Ece),n=t[s];return n?n.includes(a):!0}):!0}function OB(e){let t=new Set;if("children"in e)t.add(e);else for(let r of e.requests.values())t.add(r);for(let r of t)for(let s of r.children.values())t.add(s);return t}var iI,u8,yce,PB,SXe,Ece,DXe,bQ,FXe,NXe,OXe,l8,LXe,MXe,UXe,YXe,VXe,Zo=Xe(()=>{iI=et(Ie("querystring")),u8=et(pi()),yce=et(Hie());Qc();E0();kc();Zo();PB="virtual:",SXe=5,Ece=/(os|cpu|libc)=([a-z0-9_-]+)/,DXe=(0,yce.makeParser)(Ece);bQ=/^[^#]*#/;FXe=/^(?:@([^/]+?)\/)?([^@/]+)$/;NXe=/^(?:@([^/]+?)\/)?([^@/]+?)(?:@(.+))$/,OXe=/^(?:@([^/]+?)\/)?([^@/]+?)(?:@(.+))?$/,l8="unknown";LXe=/^(?:@([^/]+?)\/)?([^@/]+?)(?:@(.+))$/,MXe=/^(?:@([^/]+?)\/)?([^@/]+?)(?:@(.+))?$/;UXe=/^([^#:]*:)?((?:(?!::)[^#])*)(?:#((?:(?!::).)*))?(?:::(.*))?$/;YXe=/:$/;VXe=/\?.*/});var wce,Bce=Xe(()=>{Zo();wce={hooks:{reduceDependency:(e,t,r,s,{resolver:a,resolveOptions:n})=>{for(let{pattern:c,reference:f}of t.topLevelWorkspace.manifest.resolutions){if(c.from&&(c.from.fullName!==fn(r)||t.configuration.normalizeLocator(Js(xa(c.from.fullName),c.from.description??r.reference)).locatorHash!==r.locatorHash)||c.descriptor.fullName!==fn(e)||t.configuration.normalizeDependency(Mn(Tp(c.descriptor.fullName),c.descriptor.description??e.range)).descriptorHash!==e.descriptorHash)continue;return a.bindDescriptor(t.configuration.normalizeDependency(Mn(e,f)),t.topLevelWorkspace.anchoredLocator,n)}return e},validateProject:async(e,t)=>{for(let r of e.workspaces){let s=NB(e.configuration,r);await e.configuration.triggerHook(a=>a.validateWorkspace,r,{reportWarning:(a,n)=>t.reportWarning(a,`${s}: ${n}`),reportError:(a,n)=>t.reportError(a,`${s}: ${n}`)})}},validateWorkspace:async(e,t)=>{let{manifest:r}=e;r.resolutions.length&&e.cwd!==e.project.cwd&&r.errors.push(new Error("Resolutions field will be ignored"));for(let s of r.errors)t.reportWarning(57,s.message)}}}});var Ii,$g=Xe(()=>{Ii=class e{static{this.protocol="workspace:"}supportsDescriptor(t,r){return!!(t.range.startsWith(e.protocol)||r.project.tryWorkspaceByDescriptor(t)!==null)}supportsLocator(t,r){return!!t.reference.startsWith(e.protocol)}shouldPersistResolution(t,r){return!1}bindDescriptor(t,r,s){return t}getResolutionDependencies(t,r){return{}}async getCandidates(t,r,s){return[s.project.getWorkspaceByDescriptor(t).anchoredLocator]}async getSatisfying(t,r,s,a){let[n]=await this.getCandidates(t,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(t,r){let s=r.project.getWorkspaceByCwd(t.reference.slice(e.protocol.length));return{...t,version:s.manifest.version||"0.0.0",languageName:"unknown",linkType:"SOFT",conditions:null,dependencies:r.project.configuration.normalizeDependencyMap(new Map([...s.manifest.dependencies,...s.manifest.devDependencies])),peerDependencies:new Map([...s.manifest.peerDependencies]),dependenciesMeta:s.manifest.dependenciesMeta,peerDependenciesMeta:s.manifest.peerDependenciesMeta,bin:s.manifest.bin}}}});var kr={};Vt(kr,{SemVer:()=>Pce.SemVer,clean:()=>zXe,getComparator:()=>Dce,mergeComparators:()=>g8,satisfiesWithPrereleases:()=>tA,simplifyRanges:()=>m8,stringifyComparator:()=>bce,validRange:()=>yl});function tA(e,t,r=!1){if(!e)return!1;let s=`${t}${r}`,a=vce.get(s);if(typeof a>"u")try{a=new Fp.default.Range(t,{includePrerelease:!0,loose:r})}catch{return!1}finally{vce.set(s,a||null)}else if(a===null)return!1;let n;try{n=new Fp.default.SemVer(e,a)}catch{return!1}return a.test(n)?!0:(n.prerelease&&(n.prerelease=[]),a.set.some(c=>{for(let f of c)f.semver.prerelease&&(f.semver.prerelease=[]);return c.every(f=>f.test(n))}))}function yl(e){if(e.indexOf(":")!==-1)return null;let t=Sce.get(e);if(typeof t<"u")return t;try{t=new Fp.default.Range(e)}catch{t=null}return Sce.set(e,t),t}function zXe(e){let t=KXe.exec(e);return t?t[1]:null}function Dce(e){if(e.semver===Fp.default.Comparator.ANY)return{gt:null,lt:null};switch(e.operator){case"":return{gt:[">=",e.semver],lt:["<=",e.semver]};case">":case">=":return{gt:[e.operator,e.semver],lt:null};case"<":case"<=":return{gt:null,lt:[e.operator,e.semver]};default:throw new Error(`Assertion failed: Unexpected comparator operator (${e.operator})`)}}function g8(e){if(e.length===0)return null;let t=null,r=null;for(let s of e){if(s.gt){let a=t!==null?Fp.default.compare(s.gt[1],t[1]):null;(a===null||a>0||a===0&&s.gt[0]===">")&&(t=s.gt)}if(s.lt){let a=r!==null?Fp.default.compare(s.lt[1],r[1]):null;(a===null||a<0||a===0&&s.lt[0]==="<")&&(r=s.lt)}}if(t&&r){let s=Fp.default.compare(t[1],r[1]);if(s===0&&(t[0]===">"||r[0]==="<")||s>0)return null}return{gt:t,lt:r}}function bce(e){if(e.gt&&e.lt){if(e.gt[0]===">="&&e.lt[0]==="<="&&e.gt[1].version===e.lt[1].version)return e.gt[1].version;if(e.gt[0]===">="&&e.lt[0]==="<"){if(e.lt[1].version===`${e.gt[1].major+1}.0.0-0`)return`^${e.gt[1].version}`;if(e.lt[1].version===`${e.gt[1].major}.${e.gt[1].minor+1}.0-0`)return`~${e.gt[1].version}`}}let t=[];return e.gt&&t.push(e.gt[0]+e.gt[1].version),e.lt&&t.push(e.lt[0]+e.lt[1].version),t.length?t.join(" "):"*"}function m8(e){let t=e.map(XXe).map(s=>yl(s).set.map(a=>a.map(n=>Dce(n)))),r=t.shift().map(s=>g8(s)).filter(s=>s!==null);for(let s of t){let a=[];for(let n of r)for(let c of s){let f=g8([n,...c]);f!==null&&a.push(f)}r=a}return r.length===0?null:r.map(s=>bce(s)).join(" || ")}function XXe(e){let t=e.split("||");if(t.length>1){let r=new Set;for(let s of t)t.some(a=>a!==s&&Fp.default.subset(s,a))||r.add(s);if(r.size{Fp=et(pi()),Pce=et(pi()),vce=new Map;Sce=new Map;KXe=/^(?:[\sv=]*?)((0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)(?:\s*)$/});function xce(e){let t=e.match(/^[ \t]+/m);return t?t[0]:" "}function kce(e){return e.charCodeAt(0)===65279?e.slice(1):e}function Qa(e){return e.replace(/\\/g,"/")}function RQ(e,{yamlCompatibilityMode:t}){return t?k4(e):typeof e>"u"||typeof e=="boolean"?e:null}function Qce(e,t){let r=t.search(/[^!]/);if(r===-1)return"invalid";let s=r%2===0?"":"!",a=t.slice(r);return`${s}${e}=${a}`}function y8(e,t){return t.length===1?Qce(e,t[0]):`(${t.map(r=>Qce(e,r)).join(" | ")})`}var Rce,_t,cI=Xe(()=>{Dt();vc();Rce=et(pi());$g();kc();Np();Zo();_t=class e{constructor(){this.indent=" ";this.name=null;this.version=null;this.os=null;this.cpu=null;this.libc=null;this.type=null;this.packageManager=null;this.private=!1;this.license=null;this.main=null;this.module=null;this.browser=null;this.languageName=null;this.bin=new Map;this.scripts=new Map;this.dependencies=new Map;this.devDependencies=new Map;this.peerDependencies=new Map;this.workspaceDefinitions=[];this.dependenciesMeta=new Map;this.peerDependenciesMeta=new Map;this.resolutions=[];this.files=null;this.publishConfig=null;this.installConfig=null;this.preferUnplugged=null;this.raw={};this.errors=[]}static{this.fileName="package.json"}static{this.allDependencies=["dependencies","devDependencies","peerDependencies"]}static{this.hardDependencies=["dependencies","devDependencies"]}static async tryFind(t,{baseFs:r=new Vn}={}){let s=J.join(t,"package.json");try{return await e.fromFile(s,{baseFs:r})}catch(a){if(a.code==="ENOENT")return null;throw a}}static async find(t,{baseFs:r}={}){let s=await e.tryFind(t,{baseFs:r});if(s===null)throw new Error("Manifest not found");return s}static async fromFile(t,{baseFs:r=new Vn}={}){let s=new e;return await s.loadFile(t,{baseFs:r}),s}static fromText(t){let r=new e;return r.loadFromText(t),r}loadFromText(t){let r;try{r=JSON.parse(kce(t)||"{}")}catch(s){throw s.message+=` (when parsing ${t})`,s}this.load(r),this.indent=xce(t)}async loadFile(t,{baseFs:r=new Vn}){let s=await r.readFilePromise(t,"utf8"),a;try{a=JSON.parse(kce(s)||"{}")}catch(n){throw n.message+=` (when parsing ${t})`,n}this.load(a),this.indent=xce(s)}load(t,{yamlCompatibilityMode:r=!1}={}){if(typeof t!="object"||t===null)throw new Error(`Utterly invalid manifest data (${t})`);this.raw=t;let s=[];if(this.name=null,typeof t.name=="string")try{this.name=xa(t.name)}catch{s.push(new Error("Parsing failed for the 'name' field"))}if(typeof t.version=="string"?this.version=t.version:this.version=null,Array.isArray(t.os)){let n=[];this.os=n;for(let c of t.os)typeof c!="string"?s.push(new Error("Parsing failed for the 'os' field")):n.push(c)}else this.os=null;if(Array.isArray(t.cpu)){let n=[];this.cpu=n;for(let c of t.cpu)typeof c!="string"?s.push(new Error("Parsing failed for the 'cpu' field")):n.push(c)}else this.cpu=null;if(Array.isArray(t.libc)){let n=[];this.libc=n;for(let c of t.libc)typeof c!="string"?s.push(new Error("Parsing failed for the 'libc' field")):n.push(c)}else this.libc=null;if(typeof t.type=="string"?this.type=t.type:this.type=null,typeof t.packageManager=="string"?this.packageManager=t.packageManager:this.packageManager=null,typeof t.private=="boolean"?this.private=t.private:this.private=!1,typeof t.license=="string"?this.license=t.license:this.license=null,typeof t.languageName=="string"?this.languageName=t.languageName:this.languageName=null,typeof t.main=="string"?this.main=Qa(t.main):this.main=null,typeof t.module=="string"?this.module=Qa(t.module):this.module=null,t.browser!=null)if(typeof t.browser=="string")this.browser=Qa(t.browser);else{this.browser=new Map;for(let[n,c]of Object.entries(t.browser))this.browser.set(Qa(n),typeof c=="string"?Qa(c):c)}else this.browser=null;if(this.bin=new Map,typeof t.bin=="string")t.bin.trim()===""?s.push(new Error("Invalid bin field")):this.name!==null?this.bin.set(this.name.name,Qa(t.bin)):s.push(new Error("String bin field, but no attached package name"));else if(typeof t.bin=="object"&&t.bin!==null)for(let[n,c]of Object.entries(t.bin)){if(typeof c!="string"||c.trim()===""){s.push(new Error(`Invalid bin definition for '${n}'`));continue}let f=xa(n);this.bin.set(f.name,Qa(c))}if(this.scripts=new Map,typeof t.scripts=="object"&&t.scripts!==null)for(let[n,c]of Object.entries(t.scripts)){if(typeof c!="string"){s.push(new Error(`Invalid script definition for '${n}'`));continue}this.scripts.set(n,c)}if(this.dependencies=new Map,typeof t.dependencies=="object"&&t.dependencies!==null)for(let[n,c]of Object.entries(t.dependencies)){if(typeof c!="string"){s.push(new Error(`Invalid dependency range for '${n}'`));continue}let f;try{f=xa(n)}catch{s.push(new Error(`Parsing failed for the dependency name '${n}'`));continue}let p=Mn(f,c);this.dependencies.set(p.identHash,p)}if(this.devDependencies=new Map,typeof t.devDependencies=="object"&&t.devDependencies!==null)for(let[n,c]of Object.entries(t.devDependencies)){if(typeof c!="string"){s.push(new Error(`Invalid dependency range for '${n}'`));continue}let f;try{f=xa(n)}catch{s.push(new Error(`Parsing failed for the dependency name '${n}'`));continue}let p=Mn(f,c);this.devDependencies.set(p.identHash,p)}if(this.peerDependencies=new Map,typeof t.peerDependencies=="object"&&t.peerDependencies!==null)for(let[n,c]of Object.entries(t.peerDependencies)){let f;try{f=xa(n)}catch{s.push(new Error(`Parsing failed for the dependency name '${n}'`));continue}(typeof c!="string"||!c.startsWith(Ii.protocol)&&!yl(c))&&(s.push(new Error(`Invalid dependency range for '${n}'`)),c="*");let p=Mn(f,c);this.peerDependencies.set(p.identHash,p)}typeof t.workspaces=="object"&&t.workspaces!==null&&t.workspaces.nohoist&&s.push(new Error("'nohoist' is deprecated, please use 'installConfig.hoistingLimits' instead"));let a=Array.isArray(t.workspaces)?t.workspaces:typeof t.workspaces=="object"&&t.workspaces!==null&&Array.isArray(t.workspaces.packages)?t.workspaces.packages:[];this.workspaceDefinitions=[];for(let n of a){if(typeof n!="string"){s.push(new Error(`Invalid workspace definition for '${n}'`));continue}this.workspaceDefinitions.push({pattern:n})}if(this.dependenciesMeta=new Map,typeof t.dependenciesMeta=="object"&&t.dependenciesMeta!==null)for(let[n,c]of Object.entries(t.dependenciesMeta)){if(typeof c!="object"||c===null){s.push(new Error(`Invalid meta field for '${n}`));continue}let f=I0(n),p=this.ensureDependencyMeta(f),h=RQ(c.built,{yamlCompatibilityMode:r});if(h===null){s.push(new Error(`Invalid built meta field for '${n}'`));continue}let E=RQ(c.optional,{yamlCompatibilityMode:r});if(E===null){s.push(new Error(`Invalid optional meta field for '${n}'`));continue}let C=RQ(c.unplugged,{yamlCompatibilityMode:r});if(C===null){s.push(new Error(`Invalid unplugged meta field for '${n}'`));continue}Object.assign(p,{built:h,optional:E,unplugged:C})}if(this.peerDependenciesMeta=new Map,typeof t.peerDependenciesMeta=="object"&&t.peerDependenciesMeta!==null)for(let[n,c]of Object.entries(t.peerDependenciesMeta)){if(typeof c!="object"||c===null){s.push(new Error(`Invalid meta field for '${n}'`));continue}let f=I0(n),p=this.ensurePeerDependencyMeta(f),h=RQ(c.optional,{yamlCompatibilityMode:r});if(h===null){s.push(new Error(`Invalid optional meta field for '${n}'`));continue}Object.assign(p,{optional:h})}if(this.resolutions=[],typeof t.resolutions=="object"&&t.resolutions!==null)for(let[n,c]of Object.entries(t.resolutions)){if(typeof c!="string"){s.push(new Error(`Invalid resolution entry for '${n}'`));continue}try{this.resolutions.push({pattern:px(n),reference:c})}catch(f){s.push(f);continue}}if(Array.isArray(t.files)){this.files=new Set;for(let n of t.files){if(typeof n!="string"){s.push(new Error(`Invalid files entry for '${n}'`));continue}this.files.add(n)}}else this.files=null;if(typeof t.publishConfig=="object"&&t.publishConfig!==null){if(this.publishConfig={},typeof t.publishConfig.access=="string"&&(this.publishConfig.access=t.publishConfig.access),typeof t.publishConfig.main=="string"&&(this.publishConfig.main=Qa(t.publishConfig.main)),typeof t.publishConfig.module=="string"&&(this.publishConfig.module=Qa(t.publishConfig.module)),t.publishConfig.browser!=null)if(typeof t.publishConfig.browser=="string")this.publishConfig.browser=Qa(t.publishConfig.browser);else{this.publishConfig.browser=new Map;for(let[n,c]of Object.entries(t.publishConfig.browser))this.publishConfig.browser.set(Qa(n),typeof c=="string"?Qa(c):c)}if(typeof t.publishConfig.registry=="string"&&(this.publishConfig.registry=t.publishConfig.registry),typeof t.publishConfig.provenance=="boolean"&&(this.publishConfig.provenance=t.publishConfig.provenance),typeof t.publishConfig.bin=="string")this.name!==null?this.publishConfig.bin=new Map([[this.name.name,Qa(t.publishConfig.bin)]]):s.push(new Error("String bin field, but no attached package name"));else if(typeof t.publishConfig.bin=="object"&&t.publishConfig.bin!==null){this.publishConfig.bin=new Map;for(let[n,c]of Object.entries(t.publishConfig.bin)){if(typeof c!="string"){s.push(new Error(`Invalid bin definition for '${n}'`));continue}this.publishConfig.bin.set(n,Qa(c))}}if(Array.isArray(t.publishConfig.executableFiles)){this.publishConfig.executableFiles=new Set;for(let n of t.publishConfig.executableFiles){if(typeof n!="string"){s.push(new Error("Invalid executable file definition"));continue}this.publishConfig.executableFiles.add(Qa(n))}}}else this.publishConfig=null;if(typeof t.installConfig=="object"&&t.installConfig!==null){this.installConfig={};for(let n of Object.keys(t.installConfig))n==="hoistingLimits"?typeof t.installConfig.hoistingLimits=="string"?this.installConfig.hoistingLimits=t.installConfig.hoistingLimits:s.push(new Error("Invalid hoisting limits definition")):n=="selfReferences"?typeof t.installConfig.selfReferences=="boolean"?this.installConfig.selfReferences=t.installConfig.selfReferences:s.push(new Error("Invalid selfReferences definition, must be a boolean value")):s.push(new Error(`Unrecognized installConfig key: ${n}`))}else this.installConfig=null;if(typeof t.optionalDependencies=="object"&&t.optionalDependencies!==null)for(let[n,c]of Object.entries(t.optionalDependencies)){if(typeof c!="string"){s.push(new Error(`Invalid dependency range for '${n}'`));continue}let f;try{f=xa(n)}catch{s.push(new Error(`Parsing failed for the dependency name '${n}'`));continue}let p=Mn(f,c);this.dependencies.set(p.identHash,p);let h=Mn(f,"unknown"),E=this.ensureDependencyMeta(h);Object.assign(E,{optional:!0})}typeof t.preferUnplugged=="boolean"?this.preferUnplugged=t.preferUnplugged:this.preferUnplugged=null,this.errors=s}getForScope(t){switch(t){case"dependencies":return this.dependencies;case"devDependencies":return this.devDependencies;case"peerDependencies":return this.peerDependencies;default:throw new Error(`Unsupported value ("${t}")`)}}hasConsumerDependency(t){return!!(this.dependencies.has(t.identHash)||this.peerDependencies.has(t.identHash))}hasHardDependency(t){return!!(this.dependencies.has(t.identHash)||this.devDependencies.has(t.identHash))}hasSoftDependency(t){return!!this.peerDependencies.has(t.identHash)}hasDependency(t){return!!(this.hasHardDependency(t)||this.hasSoftDependency(t))}getConditions(){let t=[];return this.os&&this.os.length>0&&t.push(y8("os",this.os)),this.cpu&&this.cpu.length>0&&t.push(y8("cpu",this.cpu)),this.libc&&this.libc.length>0&&t.push(y8("libc",this.libc)),t.length>0?t.join(" & "):null}ensureDependencyMeta(t){if(t.range!=="unknown"&&!Rce.default.valid(t.range))throw new Error(`Invalid meta field range for '${gl(t)}'`);let r=fn(t),s=t.range!=="unknown"?t.range:null,a=this.dependenciesMeta.get(r);a||this.dependenciesMeta.set(r,a=new Map);let n=a.get(s);return n||a.set(s,n={}),n}ensurePeerDependencyMeta(t){if(t.range!=="unknown")throw new Error(`Invalid meta field range for '${gl(t)}'`);let r=fn(t),s=this.peerDependenciesMeta.get(r);return s||this.peerDependenciesMeta.set(r,s={}),s}setRawField(t,r,{after:s=[]}={}){let a=new Set(s.filter(n=>Object.hasOwn(this.raw,n)));if(a.size===0||Object.hasOwn(this.raw,t))this.raw[t]=r;else{let n=this.raw,c=this.raw={},f=!1;for(let p of Object.keys(n))c[p]=n[p],f||(a.delete(p),a.size===0&&(c[t]=r,f=!0))}}exportTo(t,{compatibilityMode:r=!0}={}){if(Object.assign(t,this.raw),this.name!==null?t.name=fn(this.name):delete t.name,this.version!==null?t.version=this.version:delete t.version,this.os!==null?t.os=this.os:delete t.os,this.cpu!==null?t.cpu=this.cpu:delete t.cpu,this.type!==null?t.type=this.type:delete t.type,this.packageManager!==null?t.packageManager=this.packageManager:delete t.packageManager,this.private?t.private=!0:delete t.private,this.license!==null?t.license=this.license:delete t.license,this.languageName!==null?t.languageName=this.languageName:delete t.languageName,this.main!==null?t.main=this.main:delete t.main,this.module!==null?t.module=this.module:delete t.module,this.browser!==null){let n=this.browser;typeof n=="string"?t.browser=n:n instanceof Map&&(t.browser=Object.assign({},...Array.from(n.keys()).sort().map(c=>({[c]:n.get(c)}))))}else delete t.browser;this.bin.size===1&&this.name!==null&&this.bin.has(this.name.name)?t.bin=this.bin.get(this.name.name):this.bin.size>0?t.bin=Object.assign({},...Array.from(this.bin.keys()).sort().map(n=>({[n]:this.bin.get(n)}))):delete t.bin,this.workspaceDefinitions.length>0?this.raw.workspaces&&!Array.isArray(this.raw.workspaces)?t.workspaces={...this.raw.workspaces,packages:this.workspaceDefinitions.map(({pattern:n})=>n)}:t.workspaces=this.workspaceDefinitions.map(({pattern:n})=>n):this.raw.workspaces&&!Array.isArray(this.raw.workspaces)&&Object.keys(this.raw.workspaces).length>0?t.workspaces=this.raw.workspaces:delete t.workspaces;let s=[],a=[];for(let n of this.dependencies.values()){let c=this.dependenciesMeta.get(fn(n)),f=!1;if(r&&c){let p=c.get(null);p&&p.optional&&(f=!0)}f?a.push(n):s.push(n)}s.length>0?t.dependencies=Object.assign({},...lI(s).map(n=>({[fn(n)]:n.range}))):delete t.dependencies,a.length>0?t.optionalDependencies=Object.assign({},...lI(a).map(n=>({[fn(n)]:n.range}))):delete t.optionalDependencies,this.devDependencies.size>0?t.devDependencies=Object.assign({},...lI(this.devDependencies.values()).map(n=>({[fn(n)]:n.range}))):delete t.devDependencies,this.peerDependencies.size>0?t.peerDependencies=Object.assign({},...lI(this.peerDependencies.values()).map(n=>({[fn(n)]:n.range}))):delete t.peerDependencies,t.dependenciesMeta={};for(let[n,c]of Vs(this.dependenciesMeta.entries(),([f,p])=>f))for(let[f,p]of Vs(c.entries(),([h,E])=>h!==null?`0${h}`:"1")){let h=f!==null?gl(Mn(xa(n),f)):n,E={...p};r&&f===null&&delete E.optional,Object.keys(E).length!==0&&(t.dependenciesMeta[h]=E)}if(Object.keys(t.dependenciesMeta).length===0&&delete t.dependenciesMeta,this.peerDependenciesMeta.size>0?t.peerDependenciesMeta=Object.assign({},...Vs(this.peerDependenciesMeta.entries(),([n,c])=>n).map(([n,c])=>({[n]:c}))):delete t.peerDependenciesMeta,this.resolutions.length>0?t.resolutions=Object.assign({},...this.resolutions.map(({pattern:n,reference:c})=>({[hx(n)]:c}))):delete t.resolutions,this.files!==null?t.files=Array.from(this.files):delete t.files,this.preferUnplugged!==null?t.preferUnplugged=this.preferUnplugged:delete t.preferUnplugged,this.scripts!==null&&this.scripts.size>0){t.scripts??={};for(let n of Object.keys(t.scripts))this.scripts.has(n)||delete t.scripts[n];for(let[n,c]of this.scripts.entries())t.scripts[n]=c}else delete t.scripts;return t}}});function $Xe(e){return typeof e.reportCode<"u"}var Tce,Fce,ZXe,Lt,yo,Fc=Xe(()=>{zl();Tce=Ie("stream"),Fce=Ie("string_decoder"),ZXe=15,Lt=class extends Error{constructor(r,s,a){super(s);this.reportExtra=a;this.reportCode=r}};yo=class{constructor(){this.cacheHits=new Set;this.cacheMisses=new Set;this.reportedInfos=new Set;this.reportedWarnings=new Set;this.reportedErrors=new Set}getRecommendedLength(){return 180}reportCacheHit(t){this.cacheHits.add(t.locatorHash)}reportCacheMiss(t,r){this.cacheMisses.add(t.locatorHash)}static progressViaCounter(t){let r=0,s,a=new Promise(p=>{s=p}),n=p=>{let h=s;a=new Promise(E=>{s=E}),r=p,h()},c=(p=0)=>{n(r+1)},f=async function*(){for(;r{r=c}),a=d4(c=>{let f=r;s=new Promise(p=>{r=p}),t=c,f()},1e3/ZXe),n=async function*(){for(;;)await s,yield{title:t}}();return{[Symbol.asyncIterator](){return n},hasProgress:!1,hasTitle:!0,setTitle:a}}async startProgressPromise(t,r){let s=this.reportProgress(t);try{return await r(t)}finally{s.stop()}}startProgressSync(t,r){let s=this.reportProgress(t);try{return r(t)}finally{s.stop()}}reportInfoOnce(t,r,s){let a=s&&s.key?s.key:r;this.reportedInfos.has(a)||(this.reportedInfos.add(a),this.reportInfo(t,r),s?.reportExtra?.(this))}reportWarningOnce(t,r,s){let a=s&&s.key?s.key:r;this.reportedWarnings.has(a)||(this.reportedWarnings.add(a),this.reportWarning(t,r),s?.reportExtra?.(this))}reportErrorOnce(t,r,s){let a=s&&s.key?s.key:r;this.reportedErrors.has(a)||(this.reportedErrors.add(a),this.reportError(t,r),s?.reportExtra?.(this))}reportExceptionOnce(t){$Xe(t)?this.reportErrorOnce(t.reportCode,t.message,{key:t,reportExtra:t.reportExtra}):this.reportErrorOnce(1,t.stack||t.message,{key:t})}createStreamReporter(t=null){let r=new Tce.PassThrough,s=new Fce.StringDecoder,a="";return r.on("data",n=>{let c=s.write(n),f;do if(f=c.indexOf(` `),f!==-1){let p=a+c.substring(0,f);c=c.substring(f+1),a="",t!==null?this.reportInfo(null,`${t} ${p}`):this.reportInfo(null,p)}while(f!==-1);a+=c}),r.on("end",()=>{let n=s.end();n!==""&&(t!==null?this.reportInfo(null,`${t} ${n}`):this.reportInfo(null,n))}),r}}});var uI,E8=Xe(()=>{Fc();Zo();uI=class{constructor(t){this.fetchers=t}supports(t,r){return!!this.tryFetcher(t,r)}getLocalPath(t,r){return this.getFetcher(t,r).getLocalPath(t,r)}async fetch(t,r){return await this.getFetcher(t,r).fetch(t,r)}tryFetcher(t,r){let s=this.fetchers.find(a=>a.supports(t,r));return s||null}getFetcher(t,r){let s=this.fetchers.find(a=>a.supports(t,r));if(!s)throw new Lt(11,`${Yr(r.project.configuration,t)} isn't supported by any available fetcher`);return s}}});var em,I8=Xe(()=>{Zo();em=class{constructor(t){this.resolvers=t.filter(r=>r)}supportsDescriptor(t,r){return!!this.tryResolverByDescriptor(t,r)}supportsLocator(t,r){return!!this.tryResolverByLocator(t,r)}shouldPersistResolution(t,r){return this.getResolverByLocator(t,r).shouldPersistResolution(t,r)}bindDescriptor(t,r,s){return this.getResolverByDescriptor(t,s).bindDescriptor(t,r,s)}getResolutionDependencies(t,r){return this.getResolverByDescriptor(t,r).getResolutionDependencies(t,r)}async getCandidates(t,r,s){return await this.getResolverByDescriptor(t,s).getCandidates(t,r,s)}async getSatisfying(t,r,s,a){return this.getResolverByDescriptor(t,a).getSatisfying(t,r,s,a)}async resolve(t,r){return await this.getResolverByLocator(t,r).resolve(t,r)}tryResolverByDescriptor(t,r){let s=this.resolvers.find(a=>a.supportsDescriptor(t,r));return s||null}getResolverByDescriptor(t,r){let s=this.resolvers.find(a=>a.supportsDescriptor(t,r));if(!s)throw new Error(`${oi(r.project.configuration,t)} isn't supported by any available resolver`);return s}tryResolverByLocator(t,r){let s=this.resolvers.find(a=>a.supportsLocator(t,r));return s||null}getResolverByLocator(t,r){let s=this.resolvers.find(a=>a.supportsLocator(t,r));if(!s)throw new Error(`${Yr(r.project.configuration,t)} isn't supported by any available resolver`);return s}}});var fI,C8=Xe(()=>{Dt();Zo();fI=class{supports(t){return!!t.reference.startsWith("virtual:")}getLocalPath(t,r){let s=t.reference.indexOf("#");if(s===-1)throw new Error("Invalid virtual package reference");let a=t.reference.slice(s+1),n=Js(t,a);return r.fetcher.getLocalPath(n,r)}async fetch(t,r){let s=t.reference.indexOf("#");if(s===-1)throw new Error("Invalid virtual package reference");let a=t.reference.slice(s+1),n=Js(t,a),c=await r.fetcher.fetch(n,r);return await this.ensureVirtualLink(t,c,r)}getLocatorFilename(t){return oI(t)}async ensureVirtualLink(t,r,s){let a=r.packageFs.getRealPath(),n=s.project.configuration.get("virtualFolder"),c=this.getLocatorFilename(t),f=mo.makeVirtualPath(n,c,a),p=new Gf(f,{baseFs:r.packageFs,pathUtils:J});return{...r,packageFs:p}}}});var TQ,Nce=Xe(()=>{TQ=class e{static{this.protocol="virtual:"}static isVirtualDescriptor(t){return!!t.range.startsWith(e.protocol)}static isVirtualLocator(t){return!!t.reference.startsWith(e.protocol)}supportsDescriptor(t,r){return e.isVirtualDescriptor(t)}supportsLocator(t,r){return e.isVirtualLocator(t)}shouldPersistResolution(t,r){return!1}bindDescriptor(t,r,s){throw new Error('Assertion failed: calling "bindDescriptor" on a virtual descriptor is unsupported')}getResolutionDependencies(t,r){throw new Error('Assertion failed: calling "getResolutionDependencies" on a virtual descriptor is unsupported')}async getCandidates(t,r,s){throw new Error('Assertion failed: calling "getCandidates" on a virtual descriptor is unsupported')}async getSatisfying(t,r,s,a){throw new Error('Assertion failed: calling "getSatisfying" on a virtual descriptor is unsupported')}async resolve(t,r){throw new Error('Assertion failed: calling "resolve" on a virtual locator is unsupported')}}});var AI,w8=Xe(()=>{Dt();$g();AI=class{supports(t){return!!t.reference.startsWith(Ii.protocol)}getLocalPath(t,r){return this.getWorkspace(t,r).cwd}async fetch(t,r){let s=this.getWorkspace(t,r).cwd;return{packageFs:new bn(s),prefixPath:vt.dot,localPath:s}}getWorkspace(t,r){return r.project.getWorkspaceByCwd(t.reference.slice(Ii.protocol.length))}}});function LB(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function Oce(e){return typeof e>"u"?3:LB(e)?0:Array.isArray(e)?1:2}function S8(e,t){return Object.hasOwn(e,t)}function tZe(e){return LB(e)&&S8(e,"onConflict")&&typeof e.onConflict=="string"}function rZe(e){if(typeof e>"u")return{onConflict:"default",value:e};if(!tZe(e))return{onConflict:"default",value:e};if(S8(e,"value"))return e;let{onConflict:t,...r}=e;return{onConflict:t,value:r}}function Lce(e,t){let r=LB(e)&&S8(e,t)?e[t]:void 0;return rZe(r)}function pI(e,t){return[e,t,Mce]}function D8(e){return Array.isArray(e)?e[2]===Mce:!1}function B8(e,t){if(LB(e)){let r={};for(let s of Object.keys(e))r[s]=B8(e[s],t);return pI(t,r)}return Array.isArray(e)?pI(t,e.map(r=>B8(r,t))):pI(t,e)}function v8(e,t,r,s,a){let n,c=[],f=a,p=0;for(let E=a-1;E>=s;--E){let[C,S]=e[E],{onConflict:x,value:I}=Lce(S,r),T=Oce(I);if(T!==3){if(n??=T,T!==n||x==="hardReset"){p=f;break}if(T===2)return pI(C,I);if(c.unshift([C,I]),x==="reset"){p=E;break}x==="extend"&&E===s&&(s=0),f=E}}if(typeof n>"u")return null;let h=c.map(([E])=>E).join(", ");switch(n){case 1:return pI(h,new Array().concat(...c.map(([E,C])=>C.map(S=>B8(S,E)))));case 0:{let E=Object.assign({},...c.map(([,T])=>T)),C=Object.keys(E),S={},x=e.map(([T,O])=>[T,Lce(O,r).value]),I=eZe(x,([T,O])=>{let U=Oce(O);return U!==0&&U!==3});if(I!==-1){let T=x.slice(I+1);for(let O of C)S[O]=v8(T,t,O,0,T.length)}else for(let T of C)S[T]=v8(x,t,T,p,x.length);return pI(h,S)}default:throw new Error("Assertion failed: Non-extendable value type")}}function Uce(e){return v8(e.map(([t,r])=>[t,{".":r}]),[],".",0,e.length)}function MB(e){return D8(e)?e[1]:e}function FQ(e){let t=D8(e)?e[1]:e;if(Array.isArray(t))return t.map(r=>FQ(r));if(LB(t)){let r={};for(let[s,a]of Object.entries(t))r[s]=FQ(a);return r}return t}function b8(e){return D8(e)?e[0]:null}var eZe,Mce,_ce=Xe(()=>{eZe=(e,t,r)=>{let s=[...e];return s.reverse(),s.findIndex(t,r)};Mce=Symbol()});var NQ={};Vt(NQ,{getDefaultGlobalFolder:()=>x8,getHomeFolder:()=>hI,isFolderInside:()=>k8});function x8(){if(process.platform==="win32"){let e=fe.toPortablePath(process.env.LOCALAPPDATA||fe.join((0,P8.homedir)(),"AppData","Local"));return J.resolve(e,"Yarn/Berry")}if(process.env.XDG_DATA_HOME){let e=fe.toPortablePath(process.env.XDG_DATA_HOME);return J.resolve(e,"yarn/berry")}return J.resolve(hI(),".yarn/berry")}function hI(){return fe.toPortablePath((0,P8.homedir)()||"/usr/local/share")}function k8(e,t){let r=J.relative(t,e);return r&&!r.startsWith("..")&&!J.isAbsolute(r)}var P8,OQ=Xe(()=>{Dt();P8=Ie("os")});var Gce=G((NNt,jce)=>{"use strict";var Q8=Ie("https"),R8=Ie("http"),{URL:Hce}=Ie("url"),T8=class extends R8.Agent{constructor(t){let{proxy:r,proxyRequestOptions:s,...a}=t;super(a),this.proxy=typeof r=="string"?new Hce(r):r,this.proxyRequestOptions=s||{}}createConnection(t,r){let s={...this.proxyRequestOptions,method:"CONNECT",host:this.proxy.hostname,port:this.proxy.port,path:`${t.host}:${t.port}`,setHost:!1,headers:{...this.proxyRequestOptions.headers,connection:this.keepAlive?"keep-alive":"close",host:`${t.host}:${t.port}`},agent:!1,timeout:t.timeout||0};if(this.proxy.username||this.proxy.password){let n=Buffer.from(`${decodeURIComponent(this.proxy.username||"")}:${decodeURIComponent(this.proxy.password||"")}`).toString("base64");s.headers["proxy-authorization"]=`Basic ${n}`}this.proxy.protocol==="https:"&&(s.servername=this.proxy.hostname);let a=(this.proxy.protocol==="http:"?R8:Q8).request(s);a.once("connect",(n,c,f)=>{a.removeAllListeners(),c.removeAllListeners(),n.statusCode===200?r(null,c):(c.destroy(),r(new Error(`Bad response: ${n.statusCode}`),null))}),a.once("timeout",()=>{a.destroy(new Error("Proxy timeout"))}),a.once("error",n=>{a.removeAllListeners(),r(n,null)}),a.end()}},F8=class extends Q8.Agent{constructor(t){let{proxy:r,proxyRequestOptions:s,...a}=t;super(a),this.proxy=typeof r=="string"?new Hce(r):r,this.proxyRequestOptions=s||{}}createConnection(t,r){let s={...this.proxyRequestOptions,method:"CONNECT",host:this.proxy.hostname,port:this.proxy.port,path:`${t.host}:${t.port}`,setHost:!1,headers:{...this.proxyRequestOptions.headers,connection:this.keepAlive?"keep-alive":"close",host:`${t.host}:${t.port}`},agent:!1,timeout:t.timeout||0};if(this.proxy.username||this.proxy.password){let n=Buffer.from(`${decodeURIComponent(this.proxy.username||"")}:${decodeURIComponent(this.proxy.password||"")}`).toString("base64");s.headers["proxy-authorization"]=`Basic ${n}`}this.proxy.protocol==="https:"&&(s.servername=this.proxy.hostname);let a=(this.proxy.protocol==="http:"?R8:Q8).request(s);a.once("connect",(n,c,f)=>{if(a.removeAllListeners(),c.removeAllListeners(),n.statusCode===200){let p=super.createConnection({...t,socket:c});r(null,p)}else c.destroy(),r(new Error(`Bad response: ${n.statusCode}`),null)}),a.once("timeout",()=>{a.destroy(new Error("Proxy timeout"))}),a.once("error",n=>{a.removeAllListeners(),r(n,null)}),a.end()}};jce.exports={HttpProxyAgent:T8,HttpsProxyAgent:F8}});var N8,qce,Wce,Yce=Xe(()=>{N8=et(Gce(),1),qce=N8.default.HttpProxyAgent,Wce=N8.default.HttpsProxyAgent});var Lp=G((Op,LQ)=>{"use strict";Object.defineProperty(Op,"__esModule",{value:!0});var Vce=["Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Uint16Array","Int32Array","Uint32Array","Float32Array","Float64Array","BigInt64Array","BigUint64Array"];function iZe(e){return Vce.includes(e)}var sZe=["Function","Generator","AsyncGenerator","GeneratorFunction","AsyncGeneratorFunction","AsyncFunction","Observable","Array","Buffer","Blob","Object","RegExp","Date","Error","Map","Set","WeakMap","WeakSet","ArrayBuffer","SharedArrayBuffer","DataView","Promise","URL","FormData","URLSearchParams","HTMLElement",...Vce];function oZe(e){return sZe.includes(e)}var aZe=["null","undefined","string","number","bigint","boolean","symbol"];function lZe(e){return aZe.includes(e)}function dI(e){return t=>typeof t===e}var{toString:Jce}=Object.prototype,UB=e=>{let t=Jce.call(e).slice(8,-1);if(/HTML\w+Element/.test(t)&&Pe.domElement(e))return"HTMLElement";if(oZe(t))return t},hi=e=>t=>UB(t)===e;function Pe(e){if(e===null)return"null";switch(typeof e){case"undefined":return"undefined";case"string":return"string";case"number":return"number";case"boolean":return"boolean";case"function":return"Function";case"bigint":return"bigint";case"symbol":return"symbol";default:}if(Pe.observable(e))return"Observable";if(Pe.array(e))return"Array";if(Pe.buffer(e))return"Buffer";let t=UB(e);if(t)return t;if(e instanceof String||e instanceof Boolean||e instanceof Number)throw new TypeError("Please don't use object wrappers for primitive types");return"Object"}Pe.undefined=dI("undefined");Pe.string=dI("string");var cZe=dI("number");Pe.number=e=>cZe(e)&&!Pe.nan(e);Pe.bigint=dI("bigint");Pe.function_=dI("function");Pe.null_=e=>e===null;Pe.class_=e=>Pe.function_(e)&&e.toString().startsWith("class ");Pe.boolean=e=>e===!0||e===!1;Pe.symbol=dI("symbol");Pe.numericString=e=>Pe.string(e)&&!Pe.emptyStringOrWhitespace(e)&&!Number.isNaN(Number(e));Pe.array=(e,t)=>Array.isArray(e)?Pe.function_(t)?e.every(t):!0:!1;Pe.buffer=e=>{var t,r,s,a;return(a=(s=(r=(t=e)===null||t===void 0?void 0:t.constructor)===null||r===void 0?void 0:r.isBuffer)===null||s===void 0?void 0:s.call(r,e))!==null&&a!==void 0?a:!1};Pe.blob=e=>hi("Blob")(e);Pe.nullOrUndefined=e=>Pe.null_(e)||Pe.undefined(e);Pe.object=e=>!Pe.null_(e)&&(typeof e=="object"||Pe.function_(e));Pe.iterable=e=>{var t;return Pe.function_((t=e)===null||t===void 0?void 0:t[Symbol.iterator])};Pe.asyncIterable=e=>{var t;return Pe.function_((t=e)===null||t===void 0?void 0:t[Symbol.asyncIterator])};Pe.generator=e=>{var t,r;return Pe.iterable(e)&&Pe.function_((t=e)===null||t===void 0?void 0:t.next)&&Pe.function_((r=e)===null||r===void 0?void 0:r.throw)};Pe.asyncGenerator=e=>Pe.asyncIterable(e)&&Pe.function_(e.next)&&Pe.function_(e.throw);Pe.nativePromise=e=>hi("Promise")(e);var uZe=e=>{var t,r;return Pe.function_((t=e)===null||t===void 0?void 0:t.then)&&Pe.function_((r=e)===null||r===void 0?void 0:r.catch)};Pe.promise=e=>Pe.nativePromise(e)||uZe(e);Pe.generatorFunction=hi("GeneratorFunction");Pe.asyncGeneratorFunction=e=>UB(e)==="AsyncGeneratorFunction";Pe.asyncFunction=e=>UB(e)==="AsyncFunction";Pe.boundFunction=e=>Pe.function_(e)&&!e.hasOwnProperty("prototype");Pe.regExp=hi("RegExp");Pe.date=hi("Date");Pe.error=hi("Error");Pe.map=e=>hi("Map")(e);Pe.set=e=>hi("Set")(e);Pe.weakMap=e=>hi("WeakMap")(e);Pe.weakSet=e=>hi("WeakSet")(e);Pe.int8Array=hi("Int8Array");Pe.uint8Array=hi("Uint8Array");Pe.uint8ClampedArray=hi("Uint8ClampedArray");Pe.int16Array=hi("Int16Array");Pe.uint16Array=hi("Uint16Array");Pe.int32Array=hi("Int32Array");Pe.uint32Array=hi("Uint32Array");Pe.float32Array=hi("Float32Array");Pe.float64Array=hi("Float64Array");Pe.bigInt64Array=hi("BigInt64Array");Pe.bigUint64Array=hi("BigUint64Array");Pe.arrayBuffer=hi("ArrayBuffer");Pe.sharedArrayBuffer=hi("SharedArrayBuffer");Pe.dataView=hi("DataView");Pe.enumCase=(e,t)=>Object.values(t).includes(e);Pe.directInstanceOf=(e,t)=>Object.getPrototypeOf(e)===t.prototype;Pe.urlInstance=e=>hi("URL")(e);Pe.urlString=e=>{if(!Pe.string(e))return!1;try{return new URL(e),!0}catch{return!1}};Pe.truthy=e=>!!e;Pe.falsy=e=>!e;Pe.nan=e=>Number.isNaN(e);Pe.primitive=e=>Pe.null_(e)||lZe(typeof e);Pe.integer=e=>Number.isInteger(e);Pe.safeInteger=e=>Number.isSafeInteger(e);Pe.plainObject=e=>{if(Jce.call(e)!=="[object Object]")return!1;let t=Object.getPrototypeOf(e);return t===null||t===Object.getPrototypeOf({})};Pe.typedArray=e=>iZe(UB(e));var fZe=e=>Pe.safeInteger(e)&&e>=0;Pe.arrayLike=e=>!Pe.nullOrUndefined(e)&&!Pe.function_(e)&&fZe(e.length);Pe.inRange=(e,t)=>{if(Pe.number(t))return e>=Math.min(0,t)&&e<=Math.max(t,0);if(Pe.array(t)&&t.length===2)return e>=Math.min(...t)&&e<=Math.max(...t);throw new TypeError(`Invalid range: ${JSON.stringify(t)}`)};var AZe=1,pZe=["innerHTML","ownerDocument","style","attributes","nodeValue"];Pe.domElement=e=>Pe.object(e)&&e.nodeType===AZe&&Pe.string(e.nodeName)&&!Pe.plainObject(e)&&pZe.every(t=>t in e);Pe.observable=e=>{var t,r,s,a;return e?e===((r=(t=e)[Symbol.observable])===null||r===void 0?void 0:r.call(t))||e===((a=(s=e)["@@observable"])===null||a===void 0?void 0:a.call(s)):!1};Pe.nodeStream=e=>Pe.object(e)&&Pe.function_(e.pipe)&&!Pe.observable(e);Pe.infinite=e=>e===1/0||e===-1/0;var Kce=e=>t=>Pe.integer(t)&&Math.abs(t%2)===e;Pe.evenInteger=Kce(0);Pe.oddInteger=Kce(1);Pe.emptyArray=e=>Pe.array(e)&&e.length===0;Pe.nonEmptyArray=e=>Pe.array(e)&&e.length>0;Pe.emptyString=e=>Pe.string(e)&&e.length===0;var hZe=e=>Pe.string(e)&&!/\S/.test(e);Pe.emptyStringOrWhitespace=e=>Pe.emptyString(e)||hZe(e);Pe.nonEmptyString=e=>Pe.string(e)&&e.length>0;Pe.nonEmptyStringAndNotWhitespace=e=>Pe.string(e)&&!Pe.emptyStringOrWhitespace(e);Pe.emptyObject=e=>Pe.object(e)&&!Pe.map(e)&&!Pe.set(e)&&Object.keys(e).length===0;Pe.nonEmptyObject=e=>Pe.object(e)&&!Pe.map(e)&&!Pe.set(e)&&Object.keys(e).length>0;Pe.emptySet=e=>Pe.set(e)&&e.size===0;Pe.nonEmptySet=e=>Pe.set(e)&&e.size>0;Pe.emptyMap=e=>Pe.map(e)&&e.size===0;Pe.nonEmptyMap=e=>Pe.map(e)&&e.size>0;Pe.propertyKey=e=>Pe.any([Pe.string,Pe.number,Pe.symbol],e);Pe.formData=e=>hi("FormData")(e);Pe.urlSearchParams=e=>hi("URLSearchParams")(e);var zce=(e,t,r)=>{if(!Pe.function_(t))throw new TypeError(`Invalid predicate: ${JSON.stringify(t)}`);if(r.length===0)throw new TypeError("Invalid number of values");return e.call(r,t)};Pe.any=(e,...t)=>(Pe.array(e)?e:[e]).some(s=>zce(Array.prototype.some,s,t));Pe.all=(e,...t)=>zce(Array.prototype.every,e,t);var Ht=(e,t,r,s={})=>{if(!e){let{multipleValues:a}=s,n=a?`received values of types ${[...new Set(r.map(c=>`\`${Pe(c)}\``))].join(", ")}`:`received value of type \`${Pe(r)}\``;throw new TypeError(`Expected value which is \`${t}\`, ${n}.`)}};Op.assert={undefined:e=>Ht(Pe.undefined(e),"undefined",e),string:e=>Ht(Pe.string(e),"string",e),number:e=>Ht(Pe.number(e),"number",e),bigint:e=>Ht(Pe.bigint(e),"bigint",e),function_:e=>Ht(Pe.function_(e),"Function",e),null_:e=>Ht(Pe.null_(e),"null",e),class_:e=>Ht(Pe.class_(e),"Class",e),boolean:e=>Ht(Pe.boolean(e),"boolean",e),symbol:e=>Ht(Pe.symbol(e),"symbol",e),numericString:e=>Ht(Pe.numericString(e),"string with a number",e),array:(e,t)=>{Ht(Pe.array(e),"Array",e),t&&e.forEach(t)},buffer:e=>Ht(Pe.buffer(e),"Buffer",e),blob:e=>Ht(Pe.blob(e),"Blob",e),nullOrUndefined:e=>Ht(Pe.nullOrUndefined(e),"null or undefined",e),object:e=>Ht(Pe.object(e),"Object",e),iterable:e=>Ht(Pe.iterable(e),"Iterable",e),asyncIterable:e=>Ht(Pe.asyncIterable(e),"AsyncIterable",e),generator:e=>Ht(Pe.generator(e),"Generator",e),asyncGenerator:e=>Ht(Pe.asyncGenerator(e),"AsyncGenerator",e),nativePromise:e=>Ht(Pe.nativePromise(e),"native Promise",e),promise:e=>Ht(Pe.promise(e),"Promise",e),generatorFunction:e=>Ht(Pe.generatorFunction(e),"GeneratorFunction",e),asyncGeneratorFunction:e=>Ht(Pe.asyncGeneratorFunction(e),"AsyncGeneratorFunction",e),asyncFunction:e=>Ht(Pe.asyncFunction(e),"AsyncFunction",e),boundFunction:e=>Ht(Pe.boundFunction(e),"Function",e),regExp:e=>Ht(Pe.regExp(e),"RegExp",e),date:e=>Ht(Pe.date(e),"Date",e),error:e=>Ht(Pe.error(e),"Error",e),map:e=>Ht(Pe.map(e),"Map",e),set:e=>Ht(Pe.set(e),"Set",e),weakMap:e=>Ht(Pe.weakMap(e),"WeakMap",e),weakSet:e=>Ht(Pe.weakSet(e),"WeakSet",e),int8Array:e=>Ht(Pe.int8Array(e),"Int8Array",e),uint8Array:e=>Ht(Pe.uint8Array(e),"Uint8Array",e),uint8ClampedArray:e=>Ht(Pe.uint8ClampedArray(e),"Uint8ClampedArray",e),int16Array:e=>Ht(Pe.int16Array(e),"Int16Array",e),uint16Array:e=>Ht(Pe.uint16Array(e),"Uint16Array",e),int32Array:e=>Ht(Pe.int32Array(e),"Int32Array",e),uint32Array:e=>Ht(Pe.uint32Array(e),"Uint32Array",e),float32Array:e=>Ht(Pe.float32Array(e),"Float32Array",e),float64Array:e=>Ht(Pe.float64Array(e),"Float64Array",e),bigInt64Array:e=>Ht(Pe.bigInt64Array(e),"BigInt64Array",e),bigUint64Array:e=>Ht(Pe.bigUint64Array(e),"BigUint64Array",e),arrayBuffer:e=>Ht(Pe.arrayBuffer(e),"ArrayBuffer",e),sharedArrayBuffer:e=>Ht(Pe.sharedArrayBuffer(e),"SharedArrayBuffer",e),dataView:e=>Ht(Pe.dataView(e),"DataView",e),enumCase:(e,t)=>Ht(Pe.enumCase(e,t),"EnumCase",e),urlInstance:e=>Ht(Pe.urlInstance(e),"URL",e),urlString:e=>Ht(Pe.urlString(e),"string with a URL",e),truthy:e=>Ht(Pe.truthy(e),"truthy",e),falsy:e=>Ht(Pe.falsy(e),"falsy",e),nan:e=>Ht(Pe.nan(e),"NaN",e),primitive:e=>Ht(Pe.primitive(e),"primitive",e),integer:e=>Ht(Pe.integer(e),"integer",e),safeInteger:e=>Ht(Pe.safeInteger(e),"integer",e),plainObject:e=>Ht(Pe.plainObject(e),"plain object",e),typedArray:e=>Ht(Pe.typedArray(e),"TypedArray",e),arrayLike:e=>Ht(Pe.arrayLike(e),"array-like",e),domElement:e=>Ht(Pe.domElement(e),"HTMLElement",e),observable:e=>Ht(Pe.observable(e),"Observable",e),nodeStream:e=>Ht(Pe.nodeStream(e),"Node.js Stream",e),infinite:e=>Ht(Pe.infinite(e),"infinite number",e),emptyArray:e=>Ht(Pe.emptyArray(e),"empty array",e),nonEmptyArray:e=>Ht(Pe.nonEmptyArray(e),"non-empty array",e),emptyString:e=>Ht(Pe.emptyString(e),"empty string",e),emptyStringOrWhitespace:e=>Ht(Pe.emptyStringOrWhitespace(e),"empty string or whitespace",e),nonEmptyString:e=>Ht(Pe.nonEmptyString(e),"non-empty string",e),nonEmptyStringAndNotWhitespace:e=>Ht(Pe.nonEmptyStringAndNotWhitespace(e),"non-empty string and not whitespace",e),emptyObject:e=>Ht(Pe.emptyObject(e),"empty object",e),nonEmptyObject:e=>Ht(Pe.nonEmptyObject(e),"non-empty object",e),emptySet:e=>Ht(Pe.emptySet(e),"empty set",e),nonEmptySet:e=>Ht(Pe.nonEmptySet(e),"non-empty set",e),emptyMap:e=>Ht(Pe.emptyMap(e),"empty map",e),nonEmptyMap:e=>Ht(Pe.nonEmptyMap(e),"non-empty map",e),propertyKey:e=>Ht(Pe.propertyKey(e),"PropertyKey",e),formData:e=>Ht(Pe.formData(e),"FormData",e),urlSearchParams:e=>Ht(Pe.urlSearchParams(e),"URLSearchParams",e),evenInteger:e=>Ht(Pe.evenInteger(e),"even integer",e),oddInteger:e=>Ht(Pe.oddInteger(e),"odd integer",e),directInstanceOf:(e,t)=>Ht(Pe.directInstanceOf(e,t),"T",e),inRange:(e,t)=>Ht(Pe.inRange(e,t),"in range",e),any:(e,...t)=>Ht(Pe.any(e,...t),"predicate returns truthy for any value",t,{multipleValues:!0}),all:(e,...t)=>Ht(Pe.all(e,...t),"predicate returns truthy for all values",t,{multipleValues:!0})};Object.defineProperties(Pe,{class:{value:Pe.class_},function:{value:Pe.function_},null:{value:Pe.null_}});Object.defineProperties(Op.assert,{class:{value:Op.assert.class_},function:{value:Op.assert.function_},null:{value:Op.assert.null_}});Op.default=Pe;LQ.exports=Pe;LQ.exports.default=Pe;LQ.exports.assert=Op.assert});var Xce=G((LNt,O8)=>{"use strict";var MQ=class extends Error{constructor(t){super(t||"Promise was canceled"),this.name="CancelError"}get isCanceled(){return!0}},UQ=class e{static fn(t){return(...r)=>new e((s,a,n)=>{r.push(n),t(...r).then(s,a)})}constructor(t){this._cancelHandlers=[],this._isPending=!0,this._isCanceled=!1,this._rejectOnCancel=!0,this._promise=new Promise((r,s)=>{this._reject=s;let a=f=>{this._isPending=!1,r(f)},n=f=>{this._isPending=!1,s(f)},c=f=>{if(!this._isPending)throw new Error("The `onCancel` handler was attached after the promise settled.");this._cancelHandlers.push(f)};return Object.defineProperties(c,{shouldReject:{get:()=>this._rejectOnCancel,set:f=>{this._rejectOnCancel=f}}}),t(a,n,c)})}then(t,r){return this._promise.then(t,r)}catch(t){return this._promise.catch(t)}finally(t){return this._promise.finally(t)}cancel(t){if(!(!this._isPending||this._isCanceled)){if(this._cancelHandlers.length>0)try{for(let r of this._cancelHandlers)r()}catch(r){this._reject(r)}this._isCanceled=!0,this._rejectOnCancel&&this._reject(new MQ(t))}}get isCanceled(){return this._isCanceled}};Object.setPrototypeOf(UQ.prototype,Promise.prototype);O8.exports=UQ;O8.exports.CancelError=MQ});var Zce=G((M8,U8)=>{"use strict";Object.defineProperty(M8,"__esModule",{value:!0});function dZe(e){return e.encrypted}var L8=(e,t)=>{let r;typeof t=="function"?r={connect:t}:r=t;let s=typeof r.connect=="function",a=typeof r.secureConnect=="function",n=typeof r.close=="function",c=()=>{s&&r.connect(),dZe(e)&&a&&(e.authorized?r.secureConnect():e.authorizationError||e.once("secureConnect",r.secureConnect)),n&&e.once("close",r.close)};e.writable&&!e.connecting?c():e.connecting?e.once("connect",c):e.destroyed&&n&&r.close(e._hadError)};M8.default=L8;U8.exports=L8;U8.exports.default=L8});var $ce=G((H8,j8)=>{"use strict";Object.defineProperty(H8,"__esModule",{value:!0});var gZe=Zce(),mZe=Number(process.versions.node.split(".")[0]),_8=e=>{let t={start:Date.now(),socket:void 0,lookup:void 0,connect:void 0,secureConnect:void 0,upload:void 0,response:void 0,end:void 0,error:void 0,abort:void 0,phases:{wait:void 0,dns:void 0,tcp:void 0,tls:void 0,request:void 0,firstByte:void 0,download:void 0,total:void 0}};e.timings=t;let r=c=>{let f=c.emit.bind(c);c.emit=(p,...h)=>(p==="error"&&(t.error=Date.now(),t.phases.total=t.error-t.start,c.emit=f),f(p,...h))};r(e),e.prependOnceListener("abort",()=>{t.abort=Date.now(),(!t.response||mZe>=13)&&(t.phases.total=Date.now()-t.start)});let s=c=>{t.socket=Date.now(),t.phases.wait=t.socket-t.start;let f=()=>{t.lookup=Date.now(),t.phases.dns=t.lookup-t.socket};c.prependOnceListener("lookup",f),gZe.default(c,{connect:()=>{t.connect=Date.now(),t.lookup===void 0&&(c.removeListener("lookup",f),t.lookup=t.connect,t.phases.dns=t.lookup-t.socket),t.phases.tcp=t.connect-t.lookup},secureConnect:()=>{t.secureConnect=Date.now(),t.phases.tls=t.secureConnect-t.connect}})};e.socket?s(e.socket):e.prependOnceListener("socket",s);let a=()=>{var c;t.upload=Date.now(),t.phases.request=t.upload-(c=t.secureConnect,c??t.connect)};return(typeof e.writableFinished=="boolean"?e.writableFinished:e.finished&&e.outputSize===0&&(!e.socket||e.socket.writableLength===0))?a():e.prependOnceListener("finish",a),e.prependOnceListener("response",c=>{t.response=Date.now(),t.phases.firstByte=t.response-t.upload,c.timings=t,r(c),c.prependOnceListener("end",()=>{t.end=Date.now(),t.phases.download=t.end-t.response,t.phases.total=t.end-t.start})}),t};H8.default=_8;j8.exports=_8;j8.exports.default=_8});var oue=G((MNt,W8)=>{"use strict";var{V4MAPPED:yZe,ADDRCONFIG:EZe,ALL:sue,promises:{Resolver:eue},lookup:IZe}=Ie("dns"),{promisify:G8}=Ie("util"),CZe=Ie("os"),gI=Symbol("cacheableLookupCreateConnection"),q8=Symbol("cacheableLookupInstance"),tue=Symbol("expires"),wZe=typeof sue=="number",rue=e=>{if(!(e&&typeof e.createConnection=="function"))throw new Error("Expected an Agent instance as the first argument")},BZe=e=>{for(let t of e)t.family!==6&&(t.address=`::ffff:${t.address}`,t.family=6)},nue=()=>{let e=!1,t=!1;for(let r of Object.values(CZe.networkInterfaces()))for(let s of r)if(!s.internal&&(s.family==="IPv6"?t=!0:e=!0,e&&t))return{has4:e,has6:t};return{has4:e,has6:t}},vZe=e=>Symbol.iterator in e,iue={ttl:!0},SZe={all:!0},_Q=class{constructor({cache:t=new Map,maxTtl:r=1/0,fallbackDuration:s=3600,errorTtl:a=.15,resolver:n=new eue,lookup:c=IZe}={}){if(this.maxTtl=r,this.errorTtl=a,this._cache=t,this._resolver=n,this._dnsLookup=G8(c),this._resolver instanceof eue?(this._resolve4=this._resolver.resolve4.bind(this._resolver),this._resolve6=this._resolver.resolve6.bind(this._resolver)):(this._resolve4=G8(this._resolver.resolve4.bind(this._resolver)),this._resolve6=G8(this._resolver.resolve6.bind(this._resolver))),this._iface=nue(),this._pending={},this._nextRemovalTime=!1,this._hostnamesToFallback=new Set,s<1)this._fallback=!1;else{this._fallback=!0;let f=setInterval(()=>{this._hostnamesToFallback.clear()},s*1e3);f.unref&&f.unref()}this.lookup=this.lookup.bind(this),this.lookupAsync=this.lookupAsync.bind(this)}set servers(t){this.clear(),this._resolver.setServers(t)}get servers(){return this._resolver.getServers()}lookup(t,r,s){if(typeof r=="function"?(s=r,r={}):typeof r=="number"&&(r={family:r}),!s)throw new Error("Callback must be a function.");this.lookupAsync(t,r).then(a=>{r.all?s(null,a):s(null,a.address,a.family,a.expires,a.ttl)},s)}async lookupAsync(t,r={}){typeof r=="number"&&(r={family:r});let s=await this.query(t);if(r.family===6){let a=s.filter(n=>n.family===6);r.hints&yZe&&(wZe&&r.hints&sue||a.length===0)?BZe(s):s=a}else r.family===4&&(s=s.filter(a=>a.family===4));if(r.hints&EZe){let{_iface:a}=this;s=s.filter(n=>n.family===6?a.has6:a.has4)}if(s.length===0){let a=new Error(`cacheableLookup ENOTFOUND ${t}`);throw a.code="ENOTFOUND",a.hostname=t,a}return r.all?s:s[0]}async query(t){let r=await this._cache.get(t);if(!r){let s=this._pending[t];if(s)r=await s;else{let a=this.queryAndCache(t);this._pending[t]=a,r=await a}}return r=r.map(s=>({...s})),r}async _resolve(t){let r=async h=>{try{return await h}catch(E){if(E.code==="ENODATA"||E.code==="ENOTFOUND")return[];throw E}},[s,a]=await Promise.all([this._resolve4(t,iue),this._resolve6(t,iue)].map(h=>r(h))),n=0,c=0,f=0,p=Date.now();for(let h of s)h.family=4,h.expires=p+h.ttl*1e3,n=Math.max(n,h.ttl);for(let h of a)h.family=6,h.expires=p+h.ttl*1e3,c=Math.max(c,h.ttl);return s.length>0?a.length>0?f=Math.min(n,c):f=n:f=c,{entries:[...s,...a],cacheTtl:f}}async _lookup(t){try{return{entries:await this._dnsLookup(t,{all:!0}),cacheTtl:0}}catch{return{entries:[],cacheTtl:0}}}async _set(t,r,s){if(this.maxTtl>0&&s>0){s=Math.min(s,this.maxTtl)*1e3,r[tue]=Date.now()+s;try{await this._cache.set(t,r,s)}catch(a){this.lookupAsync=async()=>{let n=new Error("Cache Error. Please recreate the CacheableLookup instance.");throw n.cause=a,n}}vZe(this._cache)&&this._tick(s)}}async queryAndCache(t){if(this._hostnamesToFallback.has(t))return this._dnsLookup(t,SZe);try{let r=await this._resolve(t);r.entries.length===0&&this._fallback&&(r=await this._lookup(t),r.entries.length!==0&&this._hostnamesToFallback.add(t));let s=r.entries.length===0?this.errorTtl:r.cacheTtl;return await this._set(t,r.entries,s),delete this._pending[t],r.entries}catch(r){throw delete this._pending[t],r}}_tick(t){let r=this._nextRemovalTime;(!r||t{this._nextRemovalTime=!1;let s=1/0,a=Date.now();for(let[n,c]of this._cache){let f=c[tue];a>=f?this._cache.delete(n):f("lookup"in r||(r.lookup=this.lookup),t[gI](r,s))}uninstall(t){if(rue(t),t[gI]){if(t[q8]!==this)throw new Error("The agent is not owned by this CacheableLookup instance");t.createConnection=t[gI],delete t[gI],delete t[q8]}}updateInterfaceInfo(){let{_iface:t}=this;this._iface=nue(),(t.has4&&!this._iface.has4||t.has6&&!this._iface.has6)&&this._cache.clear()}clear(t){if(t){this._cache.delete(t);return}this._cache.clear()}};W8.exports=_Q;W8.exports.default=_Q});var cue=G((UNt,Y8)=>{"use strict";var DZe=typeof URL>"u"?Ie("url").URL:URL,bZe="text/plain",PZe="us-ascii",aue=(e,t)=>t.some(r=>r instanceof RegExp?r.test(e):r===e),xZe=(e,{stripHash:t})=>{let r=e.match(/^data:([^,]*?),([^#]*?)(?:#(.*))?$/);if(!r)throw new Error(`Invalid URL: ${e}`);let s=r[1].split(";"),a=r[2],n=t?"":r[3],c=!1;s[s.length-1]==="base64"&&(s.pop(),c=!0);let f=(s.shift()||"").toLowerCase(),h=[...s.map(E=>{let[C,S=""]=E.split("=").map(x=>x.trim());return C==="charset"&&(S=S.toLowerCase(),S===PZe)?"":`${C}${S?`=${S}`:""}`}).filter(Boolean)];return c&&h.push("base64"),(h.length!==0||f&&f!==bZe)&&h.unshift(f),`data:${h.join(";")},${c?a.trim():a}${n?`#${n}`:""}`},lue=(e,t)=>{if(t={defaultProtocol:"http:",normalizeProtocol:!0,forceHttp:!1,forceHttps:!1,stripAuthentication:!0,stripHash:!1,stripWWW:!0,removeQueryParameters:[/^utm_\w+/i],removeTrailingSlash:!0,removeDirectoryIndex:!1,sortQueryParameters:!0,...t},Reflect.has(t,"normalizeHttps"))throw new Error("options.normalizeHttps is renamed to options.forceHttp");if(Reflect.has(t,"normalizeHttp"))throw new Error("options.normalizeHttp is renamed to options.forceHttps");if(Reflect.has(t,"stripFragment"))throw new Error("options.stripFragment is renamed to options.stripHash");if(e=e.trim(),/^data:/i.test(e))return xZe(e,t);let r=e.startsWith("//");!r&&/^\.*\//.test(e)||(e=e.replace(/^(?!(?:\w+:)?\/\/)|^\/\//,t.defaultProtocol));let a=new DZe(e);if(t.forceHttp&&t.forceHttps)throw new Error("The `forceHttp` and `forceHttps` options cannot be used together");if(t.forceHttp&&a.protocol==="https:"&&(a.protocol="http:"),t.forceHttps&&a.protocol==="http:"&&(a.protocol="https:"),t.stripAuthentication&&(a.username="",a.password=""),t.stripHash&&(a.hash=""),a.pathname&&(a.pathname=a.pathname.replace(/((?!:).|^)\/{2,}/g,(n,c)=>/^(?!\/)/g.test(c)?`${c}/`:"/")),a.pathname&&(a.pathname=decodeURI(a.pathname)),t.removeDirectoryIndex===!0&&(t.removeDirectoryIndex=[/^index\.[a-z]+$/]),Array.isArray(t.removeDirectoryIndex)&&t.removeDirectoryIndex.length>0){let n=a.pathname.split("/"),c=n[n.length-1];aue(c,t.removeDirectoryIndex)&&(n=n.slice(0,n.length-1),a.pathname=n.slice(1).join("/")+"/")}if(a.hostname&&(a.hostname=a.hostname.replace(/\.$/,""),t.stripWWW&&/^www\.([a-z\-\d]{2,63})\.([a-z.]{2,5})$/.test(a.hostname)&&(a.hostname=a.hostname.replace(/^www\./,""))),Array.isArray(t.removeQueryParameters))for(let n of[...a.searchParams.keys()])aue(n,t.removeQueryParameters)&&a.searchParams.delete(n);return t.sortQueryParameters&&a.searchParams.sort(),t.removeTrailingSlash&&(a.pathname=a.pathname.replace(/\/$/,"")),e=a.toString(),(t.removeTrailingSlash||a.pathname==="/")&&a.hash===""&&(e=e.replace(/\/$/,"")),r&&!t.normalizeProtocol&&(e=e.replace(/^http:\/\//,"//")),t.stripProtocol&&(e=e.replace(/^(?:https?:)?\/\//,"")),e};Y8.exports=lue;Y8.exports.default=lue});var Aue=G((_Nt,fue)=>{fue.exports=uue;function uue(e,t){if(e&&t)return uue(e)(t);if(typeof e!="function")throw new TypeError("need wrapper function");return Object.keys(e).forEach(function(s){r[s]=e[s]}),r;function r(){for(var s=new Array(arguments.length),a=0;a{var pue=Aue();V8.exports=pue(HQ);V8.exports.strict=pue(hue);HQ.proto=HQ(function(){Object.defineProperty(Function.prototype,"once",{value:function(){return HQ(this)},configurable:!0}),Object.defineProperty(Function.prototype,"onceStrict",{value:function(){return hue(this)},configurable:!0})});function HQ(e){var t=function(){return t.called?t.value:(t.called=!0,t.value=e.apply(this,arguments))};return t.called=!1,t}function hue(e){var t=function(){if(t.called)throw new Error(t.onceError);return t.called=!0,t.value=e.apply(this,arguments)},r=e.name||"Function wrapped with `once`";return t.onceError=r+" shouldn't be called more than once",t.called=!1,t}});var K8=G((jNt,gue)=>{var kZe=J8(),QZe=function(){},RZe=function(e){return e.setHeader&&typeof e.abort=="function"},TZe=function(e){return e.stdio&&Array.isArray(e.stdio)&&e.stdio.length===3},due=function(e,t,r){if(typeof t=="function")return due(e,null,t);t||(t={}),r=kZe(r||QZe);var s=e._writableState,a=e._readableState,n=t.readable||t.readable!==!1&&e.readable,c=t.writable||t.writable!==!1&&e.writable,f=function(){e.writable||p()},p=function(){c=!1,n||r.call(e)},h=function(){n=!1,c||r.call(e)},E=function(I){r.call(e,I?new Error("exited with error code: "+I):null)},C=function(I){r.call(e,I)},S=function(){if(n&&!(a&&a.ended))return r.call(e,new Error("premature close"));if(c&&!(s&&s.ended))return r.call(e,new Error("premature close"))},x=function(){e.req.on("finish",p)};return RZe(e)?(e.on("complete",p),e.on("abort",S),e.req?x():e.on("request",x)):c&&!s&&(e.on("end",f),e.on("close",f)),TZe(e)&&e.on("exit",E),e.on("end",h),e.on("finish",p),t.error!==!1&&e.on("error",C),e.on("close",S),function(){e.removeListener("complete",p),e.removeListener("abort",S),e.removeListener("request",x),e.req&&e.req.removeListener("finish",p),e.removeListener("end",f),e.removeListener("close",f),e.removeListener("finish",p),e.removeListener("exit",E),e.removeListener("end",h),e.removeListener("error",C),e.removeListener("close",S)}};gue.exports=due});var Eue=G((GNt,yue)=>{var FZe=J8(),NZe=K8(),z8=Ie("fs"),_B=function(){},OZe=/^v?\.0/.test(process.version),jQ=function(e){return typeof e=="function"},LZe=function(e){return!OZe||!z8?!1:(e instanceof(z8.ReadStream||_B)||e instanceof(z8.WriteStream||_B))&&jQ(e.close)},MZe=function(e){return e.setHeader&&jQ(e.abort)},UZe=function(e,t,r,s){s=FZe(s);var a=!1;e.on("close",function(){a=!0}),NZe(e,{readable:t,writable:r},function(c){if(c)return s(c);a=!0,s()});var n=!1;return function(c){if(!a&&!n){if(n=!0,LZe(e))return e.close(_B);if(MZe(e))return e.abort();if(jQ(e.destroy))return e.destroy();s(c||new Error("stream was destroyed"))}}},mue=function(e){e()},_Ze=function(e,t){return e.pipe(t)},HZe=function(){var e=Array.prototype.slice.call(arguments),t=jQ(e[e.length-1]||_B)&&e.pop()||_B;if(Array.isArray(e[0])&&(e=e[0]),e.length<2)throw new Error("pump requires two streams per minimum");var r,s=e.map(function(a,n){var c=n0;return UZe(a,c,f,function(p){r||(r=p),p&&s.forEach(mue),!c&&(s.forEach(mue),t(r))})});return e.reduce(_Ze)};yue.exports=HZe});var Cue=G((qNt,Iue)=>{"use strict";var{PassThrough:jZe}=Ie("stream");Iue.exports=e=>{e={...e};let{array:t}=e,{encoding:r}=e,s=r==="buffer",a=!1;t?a=!(r||s):r=r||"utf8",s&&(r=null);let n=new jZe({objectMode:a});r&&n.setEncoding(r);let c=0,f=[];return n.on("data",p=>{f.push(p),a?c=f.length:c+=p.length}),n.getBufferedValue=()=>t?f:s?Buffer.concat(f,c):f.join(""),n.getBufferedLength=()=>c,n}});var wue=G((WNt,mI)=>{"use strict";var GZe=Eue(),qZe=Cue(),GQ=class extends Error{constructor(){super("maxBuffer exceeded"),this.name="MaxBufferError"}};async function qQ(e,t){if(!e)return Promise.reject(new Error("Expected a stream"));t={maxBuffer:1/0,...t};let{maxBuffer:r}=t,s;return await new Promise((a,n)=>{let c=f=>{f&&(f.bufferedData=s.getBufferedValue()),n(f)};s=GZe(e,qZe(t),f=>{if(f){c(f);return}a()}),s.on("data",()=>{s.getBufferedLength()>r&&c(new GQ)})}),s.getBufferedValue()}mI.exports=qQ;mI.exports.default=qQ;mI.exports.buffer=(e,t)=>qQ(e,{...t,encoding:"buffer"});mI.exports.array=(e,t)=>qQ(e,{...t,array:!0});mI.exports.MaxBufferError=GQ});var vue=G((VNt,Bue)=>{"use strict";var WZe=new Set([200,203,204,206,300,301,308,404,405,410,414,501]),YZe=new Set([200,203,204,300,301,302,303,307,308,404,405,410,414,501]),VZe=new Set([500,502,503,504]),JZe={date:!0,connection:!0,"keep-alive":!0,"proxy-authenticate":!0,"proxy-authorization":!0,te:!0,trailer:!0,"transfer-encoding":!0,upgrade:!0},KZe={"content-length":!0,"content-encoding":!0,"transfer-encoding":!0,"content-range":!0};function tm(e){let t=parseInt(e,10);return isFinite(t)?t:0}function zZe(e){return e?VZe.has(e.status):!0}function X8(e){let t={};if(!e)return t;let r=e.trim().split(/,/);for(let s of r){let[a,n]=s.split(/=/,2);t[a.trim()]=n===void 0?!0:n.trim().replace(/^"|"$/g,"")}return t}function XZe(e){let t=[];for(let r in e){let s=e[r];t.push(s===!0?r:r+"="+s)}if(t.length)return t.join(", ")}Bue.exports=class{constructor(t,r,{shared:s,cacheHeuristic:a,immutableMinTimeToLive:n,ignoreCargoCult:c,_fromObject:f}={}){if(f){this._fromObject(f);return}if(!r||!r.headers)throw Error("Response headers missing");this._assertRequestHasHeaders(t),this._responseTime=this.now(),this._isShared=s!==!1,this._cacheHeuristic=a!==void 0?a:.1,this._immutableMinTtl=n!==void 0?n:24*3600*1e3,this._status="status"in r?r.status:200,this._resHeaders=r.headers,this._rescc=X8(r.headers["cache-control"]),this._method="method"in t?t.method:"GET",this._url=t.url,this._host=t.headers.host,this._noAuthorization=!t.headers.authorization,this._reqHeaders=r.headers.vary?t.headers:null,this._reqcc=X8(t.headers["cache-control"]),c&&"pre-check"in this._rescc&&"post-check"in this._rescc&&(delete this._rescc["pre-check"],delete this._rescc["post-check"],delete this._rescc["no-cache"],delete this._rescc["no-store"],delete this._rescc["must-revalidate"],this._resHeaders=Object.assign({},this._resHeaders,{"cache-control":XZe(this._rescc)}),delete this._resHeaders.expires,delete this._resHeaders.pragma),r.headers["cache-control"]==null&&/no-cache/.test(r.headers.pragma)&&(this._rescc["no-cache"]=!0)}now(){return Date.now()}storable(){return!!(!this._reqcc["no-store"]&&(this._method==="GET"||this._method==="HEAD"||this._method==="POST"&&this._hasExplicitExpiration())&&YZe.has(this._status)&&!this._rescc["no-store"]&&(!this._isShared||!this._rescc.private)&&(!this._isShared||this._noAuthorization||this._allowsStoringAuthenticated())&&(this._resHeaders.expires||this._rescc["max-age"]||this._isShared&&this._rescc["s-maxage"]||this._rescc.public||WZe.has(this._status)))}_hasExplicitExpiration(){return this._isShared&&this._rescc["s-maxage"]||this._rescc["max-age"]||this._resHeaders.expires}_assertRequestHasHeaders(t){if(!t||!t.headers)throw Error("Request headers missing")}satisfiesWithoutRevalidation(t){this._assertRequestHasHeaders(t);let r=X8(t.headers["cache-control"]);return r["no-cache"]||/no-cache/.test(t.headers.pragma)||r["max-age"]&&this.age()>r["max-age"]||r["min-fresh"]&&this.timeToLive()<1e3*r["min-fresh"]||this.stale()&&!(r["max-stale"]&&!this._rescc["must-revalidate"]&&(r["max-stale"]===!0||r["max-stale"]>this.age()-this.maxAge()))?!1:this._requestMatches(t,!1)}_requestMatches(t,r){return(!this._url||this._url===t.url)&&this._host===t.headers.host&&(!t.method||this._method===t.method||r&&t.method==="HEAD")&&this._varyMatches(t)}_allowsStoringAuthenticated(){return this._rescc["must-revalidate"]||this._rescc.public||this._rescc["s-maxage"]}_varyMatches(t){if(!this._resHeaders.vary)return!0;if(this._resHeaders.vary==="*")return!1;let r=this._resHeaders.vary.trim().toLowerCase().split(/\s*,\s*/);for(let s of r)if(t.headers[s]!==this._reqHeaders[s])return!1;return!0}_copyWithoutHopByHopHeaders(t){let r={};for(let s in t)JZe[s]||(r[s]=t[s]);if(t.connection){let s=t.connection.trim().split(/\s*,\s*/);for(let a of s)delete r[a]}if(r.warning){let s=r.warning.split(/,/).filter(a=>!/^\s*1[0-9][0-9]/.test(a));s.length?r.warning=s.join(",").trim():delete r.warning}return r}responseHeaders(){let t=this._copyWithoutHopByHopHeaders(this._resHeaders),r=this.age();return r>3600*24&&!this._hasExplicitExpiration()&&this.maxAge()>3600*24&&(t.warning=(t.warning?`${t.warning}, `:"")+'113 - "rfc7234 5.5.4"'),t.age=`${Math.round(r)}`,t.date=new Date(this.now()).toUTCString(),t}date(){let t=Date.parse(this._resHeaders.date);return isFinite(t)?t:this._responseTime}age(){let t=this._ageValue(),r=(this.now()-this._responseTime)/1e3;return t+r}_ageValue(){return tm(this._resHeaders.age)}maxAge(){if(!this.storable()||this._rescc["no-cache"]||this._isShared&&this._resHeaders["set-cookie"]&&!this._rescc.public&&!this._rescc.immutable||this._resHeaders.vary==="*")return 0;if(this._isShared){if(this._rescc["proxy-revalidate"])return 0;if(this._rescc["s-maxage"])return tm(this._rescc["s-maxage"])}if(this._rescc["max-age"])return tm(this._rescc["max-age"]);let t=this._rescc.immutable?this._immutableMinTtl:0,r=this.date();if(this._resHeaders.expires){let s=Date.parse(this._resHeaders.expires);return Number.isNaN(s)||ss)return Math.max(t,(r-s)/1e3*this._cacheHeuristic)}return t}timeToLive(){let t=this.maxAge()-this.age(),r=t+tm(this._rescc["stale-if-error"]),s=t+tm(this._rescc["stale-while-revalidate"]);return Math.max(0,t,r,s)*1e3}stale(){return this.maxAge()<=this.age()}_useStaleIfError(){return this.maxAge()+tm(this._rescc["stale-if-error"])>this.age()}useStaleWhileRevalidate(){return this.maxAge()+tm(this._rescc["stale-while-revalidate"])>this.age()}static fromObject(t){return new this(void 0,void 0,{_fromObject:t})}_fromObject(t){if(this._responseTime)throw Error("Reinitialized");if(!t||t.v!==1)throw Error("Invalid serialization");this._responseTime=t.t,this._isShared=t.sh,this._cacheHeuristic=t.ch,this._immutableMinTtl=t.imm!==void 0?t.imm:24*3600*1e3,this._status=t.st,this._resHeaders=t.resh,this._rescc=t.rescc,this._method=t.m,this._url=t.u,this._host=t.h,this._noAuthorization=t.a,this._reqHeaders=t.reqh,this._reqcc=t.reqcc}toObject(){return{v:1,t:this._responseTime,sh:this._isShared,ch:this._cacheHeuristic,imm:this._immutableMinTtl,st:this._status,resh:this._resHeaders,rescc:this._rescc,m:this._method,u:this._url,h:this._host,a:this._noAuthorization,reqh:this._reqHeaders,reqcc:this._reqcc}}revalidationHeaders(t){this._assertRequestHasHeaders(t);let r=this._copyWithoutHopByHopHeaders(t.headers);if(delete r["if-range"],!this._requestMatches(t,!0)||!this.storable())return delete r["if-none-match"],delete r["if-modified-since"],r;if(this._resHeaders.etag&&(r["if-none-match"]=r["if-none-match"]?`${r["if-none-match"]}, ${this._resHeaders.etag}`:this._resHeaders.etag),r["accept-ranges"]||r["if-match"]||r["if-unmodified-since"]||this._method&&this._method!="GET"){if(delete r["if-modified-since"],r["if-none-match"]){let a=r["if-none-match"].split(/,/).filter(n=>!/^\s*W\//.test(n));a.length?r["if-none-match"]=a.join(",").trim():delete r["if-none-match"]}}else this._resHeaders["last-modified"]&&!r["if-modified-since"]&&(r["if-modified-since"]=this._resHeaders["last-modified"]);return r}revalidatedPolicy(t,r){if(this._assertRequestHasHeaders(t),this._useStaleIfError()&&zZe(r))return{modified:!1,matches:!1,policy:this};if(!r||!r.headers)throw Error("Response headers missing");let s=!1;if(r.status!==void 0&&r.status!=304?s=!1:r.headers.etag&&!/^\s*W\//.test(r.headers.etag)?s=this._resHeaders.etag&&this._resHeaders.etag.replace(/^\s*W\//,"")===r.headers.etag:this._resHeaders.etag&&r.headers.etag?s=this._resHeaders.etag.replace(/^\s*W\//,"")===r.headers.etag.replace(/^\s*W\//,""):this._resHeaders["last-modified"]?s=this._resHeaders["last-modified"]===r.headers["last-modified"]:!this._resHeaders.etag&&!this._resHeaders["last-modified"]&&!r.headers.etag&&!r.headers["last-modified"]&&(s=!0),!s)return{policy:new this.constructor(t,r),modified:r.status!=304,matches:!1};let a={};for(let c in this._resHeaders)a[c]=c in r.headers&&!KZe[c]?r.headers[c]:this._resHeaders[c];let n=Object.assign({},r,{status:this._status,method:this._method,headers:a});return{policy:new this.constructor(t,n,{shared:this._isShared,cacheHeuristic:this._cacheHeuristic,immutableMinTimeToLive:this._immutableMinTtl}),modified:!1,matches:!0}}}});var WQ=G((JNt,Sue)=>{"use strict";Sue.exports=e=>{let t={};for(let[r,s]of Object.entries(e))t[r.toLowerCase()]=s;return t}});var bue=G((KNt,Due)=>{"use strict";var ZZe=Ie("stream").Readable,$Ze=WQ(),Z8=class extends ZZe{constructor(t,r,s,a){if(typeof t!="number")throw new TypeError("Argument `statusCode` should be a number");if(typeof r!="object")throw new TypeError("Argument `headers` should be an object");if(!(s instanceof Buffer))throw new TypeError("Argument `body` should be a buffer");if(typeof a!="string")throw new TypeError("Argument `url` should be a string");super(),this.statusCode=t,this.headers=$Ze(r),this.body=s,this.url=a}_read(){this.push(this.body),this.push(null)}};Due.exports=Z8});var xue=G((zNt,Pue)=>{"use strict";var e$e=["destroy","setTimeout","socket","headers","trailers","rawHeaders","statusCode","httpVersion","httpVersionMinor","httpVersionMajor","rawTrailers","statusMessage"];Pue.exports=(e,t)=>{let r=new Set(Object.keys(e).concat(e$e));for(let s of r)s in t||(t[s]=typeof e[s]=="function"?e[s].bind(e):e[s])}});var Que=G((XNt,kue)=>{"use strict";var t$e=Ie("stream").PassThrough,r$e=xue(),n$e=e=>{if(!(e&&e.pipe))throw new TypeError("Parameter `response` must be a response stream.");let t=new t$e;return r$e(e,t),e.pipe(t)};kue.exports=n$e});var Rue=G($8=>{$8.stringify=function e(t){if(typeof t>"u")return t;if(t&&Buffer.isBuffer(t))return JSON.stringify(":base64:"+t.toString("base64"));if(t&&t.toJSON&&(t=t.toJSON()),t&&typeof t=="object"){var r="",s=Array.isArray(t);r=s?"[":"{";var a=!0;for(var n in t){var c=typeof t[n]=="function"||!s&&typeof t[n]>"u";Object.hasOwnProperty.call(t,n)&&!c&&(a||(r+=","),a=!1,s?t[n]==null?r+="null":r+=e(t[n]):t[n]!==void 0&&(r+=e(n)+":"+e(t[n])))}return r+=s?"]":"}",r}else return typeof t=="string"?JSON.stringify(/^:/.test(t)?":"+t:t):typeof t>"u"?"null":JSON.stringify(t)};$8.parse=function(e){return JSON.parse(e,function(t,r){return typeof r=="string"?/^:base64:/.test(r)?Buffer.from(r.substring(8),"base64"):/^:/.test(r)?r.substring(1):r:r})}});var Oue=G(($Nt,Nue)=>{"use strict";var i$e=Ie("events"),Tue=Rue(),s$e=e=>{let t={redis:"@keyv/redis",rediss:"@keyv/redis",mongodb:"@keyv/mongo",mongo:"@keyv/mongo",sqlite:"@keyv/sqlite",postgresql:"@keyv/postgres",postgres:"@keyv/postgres",mysql:"@keyv/mysql",etcd:"@keyv/etcd",offline:"@keyv/offline",tiered:"@keyv/tiered"};if(e.adapter||e.uri){let r=e.adapter||/^[^:+]*/.exec(e.uri)[0];return new(Ie(t[r]))(e)}return new Map},Fue=["sqlite","postgres","mysql","mongo","redis","tiered"],eH=class extends i$e{constructor(t,{emitErrors:r=!0,...s}={}){if(super(),this.opts={namespace:"keyv",serialize:Tue.stringify,deserialize:Tue.parse,...typeof t=="string"?{uri:t}:t,...s},!this.opts.store){let n={...this.opts};this.opts.store=s$e(n)}if(this.opts.compression){let n=this.opts.compression;this.opts.serialize=n.serialize.bind(n),this.opts.deserialize=n.deserialize.bind(n)}typeof this.opts.store.on=="function"&&r&&this.opts.store.on("error",n=>this.emit("error",n)),this.opts.store.namespace=this.opts.namespace;let a=n=>async function*(){for await(let[c,f]of typeof n=="function"?n(this.opts.store.namespace):n){let p=await this.opts.deserialize(f);if(!(this.opts.store.namespace&&!c.includes(this.opts.store.namespace))){if(typeof p.expires=="number"&&Date.now()>p.expires){this.delete(c);continue}yield[this._getKeyUnprefix(c),p.value]}}};typeof this.opts.store[Symbol.iterator]=="function"&&this.opts.store instanceof Map?this.iterator=a(this.opts.store):typeof this.opts.store.iterator=="function"&&this.opts.store.opts&&this._checkIterableAdaptar()&&(this.iterator=a(this.opts.store.iterator.bind(this.opts.store)))}_checkIterableAdaptar(){return Fue.includes(this.opts.store.opts.dialect)||Fue.findIndex(t=>this.opts.store.opts.url.includes(t))>=0}_getKeyPrefix(t){return`${this.opts.namespace}:${t}`}_getKeyPrefixArray(t){return t.map(r=>`${this.opts.namespace}:${r}`)}_getKeyUnprefix(t){return t.split(":").splice(1).join(":")}get(t,r){let{store:s}=this.opts,a=Array.isArray(t),n=a?this._getKeyPrefixArray(t):this._getKeyPrefix(t);if(a&&s.getMany===void 0){let c=[];for(let f of n)c.push(Promise.resolve().then(()=>s.get(f)).then(p=>typeof p=="string"?this.opts.deserialize(p):this.opts.compression?this.opts.deserialize(p):p).then(p=>{if(p!=null)return typeof p.expires=="number"&&Date.now()>p.expires?this.delete(f).then(()=>{}):r&&r.raw?p:p.value}));return Promise.allSettled(c).then(f=>{let p=[];for(let h of f)p.push(h.value);return p})}return Promise.resolve().then(()=>a?s.getMany(n):s.get(n)).then(c=>typeof c=="string"?this.opts.deserialize(c):this.opts.compression?this.opts.deserialize(c):c).then(c=>{if(c!=null)return a?c.map((f,p)=>{if(typeof f=="string"&&(f=this.opts.deserialize(f)),f!=null){if(typeof f.expires=="number"&&Date.now()>f.expires){this.delete(t[p]).then(()=>{});return}return r&&r.raw?f:f.value}}):typeof c.expires=="number"&&Date.now()>c.expires?this.delete(t).then(()=>{}):r&&r.raw?c:c.value})}set(t,r,s){let a=this._getKeyPrefix(t);typeof s>"u"&&(s=this.opts.ttl),s===0&&(s=void 0);let{store:n}=this.opts;return Promise.resolve().then(()=>{let c=typeof s=="number"?Date.now()+s:null;return typeof r=="symbol"&&this.emit("error","symbol cannot be serialized"),r={value:r,expires:c},this.opts.serialize(r)}).then(c=>n.set(a,c,s)).then(()=>!0)}delete(t){let{store:r}=this.opts;if(Array.isArray(t)){let a=this._getKeyPrefixArray(t);if(r.deleteMany===void 0){let n=[];for(let c of a)n.push(r.delete(c));return Promise.allSettled(n).then(c=>c.every(f=>f.value===!0))}return Promise.resolve().then(()=>r.deleteMany(a))}let s=this._getKeyPrefix(t);return Promise.resolve().then(()=>r.delete(s))}clear(){let{store:t}=this.opts;return Promise.resolve().then(()=>t.clear())}has(t){let r=this._getKeyPrefix(t),{store:s}=this.opts;return Promise.resolve().then(async()=>typeof s.has=="function"?s.has(r):await s.get(r)!==void 0)}disconnect(){let{store:t}=this.opts;if(typeof t.disconnect=="function")return t.disconnect()}};Nue.exports=eH});var Uue=G((tOt,Mue)=>{"use strict";var o$e=Ie("events"),YQ=Ie("url"),a$e=cue(),l$e=wue(),tH=vue(),Lue=bue(),c$e=WQ(),u$e=Que(),f$e=Oue(),HB=class e{constructor(t,r){if(typeof t!="function")throw new TypeError("Parameter `request` must be a function");return this.cache=new f$e({uri:typeof r=="string"&&r,store:typeof r!="string"&&r,namespace:"cacheable-request"}),this.createCacheableRequest(t)}createCacheableRequest(t){return(r,s)=>{let a;if(typeof r=="string")a=rH(YQ.parse(r)),r={};else if(r instanceof YQ.URL)a=rH(YQ.parse(r.toString())),r={};else{let[C,...S]=(r.path||"").split("?"),x=S.length>0?`?${S.join("?")}`:"";a=rH({...r,pathname:C,search:x})}r={headers:{},method:"GET",cache:!0,strictTtl:!1,automaticFailover:!1,...r,...A$e(a)},r.headers=c$e(r.headers);let n=new o$e,c=a$e(YQ.format(a),{stripWWW:!1,removeTrailingSlash:!1,stripAuthentication:!1}),f=`${r.method}:${c}`,p=!1,h=!1,E=C=>{h=!0;let S=!1,x,I=new Promise(O=>{x=()=>{S||(S=!0,O())}}),T=O=>{if(p&&!C.forceRefresh){O.status=O.statusCode;let V=tH.fromObject(p.cachePolicy).revalidatedPolicy(C,O);if(!V.modified){let te=V.policy.responseHeaders();O=new Lue(p.statusCode,te,p.body,p.url),O.cachePolicy=V.policy,O.fromCache=!0}}O.fromCache||(O.cachePolicy=new tH(C,O,C),O.fromCache=!1);let U;C.cache&&O.cachePolicy.storable()?(U=u$e(O),(async()=>{try{let V=l$e.buffer(O);if(await Promise.race([I,new Promise(ae=>O.once("end",ae))]),S)return;let te=await V,ie={cachePolicy:O.cachePolicy.toObject(),url:O.url,statusCode:O.fromCache?p.statusCode:O.statusCode,body:te},ue=C.strictTtl?O.cachePolicy.timeToLive():void 0;C.maxTtl&&(ue=ue?Math.min(ue,C.maxTtl):C.maxTtl),await this.cache.set(f,ie,ue)}catch(V){n.emit("error",new e.CacheError(V))}})()):C.cache&&p&&(async()=>{try{await this.cache.delete(f)}catch(V){n.emit("error",new e.CacheError(V))}})(),n.emit("response",U||O),typeof s=="function"&&s(U||O)};try{let O=t(C,T);O.once("error",x),O.once("abort",x),n.emit("request",O)}catch(O){n.emit("error",new e.RequestError(O))}};return(async()=>{let C=async x=>{await Promise.resolve();let I=x.cache?await this.cache.get(f):void 0;if(typeof I>"u")return E(x);let T=tH.fromObject(I.cachePolicy);if(T.satisfiesWithoutRevalidation(x)&&!x.forceRefresh){let O=T.responseHeaders(),U=new Lue(I.statusCode,O,I.body,I.url);U.cachePolicy=T,U.fromCache=!0,n.emit("response",U),typeof s=="function"&&s(U)}else p=I,x.headers=T.revalidationHeaders(x),E(x)},S=x=>n.emit("error",new e.CacheError(x));this.cache.once("error",S),n.on("response",()=>this.cache.removeListener("error",S));try{await C(r)}catch(x){r.automaticFailover&&!h&&E(r),n.emit("error",new e.CacheError(x))}})(),n}}};function A$e(e){let t={...e};return t.path=`${e.pathname||"/"}${e.search||""}`,delete t.pathname,delete t.search,t}function rH(e){return{protocol:e.protocol,auth:e.auth,hostname:e.hostname||e.host||"localhost",port:e.port,pathname:e.pathname,search:e.search}}HB.RequestError=class extends Error{constructor(e){super(e.message),this.name="RequestError",Object.assign(this,e)}};HB.CacheError=class extends Error{constructor(e){super(e.message),this.name="CacheError",Object.assign(this,e)}};Mue.exports=HB});var Hue=G((iOt,_ue)=>{"use strict";var p$e=["aborted","complete","headers","httpVersion","httpVersionMinor","httpVersionMajor","method","rawHeaders","rawTrailers","setTimeout","socket","statusCode","statusMessage","trailers","url"];_ue.exports=(e,t)=>{if(t._readableState.autoDestroy)throw new Error("The second stream must have the `autoDestroy` option set to `false`");let r=new Set(Object.keys(e).concat(p$e)),s={};for(let a of r)a in t||(s[a]={get(){let n=e[a];return typeof n=="function"?n.bind(e):n},set(n){e[a]=n},enumerable:!0,configurable:!1});return Object.defineProperties(t,s),e.once("aborted",()=>{t.destroy(),t.emit("aborted")}),e.once("close",()=>{e.complete&&t.readable?t.once("end",()=>{t.emit("close")}):t.emit("close")}),t}});var Gue=G((sOt,jue)=>{"use strict";var{Transform:h$e,PassThrough:d$e}=Ie("stream"),nH=Ie("zlib"),g$e=Hue();jue.exports=e=>{let t=(e.headers["content-encoding"]||"").toLowerCase();if(!["gzip","deflate","br"].includes(t))return e;let r=t==="br";if(r&&typeof nH.createBrotliDecompress!="function")return e.destroy(new Error("Brotli is not supported on Node.js < 12")),e;let s=!0,a=new h$e({transform(f,p,h){s=!1,h(null,f)},flush(f){f()}}),n=new d$e({autoDestroy:!1,destroy(f,p){e.destroy(),p(f)}}),c=r?nH.createBrotliDecompress():nH.createUnzip();return c.once("error",f=>{if(s&&!e.readable){n.end();return}n.destroy(f)}),g$e(e,n),e.pipe(a).pipe(c).pipe(n),n}});var sH=G((oOt,que)=>{"use strict";var iH=class{constructor(t={}){if(!(t.maxSize&&t.maxSize>0))throw new TypeError("`maxSize` must be a number greater than 0");this.maxSize=t.maxSize,this.onEviction=t.onEviction,this.cache=new Map,this.oldCache=new Map,this._size=0}_set(t,r){if(this.cache.set(t,r),this._size++,this._size>=this.maxSize){if(this._size=0,typeof this.onEviction=="function")for(let[s,a]of this.oldCache.entries())this.onEviction(s,a);this.oldCache=this.cache,this.cache=new Map}}get(t){if(this.cache.has(t))return this.cache.get(t);if(this.oldCache.has(t)){let r=this.oldCache.get(t);return this.oldCache.delete(t),this._set(t,r),r}}set(t,r){return this.cache.has(t)?this.cache.set(t,r):this._set(t,r),this}has(t){return this.cache.has(t)||this.oldCache.has(t)}peek(t){if(this.cache.has(t))return this.cache.get(t);if(this.oldCache.has(t))return this.oldCache.get(t)}delete(t){let r=this.cache.delete(t);return r&&this._size--,this.oldCache.delete(t)||r}clear(){this.cache.clear(),this.oldCache.clear(),this._size=0}*keys(){for(let[t]of this)yield t}*values(){for(let[,t]of this)yield t}*[Symbol.iterator](){for(let t of this.cache)yield t;for(let t of this.oldCache){let[r]=t;this.cache.has(r)||(yield t)}}get size(){let t=0;for(let r of this.oldCache.keys())this.cache.has(r)||t++;return Math.min(this._size+t,this.maxSize)}};que.exports=iH});var aH=G((aOt,Jue)=>{"use strict";var m$e=Ie("events"),y$e=Ie("tls"),E$e=Ie("http2"),I$e=sH(),Ra=Symbol("currentStreamsCount"),Wue=Symbol("request"),Nc=Symbol("cachedOriginSet"),yI=Symbol("gracefullyClosing"),C$e=["maxDeflateDynamicTableSize","maxSessionMemory","maxHeaderListPairs","maxOutstandingPings","maxReservedRemoteStreams","maxSendHeaderBlockLength","paddingStrategy","localAddress","path","rejectUnauthorized","minDHSize","ca","cert","clientCertEngine","ciphers","key","pfx","servername","minVersion","maxVersion","secureProtocol","crl","honorCipherOrder","ecdhCurve","dhparam","secureOptions","sessionIdContext"],w$e=(e,t,r)=>{let s=0,a=e.length;for(;s>>1;r(e[n],t)?s=n+1:a=n}return s},B$e=(e,t)=>e.remoteSettings.maxConcurrentStreams>t.remoteSettings.maxConcurrentStreams,oH=(e,t)=>{for(let r of e)r[Nc].lengtht[Nc].includes(s))&&r[Ra]+t[Ra]<=t.remoteSettings.maxConcurrentStreams&&Vue(r)},v$e=(e,t)=>{for(let r of e)t[Nc].lengthr[Nc].includes(s))&&t[Ra]+r[Ra]<=r.remoteSettings.maxConcurrentStreams&&Vue(t)},Yue=({agent:e,isFree:t})=>{let r={};for(let s in e.sessions){let n=e.sessions[s].filter(c=>{let f=c[rm.kCurrentStreamsCount]{e[yI]=!0,e[Ra]===0&&e.close()},rm=class e extends m$e{constructor({timeout:t=6e4,maxSessions:r=1/0,maxFreeSessions:s=10,maxCachedTlsSessions:a=100}={}){super(),this.sessions={},this.queue={},this.timeout=t,this.maxSessions=r,this.maxFreeSessions=s,this._freeSessionsCount=0,this._sessionsCount=0,this.settings={enablePush:!1},this.tlsSessionCache=new I$e({maxSize:a})}static normalizeOrigin(t,r){return typeof t=="string"&&(t=new URL(t)),r&&t.hostname!==r&&(t.hostname=r),t.origin}normalizeOptions(t){let r="";if(t)for(let s of C$e)t[s]&&(r+=`:${t[s]}`);return r}_tryToCreateNewSession(t,r){if(!(t in this.queue)||!(r in this.queue[t]))return;let s=this.queue[t][r];this._sessionsCount{Array.isArray(s)?(s=[...s],a()):s=[{resolve:a,reject:n}];let c=this.normalizeOptions(r),f=e.normalizeOrigin(t,r&&r.servername);if(f===void 0){for(let{reject:E}of s)E(new TypeError("The `origin` argument needs to be a string or an URL object"));return}if(c in this.sessions){let E=this.sessions[c],C=-1,S=-1,x;for(let I of E){let T=I.remoteSettings.maxConcurrentStreams;if(T=T||I[yI]||I.destroyed)continue;x||(C=T),O>S&&(x=I,S=O)}}if(x){if(s.length!==1){for(let{reject:I}of s){let T=new Error(`Expected the length of listeners to be 1, got ${s.length}. Please report this to https://github.com/szmarczak/http2-wrapper/`);I(T)}return}s[0].resolve(x);return}}if(c in this.queue){if(f in this.queue[c]){this.queue[c][f].listeners.push(...s),this._tryToCreateNewSession(c,f);return}}else this.queue[c]={};let p=()=>{c in this.queue&&this.queue[c][f]===h&&(delete this.queue[c][f],Object.keys(this.queue[c]).length===0&&delete this.queue[c])},h=()=>{let E=`${f}:${c}`,C=!1;try{let S=E$e.connect(t,{createConnection:this.createConnection,settings:this.settings,session:this.tlsSessionCache.get(E),...r});S[Ra]=0,S[yI]=!1;let x=()=>S[Ra]{this.tlsSessionCache.set(E,O)}),S.once("error",O=>{for(let{reject:U}of s)U(O);this.tlsSessionCache.delete(E)}),S.setTimeout(this.timeout,()=>{S.destroy()}),S.once("close",()=>{if(C){I&&this._freeSessionsCount--,this._sessionsCount--;let O=this.sessions[c];O.splice(O.indexOf(S),1),O.length===0&&delete this.sessions[c]}else{let O=new Error("Session closed without receiving a SETTINGS frame");O.code="HTTP2WRAPPER_NOSETTINGS";for(let{reject:U}of s)U(O);p()}this._tryToCreateNewSession(c,f)});let T=()=>{if(!(!(c in this.queue)||!x())){for(let O of S[Nc])if(O in this.queue[c]){let{listeners:U}=this.queue[c][O];for(;U.length!==0&&x();)U.shift().resolve(S);let V=this.queue[c];if(V[O].listeners.length===0&&(delete V[O],Object.keys(V).length===0)){delete this.queue[c];break}if(!x())break}}};S.on("origin",()=>{S[Nc]=S.originSet,x()&&(T(),oH(this.sessions[c],S))}),S.once("remoteSettings",()=>{if(S.ref(),S.unref(),this._sessionsCount++,h.destroyed){let O=new Error("Agent has been destroyed");for(let U of s)U.reject(O);S.destroy();return}S[Nc]=S.originSet;{let O=this.sessions;if(c in O){let U=O[c];U.splice(w$e(U,S,B$e),0,S)}else O[c]=[S]}this._freeSessionsCount+=1,C=!0,this.emit("session",S),T(),p(),S[Ra]===0&&this._freeSessionsCount>this.maxFreeSessions&&S.close(),s.length!==0&&(this.getSession(f,r,s),s.length=0),S.on("remoteSettings",()=>{T(),oH(this.sessions[c],S)})}),S[Wue]=S.request,S.request=(O,U)=>{if(S[yI])throw new Error("The session is gracefully closing. No new streams are allowed.");let V=S[Wue](O,U);return S.ref(),++S[Ra],S[Ra]===S.remoteSettings.maxConcurrentStreams&&this._freeSessionsCount--,V.once("close",()=>{if(I=x(),--S[Ra],!S.destroyed&&!S.closed&&(v$e(this.sessions[c],S),x()&&!S.closed)){I||(this._freeSessionsCount++,I=!0);let te=S[Ra]===0;te&&S.unref(),te&&(this._freeSessionsCount>this.maxFreeSessions||S[yI])?S.close():(oH(this.sessions[c],S),T())}}),V}}catch(S){for(let x of s)x.reject(S);p()}};h.listeners=s,h.completed=!1,h.destroyed=!1,this.queue[c][f]=h,this._tryToCreateNewSession(c,f)})}request(t,r,s,a){return new Promise((n,c)=>{this.getSession(t,r,[{reject:c,resolve:f=>{try{n(f.request(s,a))}catch(p){c(p)}}}])})}createConnection(t,r){return e.connect(t,r)}static connect(t,r){r.ALPNProtocols=["h2"];let s=t.port||443,a=t.hostname||t.host;return typeof r.servername>"u"&&(r.servername=a),y$e.connect(s,a,r)}closeFreeSessions(){for(let t of Object.values(this.sessions))for(let r of t)r[Ra]===0&&r.close()}destroy(t){for(let r of Object.values(this.sessions))for(let s of r)s.destroy(t);for(let r of Object.values(this.queue))for(let s of Object.values(r))s.destroyed=!0;this.queue={}}get freeSessions(){return Yue({agent:this,isFree:!0})}get busySessions(){return Yue({agent:this,isFree:!1})}};rm.kCurrentStreamsCount=Ra;rm.kGracefullyClosing=yI;Jue.exports={Agent:rm,globalAgent:new rm}});var cH=G((lOt,Kue)=>{"use strict";var{Readable:S$e}=Ie("stream"),lH=class extends S$e{constructor(t,r){super({highWaterMark:r,autoDestroy:!1}),this.statusCode=null,this.statusMessage="",this.httpVersion="2.0",this.httpVersionMajor=2,this.httpVersionMinor=0,this.headers={},this.trailers={},this.req=null,this.aborted=!1,this.complete=!1,this.upgrade=null,this.rawHeaders=[],this.rawTrailers=[],this.socket=t,this.connection=t,this._dumped=!1}_destroy(t){this.req._request.destroy(t)}setTimeout(t,r){return this.req.setTimeout(t,r),this}_dump(){this._dumped||(this._dumped=!0,this.removeAllListeners("data"),this.resume())}_read(){this.req&&this.req._request.resume()}};Kue.exports=lH});var uH=G((cOt,zue)=>{"use strict";zue.exports=e=>{let t={protocol:e.protocol,hostname:typeof e.hostname=="string"&&e.hostname.startsWith("[")?e.hostname.slice(1,-1):e.hostname,host:e.host,hash:e.hash,search:e.search,pathname:e.pathname,href:e.href,path:`${e.pathname||""}${e.search||""}`};return typeof e.port=="string"&&e.port.length!==0&&(t.port=Number(e.port)),(e.username||e.password)&&(t.auth=`${e.username||""}:${e.password||""}`),t}});var Zue=G((uOt,Xue)=>{"use strict";Xue.exports=(e,t,r)=>{for(let s of r)e.on(s,(...a)=>t.emit(s,...a))}});var efe=G((fOt,$ue)=>{"use strict";$ue.exports=e=>{switch(e){case":method":case":scheme":case":authority":case":path":return!0;default:return!1}}});var rfe=G((pOt,tfe)=>{"use strict";var EI=(e,t,r)=>{tfe.exports[t]=class extends e{constructor(...a){super(typeof r=="string"?r:r(a)),this.name=`${super.name} [${t}]`,this.code=t}}};EI(TypeError,"ERR_INVALID_ARG_TYPE",e=>{let t=e[0].includes(".")?"property":"argument",r=e[1],s=Array.isArray(r);return s&&(r=`${r.slice(0,-1).join(", ")} or ${r.slice(-1)}`),`The "${e[0]}" ${t} must be ${s?"one of":"of"} type ${r}. Received ${typeof e[2]}`});EI(TypeError,"ERR_INVALID_PROTOCOL",e=>`Protocol "${e[0]}" not supported. Expected "${e[1]}"`);EI(Error,"ERR_HTTP_HEADERS_SENT",e=>`Cannot ${e[0]} headers after they are sent to the client`);EI(TypeError,"ERR_INVALID_HTTP_TOKEN",e=>`${e[0]} must be a valid HTTP token [${e[1]}]`);EI(TypeError,"ERR_HTTP_INVALID_HEADER_VALUE",e=>`Invalid value "${e[0]} for header "${e[1]}"`);EI(TypeError,"ERR_INVALID_CHAR",e=>`Invalid character in ${e[0]} [${e[1]}]`)});var dH=G((hOt,cfe)=>{"use strict";var D$e=Ie("http2"),{Writable:b$e}=Ie("stream"),{Agent:nfe,globalAgent:P$e}=aH(),x$e=cH(),k$e=uH(),Q$e=Zue(),R$e=efe(),{ERR_INVALID_ARG_TYPE:fH,ERR_INVALID_PROTOCOL:T$e,ERR_HTTP_HEADERS_SENT:ife,ERR_INVALID_HTTP_TOKEN:F$e,ERR_HTTP_INVALID_HEADER_VALUE:N$e,ERR_INVALID_CHAR:O$e}=rfe(),{HTTP2_HEADER_STATUS:sfe,HTTP2_HEADER_METHOD:ofe,HTTP2_HEADER_PATH:afe,HTTP2_METHOD_CONNECT:L$e}=D$e.constants,ta=Symbol("headers"),AH=Symbol("origin"),pH=Symbol("session"),lfe=Symbol("options"),VQ=Symbol("flushedHeaders"),jB=Symbol("jobs"),M$e=/^[\^`\-\w!#$%&*+.|~]+$/,U$e=/[^\t\u0020-\u007E\u0080-\u00FF]/,hH=class extends b$e{constructor(t,r,s){super({autoDestroy:!1});let a=typeof t=="string"||t instanceof URL;if(a&&(t=k$e(t instanceof URL?t:new URL(t))),typeof r=="function"||r===void 0?(s=r,r=a?t:{...t}):r={...t,...r},r.h2session)this[pH]=r.h2session;else if(r.agent===!1)this.agent=new nfe({maxFreeSessions:0});else if(typeof r.agent>"u"||r.agent===null)typeof r.createConnection=="function"?(this.agent=new nfe({maxFreeSessions:0}),this.agent.createConnection=r.createConnection):this.agent=P$e;else if(typeof r.agent.request=="function")this.agent=r.agent;else throw new fH("options.agent",["Agent-like Object","undefined","false"],r.agent);if(r.protocol&&r.protocol!=="https:")throw new T$e(r.protocol,"https:");let n=r.port||r.defaultPort||this.agent&&this.agent.defaultPort||443,c=r.hostname||r.host||"localhost";delete r.hostname,delete r.host,delete r.port;let{timeout:f}=r;if(r.timeout=void 0,this[ta]=Object.create(null),this[jB]=[],this.socket=null,this.connection=null,this.method=r.method||"GET",this.path=r.path,this.res=null,this.aborted=!1,this.reusedSocket=!1,r.headers)for(let[p,h]of Object.entries(r.headers))this.setHeader(p,h);r.auth&&!("authorization"in this[ta])&&(this[ta].authorization="Basic "+Buffer.from(r.auth).toString("base64")),r.session=r.tlsSession,r.path=r.socketPath,this[lfe]=r,n===443?(this[AH]=`https://${c}`,":authority"in this[ta]||(this[ta][":authority"]=c)):(this[AH]=`https://${c}:${n}`,":authority"in this[ta]||(this[ta][":authority"]=`${c}:${n}`)),f&&this.setTimeout(f),s&&this.once("response",s),this[VQ]=!1}get method(){return this[ta][ofe]}set method(t){t&&(this[ta][ofe]=t.toUpperCase())}get path(){return this[ta][afe]}set path(t){t&&(this[ta][afe]=t)}get _mustNotHaveABody(){return this.method==="GET"||this.method==="HEAD"||this.method==="DELETE"}_write(t,r,s){if(this._mustNotHaveABody){s(new Error("The GET, HEAD and DELETE methods must NOT have a body"));return}this.flushHeaders();let a=()=>this._request.write(t,r,s);this._request?a():this[jB].push(a)}_final(t){if(this.destroyed)return;this.flushHeaders();let r=()=>{if(this._mustNotHaveABody){t();return}this._request.end(t)};this._request?r():this[jB].push(r)}abort(){this.res&&this.res.complete||(this.aborted||process.nextTick(()=>this.emit("abort")),this.aborted=!0,this.destroy())}_destroy(t,r){this.res&&this.res._dump(),this._request&&this._request.destroy(),r(t)}async flushHeaders(){if(this[VQ]||this.destroyed)return;this[VQ]=!0;let t=this.method===L$e,r=s=>{if(this._request=s,this.destroyed){s.destroy();return}t||Q$e(s,this,["timeout","continue","close","error"]);let a=c=>(...f)=>{!this.writable&&!this.destroyed?c(...f):this.once("finish",()=>{c(...f)})};s.once("response",a((c,f,p)=>{let h=new x$e(this.socket,s.readableHighWaterMark);this.res=h,h.req=this,h.statusCode=c[sfe],h.headers=c,h.rawHeaders=p,h.once("end",()=>{this.aborted?(h.aborted=!0,h.emit("aborted")):(h.complete=!0,h.socket=null,h.connection=null)}),t?(h.upgrade=!0,this.emit("connect",h,s,Buffer.alloc(0))?this.emit("close"):s.destroy()):(s.on("data",E=>{!h._dumped&&!h.push(E)&&s.pause()}),s.once("end",()=>{h.push(null)}),this.emit("response",h)||h._dump())})),s.once("headers",a(c=>this.emit("information",{statusCode:c[sfe]}))),s.once("trailers",a((c,f,p)=>{let{res:h}=this;h.trailers=c,h.rawTrailers=p}));let{socket:n}=s.session;this.socket=n,this.connection=n;for(let c of this[jB])c();this.emit("socket",this.socket)};if(this[pH])try{r(this[pH].request(this[ta]))}catch(s){this.emit("error",s)}else{this.reusedSocket=!0;try{r(await this.agent.request(this[AH],this[lfe],this[ta]))}catch(s){this.emit("error",s)}}}getHeader(t){if(typeof t!="string")throw new fH("name","string",t);return this[ta][t.toLowerCase()]}get headersSent(){return this[VQ]}removeHeader(t){if(typeof t!="string")throw new fH("name","string",t);if(this.headersSent)throw new ife("remove");delete this[ta][t.toLowerCase()]}setHeader(t,r){if(this.headersSent)throw new ife("set");if(typeof t!="string"||!M$e.test(t)&&!R$e(t))throw new F$e("Header name",t);if(typeof r>"u")throw new N$e(r,t);if(U$e.test(r))throw new O$e("header content",t);this[ta][t.toLowerCase()]=r}setNoDelay(){}setSocketKeepAlive(){}setTimeout(t,r){let s=()=>this._request.setTimeout(t,r);return this._request?s():this[jB].push(s),this}get maxHeadersCount(){if(!this.destroyed&&this._request)return this._request.session.localSettings.maxHeaderListSize}set maxHeadersCount(t){}};cfe.exports=hH});var ffe=G((dOt,ufe)=>{"use strict";var _$e=Ie("tls");ufe.exports=(e={},t=_$e.connect)=>new Promise((r,s)=>{let a=!1,n,c=async()=>{await p,n.off("timeout",f),n.off("error",s),e.resolveSocket?(r({alpnProtocol:n.alpnProtocol,socket:n,timeout:a}),a&&(await Promise.resolve(),n.emit("timeout"))):(n.destroy(),r({alpnProtocol:n.alpnProtocol,timeout:a}))},f=async()=>{a=!0,c()},p=(async()=>{try{n=await t(e,c),n.on("error",s),n.once("timeout",f)}catch(h){s(h)}})()})});var pfe=G((gOt,Afe)=>{"use strict";var H$e=Ie("net");Afe.exports=e=>{let t=e.host,r=e.headers&&e.headers.host;return r&&(r.startsWith("[")?r.indexOf("]")===-1?t=r:t=r.slice(1,-1):t=r.split(":",1)[0]),H$e.isIP(t)?"":t}});var gfe=G((mOt,mH)=>{"use strict";var hfe=Ie("http"),gH=Ie("https"),j$e=ffe(),G$e=sH(),q$e=dH(),W$e=pfe(),Y$e=uH(),JQ=new G$e({maxSize:100}),GB=new Map,dfe=(e,t,r)=>{t._httpMessage={shouldKeepAlive:!0};let s=()=>{e.emit("free",t,r)};t.on("free",s);let a=()=>{e.removeSocket(t,r)};t.on("close",a);let n=()=>{e.removeSocket(t,r),t.off("close",a),t.off("free",s),t.off("agentRemove",n)};t.on("agentRemove",n),e.emit("free",t,r)},V$e=async e=>{let t=`${e.host}:${e.port}:${e.ALPNProtocols.sort()}`;if(!JQ.has(t)){if(GB.has(t))return(await GB.get(t)).alpnProtocol;let{path:r,agent:s}=e;e.path=e.socketPath;let a=j$e(e);GB.set(t,a);try{let{socket:n,alpnProtocol:c}=await a;if(JQ.set(t,c),e.path=r,c==="h2")n.destroy();else{let{globalAgent:f}=gH,p=gH.Agent.prototype.createConnection;s?s.createConnection===p?dfe(s,n,e):n.destroy():f.createConnection===p?dfe(f,n,e):n.destroy()}return GB.delete(t),c}catch(n){throw GB.delete(t),n}}return JQ.get(t)};mH.exports=async(e,t,r)=>{if((typeof e=="string"||e instanceof URL)&&(e=Y$e(new URL(e))),typeof t=="function"&&(r=t,t=void 0),t={ALPNProtocols:["h2","http/1.1"],...e,...t,resolveSocket:!0},!Array.isArray(t.ALPNProtocols)||t.ALPNProtocols.length===0)throw new Error("The `ALPNProtocols` option must be an Array with at least one entry");t.protocol=t.protocol||"https:";let s=t.protocol==="https:";t.host=t.hostname||t.host||"localhost",t.session=t.tlsSession,t.servername=t.servername||W$e(t),t.port=t.port||(s?443:80),t._defaultAgent=s?gH.globalAgent:hfe.globalAgent;let a=t.agent;if(a){if(a.addRequest)throw new Error("The `options.agent` object can contain only `http`, `https` or `http2` properties");t.agent=a[s?"https":"http"]}return s&&await V$e(t)==="h2"?(a&&(t.agent=a.http2),new q$e(t,r)):hfe.request(t,r)};mH.exports.protocolCache=JQ});var yfe=G((yOt,mfe)=>{"use strict";var J$e=Ie("http2"),K$e=aH(),yH=dH(),z$e=cH(),X$e=gfe(),Z$e=(e,t,r)=>new yH(e,t,r),$$e=(e,t,r)=>{let s=new yH(e,t,r);return s.end(),s};mfe.exports={...J$e,ClientRequest:yH,IncomingMessage:z$e,...K$e,request:Z$e,get:$$e,auto:X$e}});var IH=G(EH=>{"use strict";Object.defineProperty(EH,"__esModule",{value:!0});var Efe=Lp();EH.default=e=>Efe.default.nodeStream(e)&&Efe.default.function_(e.getBoundary)});var Bfe=G(CH=>{"use strict";Object.defineProperty(CH,"__esModule",{value:!0});var Cfe=Ie("fs"),wfe=Ie("util"),Ife=Lp(),eet=IH(),tet=wfe.promisify(Cfe.stat);CH.default=async(e,t)=>{if(t&&"content-length"in t)return Number(t["content-length"]);if(!e)return 0;if(Ife.default.string(e))return Buffer.byteLength(e);if(Ife.default.buffer(e))return e.length;if(eet.default(e))return wfe.promisify(e.getLength.bind(e))();if(e instanceof Cfe.ReadStream){let{size:r}=await tet(e.path);return r===0?void 0:r}}});var BH=G(wH=>{"use strict";Object.defineProperty(wH,"__esModule",{value:!0});function ret(e,t,r){let s={};for(let a of r)s[a]=(...n)=>{t.emit(a,...n)},e.on(a,s[a]);return()=>{for(let a of r)e.off(a,s[a])}}wH.default=ret});var vfe=G(vH=>{"use strict";Object.defineProperty(vH,"__esModule",{value:!0});vH.default=()=>{let e=[];return{once(t,r,s){t.once(r,s),e.push({origin:t,event:r,fn:s})},unhandleAll(){for(let t of e){let{origin:r,event:s,fn:a}=t;r.removeListener(s,a)}e.length=0}}}});var Dfe=G(qB=>{"use strict";Object.defineProperty(qB,"__esModule",{value:!0});qB.TimeoutError=void 0;var net=Ie("net"),iet=vfe(),Sfe=Symbol("reentry"),set=()=>{},KQ=class extends Error{constructor(t,r){super(`Timeout awaiting '${r}' for ${t}ms`),this.event=r,this.name="TimeoutError",this.code="ETIMEDOUT"}};qB.TimeoutError=KQ;qB.default=(e,t,r)=>{if(Sfe in e)return set;e[Sfe]=!0;let s=[],{once:a,unhandleAll:n}=iet.default(),c=(C,S,x)=>{var I;let T=setTimeout(S,C,C,x);(I=T.unref)===null||I===void 0||I.call(T);let O=()=>{clearTimeout(T)};return s.push(O),O},{host:f,hostname:p}=r,h=(C,S)=>{e.destroy(new KQ(C,S))},E=()=>{for(let C of s)C();n()};if(e.once("error",C=>{if(E(),e.listenerCount("error")===0)throw C}),e.once("close",E),a(e,"response",C=>{a(C,"end",E)}),typeof t.request<"u"&&c(t.request,h,"request"),typeof t.socket<"u"){let C=()=>{h(t.socket,"socket")};e.setTimeout(t.socket,C),s.push(()=>{e.removeListener("timeout",C)})}return a(e,"socket",C=>{var S;let{socketPath:x}=e;if(C.connecting){let I=!!(x??net.isIP((S=p??f)!==null&&S!==void 0?S:"")!==0);if(typeof t.lookup<"u"&&!I&&typeof C.address().address>"u"){let T=c(t.lookup,h,"lookup");a(C,"lookup",T)}if(typeof t.connect<"u"){let T=()=>c(t.connect,h,"connect");I?a(C,"connect",T()):a(C,"lookup",O=>{O===null&&a(C,"connect",T())})}typeof t.secureConnect<"u"&&r.protocol==="https:"&&a(C,"connect",()=>{let T=c(t.secureConnect,h,"secureConnect");a(C,"secureConnect",T)})}if(typeof t.send<"u"){let I=()=>c(t.send,h,"send");C.connecting?a(C,"connect",()=>{a(e,"upload-complete",I())}):a(e,"upload-complete",I())}}),typeof t.response<"u"&&a(e,"upload-complete",()=>{let C=c(t.response,h,"response");a(e,"response",C)}),E}});var Pfe=G(SH=>{"use strict";Object.defineProperty(SH,"__esModule",{value:!0});var bfe=Lp();SH.default=e=>{e=e;let t={protocol:e.protocol,hostname:bfe.default.string(e.hostname)&&e.hostname.startsWith("[")?e.hostname.slice(1,-1):e.hostname,host:e.host,hash:e.hash,search:e.search,pathname:e.pathname,href:e.href,path:`${e.pathname||""}${e.search||""}`};return bfe.default.string(e.port)&&e.port.length>0&&(t.port=Number(e.port)),(e.username||e.password)&&(t.auth=`${e.username||""}:${e.password||""}`),t}});var xfe=G(DH=>{"use strict";Object.defineProperty(DH,"__esModule",{value:!0});var oet=Ie("url"),aet=["protocol","host","hostname","port","pathname","search"];DH.default=(e,t)=>{var r,s;if(t.path){if(t.pathname)throw new TypeError("Parameters `path` and `pathname` are mutually exclusive.");if(t.search)throw new TypeError("Parameters `path` and `search` are mutually exclusive.");if(t.searchParams)throw new TypeError("Parameters `path` and `searchParams` are mutually exclusive.")}if(t.search&&t.searchParams)throw new TypeError("Parameters `search` and `searchParams` are mutually exclusive.");if(!e){if(!t.protocol)throw new TypeError("No URL protocol specified");e=`${t.protocol}//${(s=(r=t.hostname)!==null&&r!==void 0?r:t.host)!==null&&s!==void 0?s:""}`}let a=new oet.URL(e);if(t.path){let n=t.path.indexOf("?");n===-1?t.pathname=t.path:(t.pathname=t.path.slice(0,n),t.search=t.path.slice(n+1)),delete t.path}for(let n of aet)t[n]&&(a[n]=t[n].toString());return a}});var kfe=G(PH=>{"use strict";Object.defineProperty(PH,"__esModule",{value:!0});var bH=class{constructor(){this.weakMap=new WeakMap,this.map=new Map}set(t,r){typeof t=="object"?this.weakMap.set(t,r):this.map.set(t,r)}get(t){return typeof t=="object"?this.weakMap.get(t):this.map.get(t)}has(t){return typeof t=="object"?this.weakMap.has(t):this.map.has(t)}};PH.default=bH});var kH=G(xH=>{"use strict";Object.defineProperty(xH,"__esModule",{value:!0});var cet=async e=>{let t=[],r=0;for await(let s of e)t.push(s),r+=Buffer.byteLength(s);return Buffer.isBuffer(t[0])?Buffer.concat(t,r):Buffer.from(t.join(""))};xH.default=cet});var Rfe=G(nm=>{"use strict";Object.defineProperty(nm,"__esModule",{value:!0});nm.dnsLookupIpVersionToFamily=nm.isDnsLookupIpVersion=void 0;var Qfe={auto:0,ipv4:4,ipv6:6};nm.isDnsLookupIpVersion=e=>e in Qfe;nm.dnsLookupIpVersionToFamily=e=>{if(nm.isDnsLookupIpVersion(e))return Qfe[e];throw new Error("Invalid DNS lookup IP version")}});var QH=G(zQ=>{"use strict";Object.defineProperty(zQ,"__esModule",{value:!0});zQ.isResponseOk=void 0;zQ.isResponseOk=e=>{let{statusCode:t}=e,r=e.request.options.followRedirect?299:399;return t>=200&&t<=r||t===304}});var Ffe=G(RH=>{"use strict";Object.defineProperty(RH,"__esModule",{value:!0});var Tfe=new Set;RH.default=e=>{Tfe.has(e)||(Tfe.add(e),process.emitWarning(`Got: ${e}`,{type:"DeprecationWarning"}))}});var Nfe=G(TH=>{"use strict";Object.defineProperty(TH,"__esModule",{value:!0});var Di=Lp(),uet=(e,t)=>{if(Di.default.null_(e.encoding))throw new TypeError("To get a Buffer, set `options.responseType` to `buffer` instead");Di.assert.any([Di.default.string,Di.default.undefined],e.encoding),Di.assert.any([Di.default.boolean,Di.default.undefined],e.resolveBodyOnly),Di.assert.any([Di.default.boolean,Di.default.undefined],e.methodRewriting),Di.assert.any([Di.default.boolean,Di.default.undefined],e.isStream),Di.assert.any([Di.default.string,Di.default.undefined],e.responseType),e.responseType===void 0&&(e.responseType="text");let{retry:r}=e;if(t?e.retry={...t.retry}:e.retry={calculateDelay:s=>s.computedValue,limit:0,methods:[],statusCodes:[],errorCodes:[],maxRetryAfter:void 0},Di.default.object(r)?(e.retry={...e.retry,...r},e.retry.methods=[...new Set(e.retry.methods.map(s=>s.toUpperCase()))],e.retry.statusCodes=[...new Set(e.retry.statusCodes)],e.retry.errorCodes=[...new Set(e.retry.errorCodes)]):Di.default.number(r)&&(e.retry.limit=r),Di.default.undefined(e.retry.maxRetryAfter)&&(e.retry.maxRetryAfter=Math.min(...[e.timeout.request,e.timeout.connect].filter(Di.default.number))),Di.default.object(e.pagination)){t&&(e.pagination={...t.pagination,...e.pagination});let{pagination:s}=e;if(!Di.default.function_(s.transform))throw new Error("`options.pagination.transform` must be implemented");if(!Di.default.function_(s.shouldContinue))throw new Error("`options.pagination.shouldContinue` must be implemented");if(!Di.default.function_(s.filter))throw new TypeError("`options.pagination.filter` must be implemented");if(!Di.default.function_(s.paginate))throw new Error("`options.pagination.paginate` must be implemented")}return e.responseType==="json"&&e.headers.accept===void 0&&(e.headers.accept="application/json"),e};TH.default=uet});var Ofe=G(WB=>{"use strict";Object.defineProperty(WB,"__esModule",{value:!0});WB.retryAfterStatusCodes=void 0;WB.retryAfterStatusCodes=new Set([413,429,503]);var fet=({attemptCount:e,retryOptions:t,error:r,retryAfter:s})=>{if(e>t.limit)return 0;let a=t.methods.includes(r.options.method),n=t.errorCodes.includes(r.code),c=r.response&&t.statusCodes.includes(r.response.statusCode);if(!a||!n&&!c)return 0;if(r.response){if(s)return t.maxRetryAfter===void 0||s>t.maxRetryAfter?0:s;if(r.response.statusCode===413)return 0}let f=Math.random()*100;return 2**(e-1)*1e3+f};WB.default=fet});var JB=G(Un=>{"use strict";Object.defineProperty(Un,"__esModule",{value:!0});Un.UnsupportedProtocolError=Un.ReadError=Un.TimeoutError=Un.UploadError=Un.CacheError=Un.HTTPError=Un.MaxRedirectsError=Un.RequestError=Un.setNonEnumerableProperties=Un.knownHookEvents=Un.withoutBody=Un.kIsNormalizedAlready=void 0;var Lfe=Ie("util"),Mfe=Ie("stream"),Aet=Ie("fs"),C0=Ie("url"),Ufe=Ie("http"),FH=Ie("http"),pet=Ie("https"),het=$ce(),det=oue(),_fe=Uue(),get=Gue(),met=yfe(),yet=WQ(),lt=Lp(),Eet=Bfe(),Hfe=IH(),Iet=BH(),jfe=Dfe(),Cet=Pfe(),Gfe=xfe(),wet=kfe(),Bet=kH(),qfe=Rfe(),vet=QH(),w0=Ffe(),Det=Nfe(),bet=Ofe(),NH,Eo=Symbol("request"),$Q=Symbol("response"),II=Symbol("responseSize"),CI=Symbol("downloadedSize"),wI=Symbol("bodySize"),BI=Symbol("uploadedSize"),XQ=Symbol("serverResponsesPiped"),Wfe=Symbol("unproxyEvents"),Yfe=Symbol("isFromCache"),OH=Symbol("cancelTimeouts"),Vfe=Symbol("startedReading"),vI=Symbol("stopReading"),ZQ=Symbol("triggerRead"),B0=Symbol("body"),YB=Symbol("jobs"),Jfe=Symbol("originalResponse"),Kfe=Symbol("retryTimeout");Un.kIsNormalizedAlready=Symbol("isNormalizedAlready");var Pet=lt.default.string(process.versions.brotli);Un.withoutBody=new Set(["GET","HEAD"]);Un.knownHookEvents=["init","beforeRequest","beforeRedirect","beforeError","beforeRetry","afterResponse"];function xet(e){for(let t in e){let r=e[t];if(!lt.default.string(r)&&!lt.default.number(r)&&!lt.default.boolean(r)&&!lt.default.null_(r)&&!lt.default.undefined(r))throw new TypeError(`The \`searchParams\` value '${String(r)}' must be a string, number, boolean or null`)}}function ket(e){return lt.default.object(e)&&!("statusCode"in e)}var LH=new wet.default,Qet=async e=>new Promise((t,r)=>{let s=a=>{r(a)};e.pending||t(),e.once("error",s),e.once("ready",()=>{e.off("error",s),t()})}),Ret=new Set([300,301,302,303,304,307,308]),Tet=["context","body","json","form"];Un.setNonEnumerableProperties=(e,t)=>{let r={};for(let s of e)if(s)for(let a of Tet)a in s&&(r[a]={writable:!0,configurable:!0,enumerable:!1,value:s[a]});Object.defineProperties(t,r)};var As=class extends Error{constructor(t,r,s){var a;if(super(t),Error.captureStackTrace(this,this.constructor),this.name="RequestError",this.code=r.code,s instanceof oR?(Object.defineProperty(this,"request",{enumerable:!1,value:s}),Object.defineProperty(this,"response",{enumerable:!1,value:s[$Q]}),Object.defineProperty(this,"options",{enumerable:!1,value:s.options})):Object.defineProperty(this,"options",{enumerable:!1,value:s}),this.timings=(a=this.request)===null||a===void 0?void 0:a.timings,lt.default.string(r.stack)&<.default.string(this.stack)){let n=this.stack.indexOf(this.message)+this.message.length,c=this.stack.slice(n).split(` `).reverse(),f=r.stack.slice(r.stack.indexOf(r.message)+r.message.length).split(` `).reverse();for(;f.length!==0&&f[0]===c[0];)c.shift();this.stack=`${this.stack.slice(0,n)}${c.reverse().join(` `)}${f.reverse().join(` `)}`}}};Un.RequestError=As;var eR=class extends As{constructor(t){super(`Redirected ${t.options.maxRedirects} times. Aborting.`,{},t),this.name="MaxRedirectsError"}};Un.MaxRedirectsError=eR;var tR=class extends As{constructor(t){super(`Response code ${t.statusCode} (${t.statusMessage})`,{},t.request),this.name="HTTPError"}};Un.HTTPError=tR;var rR=class extends As{constructor(t,r){super(t.message,t,r),this.name="CacheError"}};Un.CacheError=rR;var nR=class extends As{constructor(t,r){super(t.message,t,r),this.name="UploadError"}};Un.UploadError=nR;var iR=class extends As{constructor(t,r,s){super(t.message,t,s),this.name="TimeoutError",this.event=t.event,this.timings=r}};Un.TimeoutError=iR;var VB=class extends As{constructor(t,r){super(t.message,t,r),this.name="ReadError"}};Un.ReadError=VB;var sR=class extends As{constructor(t){super(`Unsupported protocol "${t.url.protocol}"`,{},t),this.name="UnsupportedProtocolError"}};Un.UnsupportedProtocolError=sR;var Fet=["socket","connect","continue","information","upgrade","timeout"],oR=class extends Mfe.Duplex{constructor(t,r={},s){super({autoDestroy:!1,highWaterMark:0}),this[CI]=0,this[BI]=0,this.requestInitialized=!1,this[XQ]=new Set,this.redirects=[],this[vI]=!1,this[ZQ]=!1,this[YB]=[],this.retryCount=0,this._progressCallbacks=[];let a=()=>this._unlockWrite(),n=()=>this._lockWrite();this.on("pipe",h=>{h.prependListener("data",a),h.on("data",n),h.prependListener("end",a),h.on("end",n)}),this.on("unpipe",h=>{h.off("data",a),h.off("data",n),h.off("end",a),h.off("end",n)}),this.on("pipe",h=>{h instanceof FH.IncomingMessage&&(this.options.headers={...h.headers,...this.options.headers})});let{json:c,body:f,form:p}=r;if((c||f||p)&&this._lockWrite(),Un.kIsNormalizedAlready in r)this.options=r;else try{this.options=this.constructor.normalizeArguments(t,r,s)}catch(h){lt.default.nodeStream(r.body)&&r.body.destroy(),this.destroy(h);return}(async()=>{var h;try{this.options.body instanceof Aet.ReadStream&&await Qet(this.options.body);let{url:E}=this.options;if(!E)throw new TypeError("Missing `url` property");if(this.requestUrl=E.toString(),decodeURI(this.requestUrl),await this._finalizeBody(),await this._makeRequest(),this.destroyed){(h=this[Eo])===null||h===void 0||h.destroy();return}for(let C of this[YB])C();this[YB].length=0,this.requestInitialized=!0}catch(E){if(E instanceof As){this._beforeError(E);return}this.destroyed||this.destroy(E)}})()}static normalizeArguments(t,r,s){var a,n,c,f,p;let h=r;if(lt.default.object(t)&&!lt.default.urlInstance(t))r={...s,...t,...r};else{if(t&&r&&r.url!==void 0)throw new TypeError("The `url` option is mutually exclusive with the `input` argument");r={...s,...r},t!==void 0&&(r.url=t),lt.default.urlInstance(r.url)&&(r.url=new C0.URL(r.url.toString()))}if(r.cache===!1&&(r.cache=void 0),r.dnsCache===!1&&(r.dnsCache=void 0),lt.assert.any([lt.default.string,lt.default.undefined],r.method),lt.assert.any([lt.default.object,lt.default.undefined],r.headers),lt.assert.any([lt.default.string,lt.default.urlInstance,lt.default.undefined],r.prefixUrl),lt.assert.any([lt.default.object,lt.default.undefined],r.cookieJar),lt.assert.any([lt.default.object,lt.default.string,lt.default.undefined],r.searchParams),lt.assert.any([lt.default.object,lt.default.string,lt.default.undefined],r.cache),lt.assert.any([lt.default.object,lt.default.number,lt.default.undefined],r.timeout),lt.assert.any([lt.default.object,lt.default.undefined],r.context),lt.assert.any([lt.default.object,lt.default.undefined],r.hooks),lt.assert.any([lt.default.boolean,lt.default.undefined],r.decompress),lt.assert.any([lt.default.boolean,lt.default.undefined],r.ignoreInvalidCookies),lt.assert.any([lt.default.boolean,lt.default.undefined],r.followRedirect),lt.assert.any([lt.default.number,lt.default.undefined],r.maxRedirects),lt.assert.any([lt.default.boolean,lt.default.undefined],r.throwHttpErrors),lt.assert.any([lt.default.boolean,lt.default.undefined],r.http2),lt.assert.any([lt.default.boolean,lt.default.undefined],r.allowGetBody),lt.assert.any([lt.default.string,lt.default.undefined],r.localAddress),lt.assert.any([qfe.isDnsLookupIpVersion,lt.default.undefined],r.dnsLookupIpVersion),lt.assert.any([lt.default.object,lt.default.undefined],r.https),lt.assert.any([lt.default.boolean,lt.default.undefined],r.rejectUnauthorized),r.https&&(lt.assert.any([lt.default.boolean,lt.default.undefined],r.https.rejectUnauthorized),lt.assert.any([lt.default.function_,lt.default.undefined],r.https.checkServerIdentity),lt.assert.any([lt.default.string,lt.default.object,lt.default.array,lt.default.undefined],r.https.certificateAuthority),lt.assert.any([lt.default.string,lt.default.object,lt.default.array,lt.default.undefined],r.https.key),lt.assert.any([lt.default.string,lt.default.object,lt.default.array,lt.default.undefined],r.https.certificate),lt.assert.any([lt.default.string,lt.default.undefined],r.https.passphrase),lt.assert.any([lt.default.string,lt.default.buffer,lt.default.array,lt.default.undefined],r.https.pfx)),lt.assert.any([lt.default.object,lt.default.undefined],r.cacheOptions),lt.default.string(r.method)?r.method=r.method.toUpperCase():r.method="GET",r.headers===s?.headers?r.headers={...r.headers}:r.headers=yet({...s?.headers,...r.headers}),"slashes"in r)throw new TypeError("The legacy `url.Url` has been deprecated. Use `URL` instead.");if("auth"in r)throw new TypeError("Parameter `auth` is deprecated. Use `username` / `password` instead.");if("searchParams"in r&&r.searchParams&&r.searchParams!==s?.searchParams){let x;if(lt.default.string(r.searchParams)||r.searchParams instanceof C0.URLSearchParams)x=new C0.URLSearchParams(r.searchParams);else{xet(r.searchParams),x=new C0.URLSearchParams;for(let I in r.searchParams){let T=r.searchParams[I];T===null?x.append(I,""):T!==void 0&&x.append(I,T)}}(a=s?.searchParams)===null||a===void 0||a.forEach((I,T)=>{x.has(T)||x.append(T,I)}),r.searchParams=x}if(r.username=(n=r.username)!==null&&n!==void 0?n:"",r.password=(c=r.password)!==null&&c!==void 0?c:"",lt.default.undefined(r.prefixUrl)?r.prefixUrl=(f=s?.prefixUrl)!==null&&f!==void 0?f:"":(r.prefixUrl=r.prefixUrl.toString(),r.prefixUrl!==""&&!r.prefixUrl.endsWith("/")&&(r.prefixUrl+="/")),lt.default.string(r.url)){if(r.url.startsWith("/"))throw new Error("`input` must not start with a slash when using `prefixUrl`");r.url=Gfe.default(r.prefixUrl+r.url,r)}else(lt.default.undefined(r.url)&&r.prefixUrl!==""||r.protocol)&&(r.url=Gfe.default(r.prefixUrl,r));if(r.url){"port"in r&&delete r.port;let{prefixUrl:x}=r;Object.defineProperty(r,"prefixUrl",{set:T=>{let O=r.url;if(!O.href.startsWith(T))throw new Error(`Cannot change \`prefixUrl\` from ${x} to ${T}: ${O.href}`);r.url=new C0.URL(T+O.href.slice(x.length)),x=T},get:()=>x});let{protocol:I}=r.url;if(I==="unix:"&&(I="http:",r.url=new C0.URL(`http://unix${r.url.pathname}${r.url.search}`)),r.searchParams&&(r.url.search=r.searchParams.toString()),I!=="http:"&&I!=="https:")throw new sR(r);r.username===""?r.username=r.url.username:r.url.username=r.username,r.password===""?r.password=r.url.password:r.url.password=r.password}let{cookieJar:E}=r;if(E){let{setCookie:x,getCookieString:I}=E;lt.assert.function_(x),lt.assert.function_(I),x.length===4&&I.length===0&&(x=Lfe.promisify(x.bind(r.cookieJar)),I=Lfe.promisify(I.bind(r.cookieJar)),r.cookieJar={setCookie:x,getCookieString:I})}let{cache:C}=r;if(C&&(LH.has(C)||LH.set(C,new _fe((x,I)=>{let T=x[Eo](x,I);return lt.default.promise(T)&&(T.once=(O,U)=>{if(O==="error")T.catch(U);else if(O==="abort")(async()=>{try{(await T).once("abort",U)}catch{}})();else throw new Error(`Unknown HTTP2 promise event: ${O}`);return T}),T},C))),r.cacheOptions={...r.cacheOptions},r.dnsCache===!0)NH||(NH=new det.default),r.dnsCache=NH;else if(!lt.default.undefined(r.dnsCache)&&!r.dnsCache.lookup)throw new TypeError(`Parameter \`dnsCache\` must be a CacheableLookup instance or a boolean, got ${lt.default(r.dnsCache)}`);lt.default.number(r.timeout)?r.timeout={request:r.timeout}:s&&r.timeout!==s.timeout?r.timeout={...s.timeout,...r.timeout}:r.timeout={...r.timeout},r.context||(r.context={});let S=r.hooks===s?.hooks;r.hooks={...r.hooks};for(let x of Un.knownHookEvents)if(x in r.hooks)if(lt.default.array(r.hooks[x]))r.hooks[x]=[...r.hooks[x]];else throw new TypeError(`Parameter \`${x}\` must be an Array, got ${lt.default(r.hooks[x])}`);else r.hooks[x]=[];if(s&&!S)for(let x of Un.knownHookEvents)s.hooks[x].length>0&&(r.hooks[x]=[...s.hooks[x],...r.hooks[x]]);if("family"in r&&w0.default('"options.family" was never documented, please use "options.dnsLookupIpVersion"'),s?.https&&(r.https={...s.https,...r.https}),"rejectUnauthorized"in r&&w0.default('"options.rejectUnauthorized" is now deprecated, please use "options.https.rejectUnauthorized"'),"checkServerIdentity"in r&&w0.default('"options.checkServerIdentity" was never documented, please use "options.https.checkServerIdentity"'),"ca"in r&&w0.default('"options.ca" was never documented, please use "options.https.certificateAuthority"'),"key"in r&&w0.default('"options.key" was never documented, please use "options.https.key"'),"cert"in r&&w0.default('"options.cert" was never documented, please use "options.https.certificate"'),"passphrase"in r&&w0.default('"options.passphrase" was never documented, please use "options.https.passphrase"'),"pfx"in r&&w0.default('"options.pfx" was never documented, please use "options.https.pfx"'),"followRedirects"in r)throw new TypeError("The `followRedirects` option does not exist. Use `followRedirect` instead.");if(r.agent){for(let x in r.agent)if(x!=="http"&&x!=="https"&&x!=="http2")throw new TypeError(`Expected the \`options.agent\` properties to be \`http\`, \`https\` or \`http2\`, got \`${x}\``)}return r.maxRedirects=(p=r.maxRedirects)!==null&&p!==void 0?p:0,Un.setNonEnumerableProperties([s,h],r),Det.default(r,s)}_lockWrite(){let t=()=>{throw new TypeError("The payload has been already provided")};this.write=t,this.end=t}_unlockWrite(){this.write=super.write,this.end=super.end}async _finalizeBody(){let{options:t}=this,{headers:r}=t,s=!lt.default.undefined(t.form),a=!lt.default.undefined(t.json),n=!lt.default.undefined(t.body),c=s||a||n,f=Un.withoutBody.has(t.method)&&!(t.method==="GET"&&t.allowGetBody);if(this._cannotHaveBody=f,c){if(f)throw new TypeError(`The \`${t.method}\` method cannot be used with a body`);if([n,s,a].filter(p=>p).length>1)throw new TypeError("The `body`, `json` and `form` options are mutually exclusive");if(n&&!(t.body instanceof Mfe.Readable)&&!lt.default.string(t.body)&&!lt.default.buffer(t.body)&&!Hfe.default(t.body))throw new TypeError("The `body` option must be a stream.Readable, string or Buffer");if(s&&!lt.default.object(t.form))throw new TypeError("The `form` option must be an Object");{let p=!lt.default.string(r["content-type"]);n?(Hfe.default(t.body)&&p&&(r["content-type"]=`multipart/form-data; boundary=${t.body.getBoundary()}`),this[B0]=t.body):s?(p&&(r["content-type"]="application/x-www-form-urlencoded"),this[B0]=new C0.URLSearchParams(t.form).toString()):(p&&(r["content-type"]="application/json"),this[B0]=t.stringifyJson(t.json));let h=await Eet.default(this[B0],t.headers);lt.default.undefined(r["content-length"])&<.default.undefined(r["transfer-encoding"])&&!f&&!lt.default.undefined(h)&&(r["content-length"]=String(h))}}else f?this._lockWrite():this._unlockWrite();this[wI]=Number(r["content-length"])||void 0}async _onResponseBase(t){let{options:r}=this,{url:s}=r;this[Jfe]=t,r.decompress&&(t=get(t));let a=t.statusCode,n=t;n.statusMessage=n.statusMessage?n.statusMessage:Ufe.STATUS_CODES[a],n.url=r.url.toString(),n.requestUrl=this.requestUrl,n.redirectUrls=this.redirects,n.request=this,n.isFromCache=t.fromCache||!1,n.ip=this.ip,n.retryCount=this.retryCount,this[Yfe]=n.isFromCache,this[II]=Number(t.headers["content-length"])||void 0,this[$Q]=t,t.once("end",()=>{this[II]=this[CI],this.emit("downloadProgress",this.downloadProgress)}),t.once("error",f=>{t.destroy(),this._beforeError(new VB(f,this))}),t.once("aborted",()=>{this._beforeError(new VB({name:"Error",message:"The server aborted pending request",code:"ECONNRESET"},this))}),this.emit("downloadProgress",this.downloadProgress);let c=t.headers["set-cookie"];if(lt.default.object(r.cookieJar)&&c){let f=c.map(async p=>r.cookieJar.setCookie(p,s.toString()));r.ignoreInvalidCookies&&(f=f.map(async p=>p.catch(()=>{})));try{await Promise.all(f)}catch(p){this._beforeError(p);return}}if(r.followRedirect&&t.headers.location&&Ret.has(a)){if(t.resume(),this[Eo]&&(this[OH](),delete this[Eo],this[Wfe]()),(a===303&&r.method!=="GET"&&r.method!=="HEAD"||!r.methodRewriting)&&(r.method="GET","body"in r&&delete r.body,"json"in r&&delete r.json,"form"in r&&delete r.form,this[B0]=void 0,delete r.headers["content-length"]),this.redirects.length>=r.maxRedirects){this._beforeError(new eR(this));return}try{let p=Buffer.from(t.headers.location,"binary").toString(),h=new C0.URL(p,s),E=h.toString();decodeURI(E),h.hostname!==s.hostname||h.port!==s.port?("host"in r.headers&&delete r.headers.host,"cookie"in r.headers&&delete r.headers.cookie,"authorization"in r.headers&&delete r.headers.authorization,(r.username||r.password)&&(r.username="",r.password="")):(h.username=r.username,h.password=r.password),this.redirects.push(E),r.url=h;for(let C of r.hooks.beforeRedirect)await C(r,n);this.emit("redirect",n,r),await this._makeRequest()}catch(p){this._beforeError(p);return}return}if(r.isStream&&r.throwHttpErrors&&!vet.isResponseOk(n)){this._beforeError(new tR(n));return}t.on("readable",()=>{this[ZQ]&&this._read()}),this.on("resume",()=>{t.resume()}),this.on("pause",()=>{t.pause()}),t.once("end",()=>{this.push(null)}),this.emit("response",t);for(let f of this[XQ])if(!f.headersSent){for(let p in t.headers){let h=r.decompress?p!=="content-encoding":!0,E=t.headers[p];h&&f.setHeader(p,E)}f.statusCode=a}}async _onResponse(t){try{await this._onResponseBase(t)}catch(r){this._beforeError(r)}}_onRequest(t){let{options:r}=this,{timeout:s,url:a}=r;het.default(t),this[OH]=jfe.default(t,s,a);let n=r.cache?"cacheableResponse":"response";t.once(n,p=>{this._onResponse(p)}),t.once("error",p=>{var h;t.destroy(),(h=t.res)===null||h===void 0||h.removeAllListeners("end"),p=p instanceof jfe.TimeoutError?new iR(p,this.timings,this):new As(p.message,p,this),this._beforeError(p)}),this[Wfe]=Iet.default(t,this,Fet),this[Eo]=t,this.emit("uploadProgress",this.uploadProgress);let c=this[B0],f=this.redirects.length===0?this:t;lt.default.nodeStream(c)?(c.pipe(f),c.once("error",p=>{this._beforeError(new nR(p,this))})):(this._unlockWrite(),lt.default.undefined(c)?(this._cannotHaveBody||this._noPipe)&&(f.end(),this._lockWrite()):(this._writeRequest(c,void 0,()=>{}),f.end(),this._lockWrite())),this.emit("request",t)}async _createCacheableRequest(t,r){return new Promise((s,a)=>{Object.assign(r,Cet.default(t)),delete r.url;let n,c=LH.get(r.cache)(r,async f=>{f._readableState.autoDestroy=!1,n&&(await n).emit("cacheableResponse",f),s(f)});r.url=t,c.once("error",a),c.once("request",async f=>{n=f,s(n)})})}async _makeRequest(){var t,r,s,a,n;let{options:c}=this,{headers:f}=c;for(let U in f)if(lt.default.undefined(f[U]))delete f[U];else if(lt.default.null_(f[U]))throw new TypeError(`Use \`undefined\` instead of \`null\` to delete the \`${U}\` header`);if(c.decompress&<.default.undefined(f["accept-encoding"])&&(f["accept-encoding"]=Pet?"gzip, deflate, br":"gzip, deflate"),c.cookieJar){let U=await c.cookieJar.getCookieString(c.url.toString());lt.default.nonEmptyString(U)&&(c.headers.cookie=U)}for(let U of c.hooks.beforeRequest){let V=await U(c);if(!lt.default.undefined(V)){c.request=()=>V;break}}c.body&&this[B0]!==c.body&&(this[B0]=c.body);let{agent:p,request:h,timeout:E,url:C}=c;if(c.dnsCache&&!("lookup"in c)&&(c.lookup=c.dnsCache.lookup),C.hostname==="unix"){let U=/(?.+?):(?.+)/.exec(`${C.pathname}${C.search}`);if(U?.groups){let{socketPath:V,path:te}=U.groups;Object.assign(c,{socketPath:V,path:te,host:""})}}let S=C.protocol==="https:",x;c.http2?x=met.auto:x=S?pet.request:Ufe.request;let I=(t=c.request)!==null&&t!==void 0?t:x,T=c.cache?this._createCacheableRequest:I;p&&!c.http2&&(c.agent=p[S?"https":"http"]),c[Eo]=I,delete c.request,delete c.timeout;let O=c;if(O.shared=(r=c.cacheOptions)===null||r===void 0?void 0:r.shared,O.cacheHeuristic=(s=c.cacheOptions)===null||s===void 0?void 0:s.cacheHeuristic,O.immutableMinTimeToLive=(a=c.cacheOptions)===null||a===void 0?void 0:a.immutableMinTimeToLive,O.ignoreCargoCult=(n=c.cacheOptions)===null||n===void 0?void 0:n.ignoreCargoCult,c.dnsLookupIpVersion!==void 0)try{O.family=qfe.dnsLookupIpVersionToFamily(c.dnsLookupIpVersion)}catch{throw new Error("Invalid `dnsLookupIpVersion` option value")}c.https&&("rejectUnauthorized"in c.https&&(O.rejectUnauthorized=c.https.rejectUnauthorized),c.https.checkServerIdentity&&(O.checkServerIdentity=c.https.checkServerIdentity),c.https.certificateAuthority&&(O.ca=c.https.certificateAuthority),c.https.certificate&&(O.cert=c.https.certificate),c.https.key&&(O.key=c.https.key),c.https.passphrase&&(O.passphrase=c.https.passphrase),c.https.pfx&&(O.pfx=c.https.pfx));try{let U=await T(C,O);lt.default.undefined(U)&&(U=x(C,O)),c.request=h,c.timeout=E,c.agent=p,c.https&&("rejectUnauthorized"in c.https&&delete O.rejectUnauthorized,c.https.checkServerIdentity&&delete O.checkServerIdentity,c.https.certificateAuthority&&delete O.ca,c.https.certificate&&delete O.cert,c.https.key&&delete O.key,c.https.passphrase&&delete O.passphrase,c.https.pfx&&delete O.pfx),ket(U)?this._onRequest(U):this.writable?(this.once("finish",()=>{this._onResponse(U)}),this._unlockWrite(),this.end(),this._lockWrite()):this._onResponse(U)}catch(U){throw U instanceof _fe.CacheError?new rR(U,this):new As(U.message,U,this)}}async _error(t){try{for(let r of this.options.hooks.beforeError)t=await r(t)}catch(r){t=new As(r.message,r,this)}this.destroy(t)}_beforeError(t){if(this[vI])return;let{options:r}=this,s=this.retryCount+1;this[vI]=!0,t instanceof As||(t=new As(t.message,t,this));let a=t,{response:n}=a;(async()=>{if(n&&!n.body){n.setEncoding(this._readableState.encoding);try{n.rawBody=await Bet.default(n),n.body=n.rawBody.toString()}catch{}}if(this.listenerCount("retry")!==0){let c;try{let f;n&&"retry-after"in n.headers&&(f=Number(n.headers["retry-after"]),Number.isNaN(f)?(f=Date.parse(n.headers["retry-after"])-Date.now(),f<=0&&(f=1)):f*=1e3),c=await r.retry.calculateDelay({attemptCount:s,retryOptions:r.retry,error:a,retryAfter:f,computedValue:bet.default({attemptCount:s,retryOptions:r.retry,error:a,retryAfter:f,computedValue:0})})}catch(f){this._error(new As(f.message,f,this));return}if(c){let f=async()=>{try{for(let p of this.options.hooks.beforeRetry)await p(this.options,a,s)}catch(p){this._error(new As(p.message,t,this));return}this.destroyed||(this.destroy(),this.emit("retry",s,t))};this[Kfe]=setTimeout(f,c);return}}this._error(a)})()}_read(){this[ZQ]=!0;let t=this[$Q];if(t&&!this[vI]){t.readableLength&&(this[ZQ]=!1);let r;for(;(r=t.read())!==null;){this[CI]+=r.length,this[Vfe]=!0;let s=this.downloadProgress;s.percent<1&&this.emit("downloadProgress",s),this.push(r)}}}_write(t,r,s){let a=()=>{this._writeRequest(t,r,s)};this.requestInitialized?a():this[YB].push(a)}_writeRequest(t,r,s){this[Eo].destroyed||(this._progressCallbacks.push(()=>{this[BI]+=Buffer.byteLength(t,r);let a=this.uploadProgress;a.percent<1&&this.emit("uploadProgress",a)}),this[Eo].write(t,r,a=>{!a&&this._progressCallbacks.length>0&&this._progressCallbacks.shift()(),s(a)}))}_final(t){let r=()=>{for(;this._progressCallbacks.length!==0;)this._progressCallbacks.shift()();if(!(Eo in this)){t();return}if(this[Eo].destroyed){t();return}this[Eo].end(s=>{s||(this[wI]=this[BI],this.emit("uploadProgress",this.uploadProgress),this[Eo].emit("upload-complete")),t(s)})};this.requestInitialized?r():this[YB].push(r)}_destroy(t,r){var s;this[vI]=!0,clearTimeout(this[Kfe]),Eo in this&&(this[OH](),!((s=this[$Q])===null||s===void 0)&&s.complete||this[Eo].destroy()),t!==null&&!lt.default.undefined(t)&&!(t instanceof As)&&(t=new As(t.message,t,this)),r(t)}get _isAboutToError(){return this[vI]}get ip(){var t;return(t=this.socket)===null||t===void 0?void 0:t.remoteAddress}get aborted(){var t,r,s;return((r=(t=this[Eo])===null||t===void 0?void 0:t.destroyed)!==null&&r!==void 0?r:this.destroyed)&&!(!((s=this[Jfe])===null||s===void 0)&&s.complete)}get socket(){var t,r;return(r=(t=this[Eo])===null||t===void 0?void 0:t.socket)!==null&&r!==void 0?r:void 0}get downloadProgress(){let t;return this[II]?t=this[CI]/this[II]:this[II]===this[CI]?t=1:t=0,{percent:t,transferred:this[CI],total:this[II]}}get uploadProgress(){let t;return this[wI]?t=this[BI]/this[wI]:this[wI]===this[BI]?t=1:t=0,{percent:t,transferred:this[BI],total:this[wI]}}get timings(){var t;return(t=this[Eo])===null||t===void 0?void 0:t.timings}get isFromCache(){return this[Yfe]}pipe(t,r){if(this[Vfe])throw new Error("Failed to pipe. The response has been emitted already.");return t instanceof FH.ServerResponse&&this[XQ].add(t),super.pipe(t,r)}unpipe(t){return t instanceof FH.ServerResponse&&this[XQ].delete(t),super.unpipe(t),this}};Un.default=oR});var KB=G(ju=>{"use strict";var Net=ju&&ju.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r),Object.defineProperty(e,s,{enumerable:!0,get:function(){return t[r]}})}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),Oet=ju&&ju.__exportStar||function(e,t){for(var r in e)r!=="default"&&!Object.prototype.hasOwnProperty.call(t,r)&&Net(t,e,r)};Object.defineProperty(ju,"__esModule",{value:!0});ju.CancelError=ju.ParseError=void 0;var zfe=JB(),MH=class extends zfe.RequestError{constructor(t,r){let{options:s}=r.request;super(`${t.message} in "${s.url.toString()}"`,t,r.request),this.name="ParseError"}};ju.ParseError=MH;var UH=class extends zfe.RequestError{constructor(t){super("Promise was canceled",{},t),this.name="CancelError"}get isCanceled(){return!0}};ju.CancelError=UH;Oet(JB(),ju)});var Zfe=G(_H=>{"use strict";Object.defineProperty(_H,"__esModule",{value:!0});var Xfe=KB(),Let=(e,t,r,s)=>{let{rawBody:a}=e;try{if(t==="text")return a.toString(s);if(t==="json")return a.length===0?"":r(a.toString());if(t==="buffer")return a;throw new Xfe.ParseError({message:`Unknown body type '${t}'`,name:"Error"},e)}catch(n){throw new Xfe.ParseError(n,e)}};_H.default=Let});var HH=G(v0=>{"use strict";var Met=v0&&v0.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r),Object.defineProperty(e,s,{enumerable:!0,get:function(){return t[r]}})}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),Uet=v0&&v0.__exportStar||function(e,t){for(var r in e)r!=="default"&&!Object.prototype.hasOwnProperty.call(t,r)&&Met(t,e,r)};Object.defineProperty(v0,"__esModule",{value:!0});var _et=Ie("events"),Het=Lp(),jet=Xce(),aR=KB(),$fe=Zfe(),eAe=JB(),Get=BH(),qet=kH(),tAe=QH(),Wet=["request","response","redirect","uploadProgress","downloadProgress"];function rAe(e){let t,r,s=new _et.EventEmitter,a=new jet((c,f,p)=>{let h=E=>{let C=new eAe.default(void 0,e);C.retryCount=E,C._noPipe=!0,p(()=>C.destroy()),p.shouldReject=!1,p(()=>f(new aR.CancelError(C))),t=C,C.once("response",async I=>{var T;if(I.retryCount=E,I.request.aborted)return;let O;try{O=await qet.default(C),I.rawBody=O}catch{return}if(C._isAboutToError)return;let U=((T=I.headers["content-encoding"])!==null&&T!==void 0?T:"").toLowerCase(),V=["gzip","deflate","br"].includes(U),{options:te}=C;if(V&&!te.decompress)I.body=O;else try{I.body=$fe.default(I,te.responseType,te.parseJson,te.encoding)}catch(ie){if(I.body=O.toString(),tAe.isResponseOk(I)){C._beforeError(ie);return}}try{for(let[ie,ue]of te.hooks.afterResponse.entries())I=await ue(I,async ae=>{let ge=eAe.default.normalizeArguments(void 0,{...ae,retry:{calculateDelay:()=>0},throwHttpErrors:!1,resolveBodyOnly:!1},te);ge.hooks.afterResponse=ge.hooks.afterResponse.slice(0,ie);for(let Ce of ge.hooks.beforeRetry)await Ce(ge);let Ae=rAe(ge);return p(()=>{Ae.catch(()=>{}),Ae.cancel()}),Ae})}catch(ie){C._beforeError(new aR.RequestError(ie.message,ie,C));return}if(!tAe.isResponseOk(I)){C._beforeError(new aR.HTTPError(I));return}r=I,c(C.options.resolveBodyOnly?I.body:I)});let S=I=>{if(a.isCanceled)return;let{options:T}=C;if(I instanceof aR.HTTPError&&!T.throwHttpErrors){let{response:O}=I;c(C.options.resolveBodyOnly?O.body:O);return}f(I)};C.once("error",S);let x=C.options.body;C.once("retry",(I,T)=>{var O,U;if(x===((O=T.request)===null||O===void 0?void 0:O.options.body)&&Het.default.nodeStream((U=T.request)===null||U===void 0?void 0:U.options.body)){S(T);return}h(I)}),Get.default(C,s,Wet)};h(0)});a.on=(c,f)=>(s.on(c,f),a);let n=c=>{let f=(async()=>{await a;let{options:p}=r.request;return $fe.default(r,c,p.parseJson,p.encoding)})();return Object.defineProperties(f,Object.getOwnPropertyDescriptors(a)),f};return a.json=()=>{let{headers:c}=t.options;return!t.writableFinished&&c.accept===void 0&&(c.accept="application/json"),n("json")},a.buffer=()=>n("buffer"),a.text=()=>n("text"),a}v0.default=rAe;Uet(KB(),v0)});var nAe=G(jH=>{"use strict";Object.defineProperty(jH,"__esModule",{value:!0});var Yet=KB();function Vet(e,...t){let r=(async()=>{if(e instanceof Yet.RequestError)try{for(let a of t)if(a)for(let n of a)e=await n(e)}catch(a){e=a}throw e})(),s=()=>r;return r.json=s,r.text=s,r.buffer=s,r.on=s,r}jH.default=Vet});var oAe=G(GH=>{"use strict";Object.defineProperty(GH,"__esModule",{value:!0});var iAe=Lp();function sAe(e){for(let t of Object.values(e))(iAe.default.plainObject(t)||iAe.default.array(t))&&sAe(t);return Object.freeze(e)}GH.default=sAe});var lAe=G(aAe=>{"use strict";Object.defineProperty(aAe,"__esModule",{value:!0})});var qH=G(Lc=>{"use strict";var Jet=Lc&&Lc.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r),Object.defineProperty(e,s,{enumerable:!0,get:function(){return t[r]}})}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),Ket=Lc&&Lc.__exportStar||function(e,t){for(var r in e)r!=="default"&&!Object.prototype.hasOwnProperty.call(t,r)&&Jet(t,e,r)};Object.defineProperty(Lc,"__esModule",{value:!0});Lc.defaultHandler=void 0;var cAe=Lp(),Oc=HH(),zet=nAe(),cR=JB(),Xet=oAe(),Zet={RequestError:Oc.RequestError,CacheError:Oc.CacheError,ReadError:Oc.ReadError,HTTPError:Oc.HTTPError,MaxRedirectsError:Oc.MaxRedirectsError,TimeoutError:Oc.TimeoutError,ParseError:Oc.ParseError,CancelError:Oc.CancelError,UnsupportedProtocolError:Oc.UnsupportedProtocolError,UploadError:Oc.UploadError},$et=async e=>new Promise(t=>{setTimeout(t,e)}),{normalizeArguments:lR}=cR.default,uAe=(...e)=>{let t;for(let r of e)t=lR(void 0,r,t);return t},ett=e=>e.isStream?new cR.default(void 0,e):Oc.default(e),ttt=e=>"defaults"in e&&"options"in e.defaults,rtt=["get","post","put","patch","head","delete"];Lc.defaultHandler=(e,t)=>t(e);var fAe=(e,t)=>{if(e)for(let r of e)r(t)},AAe=e=>{e._rawHandlers=e.handlers,e.handlers=e.handlers.map(s=>(a,n)=>{let c,f=s(a,p=>(c=n(p),c));if(f!==c&&!a.isStream&&c){let p=f,{then:h,catch:E,finally:C}=p;Object.setPrototypeOf(p,Object.getPrototypeOf(c)),Object.defineProperties(p,Object.getOwnPropertyDescriptors(c)),p.then=h,p.catch=E,p.finally=C}return f});let t=(s,a={},n)=>{var c,f;let p=0,h=E=>e.handlers[p++](E,p===e.handlers.length?ett:h);if(cAe.default.plainObject(s)){let E={...s,...a};cR.setNonEnumerableProperties([s,a],E),a=E,s=void 0}try{let E;try{fAe(e.options.hooks.init,a),fAe((c=a.hooks)===null||c===void 0?void 0:c.init,a)}catch(S){E=S}let C=lR(s,a,n??e.options);if(C[cR.kIsNormalizedAlready]=!0,E)throw new Oc.RequestError(E.message,E,C);return h(C)}catch(E){if(a.isStream)throw E;return zet.default(E,e.options.hooks.beforeError,(f=a.hooks)===null||f===void 0?void 0:f.beforeError)}};t.extend=(...s)=>{let a=[e.options],n=[...e._rawHandlers],c;for(let f of s)ttt(f)?(a.push(f.defaults.options),n.push(...f.defaults._rawHandlers),c=f.defaults.mutableDefaults):(a.push(f),"handlers"in f&&n.push(...f.handlers),c=f.mutableDefaults);return n=n.filter(f=>f!==Lc.defaultHandler),n.length===0&&n.push(Lc.defaultHandler),AAe({options:uAe(...a),handlers:n,mutableDefaults:!!c})};let r=async function*(s,a){let n=lR(s,a,e.options);n.resolveBodyOnly=!1;let c=n.pagination;if(!cAe.default.object(c))throw new TypeError("`options.pagination` must be implemented");let f=[],{countLimit:p}=c,h=0;for(;h{let n=[];for await(let c of r(s,a))n.push(c);return n},t.paginate.each=r,t.stream=(s,a)=>t(s,{...a,isStream:!0});for(let s of rtt)t[s]=(a,n)=>t(a,{...n,method:s}),t.stream[s]=(a,n)=>t(a,{...n,method:s,isStream:!0});return Object.assign(t,Zet),Object.defineProperty(t,"defaults",{value:e.mutableDefaults?e:Xet.default(e),writable:e.mutableDefaults,configurable:e.mutableDefaults,enumerable:!0}),t.mergeOptions=uAe,t};Lc.default=AAe;Ket(lAe(),Lc)});var dAe=G((Mp,uR)=>{"use strict";var ntt=Mp&&Mp.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r),Object.defineProperty(e,s,{enumerable:!0,get:function(){return t[r]}})}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),pAe=Mp&&Mp.__exportStar||function(e,t){for(var r in e)r!=="default"&&!Object.prototype.hasOwnProperty.call(t,r)&&ntt(t,e,r)};Object.defineProperty(Mp,"__esModule",{value:!0});var itt=Ie("url"),hAe=qH(),stt={options:{method:"GET",retry:{limit:2,methods:["GET","PUT","HEAD","DELETE","OPTIONS","TRACE"],statusCodes:[408,413,429,500,502,503,504,521,522,524],errorCodes:["ETIMEDOUT","ECONNRESET","EADDRINUSE","ECONNREFUSED","EPIPE","ENOTFOUND","ENETUNREACH","EAI_AGAIN"],maxRetryAfter:void 0,calculateDelay:({computedValue:e})=>e},timeout:{},headers:{"user-agent":"got (https://github.com/sindresorhus/got)"},hooks:{init:[],beforeRequest:[],beforeRedirect:[],beforeRetry:[],beforeError:[],afterResponse:[]},cache:void 0,dnsCache:void 0,decompress:!0,throwHttpErrors:!0,followRedirect:!0,isStream:!1,responseType:"text",resolveBodyOnly:!1,maxRedirects:10,prefixUrl:"",methodRewriting:!0,ignoreInvalidCookies:!1,context:{},http2:!1,allowGetBody:!1,https:void 0,pagination:{transform:e=>e.request.options.responseType==="json"?e.body:JSON.parse(e.body),paginate:e=>{if(!Reflect.has(e.headers,"link"))return!1;let t=e.headers.link.split(","),r;for(let s of t){let a=s.split(";");if(a[1].includes("next")){r=a[0].trimStart().trim(),r=r.slice(1,-1);break}}return r?{url:new itt.URL(r)}:!1},filter:()=>!0,shouldContinue:()=>!0,countLimit:1/0,backoff:0,requestLimit:1e4,stackAllItems:!0},parseJson:e=>JSON.parse(e),stringifyJson:e=>JSON.stringify(e),cacheOptions:{}},handlers:[hAe.defaultHandler],mutableDefaults:!1},WH=hAe.default(stt);Mp.default=WH;uR.exports=WH;uR.exports.default=WH;uR.exports.__esModule=!0;pAe(qH(),Mp);pAe(HH(),Mp)});var nn={};Vt(nn,{Method:()=>CAe,del:()=>utt,get:()=>JH,getNetworkSettings:()=>IAe,post:()=>KH,put:()=>ctt,request:()=>zB});async function YH(e){return Zl(mAe,e,()=>le.readFilePromise(e).then(t=>(mAe.set(e,t),t)))}function ltt({statusCode:e,statusMessage:t},r){let s=jt(r,e,dt.NUMBER),a=`https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/${e}`;return ZE(r,`${s}${t?` (${t})`:""}`,a)}async function fR(e,{configuration:t,customErrorMessage:r}){try{return await e}catch(s){if(s.name!=="HTTPError")throw s;let a=r?.(s,t)??s.response.body?.error;a==null&&(s.message.startsWith("Response code")?a="The remote server failed to provide the requested resource":a=s.message),s.code==="ETIMEDOUT"&&s.event==="socket"&&(a+=`(can be increased via ${jt(t,"httpTimeout",dt.SETTING)})`);let n=new Lt(35,a,c=>{s.response&&c.reportError(35,` ${Zf(t,{label:"Response Code",value:Mu(dt.NO_HINT,ltt(s.response,t))})}`),s.request&&(c.reportError(35,` ${Zf(t,{label:"Request Method",value:Mu(dt.NO_HINT,s.request.options.method)})}`),c.reportError(35,` ${Zf(t,{label:"Request URL",value:Mu(dt.URL,s.request.requestUrl)})}`)),s.request.redirects.length>0&&c.reportError(35,` ${Zf(t,{label:"Request Redirects",value:Mu(dt.NO_HINT,O4(t,s.request.redirects,dt.URL))})}`),s.request.retryCount===s.request.options.retry.limit&&c.reportError(35,` ${Zf(t,{label:"Request Retry Count",value:Mu(dt.NO_HINT,`${jt(t,s.request.retryCount,dt.NUMBER)} (can be increased via ${jt(t,"httpRetry",dt.SETTING)})`)})}`)});throw n.originalError=s,n}}function IAe(e,t){let r=[...t.configuration.get("networkSettings")].sort(([c],[f])=>f.length-c.length),s={enableNetwork:void 0,httpsCaFilePath:void 0,httpProxy:void 0,httpsProxy:void 0,httpsKeyFilePath:void 0,httpsCertFilePath:void 0},a=Object.keys(s),n=typeof e=="string"?new URL(e):e;for(let[c,f]of r)if(VH.default.isMatch(n.hostname,c))for(let p of a){let h=f.get(p);h!==null&&typeof s[p]>"u"&&(s[p]=h)}for(let c of a)typeof s[c]>"u"&&(s[c]=t.configuration.get(c));return s}async function zB(e,t,{configuration:r,headers:s,jsonRequest:a,jsonResponse:n,method:c="GET",wrapNetworkRequest:f}){let p={target:e,body:t,configuration:r,headers:s,jsonRequest:a,jsonResponse:n,method:c},h=async()=>await ftt(e,t,p),E=typeof f<"u"?await f(h,p):h;return await(await r.reduceHook(S=>S.wrapNetworkRequest,E,p))()}async function JH(e,{configuration:t,jsonResponse:r,customErrorMessage:s,wrapNetworkRequest:a,...n}){let c=()=>fR(zB(e,null,{configuration:t,wrapNetworkRequest:a,...n}),{configuration:t,customErrorMessage:s}).then(p=>p.body),f=await(typeof a<"u"?c():Zl(gAe,e,()=>c().then(p=>(gAe.set(e,p),p))));return r?JSON.parse(f.toString()):f}async function ctt(e,t,{customErrorMessage:r,...s}){return(await fR(zB(e,t,{...s,method:"PUT"}),{customErrorMessage:r,configuration:s.configuration})).body}async function KH(e,t,{customErrorMessage:r,...s}){return(await fR(zB(e,t,{...s,method:"POST"}),{customErrorMessage:r,configuration:s.configuration})).body}async function utt(e,{customErrorMessage:t,...r}){return(await fR(zB(e,null,{...r,method:"DELETE"}),{customErrorMessage:t,configuration:r.configuration})).body}async function ftt(e,t,{configuration:r,headers:s,jsonRequest:a,jsonResponse:n,method:c="GET"}){let f=typeof e=="string"?new URL(e):e,p=IAe(f,{configuration:r});if(p.enableNetwork===!1)throw new Lt(80,`Request to '${f.href}' has been blocked because of your configuration settings`);if(f.protocol==="http:"&&!VH.default.isMatch(f.hostname,r.get("unsafeHttpWhitelist")))throw new Lt(81,`Unsafe http requests must be explicitly whitelisted in your configuration (${f.hostname})`);let h={headers:s,method:c};h.responseType=n?"json":"buffer",t!==null&&(Buffer.isBuffer(t)||!a&&typeof t=="string"?h.body=t:h.json=t);let E=r.get("httpTimeout"),C=r.get("httpRetry"),S=r.get("enableStrictSsl"),x=p.httpsCaFilePath,I=p.httpsCertFilePath,T=p.httpsKeyFilePath,{default:O}=await Promise.resolve().then(()=>et(dAe())),U=x?await YH(x):void 0,V=I?await YH(I):void 0,te=T?await YH(T):void 0,ie={rejectUnauthorized:S,ca:U,cert:V,key:te},ue={http:p.httpProxy?new qce({proxy:p.httpProxy,proxyRequestOptions:ie}):ott,https:p.httpsProxy?new Wce({proxy:p.httpsProxy,proxyRequestOptions:ie}):att},ae=O.extend({timeout:{socket:E},retry:C,agent:ue,https:{rejectUnauthorized:S,certificateAuthority:U,certificate:V,key:te},...h});return r.getLimit("networkConcurrency")(()=>ae(f))}var yAe,EAe,VH,gAe,mAe,ott,att,CAe,AR=Xe(()=>{Dt();Yce();yAe=Ie("https"),EAe=Ie("http"),VH=et(zo());Fc();Qc();kc();gAe=new Map,mAe=new Map,ott=new EAe.Agent({keepAlive:!0}),att=new yAe.Agent({keepAlive:!0});CAe=(a=>(a.GET="GET",a.PUT="PUT",a.POST="POST",a.DELETE="DELETE",a))(CAe||{})});var Ui={};Vt(Ui,{availableParallelism:()=>XH,getArchitecture:()=>XB,getArchitectureName:()=>gtt,getArchitectureSet:()=>zH,getCaller:()=>Itt,major:()=>Att,openUrl:()=>ptt});function dtt(){if(process.platform!=="linux")return null;let e;try{e=le.readFileSync(htt)}catch{}if(typeof e<"u"){if(e&&(e.includes("GLIBC")||e.includes("GNU libc")||e.includes("GNU C Library")))return"glibc";if(e&&e.includes("musl"))return"musl"}let r=(process.report?.getReport()??{}).sharedObjects??[],s=/\/(?:(ld-linux-|[^/]+-linux-gnu\/)|(libc.musl-|ld-musl-))/;return A0(r,a=>{let n=a.match(s);if(!n)return A0.skip;if(n[1])return"glibc";if(n[2])return"musl";throw new Error("Assertion failed: Expected the libc variant to have been detected")})??null}function XB(){return BAe=BAe??{os:(process.env.YARN_IS_TEST_ENV?process.env.YARN_OS_OVERRIDE:void 0)??process.platform,cpu:(process.env.YARN_IS_TEST_ENV?process.env.YARN_CPU_OVERRIDE:void 0)??process.arch,libc:(process.env.YARN_IS_TEST_ENV?process.env.YARN_LIBC_OVERRIDE:void 0)??dtt()}}function gtt(e=XB()){return e.libc?`${e.os}-${e.cpu}-${e.libc}`:`${e.os}-${e.cpu}`}function zH(){let e=XB();return vAe=vAe??{os:[e.os],cpu:[e.cpu],libc:e.libc?[e.libc]:[]}}function Ett(e){let t=mtt.exec(e);if(!t)return null;let r=t[2]&&t[2].indexOf("native")===0,s=t[2]&&t[2].indexOf("eval")===0,a=ytt.exec(t[2]);return s&&a!=null&&(t[2]=a[1],t[3]=a[2],t[4]=a[3]),{file:r?null:t[2],methodName:t[1]||"",arguments:r?[t[2]]:[],line:t[3]?+t[3]:null,column:t[4]?+t[4]:null}}function Itt(){let t=new Error().stack.split(` `)[3];return Ett(t)}function XH(){return typeof pR.default.availableParallelism<"u"?pR.default.availableParallelism():Math.max(1,pR.default.cpus().length)}var pR,Att,wAe,ptt,htt,BAe,vAe,mtt,ytt,hR=Xe(()=>{Dt();pR=et(Ie("os"));dR();kc();Att=Number(process.versions.node.split(".")[0]),wAe=new Map([["darwin","open"],["linux","xdg-open"],["win32","explorer.exe"]]).get(process.platform),ptt=typeof wAe<"u"?async e=>{try{return await ZH(wAe,[e],{cwd:J.cwd()}),!0}catch{return!1}}:void 0,htt="/usr/bin/ldd";mtt=/^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack||\/|[a-z]:\\|\\\\).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i,ytt=/\((\S*)(?::(\d+))(?::(\d+))\)/});function ij(e,t,r,s,a){let n=MB(r);if(s.isArray||s.type==="ANY"&&Array.isArray(n))return Array.isArray(n)?n.map((c,f)=>$H(e,`${t}[${f}]`,c,s,a)):String(n).split(/,/).map(c=>$H(e,t,c,s,a));if(Array.isArray(n))throw new Error(`Non-array configuration settings "${t}" cannot be an array`);return $H(e,t,r,s,a)}function $H(e,t,r,s,a){let n=MB(r);switch(s.type){case"ANY":return FQ(n);case"SHAPE":return vtt(e,t,r,s,a);case"MAP":return Stt(e,t,r,s,a)}if(n===null&&!s.isNullable&&s.default!==null)throw new Error(`Non-nullable configuration settings "${t}" cannot be set to null`);if("values"in s&&s.values?.includes(n))return n;let f=(()=>{if(s.type==="BOOLEAN"&&typeof n!="string")return wB(n);if(typeof n!="string")throw new Error(`Expected configuration setting "${t}" to be a string, got ${typeof n}`);let p=Yk(n,{env:e.env});switch(s.type){case"ABSOLUTE_PATH":{let h=a,E=b8(r);return E&&E[0]!=="<"&&(h=J.dirname(E)),J.resolve(h,fe.toPortablePath(p))}case"LOCATOR_LOOSE":return Tp(p,!1);case"NUMBER":return parseInt(p);case"LOCATOR":return Tp(p);case"BOOLEAN":return wB(p);case"DURATION":return Vk(p,s.unit);default:return p}})();if("values"in s&&s.values&&!s.values.includes(f))throw new Error(`Invalid value, expected one of ${s.values.join(", ")}`);return f}function vtt(e,t,r,s,a){let n=MB(r);if(typeof n!="object"||Array.isArray(n))throw new it(`Object configuration settings "${t}" must be an object`);let c=sj(e,s,{ignoreArrays:!0});if(n===null)return c;for(let[f,p]of Object.entries(n)){let h=`${t}.${f}`;if(!s.properties[f])throw new it(`Unrecognized configuration settings found: ${t}.${f} - run "yarn config" to see the list of settings supported in Yarn`);c.set(f,ij(e,h,p,s.properties[f],a))}return c}function Stt(e,t,r,s,a){let n=MB(r),c=new Map;if(typeof n!="object"||Array.isArray(n))throw new it(`Map configuration settings "${t}" must be an object`);if(n===null)return c;for(let[f,p]of Object.entries(n)){let h=s.normalizeKeys?s.normalizeKeys(f):f,E=`${t}['${h}']`,C=s.valueDefinition;c.set(h,ij(e,E,p,C,a))}return c}function sj(e,t,{ignoreArrays:r=!1}={}){switch(t.type){case"SHAPE":{if(t.isArray&&!r)return[];let s=new Map;for(let[a,n]of Object.entries(t.properties))s.set(a,sj(e,n));return s}case"MAP":return t.isArray&&!r?[]:new Map;case"ABSOLUTE_PATH":return t.default===null?null:e.projectCwd===null?Array.isArray(t.default)?t.default.map(s=>J.normalize(s)):J.isAbsolute(t.default)?J.normalize(t.default):t.isNullable?null:void 0:Array.isArray(t.default)?t.default.map(s=>J.resolve(e.projectCwd,s)):J.resolve(e.projectCwd,t.default);case"DURATION":return Vk(t.default,t.unit);default:return t.default}}function mR(e,t,r){if(t.type==="SECRET"&&typeof e=="string"&&r.hideSecrets)return Btt;if(t.type==="ABSOLUTE_PATH"&&typeof e=="string"&&r.getNativePaths)return fe.fromPortablePath(e);if(t.isArray&&Array.isArray(e)){let s=[];for(let a of e)s.push(mR(a,t,r));return s}if(t.type==="MAP"&&e instanceof Map){if(e.size===0)return;let s=new Map;for(let[a,n]of e.entries()){let c=mR(n,t.valueDefinition,r);typeof c<"u"&&s.set(a,c)}return s}if(t.type==="SHAPE"&&e instanceof Map){if(e.size===0)return;let s=new Map;for(let[a,n]of e.entries()){let c=t.properties[a],f=mR(n,c,r);typeof f<"u"&&s.set(a,f)}return s}return e}function Dtt(){let e={};for(let[t,r]of Object.entries(process.env))t=t.toLowerCase(),t.startsWith(yR)&&(t=(0,DAe.default)(t.slice(yR.length)),e[t]=r);return e}function tj(){let e=`${yR}rc_filename`;for(let[t,r]of Object.entries(process.env))if(t.toLowerCase()===e&&typeof r=="string")return r;return rj}async function SAe(e){try{return await le.readFilePromise(e)}catch{return Buffer.of()}}async function btt(e,t){return Buffer.compare(...await Promise.all([SAe(e),SAe(t)]))===0}async function Ptt(e,t){let[r,s]=await Promise.all([le.statPromise(e),le.statPromise(t)]);return r.dev===s.dev&&r.ino===s.ino}async function ktt({configuration:e,selfPath:t}){let r=e.get("yarnPath");return e.get("ignorePath")||r===null||r===t||await xtt(r,t)?null:r}var DAe,Up,bAe,PAe,xAe,ej,Ctt,ZB,wtt,_p,yR,rj,Btt,SI,kAe,nj,ER,gR,xtt,ze,$B=Xe(()=>{Dt();vc();DAe=et(Wte()),Up=et(Rg());Yt();bAe=et(Mre()),PAe=Ie("module"),xAe=et(Ng()),ej=Ie("stream");Bce();cI();E8();I8();C8();Nce();w8();$g();_ce();OQ();Qc();E0();AR();kc();hR();Np();Zo();Ctt=function(){if(!Up.GITHUB_ACTIONS||!process.env.GITHUB_EVENT_PATH)return!1;let e=fe.toPortablePath(process.env.GITHUB_EVENT_PATH),t;try{t=le.readJsonSync(e)}catch{return!1}return!(!("repository"in t)||!t.repository||(t.repository.private??!0))}(),ZB=new Set(["@yarnpkg/plugin-constraints","@yarnpkg/plugin-exec","@yarnpkg/plugin-interactive-tools","@yarnpkg/plugin-stage","@yarnpkg/plugin-typescript","@yarnpkg/plugin-version","@yarnpkg/plugin-workspace-tools"]),wtt=new Set(["isTestEnv","injectNpmUser","injectNpmPassword","injectNpm2FaToken","zipDataEpilogue","cacheCheckpointOverride","cacheVersionOverride","lockfileVersionOverride","osOverride","cpuOverride","libcOverride","binFolder","version","flags","profile","gpg","ignoreNode","wrapOutput","home","confDir","registry","ignoreCwd"]),_p=/^(?!v)[a-z0-9._-]+$/i,yR="yarn_",rj=".yarnrc.yml",Btt="********",SI=(C=>(C.ANY="ANY",C.BOOLEAN="BOOLEAN",C.ABSOLUTE_PATH="ABSOLUTE_PATH",C.LOCATOR="LOCATOR",C.LOCATOR_LOOSE="LOCATOR_LOOSE",C.NUMBER="NUMBER",C.STRING="STRING",C.DURATION="DURATION",C.SECRET="SECRET",C.SHAPE="SHAPE",C.MAP="MAP",C))(SI||{}),kAe=dt,nj=(c=>(c.MILLISECONDS="ms",c.SECONDS="s",c.MINUTES="m",c.HOURS="h",c.DAYS="d",c.WEEKS="w",c))(nj||{}),ER=(r=>(r.JUNCTIONS="junctions",r.SYMLINKS="symlinks",r))(ER||{}),gR={lastUpdateCheck:{description:"Last timestamp we checked whether new Yarn versions were available",type:"STRING",default:null},yarnPath:{description:"Path to the local executable that must be used over the global one",type:"ABSOLUTE_PATH",default:null},ignorePath:{description:"If true, the local executable will be ignored when using the global one",type:"BOOLEAN",default:!1},globalFolder:{description:"Folder where all system-global files are stored",type:"ABSOLUTE_PATH",default:x8()},cacheFolder:{description:"Folder where the cache files must be written",type:"ABSOLUTE_PATH",default:"./.yarn/cache"},compressionLevel:{description:"Zip files compression level, from 0 to 9 or mixed (a variant of 9, which stores some files uncompressed, when compression doesn't yield good results)",type:"NUMBER",values:["mixed",0,1,2,3,4,5,6,7,8,9],default:0},virtualFolder:{description:"Folder where the virtual packages (cf doc) will be mapped on the disk (must be named __virtual__)",type:"ABSOLUTE_PATH",default:"./.yarn/__virtual__"},installStatePath:{description:"Path of the file where the install state will be persisted",type:"ABSOLUTE_PATH",default:"./.yarn/install-state.gz"},immutablePatterns:{description:"Array of glob patterns; files matching them won't be allowed to change during immutable installs",type:"STRING",default:[],isArray:!0},rcFilename:{description:"Name of the files where the configuration can be found",type:"STRING",default:tj()},enableGlobalCache:{description:"If true, the system-wide cache folder will be used regardless of `cache-folder`",type:"BOOLEAN",default:!0},cacheMigrationMode:{description:"Defines the conditions under which Yarn upgrades should cause the cache archives to be regenerated.",type:"STRING",values:["always","match-spec","required-only"],default:"always"},enableColors:{description:"If true, the CLI is allowed to use colors in its output",type:"BOOLEAN",default:Xk,defaultText:""},enableHyperlinks:{description:"If true, the CLI is allowed to use hyperlinks in its output",type:"BOOLEAN",default:N4,defaultText:""},enableInlineBuilds:{description:"If true, the CLI will print the build output on the command line",type:"BOOLEAN",default:Up.isCI,defaultText:""},enableMessageNames:{description:"If true, the CLI will prefix most messages with codes suitable for search engines",type:"BOOLEAN",default:!0},enableProgressBars:{description:"If true, the CLI is allowed to show a progress bar for long-running events",type:"BOOLEAN",default:!Up.isCI,defaultText:""},enableTimers:{description:"If true, the CLI is allowed to print the time spent executing commands",type:"BOOLEAN",default:!0},enableTips:{description:"If true, installs will print a helpful message every day of the week",type:"BOOLEAN",default:!Up.isCI,defaultText:""},preferInteractive:{description:"If true, the CLI will automatically use the interactive mode when called from a TTY",type:"BOOLEAN",default:!1},preferTruncatedLines:{description:"If true, the CLI will truncate lines that would go beyond the size of the terminal",type:"BOOLEAN",default:!1},progressBarStyle:{description:"Which style of progress bar should be used (only when progress bars are enabled)",type:"STRING",default:void 0,defaultText:""},defaultLanguageName:{description:"Default language mode that should be used when a package doesn't offer any insight",type:"STRING",default:"node"},defaultProtocol:{description:"Default resolution protocol used when resolving pure semver and tag ranges",type:"STRING",default:"npm:"},enableTransparentWorkspaces:{description:"If false, Yarn won't automatically resolve workspace dependencies unless they use the `workspace:` protocol",type:"BOOLEAN",default:!0},supportedArchitectures:{description:"Architectures that Yarn will fetch and inject into the resolver",type:"SHAPE",properties:{os:{description:"Array of supported process.platform strings, or null to target them all",type:"STRING",isArray:!0,isNullable:!0,default:["current"]},cpu:{description:"Array of supported process.arch strings, or null to target them all",type:"STRING",isArray:!0,isNullable:!0,default:["current"]},libc:{description:"Array of supported libc libraries, or null to target them all",type:"STRING",isArray:!0,isNullable:!0,default:["current"]}}},enableMirror:{description:"If true, the downloaded packages will be retrieved and stored in both the local and global folders",type:"BOOLEAN",default:!0},enableNetwork:{description:"If false, Yarn will refuse to use the network if required to",type:"BOOLEAN",default:!0},enableOfflineMode:{description:"If true, Yarn will attempt to retrieve files and metadata from the global cache rather than the network",type:"BOOLEAN",default:!1},httpProxy:{description:"URL of the http proxy that must be used for outgoing http requests",type:"STRING",default:null},httpsProxy:{description:"URL of the http proxy that must be used for outgoing https requests",type:"STRING",default:null},unsafeHttpWhitelist:{description:"List of the hostnames for which http queries are allowed (glob patterns are supported)",type:"STRING",default:[],isArray:!0},httpTimeout:{description:"Timeout of each http request",type:"DURATION",unit:"ms",default:"1m"},httpRetry:{description:"Retry times on http failure",type:"NUMBER",default:3},networkConcurrency:{description:"Maximal number of concurrent requests",type:"NUMBER",default:50},taskPoolConcurrency:{description:"Maximal amount of concurrent heavy task processing",type:"NUMBER",default:XH()},taskPoolMode:{description:"Execution strategy for heavy tasks",type:"STRING",values:["async","workers"],default:"workers"},networkSettings:{description:"Network settings per hostname (glob patterns are supported)",type:"MAP",valueDefinition:{description:"",type:"SHAPE",properties:{httpsCaFilePath:{description:"Path to file containing one or multiple Certificate Authority signing certificates",type:"ABSOLUTE_PATH",default:null},enableNetwork:{description:"If false, the package manager will refuse to use the network if required to",type:"BOOLEAN",default:null},httpProxy:{description:"URL of the http proxy that must be used for outgoing http requests",type:"STRING",default:null},httpsProxy:{description:"URL of the http proxy that must be used for outgoing https requests",type:"STRING",default:null},httpsKeyFilePath:{description:"Path to file containing private key in PEM format",type:"ABSOLUTE_PATH",default:null},httpsCertFilePath:{description:"Path to file containing certificate chain in PEM format",type:"ABSOLUTE_PATH",default:null}}}},httpsCaFilePath:{description:"A path to a file containing one or multiple Certificate Authority signing certificates",type:"ABSOLUTE_PATH",default:null},httpsKeyFilePath:{description:"Path to file containing private key in PEM format",type:"ABSOLUTE_PATH",default:null},httpsCertFilePath:{description:"Path to file containing certificate chain in PEM format",type:"ABSOLUTE_PATH",default:null},enableStrictSsl:{description:"If false, SSL certificate errors will be ignored",type:"BOOLEAN",default:!0},logFilters:{description:"Overrides for log levels",type:"SHAPE",isArray:!0,concatenateValues:!0,properties:{code:{description:"Code of the messages covered by this override",type:"STRING",default:void 0},text:{description:"Code of the texts covered by this override",type:"STRING",default:void 0},pattern:{description:"Code of the patterns covered by this override",type:"STRING",default:void 0},level:{description:"Log level override, set to null to remove override",type:"STRING",values:Object.values($k),isNullable:!0,default:void 0}}},enableTelemetry:{description:"If true, telemetry will be periodically sent, following the rules in https://yarnpkg.com/advanced/telemetry",type:"BOOLEAN",default:!0},telemetryInterval:{description:"Minimal amount of time between two telemetry uploads",type:"DURATION",unit:"d",default:"7d"},telemetryUserId:{description:"If you desire to tell us which project you are, you can set this field. Completely optional and opt-in.",type:"STRING",default:null},enableHardenedMode:{description:"If true, automatically enable --check-resolutions --refresh-lockfile on installs",type:"BOOLEAN",default:Up.isPR&&Ctt,defaultText:""},enableScripts:{description:"If true, packages are allowed to have install scripts by default",type:"BOOLEAN",default:!1},enableStrictSettings:{description:"If true, unknown settings will cause Yarn to abort",type:"BOOLEAN",default:!0},enableImmutableCache:{description:"If true, the cache is reputed immutable and actions that would modify it will throw",type:"BOOLEAN",default:!1},enableCacheClean:{description:"If false, disallows the `cache clean` command",type:"BOOLEAN",default:!0},checksumBehavior:{description:"Enumeration defining what to do when a checksum doesn't match expectations",type:"STRING",default:"throw"},injectEnvironmentFiles:{description:"List of all the environment files that Yarn should inject inside the process when it starts",type:"ABSOLUTE_PATH",default:[".env.yarn?"],isArray:!0},packageExtensions:{description:"Map of package corrections to apply on the dependency tree",type:"MAP",valueDefinition:{description:"The extension that will be applied to any package whose version matches the specified range",type:"SHAPE",properties:{dependencies:{description:"The set of dependencies that must be made available to the current package in order for it to work properly",type:"MAP",valueDefinition:{description:"A range",type:"STRING"}},peerDependencies:{description:"Inherited dependencies - the consumer of the package will be tasked to provide them",type:"MAP",valueDefinition:{description:"A semver range",type:"STRING"}},peerDependenciesMeta:{description:"Extra information related to the dependencies listed in the peerDependencies field",type:"MAP",valueDefinition:{description:"The peerDependency meta",type:"SHAPE",properties:{optional:{description:"If true, the selected peer dependency will be marked as optional by the package manager and the consumer omitting it won't be reported as an error",type:"BOOLEAN",default:!1}}}}}}}};xtt=process.platform==="win32"?btt:Ptt;ze=class e{constructor(t){this.isCI=Up.isCI;this.projectCwd=null;this.plugins=new Map;this.settings=new Map;this.values=new Map;this.sources=new Map;this.invalid=new Map;this.env={};this.limits=new Map;this.packageExtensions=null;this.startingCwd=t}static{this.deleteProperty=Symbol()}static{this.telemetry=null}static create(t,r,s){let a=new e(t);typeof r<"u"&&!(r instanceof Map)&&(a.projectCwd=r),a.importSettings(gR);let n=typeof s<"u"?s:r instanceof Map?r:new Map;for(let[c,f]of n)a.activatePlugin(c,f);return a}static async find(t,r,{strict:s=!0,usePathCheck:a=null,useRc:n=!0}={}){let c=Dtt();delete c.rcFilename;let f=new e(t),p=await e.findRcFiles(t),h=await e.findFolderRcFile(hI());h&&(p.find(ge=>ge.path===h.path)||p.unshift(h));let E=Uce(p.map(ae=>[ae.path,ae.data])),C=vt.dot,S=new Set(Object.keys(gR)),x=({yarnPath:ae,ignorePath:ge,injectEnvironmentFiles:Ae})=>({yarnPath:ae,ignorePath:ge,injectEnvironmentFiles:Ae}),I=({yarnPath:ae,ignorePath:ge,injectEnvironmentFiles:Ae,...Ce})=>{let Ee={};for(let[d,Se]of Object.entries(Ce))S.has(d)&&(Ee[d]=Se);return Ee},T=({yarnPath:ae,ignorePath:ge,...Ae})=>{let Ce={};for(let[Ee,d]of Object.entries(Ae))S.has(Ee)||(Ce[Ee]=d);return Ce};if(f.importSettings(x(gR)),f.useWithSource("",x(c),t,{strict:!1}),E){let[ae,ge]=E;f.useWithSource(ae,x(ge),C,{strict:!1})}if(a){if(await ktt({configuration:f,selfPath:a})!==null)return f;f.useWithSource("",{ignorePath:!0},t,{strict:!1,overwrite:!0})}let O=await e.findProjectCwd(t);f.startingCwd=t,f.projectCwd=O;let U=Object.assign(Object.create(null),process.env);f.env=U;let V=await Promise.all(f.get("injectEnvironmentFiles").map(async ae=>{let ge=ae.endsWith("?")?await le.readFilePromise(ae.slice(0,-1),"utf8").catch(()=>""):await le.readFilePromise(ae,"utf8");return(0,bAe.parse)(ge)}));for(let ae of V)for(let[ge,Ae]of Object.entries(ae))f.env[ge]=Yk(Ae,{env:U});if(f.importSettings(I(gR)),f.useWithSource("",I(c),t,{strict:s}),E){let[ae,ge]=E;f.useWithSource(ae,I(ge),C,{strict:s})}let te=ae=>"default"in ae?ae.default:ae,ie=new Map([["@@core",wce]]);if(r!==null)for(let ae of r.plugins.keys())ie.set(ae,te(r.modules.get(ae)));for(let[ae,ge]of ie)f.activatePlugin(ae,ge);let ue=new Map([]);if(r!==null){let ae=new Map;for(let[Ce,Ee]of r.modules)ae.set(Ce,()=>Ee);let ge=new Set,Ae=async(Ce,Ee)=>{let{factory:d,name:Se}=kp(Ce);if(!d||ge.has(Se))return;let Be=new Map(ae),me=Z=>{if((0,PAe.isBuiltin)(Z))return kp(Z);if(Be.has(Z))return Be.get(Z)();throw new it(`This plugin cannot access the package referenced via ${Z} which is neither a builtin, nor an exposed entry`)},ce=await VE(async()=>te(await d(me)),Z=>`${Z} (when initializing ${Se}, defined in ${Ee})`);ae.set(Se,()=>ce),ge.add(Se),ue.set(Se,ce)};if(c.plugins)for(let Ce of c.plugins.split(";")){let Ee=J.resolve(t,fe.toPortablePath(Ce));await Ae(Ee,"")}for(let{path:Ce,cwd:Ee,data:d}of p)if(n&&Array.isArray(d.plugins))for(let Se of d.plugins){let Be=typeof Se!="string"?Se.path:Se,me=Se?.spec??"",ce=Se?.checksum??"";if(ZB.has(me))continue;let Z=J.resolve(Ee,fe.toPortablePath(Be));if(!await le.existsPromise(Z)){if(!me){let st=jt(f,J.basename(Z,".cjs"),dt.NAME),_=jt(f,".gitignore",dt.NAME),tt=jt(f,f.values.get("rcFilename"),dt.NAME),Ne=jt(f,"https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored",dt.URL);throw new it(`Missing source for the ${st} plugin - please try to remove the plugin from ${tt} then reinstall it manually. This error usually occurs because ${_} is incorrect, check ${Ne} to make sure your plugin folder isn't gitignored.`)}if(!me.match(/^https?:/)){let st=jt(f,J.basename(Z,".cjs"),dt.NAME),_=jt(f,f.values.get("rcFilename"),dt.NAME);throw new it(`Failed to recognize the source for the ${st} plugin - please try to delete the plugin from ${_} then reinstall it manually.`)}let De=await JH(me,{configuration:f}),Qe=fs(De);if(ce&&ce!==Qe){let st=jt(f,J.basename(Z,".cjs"),dt.NAME),_=jt(f,f.values.get("rcFilename"),dt.NAME),tt=jt(f,`yarn plugin import ${me}`,dt.CODE);throw new it(`Failed to fetch the ${st} plugin from its remote location: its checksum seems to have changed. If this is expected, please remove the plugin from ${_} then run ${tt} to reimport it.`)}await le.mkdirPromise(J.dirname(Z),{recursive:!0}),await le.writeFilePromise(Z,De)}await Ae(Z,Ce)}}for(let[ae,ge]of ue)f.activatePlugin(ae,ge);if(f.useWithSource("",T(c),t,{strict:s}),E){let[ae,ge]=E;f.useWithSource(ae,T(ge),C,{strict:s})}return f.get("enableGlobalCache")&&(f.values.set("cacheFolder",`${f.get("globalFolder")}/cache`),f.sources.set("cacheFolder","")),f}static async findRcFiles(t){let r=tj(),s=[],a=t,n=null;for(;a!==n;){n=a;let c=J.join(n,r);if(le.existsSync(c)){let f,p;try{p=await le.readFilePromise(c,"utf8"),f=cs(p)}catch{let h="";throw p?.match(/^\s+(?!-)[^:]+\s+\S+/m)&&(h=" (in particular, make sure you list the colons after each key name)"),new it(`Parse error when loading ${c}; please check it's proper Yaml${h}`)}s.unshift({path:c,cwd:n,data:f})}a=J.dirname(n)}return s}static async findFolderRcFile(t){let r=J.join(t,Er.rc),s;try{s=await le.readFilePromise(r,"utf8")}catch(n){if(n.code==="ENOENT")return null;throw n}let a=cs(s);return{path:r,cwd:t,data:a}}static async findProjectCwd(t){let r=null,s=t,a=null;for(;s!==a;){if(a=s,le.existsSync(J.join(a,Er.lockfile)))return a;le.existsSync(J.join(a,Er.manifest))&&(r=a),s=J.dirname(a)}return r}static async updateConfiguration(t,r,s={}){let a=tj(),n=J.join(t,a),c=le.existsSync(n)?cs(await le.readFilePromise(n,"utf8")):{},f=!1,p;if(typeof r=="function"){try{p=r(c)}catch{p=r({})}if(p===c)return!1}else{p=c;for(let h of Object.keys(r)){let E=c[h],C=r[h],S;if(typeof C=="function")try{S=C(E)}catch{S=C(void 0)}else S=C;E!==S&&(S===e.deleteProperty?delete p[h]:p[h]=S,f=!0)}if(!f)return!1}return await le.changeFilePromise(n,fl(p),{automaticNewlines:!0}),!0}static async addPlugin(t,r){r.length!==0&&await e.updateConfiguration(t,s=>{let a=s.plugins??[];if(a.length===0)return{...s,plugins:r};let n=[],c=[...r];for(let f of a){let p=typeof f!="string"?f.path:f,h=c.find(E=>E.path===p);h?(n.push(h),c=c.filter(E=>E!==h)):n.push(f)}return n.push(...c),{...s,plugins:n}})}static async updateHomeConfiguration(t){let r=hI();return await e.updateConfiguration(r,t)}activatePlugin(t,r){this.plugins.set(t,r),typeof r.configuration<"u"&&this.importSettings(r.configuration)}importSettings(t){for(let[r,s]of Object.entries(t))if(s!=null){if(this.settings.has(r))throw new Error(`Cannot redefine settings "${r}"`);this.settings.set(r,s),this.values.set(r,sj(this,s))}}useWithSource(t,r,s,a){try{this.use(t,r,s,a)}catch(n){throw n.message+=` (in ${jt(this,t,dt.PATH)})`,n}}use(t,r,s,{strict:a=!0,overwrite:n=!1}={}){a=a&&this.get("enableStrictSettings");for(let c of["enableStrictSettings",...Object.keys(r)]){let f=r[c],p=b8(f);if(p&&(t=p),typeof f>"u"||c==="plugins"||t===""&&wtt.has(c))continue;if(c==="rcFilename")throw new it(`The rcFilename settings can only be set via ${`${yR}RC_FILENAME`.toUpperCase()}, not via a rc file`);let h=this.settings.get(c);if(!h){let C=hI(),S=t[0]!=="<"?J.dirname(t):null;if(a&&!(S!==null?C===S:!1))throw new it(`Unrecognized or legacy configuration settings found: ${c} - run "yarn config" to see the list of settings supported in Yarn`);this.invalid.set(c,t);continue}if(this.sources.has(c)&&!(n||h.type==="MAP"||h.isArray&&h.concatenateValues))continue;let E;try{E=ij(this,c,f,h,s)}catch(C){throw C.message+=` in ${jt(this,t,dt.PATH)}`,C}if(c==="enableStrictSettings"&&t!==""){a=E;continue}if(h.type==="MAP"){let C=this.values.get(c);this.values.set(c,new Map(n?[...C,...E]:[...E,...C])),this.sources.set(c,`${this.sources.get(c)}, ${t}`)}else if(h.isArray&&h.concatenateValues){let C=this.values.get(c);this.values.set(c,n?[...C,...E]:[...E,...C]),this.sources.set(c,`${this.sources.get(c)}, ${t}`)}else this.values.set(c,E),this.sources.set(c,t)}}get(t){if(!this.values.has(t))throw new Error(`Invalid configuration key "${t}"`);return this.values.get(t)}getSpecial(t,{hideSecrets:r=!1,getNativePaths:s=!1}){let a=this.get(t),n=this.settings.get(t);if(typeof n>"u")throw new it(`Couldn't find a configuration settings named "${t}"`);return mR(a,n,{hideSecrets:r,getNativePaths:s})}getSubprocessStreams(t,{header:r,prefix:s,report:a}){let n,c,f=le.createWriteStream(t);if(this.get("enableInlineBuilds")){let p=a.createStreamReporter(`${s} ${jt(this,"STDOUT","green")}`),h=a.createStreamReporter(`${s} ${jt(this,"STDERR","red")}`);n=new ej.PassThrough,n.pipe(p),n.pipe(f),c=new ej.PassThrough,c.pipe(h),c.pipe(f)}else n=f,c=f,typeof r<"u"&&n.write(`${r} `);return{stdout:n,stderr:c}}makeResolver(){let t=[];for(let r of this.plugins.values())for(let s of r.resolvers||[])t.push(new s);return new em([new TQ,new Ii,...t])}makeFetcher(){let t=[];for(let r of this.plugins.values())for(let s of r.fetchers||[])t.push(new s);return new uI([new fI,new AI,...t])}getLinkers(){let t=[];for(let r of this.plugins.values())for(let s of r.linkers||[])t.push(new s);return t}getSupportedArchitectures(){let t=XB(),r=this.get("supportedArchitectures"),s=r.get("os");s!==null&&(s=s.map(c=>c==="current"?t.os:c));let a=r.get("cpu");a!==null&&(a=a.map(c=>c==="current"?t.cpu:c));let n=r.get("libc");return n!==null&&(n=Xl(n,c=>c==="current"?t.libc??Xl.skip:c)),{os:s,cpu:a,libc:n}}isInteractive({interactive:t,stdout:r}){return r.isTTY?t??this.get("preferInteractive"):!1}async getPackageExtensions(){if(this.packageExtensions!==null)return this.packageExtensions;this.packageExtensions=new Map;let t=this.packageExtensions,r=(s,a,{userProvided:n=!1}={})=>{if(!yl(s.range))throw new Error("Only semver ranges are allowed as keys for the packageExtensions setting");let c=new _t;c.load(a,{yamlCompatibilityMode:!0});let f=CB(t,s.identHash),p=[];f.push([s.range,p]);let h={status:"inactive",userProvided:n,parentDescriptor:s};for(let E of c.dependencies.values())p.push({...h,type:"Dependency",descriptor:E});for(let E of c.peerDependencies.values())p.push({...h,type:"PeerDependency",descriptor:E});for(let[E,C]of c.peerDependenciesMeta)for(let[S,x]of Object.entries(C))p.push({...h,type:"PeerDependencyMeta",selector:E,key:S,value:x})};await this.triggerHook(s=>s.registerPackageExtensions,this,r);for(let[s,a]of this.get("packageExtensions"))r(I0(s,!0),Wk(a),{userProvided:!0});return t}normalizeLocator(t){return yl(t.reference)?Js(t,`${this.get("defaultProtocol")}${t.reference}`):_p.test(t.reference)?Js(t,`${this.get("defaultProtocol")}${t.reference}`):t}normalizeDependency(t){return yl(t.range)?Mn(t,`${this.get("defaultProtocol")}${t.range}`):_p.test(t.range)?Mn(t,`${this.get("defaultProtocol")}${t.range}`):t}normalizeDependencyMap(t){return new Map([...t].map(([r,s])=>[r,this.normalizeDependency(s)]))}normalizePackage(t,{packageExtensions:r}){let s=xB(t),a=r.get(t.identHash);if(typeof a<"u"){let c=t.version;if(c!==null){for(let[f,p]of a)if(tA(c,f))for(let h of p)switch(h.status==="inactive"&&(h.status="redundant"),h.type){case"Dependency":typeof s.dependencies.get(h.descriptor.identHash)>"u"&&(h.status="active",s.dependencies.set(h.descriptor.identHash,this.normalizeDependency(h.descriptor)));break;case"PeerDependency":typeof s.peerDependencies.get(h.descriptor.identHash)>"u"&&(h.status="active",s.peerDependencies.set(h.descriptor.identHash,h.descriptor));break;case"PeerDependencyMeta":{let E=s.peerDependenciesMeta.get(h.selector);(typeof E>"u"||!Object.hasOwn(E,h.key)||E[h.key]!==h.value)&&(h.status="active",Zl(s.peerDependenciesMeta,h.selector,()=>({}))[h.key]=h.value)}break;default:b4(h)}}}let n=c=>c.scope?`${c.scope}__${c.name}`:`${c.name}`;for(let c of s.peerDependenciesMeta.keys()){let f=xa(c);s.peerDependencies.has(f.identHash)||s.peerDependencies.set(f.identHash,Mn(f,"*"))}for(let c of s.peerDependencies.values()){if(c.scope==="types")continue;let f=n(c),p=ka("types",f),h=fn(p);s.peerDependencies.has(p.identHash)||s.peerDependenciesMeta.has(h)||s.dependencies.has(p.identHash)||(s.peerDependencies.set(p.identHash,Mn(p,"*")),s.peerDependenciesMeta.set(h,{optional:!0}))}return s.dependencies=new Map(Vs(s.dependencies,([,c])=>gl(c))),s.peerDependencies=new Map(Vs(s.peerDependencies,([,c])=>gl(c))),s}getLimit(t){return Zl(this.limits,t,()=>(0,xAe.default)(this.get(t)))}async triggerHook(t,...r){for(let s of this.plugins.values()){let a=s.hooks;if(!a)continue;let n=t(a);n&&await n(...r)}}async triggerMultipleHooks(t,r){for(let s of r)await this.triggerHook(t,...s)}async reduceHook(t,r,...s){let a=r;for(let n of this.plugins.values()){let c=n.hooks;if(!c)continue;let f=t(c);f&&(a=await f(a,...s))}return a}async firstHook(t,...r){for(let s of this.plugins.values()){let a=s.hooks;if(!a)continue;let n=t(a);if(!n)continue;let c=await n(...r);if(typeof c<"u")return c}return null}}});var qr={};Vt(qr,{EndStrategy:()=>cj,ExecError:()=>IR,PipeError:()=>ev,execvp:()=>ZH,pipevp:()=>Gu});function im(e){return e!==null&&typeof e.fd=="number"}function oj(){}function aj(){for(let e of sm)e.kill()}async function Gu(e,t,{cwd:r,env:s=process.env,strict:a=!1,stdin:n=null,stdout:c,stderr:f,end:p=2}){let h=["pipe","pipe","pipe"];n===null?h[0]="ignore":im(n)&&(h[0]=n),im(c)&&(h[1]=c),im(f)&&(h[2]=f);let E=(0,lj.default)(e,t,{cwd:fe.fromPortablePath(r),env:{...s,PWD:fe.fromPortablePath(r)},stdio:h});sm.add(E),sm.size===1&&(process.on("SIGINT",oj),process.on("SIGTERM",aj)),!im(n)&&n!==null&&n.pipe(E.stdin),im(c)||E.stdout.pipe(c,{end:!1}),im(f)||E.stderr.pipe(f,{end:!1});let C=()=>{for(let S of new Set([c,f]))im(S)||S.end()};return new Promise((S,x)=>{E.on("error",I=>{sm.delete(E),sm.size===0&&(process.off("SIGINT",oj),process.off("SIGTERM",aj)),(p===2||p===1)&&C(),x(I)}),E.on("close",(I,T)=>{sm.delete(E),sm.size===0&&(process.off("SIGINT",oj),process.off("SIGTERM",aj)),(p===2||p===1&&I!==0)&&C(),I===0||!a?S({code:uj(I,T)}):x(new ev({fileName:e,code:I,signal:T}))})})}async function ZH(e,t,{cwd:r,env:s=process.env,encoding:a="utf8",strict:n=!1}){let c=["ignore","pipe","pipe"],f=[],p=[],h=fe.fromPortablePath(r);typeof s.PWD<"u"&&(s={...s,PWD:h});let E=(0,lj.default)(e,t,{cwd:h,env:s,stdio:c});return E.stdout.on("data",C=>{f.push(C)}),E.stderr.on("data",C=>{p.push(C)}),await new Promise((C,S)=>{E.on("error",x=>{let I=ze.create(r),T=jt(I,e,dt.PATH);S(new Lt(1,`Process ${T} failed to spawn`,O=>{O.reportError(1,` ${Zf(I,{label:"Thrown Error",value:Mu(dt.NO_HINT,x.message)})}`)}))}),E.on("close",(x,I)=>{let T=a==="buffer"?Buffer.concat(f):Buffer.concat(f).toString(a),O=a==="buffer"?Buffer.concat(p):Buffer.concat(p).toString(a);x===0||!n?C({code:uj(x,I),stdout:T,stderr:O}):S(new IR({fileName:e,code:x,signal:I,stdout:T,stderr:O}))})})}function uj(e,t){let r=Qtt.get(t);return typeof r<"u"?128+r:e??1}function Rtt(e,t,{configuration:r,report:s}){s.reportError(1,` ${Zf(r,e!==null?{label:"Exit Code",value:Mu(dt.NUMBER,e)}:{label:"Exit Signal",value:Mu(dt.CODE,t)})}`)}var lj,cj,ev,IR,sm,Qtt,dR=Xe(()=>{Dt();lj=et(vU());$B();Fc();Qc();cj=(s=>(s[s.Never=0]="Never",s[s.ErrorCode=1]="ErrorCode",s[s.Always=2]="Always",s))(cj||{}),ev=class extends Lt{constructor({fileName:t,code:r,signal:s}){let a=ze.create(J.cwd()),n=jt(a,t,dt.PATH);super(1,`Child ${n} reported an error`,c=>{Rtt(r,s,{configuration:a,report:c})}),this.code=uj(r,s)}},IR=class extends ev{constructor({fileName:t,code:r,signal:s,stdout:a,stderr:n}){super({fileName:t,code:r,signal:s}),this.stdout=a,this.stderr=n}};sm=new Set;Qtt=new Map([["SIGINT",2],["SIGQUIT",3],["SIGKILL",9],["SIGTERM",15]])});function RAe(e){QAe=e}function tv(){return typeof fj>"u"&&(fj=QAe()),fj}var fj,QAe,Aj=Xe(()=>{QAe=()=>{throw new Error("Assertion failed: No libzip instance is available, and no factory was configured")}});var TAe=G((CR,hj)=>{var Ttt=Object.assign({},Ie("fs")),pj=function(){var e=typeof document<"u"&&document.currentScript?document.currentScript.src:void 0;return typeof __filename<"u"&&(e=e||__filename),function(t){t=t||{};var r=typeof t<"u"?t:{},s,a;r.ready=new Promise(function(Ke,ot){s=Ke,a=ot});var n={},c;for(c in r)r.hasOwnProperty(c)&&(n[c]=r[c]);var f=[],p="./this.program",h=function(Ke,ot){throw ot},E=!1,C=!0,S="";function x(Ke){return r.locateFile?r.locateFile(Ke,S):S+Ke}var I,T,O,U;C&&(E?S=Ie("path").dirname(S)+"/":S=__dirname+"/",I=function(ot,St){var lr=Ga(ot);return lr?St?lr:lr.toString():(O||(O=Ttt),U||(U=Ie("path")),ot=U.normalize(ot),O.readFileSync(ot,St?null:"utf8"))},T=function(ot){var St=I(ot,!0);return St.buffer||(St=new Uint8Array(St)),Se(St.buffer),St},process.argv.length>1&&(p=process.argv[1].replace(/\\/g,"/")),f=process.argv.slice(2),h=function(Ke){process.exit(Ke)},r.inspect=function(){return"[Emscripten Module object]"});var V=r.print||console.log.bind(console),te=r.printErr||console.warn.bind(console);for(c in n)n.hasOwnProperty(c)&&(r[c]=n[c]);n=null,r.arguments&&(f=r.arguments),r.thisProgram&&(p=r.thisProgram),r.quit&&(h=r.quit);var ie=0,ue=function(Ke){ie=Ke},ae;r.wasmBinary&&(ae=r.wasmBinary);var ge=r.noExitRuntime||!0;typeof WebAssembly!="object"&&ns("no native wasm support detected");function Ae(Ke,ot,St){switch(ot=ot||"i8",ot.charAt(ot.length-1)==="*"&&(ot="i32"),ot){case"i1":return je[Ke>>0];case"i8":return je[Ke>>0];case"i16":return gh((Ke>>1)*2);case"i32":return ao((Ke>>2)*4);case"i64":return ao((Ke>>2)*4);case"float":return df((Ke>>2)*4);case"double":return dh((Ke>>3)*8);default:ns("invalid type for getValue: "+ot)}return null}var Ce,Ee=!1,d;function Se(Ke,ot){Ke||ns("Assertion failed: "+ot)}function Be(Ke){var ot=r["_"+Ke];return Se(ot,"Cannot call unknown function "+Ke+", make sure it is exported"),ot}function me(Ke,ot,St,lr,ee){var ye={string:function(qi){var Fn=0;if(qi!=null&&qi!==0){var Xa=(qi.length<<2)+1;Fn=Bi(Xa),st(qi,Fn,Xa)}return Fn},array:function(qi){var Fn=Bi(qi.length);return Ne(qi,Fn),Fn}};function Oe(qi){return ot==="string"?De(qi):ot==="boolean"?!!qi:qi}var mt=Be(Ke),Et=[],bt=0;if(lr)for(var tr=0;tr=St)&&Re[lr];)++lr;return Z.decode(Re.subarray(Ke,lr))}function Qe(Ke,ot,St,lr){if(!(lr>0))return 0;for(var ee=St,ye=St+lr-1,Oe=0;Oe=55296&&mt<=57343){var Et=Ke.charCodeAt(++Oe);mt=65536+((mt&1023)<<10)|Et&1023}if(mt<=127){if(St>=ye)break;ot[St++]=mt}else if(mt<=2047){if(St+1>=ye)break;ot[St++]=192|mt>>6,ot[St++]=128|mt&63}else if(mt<=65535){if(St+2>=ye)break;ot[St++]=224|mt>>12,ot[St++]=128|mt>>6&63,ot[St++]=128|mt&63}else{if(St+3>=ye)break;ot[St++]=240|mt>>18,ot[St++]=128|mt>>12&63,ot[St++]=128|mt>>6&63,ot[St++]=128|mt&63}}return ot[St]=0,St-ee}function st(Ke,ot,St){return Qe(Ke,Re,ot,St)}function _(Ke){for(var ot=0,St=0;St=55296&&lr<=57343&&(lr=65536+((lr&1023)<<10)|Ke.charCodeAt(++St)&1023),lr<=127?++ot:lr<=2047?ot+=2:lr<=65535?ot+=3:ot+=4}return ot}function tt(Ke){var ot=_(Ke)+1,St=Ya(ot);return St&&Qe(Ke,je,St,ot),St}function Ne(Ke,ot){je.set(Ke,ot)}function ke(Ke,ot){return Ke%ot>0&&(Ke+=ot-Ke%ot),Ke}var be,je,Re,ct,Me,P,w,b,y,F;function z(Ke){be=Ke,r.HEAP_DATA_VIEW=F=new DataView(Ke),r.HEAP8=je=new Int8Array(Ke),r.HEAP16=ct=new Int16Array(Ke),r.HEAP32=P=new Int32Array(Ke),r.HEAPU8=Re=new Uint8Array(Ke),r.HEAPU16=Me=new Uint16Array(Ke),r.HEAPU32=w=new Uint32Array(Ke),r.HEAPF32=b=new Float32Array(Ke),r.HEAPF64=y=new Float64Array(Ke)}var X=r.INITIAL_MEMORY||16777216,$,se=[],xe=[],Fe=[],ut=!1;function Ct(){if(r.preRun)for(typeof r.preRun=="function"&&(r.preRun=[r.preRun]);r.preRun.length;)Pt(r.preRun.shift());Ns(se)}function qt(){ut=!0,Ns(xe)}function ir(){if(r.postRun)for(typeof r.postRun=="function"&&(r.postRun=[r.postRun]);r.postRun.length;)Pr(r.postRun.shift());Ns(Fe)}function Pt(Ke){se.unshift(Ke)}function gn(Ke){xe.unshift(Ke)}function Pr(Ke){Fe.unshift(Ke)}var Cr=0,Or=null,on=null;function li(Ke){Cr++,r.monitorRunDependencies&&r.monitorRunDependencies(Cr)}function Do(Ke){if(Cr--,r.monitorRunDependencies&&r.monitorRunDependencies(Cr),Cr==0&&(Or!==null&&(clearInterval(Or),Or=null),on)){var ot=on;on=null,ot()}}r.preloadedImages={},r.preloadedAudios={};function ns(Ke){r.onAbort&&r.onAbort(Ke),Ke+="",te(Ke),Ee=!0,d=1,Ke="abort("+Ke+"). Build with -s ASSERTIONS=1 for more info.";var ot=new WebAssembly.RuntimeError(Ke);throw a(ot),ot}var so="data:application/octet-stream;base64,";function bo(Ke){return Ke.startsWith(so)}var ji="data:application/octet-stream;base64,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QCAAKAJIIgEgASADaiADEAcaAkAgAyAAKAJsIgFNBEAgACABIANrNgJsDAELIABCADcCbAsgACAAKAJoIANrIgE2AmggACAAKAJYIANrNgJYIAEgACgChC5JBEAgACABNgKELgsgAEH8gAEoAgARAwAgAiADaiECCwJAIAAoAgAiASgCBCIERQ0AIAAoAjwhBSAAIAIgBCACIARJGyICBH8gACgCSCAAKAJoaiAFaiEFIAEgBCACazYCBAJAAkACQAJAIAEoAhwiBCgCFEEBaw4CAQACCyAEQaABaiAFIAEoAgAgAkHcgAEoAgARCAAMAgsgASABKAIwIAUgASgCACACQcSAASgCABEEADYCMAwBCyAFIAEoAgAgAhAHGgsgASABKAIAIAJqNgIAIAEgASgCCCACajYCCCAAKAI8BSAFCyACaiICNgI8AkAgACgChC4iASACakEDSQ0AIAAoAmggAWshAQJAIAAoAnRBgQhPBEAgACAAIAAoAkggAWoiAi0AACACLQABIAAoAnwRAAA2AlQMAQsgAUUNACAAIAFBAWsgACgChAERAgAaCyAAKAKELiAAKAI8IgJBAUZrIgRFDQAgACABIAQgACgCgAERBQAgACAAKAKELiAEazYChC4gACgCPCECCyACQYUCSw0AIAAoAgAoAgRFDQAgACgCMCEBDAELCwJAIAAoAkQiAiAAKAJAIgNNDQAgAAJ/IAAoAjwgACgCaGoiASADSwRAIAAoAkggAWpBACACIAFrIgNBggIgA0GCAkkbIgMQGSABIANqDAELIAFBggJqIgEgA00NASAAKAJIIANqQQAgAiADayICIAEgA2siAyACIANJGyIDEBkgACgCQCADags2AkALC50CAQF/AkAgAAJ/IAAoAqAuIgFBwABGBEAgACgCBCAAKAIQaiAAKQOYLjcAACAAQgA3A5guIAAgACgCEEEIajYCEEEADAELIAFBIE4EQCAAKAIEIAAoAhBqIAApA5guPgAAIAAgAEGcLmo1AgA3A5guIAAgACgCEEEEajYCECAAIAAoAqAuQSBrIgE2AqAuCyABQRBOBEAgACgCBCAAKAIQaiAAKQOYLj0AACAAIAAoAhBBAmo2AhAgACAAKQOYLkIQiDcDmC4gACAAKAKgLkEQayIBNgKgLgsgAUEISA0BIAAgACgCECIBQQFqNgIQ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KAJUIgEEfyABQQAgARAiEBkgACgCVAVBAAsQBiAAQQA2AlQLC9wMAgl/AX4jAEFAaiIGJAACQAJAAkACQAJAIAEoAjBBABAjIgVBAkZBACABKAI4QQAQIyIEQQFGGw0AIAVBAUZBACAEQQJGGw0AIAVBAkciAw0BIARBAkcNAQsgASABLwEMQYAQcjsBDEEAIQMMAQsgASABLwEMQf/vA3E7AQxBACEFIANFBEBB9eABIAEoAjAgAEEIahBpIgVFDQILIAJBgAJxBEAgBSEDDAELIARBAkcEQCAFIQMMAQtB9cYBIAEoAjggAEEIahBpIgNFBEAgBRAcDAILIAMgBTYCAAsgASABLwEMQf7/A3EgAS8BUiIFQQBHcjsBDAJAAkACQAJAAn8CQAJAIAEpAyhC/v///w9WDQAgASkDIEL+////D1YNACACQYAEcUUNASABKQNIQv////8PVA0BCyAFQYECa0H//wNxQQNJIQdBAQwBCyAFQYECa0H//wNxIQQgAkGACnFBgApHDQEgBEEDSSEHQQALIQkgBkIcEBciBEUEQCAAQQhqIgAEQCAAQQA2AgQgAEEONgIACyADEBwMBQsgAkGACHEhBQJAAkAgAkGAAnEEQAJAIAUNACABKQMgQv////8PVg0AIAEpAyhCgICAgBBUDQMLIAQgASkDKBAYIAEpAyAhDAwBCwJAAkACQCAFDQAgASkDIEL/////D1YNACABKQMoIgxC/////w9WDQEgASkDSEKAgICAEFQNBAsgASkDKCIMQv////8PVA0BCyAEIAwQGAsgASkDICIMQv////8PWgRAIAQgDBAYCyABKQNIIgxC/////w9UDQELIAQgDBAYCyAELQAARQRAIABBCGoiAARAIABBADYCBCAAQRQ2AgALIAQQCCADEBwMBQtBASEKQQEgBC0AAAR+IAQpAxAFQgALp0H//wNxIAYQRyEFIAQQCCAFIAM2AgAgBw0BDAILIAMhBSAEQQJLDQELIAZCBxAXIgRFBEAgAEEIaiIABEAgAEEANgIEIABBDjYCAAsgBRAcDAMLIARBAhANIARBhxJBAhAsIAQgAS0AUhBwIAQgAS8BEBANIAQtAABFBEAgAEEIaiIABEAgAEEANgIEIABBFDYCAAsgBBAIDAILQYGyAkEHIAYQRyEDIAQQCCADIAU2AgBBASEL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NgIEIABB7A42AhggGiEdDAMLIARB/wFxIgJBwABxRQRAIAggDSADLwECQQJ0aiAdp0F/IAJ0QX9zcUECdGoiAy0AASICayEIIB0gAq2IIR0gAy0AACIERQ0HDAELCyAEQSBxBEAgB0G//gA2AgQgASECDAgLIAdB0f4ANgIEIABB0A42AhggASECDAcLIB1BfyAGdEF/c62DIBt8IhunIQUgCCAEQQ9xIgNrIQggGiAErUIPg4ghHSABIBdrIgYgAjMBAiAaQX8gA3RBf3Otg3ynIgRPDQIgBCAGayIGIBlNDQEgBygCjEdFDQEgB0HR/gA2AgQgAEG5DDYCGAsgASECIAshBQwFCwJAIA5FBEAgDCARIAZraiEDDAELIAYgDk0EQCAMIA4gBmtqIQMMAQsgDCARIAYgDmsiBmtqIQMgBSAGTQ0AIAUgBmshBQJAAkAgASADTSABIA8gAWusIhogBq0iGyAaIBtUGyIapyIGaiICIANLcQ0AIAMgBmogAUsgASADT3ENACABIAMgBhAHGiACIQEMAQsgASADIAMgAWsiASABQR91IgFqIAFzIgIQByACaiEBIBogAq0iHn0iHFANACACIANqIQIDQAJAIBwgHiAcIB5UGyIbQiBUBEAgGyEaDAELIBsiGkIgfSIgQgWIQgF8QgODIh9QRQRAA0AgASACKQAANwAAIAEgAikAGDcAGCABIAIpABA3ABAgASACKQAINwAIIBpCIH0hGiACQSBqIQIgAUEgaiEBIB9CAX0iH0IAUg0ACwsgIELgAFQNAANAIAEgAikAADcAACABIAIpABg3ABggASACKQAQNwAQIAEgAikACDcACCABIAIpADg3ADggASACKQAwNwAwIAEgAikAKDcAKCABIAIpACA3ACAgASACKQBYNwBYIAEgAikAUDcAUCABIAIpAEg3AEggASACKQBANwBAIAEgAikAYDcAYCABIAIpAGg3AGggASACKQBwNwBwIAEgAikAeDcAeCACQYABaiECIAFBgAFqIQEgGkKAAX0iGkIfVg0ACwsgGkIQWgRAIAEgAikAADcAACABIAIpAAg3AAggGkIQfSEaIAJBEGohAiABQRBqIQELIBpCCFoEQCABIAIpAAA3AAAgGkIIfSEaIAJBCGohAiABQQhqIQELIBpCBFoEQCABIAIoAAA2AAAgGkIEfSEaIAJBBGohAiABQQRqIQELIBpCAloEQCABIAIvAAA7AAAgGkICfSEaIAJBAmohAiABQQJqIQELIBwgG30hHCAaUEUEQCABIAItAAA6AAAgAkEBaiECIAFBAWohAQsgHEIAUg0ACwsgDiEGIAwhAwsgBSAGSwRAAkACQCABIANNIAEgDyABa6wiGiAGrSIbIBogG1QbIhqnIglqIgIgA0txDQAgAyAJaiABSyABIANPcQ0AIAEgAyAJEAcaDAELIAEgAyADIAFrIgEgAUEfdSIBaiABcyIBEAcgAWohAiAaIAGtIh59IhxQDQAgASADaiEBA0ACQCAcIB4gHCAeVBsiG0IgVARAIBshGgwBCyAbIhpCIH0iIEIFiEIBfEIDgyIfUEUEQANAIAIgASkAADcAACACIAEpABg3ABggAiABKQAQNwAQIAIgASkACDcACCAaQiB9IRogAUEgaiEBIAJBIGohAiAfQgF9Ih9CAFINAAsLICBC4ABUDQADQCACIAEpAAA3AAAgAiABKQAYNwAYIAIgASkAEDcAECACIAEpAAg3AAggAiABKQA4NwA4IAIgASkAMDcAMCACIAEpACg3ACggAiABKQAgNwAgIAIgASkAWDcAWCACIAEpAFA3AFAgAiABKQBINwBIIAIgASkAQDcAQCACIAEpAGA3AGAgAiABKQBoNwBoIAIgASkAcDcAcCACIAEpAHg3AHggAUGAAWohASACQYABaiECIBpCgAF9IhpCH1YNAAsLIBpCEFoEQCACIAEpAAA3AAAgAiABKQAINwAIIBpCEH0hGiACQRBqIQIgAUEQaiEBCyAaQghaBEAgAiABKQAANwAAIBpCCH0hGiACQQhqIQIgAUEIaiEBCyAaQgRaBEAgAiABKAAANgAAIBpCBH0hGiACQQRqIQIgAUEEaiEBCyAaQgJaBEAgAiABLwAAOwAAIBpCAn0hGiACQQJqIQIgAUECaiEBCyAcIBt9IRwgGlBFBEAgAiABLQAAOgAAIAJBAWohAiABQQFqIQELIBxCAFINAAsLIAUgBmshAUEAIARrIQUCQCAEQQdLBEAgBCEDDAELIAEgBE0EQCAEIQMMAQsgAiAEayEFA0ACQCACIAUpAAA3AAAgBEEBdCEDIAEgBGshASACIARqIQIgBEEDSw0AIAMhBCABIANLDQELC0EAIANrIQULIAIgBWohBAJAIAUgDyACa6wiGiABrSIbIBogG1QbIhqnIgFIIAVBf0pxDQAgBUEBSCABIARqIAJLcQ0AIAIgBCABEAcgAWohAgwDCyACIAQgAyADQR91IgFqIAFzIgEQByABaiECIBogAa0iHn0iHFANAiABIARqIQEDQAJAIBwgHiAcIB5UGyIbQiBUBEAgGyEaDAELIBsiGkIgfSIgQgWIQgF8QgODIh9QRQRAA0AgAiABKQAANwAAIAIgASkAGDcAGCACIAEpABA3ABAgAiABKQAINwAIIBpCIH0hGiABQSBqIQEgAkEgaiECIB9CAX0iH0IAUg0ACwsgIELgAFQNAANAIAIgASkAADcAACACIAEpABg3ABggAiABKQAQNwAQIAIgASkACDcACCACIAEpADg3ADggAiABKQAwNwAwIAIgASkAKDcAKCACIAEpACA3ACAgAiABKQBYNwBYIAIgASkAUDcAUCACIAEpAEg3AEggAiABKQBANwBAIAIgASkAYDcAYCACIAEpAGg3AGggAiABKQBwNwBwIAIgASkAeDcAeCABQYABaiEBIAJBgAFqIQIgGkKAAX0iGkIfVg0ACwsgGkIQWgRAIAIgASkAADcAACACIAEpAAg3AAggGkIQfSEaIAJBEGohAiABQRBqIQELIBpCCFoEQCACIAEpAAA3AAAgGkIIfSEaIAJBCGohAiABQQhqIQELIBpCBFoEQCACIAEoAAA2AAAgGkIEfSEaIAJBBGohAiABQQRqIQELIBpCAloEQCACIAEvAAA7AAAgGkICfSEaIAJBAmohAiABQQJqIQELIBwgG30hHCAaUEUEQCACIAEtAAA6AAAgAkEBaiECIAFBAWohAQsgHFBFDQALDAILAkAgASADTSABIA8gAWusIhogBa0iGyAaIBtUGyIapyIEaiICIANLcQ0AIAMgBGogAUsgASADT3ENACABIAMgBBAHGgwCCyABIAMgAyABayIBIAFBH3UiAWogAXMiARAHIAFqIQIgGiABrSIefSIcUA0BIAEgA2ohAQNAAkAgHCAeIBwgHlQbIhtCIFQEQCAbIRoMAQsgGyIaQiB9IiBCBYhCAXxCA4MiH1BFBEADQCACIAEpAAA3AAAgAiABKQAYNwAYIAIgASkAEDcAECACIAEpAAg3AAggGkIgfSEaIAFBIGohASACQSBqIQIgH0IBfSIfQgBSDQALCyAgQuAAVA0AA0AgAiABKQAANwAAIAIgASkAGDcAGCACIAEpABA3ABAgAiABKQAINwAIIAIgASkAODcAOCACIAEpADA3ADAgAiABKQAoNwAoIAIgASkAIDcAICACIAEpAFg3AFggAiABKQBQNwBQIAIgASkASDcASCACIAEpAEA3AEAgAiABKQBgNwBgIAIgASkAaDcAaCACIAEpAHA3AHAgAiABKQB4NwB4IAFBgAFqIQEgAkGAAWohAiAaQoABfSIaQh9WDQALCyAaQhBaBEAgAiABKQAANwAAIAIgASkACDcACCAaQhB9IRogAkEQaiECIAFBEGohAQsgGkIIWgRAIAIgASkAADcAACAaQgh9IRogAkEIaiECIAFBCGohAQsgGkIEWgRAIAIgASgAADYAACAaQgR9IRogAkEEaiECIAFBBGohAQsgGkICWgRAIAIgAS8AADsAACAaQgJ9IRogAkECaiECIAFBAmohAQsgHCAbfSEcIBpQRQRAIAIgAS0AADoAACACQQFqIQIgAUEBaiEBCyAcUEUNAAsMAQsCQAJAIBYEQAJAIAQgBUkEQCAHKAKYRyAESw0BCyABIARrIQMCQEEAIARrIgVBf0ogDyABa6wiGiAbIBogG1QbIhqnIgIgBUpxDQAgBUEBSCACIANqIAFLcQ0AIAEgAyACEAcgAmohAgwFCyABIAMgBCAEQR91IgFqIAFzIgEQByABaiECIBogAa0iHn0iHFANBCABIANqIQEDQAJAIBwgHiAcIB5UGyIbQiBUBEAgGyEaDAELIBsiGkIgfSIgQgWIQgF8QgODIh9QRQRAA0AgAiABKQAANwAAIAIgASkAGDcAGCACIAEpABA3ABAgAiABKQAINwAIIBpCIH0hGiABQSBqIQEgAkEgaiECIB9CAX0iH0IAUg0ACwsgIELgAFQNAANAIAIgASkAADcAACACIAEpABg3ABggAiABKQAQNwAQIAIgASkACDcACCACIAEpADg3ADggAiABKQAwNwAwIAIgASkAKDcAKCACIAEpACA3ACAgAiABKQBYNwBYIAIgASkAUDcAUCACIAEpAEg3AEggAiABKQBANwBAIAIgASkAYDcAYCACIAEpAGg3AGggAiABKQBwNwBwIAIgASkAeDcAeCABQYABaiEBIAJBgAFqIQIgGkKAAX0iGkIfVg0ACwsgGkIQWgRAIAIgASkAADcAACACIAEpAAg3AAggGkIQfSEaIAJBEGohAiABQRBqIQELIBpCCFoEQCACIAEpAAA3AAAgGkIIfSEaIAJBCGohAiABQQhqIQELIBpCBFoEQCACIAEoAAA2AAAgGkIEfSEaIAJBBGohAiABQQRqIQELIBpCAloEQCACIAEvAAA7AAAgGkICfSEaIAJBAmohAiABQQJqIQELIBwgG30hHCAaUEUEQCACIAEtAAA6AAAgAkEBaiECIAFBAWohAQsgHFBFDQALDAQLIBAgAWsiCUEBaiIGIAUgBSAGSxshAyABIARrIQIgAUEHcUUNAiADRQ0CIAEgAi0AADoAACACQQFqIQIgAUEBaiIGQQdxQQAgA0EBayIFGw0BIAYhASAFIQMgCSEGDAILAkAgBCAFSQRAIAcoAphHIARLDQELIAEgASAEayIGKQAANwAAIAEgBUEBa0EHcUEBaiIDaiECIAUgA2siBEUNAyADIAZqIQEDQCACIAEpAAA3AAAgAUEIaiEBIAJBCGohAiAEQQhrIgQNAAsMAwsgASAEIAUQPyECDAILIAEgAi0AADoAASAJQQFrIQYgA0ECayEFIAJBAWohAgJAIAFBAmoiCkEHcUUNACAFRQ0AIAEgAi0AADoAAiAJQQJrIQYgA0EDayEFIAJBAWohAgJAIAFBA2oiCkEHcUUNACAFRQ0AIAEgAi0AADoAAyAJQQNrIQYgA0EEayEFIAJBAWohAgJAIAFBBGoiCkEHcUUNACAFRQ0AIAEgAi0AADoABCAJQQRrIQYgA0EFayEFIAJBAWohAgJAIAFBBWoiCkEHcUUNACAFRQ0AIAEgAi0AADoABSAJQQVrIQYgA0EGayEFIAJBAWohAgJAIAFBBmoiCkEHcUUNACAFRQ0AIAEgAi0AADoABiAJQQZrIQYgA0EHayEFIAJBAWohAgJAIAFBB2oiCkEHcUUNACAFRQ0AIAEgAi0AADoAByAJQQdrIQYgA0EIayEDIAFBCGohASACQQFqIQIMBgsgCiEBIAUhAwwFCyAKIQEgBSEDDAQLIAohASAFIQMMAwsgCiEBIAUhAwwCCyAKIQEgBSEDDAELIAohASAFIQMLAkACQCAGQRdNBEAgA0UNASADQQFrIQUgA0EHcSIEBEADQCABIAItAAA6AAAgA0EBayEDIAFBAWohASACQQFqIQIgBEEBayIEDQALCyAFQQdJDQEDQCABIAItAAA6AAAgASACLQABOgABIAEgAi0AAjoAAiABIAItAAM6AAMgASACLQAEOgAEIAEgAi0ABToABSABIAItAAY6AAYgASACLQAHOgAHIAFBCGohASACQQhqIQIgA0EIayIDDQALDAELIAMNAQsgASECDAELIAEgBCADED8hAgsgCyEFDAELIAEgAy0AAjoAACABQQFqIQILIAUgFE8NACACIBNJDQELCyAAIAI2AgwgACAFIAhBA3ZrIgE2AgAgACATIAJrQYMCajYCECAAIBQgAWtBDmo2AgQgByAIQQdxIgA2AogBIAcgHUJ/IACthkJ/hYM+AoQBC+cFAQR/IAMgAiACIANLGyEEIAAgAWshAgJAIABBB3FFDQAgBEUNACAAIAItAAA6AAAgA0EBayEGIAJBAWohAiAAQQFqIgdBB3FBACAEQQFrIgUbRQRAIAchACAFIQQgBiEDDAELIAAgAi0AADoAASADQQJrIQYgBEECayEFIAJBAWohAgJAIABBAmoiB0EHcUUNACAFRQ0AIAAgAi0AADoAAiADQQNrIQYgBEEDayEFIAJBAWohAgJAIABBA2oiB0EHcUUNACAFRQ0AIAAgAi0AADoAAyADQQRrIQYgBEEEayEFIAJBAWohAgJAIABBBGoiB0EHcUUNACAFRQ0AIAAgAi0AADoABCADQQVrIQYgBEEFayEFIAJBAWohAgJAIABBBWoiB0EHcUUNACAFRQ0AIAAgAi0AADoABSADQQZrIQYgBEEGayEFIAJBAWohAgJAIABBBmoiB0EHcUUNACAFRQ0AIAAgAi0AADoABiADQQdrIQYgBEEHayEFIAJBAWohAgJAIABBB2oiB0EHcUUNACAFRQ0AIAAgAi0AADoAByADQQhrIQMgBEEIayEEIABBCGohACACQQFqIQIMBgsgByEAIAUhBCAGIQMMBQsgByEAIAUhBCAGIQMMBAsgByEAIAUhBCAGIQMMAwsgByEAIAUhBCAGIQMMAgsgByEAIAUhBCAGIQMMAQsgByEAIAUhBCAGIQMLAkAgA0EXTQRAIARFDQEgBEEBayEBIARBB3EiAwRAA0AgACACLQAAOgAAIARBAWshBCAAQQFqIQAgAkEBaiECIANBAWsi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CyAAIAMtAAAEfiADKQMQBUIAC6dB//8DcSADKAIEEEcLIQUgAxAICyAEQRBqJAAgBQvgAQICfwF+QTAQCSICRQRAIAEEQCABQQA2AgQgAUEONgIAC0EADwsgAkIANwMIIAJBADYCACACQgA3AxAgAkIANwMYIAJCADcDICACQgA3ACUgAFAEQCACDwsCQCAAQv////8AVg0AIACnQQR0EAkiA0UNACACIAM2AgBBACEBQgEhBANAIAMgAUEEdGoiAUIANwIAIAFCADcABSAAIARSBEAgBKchASAEQgF8IQQMAQsLIAIgADcDCCACIAA3AxAgAg8LIAEEQCABQQA2AgQgAUEONgIAC0EAEBAgAhAGQQAL7gECA38BfiMAQRBrIgQkAAJAIARBDGpCBBAXIgNFBEBBfyECDAELAkAgAQRAIAJBgAZxIQUDQAJAIAUgASgCBHFFDQACQCADKQMIQgBUBEAgA0EAOgAADAELIANCADcDECADQQE6AAALIAMgAS8BCBANIAMgAS8BChANIAMtAABFBEAgAEEIaiIABEAgAEEANgIEIABBFDYCAAtBfyECDAQLQX8hAiAAIARBDGpCBBAbQQBIDQMgATMBCiIGUA0AIAAgASgCDCAGEBtBAEgNAwsgASgCACIBDQALC0EAIQILIAMQCAsgBEEQaiQAIAILPAEBfyAABEAgAUGABnEhAQNAIAEgACgCBHEEQCACIAAvAQpqQQRqIQILIAAoAgAiAA0ACwsgAkH//wNxC5wBAQN/IABFBEBBAA8LIAAhAwNAAn8CQAJAIAAvAQgiAUH04AFNBEAgAUEBRg0BIAFB9cYBRg0BDAILIAFBgbICRg0AIAFB9eABRw0BCyAAKAIAIQEgAEEANgIAIAAoAgwQBiAAEAYgASADIAAgA0YbIQMCQCACRQRAQQAhAgwBCyACIAE2AgALIAEMAQsgACICKAIACyIADQALIAMLsgQCBX8BfgJAAkACQCAAIAGtEBciAQRAIAEtAAANAUEAIQAMAgsgBARAIARBADYCBCAEQQ42AgALQQAPC0EAIQADQCABLQAABH4gASkDCCABKQMQfQVCAAtCBFQNASABEAwhByABIAEQDCIGrRATIghFBEBBACECIAQEQCAEQQA2AgQgBEEVNgIACyABEAggAEUNAwNAIAAoAgAhASAAKAIMEAYg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DCAAQRg2AggLDAMLIAAgBiAFLwFqQQAgBCABEQYAIQEgBhALIAFFDQILAkAgDUUEQCABIQMMAQsgACABIAUvAWgQgQEhAyABEAsgA0UNAQsCQCAJRSAMckUEQCADIQEMAQsgACADQQEQgAEhASADEAsgAUUNAQsgASEDDAELQQAhAwsgBUHwAGokACADC4UBAQF/IAFFBEAgAEEIaiIABEAgAEEANgIEIABBEjYCAAtBAA8LQTgQCSIDRQRAIABBCGoiAARAIABBADYCBCAAQQ42AgALQQAPCyADQQA2AhAgA0IANwIIIANCADcDKCADQQA2AgQgAyACNgIAIANCADcDGCADQQA2AjAgACABQTsgAxBCCw8AIAAgASACQQBBABCCAQusAgECfyABRQRAIABBCGoiAARAIABBADYCBCAAQRI2AgALQQAPCwJAIAJBfUsNACACQf//A3FBCEYNACAAQQhqIgAEQCAAQQA2AgQgAEEQNgIAC0EADwsCQEGwwAAQCSIFBEAgBUEANgIIIAVCADcCACAFQYiBAUGogQEgAxs2AqhAIAUgAjYCFCAFIAM6ABAgBUEAOgAPIAVBADsBDCAFIAMgAkF9SyIGcToADiAFQQggAiAGG0H//wNxIAQgBUGIgQFBqIEBIAMbKAIAEQAAIgI2AqxAIAINASAFEDEgBRAGCyAAQQhqIgAEQCAAQQA2AgQgAEEONgIAC0EADwsgACABQTogBRBCIgAEfyAABSAFKAKsQCAFKAKoQCgCBBEDACAFEDEgBRAGQQALC6ABAQF/IAIgACgCBCIDIAIgA0kbIgIEQCAAIAMgAms2AgQCQAJAAkACQCAAKAIcIgMoAhRBAWsOAgEAAgsgA0GgAWogASAAKAIAIAJB3IABKAIAEQgADAILIAAgACgCMCABIAAoAgAgAkHEgAEoAgARBAA2AjAMAQsgASAAKAIAIAIQBxoLIAAgACgCACACajYCACAAIAAoAgggAmo2AggLC7cCAQR/QX4hAgJAIABFDQAgACgCIEUNACAAKAIkIgRFDQAgACgCHCIBRQ0AIAEoAgAgAEcNAAJAAkAgASgCICIDQTlrDjkBAgICAgICAgICAgIBAgICAQICAgICAgICAgICAgICAgICAQICAgICAgICAgICAQICAgICAgICAgEACyADQZoFRg0AIANBKkcN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DCABajYCDCAGIAYoAgggAWo2AgggBSAFKAIUIAFqNgIUIAUgBSgCECABazYCECAGIAYoAhAgAWsiATYCECABDQAgBiAGKAIENgIIC0EAIQRBACEJIAMoAhBFDQAMCgsgAygCHCgCJCEGIAMgAygCGCIBQQFqNgIYIAEgBmotAAAhASADIARBAWo2AhAgAygCBCAEaiABOgAAIAEEQCADKAIQIQQMAQsLIAMoAhwoAixFDQAgAygCECIGIAlNDQAgBQJ/IAUoAjAhBEEAIAMoAgQgCWoiAUUNABogBCABIAYgCWtB1IABKAIAEQAACzYCMAsgA0HnADYCIAsCQCADKAIcKAIsBEAgAygCDCADKAIQIgFBAmpJBH8gBRAKIAMoAhANAkEABSABCyADKAIEaiAFKAIwOwAAIAMgAygCEEECajYCECADQaABakHkgAEoAgARAQAaCyADQfEANgIgIAUQCiADKAIQRQ0BDAcLDAYLIAUoAgQNAQsgAygCPA0AIApFDQEgAygCIEGaBUYNAQsCfyADKAKIASIBRQRAIAMgChCFAQwBCwJAAkACQCADKAKMAUECaw4CAAECCwJ/AkADQAJAAkAgAygCPA0AIAMQLyADKAI8DQAgCg0BQQAMBAsgAygCSCADKAJoai0AACEEIAMgAygC8C0iAUEBajYC8C0gASADKALsLWpBADoAACADIAMoAvAtIgFBAWo2AvAtIAEgAygC7C1qQQA6AAAgAyADKALwLSIBQQFqNgLwLSABIAMoAuwtaiAEOgAAIAMgBEECdGoiASABLwHkAUEBajsB5AEgAyADKAI8QQFrNgI8IAMgAygCaEEBaiIBNgJoIAMoAvAtIAMoAvQtRw0BQQAhBCADIAMoAlgiBkEATgR/IAMoAkggBmoFQQALIAEgBmtBABAPIAMgAygCaDYCWCADKAIAEAogAygCACgCEA0BDAILCyADQQA2AoQuIApBBEYEQCADIAMoAlgiAUEATgR/IAMoAkggAWoFQQALIAMoAmggAWtBARAPIAMgAygCaDYCWCADKAIAEApBA0ECIAMoAgAoAhAbDAILIAMoAvAtBEBBACEEIAMgAygCWCIBQQBOBH8gAygCSCABagVBAAsgAygCaCABa0EAEA8gAyADKAJoNgJYIAMoAgAQCiADKAIAKAIQRQ0BC0EBIQQLIAQL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bGl0ZXJhbC9sZW5ndGhzIHNldABpbnZhbGlkIGNvZGUgbGVuZ3RocyBzZXQAdW5rbm93biBoZWFkZXIgZmxhZ3Mgc2V0AGludmFsaWQgZGlzdGFuY2VzIHNldABpbnZhbGlkIGJpdCBsZW5ndGggcmVwZWF0AEZpbGUgYWxyZWFkeSBleGlzdHMAdG9vIG1hbnkgbGVuZ3RoIG9yIGRpc3RhbmNlIHN5bWJvbHMAaW52YWxpZCBzdG9yZWQgYmxvY2sgbGVuZ3RocwAlcyVzJXMAYnVmZmVyIGVycm9yAE5vIGVycm9yAHN0cmVhbSBlcnJvcgBUZWxsIGVycm9yAEludGVybmFsIGVycm9yAFNlZWsgZXJyb3IAV3JpdGUgZXJyb3IAZmlsZSBlcnJvcgBSZWFkIGVycm9yAFpsaWIgZXJyb3IAZGF0YSBlcnJvcgBDUkMgZXJyb3IAaW5jb21wYXRpYmxlIHZlcnNpb24AaW52YWxpZCBjb2RlIC0tIG1pc3NpbmcgZW5kLW9mLWJsb2NrAGluY29ycmVjdCBoZWFkZXIgY2hlY2sAaW5jb3JyZWN0IGxlbmd0aCBjaGVjawBpbmNvcnJlY3QgZGF0YSBjaGVjawBpbnZhbGlkIGRpc3RhbmNlIHRvbyBmYXIgYmFjawBoZWFkZXIgY3JjIG1pc21hdGNoADEuMi4xMy56bGliLW5nAGludmFsaWQgd2luZG93IHNpemUAUmVhZC1vbmx5IGFyY2hpdmUATm90IGEgemlwIGFyY2hpdmUAUmVzb3VyY2Ugc3RpbGwgaW4gdXNlAE1hbGxvYyBmYWlsdXJlAGludmFsaWQgYmxvY2sgdHlwZQBGYWlsdXJlIHRvIGNyZWF0ZSB0ZW1wb3JhcnkgZmlsZQBDYW4ndCBvcGVuIGZpbGUATm8gc3VjaCBmaWxlAFByZW1hdHVyZSBlbmQgb2YgZmlsZQBDYW4ndCByZW1vdmUgZmlsZQBpbnZhbGlkIGxpdGVyYWwvbGVuZ3RoIGNvZGUAaW52YWxpZCBkaXN0YW5jZSBjb2RlAHVua25vd24gY29tcHJlc3Npb24gbWV0aG9kAHN0cmVhbSBlbmQAQ29tcHJlc3NlZCBkYXRhIGludmFsaWQATXVsdGktZGlzayB6aXAgYXJjaGl2ZXMgbm90IHN1cHBvcnRl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UgAIANIACAAyAAgAsgAIAHIACADyAAgACgAIAIoACABKAAgAygAIACoACACqAAgAagAIAOoACAAaAAgAmgAIAFoACADaAAgAOgAIALoACAB6AAgA+gAIAAYACACGAAgARgAIAMYACAAmAAgApgAIAGYACADmAAgAFgAIAJYACABWAAgA1gAIADYACAC2AAgAdgAIAPYACAAOAAgAjgAIAE4ACADOAAgALgAIAK4ACABuAAgA7gAIAB4ACACeAAgAXgAIAN4ACAA+AAgAvgAIAH4ACAD+AAgAAQAIAIEACABBAAgAwQAIACEACAChAAgAYQAIAOEACAARAAgAkQAIAFEACADRAAgAMQAIALEACABxAAgA8QAIAAkACACJAAgASQAIAMkACAApAAgAqQAIAGkACADpAAgAGQAIAJkACABZAAgA2QAIADkACAC5AAgAeQAIAPkACAAFAAgAhQAIAEUACADFAAgAJQAIAKUACABlAAgA5QAIABUACACVAAgAVQAIANUACAA1AAgAtQAIAHUACAD1AAgADQAIAI0ACABNAAgAzQAIAC0ACACtAAgAbQAIAO0ACAAdAAgAnQAIAF0ACADdAAgAPQAIAL0ACAB9AAgA/QAIABMACQATAQkAkwAJAJMBCQBTAAkAUwEJANMACQDTAQkAMwAJADMBCQCzAAkAswEJAHMACQBzAQkA8wAJAPMBCQALAAkACwEJAIsACQCLAQkASwAJAEsBCQDLAAkAywEJACsACQArAQkAqwAJAKsBCQBrAAkAawEJAOsACQDrAQkAGwAJABsBCQCbAAkAmwEJAFsACQBbAQkA2wAJANsBCQA7AAkAOwEJALsACQC7AQkAewAJAHsBCQD7AAkA+wEJAAcACQAHAQkAhwAJAIcBCQBHAAkARwEJAMcACQDHAQkAJwAJACcBCQCnAAkApwEJAGcACQBnAQkA5wAJAOcBCQAXAAkAFwEJAJcACQCXAQkAVwAJAFcBCQDXAAkA1wEJADcACQA3AQkAtwAJALcBCQB3AAkAdwEJAPcACQD3AQkADwAJAA8BCQCPAAkAjwEJAE8ACQBPAQkAzwAJAM8BCQAvAAkALwEJAK8ACQCvAQkAbwAJAG8BCQDvAAkA7wEJAB8ACQAfAQkAnwAJAJ8BCQBfAAkAXwEJAN8ACQDfAQkAPwAJAD8BCQC/AAkA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SXMgYSBkaXJlY3RvcnkAVGV4dCBmaWxlIGJ1c3kARXhlYyBmb3JtYXQgZXJyb3IASW52YWxpZCBhcmd1bWVudABBcmd1bWVudCBsaXN0IHRvbyBsb25nAFN5bWJvbGljIGxpbmsgbG9vcABGaWxlbmFtZSB0b28gbG9uZwBUb28gbWFueSBvcGVuIGZpbGVzIGluIHN5c3RlbQBObyBmaWxlIGRlc2NyaXB0b3JzIGF2YWlsYWJsZQBCYWQgZmlsZSBkZXNjcmlwdG9yAE5vIGNoaWxkIHByb2Nlc3MAQmFkIGFkZHJlc3MARmlsZSB0b28gbGFyZ2UAVG9vIG1hbnkgbGlua3MATm8gbG9ja3MgYXZhaWxhYmxlAFJlc291cmNlIGRlYWRsb2NrIHdvdWxkIG9jY3VyAFN0YXRlIG5vdCByZWNvdmVyYWJsZQBQcmV2aW91cyBvd25lciBkaWVkAE9wZXJhdGlvbiBjYW5jZWxlZABGdW5jdGlvbiBub3QgaW1wbGVtZW50ZWQATm8gbWVzc2FnZSBvZiBkZXNpcmVkIHR5cGUASWRlbnRpZmllciByZW1vdmVkAERldmljZSBub3QgYSBzdHJlYW0ATm8gZGF0YSBhdmFpbGFibGUARGV2aWNlIHRpbWVvdXQAT3V0IG9mIHN0cmVhbXMgcmVzb3VyY2VzAExpbmsgaGFzIGJlZW4gc2V2ZXJlZABQcm90b2NvbCBlcnJvcgBCYWQgbWVzc2FnZQBGaWxlIGRlc2NyaXB0b3IgaW4gYmFkIHN0YXRlAE5vdCBhIHNvY2tldABEZXN0aW5hdGlvbiBhZGRyZXNzIHJlcXVpcmVkAE1lc3NhZ2UgdG9vIGxhcmdlAFByb3RvY29sIHdyb25nIHR5cGUgZm9yIHNvY2tldABQcm90b2NvbCBub3QgYXZhaWxhYmxlAFByb3RvY29sIG5vdCBzdXBwb3J0ZWQAU29ja2V0IHR5cGUgbm90IHN1cHBvcnRlZABOb3Qgc3VwcG9ydGVkAFByb3RvY29sIGZhbWlseSBub3Qgc3VwcG9ydGVkAEFkZHJlc3MgZmFtaWx5IG5vdCBzdXBwb3J0ZWQgYnkgcHJvdG9jb2wAQWRkcmVzcyBub3QgYXZhaWxhYmxlAE5ldHdvcmsgaXMgZG93bgBOZXR3b3JrIHVucmVhY2hhYmxlAENvbm5lY3Rpb24gcmVzZXQgYnkgbmV0d29yawBDb25uZWN0aW9uIGFib3J0ZWQATm8gYnVmZmVyIHNwYWNlIGF2YWlsYWJsZQBTb2NrZXQgaXMgY29ubmVjdGVkAFNvY2tldCBub3QgY29ubmVjdGVkAENhbm5vdCBzZW5kIGFmdGVyIHNvY2tldCBzaHV0ZG93bgBPcGVyYXRpb24gYWxyZWFkeSBpbiBwcm9ncmVzcwBPcGVyYXRpb24gaW4gcHJvZ3Jlc3MAU3RhbGUgZmlsZSBoYW5kbGUAUmVtb3RlIEkvTyBlcnJvcgBRdW90YSBleGNlZWRlZABObyBtZWRpdW0gZm91bmQAV3JvbmcgbWVkaXVtIHR5cGUATm8gZXJyb3IgaW5mb3JtYXRpb24AQcCAAQuFARMAAAAUAAAAFQAAABYAAAAXAAAAGAAAABkAAAAaAAAAGwAAABwAAAAdAAAAHgAAAB8AAAAgAAAAIQAAACIAAAAjAAAAgERQADEAAAAyAAAAMwAAADQAAAA1AAAANgAAADcAAAA4AAAAOQAAADIAAAAzAAAANAAAADUAAAA2AAAANwAAADgAQfSCAQsCXEQAQbCDAQsQ/////////////////////w==";bo(ji)||(ji=x(ji));function oo(Ke){try{if(Ke==ji&&ae)return new Uint8Array(ae);var ot=Ga(Ke);if(ot)return ot;if(T)return T(Ke);throw"sync fetching of the wasm failed: you can preload it to Module['wasmBinary'] manually, or emcc.py will do that for you when generating HTML (but not JS)"}catch(St){ns(St)}}function Po(Ke,ot){var St,lr,ee;try{ee=oo(Ke),lr=new WebAssembly.Module(ee),St=new WebAssembly.Instance(lr,ot)}catch(Oe){var ye=Oe.toString();throw te("failed to compile wasm module: "+ye),(ye.includes("imported Memory")||ye.includes("memory import"))&&te("Memory size incompatibility issues may be due to changing INITIAL_MEMORY at runtime to something too large. Use ALLOW_MEMORY_GROWTH to allow any size memory (and also make sure not to set INITIAL_MEMORY at runtime to something smaller than it was at compile time)."),Oe}return[St,lr]}function TA(){var Ke={a:Ue};function ot(ee,ye){var Oe=ee.exports;r.asm=Oe,Ce=r.asm.g,z(Ce.buffer),$=r.asm.W,gn(r.asm.h),Do("wasm-instantiate")}if(li("wasm-instantiate"),r.instantiateWasm)try{var St=r.instantiateWasm(Ke,ot);return St}catch(ee){return te("Module.instantiateWasm callback failed with error: "+ee),!1}var lr=Po(ji,Ke);return ot(lr[0]),r.asm}function df(Ke){return F.getFloat32(Ke,!0)}function dh(Ke){return F.getFloat64(Ke,!0)}function gh(Ke){return F.getInt16(Ke,!0)}function ao(Ke){return F.getInt32(Ke,!0)}function Gn(Ke,ot){F.setInt32(Ke,ot,!0)}function Ns(Ke){for(;Ke.length>0;){var ot=Ke.shift();if(typeof ot=="function"){ot(r);continue}var St=ot.func;typeof St=="number"?ot.arg===void 0?$.get(St)():$.get(St)(ot.arg):St(ot.arg===void 0?null:ot.arg)}}function lo(Ke,ot){var St=new Date(ao((Ke>>2)*4)*1e3);Gn((ot>>2)*4,St.getUTCSeconds()),Gn((ot+4>>2)*4,St.getUTCMinutes()),Gn((ot+8>>2)*4,St.getUTCHours()),Gn((ot+12>>2)*4,St.getUTCDate()),Gn((ot+16>>2)*4,St.getUTCMonth()),Gn((ot+20>>2)*4,St.getUTCFullYear()-1900),Gn((ot+24>>2)*4,St.getUTCDay()),Gn((ot+36>>2)*4,0),Gn((ot+32>>2)*4,0);var lr=Date.UTC(St.getUTCFullYear(),0,1,0,0,0,0),ee=(St.getTime()-lr)/(1e3*60*60*24)|0;return Gn((ot+28>>2)*4,ee),lo.GMTString||(lo.GMTString=tt("GMT")),Gn((ot+40>>2)*4,lo.GMTString),ot}function su(Ke,ot){return lo(Ke,ot)}function ou(Ke,ot,St){Re.copyWithin(Ke,ot,ot+St)}function au(Ke){try{return Ce.grow(Ke-be.byteLength+65535>>>16),z(Ce.buffer),1}catch{}}function FA(Ke){var ot=Re.length;Ke=Ke>>>0;var St=2147483648;if(Ke>St)return!1;for(var lr=1;lr<=4;lr*=2){var ee=ot*(1+.2/lr);ee=Math.min(ee,Ke+100663296);var ye=Math.min(St,ke(Math.max(Ke,ee),65536)),Oe=au(ye);if(Oe)return!0}return!1}function NA(Ke){ue(Ke)}function fa(Ke){var ot=Date.now()/1e3|0;return Ke&&Gn((Ke>>2)*4,ot),ot}function Aa(){if(Aa.called)return;Aa.called=!0;var Ke=new Date().getFullYear(),ot=new Date(Ke,0,1),St=new Date(Ke,6,1),lr=ot.getTimezoneOffset(),ee=St.getTimezoneOffset(),ye=Math.max(lr,ee);Gn((Ql()>>2)*4,ye*60),Gn((Bs()>>2)*4,+(lr!=ee));function Oe(pn){var ci=pn.toTimeString().match(/\(([A-Za-z ]+)\)$/);return ci?ci[1]:"GMT"}var mt=Oe(ot),Et=Oe(St),bt=tt(mt),tr=tt(Et);ee>2)*4,bt),Gn((Mi()+4>>2)*4,tr)):(Gn((Mi()>>2)*4,tr),Gn((Mi()+4>>2)*4,bt))}function OA(Ke){Aa();var ot=Date.UTC(ao((Ke+20>>2)*4)+1900,ao((Ke+16>>2)*4),ao((Ke+12>>2)*4),ao((Ke+8>>2)*4),ao((Ke+4>>2)*4),ao((Ke>>2)*4),0),St=new Date(ot);Gn((Ke+24>>2)*4,St.getUTCDay());var lr=Date.UTC(St.getUTCFullYear(),0,1,0,0,0,0),ee=(St.getTime()-lr)/(1e3*60*60*24)|0;return Gn((Ke+28>>2)*4,ee),St.getTime()/1e3|0}var dr=typeof atob=="function"?atob:function(Ke){var ot="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",St="",lr,ee,ye,Oe,mt,Et,bt,tr=0;Ke=Ke.replace(/[^A-Za-z0-9\+\/\=]/g,"");do Oe=ot.indexOf(Ke.charAt(tr++)),mt=ot.indexOf(Ke.charAt(tr++)),Et=ot.indexOf(Ke.charAt(tr++)),bt=ot.indexOf(Ke.charAt(tr++)),lr=Oe<<2|mt>>4,ee=(mt&15)<<4|Et>>2,ye=(Et&3)<<6|bt,St=St+String.fromCharCode(lr),Et!==64&&(St=St+String.fromCharCode(ee)),bt!==64&&(St=St+String.fromCharCode(ye));while(tr0||(Ct(),Cr>0))return;function ot(){Tn||(Tn=!0,r.calledRun=!0,!Ee&&(qt(),s(r),r.onRuntimeInitialized&&r.onRuntimeInitialized(),ir()))}r.setStatus?(r.setStatus("Running..."),setTimeout(function(){setTimeout(function(){r.setStatus("")},1),ot()},1)):ot()}if(r.run=hc,r.preInit)for(typeof r.preInit=="function"&&(r.preInit=[r.preInit]);r.preInit.length>0;)r.preInit.pop()();return hc(),t}}();typeof CR=="object"&&typeof hj=="object"?hj.exports=pj:typeof define=="function"&&define.amd?define([],function(){return pj}):typeof CR=="object"&&(CR.createModule=pj)});var Hp,FAe,NAe,OAe=Xe(()=>{Hp=["number","number"],FAe=(Z=>(Z[Z.ZIP_ER_OK=0]="ZIP_ER_OK",Z[Z.ZIP_ER_MULTIDISK=1]="ZIP_ER_MULTIDISK",Z[Z.ZIP_ER_RENAME=2]="ZIP_ER_RENAME",Z[Z.ZIP_ER_CLOSE=3]="ZIP_ER_CLOSE",Z[Z.ZIP_ER_SEEK=4]="ZIP_ER_SEEK",Z[Z.ZIP_ER_READ=5]="ZIP_ER_READ",Z[Z.ZIP_ER_WRITE=6]="ZIP_ER_WRITE",Z[Z.ZIP_ER_CRC=7]="ZIP_ER_CRC",Z[Z.ZIP_ER_ZIPCLOSED=8]="ZIP_ER_ZIPCLOSED",Z[Z.ZIP_ER_NOENT=9]="ZIP_ER_NOENT",Z[Z.ZIP_ER_EXISTS=10]="ZIP_ER_EXISTS",Z[Z.ZIP_ER_OPEN=11]="ZIP_ER_OPEN",Z[Z.ZIP_ER_TMPOPEN=12]="ZIP_ER_TMPOPEN",Z[Z.ZIP_ER_ZLIB=13]="ZIP_ER_ZLIB",Z[Z.ZIP_ER_MEMORY=14]="ZIP_ER_MEMORY",Z[Z.ZIP_ER_CHANGED=15]="ZIP_ER_CHANGED",Z[Z.ZIP_ER_COMPNOTSUPP=16]="ZIP_ER_COMPNOTSUPP",Z[Z.ZIP_ER_EOF=17]="ZIP_ER_EOF",Z[Z.ZIP_ER_INVAL=18]="ZIP_ER_INVAL",Z[Z.ZIP_ER_NOZIP=19]="ZIP_ER_NOZIP",Z[Z.ZIP_ER_INTERNAL=20]="ZIP_ER_INTERNAL",Z[Z.ZIP_ER_INCONS=21]="ZIP_ER_INCONS",Z[Z.ZIP_ER_REMOVE=22]="ZIP_ER_REMOVE",Z[Z.ZIP_ER_DELETED=23]="ZIP_ER_DELETED",Z[Z.ZIP_ER_ENCRNOTSUPP=24]="ZIP_ER_ENCRNOTSUPP",Z[Z.ZIP_ER_RDONLY=25]="ZIP_ER_RDONLY",Z[Z.ZIP_ER_NOPASSWD=26]="ZIP_ER_NOPASSWD",Z[Z.ZIP_ER_WRONGPASSWD=27]="ZIP_ER_WRONGPASSWD",Z[Z.ZIP_ER_OPNOTSUPP=28]="ZIP_ER_OPNOTSUPP",Z[Z.ZIP_ER_INUSE=29]="ZIP_ER_INUSE",Z[Z.ZIP_ER_TELL=30]="ZIP_ER_TELL",Z[Z.ZIP_ER_COMPRESSED_DATA=31]="ZIP_ER_COMPRESSED_DATA",Z))(FAe||{}),NAe=e=>({get HEAPU8(){return e.HEAPU8},errors:FAe,SEEK_SET:0,SEEK_CUR:1,SEEK_END:2,ZIP_CHECKCONS:4,ZIP_EXCL:2,ZIP_RDONLY:16,ZIP_FL_OVERWRITE:8192,ZIP_FL_COMPRESSED:4,ZIP_OPSYS_DOS:0,ZIP_OPSYS_AMIGA:1,ZIP_OPSYS_OPENVMS:2,ZIP_OPSYS_UNIX:3,ZIP_OPSYS_VM_CMS:4,ZIP_OPSYS_ATARI_ST:5,ZIP_OPSYS_OS_2:6,ZIP_OPSYS_MACINTOSH:7,ZIP_OPSYS_Z_SYSTEM:8,ZIP_OPSYS_CPM:9,ZIP_OPSYS_WINDOWS_NTFS:10,ZIP_OPSYS_MVS:11,ZIP_OPSYS_VSE:12,ZIP_OPSYS_ACORN_RISC:13,ZIP_OPSYS_VFAT:14,ZIP_OPSYS_ALTERNATE_MVS:15,ZIP_OPSYS_BEOS:16,ZIP_OPSYS_TANDEM:17,ZIP_OPSYS_OS_400:18,ZIP_OPSYS_OS_X:19,ZIP_CM_DEFAULT:-1,ZIP_CM_STORE:0,ZIP_CM_DEFLATE:8,uint08S:e._malloc(1),uint32S:e._malloc(4),malloc:e._malloc,free:e._free,getValue:e.getValue,openFromSource:e.cwrap("zip_open_from_source","number",["number","number","number"]),close:e.cwrap("zip_close","number",["number"]),discard:e.cwrap("zip_discard",null,["number"]),getError:e.cwrap("zip_get_error","number",["number"]),getName:e.cwrap("zip_get_name","string",["number","number","number"]),getNumEntries:e.cwrap("zip_get_num_entries","number",["number","number"]),delete:e.cwrap("zip_delete","number",["number","number"]),statIndex:e.cwrap("zip_stat_index","number",["number",...Hp,"number","number"]),fopenIndex:e.cwrap("zip_fopen_index","number",["number",...Hp,"number"]),fread:e.cwrap("zip_fread","number",["number","number","number","number"]),fclose:e.cwrap("zip_fclose","number",["number"]),dir:{add:e.cwrap("zip_dir_add","number",["number","string"])},file:{add:e.cwrap("zip_file_add","number",["number","string","number","number"]),getError:e.cwrap("zip_file_get_error","number",["number"]),getExternalAttributes:e.cwrap("zip_file_get_external_attributes","number",["number",...Hp,"number","number","number"]),setExternalAttributes:e.cwrap("zip_file_set_external_attributes","number",["number",...Hp,"number","number","number"]),setMtime:e.cwrap("zip_file_set_mtime","number",["number",...Hp,"number","number"]),setCompression:e.cwrap("zip_set_file_compression","number",["number",...Hp,"number","number"])},ext:{countSymlinks:e.cwrap("zip_ext_count_symlinks","number",["number"])},error:{initWithCode:e.cwrap("zip_error_init_with_code",null,["number","number"]),strerror:e.cwrap("zip_error_strerror","string",["number"])},name:{locate:e.cwrap("zip_name_locate","number",["number","string","number"])},source:{fromUnattachedBuffer:e.cwrap("zip_source_buffer_create","number",["number",...Hp,"number","number"]),fromBuffer:e.cwrap("zip_source_buffer","number",["number","number",...Hp,"number"]),free:e.cwrap("zip_source_free",null,["number"]),keep:e.cwrap("zip_source_keep",null,["number"]),open:e.cwrap("zip_source_open","number",["number"]),close:e.cwrap("zip_source_close","number",["number"]),seek:e.cwrap("zip_source_seek","number",["number",...Hp,"number"]),tell:e.cwrap("zip_source_tell","number",["number"]),read:e.cwrap("zip_source_read","number",["number","number","number"]),error:e.cwrap("zip_source_error","number",["number"])},struct:{statS:e.cwrap("zipstruct_statS","number",[]),statSize:e.cwrap("zipstruct_stat_size","number",["number"]),statCompSize:e.cwrap("zipstruct_stat_comp_size","number",["number"]),statCompMethod:e.cwrap("zipstruct_stat_comp_method","number",["number"]),statMtime:e.cwrap("zipstruct_stat_mtime","number",["number"]),statCrc:e.cwrap("zipstruct_stat_crc","number",["number"]),errorS:e.cwrap("zipstruct_errorS","number",[]),errorCodeZip:e.cwrap("zipstruct_error_code_zip","number",["number"])}})});function dj(e,t){let r=e.indexOf(t);if(r<=0)return null;let s=r;for(;r>=0&&(s=r+t.length,e[s]!==J.sep);){if(e[r-1]===J.sep)return null;r=e.indexOf(t,s)}return e.length>s&&e[s]!==J.sep?null:e.slice(0,s)}var rA,LAe=Xe(()=>{Dt();Dt();nA();rA=class e extends $h{static async openPromise(t,r){let s=new e(r);try{return await t(s)}finally{s.saveAndClose()}}constructor(t={}){let r=t.fileExtensions,s=t.readOnlyArchives,a=typeof r>"u"?f=>dj(f,".zip"):f=>{for(let p of r){let h=dj(f,p);if(h)return h}return null},n=(f,p)=>new ps(p,{baseFs:f,readOnly:s,stats:f.statSync(p),customZipImplementation:t.customZipImplementation}),c=async(f,p)=>{let h={baseFs:f,readOnly:s,stats:await f.statPromise(p),customZipImplementation:t.customZipImplementation};return()=>new ps(p,h)};super({...t,factorySync:n,factoryPromise:c,getMountPoint:a})}}});var gj,DI,mj=Xe(()=>{Aj();gj=class extends Error{constructor(t,r){super(t),this.name="Libzip Error",this.code=r}},DI=class{constructor(t){this.filesShouldBeCached=!0;let r="buffer"in t?t.buffer:t.baseFs.readFileSync(t.path);this.libzip=tv();let s=this.libzip.malloc(4);try{let c=0;t.readOnly&&(c|=this.libzip.ZIP_RDONLY);let f=this.allocateUnattachedSource(r);try{this.zip=this.libzip.openFromSource(f,c,s),this.lzSource=f}catch(p){throw this.libzip.source.free(f),p}if(this.zip===0){let p=this.libzip.struct.errorS();throw this.libzip.error.initWithCode(p,this.libzip.getValue(s,"i32")),this.makeLibzipError(p)}}finally{this.libzip.free(s)}let a=this.libzip.getNumEntries(this.zip,0),n=new Array(a);for(let c=0;c>>0,n=this.libzip.struct.statMtime(r)>>>0,c=this.libzip.struct.statCrc(r)>>>0;return{size:a,mtime:n,crc:c}}makeLibzipError(t){let r=this.libzip.struct.errorCodeZip(t),s=this.libzip.error.strerror(t),a=new gj(s,this.libzip.errors[r]);if(r===this.libzip.errors.ZIP_ER_CHANGED)throw new Error(`Assertion failed: Unexpected libzip error: ${a.message}`);return a}setFileSource(t,r,s){let a=this.allocateSource(s);try{let n=this.libzip.file.add(this.zip,t,a,this.libzip.ZIP_FL_OVERWRITE);if(n===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));if(r!==null&&this.libzip.file.setCompression(this.zip,n,0,r[0],r[1])===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));return n}catch(n){throw this.libzip.source.free(a),n}}setMtime(t,r){if(this.libzip.file.setMtime(this.zip,t,0,r,0)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip))}getExternalAttributes(t){if(this.libzip.file.getExternalAttributes(this.zip,t,0,0,this.libzip.uint08S,this.libzip.uint32S)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));let s=this.libzip.getValue(this.libzip.uint08S,"i8")>>>0,a=this.libzip.getValue(this.libzip.uint32S,"i32")>>>0;return[s,a]}setExternalAttributes(t,r,s){if(this.libzip.file.setExternalAttributes(this.zip,t,0,0,r,s)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip))}locate(t){return this.libzip.name.locate(this.zip,t,0)}getFileSource(t){let r=this.libzip.struct.statS();if(this.libzip.statIndex(this.zip,t,0,0,r)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));let a=this.libzip.struct.statCompSize(r),n=this.libzip.struct.statCompMethod(r),c=this.libzip.malloc(a);try{let f=this.libzip.fopenIndex(this.zip,t,0,this.libzip.ZIP_FL_COMPRESSED);if(f===0)throw this.makeLibzipError(this.libzip.getError(this.zip));try{let p=this.libzip.fread(f,c,a,0);if(p===-1)throw this.makeLibzipError(this.libzip.file.getError(f));if(pa)throw new Error("Overread");let h=this.libzip.HEAPU8.subarray(c,c+a);return{data:Buffer.from(h),compressionMethod:n}}finally{this.libzip.fclose(f)}}finally{this.libzip.free(c)}}deleteEntry(t){if(this.libzip.delete(this.zip,t)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip))}addDirectory(t){let r=this.libzip.dir.add(this.zip,t);if(r===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));return r}getBufferAndClose(){try{if(this.libzip.source.keep(this.lzSource),this.libzip.close(this.zip)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));if(this.libzip.source.open(this.lzSource)===-1)throw this.makeLibzipError(this.libzip.source.error(this.lzSource));if(this.libzip.source.seek(this.lzSource,0,0,this.libzip.SEEK_END)===-1)throw this.makeLibzipError(this.libzip.source.error(this.lzSource));let t=this.libzip.source.tell(this.lzSource);if(t===-1)throw this.makeLibzipError(this.libzip.source.error(this.lzSource));if(this.libzip.source.seek(this.lzSource,0,0,this.libzip.SEEK_SET)===-1)throw this.makeLibzipError(this.libzip.source.error(this.lzSource));let r=this.libzip.malloc(t);if(!r)throw new Error("Couldn't allocate enough memory");try{let s=this.libzip.source.read(this.lzSource,r,t);if(s===-1)throw this.makeLibzipError(this.libzip.source.error(this.lzSource));if(st)throw new Error("Overread");let a=Buffer.from(this.libzip.HEAPU8.subarray(r,r+t));return process.env.YARN_IS_TEST_ENV&&process.env.YARN_ZIP_DATA_EPILOGUE&&(a=Buffer.concat([a,Buffer.from(process.env.YARN_ZIP_DATA_EPILOGUE)])),a}finally{this.libzip.free(r)}}finally{this.libzip.source.close(this.lzSource),this.libzip.source.free(this.lzSource)}}allocateBuffer(t){Buffer.isBuffer(t)||(t=Buffer.from(t));let r=this.libzip.malloc(t.byteLength);if(!r)throw new Error("Couldn't allocate enough memory");return new Uint8Array(this.libzip.HEAPU8.buffer,r,t.byteLength).set(t),{buffer:r,byteLength:t.byteLength}}allocateUnattachedSource(t){let r=this.libzip.struct.errorS(),{buffer:s,byteLength:a}=this.allocateBuffer(t),n=this.libzip.source.fromUnattachedBuffer(s,a,0,1,r);if(n===0)throw this.libzip.free(r),this.makeLibzipError(r);return n}allocateSource(t){let{buffer:r,byteLength:s}=this.allocateBuffer(t),a=this.libzip.source.fromBuffer(this.zip,r,s,0,1);if(a===0)throw this.libzip.free(r),this.makeLibzipError(this.libzip.getError(this.zip));return a}discard(){this.libzip.discard(this.zip)}}});function Ftt(e){if(typeof e=="string"&&String(+e)===e)return+e;if(typeof e=="number"&&Number.isFinite(e))return e<0?Date.now()/1e3:e;if(MAe.types.isDate(e))return e.getTime()/1e3;throw new Error("Invalid time")}function wR(){return Buffer.from([80,75,5,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0])}var Ta,yj,MAe,Ej,om,Ij,Cj,UAe,ps,BR=Xe(()=>{Dt();Dt();Dt();Dt();Dt();Dt();Ta=Ie("fs"),yj=Ie("stream"),MAe=Ie("util"),Ej=et(Ie("zlib"));mj();om=3,Ij=0,Cj=8,UAe="mixed";ps=class extends jf{constructor(r,s={}){super();this.listings=new Map;this.entries=new Map;this.fileSources=new Map;this.fds=new Map;this.nextFd=0;this.ready=!1;this.readOnly=!1;s.readOnly&&(this.readOnly=!0);let a=s;this.level=typeof a.level<"u"?a.level:UAe;let n=s.customZipImplementation??DI;if(typeof r=="string"){let{baseFs:f=new Vn}=a;this.baseFs=f,this.path=r}else this.path=null,this.baseFs=null;if(s.stats)this.stats=s.stats;else if(typeof r=="string")try{this.stats=this.baseFs.statSync(r)}catch(f){if(f.code==="ENOENT"&&a.create)this.stats=al.makeDefaultStats();else throw f}else this.stats=al.makeDefaultStats();typeof r=="string"?s.create?this.zipImpl=new n({buffer:wR(),readOnly:this.readOnly}):this.zipImpl=new n({path:r,baseFs:this.baseFs,readOnly:this.readOnly,size:this.stats.size}):this.zipImpl=new n({buffer:r??wR(),readOnly:this.readOnly}),this.listings.set(vt.root,new Set);let c=this.zipImpl.getListings();for(let f=0;f{this.closeSync(f)}})}async readPromise(r,s,a,n,c){return this.readSync(r,s,a,n,c)}readSync(r,s,a=0,n=s.byteLength,c=-1){let f=this.fds.get(r);if(typeof f>"u")throw or.EBADF("read");let p=c===-1||c===null?f.cursor:c,h=this.readFileSync(f.p);h.copy(s,a,p,p+n);let E=Math.max(0,Math.min(h.length-p,n));return(c===-1||c===null)&&(f.cursor+=E),E}async writePromise(r,s,a,n,c){return typeof s=="string"?this.writeSync(r,s,c):this.writeSync(r,s,a,n,c)}writeSync(r,s,a,n,c){throw typeof this.fds.get(r)>"u"?or.EBADF("read"):new Error("Unimplemented")}async closePromise(r){return this.closeSync(r)}closeSync(r){if(typeof this.fds.get(r)>"u")throw or.EBADF("read");this.fds.delete(r)}createReadStream(r,{encoding:s}={}){if(r===null)throw new Error("Unimplemented");let a=this.openSync(r,"r"),n=Object.assign(new yj.PassThrough({emitClose:!0,autoDestroy:!0,destroy:(f,p)=>{clearImmediate(c),this.closeSync(a),p(f)}}),{close(){n.destroy()},bytesRead:0,path:r,pending:!1}),c=setImmediate(async()=>{try{let f=await this.readFilePromise(r,s);n.bytesRead=f.length,n.end(f)}catch(f){n.destroy(f)}});return n}createWriteStream(r,{encoding:s}={}){if(this.readOnly)throw or.EROFS(`open '${r}'`);if(r===null)throw new Error("Unimplemented");let a=[],n=this.openSync(r,"w"),c=Object.assign(new yj.PassThrough({autoDestroy:!0,emitClose:!0,destroy:(f,p)=>{try{f?p(f):(this.writeFileSync(r,Buffer.concat(a),s),p(null))}catch(h){p(h)}finally{this.closeSync(n)}}}),{close(){c.destroy()},bytesWritten:0,path:r,pending:!1});return c.on("data",f=>{let p=Buffer.from(f);c.bytesWritten+=p.length,a.push(p)}),c}async realpathPromise(r){return this.realpathSync(r)}realpathSync(r){let s=this.resolveFilename(`lstat '${r}'`,r);if(!this.entries.has(s)&&!this.listings.has(s))throw or.ENOENT(`lstat '${r}'`);return s}async existsPromise(r){return this.existsSync(r)}existsSync(r){if(!this.ready)throw or.EBUSY(`archive closed, existsSync '${r}'`);if(this.symlinkCount===0){let a=J.resolve(vt.root,r);return this.entries.has(a)||this.listings.has(a)}let s;try{s=this.resolveFilename(`stat '${r}'`,r,void 0,!1)}catch{return!1}return s===void 0?!1:this.entries.has(s)||this.listings.has(s)}async accessPromise(r,s){return this.accessSync(r,s)}accessSync(r,s=Ta.constants.F_OK){let a=this.resolveFilename(`access '${r}'`,r);if(!this.entries.has(a)&&!this.listings.has(a))throw or.ENOENT(`access '${r}'`);if(this.readOnly&&s&Ta.constants.W_OK)throw or.EROFS(`access '${r}'`)}async statPromise(r,s={bigint:!1}){return s.bigint?this.statSync(r,{bigint:!0}):this.statSync(r)}statSync(r,s={bigint:!1,throwIfNoEntry:!0}){let a=this.resolveFilename(`stat '${r}'`,r,void 0,s.throwIfNoEntry);if(a!==void 0){if(!this.entries.has(a)&&!this.listings.has(a)){if(s.throwIfNoEntry===!1)return;throw or.ENOENT(`stat '${r}'`)}if(r[r.length-1]==="/"&&!this.listings.has(a))throw or.ENOTDIR(`stat '${r}'`);return this.statImpl(`stat '${r}'`,a,s)}}async fstatPromise(r,s){return this.fstatSync(r,s)}fstatSync(r,s){let a=this.fds.get(r);if(typeof a>"u")throw or.EBADF("fstatSync");let{p:n}=a,c=this.resolveFilename(`stat '${n}'`,n);if(!this.entries.has(c)&&!this.listings.has(c))throw or.ENOENT(`stat '${n}'`);if(n[n.length-1]==="/"&&!this.listings.has(c))throw or.ENOTDIR(`stat '${n}'`);return this.statImpl(`fstat '${n}'`,c,s)}async lstatPromise(r,s={bigint:!1}){return s.bigint?this.lstatSync(r,{bigint:!0}):this.lstatSync(r)}lstatSync(r,s={bigint:!1,throwIfNoEntry:!0}){let a=this.resolveFilename(`lstat '${r}'`,r,!1,s.throwIfNoEntry);if(a!==void 0){if(!this.entries.has(a)&&!this.listings.has(a)){if(s.throwIfNoEntry===!1)return;throw or.ENOENT(`lstat '${r}'`)}if(r[r.length-1]==="/"&&!this.listings.has(a))throw or.ENOTDIR(`lstat '${r}'`);return this.statImpl(`lstat '${r}'`,a,s)}}statImpl(r,s,a={}){let n=this.entries.get(s);if(typeof n<"u"){let c=this.zipImpl.stat(n),f=c.crc,p=c.size,h=c.mtime*1e3,E=this.stats.uid,C=this.stats.gid,S=512,x=Math.ceil(c.size/S),I=h,T=h,O=h,U=new Date(I),V=new Date(T),te=new Date(O),ie=new Date(h),ue=this.listings.has(s)?Ta.constants.S_IFDIR:this.isSymbolicLink(n)?Ta.constants.S_IFLNK:Ta.constants.S_IFREG,ae=ue===Ta.constants.S_IFDIR?493:420,ge=ue|this.getUnixMode(n,ae)&511,Ae=Object.assign(new al.StatEntry,{uid:E,gid:C,size:p,blksize:S,blocks:x,atime:U,birthtime:V,ctime:te,mtime:ie,atimeMs:I,birthtimeMs:T,ctimeMs:O,mtimeMs:h,mode:ge,crc:f});return a.bigint===!0?al.convertToBigIntStats(Ae):Ae}if(this.listings.has(s)){let c=this.stats.uid,f=this.stats.gid,p=0,h=512,E=0,C=this.stats.mtimeMs,S=this.stats.mtimeMs,x=this.stats.mtimeMs,I=this.stats.mtimeMs,T=new Date(C),O=new Date(S),U=new Date(x),V=new Date(I),te=Ta.constants.S_IFDIR|493,ue=Object.assign(new al.StatEntry,{uid:c,gid:f,size:p,blksize:h,blocks:E,atime:T,birthtime:O,ctime:U,mtime:V,atimeMs:C,birthtimeMs:S,ctimeMs:x,mtimeMs:I,mode:te,crc:0});return a.bigint===!0?al.convertToBigIntStats(ue):ue}throw new Error("Unreachable")}getUnixMode(r,s){let[a,n]=this.zipImpl.getExternalAttributes(r);return a!==om?s:n>>>16}registerListing(r){let s=this.listings.get(r);if(s)return s;this.registerListing(J.dirname(r)).add(J.basename(r));let n=new Set;return this.listings.set(r,n),n}registerEntry(r,s){this.registerListing(J.dirname(r)).add(J.basename(r)),this.entries.set(r,s)}unregisterListing(r){this.listings.delete(r),this.listings.get(J.dirname(r))?.delete(J.basename(r))}unregisterEntry(r){this.unregisterListing(r);let s=this.entries.get(r);this.entries.delete(r),!(typeof s>"u")&&(this.fileSources.delete(s),this.isSymbolicLink(s)&&this.symlinkCount--)}deleteEntry(r,s){this.unregisterEntry(r),this.zipImpl.deleteEntry(s)}resolveFilename(r,s,a=!0,n=!0){if(!this.ready)throw or.EBUSY(`archive closed, ${r}`);let c=J.resolve(vt.root,s);if(c==="/")return vt.root;let f=this.entries.get(c);if(a&&f!==void 0)if(this.symlinkCount!==0&&this.isSymbolicLink(f)){let p=this.getFileSource(f).toString();return this.resolveFilename(r,J.resolve(J.dirname(c),p),!0,n)}else return c;for(;;){let p=this.resolveFilename(r,J.dirname(c),!0,n);if(p===void 0)return p;let h=this.listings.has(p),E=this.entries.has(p);if(!h&&!E){if(n===!1)return;throw or.ENOENT(r)}if(!h)throw or.ENOTDIR(r);if(c=J.resolve(p,J.basename(c)),!a||this.symlinkCount===0)break;let C=this.zipImpl.locate(c.slice(1));if(C===-1)break;if(this.isSymbolicLink(C)){let S=this.getFileSource(C).toString();c=J.resolve(J.dirname(c),S)}else break}return c}setFileSource(r,s){let a=Buffer.isBuffer(s)?s:Buffer.from(s),n=J.relative(vt.root,r),c=null;this.level!=="mixed"&&(c=[this.level===0?Ij:Cj,this.level]);let f=this.zipImpl.setFileSource(n,c,a);return this.fileSources.set(f,a),f}isSymbolicLink(r){if(this.symlinkCount===0)return!1;let[s,a]=this.zipImpl.getExternalAttributes(r);return s!==om?!1:(a>>>16&Ta.constants.S_IFMT)===Ta.constants.S_IFLNK}getFileSource(r,s={asyncDecompress:!1}){let a=this.fileSources.get(r);if(typeof a<"u")return a;let{data:n,compressionMethod:c}=this.zipImpl.getFileSource(r);if(c===Ij)return this.zipImpl.filesShouldBeCached&&this.fileSources.set(r,n),n;if(c===Cj){if(s.asyncDecompress)return new Promise((f,p)=>{Ej.default.inflateRaw(n,(h,E)=>{h?p(h):(this.zipImpl.filesShouldBeCached&&this.fileSources.set(r,E),f(E))})});{let f=Ej.default.inflateRawSync(n);return this.zipImpl.filesShouldBeCached&&this.fileSources.set(r,f),f}}else throw new Error(`Unsupported compression method: ${c}`)}async fchmodPromise(r,s){return this.chmodPromise(this.fdToPath(r,"fchmod"),s)}fchmodSync(r,s){return this.chmodSync(this.fdToPath(r,"fchmodSync"),s)}async chmodPromise(r,s){return this.chmodSync(r,s)}chmodSync(r,s){if(this.readOnly)throw or.EROFS(`chmod '${r}'`);s&=493;let a=this.resolveFilename(`chmod '${r}'`,r,!1),n=this.entries.get(a);if(typeof n>"u")throw new Error(`Assertion failed: The entry should have been registered (${a})`);let f=this.getUnixMode(n,Ta.constants.S_IFREG|0)&-512|s;this.zipImpl.setExternalAttributes(n,om,f<<16)}async fchownPromise(r,s,a){return this.chownPromise(this.fdToPath(r,"fchown"),s,a)}fchownSync(r,s,a){return this.chownSync(this.fdToPath(r,"fchownSync"),s,a)}async chownPromise(r,s,a){return this.chownSync(r,s,a)}chownSync(r,s,a){throw new Error("Unimplemented")}async renamePromise(r,s){return this.renameSync(r,s)}renameSync(r,s){throw new Error("Unimplemented")}async copyFilePromise(r,s,a){let{indexSource:n,indexDest:c,resolvedDestP:f}=this.prepareCopyFile(r,s,a),p=await this.getFileSource(n,{asyncDecompress:!0}),h=this.setFileSource(f,p);h!==c&&this.registerEntry(f,h)}copyFileSync(r,s,a=0){let{indexSource:n,indexDest:c,resolvedDestP:f}=this.prepareCopyFile(r,s,a),p=this.getFileSource(n),h=this.setFileSource(f,p);h!==c&&this.registerEntry(f,h)}prepareCopyFile(r,s,a=0){if(this.readOnly)throw or.EROFS(`copyfile '${r} -> '${s}'`);if(a&Ta.constants.COPYFILE_FICLONE_FORCE)throw or.ENOSYS("unsupported clone operation",`copyfile '${r}' -> ${s}'`);let n=this.resolveFilename(`copyfile '${r} -> ${s}'`,r),c=this.entries.get(n);if(typeof c>"u")throw or.EINVAL(`copyfile '${r}' -> '${s}'`);let f=this.resolveFilename(`copyfile '${r}' -> ${s}'`,s),p=this.entries.get(f);if(a&(Ta.constants.COPYFILE_EXCL|Ta.constants.COPYFILE_FICLONE_FORCE)&&typeof p<"u")throw or.EEXIST(`copyfile '${r}' -> '${s}'`);return{indexSource:c,resolvedDestP:f,indexDest:p}}async appendFilePromise(r,s,a){if(this.readOnly)throw or.EROFS(`open '${r}'`);return typeof a>"u"?a={flag:"a"}:typeof a=="string"?a={flag:"a",encoding:a}:typeof a.flag>"u"&&(a={flag:"a",...a}),this.writeFilePromise(r,s,a)}appendFileSync(r,s,a={}){if(this.readOnly)throw or.EROFS(`open '${r}'`);return typeof a>"u"?a={flag:"a"}:typeof a=="string"?a={flag:"a",encoding:a}:typeof a.flag>"u"&&(a={flag:"a",...a}),this.writeFileSync(r,s,a)}fdToPath(r,s){let a=this.fds.get(r)?.p;if(typeof a>"u")throw or.EBADF(s);return a}async writeFilePromise(r,s,a){let{encoding:n,mode:c,index:f,resolvedP:p}=this.prepareWriteFile(r,a);f!==void 0&&typeof a=="object"&&a.flag&&a.flag.includes("a")&&(s=Buffer.concat([await this.getFileSource(f,{asyncDecompress:!0}),Buffer.from(s)])),n!==null&&(s=s.toString(n));let h=this.setFileSource(p,s);h!==f&&this.registerEntry(p,h),c!==null&&await this.chmodPromise(p,c)}writeFileSync(r,s,a){let{encoding:n,mode:c,index:f,resolvedP:p}=this.prepareWriteFile(r,a);f!==void 0&&typeof a=="object"&&a.flag&&a.flag.includes("a")&&(s=Buffer.concat([this.getFileSource(f),Buffer.from(s)])),n!==null&&(s=s.toString(n));let h=this.setFileSource(p,s);h!==f&&this.registerEntry(p,h),c!==null&&this.chmodSync(p,c)}prepareWriteFile(r,s){if(typeof r=="number"&&(r=this.fdToPath(r,"read")),this.readOnly)throw or.EROFS(`open '${r}'`);let a=this.resolveFilename(`open '${r}'`,r);if(this.listings.has(a))throw or.EISDIR(`open '${r}'`);let n=null,c=null;typeof s=="string"?n=s:typeof s=="object"&&({encoding:n=null,mode:c=null}=s);let f=this.entries.get(a);return{encoding:n,mode:c,resolvedP:a,index:f}}async unlinkPromise(r){return this.unlinkSync(r)}unlinkSync(r){if(this.readOnly)throw or.EROFS(`unlink '${r}'`);let s=this.resolveFilename(`unlink '${r}'`,r);if(this.listings.has(s))throw or.EISDIR(`unlink '${r}'`);let a=this.entries.get(s);if(typeof a>"u")throw or.EINVAL(`unlink '${r}'`);this.deleteEntry(s,a)}async utimesPromise(r,s,a){return this.utimesSync(r,s,a)}utimesSync(r,s,a){if(this.readOnly)throw or.EROFS(`utimes '${r}'`);let n=this.resolveFilename(`utimes '${r}'`,r);this.utimesImpl(n,a)}async lutimesPromise(r,s,a){return this.lutimesSync(r,s,a)}lutimesSync(r,s,a){if(this.readOnly)throw or.EROFS(`lutimes '${r}'`);let n=this.resolveFilename(`utimes '${r}'`,r,!1);this.utimesImpl(n,a)}utimesImpl(r,s){this.listings.has(r)&&(this.entries.has(r)||this.hydrateDirectory(r));let a=this.entries.get(r);if(a===void 0)throw new Error("Unreachable");this.zipImpl.setMtime(a,Ftt(s))}async mkdirPromise(r,s){return this.mkdirSync(r,s)}mkdirSync(r,{mode:s=493,recursive:a=!1}={}){if(a)return this.mkdirpSync(r,{chmod:s});if(this.readOnly)throw or.EROFS(`mkdir '${r}'`);let n=this.resolveFilename(`mkdir '${r}'`,r);if(this.entries.has(n)||this.listings.has(n))throw or.EEXIST(`mkdir '${r}'`);this.hydrateDirectory(n),this.chmodSync(n,s)}async rmdirPromise(r,s){return this.rmdirSync(r,s)}rmdirSync(r,{recursive:s=!1}={}){if(this.readOnly)throw or.EROFS(`rmdir '${r}'`);if(s){this.removeSync(r);return}let a=this.resolveFilename(`rmdir '${r}'`,r),n=this.listings.get(a);if(!n)throw or.ENOTDIR(`rmdir '${r}'`);if(n.size>0)throw or.ENOTEMPTY(`rmdir '${r}'`);let c=this.entries.get(a);if(typeof c>"u")throw or.EINVAL(`rmdir '${r}'`);this.deleteEntry(r,c)}async rmPromise(r,s){return this.rmSync(r,s)}rmSync(r,{recursive:s=!1}={}){if(this.readOnly)throw or.EROFS(`rm '${r}'`);if(s){this.removeSync(r);return}let a=this.resolveFilename(`rm '${r}'`,r),n=this.listings.get(a);if(!n)throw or.ENOTDIR(`rm '${r}'`);if(n.size>0)throw or.ENOTEMPTY(`rm '${r}'`);let c=this.entries.get(a);if(typeof c>"u")throw or.EINVAL(`rm '${r}'`);this.deleteEntry(r,c)}hydrateDirectory(r){let s=this.zipImpl.addDirectory(J.relative(vt.root,r));return this.registerListing(r),this.registerEntry(r,s),s}async linkPromise(r,s){return this.linkSync(r,s)}linkSync(r,s){throw or.EOPNOTSUPP(`link '${r}' -> '${s}'`)}async symlinkPromise(r,s){return this.symlinkSync(r,s)}symlinkSync(r,s){if(this.readOnly)throw or.EROFS(`symlink '${r}' -> '${s}'`);let a=this.resolveFilename(`symlink '${r}' -> '${s}'`,s);if(this.listings.has(a))throw or.EISDIR(`symlink '${r}' -> '${s}'`);if(this.entries.has(a))throw or.EEXIST(`symlink '${r}' -> '${s}'`);let n=this.setFileSource(a,r);this.registerEntry(a,n),this.zipImpl.setExternalAttributes(n,om,(Ta.constants.S_IFLNK|511)<<16),this.symlinkCount+=1}async readFilePromise(r,s){typeof s=="object"&&(s=s?s.encoding:void 0);let a=await this.readFileBuffer(r,{asyncDecompress:!0});return s?a.toString(s):a}readFileSync(r,s){typeof s=="object"&&(s=s?s.encoding:void 0);let a=this.readFileBuffer(r);return s?a.toString(s):a}readFileBuffer(r,s={asyncDecompress:!1}){typeof r=="number"&&(r=this.fdToPath(r,"read"));let a=this.resolveFilename(`open '${r}'`,r);if(!this.entries.has(a)&&!this.listings.has(a))throw or.ENOENT(`open '${r}'`);if(r[r.length-1]==="/"&&!this.listings.has(a))throw or.ENOTDIR(`open '${r}'`);if(this.listings.has(a))throw or.EISDIR("read");let n=this.entries.get(a);if(n===void 0)throw new Error("Unreachable");return this.getFileSource(n,s)}async readdirPromise(r,s){return this.readdirSync(r,s)}readdirSync(r,s){let a=this.resolveFilename(`scandir '${r}'`,r);if(!this.entries.has(a)&&!this.listings.has(a))throw or.ENOENT(`scandir '${r}'`);let n=this.listings.get(a);if(!n)throw or.ENOTDIR(`scandir '${r}'`);if(s?.recursive)if(s?.withFileTypes){let c=Array.from(n,f=>Object.assign(this.statImpl("lstat",J.join(r,f)),{name:f,path:vt.dot,parentPath:vt.dot}));for(let f of c){if(!f.isDirectory())continue;let p=J.join(f.path,f.name),h=this.listings.get(J.join(a,p));for(let E of h)c.push(Object.assign(this.statImpl("lstat",J.join(r,p,E)),{name:E,path:p,parentPath:p}))}return c}else{let c=[...n];for(let f of c){let p=this.listings.get(J.join(a,f));if(!(typeof p>"u"))for(let h of p)c.push(J.join(f,h))}return c}else return s?.withFileTypes?Array.from(n,c=>Object.assign(this.statImpl("lstat",J.join(r,c)),{name:c,path:void 0,parentPath:void 0})):[...n]}async readlinkPromise(r){let s=this.prepareReadlink(r);return(await this.getFileSource(s,{asyncDecompress:!0})).toString()}readlinkSync(r){let s=this.prepareReadlink(r);return this.getFileSource(s).toString()}prepareReadlink(r){let s=this.resolveFilename(`readlink '${r}'`,r,!1);if(!this.entries.has(s)&&!this.listings.has(s))throw or.ENOENT(`readlink '${r}'`);if(r[r.length-1]==="/"&&!this.listings.has(s))throw or.ENOTDIR(`open '${r}'`);if(this.listings.has(s))throw or.EINVAL(`readlink '${r}'`);let a=this.entries.get(s);if(a===void 0)throw new Error("Unreachable");if(!this.isSymbolicLink(a))throw or.EINVAL(`readlink '${r}'`);return a}async truncatePromise(r,s=0){let a=this.resolveFilename(`open '${r}'`,r),n=this.entries.get(a);if(typeof n>"u")throw or.EINVAL(`open '${r}'`);let c=await this.getFileSource(n,{asyncDecompress:!0}),f=Buffer.alloc(s,0);return c.copy(f),await this.writeFilePromise(r,f)}truncateSync(r,s=0){let a=this.resolveFilename(`open '${r}'`,r),n=this.entries.get(a);if(typeof n>"u")throw or.EINVAL(`open '${r}'`);let c=this.getFileSource(n),f=Buffer.alloc(s,0);return c.copy(f),this.writeFileSync(r,f)}async ftruncatePromise(r,s){return this.truncatePromise(this.fdToPath(r,"ftruncate"),s)}ftruncateSync(r,s){return this.truncateSync(this.fdToPath(r,"ftruncateSync"),s)}watch(r,s,a){let n;switch(typeof s){case"function":case"string":case"undefined":n=!0;break;default:({persistent:n=!0}=s);break}if(!n)return{on:()=>{},close:()=>{}};let c=setInterval(()=>{},24*60*60*1e3);return{on:()=>{},close:()=>{clearInterval(c)}}}watchFile(r,s,a){let n=J.resolve(vt.root,r);return lE(this,n,s,a)}unwatchFile(r,s){let a=J.resolve(vt.root,r);return dg(this,a,s)}}});function HAe(e,t,r=Buffer.alloc(0),s){let a=new ps(r),n=C=>C===t||C.startsWith(`${t}/`)?C.slice(0,t.length):null,c=async(C,S)=>()=>a,f=(C,S)=>a,p={...e},h=new Vn(p),E=new $h({baseFs:h,getMountPoint:n,factoryPromise:c,factorySync:f,magicByte:21,maxAge:1/0,typeCheck:s?.typeCheck});return Q2(_Ae.default,new e0(E)),a}var _Ae,jAe=Xe(()=>{Dt();_Ae=et(Ie("fs"));BR()});var GAe=Xe(()=>{LAe();BR();jAe()});var wj,rv,vR,qAe=Xe(()=>{Dt();BR();wj={CENTRAL_DIRECTORY:33639248,END_OF_CENTRAL_DIRECTORY:101010256},rv=22,vR=class e{constructor(t){this.filesShouldBeCached=!1;if("buffer"in t)throw new Error("Buffer based zip archives are not supported");if(!t.readOnly)throw new Error("Writable zip archives are not supported");this.baseFs=t.baseFs,this.fd=this.baseFs.openSync(t.path,"r");try{this.entries=e.readZipSync(this.fd,this.baseFs,t.size)}catch(r){throw this.baseFs.closeSync(this.fd),this.fd="closed",r}}static readZipSync(t,r,s){if(s=0;O--)if(n.readUInt32LE(O)===wj.END_OF_CENTRAL_DIRECTORY){a=O;break}if(a===-1)throw new Error("Not a zip archive")}let c=n.readUInt16LE(a+10),f=n.readUInt32LE(a+12),p=n.readUInt32LE(a+16),h=n.readUInt16LE(a+20);if(a+h+rv>n.length)throw new Error("Zip archive inconsistent");if(c==65535||f==4294967295||p==4294967295)throw new Error("Zip 64 is not supported");if(f>s)throw new Error("Zip archive inconsistent");if(c>f/46)throw new Error("Zip archive inconsistent");let E=Buffer.alloc(f);if(r.readSync(t,E,0,E.length,p)!==E.length)throw new Error("Zip archive inconsistent");let C=[],S=0,x=0,I=0;for(;xE.length)throw new Error("Zip archive inconsistent");if(E.readUInt32LE(S)!==wj.CENTRAL_DIRECTORY)throw new Error("Zip archive inconsistent");let O=E.readUInt16LE(S+4)>>>8;if(E.readUInt16LE(S+8)&1)throw new Error("Encrypted zip files are not supported");let V=E.readUInt16LE(S+10),te=E.readUInt32LE(S+16),ie=E.readUInt16LE(S+28),ue=E.readUInt16LE(S+30),ae=E.readUInt16LE(S+32),ge=E.readUInt32LE(S+42),Ae=E.toString("utf8",S+46,S+46+ie).replaceAll("\0"," ");if(Ae.includes("\0"))throw new Error("Invalid ZIP file");let Ce=E.readUInt32LE(S+20),Ee=E.readUInt32LE(S+38);C.push({name:Ae,os:O,mtime:Ai.SAFE_TIME,crc:te,compressionMethod:V,isSymbolicLink:O===om&&(Ee>>>16&Ai.S_IFMT)===Ai.S_IFLNK,size:E.readUInt32LE(S+24),compressedSize:Ce,externalAttributes:Ee,localHeaderOffset:ge}),I+=Ce,x+=1,S+=46+ie+ue+ae}if(I>s)throw new Error("Zip archive inconsistent");if(S!==E.length)throw new Error("Zip archive inconsistent");return C}getExternalAttributes(t){let r=this.entries[t];return[r.os,r.externalAttributes]}getListings(){return this.entries.map(t=>t.name)}getSymlinkCount(){let t=0;for(let r of this.entries)r.isSymbolicLink&&(t+=1);return t}stat(t){let r=this.entries[t];return{crc:r.crc,mtime:r.mtime,size:r.size}}locate(t){for(let r=0;rUAe,DEFLATE:()=>Cj,JsZipImpl:()=>vR,LibZipImpl:()=>DI,STORE:()=>Ij,ZIP_UNIX:()=>om,ZipFS:()=>ps,ZipOpenFS:()=>rA,getArchivePart:()=>dj,getLibzipPromise:()=>Ott,getLibzipSync:()=>Ntt,makeEmptyArchive:()=>wR,mountMemoryDrive:()=>HAe});function Ntt(){return tv()}async function Ott(){return tv()}var WAe,nA=Xe(()=>{Aj();WAe=et(TAe());OAe();GAe();qAe();mj();RAe(()=>{let e=(0,WAe.default)();return NAe(e)})});var iv,YAe=Xe(()=>{Dt();Yt();sv();iv=class extends at{constructor(){super(...arguments);this.cwd=he.String("--cwd",process.cwd(),{description:"The directory to run the command in"});this.commandName=he.String();this.args=he.Proxy()}static{this.usage={description:"run a command using yarn's portable shell",details:` This command will run a command using Yarn's portable shell. Make sure to escape glob patterns, redirections, and other features that might be expanded by your own shell. Note: To escape something from Yarn's shell, you might have to escape it twice, the first time from your own shell. Note: Don't use this command in Yarn scripts, as Yarn's shell is automatically used. For a list of features, visit: https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-shell/README.md. `,examples:[["Run a simple command","$0 echo Hello"],["Run a command with a glob pattern","$0 echo '*.js'"],["Run a command with a redirection","$0 echo Hello World '>' hello.txt"],["Run a command with an escaped glob pattern (The double escape is needed in Unix shells)",`$0 echo '"*.js"'`],["Run a command with a variable (Double quotes are needed in Unix shells, to prevent them from expanding the variable)",'$0 "GREETING=Hello echo $GREETING World"']]}}async execute(){let r=this.args.length>0?`${this.commandName} ${this.args.join(" ")}`:this.commandName;return await bI(r,[],{cwd:fe.toPortablePath(this.cwd),stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr})}}});var $l,VAe=Xe(()=>{$l=class extends Error{constructor(t){super(t),this.name="ShellError"}}});var bR={};Vt(bR,{fastGlobOptions:()=>zAe,isBraceExpansion:()=>Bj,isGlobPattern:()=>Ltt,match:()=>Mtt,micromatchOptions:()=>DR});function Ltt(e){if(!SR.default.scan(e,DR).isGlob)return!1;try{SR.default.parse(e,DR)}catch{return!1}return!0}function Mtt(e,{cwd:t,baseFs:r}){return(0,JAe.default)(e,{...zAe,cwd:fe.fromPortablePath(t),fs:ax(KAe.default,new e0(r))})}function Bj(e){return SR.default.scan(e,DR).isBrace}var JAe,KAe,SR,DR,zAe,XAe=Xe(()=>{Dt();JAe=et(wQ()),KAe=et(Ie("fs")),SR=et(zo()),DR={strictBrackets:!0},zAe={onlyDirectories:!1,onlyFiles:!1}});function vj(){}function Sj(){for(let e of am)e.kill()}function tpe(e,t,r,s){return a=>{let n=a[0]instanceof iA.Transform?"pipe":a[0],c=a[1]instanceof iA.Transform?"pipe":a[1],f=a[2]instanceof iA.Transform?"pipe":a[2],p=(0,$Ae.default)(e,t,{...s,stdio:[n,c,f]});return am.add(p),am.size===1&&(process.on("SIGINT",vj),process.on("SIGTERM",Sj)),a[0]instanceof iA.Transform&&a[0].pipe(p.stdin),a[1]instanceof iA.Transform&&p.stdout.pipe(a[1],{end:!1}),a[2]instanceof iA.Transform&&p.stderr.pipe(a[2],{end:!1}),{stdin:p.stdin,promise:new Promise(h=>{p.on("error",E=>{switch(am.delete(p),am.size===0&&(process.off("SIGINT",vj),process.off("SIGTERM",Sj)),E.code){case"ENOENT":a[2].write(`command not found: ${e} `),h(127);break;case"EACCES":a[2].write(`permission denied: ${e} `),h(128);break;default:a[2].write(`uncaught error: ${E.message} `),h(1);break}}),p.on("close",E=>{am.delete(p),am.size===0&&(process.off("SIGINT",vj),process.off("SIGTERM",Sj)),h(E!==null?E:129)})})}}}function rpe(e){return t=>{let r=t[0]==="pipe"?new iA.PassThrough:t[0];return{stdin:r,promise:Promise.resolve().then(()=>e({stdin:r,stdout:t[1],stderr:t[2]}))}}}function PR(e,t){return bj.start(e,t)}function ZAe(e,t=null){let r=new iA.PassThrough,s=new epe.StringDecoder,a="";return r.on("data",n=>{let c=s.write(n),f;do if(f=c.indexOf(` `),f!==-1){let p=a+c.substring(0,f);c=c.substring(f+1),a="",e(t!==null?`${t} ${p}`:p)}while(f!==-1);a+=c}),r.on("end",()=>{let n=s.end();n!==""&&e(t!==null?`${t} ${n}`:n)}),r}function npe(e,{prefix:t}){return{stdout:ZAe(r=>e.stdout.write(`${r} `),e.stdout.isTTY?t:null),stderr:ZAe(r=>e.stderr.write(`${r} `),e.stderr.isTTY?t:null)}}var $Ae,iA,epe,am,Mc,Dj,bj,Pj=Xe(()=>{$Ae=et(vU()),iA=Ie("stream"),epe=Ie("string_decoder"),am=new Set;Mc=class{constructor(t){this.stream=t}close(){}get(){return this.stream}},Dj=class{constructor(){this.stream=null}close(){if(this.stream===null)throw new Error("Assertion failed: No stream attached");this.stream.end()}attach(t){this.stream=t}get(){if(this.stream===null)throw new Error("Assertion failed: No stream attached");return this.stream}},bj=class e{constructor(t,r){this.stdin=null;this.stdout=null;this.stderr=null;this.pipe=null;this.ancestor=t,this.implementation=r}static start(t,{stdin:r,stdout:s,stderr:a}){let n=new e(null,t);return n.stdin=r,n.stdout=s,n.stderr=a,n}pipeTo(t,r=1){let s=new e(this,t),a=new Dj;return s.pipe=a,s.stdout=this.stdout,s.stderr=this.stderr,(r&1)===1?this.stdout=a:this.ancestor!==null&&(this.stderr=this.ancestor.stdout),(r&2)===2?this.stderr=a:this.ancestor!==null&&(this.stderr=this.ancestor.stderr),s}async exec(){let t=["ignore","ignore","ignore"];if(this.pipe)t[0]="pipe";else{if(this.stdin===null)throw new Error("Assertion failed: No input stream registered");t[0]=this.stdin.get()}let r;if(this.stdout===null)throw new Error("Assertion failed: No output stream registered");r=this.stdout,t[1]=r.get();let s;if(this.stderr===null)throw new Error("Assertion failed: No error stream registered");s=this.stderr,t[2]=s.get();let a=this.implementation(t);return this.pipe&&this.pipe.attach(a.stdin),await a.promise.then(n=>(r.close(),s.close(),n))}async run(){let t=[];for(let s=this;s;s=s.ancestor)t.push(s.exec());return(await Promise.all(t))[0]}}});var cv={};Vt(cv,{EntryCommand:()=>iv,ShellError:()=>$l,execute:()=>bI,globUtils:()=>bR});function ipe(e,t,r){let s=new ec.PassThrough({autoDestroy:!0});switch(e){case 0:(t&1)===1&&r.stdin.pipe(s,{end:!1}),(t&2)===2&&r.stdin instanceof ec.Writable&&s.pipe(r.stdin,{end:!1});break;case 1:(t&1)===1&&r.stdout.pipe(s,{end:!1}),(t&2)===2&&s.pipe(r.stdout,{end:!1});break;case 2:(t&1)===1&&r.stderr.pipe(s,{end:!1}),(t&2)===2&&s.pipe(r.stderr,{end:!1});break;default:throw new $l(`Bad file descriptor: "${e}"`)}return s}function kR(e,t={}){let r={...e,...t};return r.environment={...e.environment,...t.environment},r.variables={...e.variables,...t.variables},r}async function _tt(e,t,r){let s=[],a=new ec.PassThrough;return a.on("data",n=>s.push(n)),await QR(e,t,kR(r,{stdout:a})),Buffer.concat(s).toString().replace(/[\r\n]+$/,"")}async function spe(e,t,r){let s=e.map(async n=>{let c=await lm(n.args,t,r);return{name:n.name,value:c.join(" ")}});return(await Promise.all(s)).reduce((n,c)=>(n[c.name]=c.value,n),{})}function xR(e){return e.match(/[^ \r\n\t]+/g)||[]}async function fpe(e,t,r,s,a=s){switch(e.name){case"$":s(String(process.pid));break;case"#":s(String(t.args.length));break;case"@":if(e.quoted)for(let n of t.args)a(n);else for(let n of t.args){let c=xR(n);for(let f=0;f=0&&n"u"&&(e.defaultValue?c=(await lm(e.defaultValue,t,r)).join(" "):e.alternativeValue&&(c="")),typeof c>"u")throw f?new $l(`Unbound argument #${n}`):new $l(`Unbound variable "${e.name}"`);if(e.quoted)s(c);else{let p=xR(c);for(let E=0;Es.push(n));let a=Number(s.join(" "));return Number.isNaN(a)?ov({type:"variable",name:s.join(" ")},t,r):ov({type:"number",value:a},t,r)}else return Htt[e.type](await ov(e.left,t,r),await ov(e.right,t,r))}async function lm(e,t,r){let s=new Map,a=[],n=[],c=E=>{n.push(E)},f=()=>{n.length>0&&a.push(n.join("")),n=[]},p=E=>{c(E),f()},h=(E,C,S)=>{let x=JSON.stringify({type:E,fd:C}),I=s.get(x);typeof I>"u"&&s.set(x,I=[]),I.push(S)};for(let E of e){let C=!1;switch(E.type){case"redirection":{let S=await lm(E.args,t,r);for(let x of S)h(E.subtype,E.fd,x)}break;case"argument":for(let S of E.segments)switch(S.type){case"text":c(S.text);break;case"glob":c(S.pattern),C=!0;break;case"shell":{let x=await _tt(S.shell,t,r);if(S.quoted)c(x);else{let I=xR(x);for(let T=0;T"u")throw new Error("Assertion failed: Expected a glob pattern to have been set");let x=await t.glob.match(S,{cwd:r.cwd,baseFs:t.baseFs});if(x.length===0){let I=Bj(S)?". Note: Brace expansion of arbitrary strings isn't currently supported. For more details, please read this issue: https://github.com/yarnpkg/berry/issues/22":"";throw new $l(`No matches found: "${S}"${I}`)}for(let I of x.sort())p(I)}}if(s.size>0){let E=[];for(let[C,S]of s.entries())E.splice(E.length,0,C,String(S.length),...S);a.splice(0,0,"__ysh_set_redirects",...E,"--")}return a}function av(e,t,r){t.builtins.has(e[0])||(e=["command",...e]);let s=fe.fromPortablePath(r.cwd),a=r.environment;typeof a.PWD<"u"&&(a={...a,PWD:s});let[n,...c]=e;if(n==="command")return tpe(c[0],c.slice(1),t,{cwd:s,env:a});let f=t.builtins.get(n);if(typeof f>"u")throw new Error(`Assertion failed: A builtin should exist for "${n}"`);return rpe(async({stdin:p,stdout:h,stderr:E})=>{let{stdin:C,stdout:S,stderr:x}=r;r.stdin=p,r.stdout=h,r.stderr=E;try{return await f(c,t,r)}finally{r.stdin=C,r.stdout=S,r.stderr=x}})}function jtt(e,t,r){return s=>{let a=new ec.PassThrough,n=QR(e,t,kR(r,{stdin:a}));return{stdin:a,promise:n}}}function Gtt(e,t,r){return s=>{let a=new ec.PassThrough,n=QR(e,t,r);return{stdin:a,promise:n}}}function ope(e,t,r,s){if(t.length===0)return e;{let a;do a=String(Math.random());while(Object.hasOwn(s.procedures,a));return s.procedures={...s.procedures},s.procedures[a]=e,av([...t,"__ysh_run_procedure",a],r,s)}}async function ape(e,t,r){let s=e,a=null,n=null;for(;s;){let c=s.then?{...r}:r,f;switch(s.type){case"command":{let p=await lm(s.args,t,r),h=await spe(s.envs,t,r);f=s.envs.length?av(p,t,kR(c,{environment:h})):av(p,t,c)}break;case"subshell":{let p=await lm(s.args,t,r),h=jtt(s.subshell,t,c);f=ope(h,p,t,c)}break;case"group":{let p=await lm(s.args,t,r),h=Gtt(s.group,t,c);f=ope(h,p,t,c)}break;case"envs":{let p=await spe(s.envs,t,r);c.environment={...c.environment,...p},f=av(["true"],t,c)}break}if(typeof f>"u")throw new Error("Assertion failed: An action should have been generated");if(a===null)n=PR(f,{stdin:new Mc(c.stdin),stdout:new Mc(c.stdout),stderr:new Mc(c.stderr)});else{if(n===null)throw new Error("Assertion failed: The execution pipeline should have been setup");switch(a){case"|":n=n.pipeTo(f,1);break;case"|&":n=n.pipeTo(f,3);break}}s.then?(a=s.then.type,s=s.then.chain):s=null}if(n===null)throw new Error("Assertion failed: The execution pipeline should have been setup");return await n.run()}async function qtt(e,t,r,{background:s=!1}={}){function a(n){let c=["#2E86AB","#A23B72","#F18F01","#C73E1D","#CCE2A3"],f=c[n%c.length];return lpe.default.hex(f)}if(s){let n=r.nextBackgroundJobIndex++,c=a(n),f=`[${n}]`,p=c(f),{stdout:h,stderr:E}=npe(r,{prefix:p});return r.backgroundJobs.push(ape(e,t,kR(r,{stdout:h,stderr:E})).catch(C=>E.write(`${C.message} `)).finally(()=>{r.stdout.isTTY&&r.stdout.write(`Job ${p}, '${c(dE(e))}' has ended `)})),0}return await ape(e,t,r)}async function Wtt(e,t,r,{background:s=!1}={}){let a,n=f=>{a=f,r.variables["?"]=String(f)},c=async f=>{try{return await qtt(f.chain,t,r,{background:s&&typeof f.then>"u"})}catch(p){if(!(p instanceof $l))throw p;return r.stderr.write(`${p.message} `),1}};for(n(await c(e));e.then;){if(r.exitCode!==null)return r.exitCode;switch(e.then.type){case"&&":a===0&&n(await c(e.then.line));break;case"||":a!==0&&n(await c(e.then.line));break;default:throw new Error(`Assertion failed: Unsupported command type: "${e.then.type}"`)}e=e.then.line}return a}async function QR(e,t,r){let s=r.backgroundJobs;r.backgroundJobs=[];let a=0;for(let{command:n,type:c}of e){if(a=await Wtt(n,t,r,{background:c==="&"}),r.exitCode!==null)return r.exitCode;r.variables["?"]=String(a)}return await Promise.all(r.backgroundJobs),r.backgroundJobs=s,a}function Ape(e){switch(e.type){case"variable":return e.name==="@"||e.name==="#"||e.name==="*"||Number.isFinite(parseInt(e.name,10))||"defaultValue"in e&&!!e.defaultValue&&e.defaultValue.some(t=>lv(t))||"alternativeValue"in e&&!!e.alternativeValue&&e.alternativeValue.some(t=>lv(t));case"arithmetic":return xj(e.arithmetic);case"shell":return kj(e.shell);default:return!1}}function lv(e){switch(e.type){case"redirection":return e.args.some(t=>lv(t));case"argument":return e.segments.some(t=>Ape(t));default:throw new Error(`Assertion failed: Unsupported argument type: "${e.type}"`)}}function xj(e){switch(e.type){case"variable":return Ape(e);case"number":return!1;default:return xj(e.left)||xj(e.right)}}function kj(e){return e.some(({command:t})=>{for(;t;){let r=t.chain;for(;r;){let s;switch(r.type){case"subshell":s=kj(r.subshell);break;case"command":s=r.envs.some(a=>a.args.some(n=>lv(n)))||r.args.some(a=>lv(a));break}if(s)return!0;if(!r.then)break;r=r.then.chain}if(!t.then)break;t=t.then.line}return!1})}async function bI(e,t=[],{baseFs:r=new Vn,builtins:s={},cwd:a=fe.toPortablePath(process.cwd()),env:n=process.env,stdin:c=process.stdin,stdout:f=process.stdout,stderr:p=process.stderr,variables:h={},glob:E=bR}={}){let C={};for(let[I,T]of Object.entries(n))typeof T<"u"&&(C[I]=T);let S=new Map(Utt);for(let[I,T]of Object.entries(s))S.set(I,T);c===null&&(c=new ec.PassThrough,c.end());let x=ux(e,E);if(!kj(x)&&x.length>0&&t.length>0){let{command:I}=x[x.length-1];for(;I.then;)I=I.then.line;let T=I.chain;for(;T.then;)T=T.then.chain;T.type==="command"&&(T.args=T.args.concat(t.map(O=>({type:"argument",segments:[{type:"text",text:O}]}))))}return await QR(x,{args:t,baseFs:r,builtins:S,initialStdin:c,initialStdout:f,initialStderr:p,glob:E},{cwd:a,environment:C,exitCode:null,procedures:{},stdin:c,stdout:f,stderr:p,variables:Object.assign({},h,{"?":0}),nextBackgroundJobIndex:1,backgroundJobs:[]})}var lpe,cpe,ec,upe,Utt,Htt,sv=Xe(()=>{Dt();vc();lpe=et(NE()),cpe=Ie("os"),ec=Ie("stream"),upe=Ie("timers/promises");YAe();VAe();XAe();Pj();Pj();Utt=new Map([["cd",async([e=(0,cpe.homedir)(),...t],r,s)=>{let a=J.resolve(s.cwd,fe.toPortablePath(e));if(!(await r.baseFs.statPromise(a).catch(c=>{throw c.code==="ENOENT"?new $l(`cd: no such file or directory: ${e}`):c})).isDirectory())throw new $l(`cd: not a directory: ${e}`);return s.cwd=a,0}],["pwd",async(e,t,r)=>(r.stdout.write(`${fe.fromPortablePath(r.cwd)} `),0)],[":",async(e,t,r)=>0],["true",async(e,t,r)=>0],["false",async(e,t,r)=>1],["exit",async([e,...t],r,s)=>s.exitCode=parseInt(e??s.variables["?"],10)],["echo",async(e,t,r)=>(r.stdout.write(`${e.join(" ")} `),0)],["sleep",async([e],t,r)=>{if(typeof e>"u")throw new $l("sleep: missing operand");let s=Number(e);if(Number.isNaN(s))throw new $l(`sleep: invalid time interval '${e}'`);return await(0,upe.setTimeout)(1e3*s,0)}],["unset",async(e,t,r)=>{for(let s of e)delete r.environment[s],delete r.variables[s];return 0}],["__ysh_run_procedure",async(e,t,r)=>{let s=r.procedures[e[0]];return await PR(s,{stdin:new Mc(r.stdin),stdout:new Mc(r.stdout),stderr:new Mc(r.stderr)}).run()}],["__ysh_set_redirects",async(e,t,r)=>{let s=r.stdin,a=r.stdout,n=r.stderr,c=[],f=[],p=[],h=0;for(;e[h]!=="--";){let C=e[h++],{type:S,fd:x}=JSON.parse(C),I=V=>{switch(x){case null:case 0:c.push(V);break;default:throw new Error(`Unsupported file descriptor: "${x}"`)}},T=V=>{switch(x){case null:case 1:f.push(V);break;case 2:p.push(V);break;default:throw new Error(`Unsupported file descriptor: "${x}"`)}},O=Number(e[h++]),U=h+O;for(let V=h;Vt.baseFs.createReadStream(J.resolve(r.cwd,fe.toPortablePath(e[V]))));break;case"<<<":I(()=>{let te=new ec.PassThrough;return process.nextTick(()=>{te.write(`${e[V]} `),te.end()}),te});break;case"<&":I(()=>ipe(Number(e[V]),1,r));break;case">":case">>":{let te=J.resolve(r.cwd,fe.toPortablePath(e[V]));T(te==="/dev/null"?new ec.Writable({autoDestroy:!0,emitClose:!0,write(ie,ue,ae){setImmediate(ae)}}):t.baseFs.createWriteStream(te,S===">>"?{flags:"a"}:void 0))}break;case">&":T(ipe(Number(e[V]),2,r));break;default:throw new Error(`Assertion failed: Unsupported redirection type: "${S}"`)}}if(c.length>0){let C=new ec.PassThrough;s=C;let S=x=>{if(x===c.length)C.end();else{let I=c[x]();I.pipe(C,{end:!1}),I.on("end",()=>{S(x+1)})}};S(0)}if(f.length>0){let C=new ec.PassThrough;a=C;for(let S of f)C.pipe(S)}if(p.length>0){let C=new ec.PassThrough;n=C;for(let S of p)C.pipe(S)}let E=await PR(av(e.slice(h+1),t,r),{stdin:new Mc(s),stdout:new Mc(a),stderr:new Mc(n)}).run();return await Promise.all(f.map(C=>new Promise((S,x)=>{C.on("error",I=>{x(I)}),C.on("close",()=>{S()}),C.end()}))),await Promise.all(p.map(C=>new Promise((S,x)=>{C.on("error",I=>{x(I)}),C.on("close",()=>{S()}),C.end()}))),E}]]);Htt={addition:(e,t)=>e+t,subtraction:(e,t)=>e-t,multiplication:(e,t)=>e*t,division:(e,t)=>Math.trunc(e/t)}});var ppe=G((HMt,RR)=>{function Ytt(){var e=0,t=1,r=2,s=3,a=4,n=5,c=6,f=7,p=8,h=9,E=10,C=11,S=12,x=13,I=14,T=15,O=16,U=17,V=0,te=1,ie=2,ue=3,ae=4;function ge(d,Se){return 55296<=d.charCodeAt(Se)&&d.charCodeAt(Se)<=56319&&56320<=d.charCodeAt(Se+1)&&d.charCodeAt(Se+1)<=57343}function Ae(d,Se){Se===void 0&&(Se=0);var Be=d.charCodeAt(Se);if(55296<=Be&&Be<=56319&&Se=1){var me=d.charCodeAt(Se-1),ce=Be;return 55296<=me&&me<=56319?(me-55296)*1024+(ce-56320)+65536:ce}return Be}function Ce(d,Se,Be){var me=[d].concat(Se).concat([Be]),ce=me[me.length-2],Z=Be,De=me.lastIndexOf(I);if(De>1&&me.slice(1,De).every(function(_){return _==s})&&[s,x,U].indexOf(d)==-1)return ie;var Qe=me.lastIndexOf(a);if(Qe>0&&me.slice(1,Qe).every(function(_){return _==a})&&[S,a].indexOf(ce)==-1)return me.filter(function(_){return _==a}).length%2==1?ue:ae;if(ce==e&&Z==t)return V;if(ce==r||ce==e||ce==t)return Z==I&&Se.every(function(_){return _==s})?ie:te;if(Z==r||Z==e||Z==t)return te;if(ce==c&&(Z==c||Z==f||Z==h||Z==E))return V;if((ce==h||ce==f)&&(Z==f||Z==p))return V;if((ce==E||ce==p)&&Z==p)return V;if(Z==s||Z==T)return V;if(Z==n)return V;if(ce==S)return V;var st=me.indexOf(s)!=-1?me.lastIndexOf(s)-1:me.length-2;return[x,U].indexOf(me[st])!=-1&&me.slice(st+1,-1).every(function(_){return _==s})&&Z==I||ce==T&&[O,U].indexOf(Z)!=-1?V:Se.indexOf(a)!=-1?ie:ce==a&&Z==a?V:te}this.nextBreak=function(d,Se){if(Se===void 0&&(Se=0),Se<0)return 0;if(Se>=d.length-1)return d.length;for(var Be=Ee(Ae(d,Se)),me=[],ce=Se+1;ce{var Vtt=/^(.*?)(\x1b\[[^m]+m|\x1b\]8;;.*?(\x1b\\|\u0007))/,TR;function Jtt(){if(TR)return TR;if(typeof Intl.Segmenter<"u"){let e=new Intl.Segmenter("en",{granularity:"grapheme"});return TR=t=>Array.from(e.segment(t),({segment:r})=>r)}else{let e=ppe(),t=new e;return TR=r=>t.splitGraphemes(r)}}hpe.exports=(e,t=0,r=e.length)=>{if(t<0||r<0)throw new RangeError("Negative indices aren't supported by this implementation");let s=r-t,a="",n=0,c=0;for(;e.length>0;){let f=e.match(Vtt)||[e,e,void 0],p=Jtt()(f[1]),h=Math.min(t-n,p.length);p=p.slice(h);let E=Math.min(s-c,p.length);a+=p.slice(0,E).join(""),n+=h,c+=E,typeof f[2]<"u"&&(a+=f[2]),e=e.slice(f[0].length)}return a}});var An,uv=Xe(()=>{An=process.env.YARN_IS_TEST_ENV?"0.0.0":"4.14.1"});function Cpe(e,{configuration:t,json:r}){if(!t.get("enableMessageNames"))return"";let a=Kf(e===null?0:e);return!r&&e===null?jt(t,a,"grey"):a}function Qj(e,{configuration:t,json:r}){let s=Cpe(e,{configuration:t,json:r});if(!s||e===null||e===0)return s;let a=Ir[e],n=`https://yarnpkg.com/advanced/error-codes#${s}---${a}`.toLowerCase();return ZE(t,s,n)}async function PI({configuration:e,stdout:t,forceError:r},s){let a=await Ot.start({configuration:e,stdout:t,includeFooter:!1},async n=>{let c=!1,f=!1;for(let p of s)typeof p.option<"u"&&(p.error||r?(f=!0,n.reportError(50,p.message)):(c=!0,n.reportWarning(50,p.message)),p.callback?.());c&&!f&&n.reportSeparator()});return a.hasErrors()?a.exitCode():null}var Epe,FR,Ktt,gpe,mpe,S0,Ipe,ype,ztt,Xtt,NR,Ztt,Ot,fv=Xe(()=>{Epe=et(dpe()),FR=et(Rg());Gx();Fc();uv();Qc();Ktt="\xB7",gpe=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"],mpe=80,S0=FR.default.GITHUB_ACTIONS?{start:e=>`::group::${e} `,end:e=>`::endgroup:: `}:FR.default.TRAVIS?{start:e=>`travis_fold:start:${e} `,end:e=>`travis_fold:end:${e} `}:FR.default.GITLAB?{start:e=>`section_start:${Math.floor(Date.now()/1e3)}:${e.toLowerCase().replace(/\W+/g,"_")}[collapsed=true]\r\x1B[0K${e} `,end:e=>`section_end:${Math.floor(Date.now()/1e3)}:${e.toLowerCase().replace(/\W+/g,"_")}\r\x1B[0K`}:null,Ipe=S0!==null,ype=new Date,ztt=["iTerm.app","Apple_Terminal","WarpTerminal","vscode"].includes(process.env.TERM_PROGRAM)||!!process.env.WT_SESSION,Xtt=e=>e,NR=Xtt({patrick:{date:[17,3],chars:["\u{1F340}","\u{1F331}"],size:40},simba:{date:[19,7],chars:["\u{1F981}","\u{1F334}"],size:40},jack:{date:[31,10],chars:["\u{1F383}","\u{1F987}"],size:40},hogsfather:{date:[31,12],chars:["\u{1F389}","\u{1F384}"],size:40},default:{chars:["=","-"],size:80}}),Ztt=ztt&&Object.keys(NR).find(e=>{let t=NR[e];return!(t.date&&(t.date[0]!==ype.getDate()||t.date[1]!==ype.getMonth()+1))})||"default";Ot=class extends yo{constructor({configuration:r,stdout:s,json:a=!1,forceSectionAlignment:n=!1,includeNames:c=!0,includePrefix:f=!0,includeFooter:p=!0,includeLogs:h=!a,includeInfos:E=h,includeWarnings:C=h}){super();this.uncommitted=new Set;this.warningCount=0;this.errorCount=0;this.timerFooter=[];this.startTime=Date.now();this.indent=0;this.level=0;this.progress=new Map;this.progressTime=0;this.progressFrame=0;this.progressTimeout=null;this.progressStyle=null;this.progressMaxScaledSize=null;if(SB(this,{configuration:r}),this.configuration=r,this.forceSectionAlignment=n,this.includeNames=c,this.includePrefix=f,this.includeFooter=p,this.includeInfos=E,this.includeWarnings=C,this.json=a,this.stdout=s,r.get("enableProgressBars")&&!a&&s.isTTY&&s.columns>22){let S=r.get("progressBarStyle")||Ztt;if(!Object.hasOwn(NR,S))throw new Error("Assertion failed: Invalid progress bar style");this.progressStyle=NR[S];let x=Math.min(this.getRecommendedLength(),80);this.progressMaxScaledSize=Math.floor(this.progressStyle.size*x/80)}}static async start(r,s){let a=new this(r),n=process.emitWarning;process.emitWarning=(c,f)=>{if(typeof c!="string"){let h=c;c=h.message,f=f??h.name}let p=typeof f<"u"?`${f}: ${c}`:c;a.reportWarning(0,p)},r.includeVersion&&a.reportInfo(0,Jg(r.configuration,`Yarn ${An}`,2));try{await s(a)}catch(c){a.reportExceptionOnce(c)}finally{await a.finalize(),process.emitWarning=n}return a}hasErrors(){return this.errorCount>0}exitCode(){return this.hasErrors()?1:0}getRecommendedLength(){let s=this.progressStyle!==null?this.stdout.columns-1:super.getRecommendedLength();return Math.max(40,s-12-this.indent*2)}startSectionSync({reportHeader:r,reportFooter:s,skipIfEmpty:a},n){let c={committed:!1,action:()=>{r?.()}};a?this.uncommitted.add(c):(c.action(),c.committed=!0);let f=Date.now();try{return n()}catch(p){throw this.reportExceptionOnce(p),p}finally{let p=Date.now();this.uncommitted.delete(c),c.committed&&s?.(p-f)}}async startSectionPromise({reportHeader:r,reportFooter:s,skipIfEmpty:a},n){let c={committed:!1,action:()=>{r?.()}};a?this.uncommitted.add(c):(c.action(),c.committed=!0);let f=Date.now();try{return await n()}catch(p){throw this.reportExceptionOnce(p),p}finally{let p=Date.now();this.uncommitted.delete(c),c.committed&&s?.(p-f)}}startTimerImpl(r,s,a){return{cb:typeof s=="function"?s:a,reportHeader:()=>{this.level+=1,this.reportInfo(null,`\u250C ${r}`),this.indent+=1,S0!==null&&!this.json&&this.includeInfos&&this.stdout.write(S0.start(r))},reportFooter:f=>{if(this.indent-=1,S0!==null&&!this.json&&this.includeInfos){this.stdout.write(S0.end(r));for(let p of this.timerFooter)p()}this.configuration.get("enableTimers")&&f>200?this.reportInfo(null,`\u2514 Completed in ${jt(this.configuration,f,dt.DURATION)}`):this.reportInfo(null,"\u2514 Completed"),this.level-=1},skipIfEmpty:(typeof s=="function"?{}:s).skipIfEmpty}}startTimerSync(r,s,a){let{cb:n,...c}=this.startTimerImpl(r,s,a);return this.startSectionSync(c,n)}async startTimerPromise(r,s,a){let{cb:n,...c}=this.startTimerImpl(r,s,a);return this.startSectionPromise(c,n)}reportSeparator(){this.indent===0?this.writeLine(""):this.reportInfo(null,"")}reportInfo(r,s){if(!this.includeInfos)return;this.commit();let a=this.formatNameWithHyperlink(r),n=a?`${a}: `:"",c=`${this.formatPrefix(n,"blueBright")}${s}`;this.json?this.reportJson({type:"info",name:r,displayName:this.formatName(r),indent:this.formatIndent(),data:s}):this.writeLine(c)}reportWarning(r,s){if(this.warningCount+=1,!this.includeWarnings)return;this.commit();let a=this.formatNameWithHyperlink(r),n=a?`${a}: `:"";this.json?this.reportJson({type:"warning",name:r,displayName:this.formatName(r),indent:this.formatIndent(),data:s}):this.writeLine(`${this.formatPrefix(n,"yellowBright")}${s}`)}reportError(r,s){this.errorCount+=1,this.timerFooter.push(()=>this.reportErrorImpl(r,s)),this.reportErrorImpl(r,s)}reportErrorImpl(r,s){this.commit();let a=this.formatNameWithHyperlink(r),n=a?`${a}: `:"";this.json?this.reportJson({type:"error",name:r,displayName:this.formatName(r),indent:this.formatIndent(),data:s}):this.writeLine(`${this.formatPrefix(n,"redBright")}${s}`,{truncate:!1})}reportFold(r,s){if(!S0)return;let a=`${S0.start(r)}${s}${S0.end(r)}`;this.timerFooter.push(()=>this.stdout.write(a))}reportProgress(r){if(this.progressStyle===null)return{...Promise.resolve(),stop:()=>{}};if(r.hasProgress&&r.hasTitle)throw new Error("Unimplemented: Progress bars can't have both progress and titles.");let s=!1,a=Promise.resolve().then(async()=>{let c={progress:r.hasProgress?0:void 0,title:r.hasTitle?"":void 0};this.progress.set(r,{definition:c,lastScaledSize:r.hasProgress?-1:void 0,lastTitle:void 0}),this.refreshProgress({delta:-1});for await(let{progress:f,title:p}of r)s||c.progress===f&&c.title===p||(c.progress=f,c.title=p,this.refreshProgress());n()}),n=()=>{s||(s=!0,this.progress.delete(r),this.refreshProgress({delta:1}))};return{...a,stop:n}}reportJson(r){this.json&&this.writeLine(`${JSON.stringify(r)}`)}async finalize(){if(!this.includeFooter)return;let r="";this.errorCount>0?r="Failed with errors":this.warningCount>0?r="Done with warnings":r="Done";let s=jt(this.configuration,Date.now()-this.startTime,dt.DURATION),a=this.configuration.get("enableTimers")?`${r} in ${s}`:r;this.errorCount>0?this.reportError(0,a):this.warningCount>0?this.reportWarning(0,a):this.reportInfo(0,a)}writeLine(r,{truncate:s}={}){this.clearProgress({clear:!0}),this.stdout.write(`${this.truncate(r,{truncate:s})} `),this.writeProgress()}writeLines(r,{truncate:s}={}){this.clearProgress({delta:r.length});for(let a of r)this.stdout.write(`${this.truncate(a,{truncate:s})} `);this.writeProgress()}commit(){let r=this.uncommitted;this.uncommitted=new Set;for(let s of r)s.committed=!0,s.action()}clearProgress({delta:r=0,clear:s=!1}){this.progressStyle!==null&&this.progress.size+r>0&&(this.stdout.write(`\x1B[${this.progress.size+r}A`),(r>0||s)&&this.stdout.write("\x1B[0J"))}writeProgress(){if(this.progressStyle===null||(this.progressTimeout!==null&&clearTimeout(this.progressTimeout),this.progressTimeout=null,this.progress.size===0))return;let r=Date.now();r-this.progressTime>mpe&&(this.progressFrame=(this.progressFrame+1)%gpe.length,this.progressTime=r);let s=gpe[this.progressFrame];for(let a of this.progress.values()){let n="";if(typeof a.lastScaledSize<"u"){let h=this.progressStyle.chars[0].repeat(a.lastScaledSize),E=this.progressStyle.chars[1].repeat(this.progressMaxScaledSize-a.lastScaledSize);n=` ${h}${E}`}let c=this.formatName(null),f=c?`${c}: `:"",p=a.definition.title?` ${a.definition.title}`:"";this.stdout.write(`${jt(this.configuration,"\u27A4","blueBright")} ${f}${s}${n}${p} `)}this.progressTimeout=setTimeout(()=>{this.refreshProgress({force:!0})},mpe)}refreshProgress({delta:r=0,force:s=!1}={}){let a=!1,n=!1;if(s||this.progress.size===0)a=!0;else for(let c of this.progress.values()){let f=typeof c.definition.progress<"u"?Math.trunc(this.progressMaxScaledSize*c.definition.progress):void 0,p=c.lastScaledSize;c.lastScaledSize=f;let h=c.lastTitle;if(c.lastTitle=c.definition.title,f!==p||(n=h!==c.definition.title)){a=!0;break}}a&&(this.clearProgress({delta:r,clear:n}),this.writeProgress())}truncate(r,{truncate:s}={}){return this.progressStyle===null&&(s=!1),typeof s>"u"&&(s=this.configuration.get("preferTruncatedLines")),s&&(r=(0,Epe.default)(r,0,this.stdout.columns-1)),r}formatName(r){return this.includeNames?Cpe(r,{configuration:this.configuration,json:this.json}):""}formatPrefix(r,s){return this.includePrefix?`${jt(this.configuration,"\u27A4",s)} ${r}${this.formatIndent()}`:""}formatNameWithHyperlink(r){return this.includeNames?Qj(r,{configuration:this.configuration,json:this.json}):""}formatIndent(){return this.level>0||!this.forceSectionAlignment?"\u2502 ".repeat(this.indent):`${Ktt} `}}});var Cn={};Vt(Cn,{PackageManager:()=>Bpe,detectPackageManager:()=>vpe,executePackageAccessibleBinary:()=>xpe,executePackageScript:()=>OR,executePackageShellcode:()=>Rj,executeWorkspaceAccessibleBinary:()=>srt,executeWorkspaceLifecycleScript:()=>bpe,executeWorkspaceScript:()=>Dpe,getPackageAccessibleBinaries:()=>LR,getWorkspaceAccessibleBinaries:()=>Ppe,hasPackageScript:()=>rrt,hasWorkspaceScript:()=>Tj,isNodeScript:()=>Fj,makeScriptEnv:()=>Av,maybeExecuteWorkspaceLifecycleScript:()=>irt,prepareExternalProject:()=>trt});async function D0(e,t,r,s=[]){if(process.platform==="win32"){let a=`@goto #_undefined_# 2>NUL || @title %COMSPEC% & @setlocal & @"${r}" ${s.map(n=>`"${n.replace('"','""')}"`).join(" ")} %*`;await le.writeFilePromise(J.format({dir:e,name:t,ext:".cmd"}),a)}await le.writeFilePromise(J.join(e,t),`#!/bin/sh exec "${r}" ${s.map(a=>`'${a.replace(/'/g,`'"'"'`)}'`).join(" ")} "$@" `,{mode:493})}async function vpe(e){let t=await _t.tryFind(e);if(t?.packageManager){let s=PQ(t.packageManager);if(s?.name){let a=`found ${JSON.stringify({packageManager:t.packageManager})} in manifest`,[n]=s.reference.split(".");switch(s.name){case"yarn":return{packageManagerField:!0,packageManager:Number(n)===1?"Yarn Classic":"Yarn",reason:a};case"npm":return{packageManagerField:!0,packageManager:"npm",reason:a};case"pnpm":return{packageManagerField:!0,packageManager:"pnpm",reason:a}}}}let r;try{r=await le.readFilePromise(J.join(e,Er.lockfile),"utf8")}catch{}return r!==void 0?r.match(/^__metadata:$/m)?{packageManager:"Yarn",reason:'"__metadata" key found in yarn.lock'}:{packageManager:"Yarn Classic",reason:'"__metadata" key not found in yarn.lock, must be a Yarn classic lockfile'}:le.existsSync(J.join(e,"package-lock.json"))?{packageManager:"npm",reason:`found npm's "package-lock.json" lockfile`}:le.existsSync(J.join(e,"pnpm-lock.yaml"))?{packageManager:"pnpm",reason:`found pnpm's "pnpm-lock.yaml" lockfile`}:null}async function Av({project:e,locator:t,binFolder:r,ignoreCorepack:s,lifecycleScript:a,baseEnv:n=e?.configuration.env??process.env}){let c={};for(let[E,C]of Object.entries(n))typeof C<"u"&&(c[E.toLowerCase()!=="path"?E:"PATH"]=C);let f=fe.fromPortablePath(r);c.BERRY_BIN_FOLDER=fe.fromPortablePath(f);let p=process.env.COREPACK_ROOT&&!s?fe.join(process.env.COREPACK_ROOT,"dist/yarn.js"):process.argv[1];if(await Promise.all([D0(r,"node",process.execPath),...An!==null?[D0(r,"run",process.execPath,[p,"run"]),D0(r,"yarn",process.execPath,[p]),D0(r,"yarnpkg",process.execPath,[p]),D0(r,"node-gyp",process.execPath,[p,"run","--top-level","node-gyp"])]:[]]),e&&(c.INIT_CWD=fe.fromPortablePath(e.configuration.startingCwd),c.PROJECT_CWD=fe.fromPortablePath(e.cwd)),c.PATH=c.PATH?`${f}${fe.delimiter}${c.PATH}`:`${f}`,c.npm_execpath=`${f}${fe.sep}yarn`,c.npm_node_execpath=`${f}${fe.sep}node`,t){if(!e)throw new Error("Assertion failed: Missing project");let E=e.tryWorkspaceByLocator(t),C=E?E.manifest.version??"":e.storedPackages.get(t.locatorHash).version??"";c.npm_package_name=fn(t),c.npm_package_version=C;let S;if(E)S=E.cwd;else{let x=e.storedPackages.get(t.locatorHash);if(!x)throw new Error(`Package for ${Yr(e.configuration,t)} not found in the project`);let I=e.configuration.getLinkers(),T={project:e,report:new Ot({stdout:new b0.PassThrough,configuration:e.configuration})},O=I.find(U=>U.supportsPackage(x,T));if(!O)throw new Error(`The package ${Yr(e.configuration,x)} isn't supported by any of the available linkers`);S=await O.findPackageLocation(x,T)}c.npm_package_json=fe.fromPortablePath(J.join(S,Er.manifest))}let h=An!==null?`yarn/${An}`:`yarn/${kp("@yarnpkg/core").version}-core`;return c.npm_config_user_agent=`${h} npm/? node/${process.version} ${process.platform} ${process.arch}`,a&&(c.npm_lifecycle_event=a),e&&await e.configuration.triggerHook(E=>E.setupScriptEnvironment,e,c,async(E,C,S)=>await D0(r,E,C,S)),c}async function trt(e,t,{configuration:r,report:s,workspace:a=null,locator:n=null}){await ert(async()=>{await le.mktempPromise(async c=>{let f=J.join(c,"pack.log"),p=null,{stdout:h,stderr:E}=r.getSubprocessStreams(f,{prefix:fe.fromPortablePath(e),report:s}),C=n&&Hu(n)?sI(n):n,S=C?ml(C):"an external project";h.write(`Packing ${S} from sources `);let x=await vpe(e),I;x!==null?(h.write(`Using ${x.packageManager} for bootstrap. Reason: ${x.reason} `),I=x.packageManager):(h.write(`No package manager configuration detected; defaulting to Yarn `),I="Yarn");let T=I==="Yarn"&&!x?.packageManagerField;await le.mktempPromise(async O=>{let U=await Av({binFolder:O,ignoreCorepack:T,baseEnv:{...process.env,COREPACK_ENABLE_AUTO_PIN:"0"}}),te=new Map([["Yarn Classic",async()=>{let ue=a!==null?["workspace",a]:[],ae=J.join(e,Er.manifest),ge=await le.readFilePromise(ae),Ae=await Gu(process.execPath,[process.argv[1],"set","version","classic","--only-if-needed","--yarn-path"],{cwd:e,env:U,stdin:p,stdout:h,stderr:E,end:1});if(Ae.code!==0)return Ae.code;await le.writeFilePromise(ae,ge),await le.appendFilePromise(J.join(e,".npmignore"),`/.yarn `),h.write(` `),delete U.NODE_ENV;let Ce=await Gu("yarn",["install"],{cwd:e,env:U,stdin:p,stdout:h,stderr:E,end:1});if(Ce.code!==0)return Ce.code;h.write(` `);let Ee=await Gu("yarn",[...ue,"pack","--filename",fe.fromPortablePath(t)],{cwd:e,env:U,stdin:p,stdout:h,stderr:E});return Ee.code!==0?Ee.code:0}],["Yarn",async()=>{let ue=a!==null?["workspace",a]:[];U.YARN_ENABLE_INLINE_BUILDS="1";let ae=J.join(e,Er.lockfile);await le.existsPromise(ae)||await le.writeFilePromise(ae,"");let ge=await Gu("yarn",[...ue,"pack","--install-if-needed","--filename",fe.fromPortablePath(t)],{cwd:e,env:U,stdin:p,stdout:h,stderr:E});return ge.code!==0?ge.code:0}],["npm",async()=>{if(a!==null){let Se=new b0.PassThrough,Be=JE(Se);Se.pipe(h,{end:!1});let me=await Gu("npm",["--version"],{cwd:e,env:U,stdin:p,stdout:Se,stderr:E,end:0});if(Se.end(),me.code!==0)return h.end(),E.end(),me.code;let ce=(await Be).toString().trim();if(!tA(ce,">=7.x")){let Z=ka(null,"npm"),De=Mn(Z,ce),Qe=Mn(Z,">=7.x");throw new Error(`Workspaces aren't supported by ${oi(r,De)}; please upgrade to ${oi(r,Qe)} (npm has been detected as the primary package manager for ${jt(r,e,dt.PATH)})`)}}let ue=a!==null?["--workspace",a]:[];delete U.npm_config_user_agent,delete U.npm_config_production,delete U.NPM_CONFIG_PRODUCTION,delete U.NODE_ENV;let ae=await Gu("npm",["install","--legacy-peer-deps"],{cwd:e,env:U,stdin:p,stdout:h,stderr:E,end:1});if(ae.code!==0)return ae.code;let ge=new b0.PassThrough,Ae=JE(ge);ge.pipe(h);let Ce=await Gu("npm",["pack","--silent",...ue],{cwd:e,env:U,stdin:p,stdout:ge,stderr:E});if(Ce.code!==0)return Ce.code;let Ee=(await Ae).toString().trim().replace(/^.*\n/s,""),d=J.resolve(e,fe.toPortablePath(Ee));return await le.renamePromise(d,t),0}]]).get(I);if(typeof te>"u")throw new Error("Assertion failed: Unsupported workflow");let ie=await te();if(!(ie===0||typeof ie>"u"))throw le.detachTemp(c),new Lt(58,`Packing the package failed (exit code ${ie}, logs can be found here: ${jt(r,f,dt.PATH)})`)})})})}async function rrt(e,t,{project:r}){let s=r.tryWorkspaceByLocator(e);if(s!==null)return Tj(s,t);let a=r.storedPackages.get(e.locatorHash);if(!a)throw new Error(`Package for ${Yr(r.configuration,e)} not found in the project`);return await rA.openPromise(async n=>{let c=r.configuration,f=r.configuration.getLinkers(),p={project:r,report:new Ot({stdout:new b0.PassThrough,configuration:c})},h=f.find(x=>x.supportsPackage(a,p));if(!h)throw new Error(`The package ${Yr(r.configuration,a)} isn't supported by any of the available linkers`);let E=await h.findPackageLocation(a,p),C=new bn(E,{baseFs:n});return(await _t.find(vt.dot,{baseFs:C})).scripts.has(t)})}async function OR(e,t,r,{cwd:s,project:a,stdin:n,stdout:c,stderr:f}){return await le.mktempPromise(async p=>{let{manifest:h,env:E,cwd:C}=await Spe(e,{project:a,binFolder:p,cwd:s,lifecycleScript:t}),S=h.scripts.get(t);if(typeof S>"u")return 1;let x=async()=>await bI(S,r,{cwd:C,env:E,stdin:n,stdout:c,stderr:f});return await(await a.configuration.reduceHook(T=>T.wrapScriptExecution,x,a,e,t,{script:S,args:r,cwd:C,env:E,stdin:n,stdout:c,stderr:f}))()})}async function Rj(e,t,r,{cwd:s,project:a,stdin:n,stdout:c,stderr:f}){return await le.mktempPromise(async p=>{let{env:h,cwd:E}=await Spe(e,{project:a,binFolder:p,cwd:s});return await bI(t,r,{cwd:E,env:h,stdin:n,stdout:c,stderr:f})})}async function nrt(e,{binFolder:t,cwd:r,lifecycleScript:s}){let a=await Av({project:e.project,locator:e.anchoredLocator,binFolder:t,lifecycleScript:s});return await Nj(t,await Ppe(e)),typeof r>"u"&&(r=J.dirname(await le.realpathPromise(J.join(e.cwd,"package.json")))),{manifest:e.manifest,binFolder:t,env:a,cwd:r}}async function Spe(e,{project:t,binFolder:r,cwd:s,lifecycleScript:a}){let n=t.tryWorkspaceByLocator(e);if(n!==null)return nrt(n,{binFolder:r,cwd:s,lifecycleScript:a});let c=t.storedPackages.get(e.locatorHash);if(!c)throw new Error(`Package for ${Yr(t.configuration,e)} not found in the project`);return await rA.openPromise(async f=>{let p=t.configuration,h=t.configuration.getLinkers(),E={project:t,report:new Ot({stdout:new b0.PassThrough,configuration:p})},C=h.find(O=>O.supportsPackage(c,E));if(!C)throw new Error(`The package ${Yr(t.configuration,c)} isn't supported by any of the available linkers`);let S=await Av({project:t,locator:e,binFolder:r,lifecycleScript:a});await Nj(r,await LR(e,{project:t}));let x=await C.findPackageLocation(c,E),I=new bn(x,{baseFs:f}),T=await _t.find(vt.dot,{baseFs:I});return typeof s>"u"&&(s=x),{manifest:T,binFolder:r,env:S,cwd:s}})}async function Dpe(e,t,r,{cwd:s,stdin:a,stdout:n,stderr:c}){return await OR(e.anchoredLocator,t,r,{cwd:s,project:e.project,stdin:a,stdout:n,stderr:c})}function Tj(e,t){return e.manifest.scripts.has(t)}async function bpe(e,t,{cwd:r,report:s}){let{configuration:a}=e.project,n=null;await le.mktempPromise(async c=>{let f=J.join(c,`${t}.log`),p=`# This file contains the result of Yarn calling the "${t}" lifecycle script inside a workspace ("${fe.fromPortablePath(e.cwd)}") `,{stdout:h,stderr:E}=a.getSubprocessStreams(f,{report:s,prefix:Yr(a,e.anchoredLocator),header:p});s.reportInfo(36,`Calling the "${t}" lifecycle script`);let C=await Dpe(e,t,[],{cwd:r,stdin:n,stdout:h,stderr:E});if(h.end(),E.end(),C!==0)throw le.detachTemp(c),new Lt(36,`${EB(t)} script failed (exit code ${jt(a,C,dt.NUMBER)}, logs can be found here: ${jt(a,f,dt.PATH)}); run ${jt(a,`yarn ${t}`,dt.CODE)} to investigate`)})}async function irt(e,t,r){Tj(e,t)&&await bpe(e,t,r)}function Fj(e){let t=J.extname(e);if(t.match(/\.[cm]?[jt]sx?$/))return!0;if(t===".exe"||t===".bin")return!1;let r=Buffer.alloc(4),s;try{s=le.openSync(e,"r")}catch{return!0}try{le.readSync(s,r,0,r.length,0)}finally{le.closeSync(s)}let a=r.readUint32BE();return!(a===3405691582||a===3489328638||a===2135247942||(a&4294901760)===1297743872)}async function LR(e,{project:t}){let r=t.configuration,s=new Map,a=t.storedPackages.get(e.locatorHash);if(!a)throw new Error(`Package for ${Yr(r,e)} not found in the project`);let n=new b0.Writable,c=r.getLinkers(),f={project:t,report:new Ot({configuration:r,stdout:n})},p=new Set([e.locatorHash]);for(let E of a.dependencies.values()){let C=t.storedResolutions.get(E.descriptorHash);if(!C)throw new Error(`Assertion failed: The resolution (${oi(r,E)}) should have been registered`);p.add(C)}let h=await Promise.all(Array.from(p,async E=>{let C=t.storedPackages.get(E);if(!C)throw new Error(`Assertion failed: The package (${E}) should have been registered`);if(C.bin.size===0)return Xl.skip;let S=c.find(I=>I.supportsPackage(C,f));if(!S)return Xl.skip;let x=null;try{x=await S.findPackageLocation(C,f)}catch(I){if(I.code==="LOCATOR_NOT_INSTALLED")return Xl.skip;throw I}return{dependency:C,packageLocation:x}}));for(let E of h){if(E===Xl.skip)continue;let{dependency:C,packageLocation:S}=E;for(let[x,I]of C.bin){let T=J.resolve(S,I);s.set(x,[C,fe.fromPortablePath(T),Fj(T)])}}return s}async function Ppe(e){return await LR(e.anchoredLocator,{project:e.project})}async function Nj(e,t){await Promise.all(Array.from(t,([r,[,s,a]])=>a?D0(e,r,process.execPath,[s]):D0(e,r,s,[])))}async function xpe(e,t,r,{cwd:s,project:a,stdin:n,stdout:c,stderr:f,nodeArgs:p=[],packageAccessibleBinaries:h}){h??=await LR(e,{project:a});let E=h.get(t);if(!E)throw new Error(`Binary not found (${t}) for ${Yr(a.configuration,e)}`);return await le.mktempPromise(async C=>{let[,S]=E,x=await Av({project:a,locator:e,binFolder:C});await Nj(x.BERRY_BIN_FOLDER,h);let I=Fj(fe.toPortablePath(S))?Gu(process.execPath,[...p,S,...r],{cwd:s,env:x,stdin:n,stdout:c,stderr:f}):Gu(S,r,{cwd:s,env:x,stdin:n,stdout:c,stderr:f}),T;try{T=await I}finally{await le.removePromise(x.BERRY_BIN_FOLDER)}return T.code})}async function srt(e,t,r,{cwd:s,stdin:a,stdout:n,stderr:c,packageAccessibleBinaries:f}){return await xpe(e.anchoredLocator,t,r,{project:e.project,cwd:s,stdin:a,stdout:n,stderr:c,packageAccessibleBinaries:f})}var wpe,b0,Bpe,$tt,ert,Oj=Xe(()=>{Dt();Dt();nA();sv();zl();wpe=et(Ng()),b0=Ie("stream");cI();Fc();fv();uv();dR();Qc();kc();Np();Zo();Bpe=(a=>(a.Yarn1="Yarn Classic",a.Yarn2="Yarn",a.Npm="npm",a.Pnpm="pnpm",a))(Bpe||{});$tt=2,ert=(0,wpe.default)($tt)});function ynt(e,t,r){let s=t,a=t?t.next:e.head,n=new _6(r,s,a,e);return n.next===void 0&&(e.tail=n),n.prev===void 0&&(e.head=n),e.length++,n}function Ent(e,t){e.tail=new _6(t,e.tail,void 0,e),e.head||(e.head=e.tail),e.length++}function Int(e,t){e.head=new _6(t,void 0,e.head,e),e.tail||(e.tail=e.head),e.length++}var ohe,Na,pT,R6,ahe,F6,Em,dT,uhe,cT,O0,Fpe,fhe,ym,mhe,Fv,Vu,h6,whe,w6,j6,Phe,G6,wn,ds,q6,Lv,MI,Il,The,Nv,W6,El,V6,ort,art,kpe,lrt,crt,urt,jp,Gp,P0,MR,pv,UR,Qpe,_R,Rpe,qu,xI,Ks,hv,TI,zs,Fa,Xs,Lj,HR,ra,es,Mj,Uj,Tpe,_j,sA,Hj,jR,dv,cm,tc,gv,frt,Art,prt,hrt,lhe,drt,grt,mrt,vm,yrt,M0,Ku,mv,Jn,jj,Vp,Gj,u6,f6,bv,GR,LI,UI,qj,FI,U0,Ju,R0,oA,NI,Wj,Yp,yv,Yj,ZR,um,$R,Im,T6,Ert,hT,che,Irt,Crt,wrt,Brt,vrt,Srt,Drt,N6,Ov,brt,cA,Prt,Npe,xrt,Vj,Cm,qR,Jj,O6,Ahe,krt,Qrt,phe,Rrt,Trt,hhe,Frt,Nrt,Ort,Lrt,Mrt,Urt,_rt,Hrt,dhe,ghe,jrt,eT,Grt,gT,L6,wm,qrt,fm,Kj,Wrt,T0,Yrt,Vrt,Jrt,F0,Krt,zrt,Xrt,zj,Zrt,Am,uT,$rt,ent,tnt,rnt,cn,Ehe,mT,nnt,A6,p6,int,Uc,pm,qp,Xj,Ope,aA,Ev,x0,Lpe,Ci,Wp,k0,Zj,hm,hs,WR,YR,$j,Mpe,Upe,Iv,e6,VR,kI,Q0,JR,dm,KR,zR,_pe,snt,Bm,Pv,ont,Ihe,ant,lnt,yT,Che,cnt,Hpe,M6,ET,U6,unt,fnt,jpe,Ant,Bhe,pnt,Gpe,qpe,Wpe,d6,Ype,Cv,tT,g6,rT,m6,y6,E6,I6,N0,fT,C6,t6,lA,vhe,hnt,dnt,gnt,mnt,_6,Vpe,Jpe,nT,wv,Wu,gm,mm,Bv,r6,Yu,n6,iT,Kpe,B6,v6,sT,oT,zpe,i6,aT,She,s6,IT,H6,Cnt,wnt,Dhe,bhe,Bnt,vnt,uUt,Snt,Dnt,bnt,Pnt,xnt,xhe,knt,Qnt,Rnt,khe,S6,AT,Tnt,Qhe,Fnt,Rhe,Fhe,CT,Nnt,Ont,D6,Nhe,Lnt,Mnt,o6,Xpe,QI,Unt,_nt,Hnt,jnt,Gnt,qnt,Zpe,b6,$pe,P6,_c,x6,k6,lT,ehe,the,Dv,rhe,nhe,a6,L0,Zs,XR,ihe,RI,l6,c6,Q6,xv,kv,Qv,Rv,Wnt,Tv,Ynt,Vnt,Jnt,she,Y6,vv,Ohe,Knt,znt,fUt,Xnt,Znt,$nt,eit,tit,Sv,AUt,rit,Lhe=Xe(()=>{ohe=et(Ie("events"),1),Na=et(Ie("fs"),1),pT=Ie("node:events"),R6=et(Ie("node:stream"),1),ahe=Ie("node:string_decoder"),F6=et(Ie("node:path"),1),Em=et(Ie("node:fs"),1),dT=Ie("path"),uhe=Ie("events"),cT=et(Ie("assert"),1),O0=Ie("buffer"),Fpe=et(Ie("zlib"),1),fhe=et(Ie("zlib"),1),ym=Ie("node:path"),mhe=Ie("node:path"),Fv=et(Ie("fs"),1),Vu=et(Ie("fs"),1),h6=et(Ie("path"),1),whe=Ie("node:path"),w6=et(Ie("path"),1),j6=et(Ie("node:fs"),1),Phe=et(Ie("node:assert"),1),G6=Ie("node:crypto"),wn=et(Ie("node:fs"),1),ds=et(Ie("node:path"),1),q6=et(Ie("fs"),1),Lv=et(Ie("node:fs"),1),MI=et(Ie("node:path"),1),Il=et(Ie("node:fs"),1),The=et(Ie("node:fs/promises"),1),Nv=et(Ie("node:path"),1),W6=Ie("node:path"),El=et(Ie("node:fs"),1),V6=et(Ie("node:path"),1),ort=Object.defineProperty,art=(e,t)=>{for(var r in t)ort(e,r,{get:t[r],enumerable:!0})},kpe=typeof process=="object"&&process?process:{stdout:null,stderr:null},lrt=e=>!!e&&typeof e=="object"&&(e instanceof vm||e instanceof R6.default||crt(e)||urt(e)),crt=e=>!!e&&typeof e=="object"&&e instanceof pT.EventEmitter&&typeof e.pipe=="function"&&e.pipe!==R6.default.Writable.prototype.pipe,urt=e=>!!e&&typeof e=="object"&&e instanceof pT.EventEmitter&&typeof e.write=="function"&&typeof e.end=="function",jp=Symbol("EOF"),Gp=Symbol("maybeEmitEnd"),P0=Symbol("emittedEnd"),MR=Symbol("emittingEnd"),pv=Symbol("emittedError"),UR=Symbol("closed"),Qpe=Symbol("read"),_R=Symbol("flush"),Rpe=Symbol("flushChunk"),qu=Symbol("encoding"),xI=Symbol("decoder"),Ks=Symbol("flowing"),hv=Symbol("paused"),TI=Symbol("resume"),zs=Symbol("buffer"),Fa=Symbol("pipes"),Xs=Symbol("bufferLength"),Lj=Symbol("bufferPush"),HR=Symbol("bufferShift"),ra=Symbol("objectMode"),es=Symbol("destroyed"),Mj=Symbol("error"),Uj=Symbol("emitData"),Tpe=Symbol("emitEnd"),_j=Symbol("emitEnd2"),sA=Symbol("async"),Hj=Symbol("abort"),jR=Symbol("aborted"),dv=Symbol("signal"),cm=Symbol("dataListeners"),tc=Symbol("discarded"),gv=e=>Promise.resolve().then(e),frt=e=>e(),Art=e=>e==="end"||e==="finish"||e==="prefinish",prt=e=>e instanceof ArrayBuffer||!!e&&typeof e=="object"&&e.constructor&&e.constructor.name==="ArrayBuffer"&&e.byteLength>=0,hrt=e=>!Buffer.isBuffer(e)&&ArrayBuffer.isView(e),lhe=class{src;dest;opts;ondrain;constructor(e,t,r){this.src=e,this.dest=t,this.opts=r,this.ondrain=()=>e[TI](),this.dest.on("drain",this.ondrain)}unpipe(){this.dest.removeListener("drain",this.ondrain)}proxyErrors(e){}end(){this.unpipe(),this.opts.end&&this.dest.end()}},drt=class extends lhe{unpipe(){this.src.removeListener("error",this.proxyErrors),super.unpipe()}constructor(e,t,r){super(e,t,r),this.proxyErrors=s=>t.emit("error",s),e.on("error",this.proxyErrors)}},grt=e=>!!e.objectMode,mrt=e=>!e.objectMode&&!!e.encoding&&e.encoding!=="buffer",vm=class extends pT.EventEmitter{[Ks]=!1;[hv]=!1;[Fa]=[];[zs]=[];[ra];[qu];[sA];[xI];[jp]=!1;[P0]=!1;[MR]=!1;[UR]=!1;[pv]=null;[Xs]=0;[es]=!1;[dv];[jR]=!1;[cm]=0;[tc]=!1;writable=!0;readable=!0;constructor(...e){let t=e[0]||{};if(super(),t.objectMode&&typeof t.encoding=="string")throw new TypeError("Encoding and objectMode may not be used together");grt(t)?(this[ra]=!0,this[qu]=null):mrt(t)?(this[qu]=t.encoding,this[ra]=!1):(this[ra]=!1,this[qu]=null),this[sA]=!!t.async,this[xI]=this[qu]?new ahe.StringDecoder(this[qu]):null,t&&t.debugExposeBuffer===!0&&Object.defineProperty(this,"buffer",{get:()=>this[zs]}),t&&t.debugExposePipes===!0&&Object.defineProperty(this,"pipes",{get:()=>this[Fa]});let{signal:r}=t;r&&(this[dv]=r,r.aborted?this[Hj]():r.addEventListener("abort",()=>this[Hj]()))}get bufferLength(){return this[Xs]}get encoding(){return this[qu]}set encoding(e){throw new Error("Encoding must be set at instantiation time")}setEncoding(e){throw new Error("Encoding must be set at instantiation time")}get objectMode(){return this[ra]}set objectMode(e){throw new Error("objectMode must be set at instantiation time")}get async(){return this[sA]}set async(e){this[sA]=this[sA]||!!e}[Hj](){this[jR]=!0,this.emit("abort",this[dv]?.reason),this.destroy(this[dv]?.reason)}get aborted(){return this[jR]}set aborted(e){}write(e,t,r){if(this[jR])return!1;if(this[jp])throw new Error("write after end");if(this[es])return this.emit("error",Object.assign(new Error("Cannot call write after a stream was destroyed"),{code:"ERR_STREAM_DESTROYED"})),!0;typeof t=="function"&&(r=t,t="utf8"),t||(t="utf8");let s=this[sA]?gv:frt;if(!this[ra]&&!Buffer.isBuffer(e)){if(hrt(e))e=Buffer.from(e.buffer,e.byteOffset,e.byteLength);else if(prt(e))e=Buffer.from(e);else if(typeof e!="string")throw new Error("Non-contiguous data written to non-objectMode stream")}return this[ra]?(this[Ks]&&this[Xs]!==0&&this[_R](!0),this[Ks]?this.emit("data",e):this[Lj](e),this[Xs]!==0&&this.emit("readable"),r&&s(r),this[Ks]):e.length?(typeof e=="string"&&!(t===this[qu]&&!this[xI]?.lastNeed)&&(e=Buffer.from(e,t)),Buffer.isBuffer(e)&&this[qu]&&(e=this[xI].write(e)),this[Ks]&&this[Xs]!==0&&this[_R](!0),this[Ks]?this.emit("data",e):this[Lj](e),this[Xs]!==0&&this.emit("readable"),r&&s(r),this[Ks]):(this[Xs]!==0&&this.emit("readable"),r&&s(r),this[Ks])}read(e){if(this[es])return null;if(this[tc]=!1,this[Xs]===0||e===0||e&&e>this[Xs])return this[Gp](),null;this[ra]&&(e=null),this[zs].length>1&&!this[ra]&&(this[zs]=[this[qu]?this[zs].join(""):Buffer.concat(this[zs],this[Xs])]);let t=this[Qpe](e||null,this[zs][0]);return this[Gp](),t}[Qpe](e,t){if(this[ra])this[HR]();else{let r=t;e===r.length||e===null?this[HR]():typeof r=="string"?(this[zs][0]=r.slice(e),t=r.slice(0,e),this[Xs]-=e):(this[zs][0]=r.subarray(e),t=r.subarray(0,e),this[Xs]-=e)}return this.emit("data",t),!this[zs].length&&!this[jp]&&this.emit("drain"),t}end(e,t,r){return typeof e=="function"&&(r=e,e=void 0),typeof t=="function"&&(r=t,t="utf8"),e!==void 0&&this.write(e,t),r&&this.once("end",r),this[jp]=!0,this.writable=!1,(this[Ks]||!this[hv])&&this[Gp](),this}[TI](){this[es]||(!this[cm]&&!this[Fa].length&&(this[tc]=!0),this[hv]=!1,this[Ks]=!0,this.emit("resume"),this[zs].length?this[_R]():this[jp]?this[Gp]():this.emit("drain"))}resume(){return this[TI]()}pause(){this[Ks]=!1,this[hv]=!0,this[tc]=!1}get destroyed(){return this[es]}get flowing(){return this[Ks]}get paused(){return this[hv]}[Lj](e){this[ra]?this[Xs]+=1:this[Xs]+=e.length,this[zs].push(e)}[HR](){return this[ra]?this[Xs]-=1:this[Xs]-=this[zs][0].length,this[zs].shift()}[_R](e=!1){do;while(this[Rpe](this[HR]())&&this[zs].length);!e&&!this[zs].length&&!this[jp]&&this.emit("drain")}[Rpe](e){return this.emit("data",e),this[Ks]}pipe(e,t){if(this[es])return e;this[tc]=!1;let r=this[P0];return t=t||{},e===kpe.stdout||e===kpe.stderr?t.end=!1:t.end=t.end!==!1,t.proxyErrors=!!t.proxyErrors,r?t.end&&e.end():(this[Fa].push(t.proxyErrors?new drt(this,e,t):new lhe(this,e,t)),this[sA]?gv(()=>this[TI]()):this[TI]()),e}unpipe(e){let t=this[Fa].find(r=>r.dest===e);t&&(this[Fa].length===1?(this[Ks]&&this[cm]===0&&(this[Ks]=!1),this[Fa]=[]):this[Fa].splice(this[Fa].indexOf(t),1),t.unpipe())}addListener(e,t){return this.on(e,t)}on(e,t){let r=super.on(e,t);if(e==="data")this[tc]=!1,this[cm]++,!this[Fa].length&&!this[Ks]&&this[TI]();else if(e==="readable"&&this[Xs]!==0)super.emit("readable");else if(Art(e)&&this[P0])super.emit(e),this.removeAllListeners(e);else if(e==="error"&&this[pv]){let s=t;this[sA]?gv(()=>s.call(this,this[pv])):s.call(this,this[pv])}return r}removeListener(e,t){return this.off(e,t)}off(e,t){let r=super.off(e,t);return e==="data"&&(this[cm]=this.listeners("data").length,this[cm]===0&&!this[tc]&&!this[Fa].length&&(this[Ks]=!1)),r}removeAllListeners(e){let t=super.removeAllListeners(e);return(e==="data"||e===void 0)&&(this[cm]=0,!this[tc]&&!this[Fa].length&&(this[Ks]=!1)),t}get emittedEnd(){return this[P0]}[Gp](){!this[MR]&&!this[P0]&&!this[es]&&this[zs].length===0&&this[jp]&&(this[MR]=!0,this.emit("end"),this.emit("prefinish"),this.emit("finish"),this[UR]&&this.emit("close"),this[MR]=!1)}emit(e,...t){let r=t[0];if(e!=="error"&&e!=="close"&&e!==es&&this[es])return!1;if(e==="data")return!this[ra]&&!r?!1:this[sA]?(gv(()=>this[Uj](r)),!0):this[Uj](r);if(e==="end")return this[Tpe]();if(e==="close"){if(this[UR]=!0,!this[P0]&&!this[es])return!1;let a=super.emit("close");return this.removeAllListeners("close"),a}else if(e==="error"){this[pv]=r,super.emit(Mj,r);let a=!this[dv]||this.listeners("error").length?super.emit("error",r):!1;return this[Gp](),a}else if(e==="resume"){let a=super.emit("resume");return this[Gp](),a}else if(e==="finish"||e==="prefinish"){let a=super.emit(e);return this.removeAllListeners(e),a}let s=super.emit(e,...t);return this[Gp](),s}[Uj](e){for(let r of this[Fa])r.dest.write(e)===!1&&this.pause();let t=this[tc]?!1:super.emit("data",e);return this[Gp](),t}[Tpe](){return this[P0]?!1:(this[P0]=!0,this.readable=!1,this[sA]?(gv(()=>this[_j]()),!0):this[_j]())}[_j](){if(this[xI]){let t=this[xI].end();if(t){for(let r of this[Fa])r.dest.write(t);this[tc]||super.emit("data",t)}}for(let t of this[Fa])t.end();let e=super.emit("end");return this.removeAllListeners("end"),e}async collect(){let e=Object.assign([],{dataLength:0});this[ra]||(e.dataLength=0);let t=this.promise();return this.on("data",r=>{e.push(r),this[ra]||(e.dataLength+=r.length)}),await t,e}async concat(){if(this[ra])throw new Error("cannot concat in objectMode");let e=await this.collect();return this[qu]?e.join(""):Buffer.concat(e,e.dataLength)}async promise(){return new Promise((e,t)=>{this.on(es,()=>t(new Error("stream destroyed"))),this.on("error",r=>t(r)),this.on("end",()=>e())})}[Symbol.asyncIterator](){this[tc]=!1;let e=!1,t=async()=>(this.pause(),e=!0,{value:void 0,done:!0});return{next:()=>{if(e)return t();let r=this.read();if(r!==null)return Promise.resolve({done:!1,value:r});if(this[jp])return t();let s,a,n=h=>{this.off("data",c),this.off("end",f),this.off(es,p),t(),a(h)},c=h=>{this.off("error",n),this.off("end",f),this.off(es,p),this.pause(),s({value:h,done:!!this[jp]})},f=()=>{this.off("error",n),this.off("data",c),this.off(es,p),t(),s({done:!0,value:void 0})},p=()=>n(new Error("stream destroyed"));return new Promise((h,E)=>{a=E,s=h,this.once(es,p),this.once("error",n),this.once("end",f),this.once("data",c)})},throw:t,return:t,[Symbol.asyncIterator](){return this}}}[Symbol.iterator](){this[tc]=!1;let e=!1,t=()=>(this.pause(),this.off(Mj,t),this.off(es,t),this.off("end",t),e=!0,{done:!0,value:void 0}),r=()=>{if(e)return t();let s=this.read();return s===null?t():{done:!1,value:s}};return this.once("end",t),this.once(Mj,t),this.once(es,t),{next:r,throw:t,return:t,[Symbol.iterator](){return this}}}destroy(e){if(this[es])return e?this.emit("error",e):this.emit(es),this;this[es]=!0,this[tc]=!0,this[zs].length=0,this[Xs]=0;let t=this;return typeof t.close=="function"&&!this[UR]&&t.close(),e?this.emit("error",e):this.emit(es),this}static get isStream(){return lrt}},yrt=Na.default.writev,M0=Symbol("_autoClose"),Ku=Symbol("_close"),mv=Symbol("_ended"),Jn=Symbol("_fd"),jj=Symbol("_finished"),Vp=Symbol("_flags"),Gj=Symbol("_flush"),u6=Symbol("_handleChunk"),f6=Symbol("_makeBuf"),bv=Symbol("_mode"),GR=Symbol("_needDrain"),LI=Symbol("_onerror"),UI=Symbol("_onopen"),qj=Symbol("_onread"),FI=Symbol("_onwrite"),U0=Symbol("_open"),Ju=Symbol("_path"),R0=Symbol("_pos"),oA=Symbol("_queue"),NI=Symbol("_read"),Wj=Symbol("_readSize"),Yp=Symbol("_reading"),yv=Symbol("_remain"),Yj=Symbol("_size"),ZR=Symbol("_write"),um=Symbol("_writing"),$R=Symbol("_defaultFlag"),Im=Symbol("_errored"),T6=class extends vm{[Im]=!1;[Jn];[Ju];[Wj];[Yp]=!1;[Yj];[yv];[M0];constructor(e,t){if(t=t||{},super(t),this.readable=!0,this.writable=!1,typeof e!="string")throw new TypeError("path must be a string");this[Im]=!1,this[Jn]=typeof t.fd=="number"?t.fd:void 0,this[Ju]=e,this[Wj]=t.readSize||16*1024*1024,this[Yp]=!1,this[Yj]=typeof t.size=="number"?t.size:1/0,this[yv]=this[Yj],this[M0]=typeof t.autoClose=="boolean"?t.autoClose:!0,typeof this[Jn]=="number"?this[NI]():this[U0]()}get fd(){return this[Jn]}get path(){return this[Ju]}write(){throw new TypeError("this is a readable stream")}end(){throw new TypeError("this is a readable stream")}[U0](){Na.default.open(this[Ju],"r",(e,t)=>this[UI](e,t))}[UI](e,t){e?this[LI](e):(this[Jn]=t,this.emit("open",t),this[NI]())}[f6](){return Buffer.allocUnsafe(Math.min(this[Wj],this[yv]))}[NI](){if(!this[Yp]){this[Yp]=!0;let e=this[f6]();if(e.length===0)return process.nextTick(()=>this[qj](null,0,e));Na.default.read(this[Jn],e,0,e.length,null,(t,r,s)=>this[qj](t,r,s))}}[qj](e,t,r){this[Yp]=!1,e?this[LI](e):this[u6](t,r)&&this[NI]()}[Ku](){if(this[M0]&&typeof this[Jn]=="number"){let e=this[Jn];this[Jn]=void 0,Na.default.close(e,t=>t?this.emit("error",t):this.emit("close"))}}[LI](e){this[Yp]=!0,this[Ku](),this.emit("error",e)}[u6](e,t){let r=!1;return this[yv]-=e,e>0&&(r=super.write(ethis[UI](e,t))}[UI](e,t){this[$R]&&this[Vp]==="r+"&&e&&e.code==="ENOENT"?(this[Vp]="w",this[U0]()):e?this[LI](e):(this[Jn]=t,this.emit("open",t),this[um]||this[Gj]())}end(e,t){return e&&this.write(e,t),this[mv]=!0,!this[um]&&!this[oA].length&&typeof this[Jn]=="number"&&this[FI](null,0),this}write(e,t){return typeof e=="string"&&(e=Buffer.from(e,t)),this[mv]?(this.emit("error",new Error("write() after end()")),!1):this[Jn]===void 0||this[um]||this[oA].length?(this[oA].push(e),this[GR]=!0,!1):(this[um]=!0,this[ZR](e),!0)}[ZR](e){Na.default.write(this[Jn],e,0,e.length,this[R0],(t,r)=>this[FI](t,r))}[FI](e,t){e?this[LI](e):(this[R0]!==void 0&&typeof t=="number"&&(this[R0]+=t),this[oA].length?this[Gj]():(this[um]=!1,this[mv]&&!this[jj]?(this[jj]=!0,this[Ku](),this.emit("finish")):this[GR]&&(this[GR]=!1,this.emit("drain"))))}[Gj](){if(this[oA].length===0)this[mv]&&this[FI](null,0);else if(this[oA].length===1)this[ZR](this[oA].pop());else{let e=this[oA];this[oA]=[],yrt(this[Jn],e,this[R0],(t,r)=>this[FI](t,r))}}[Ku](){if(this[M0]&&typeof this[Jn]=="number"){let e=this[Jn];this[Jn]=void 0,Na.default.close(e,t=>t?this.emit("error",t):this.emit("close"))}}},che=class extends hT{[U0](){let e;if(this[$R]&&this[Vp]==="r+")try{e=Na.default.openSync(this[Ju],this[Vp],this[bv])}catch(t){if(t?.code==="ENOENT")return this[Vp]="w",this[U0]();throw t}else e=Na.default.openSync(this[Ju],this[Vp],this[bv]);this[UI](null,e)}[Ku](){if(this[M0]&&typeof this[Jn]=="number"){let e=this[Jn];this[Jn]=void 0,Na.default.closeSync(e),this.emit("close")}}[ZR](e){let t=!0;try{this[FI](null,Na.default.writeSync(this[Jn],e,0,e.length,this[R0])),t=!1}finally{if(t)try{this[Ku]()}catch{}}}},Irt=new Map([["C","cwd"],["f","file"],["z","gzip"],["P","preservePaths"],["U","unlink"],["strip-components","strip"],["stripComponents","strip"],["keep-newer","newer"],["keepNewer","newer"],["keep-newer-files","newer"],["keepNewerFiles","newer"],["k","keep"],["keep-existing","keep"],["keepExisting","keep"],["m","noMtime"],["no-mtime","noMtime"],["p","preserveOwner"],["L","follow"],["h","follow"],["onentry","onReadEntry"]]),Crt=e=>!!e.sync&&!!e.file,wrt=e=>!e.sync&&!!e.file,Brt=e=>!!e.sync&&!e.file,vrt=e=>!e.sync&&!e.file,Srt=e=>!!e.file,Drt=e=>Irt.get(e)||e,N6=(e={})=>{if(!e)return{};let t={};for(let[r,s]of Object.entries(e)){let a=Drt(r);t[a]=s}return t.chmod===void 0&&t.noChmod===!1&&(t.chmod=!0),delete t.noChmod,t},Ov=(e,t,r,s,a)=>Object.assign((n=[],c,f)=>{Array.isArray(n)&&(c=n,n={}),typeof c=="function"&&(f=c,c=void 0),c?c=Array.from(c):c=[];let p=N6(n);if(a?.(p,c),Crt(p)){if(typeof f=="function")throw new TypeError("callback not supported for sync tar functions");return e(p,c)}else if(wrt(p)){let h=t(p,c),E=f||void 0;return E?h.then(()=>E(),E):h}else if(Brt(p)){if(typeof f=="function")throw new TypeError("callback not supported for sync tar functions");return r(p,c)}else if(vrt(p)){if(typeof f=="function")throw new TypeError("callback only supported with file option");return s(p,c)}else throw new Error("impossible options??")},{syncFile:e,asyncFile:t,syncNoFile:r,asyncNoFile:s,validate:a}),brt=fhe.default.constants||{ZLIB_VERNUM:4736},cA=Object.freeze(Object.assign(Object.create(null),{Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_VERSION_ERROR:-6,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,DEFLATE:1,INFLATE:2,GZIP:3,GUNZIP:4,DEFLATERAW:5,INFLATERAW:6,UNZIP:7,BROTLI_DECODE:8,BROTLI_ENCODE:9,Z_MIN_WINDOWBITS:8,Z_MAX_WINDOWBITS:15,Z_DEFAULT_WINDOWBITS:15,Z_MIN_CHUNK:64,Z_MAX_CHUNK:1/0,Z_DEFAULT_CHUNK:16384,Z_MIN_MEMLEVEL:1,Z_MAX_MEMLEVEL:9,Z_DEFAULT_MEMLEVEL:8,Z_MIN_LEVEL:-1,Z_MAX_LEVEL:9,Z_DEFAULT_LEVEL:-1,BROTLI_OPERATION_PROCESS:0,BROTLI_OPERATION_FLUSH:1,BROTLI_OPERATION_FINISH:2,BROTLI_OPERATION_EMIT_METADATA:3,BROTLI_MODE_GENERIC:0,BROTLI_MODE_TEXT:1,BROTLI_MODE_FONT:2,BROTLI_DEFAULT_MODE:0,BROTLI_MIN_QUALITY:0,BROTLI_MAX_QUALITY:11,BROTLI_DEFAULT_QUALITY:11,BROTLI_MIN_WINDOW_BITS:10,BROTLI_MAX_WINDOW_BITS:24,BROTLI_LARGE_MAX_WINDOW_BITS:30,BROTLI_DEFAULT_WINDOW:22,BROTLI_MIN_INPUT_BLOCK_BITS:16,BROTLI_MAX_INPUT_BLOCK_BITS:24,BROTLI_PARAM_MODE:0,BROTLI_PARAM_QUALITY:1,BROTLI_PARAM_LGWIN:2,BROTLI_PARAM_LGBLOCK:3,BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING:4,BROTLI_PARAM_SIZE_HINT:5,BROTLI_PARAM_LARGE_WINDOW:6,BROTLI_PARAM_NPOSTFIX:7,BROTLI_PARAM_NDIRECT:8,BROTLI_DECODER_RESULT_ERROR:0,BROTLI_DECODER_RESULT_SUCCESS:1,BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT:2,BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION:0,BROTLI_DECODER_PARAM_LARGE_WINDOW:1,BROTLI_DECODER_NO_ERROR:0,BROTLI_DECODER_SUCCESS:1,BROTLI_DECODER_NEEDS_MORE_INPUT:2,BROTLI_DECODER_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE:-1,BROTLI_DECODER_ERROR_FORMAT_RESERVED:-2,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE:-3,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET:-4,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME:-5,BROTLI_DECODER_ERROR_FORMAT_CL_SPACE:-6,BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE:-7,BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT:-8,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1:-9,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2:-10,BROTLI_DECODER_ERROR_FORMAT_TRANSFORM:-11,BROTLI_DECODER_ERROR_FORMAT_DICTIONARY:-12,BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS:-13,BROTLI_DECODER_ERROR_FORMAT_PADDING_1:-14,BROTLI_DECODER_ERROR_FORMAT_PADDING_2:-15,BROTLI_DECODER_ERROR_FORMAT_DISTANCE:-16,BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET:-19,BROTLI_DECODER_ERROR_INVALID_ARGUMENTS:-20,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES:-21,BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS:-22,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP:-25,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1:-26,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2:-27,BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES:-30,BROTLI_DECODER_ERROR_UNREACHABLE:-31},brt)),Prt=O0.Buffer.concat,Npe=Object.getOwnPropertyDescriptor(O0.Buffer,"concat"),xrt=e=>e,Vj=Npe?.writable===!0||Npe?.set!==void 0?e=>{O0.Buffer.concat=e?xrt:Prt}:e=>{},Cm=Symbol("_superWrite"),qR=class extends Error{code;errno;constructor(e,t){super("zlib: "+e.message,{cause:e}),this.code=e.code,this.errno=e.errno,this.code||(this.code="ZLIB_ERROR"),this.message="zlib: "+e.message,Error.captureStackTrace(this,t??this.constructor)}get name(){return"ZlibError"}},Jj=Symbol("flushFlag"),O6=class extends vm{#e=!1;#t=!1;#s;#r;#i;#n;#o;get sawError(){return this.#e}get handle(){return this.#n}get flushFlag(){return this.#s}constructor(e,t){if(!e||typeof e!="object")throw new TypeError("invalid options for ZlibBase constructor");if(super(e),this.#s=e.flush??0,this.#r=e.finishFlush??0,this.#i=e.fullFlushFlag??0,typeof Fpe[t]!="function")throw new TypeError("Compression method not supported: "+t);try{this.#n=new Fpe[t](e)}catch(r){throw new qR(r,this.constructor)}this.#o=r=>{this.#e||(this.#e=!0,this.close(),this.emit("error",r))},this.#n?.on("error",r=>this.#o(new qR(r))),this.once("end",()=>this.close)}close(){this.#n&&(this.#n.close(),this.#n=void 0,this.emit("close"))}reset(){if(!this.#e)return(0,cT.default)(this.#n,"zlib binding closed"),this.#n.reset?.()}flush(e){this.ended||(typeof e!="number"&&(e=this.#i),this.write(Object.assign(O0.Buffer.alloc(0),{[Jj]:e})))}end(e,t,r){return typeof e=="function"&&(r=e,t=void 0,e=void 0),typeof t=="function"&&(r=t,t=void 0),e&&(t?this.write(e,t):this.write(e)),this.flush(this.#r),this.#t=!0,super.end(r)}get ended(){return this.#t}[Cm](e){return super.write(e)}write(e,t,r){if(typeof t=="function"&&(r=t,t="utf8"),typeof e=="string"&&(e=O0.Buffer.from(e,t)),this.#e)return;(0,cT.default)(this.#n,"zlib binding closed");let s=this.#n._handle,a=s.close;s.close=()=>{};let n=this.#n.close;this.#n.close=()=>{},Vj(!0);let c;try{let p=typeof e[Jj]=="number"?e[Jj]:this.#s;c=this.#n._processChunk(e,p),Vj(!1)}catch(p){Vj(!1),this.#o(new qR(p,this.write))}finally{this.#n&&(this.#n._handle=s,s.close=a,this.#n.close=n,this.#n.removeAllListeners("error"))}this.#n&&this.#n.on("error",p=>this.#o(new qR(p,this.write)));let f;if(c)if(Array.isArray(c)&&c.length>0){let p=c[0];f=this[Cm](O0.Buffer.from(p));for(let h=1;h{typeof s=="function"&&(a=s,s=this.flushFlag),this.flush(s),a?.()};try{this.handle.params(e,t)}finally{this.handle.flush=r}this.handle&&(this.#e=e,this.#t=t)}}}},krt=class extends Ahe{#e;constructor(e){super(e,"Gzip"),this.#e=e&&!!e.portable}[Cm](e){return this.#e?(this.#e=!1,e[9]=255,super[Cm](e)):super[Cm](e)}},Qrt=class extends Ahe{constructor(e){super(e,"Unzip")}},phe=class extends O6{constructor(e,t){e=e||{},e.flush=e.flush||cA.BROTLI_OPERATION_PROCESS,e.finishFlush=e.finishFlush||cA.BROTLI_OPERATION_FINISH,e.fullFlushFlag=cA.BROTLI_OPERATION_FLUSH,super(e,t)}},Rrt=class extends phe{constructor(e){super(e,"BrotliCompress")}},Trt=class extends phe{constructor(e){super(e,"BrotliDecompress")}},hhe=class extends O6{constructor(e,t){e=e||{},e.flush=e.flush||cA.ZSTD_e_continue,e.finishFlush=e.finishFlush||cA.ZSTD_e_end,e.fullFlushFlag=cA.ZSTD_e_flush,super(e,t)}},Frt=class extends hhe{constructor(e){super(e,"ZstdCompress")}},Nrt=class extends hhe{constructor(e){super(e,"ZstdDecompress")}},Ort=(e,t)=>{if(Number.isSafeInteger(e))e<0?Mrt(e,t):Lrt(e,t);else throw Error("cannot encode number outside of javascript safe integer range");return t},Lrt=(e,t)=>{t[0]=128;for(var r=t.length;r>1;r--)t[r-1]=e&255,e=Math.floor(e/256)},Mrt=(e,t)=>{t[0]=255;var r=!1;e=e*-1;for(var s=t.length;s>1;s--){var a=e&255;e=Math.floor(e/256),r?t[s-1]=dhe(a):a===0?t[s-1]=0:(r=!0,t[s-1]=ghe(a))}},Urt=e=>{let t=e[0],r=t===128?Hrt(e.subarray(1,e.length)):t===255?_rt(e):null;if(r===null)throw Error("invalid base256 encoding");if(!Number.isSafeInteger(r))throw Error("parsed number outside of javascript safe integer range");return r},_rt=e=>{for(var t=e.length,r=0,s=!1,a=t-1;a>-1;a--){var n=Number(e[a]),c;s?c=dhe(n):n===0?c=n:(s=!0,c=ghe(n)),c!==0&&(r-=c*Math.pow(256,t-a-1))}return r},Hrt=e=>{for(var t=e.length,r=0,s=t-1;s>-1;s--){var a=Number(e[s]);a!==0&&(r+=a*Math.pow(256,t-s-1))}return r},dhe=e=>(255^e)&255,ghe=e=>(255^e)+1&255,jrt={};art(jrt,{code:()=>L6,isCode:()=>eT,isName:()=>Grt,name:()=>gT});eT=e=>gT.has(e),Grt=e=>L6.has(e),gT=new Map([["0","File"],["","OldFile"],["1","Link"],["2","SymbolicLink"],["3","CharacterDevice"],["4","BlockDevice"],["5","Directory"],["6","FIFO"],["7","ContiguousFile"],["g","GlobalExtendedHeader"],["x","ExtendedHeader"],["A","SolarisACL"],["D","GNUDumpDir"],["I","Inode"],["K","NextFileHasLongLinkpath"],["L","NextFileHasLongPath"],["M","ContinuationFile"],["N","OldGnuLongPath"],["S","SparseFile"],["V","TapeVolumeHeader"],["X","OldExtendedHeader"]]),L6=new Map(Array.from(gT).map(e=>[e[1],e[0]])),wm=class{cksumValid=!1;needPax=!1;nullBlock=!1;block;path;mode;uid;gid;size;cksum;#e="Unsupported";linkpath;uname;gname;devmaj=0;devmin=0;atime;ctime;mtime;charset;comment;constructor(e,t=0,r,s){Buffer.isBuffer(e)?this.decode(e,t||0,r,s):e&&this.#t(e)}decode(e,t,r,s){if(t||(t=0),!e||!(e.length>=t+512))throw new Error("need 512 bytes for header");this.path=r?.path??fm(e,t,100),this.mode=r?.mode??s?.mode??T0(e,t+100,8),this.uid=r?.uid??s?.uid??T0(e,t+108,8),this.gid=r?.gid??s?.gid??T0(e,t+116,8),this.size=r?.size??s?.size??T0(e,t+124,12),this.mtime=r?.mtime??s?.mtime??Kj(e,t+136,12),this.cksum=T0(e,t+148,12),s&&this.#t(s,!0),r&&this.#t(r);let a=fm(e,t+156,1);if(eT(a)&&(this.#e=a||"0"),this.#e==="0"&&this.path.slice(-1)==="/"&&(this.#e="5"),this.#e==="5"&&(this.size=0),this.linkpath=fm(e,t+157,100),e.subarray(t+257,t+265).toString()==="ustar\x0000")if(this.uname=r?.uname??s?.uname??fm(e,t+265,32),this.gname=r?.gname??s?.gname??fm(e,t+297,32),this.devmaj=r?.devmaj??s?.devmaj??T0(e,t+329,8)??0,this.devmin=r?.devmin??s?.devmin??T0(e,t+337,8)??0,e[t+475]!==0){let c=fm(e,t+345,155);this.path=c+"/"+this.path}else{let c=fm(e,t+345,130);c&&(this.path=c+"/"+this.path),this.atime=r?.atime??s?.atime??Kj(e,t+476,12),this.ctime=r?.ctime??s?.ctime??Kj(e,t+488,12)}let n=256;for(let c=t;c!(s==null||r==="path"&&t||r==="linkpath"&&t||r==="global"))))}encode(e,t=0){if(e||(e=this.block=Buffer.alloc(512)),this.#e==="Unsupported"&&(this.#e="0"),!(e.length>=t+512))throw new Error("need 512 bytes for header");let r=this.ctime||this.atime?130:155,s=qrt(this.path||"",r),a=s[0],n=s[1];this.needPax=!!s[2],this.needPax=Am(e,t,100,a)||this.needPax,this.needPax=F0(e,t+100,8,this.mode)||this.needPax,this.needPax=F0(e,t+108,8,this.uid)||this.needPax,this.needPax=F0(e,t+116,8,this.gid)||this.needPax,this.needPax=F0(e,t+124,12,this.size)||this.needPax,this.needPax=zj(e,t+136,12,this.mtime)||this.needPax,e[t+156]=this.#e.charCodeAt(0),this.needPax=Am(e,t+157,100,this.linkpath)||this.needPax,e.write("ustar\x0000",t+257,8),this.needPax=Am(e,t+265,32,this.uname)||this.needPax,this.needPax=Am(e,t+297,32,this.gname)||this.needPax,this.needPax=F0(e,t+329,8,this.devmaj)||this.needPax,this.needPax=F0(e,t+337,8,this.devmin)||this.needPax,this.needPax=Am(e,t+345,r,n)||this.needPax,e[t+475]!==0?this.needPax=Am(e,t+345,155,n)||this.needPax:(this.needPax=Am(e,t+345,130,n)||this.needPax,this.needPax=zj(e,t+476,12,this.atime)||this.needPax,this.needPax=zj(e,t+488,12,this.ctime)||this.needPax);let c=256;for(let f=t;f{let r=e,s="",a,n=ym.posix.parse(e).root||".";if(Buffer.byteLength(r)<100)a=[r,s,!1];else{s=ym.posix.dirname(r),r=ym.posix.basename(r);do Buffer.byteLength(r)<=100&&Buffer.byteLength(s)<=t?a=[r,s,!1]:Buffer.byteLength(r)>100&&Buffer.byteLength(s)<=t?a=[r.slice(0,99),s,!0]:(r=ym.posix.join(ym.posix.basename(s),r),s=ym.posix.dirname(s));while(s!==n&&a===void 0);a||(a=[e.slice(0,99),"",!0])}return a},fm=(e,t,r)=>e.subarray(t,t+r).toString("utf8").replace(/\0.*/,""),Kj=(e,t,r)=>Wrt(T0(e,t,r)),Wrt=e=>e===void 0?void 0:new Date(e*1e3),T0=(e,t,r)=>Number(e[t])&128?Urt(e.subarray(t,t+r)):Vrt(e,t,r),Yrt=e=>isNaN(e)?void 0:e,Vrt=(e,t,r)=>Yrt(parseInt(e.subarray(t,t+r).toString("utf8").replace(/\0.*$/,"").trim(),8)),Jrt={12:8589934591,8:2097151},F0=(e,t,r,s)=>s===void 0?!1:s>Jrt[r]||s<0?(Ort(s,e.subarray(t,t+r)),!0):(Krt(e,t,r,s),!1),Krt=(e,t,r,s)=>e.write(zrt(s,r),t,r,"ascii"),zrt=(e,t)=>Xrt(Math.floor(e).toString(8),t),Xrt=(e,t)=>(e.length===t-1?e:new Array(t-e.length-1).join("0")+e+" ")+"\0",zj=(e,t,r,s)=>s===void 0?!1:F0(e,t,r,s.getTime()/1e3),Zrt=new Array(156).join("\0"),Am=(e,t,r,s)=>s===void 0?!1:(e.write(s+Zrt,t,r,"utf8"),s.length!==Buffer.byteLength(s)||s.length>r),uT=class yhe{atime;mtime;ctime;charset;comment;gid;uid;gname;uname;linkpath;dev;ino;nlink;path;size;mode;global;constructor(t,r=!1){this.atime=t.atime,this.charset=t.charset,this.comment=t.comment,this.ctime=t.ctime,this.dev=t.dev,this.gid=t.gid,this.global=r,this.gname=t.gname,this.ino=t.ino,this.linkpath=t.linkpath,this.mtime=t.mtime,this.nlink=t.nlink,this.path=t.path,this.size=t.size,this.uid=t.uid,this.uname=t.uname}encode(){let t=this.encodeBody();if(t==="")return Buffer.allocUnsafe(0);let r=Buffer.byteLength(t),s=512*Math.ceil(1+r/512),a=Buffer.allocUnsafe(s);for(let n=0;n<512;n++)a[n]=0;new wm({path:("PaxHeader/"+(0,mhe.basename)(this.path??"")).slice(0,99),mode:this.mode||420,uid:this.uid,gid:this.gid,size:r,mtime:this.mtime,type:this.global?"GlobalExtendedHeader":"ExtendedHeader",linkpath:"",uname:this.uname||"",gname:this.gname||"",devmaj:0,devmin:0,atime:this.atime,ctime:this.ctime}).encode(a),a.write(t,512,r,"utf8");for(let n=r+512;n=Math.pow(10,c)&&(c+=1),c+n+a}static parse(t,r,s=!1){return new yhe($rt(ent(t),r),s)}},$rt=(e,t)=>t?Object.assign({},t,e):e,ent=e=>e.replace(/\n$/,"").split(` `).reduce(tnt,Object.create(null)),tnt=(e,t)=>{let r=parseInt(t,10);if(r!==Buffer.byteLength(t)+1)return e;t=t.slice((r+" ").length);let s=t.split("="),a=s.shift();if(!a)return e;let n=a.replace(/^SCHILY\.(dev|ino|nlink)/,"$1"),c=s.join("=");return e[n]=/^([A-Z]+\.)?([mac]|birth|creation)time$/.test(n)?new Date(Number(c)*1e3):/^[0-9]+$/.test(c)?+c:c,e},rnt=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,cn=rnt!=="win32"?e=>e:e=>e&&e.replace(/\\/g,"/"),Ehe=class extends vm{extended;globalExtended;header;startBlockSize;blockRemain;remain;type;meta=!1;ignore=!1;path;mode;uid;gid;uname;gname;size=0;mtime;atime;ctime;linkpath;dev;ino;nlink;invalid=!1;absolute;unsupported=!1;constructor(e,t,r){switch(super({}),this.pause(),this.extended=t,this.globalExtended=r,this.header=e,this.remain=e.size??0,this.startBlockSize=512*Math.ceil(this.remain/512),this.blockRemain=this.startBlockSize,this.type=e.type,this.type){case"File":case"OldFile":case"Link":case"SymbolicLink":case"CharacterDevice":case"BlockDevice":case"Directory":case"FIFO":case"ContiguousFile":case"GNUDumpDir":break;case"NextFileHasLongLinkpath":case"NextFileHasLongPath":case"OldGnuLongPath":case"GlobalExtendedHeader":case"ExtendedHeader":case"OldExtendedHeader":this.meta=!0;break;default:this.ignore=!0}if(!e.path)throw new Error("no path provided for tar.ReadEntry");this.path=cn(e.path),this.mode=e.mode,this.mode&&(this.mode=this.mode&4095),this.uid=e.uid,this.gid=e.gid,this.uname=e.uname,this.gname=e.gname,this.size=this.remain,this.mtime=e.mtime,this.atime=e.atime,this.ctime=e.ctime,this.linkpath=e.linkpath?cn(e.linkpath):void 0,this.uname=e.uname,this.gname=e.gname,t&&this.#e(t),r&&this.#e(r,!0)}write(e){let t=e.length;if(t>this.blockRemain)throw new Error("writing more to entry than is appropriate");let r=this.remain,s=this.blockRemain;return this.remain=Math.max(0,r-t),this.blockRemain=Math.max(0,s-t),this.ignore?!0:r>=t?super.write(e):super.write(e.subarray(0,r))}#e(e,t=!1){e.path&&(e.path=cn(e.path)),e.linkpath&&(e.linkpath=cn(e.linkpath)),Object.assign(this,Object.fromEntries(Object.entries(e).filter(([r,s])=>!(s==null||r==="path"&&t))))}},mT=(e,t,r,s={})=>{e.file&&(s.file=e.file),e.cwd&&(s.cwd=e.cwd),s.code=r instanceof Error&&r.code||t,s.tarCode=t,!e.strict&&s.recoverable!==!1?(r instanceof Error&&(s=Object.assign(r,s),r=r.message),e.emit("warn",t,r,s)):r instanceof Error?e.emit("error",Object.assign(r,s)):e.emit("error",Object.assign(new Error(`${t}: ${r}`),s))},nnt=1024*1024,A6=Buffer.from([31,139]),p6=Buffer.from([40,181,47,253]),int=Math.max(A6.length,p6.length),Uc=Symbol("state"),pm=Symbol("writeEntry"),qp=Symbol("readEntry"),Xj=Symbol("nextEntry"),Ope=Symbol("processEntry"),aA=Symbol("extendedHeader"),Ev=Symbol("globalExtendedHeader"),x0=Symbol("meta"),Lpe=Symbol("emitMeta"),Ci=Symbol("buffer"),Wp=Symbol("queue"),k0=Symbol("ended"),Zj=Symbol("emittedEnd"),hm=Symbol("emit"),hs=Symbol("unzip"),WR=Symbol("consumeChunk"),YR=Symbol("consumeChunkSub"),$j=Symbol("consumeBody"),Mpe=Symbol("consumeMeta"),Upe=Symbol("consumeHeader"),Iv=Symbol("consuming"),e6=Symbol("bufferConcat"),VR=Symbol("maybeEnd"),kI=Symbol("writing"),Q0=Symbol("aborted"),JR=Symbol("onDone"),dm=Symbol("sawValidEntry"),KR=Symbol("sawNullBlock"),zR=Symbol("sawEOF"),_pe=Symbol("closeStream"),snt=()=>!0,Bm=class extends uhe.EventEmitter{file;strict;maxMetaEntrySize;filter;brotli;zstd;writable=!0;readable=!1;[Wp]=[];[Ci];[qp];[pm];[Uc]="begin";[x0]="";[aA];[Ev];[k0]=!1;[hs];[Q0]=!1;[dm];[KR]=!1;[zR]=!1;[kI]=!1;[Iv]=!1;[Zj]=!1;constructor(e={}){super(),this.file=e.file||"",this.on(JR,()=>{(this[Uc]==="begin"||this[dm]===!1)&&this.warn("TAR_BAD_ARCHIVE","Unrecognized archive format")}),e.ondone?this.on(JR,e.ondone):this.on(JR,()=>{this.emit("prefinish"),this.emit("finish"),this.emit("end")}),this.strict=!!e.strict,this.maxMetaEntrySize=e.maxMetaEntrySize||nnt,this.filter=typeof e.filter=="function"?e.filter:snt;let t=e.file&&(e.file.endsWith(".tar.br")||e.file.endsWith(".tbr"));this.brotli=!(e.gzip||e.zstd)&&e.brotli!==void 0?e.brotli:t?void 0:!1;let r=e.file&&(e.file.endsWith(".tar.zst")||e.file.endsWith(".tzst"));this.zstd=!(e.gzip||e.brotli)&&e.zstd!==void 0?e.zstd:r?!0:void 0,this.on("end",()=>this[_pe]()),typeof e.onwarn=="function"&&this.on("warn",e.onwarn),typeof e.onReadEntry=="function"&&this.on("entry",e.onReadEntry)}warn(e,t,r={}){mT(this,e,t,r)}[Upe](e,t){this[dm]===void 0&&(this[dm]=!1);let r;try{r=new wm(e,t,this[aA],this[Ev])}catch(s){return this.warn("TAR_ENTRY_INVALID",s)}if(r.nullBlock)this[KR]?(this[zR]=!0,this[Uc]==="begin"&&(this[Uc]="header"),this[hm]("eof")):(this[KR]=!0,this[hm]("nullBlock"));else if(this[KR]=!1,!r.cksumValid)this.warn("TAR_ENTRY_INVALID","checksum failure",{header:r});else if(!r.path)this.warn("TAR_ENTRY_INVALID","path is required",{header:r});else{let s=r.type;if(/^(Symbolic)?Link$/.test(s)&&!r.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath required",{header:r});else if(!/^(Symbolic)?Link$/.test(s)&&!/^(Global)?ExtendedHeader$/.test(s)&&r.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath forbidden",{header:r});else{let a=this[pm]=new Ehe(r,this[aA],this[Ev]);if(!this[dm])if(a.remain){let n=()=>{a.invalid||(this[dm]=!0)};a.on("end",n)}else this[dm]=!0;a.meta?a.size>this.maxMetaEntrySize?(a.ignore=!0,this[hm]("ignoredEntry",a),this[Uc]="ignore",a.resume()):a.size>0&&(this[x0]="",a.on("data",n=>this[x0]+=n),this[Uc]="meta"):(this[aA]=void 0,a.ignore=a.ignore||!this.filter(a.path,a),a.ignore?(this[hm]("ignoredEntry",a),this[Uc]=a.remain?"ignore":"header",a.resume()):(a.remain?this[Uc]="body":(this[Uc]="header",a.end()),this[qp]?this[Wp].push(a):(this[Wp].push(a),this[Xj]())))}}}[_pe](){queueMicrotask(()=>this.emit("close"))}[Ope](e){let t=!0;if(!e)this[qp]=void 0,t=!1;else if(Array.isArray(e)){let[r,...s]=e;this.emit(r,...s)}else this[qp]=e,this.emit("entry",e),e.emittedEnd||(e.on("end",()=>this[Xj]()),t=!1);return t}[Xj](){do;while(this[Ope](this[Wp].shift()));if(!this[Wp].length){let e=this[qp];!e||e.flowing||e.size===e.remain?this[kI]||this.emit("drain"):e.once("drain",()=>this.emit("drain"))}}[$j](e,t){let r=this[pm];if(!r)throw new Error("attempt to consume body without entry??");let s=r.blockRemain??0,a=s>=e.length&&t===0?e:e.subarray(t,t+s);return r.write(a),r.blockRemain||(this[Uc]="header",this[pm]=void 0,r.end()),a.length}[Mpe](e,t){let r=this[pm],s=this[$j](e,t);return!this[pm]&&r&&this[Lpe](r),s}[hm](e,t,r){!this[Wp].length&&!this[qp]?this.emit(e,t,r):this[Wp].push([e,t,r])}[Lpe](e){switch(this[hm]("meta",this[x0]),e.type){case"ExtendedHeader":case"OldExtendedHeader":this[aA]=uT.parse(this[x0],this[aA],!1);break;case"GlobalExtendedHeader":this[Ev]=uT.parse(this[x0],this[Ev],!0);break;case"NextFileHasLongPath":case"OldGnuLongPath":{let t=this[aA]??Object.create(null);this[aA]=t,t.path=this[x0].replace(/\0.*/,"");break}case"NextFileHasLongLinkpath":{let t=this[aA]||Object.create(null);this[aA]=t,t.linkpath=this[x0].replace(/\0.*/,"");break}default:throw new Error("unknown meta: "+e.type)}}abort(e){this[Q0]=!0,this.emit("abort",e),this.warn("TAR_ABORT",e,{recoverable:!1})}write(e,t,r){if(typeof t=="function"&&(r=t,t=void 0),typeof e=="string"&&(e=Buffer.from(e,typeof t=="string"?t:"utf8")),this[Q0])return r?.(),!1;if((this[hs]===void 0||this.brotli===void 0&&this[hs]===!1)&&e){if(this[Ci]&&(e=Buffer.concat([this[Ci],e]),this[Ci]=void 0),e.lengththis[WR](p)),this[hs].on("error",p=>this.abort(p)),this[hs].on("end",()=>{this[k0]=!0,this[WR]()}),this[kI]=!0;let f=!!this[hs][c?"end":"write"](e);return this[kI]=!1,r?.(),f}}this[kI]=!0,this[hs]?this[hs].write(e):this[WR](e),this[kI]=!1;let s=this[Wp].length?!1:this[qp]?this[qp].flowing:!0;return!s&&!this[Wp].length&&this[qp]?.once("drain",()=>this.emit("drain")),r?.(),s}[e6](e){e&&!this[Q0]&&(this[Ci]=this[Ci]?Buffer.concat([this[Ci],e]):e)}[VR](){if(this[k0]&&!this[Zj]&&!this[Q0]&&!this[Iv]){this[Zj]=!0;let e=this[pm];if(e&&e.blockRemain){let t=this[Ci]?this[Ci].length:0;this.warn("TAR_BAD_ARCHIVE",`Truncated input (needed ${e.blockRemain} more bytes, only ${t} available)`,{entry:e}),this[Ci]&&e.write(this[Ci]),e.end()}this[hm](JR)}}[WR](e){if(this[Iv]&&e)this[e6](e);else if(!e&&!this[Ci])this[VR]();else if(e){if(this[Iv]=!0,this[Ci]){this[e6](e);let t=this[Ci];this[Ci]=void 0,this[YR](t)}else this[YR](e);for(;this[Ci]&&this[Ci]?.length>=512&&!this[Q0]&&!this[zR];){let t=this[Ci];this[Ci]=void 0,this[YR](t)}this[Iv]=!1}(!this[Ci]||this[k0])&&this[VR]()}[YR](e){let t=0,r=e.length;for(;t+512<=r&&!this[Q0]&&!this[zR];)switch(this[Uc]){case"begin":case"header":this[Upe](e,t),t+=512;break;case"ignore":case"body":t+=this[$j](e,t);break;case"meta":t+=this[Mpe](e,t);break;default:throw new Error("invalid state: "+this[Uc])}t{let t=e.length-1,r=-1;for(;t>-1&&e.charAt(t)==="/";)r=t,t--;return r===-1?e:e.slice(0,r)},ont=e=>{let t=e.onReadEntry;e.onReadEntry=t?r=>{t(r),r.resume()}:r=>r.resume()},Ihe=(e,t)=>{let r=new Map(t.map(n=>[Pv(n),!0])),s=e.filter,a=(n,c="")=>{let f=c||(0,dT.parse)(n).root||".",p;if(n===f)p=!1;else{let h=r.get(n);h!==void 0?p=h:p=a((0,dT.dirname)(n),f)}return r.set(n,p),p};e.filter=s?(n,c)=>s(n,c)&&a(Pv(n)):n=>a(Pv(n))},ant=e=>{let t=new Bm(e),r=e.file,s;try{s=Em.default.openSync(r,"r");let a=Em.default.fstatSync(s),n=e.maxReadSize||16*1024*1024;if(a.size{let r=new Bm(e),s=e.maxReadSize||16*1024*1024,a=e.file;return new Promise((n,c)=>{r.on("error",c),r.on("end",n),Em.default.stat(a,(f,p)=>{if(f)c(f);else{let h=new T6(a,{readSize:s,size:p.size});h.on("error",c),h.pipe(r)}})})},yT=Ov(ant,lnt,e=>new Bm(e),e=>new Bm(e),(e,t)=>{t?.length&&Ihe(e,t),e.noResume||ont(e)}),Che=(e,t,r)=>(e&=4095,r&&(e=(e|384)&-19),t&&(e&256&&(e|=64),e&32&&(e|=8),e&4&&(e|=1)),e),{isAbsolute:cnt,parse:Hpe}=whe.win32,M6=e=>{let t="",r=Hpe(e);for(;cnt(e)||r.root;){let s=e.charAt(0)==="/"&&e.slice(0,4)!=="//?/"?"/":r.root;e=e.slice(s.length),t+=s,r=Hpe(e)}return[t,e]},ET=["|","<",">","?",":"],U6=ET.map(e=>String.fromCharCode(61440+e.charCodeAt(0))),unt=new Map(ET.map((e,t)=>[e,U6[t]])),fnt=new Map(U6.map((e,t)=>[e,ET[t]])),jpe=e=>ET.reduce((t,r)=>t.split(r).join(unt.get(r)),e),Ant=e=>U6.reduce((t,r)=>t.split(r).join(fnt.get(r)),e),Bhe=(e,t)=>t?(e=cn(e).replace(/^\.(\/|$)/,""),Pv(t)+"/"+e):cn(e),pnt=16*1024*1024,Gpe=Symbol("process"),qpe=Symbol("file"),Wpe=Symbol("directory"),d6=Symbol("symlink"),Ype=Symbol("hardlink"),Cv=Symbol("header"),tT=Symbol("read"),g6=Symbol("lstat"),rT=Symbol("onlstat"),m6=Symbol("onread"),y6=Symbol("onreadlink"),E6=Symbol("openfile"),I6=Symbol("onopenfile"),N0=Symbol("close"),fT=Symbol("mode"),C6=Symbol("awaitDrain"),t6=Symbol("ondrain"),lA=Symbol("prefix"),vhe=class extends vm{path;portable;myuid=process.getuid&&process.getuid()||0;myuser=process.env.USER||"";maxReadSize;linkCache;statCache;preservePaths;cwd;strict;mtime;noPax;noMtime;prefix;fd;blockLen=0;blockRemain=0;buf;pos=0;remain=0;length=0;offset=0;win32;absolute;header;type;linkpath;stat;onWriteEntry;#e=!1;constructor(e,t={}){let r=N6(t);super(),this.path=cn(e),this.portable=!!r.portable,this.maxReadSize=r.maxReadSize||pnt,this.linkCache=r.linkCache||new Map,this.statCache=r.statCache||new Map,this.preservePaths=!!r.preservePaths,this.cwd=cn(r.cwd||process.cwd()),this.strict=!!r.strict,this.noPax=!!r.noPax,this.noMtime=!!r.noMtime,this.mtime=r.mtime,this.prefix=r.prefix?cn(r.prefix):void 0,this.onWriteEntry=r.onWriteEntry,typeof r.onwarn=="function"&&this.on("warn",r.onwarn);let s=!1;if(!this.preservePaths){let[n,c]=M6(this.path);n&&typeof c=="string"&&(this.path=c,s=n)}this.win32=!!r.win32||process.platform==="win32",this.win32&&(this.path=Ant(this.path.replace(/\\/g,"/")),e=e.replace(/\\/g,"/")),this.absolute=cn(r.absolute||h6.default.resolve(this.cwd,e)),this.path===""&&(this.path="./"),s&&this.warn("TAR_ENTRY_INFO",`stripping ${s} from absolute path`,{entry:this,path:s+this.path});let a=this.statCache.get(this.absolute);a?this[rT](a):this[g6]()}warn(e,t,r={}){return mT(this,e,t,r)}emit(e,...t){return e==="error"&&(this.#e=!0),super.emit(e,...t)}[g6](){Vu.default.lstat(this.absolute,(e,t)=>{if(e)return this.emit("error",e);this[rT](t)})}[rT](e){this.statCache.set(this.absolute,e),this.stat=e,e.isFile()||(e.size=0),this.type=gnt(e),this.emit("stat",e),this[Gpe]()}[Gpe](){switch(this.type){case"File":return this[qpe]();case"Directory":return this[Wpe]();case"SymbolicLink":return this[d6]();default:return this.end()}}[fT](e){return Che(e,this.type==="Directory",this.portable)}[lA](e){return Bhe(e,this.prefix)}[Cv](){if(!this.stat)throw new Error("cannot write header before stat");this.type==="Directory"&&this.portable&&(this.noMtime=!0),this.onWriteEntry?.(this),this.header=new wm({path:this[lA](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[lA](this.linkpath):this.linkpath,mode:this[fT](this.stat.mode),uid:this.portable?void 0:this.stat.uid,gid:this.portable?void 0:this.stat.gid,size:this.stat.size,mtime:this.noMtime?void 0:this.mtime||this.stat.mtime,type:this.type==="Unsupported"?void 0:this.type,uname:this.portable?void 0:this.stat.uid===this.myuid?this.myuser:"",atime:this.portable?void 0:this.stat.atime,ctime:this.portable?void 0:this.stat.ctime}),this.header.encode()&&!this.noPax&&super.write(new uT({atime:this.portable?void 0:this.header.atime,ctime:this.portable?void 0:this.header.ctime,gid:this.portable?void 0:this.header.gid,mtime:this.noMtime?void 0:this.mtime||this.header.mtime,path:this[lA](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[lA](this.linkpath):this.linkpath,size:this.header.size,uid:this.portable?void 0:this.header.uid,uname:this.portable?void 0:this.header.uname,dev:this.portable?void 0:this.stat.dev,ino:this.portable?void 0:this.stat.ino,nlink:this.portable?void 0:this.stat.nlink}).encode());let e=this.header?.block;if(!e)throw new Error("failed to encode header");super.write(e)}[Wpe](){if(!this.stat)throw new Error("cannot create directory entry without stat");this.path.slice(-1)!=="/"&&(this.path+="/"),this.stat.size=0,this[Cv](),this.end()}[d6](){Vu.default.readlink(this.absolute,(e,t)=>{if(e)return this.emit("error",e);this[y6](t)})}[y6](e){this.linkpath=cn(e),this[Cv](),this.end()}[Ype](e){if(!this.stat)throw new Error("cannot create link entry without stat");this.type="Link",this.linkpath=cn(h6.default.relative(this.cwd,e)),this.stat.size=0,this[Cv](),this.end()}[qpe](){if(!this.stat)throw new Error("cannot create file entry without stat");if(this.stat.nlink>1){let e=`${this.stat.dev}:${this.stat.ino}`,t=this.linkCache.get(e);if(t?.indexOf(this.cwd)===0)return this[Ype](t);this.linkCache.set(e,this.absolute)}if(this[Cv](),this.stat.size===0)return this.end();this[E6]()}[E6](){Vu.default.open(this.absolute,"r",(e,t)=>{if(e)return this.emit("error",e);this[I6](t)})}[I6](e){if(this.fd=e,this.#e)return this[N0]();if(!this.stat)throw new Error("should stat before calling onopenfile");this.blockLen=512*Math.ceil(this.stat.size/512),this.blockRemain=this.blockLen;let t=Math.min(this.blockLen,this.maxReadSize);this.buf=Buffer.allocUnsafe(t),this.offset=0,this.pos=0,this.remain=this.stat.size,this.length=this.buf.length,this[tT]()}[tT](){let{fd:e,buf:t,offset:r,length:s,pos:a}=this;if(e===void 0||t===void 0)throw new Error("cannot read file without first opening");Vu.default.read(e,t,r,s,a,(n,c)=>{if(n)return this[N0](()=>this.emit("error",n));this[m6](c)})}[N0](e=()=>{}){this.fd!==void 0&&Vu.default.close(this.fd,e)}[m6](e){if(e<=0&&this.remain>0){let r=Object.assign(new Error("encountered unexpected EOF"),{path:this.absolute,syscall:"read",code:"EOF"});return this[N0](()=>this.emit("error",r))}if(e>this.remain){let r=Object.assign(new Error("did not encounter expected EOF"),{path:this.absolute,syscall:"read",code:"EOF"});return this[N0](()=>this.emit("error",r))}if(!this.buf)throw new Error("should have created buffer prior to reading");if(e===this.remain)for(let r=e;rthis[t6]())}[C6](e){this.once("drain",e)}write(e,t,r){if(typeof t=="function"&&(r=t,t=void 0),typeof e=="string"&&(e=Buffer.from(e,typeof t=="string"?t:"utf8")),this.blockRemaine?this.emit("error",e):this.end());if(!this.buf)throw new Error("buffer lost somehow in ONDRAIN");this.offset>=this.length&&(this.buf=Buffer.allocUnsafe(Math.min(this.blockRemain,this.buf.length)),this.offset=0),this.length=this.buf.length-this.offset,this[tT]()}},hnt=class extends vhe{sync=!0;[g6](){this[rT](Vu.default.lstatSync(this.absolute))}[d6](){this[y6](Vu.default.readlinkSync(this.absolute))}[E6](){this[I6](Vu.default.openSync(this.absolute,"r"))}[tT](){let e=!0;try{let{fd:t,buf:r,offset:s,length:a,pos:n}=this;if(t===void 0||r===void 0)throw new Error("fd and buf must be set in READ method");let c=Vu.default.readSync(t,r,s,a,n);this[m6](c),e=!1}finally{if(e)try{this[N0](()=>{})}catch{}}}[C6](e){e()}[N0](e=()=>{}){this.fd!==void 0&&Vu.default.closeSync(this.fd),e()}},dnt=class extends vm{blockLen=0;blockRemain=0;buf=0;pos=0;remain=0;length=0;preservePaths;portable;strict;noPax;noMtime;readEntry;type;prefix;path;mode;uid;gid;uname;gname;header;mtime;atime;ctime;linkpath;size;onWriteEntry;warn(e,t,r={}){return mT(this,e,t,r)}constructor(e,t={}){let r=N6(t);super(),this.preservePaths=!!r.preservePaths,this.portable=!!r.portable,this.strict=!!r.strict,this.noPax=!!r.noPax,this.noMtime=!!r.noMtime,this.onWriteEntry=r.onWriteEntry,this.readEntry=e;let{type:s}=e;if(s==="Unsupported")throw new Error("writing entry that should be ignored");this.type=s,this.type==="Directory"&&this.portable&&(this.noMtime=!0),this.prefix=r.prefix,this.path=cn(e.path),this.mode=e.mode!==void 0?this[fT](e.mode):void 0,this.uid=this.portable?void 0:e.uid,this.gid=this.portable?void 0:e.gid,this.uname=this.portable?void 0:e.uname,this.gname=this.portable?void 0:e.gname,this.size=e.size,this.mtime=this.noMtime?void 0:r.mtime||e.mtime,this.atime=this.portable?void 0:e.atime,this.ctime=this.portable?void 0:e.ctime,this.linkpath=e.linkpath!==void 0?cn(e.linkpath):void 0,typeof r.onwarn=="function"&&this.on("warn",r.onwarn);let a=!1;if(!this.preservePaths){let[c,f]=M6(this.path);c&&typeof f=="string"&&(this.path=f,a=c)}this.remain=e.size,this.blockRemain=e.startBlockSize,this.onWriteEntry?.(this),this.header=new wm({path:this[lA](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[lA](this.linkpath):this.linkpath,mode:this.mode,uid:this.portable?void 0:this.uid,gid:this.portable?void 0:this.gid,size:this.size,mtime:this.noMtime?void 0:this.mtime,type:this.type,uname:this.portable?void 0:this.uname,atime:this.portable?void 0:this.atime,ctime:this.portable?void 0:this.ctime}),a&&this.warn("TAR_ENTRY_INFO",`stripping ${a} from absolute path`,{entry:this,path:a+this.path}),this.header.encode()&&!this.noPax&&super.write(new uT({atime:this.portable?void 0:this.atime,ctime:this.portable?void 0:this.ctime,gid:this.portable?void 0:this.gid,mtime:this.noMtime?void 0:this.mtime,path:this[lA](this.path),linkpath:this.type==="Link"&&this.linkpath!==void 0?this[lA](this.linkpath):this.linkpath,size:this.size,uid:this.portable?void 0:this.uid,uname:this.portable?void 0:this.uname,dev:this.portable?void 0:this.readEntry.dev,ino:this.portable?void 0:this.readEntry.ino,nlink:this.portable?void 0:this.readEntry.nlink}).encode());let n=this.header?.block;if(!n)throw new Error("failed to encode header");super.write(n),e.pipe(this)}[lA](e){return Bhe(e,this.prefix)}[fT](e){return Che(e,this.type==="Directory",this.portable)}write(e,t,r){typeof t=="function"&&(r=t,t=void 0),typeof e=="string"&&(e=Buffer.from(e,typeof t=="string"?t:"utf8"));let s=e.length;if(s>this.blockRemain)throw new Error("writing more to entry than is appropriate");return this.blockRemain-=s,super.write(e,r)}end(e,t,r){return this.blockRemain&&super.write(Buffer.alloc(this.blockRemain)),typeof e=="function"&&(r=e,t=void 0,e=void 0),typeof t=="function"&&(r=t,t=void 0),typeof e=="string"&&(e=Buffer.from(e,t??"utf8")),r&&this.once("finish",r),e?super.end(e,r):super.end(r),this}},gnt=e=>e.isFile()?"File":e.isDirectory()?"Directory":e.isSymbolicLink()?"SymbolicLink":"Unsupported",mnt=class OI{tail;head;length=0;static create(t=[]){return new OI(t)}constructor(t=[]){for(let r of t)this.push(r)}*[Symbol.iterator](){for(let t=this.head;t;t=t.next)yield t.value}removeNode(t){if(t.list!==this)throw new Error("removing node which does not belong to this list");let r=t.next,s=t.prev;return r&&(r.prev=s),s&&(s.next=r),t===this.head&&(this.head=r),t===this.tail&&(this.tail=s),this.length--,t.next=void 0,t.prev=void 0,t.list=void 0,r}unshiftNode(t){if(t===this.head)return;t.list&&t.list.removeNode(t);let r=this.head;t.list=this,t.next=r,r&&(r.prev=t),this.head=t,this.tail||(this.tail=t),this.length++}pushNode(t){if(t===this.tail)return;t.list&&t.list.removeNode(t);let r=this.tail;t.list=this,t.prev=r,r&&(r.next=t),this.tail=t,this.head||(this.head=t),this.length++}push(...t){for(let r=0,s=t.length;r1)s=r;else if(this.head)a=this.head.next,s=this.head.value;else throw new TypeError("Reduce of empty list with no initial value");for(var n=0;a;n++)s=t(s,a.value,n),a=a.next;return s}reduceReverse(t,r){let s,a=this.tail;if(arguments.length>1)s=r;else if(this.tail)a=this.tail.prev,s=this.tail.value;else throw new TypeError("Reduce of empty list with no initial value");for(let n=this.length-1;a;n--)s=t(s,a.value,n),a=a.prev;return s}toArray(){let t=new Array(this.length);for(let r=0,s=this.head;s;r++)t[r]=s.value,s=s.next;return t}toArrayReverse(){let t=new Array(this.length);for(let r=0,s=this.tail;s;r++)t[r]=s.value,s=s.prev;return t}slice(t=0,r=this.length){r<0&&(r+=this.length),t<0&&(t+=this.length);let s=new OI;if(rthis.length&&(r=this.length);let a=this.head,n=0;for(n=0;a&&nthis.length&&(r=this.length);let a=this.length,n=this.tail;for(;n&&a>r;a--)n=n.prev;for(;n&&a>t;a--,n=n.prev)s.push(n.value);return s}splice(t,r=0,...s){t>this.length&&(t=this.length-1),t<0&&(t=this.length+t);let a=this.head;for(let c=0;a&&c1)throw new TypeError("gzip, brotli, zstd are mutually exclusive");if(e.gzip&&(typeof e.gzip!="object"&&(e.gzip={}),this.portable&&(e.gzip.portable=!0),this.zip=new krt(e.gzip)),e.brotli&&(typeof e.brotli!="object"&&(e.brotli={}),this.zip=new Rrt(e.brotli)),e.zstd&&(typeof e.zstd!="object"&&(e.zstd={}),this.zip=new Frt(e.zstd)),!this.zip)throw new Error("impossible");let t=this.zip;t.on("data",r=>super.write(r)),t.on("end",()=>super.end()),t.on("drain",()=>this[s6]()),this.on("resume",()=>t.resume())}else this.on("drain",this[s6]);this.noDirRecurse=!!e.noDirRecurse,this.follow=!!e.follow,this.noMtime=!!e.noMtime,e.mtime&&(this.mtime=e.mtime),this.filter=typeof e.filter=="function"?e.filter:()=>!0,this[Wu]=new mnt,this[Yu]=0,this.jobs=Number(e.jobs)||4,this[Bv]=!1,this[wv]=!1}[She](e){return super.write(e)}add(e){return this.write(e),this}end(e,t,r){return typeof e=="function"&&(r=e,e=void 0),typeof t=="function"&&(r=t,t=void 0),e&&this.add(e),this[wv]=!0,this[mm](),r&&r(),this}write(e){if(this[wv])throw new Error("write after end");return e instanceof Ehe?this[Kpe](e):this[iT](e),this.flowing}[Kpe](e){let t=cn(w6.default.resolve(this.cwd,e.path));if(!this.filter(e.path,e))e.resume();else{let r=new Vpe(e.path,t);r.entry=new dnt(e,this[i6](r)),r.entry.on("end",()=>this[n6](r)),this[Yu]+=1,this[Wu].push(r)}this[mm]()}[iT](e){let t=cn(w6.default.resolve(this.cwd,e));this[Wu].push(new Vpe(e,t)),this[mm]()}[B6](e){e.pending=!0,this[Yu]+=1;let t=this.follow?"stat":"lstat";Fv.default[t](e.absolute,(r,s)=>{e.pending=!1,this[Yu]-=1,r?this.emit("error",r):this[nT](e,s)})}[nT](e,t){this.statCache.set(e.absolute,t),e.stat=t,this.filter(e.path,t)?t.isFile()&&t.nlink>1&&e===this[gm]&&!this.linkCache.get(`${t.dev}:${t.ino}`)&&!this.sync&&this[r6](e):e.ignore=!0,this[mm]()}[v6](e){e.pending=!0,this[Yu]+=1,Fv.default.readdir(e.absolute,(t,r)=>{if(e.pending=!1,this[Yu]-=1,t)return this.emit("error",t);this[sT](e,r)})}[sT](e,t){this.readdirCache.set(e.absolute,t),e.readdir=t,this[mm]()}[mm](){if(!this[Bv]){this[Bv]=!0;for(let e=this[Wu].head;e&&this[Yu]this.warn(t,r,s),noPax:this.noPax,cwd:this.cwd,absolute:e.absolute,preservePaths:this.preservePaths,maxReadSize:this.maxReadSize,strict:this.strict,portable:this.portable,linkCache:this.linkCache,statCache:this.statCache,noMtime:this.noMtime,mtime:this.mtime,prefix:this.prefix,onWriteEntry:this.onWriteEntry}}[zpe](e){this[Yu]+=1;try{return new this[aT](e.path,this[i6](e)).on("end",()=>this[n6](e)).on("error",t=>this.emit("error",t))}catch(t){this.emit("error",t)}}[s6](){this[gm]&&this[gm].entry&&this[gm].entry.resume()}[oT](e){e.piped=!0,e.readdir&&e.readdir.forEach(s=>{let a=e.path,n=a==="./"?"":a.replace(/\/*$/,"/");this[iT](n+s)});let t=e.entry,r=this.zip;if(!t)throw new Error("cannot pipe without source");r?t.on("data",s=>{r.write(s)||t.pause()}):t.on("data",s=>{super.write(s)||t.pause()})}pause(){return this.zip&&this.zip.pause(),super.pause()}warn(e,t,r={}){mT(this,e,t,r)}},H6=class extends IT{sync=!0;constructor(e){super(e),this[aT]=hnt}pause(){}resume(){}[B6](e){let t=this.follow?"statSync":"lstatSync";this[nT](e,Fv.default[t](e.absolute))}[v6](e){this[sT](e,Fv.default.readdirSync(e.absolute))}[oT](e){let t=e.entry,r=this.zip;if(e.readdir&&e.readdir.forEach(s=>{let a=e.path,n=a==="./"?"":a.replace(/\/*$/,"/");this[iT](n+s)}),!t)throw new Error("Cannot pipe without source");r?t.on("data",s=>{r.write(s)}):t.on("data",s=>{super[She](s)})}},Cnt=(e,t)=>{let r=new H6(e),s=new che(e.file,{mode:e.mode||438});r.pipe(s),Dhe(r,t)},wnt=(e,t)=>{let r=new IT(e),s=new hT(e.file,{mode:e.mode||438});r.pipe(s);let a=new Promise((n,c)=>{s.on("error",c),s.on("close",n),r.on("error",c)});return bhe(r,t),a},Dhe=(e,t)=>{t.forEach(r=>{r.charAt(0)==="@"?yT({file:F6.default.resolve(e.cwd,r.slice(1)),sync:!0,noResume:!0,onReadEntry:s=>e.add(s)}):e.add(r)}),e.end()},bhe=async(e,t)=>{for(let r=0;r{e.add(a)}}):e.add(s)}e.end()},Bnt=(e,t)=>{let r=new H6(e);return Dhe(r,t),r},vnt=(e,t)=>{let r=new IT(e);return bhe(r,t),r},uUt=Ov(Cnt,wnt,Bnt,vnt,(e,t)=>{if(!t?.length)throw new TypeError("no paths specified to add to archive")}),Snt=process.platform,Dnt=Snt==="win32",{O_CREAT:bnt,O_TRUNC:Pnt,O_WRONLY:xnt}=q6.default.constants,xhe=Number(process.env.__FAKE_FS_O_FILENAME__)||q6.default.constants.UV_FS_O_FILEMAP||0,knt=Dnt&&!!xhe,Qnt=512*1024,Rnt=xhe|Pnt|bnt|xnt,khe=knt?e=>e"w",S6=(e,t,r)=>{try{return Lv.default.lchownSync(e,t,r)}catch(s){if(s?.code!=="ENOENT")throw s}},AT=(e,t,r,s)=>{Lv.default.lchown(e,t,r,a=>{s(a&&a?.code!=="ENOENT"?a:null)})},Tnt=(e,t,r,s,a)=>{if(t.isDirectory())Qhe(MI.default.resolve(e,t.name),r,s,n=>{if(n)return a(n);let c=MI.default.resolve(e,t.name);AT(c,r,s,a)});else{let n=MI.default.resolve(e,t.name);AT(n,r,s,a)}},Qhe=(e,t,r,s)=>{Lv.default.readdir(e,{withFileTypes:!0},(a,n)=>{if(a){if(a.code==="ENOENT")return s();if(a.code!=="ENOTDIR"&&a.code!=="ENOTSUP")return s(a)}if(a||!n.length)return AT(e,t,r,s);let c=n.length,f=null,p=h=>{if(!f){if(h)return s(f=h);if(--c===0)return AT(e,t,r,s)}};for(let h of n)Tnt(e,h,t,r,p)})},Fnt=(e,t,r,s)=>{t.isDirectory()&&Rhe(MI.default.resolve(e,t.name),r,s),S6(MI.default.resolve(e,t.name),r,s)},Rhe=(e,t,r)=>{let s;try{s=Lv.default.readdirSync(e,{withFileTypes:!0})}catch(a){let n=a;if(n?.code==="ENOENT")return;if(n?.code==="ENOTDIR"||n?.code==="ENOTSUP")return S6(e,t,r);throw n}for(let a of s)Fnt(e,a,t,r);return S6(e,t,r)},Fhe=class extends Error{path;code;syscall="chdir";constructor(e,t){super(`${t}: Cannot cd into '${e}'`),this.path=e,this.code=t}get name(){return"CwdError"}},CT=class extends Error{path;symlink;syscall="symlink";code="TAR_SYMLINK_ERROR";constructor(e,t){super("TAR_SYMLINK_ERROR: Cannot extract through symbolic link"),this.symlink=e,this.path=t}get name(){return"SymlinkError"}},Nnt=(e,t)=>{Il.default.stat(e,(r,s)=>{(r||!s.isDirectory())&&(r=new Fhe(e,r?.code||"ENOTDIR")),t(r)})},Ont=(e,t,r)=>{e=cn(e);let s=t.umask??18,a=t.mode|448,n=(a&s)!==0,c=t.uid,f=t.gid,p=typeof c=="number"&&typeof f=="number"&&(c!==t.processUid||f!==t.processGid),h=t.preserve,E=t.unlink,C=cn(t.cwd),S=(I,T)=>{I?r(I):T&&p?Qhe(T,c,f,O=>S(O)):n?Il.default.chmod(e,a,r):r()};if(e===C)return Nnt(e,S);if(h)return The.default.mkdir(e,{mode:a,recursive:!0}).then(I=>S(null,I??void 0),S);let x=cn(Nv.default.relative(C,e)).split("/");D6(C,x,a,E,C,void 0,S)},D6=(e,t,r,s,a,n,c)=>{if(!t.length)return c(null,n);let f=t.shift(),p=cn(Nv.default.resolve(e+"/"+f));Il.default.mkdir(p,r,Nhe(p,t,r,s,a,n,c))},Nhe=(e,t,r,s,a,n,c)=>f=>{f?Il.default.lstat(e,(p,h)=>{if(p)p.path=p.path&&cn(p.path),c(p);else if(h.isDirectory())D6(e,t,r,s,a,n,c);else if(s)Il.default.unlink(e,E=>{if(E)return c(E);Il.default.mkdir(e,r,Nhe(e,t,r,s,a,n,c))});else{if(h.isSymbolicLink())return c(new CT(e,e+"/"+t.join("/")));c(f)}}):(n=n||e,D6(e,t,r,s,a,n,c))},Lnt=e=>{let t=!1,r;try{t=Il.default.statSync(e).isDirectory()}catch(s){r=s?.code}finally{if(!t)throw new Fhe(e,r??"ENOTDIR")}},Mnt=(e,t)=>{e=cn(e);let r=t.umask??18,s=t.mode|448,a=(s&r)!==0,n=t.uid,c=t.gid,f=typeof n=="number"&&typeof c=="number"&&(n!==t.processUid||c!==t.processGid),p=t.preserve,h=t.unlink,E=cn(t.cwd),C=I=>{I&&f&&Rhe(I,n,c),a&&Il.default.chmodSync(e,s)};if(e===E)return Lnt(E),C();if(p)return C(Il.default.mkdirSync(e,{mode:s,recursive:!0})??void 0);let S=cn(Nv.default.relative(E,e)).split("/"),x;for(let I=S.shift(),T=E;I&&(T+="/"+I);I=S.shift()){T=cn(Nv.default.resolve(T));try{Il.default.mkdirSync(T,s),x=x||T}catch{let O=Il.default.lstatSync(T);if(O.isDirectory())continue;if(h){Il.default.unlinkSync(T),Il.default.mkdirSync(T,s),x=x||T;continue}else if(O.isSymbolicLink())return new CT(T,T+"/"+S.join("/"))}}return C(x)},o6=Object.create(null),Xpe=1e4,QI=new Set,Unt=e=>{QI.has(e)?QI.delete(e):o6[e]=e.normalize("NFD").toLocaleLowerCase("en").toLocaleUpperCase("en"),QI.add(e);let t=o6[e],r=QI.size-Xpe;if(r>Xpe/10){for(let s of QI)if(QI.delete(s),delete o6[s],--r<=0)break}return t},_nt=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,Hnt=_nt==="win32",jnt=e=>e.split("/").slice(0,-1).reduce((t,r)=>{let s=t[t.length-1];return s!==void 0&&(r=(0,W6.join)(s,r)),t.push(r||"/"),t},[]),Gnt=class{#e=new Map;#t=new Map;#s=new Set;reserve(e,t){e=Hnt?["win32 parallelization disabled"]:e.map(s=>Pv((0,W6.join)(Unt(s))));let r=new Set(e.map(s=>jnt(s)).reduce((s,a)=>s.concat(a)));this.#t.set(t,{dirs:r,paths:e});for(let s of e){let a=this.#e.get(s);a?a.push(t):this.#e.set(s,[t])}for(let s of r){let a=this.#e.get(s);if(!a)this.#e.set(s,[new Set([t])]);else{let n=a[a.length-1];n instanceof Set?n.add(t):a.push(new Set([t]))}}return this.#i(t)}#r(e){let t=this.#t.get(e);if(!t)throw new Error("function does not have any path reservations");return{paths:t.paths.map(r=>this.#e.get(r)),dirs:[...t.dirs].map(r=>this.#e.get(r))}}check(e){let{paths:t,dirs:r}=this.#r(e);return t.every(s=>s&&s[0]===e)&&r.every(s=>s&&s[0]instanceof Set&&s[0].has(e))}#i(e){return this.#s.has(e)||!this.check(e)?!1:(this.#s.add(e),e(()=>this.#n(e)),!0)}#n(e){if(!this.#s.has(e))return!1;let t=this.#t.get(e);if(!t)throw new Error("invalid reservation");let{paths:r,dirs:s}=t,a=new Set;for(let n of r){let c=this.#e.get(n);if(!c||c?.[0]!==e)continue;let f=c[1];if(!f){this.#e.delete(n);continue}if(c.shift(),typeof f=="function")a.add(f);else for(let p of f)a.add(p)}for(let n of s){let c=this.#e.get(n),f=c?.[0];if(!(!c||!(f instanceof Set)))if(f.size===1&&c.length===1){this.#e.delete(n);continue}else if(f.size===1){c.shift();let p=c[0];typeof p=="function"&&a.add(p)}else f.delete(e)}return this.#s.delete(e),a.forEach(n=>this.#i(n)),!0}},qnt=()=>process.umask(),Zpe=Symbol("onEntry"),b6=Symbol("checkFs"),$pe=Symbol("checkFs2"),P6=Symbol("isReusable"),_c=Symbol("makeFs"),x6=Symbol("file"),k6=Symbol("directory"),lT=Symbol("link"),ehe=Symbol("symlink"),the=Symbol("hardlink"),Dv=Symbol("ensureNoSymlink"),rhe=Symbol("unsupported"),nhe=Symbol("checkPath"),a6=Symbol("stripAbsolutePath"),L0=Symbol("mkdir"),Zs=Symbol("onError"),XR=Symbol("pending"),ihe=Symbol("pend"),RI=Symbol("unpend"),l6=Symbol("ended"),c6=Symbol("maybeClose"),Q6=Symbol("skip"),xv=Symbol("doChown"),kv=Symbol("uid"),Qv=Symbol("gid"),Rv=Symbol("checkedCwd"),Wnt=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,Tv=Wnt==="win32",Ynt=1024,Vnt=(e,t)=>{if(!Tv)return wn.default.unlink(e,t);let r=e+".DELETE."+(0,G6.randomBytes)(16).toString("hex");wn.default.rename(e,r,s=>{if(s)return t(s);wn.default.unlink(r,t)})},Jnt=e=>{if(!Tv)return wn.default.unlinkSync(e);let t=e+".DELETE."+(0,G6.randomBytes)(16).toString("hex");wn.default.renameSync(e,t),wn.default.unlinkSync(t)},she=(e,t,r)=>e!==void 0&&e===e>>>0?e:t!==void 0&&t===t>>>0?t:r,Y6=class extends Bm{[l6]=!1;[Rv]=!1;[XR]=0;reservations=new Gnt;transform;writable=!0;readable=!1;uid;gid;setOwner;preserveOwner;processGid;processUid;maxDepth;forceChown;win32;newer;keep;noMtime;preservePaths;unlink;cwd;strip;processUmask;umask;dmode;fmode;chmod;constructor(e={}){if(e.ondone=()=>{this[l6]=!0,this[c6]()},super(e),this.transform=e.transform,this.chmod=!!e.chmod,typeof e.uid=="number"||typeof e.gid=="number"){if(typeof e.uid!="number"||typeof e.gid!="number")throw new TypeError("cannot set owner without number uid and gid");if(e.preserveOwner)throw new TypeError("cannot preserve owner in archive and also set owner explicitly");this.uid=e.uid,this.gid=e.gid,this.setOwner=!0}else this.uid=void 0,this.gid=void 0,this.setOwner=!1;e.preserveOwner===void 0&&typeof e.uid!="number"?this.preserveOwner=!!(process.getuid&&process.getuid()===0):this.preserveOwner=!!e.preserveOwner,this.processUid=(this.preserveOwner||this.setOwner)&&process.getuid?process.getuid():void 0,this.processGid=(this.preserveOwner||this.setOwner)&&process.getgid?process.getgid():void 0,this.maxDepth=typeof e.maxDepth=="number"?e.maxDepth:Ynt,this.forceChown=e.forceChown===!0,this.win32=!!e.win32||Tv,this.newer=!!e.newer,this.keep=!!e.keep,this.noMtime=!!e.noMtime,this.preservePaths=!!e.preservePaths,this.unlink=!!e.unlink,this.cwd=cn(ds.default.resolve(e.cwd||process.cwd())),this.strip=Number(e.strip)||0,this.processUmask=this.chmod?typeof e.processUmask=="number"?e.processUmask:qnt():0,this.umask=typeof e.umask=="number"?e.umask:this.processUmask,this.dmode=e.dmode||511&~this.umask,this.fmode=e.fmode||438&~this.umask,this.on("entry",t=>this[Zpe](t))}warn(e,t,r={}){return(e==="TAR_BAD_ARCHIVE"||e==="TAR_ABORT")&&(r.recoverable=!1),super.warn(e,t,r)}[c6](){this[l6]&&this[XR]===0&&(this.emit("prefinish"),this.emit("finish"),this.emit("end"))}[a6](e,t){let r=e[t],{type:s}=e;if(!r||this.preservePaths)return!0;let a=r.split("/");if(a.includes("..")||Tv&&/^[a-z]:\.\.$/i.test(a[0]??"")){if(t==="path"||s==="Link")return this.warn("TAR_ENTRY_ERROR",`${t} contains '..'`,{entry:e,[t]:r}),!1;{let f=ds.default.posix.dirname(e.path),p=ds.default.posix.normalize(ds.default.posix.join(f,r));if(p.startsWith("../")||p==="..")return this.warn("TAR_ENTRY_ERROR",`${t} escapes extraction directory`,{entry:e,[t]:r}),!1}}let[n,c]=M6(r);return n&&(e[t]=String(c),this.warn("TAR_ENTRY_INFO",`stripping ${n} from absolute ${t}`,{entry:e,[t]:r})),!0}[nhe](e){let t=cn(e.path),r=t.split("/");if(this.strip){if(r.length=this.strip)e.linkpath=s.slice(this.strip).join("/");else return!1}r.splice(0,this.strip),e.path=r.join("/")}if(isFinite(this.maxDepth)&&r.length>this.maxDepth)return this.warn("TAR_ENTRY_ERROR","path excessively deep",{entry:e,path:t,depth:r.length,maxDepth:this.maxDepth}),!1;if(!this[a6](e,"path")||!this[a6](e,"linkpath"))return!1;if(ds.default.isAbsolute(e.path)?e.absolute=cn(ds.default.resolve(e.path)):e.absolute=cn(ds.default.resolve(this.cwd,e.path)),!this.preservePaths&&typeof e.absolute=="string"&&e.absolute.indexOf(this.cwd+"/")!==0&&e.absolute!==this.cwd)return this.warn("TAR_ENTRY_ERROR","path escaped extraction target",{entry:e,path:cn(e.path),resolvedPath:e.absolute,cwd:this.cwd}),!1;if(e.absolute===this.cwd&&e.type!=="Directory"&&e.type!=="GNUDumpDir")return!1;if(this.win32){let{root:s}=ds.default.win32.parse(String(e.absolute));e.absolute=s+jpe(String(e.absolute).slice(s.length));let{root:a}=ds.default.win32.parse(e.path);e.path=a+jpe(e.path.slice(a.length))}return!0}[Zpe](e){if(!this[nhe](e))return e.resume();switch(Phe.default.equal(typeof e.absolute,"string"),e.type){case"Directory":case"GNUDumpDir":e.mode&&(e.mode=e.mode|448);case"File":case"OldFile":case"ContiguousFile":case"Link":case"SymbolicLink":return this[b6](e);default:return this[rhe](e)}}[Zs](e,t){e.name==="CwdError"?this.emit("error",e):(this.warn("TAR_ENTRY_ERROR",e,{entry:t}),this[RI](),t.resume())}[L0](e,t,r){Ont(cn(e),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cwd:this.cwd,mode:t},r)}[xv](e){return this.forceChown||this.preserveOwner&&(typeof e.uid=="number"&&e.uid!==this.processUid||typeof e.gid=="number"&&e.gid!==this.processGid)||typeof this.uid=="number"&&this.uid!==this.processUid||typeof this.gid=="number"&&this.gid!==this.processGid}[kv](e){return she(this.uid,e.uid,this.processUid)}[Qv](e){return she(this.gid,e.gid,this.processGid)}[x6](e,t){let r=typeof e.mode=="number"?e.mode&4095:this.fmode,s=new hT(String(e.absolute),{flags:khe(e.size),mode:r,autoClose:!1});s.on("error",f=>{s.fd&&wn.default.close(s.fd,()=>{}),s.write=()=>!0,this[Zs](f,e),t()});let a=1,n=f=>{if(f){s.fd&&wn.default.close(s.fd,()=>{}),this[Zs](f,e),t();return}--a===0&&s.fd!==void 0&&wn.default.close(s.fd,p=>{p?this[Zs](p,e):this[RI](),t()})};s.on("finish",()=>{let f=String(e.absolute),p=s.fd;if(typeof p=="number"&&e.mtime&&!this.noMtime){a++;let h=e.atime||new Date,E=e.mtime;wn.default.futimes(p,h,E,C=>C?wn.default.utimes(f,h,E,S=>n(S&&C)):n())}if(typeof p=="number"&&this[xv](e)){a++;let h=this[kv](e),E=this[Qv](e);typeof h=="number"&&typeof E=="number"&&wn.default.fchown(p,h,E,C=>C?wn.default.chown(f,h,E,S=>n(S&&C)):n())}n()});let c=this.transform&&this.transform(e)||e;c!==e&&(c.on("error",f=>{this[Zs](f,e),t()}),e.pipe(c)),c.pipe(s)}[k6](e,t){let r=typeof e.mode=="number"?e.mode&4095:this.dmode;this[L0](String(e.absolute),r,s=>{if(s){this[Zs](s,e),t();return}let a=1,n=()=>{--a===0&&(t(),this[RI](),e.resume())};e.mtime&&!this.noMtime&&(a++,wn.default.utimes(String(e.absolute),e.atime||new Date,e.mtime,n)),this[xv](e)&&(a++,wn.default.chown(String(e.absolute),Number(this[kv](e)),Number(this[Qv](e)),n)),n()})}[rhe](e){e.unsupported=!0,this.warn("TAR_ENTRY_UNSUPPORTED",`unsupported entry type: ${e.type}`,{entry:e}),e.resume()}[ehe](e,t){let r=cn(ds.default.relative(this.cwd,ds.default.resolve(ds.default.dirname(String(e.absolute)),String(e.linkpath)))).split("/");this[Dv](e,this.cwd,r,()=>this[lT](e,String(e.linkpath),"symlink",t),s=>{this[Zs](s,e),t()})}[the](e,t){let r=cn(ds.default.resolve(this.cwd,String(e.linkpath))),s=cn(String(e.linkpath)).split("/");this[Dv](e,this.cwd,s,()=>this[lT](e,r,"link",t),a=>{this[Zs](a,e),t()})}[Dv](e,t,r,s,a){let n=r.shift();if(this.preservePaths||n===void 0)return s();let c=ds.default.resolve(t,n);wn.default.lstat(c,(f,p)=>{if(f)return s();if(p?.isSymbolicLink())return a(new CT(c,ds.default.resolve(c,r.join("/"))));this[Dv](e,c,r,s,a)})}[ihe](){this[XR]++}[RI](){this[XR]--,this[c6]()}[Q6](e){this[RI](),e.resume()}[P6](e,t){return e.type==="File"&&!this.unlink&&t.isFile()&&t.nlink<=1&&!Tv}[b6](e){this[ihe]();let t=[e.path];e.linkpath&&t.push(e.linkpath),this.reservations.reserve(t,r=>this[$pe](e,r))}[$pe](e,t){let r=c=>{t(c)},s=()=>{this[L0](this.cwd,this.dmode,c=>{if(c){this[Zs](c,e),r();return}this[Rv]=!0,a()})},a=()=>{if(e.absolute!==this.cwd){let c=cn(ds.default.dirname(String(e.absolute)));if(c!==this.cwd)return this[L0](c,this.dmode,f=>{if(f){this[Zs](f,e),r();return}n()})}n()},n=()=>{wn.default.lstat(String(e.absolute),(c,f)=>{if(f&&(this.keep||this.newer&&f.mtime>(e.mtime??f.mtime))){this[Q6](e),r();return}if(c||this[P6](e,f))return this[_c](null,e,r);if(f.isDirectory()){if(e.type==="Directory"){let p=this.chmod&&e.mode&&(f.mode&4095)!==e.mode,h=E=>this[_c](E??null,e,r);return p?wn.default.chmod(String(e.absolute),Number(e.mode),h):h()}if(e.absolute!==this.cwd)return wn.default.rmdir(String(e.absolute),p=>this[_c](p??null,e,r))}if(e.absolute===this.cwd)return this[_c](null,e,r);Vnt(String(e.absolute),p=>this[_c](p??null,e,r))})};this[Rv]?a():s()}[_c](e,t,r){if(e){this[Zs](e,t),r();return}switch(t.type){case"File":case"OldFile":case"ContiguousFile":return this[x6](t,r);case"Link":return this[the](t,r);case"SymbolicLink":return this[ehe](t,r);case"Directory":case"GNUDumpDir":return this[k6](t,r)}}[lT](e,t,r,s){wn.default[r](t,String(e.absolute),a=>{a?this[Zs](a,e):(this[RI](),e.resume()),s()})}},vv=e=>{try{return[null,e()]}catch(t){return[t,null]}},Ohe=class extends Y6{sync=!0;[_c](e,t){return super[_c](e,t,()=>{})}[b6](e){if(!this[Rv]){let a=this[L0](this.cwd,this.dmode);if(a)return this[Zs](a,e);this[Rv]=!0}if(e.absolute!==this.cwd){let a=cn(ds.default.dirname(String(e.absolute)));if(a!==this.cwd){let n=this[L0](a,this.dmode);if(n)return this[Zs](n,e)}}let[t,r]=vv(()=>wn.default.lstatSync(String(e.absolute)));if(r&&(this.keep||this.newer&&r.mtime>(e.mtime??r.mtime)))return this[Q6](e);if(t||this[P6](e,r))return this[_c](null,e);if(r.isDirectory()){if(e.type==="Directory"){let n=this.chmod&&e.mode&&(r.mode&4095)!==e.mode,[c]=n?vv(()=>{wn.default.chmodSync(String(e.absolute),Number(e.mode))}):[];return this[_c](c,e)}let[a]=vv(()=>wn.default.rmdirSync(String(e.absolute)));this[_c](a,e)}let[s]=e.absolute===this.cwd?[]:vv(()=>Jnt(String(e.absolute)));this[_c](s,e)}[x6](e,t){let r=typeof e.mode=="number"?e.mode&4095:this.fmode,s=c=>{let f;try{wn.default.closeSync(a)}catch(p){f=p}(c||f)&&this[Zs](c||f,e),t()},a;try{a=wn.default.openSync(String(e.absolute),khe(e.size),r)}catch(c){return s(c)}let n=this.transform&&this.transform(e)||e;n!==e&&(n.on("error",c=>this[Zs](c,e)),e.pipe(n)),n.on("data",c=>{try{wn.default.writeSync(a,c,0,c.length)}catch(f){s(f)}}),n.on("end",()=>{let c=null;if(e.mtime&&!this.noMtime){let f=e.atime||new Date,p=e.mtime;try{wn.default.futimesSync(a,f,p)}catch(h){try{wn.default.utimesSync(String(e.absolute),f,p)}catch{c=h}}}if(this[xv](e)){let f=this[kv](e),p=this[Qv](e);try{wn.default.fchownSync(a,Number(f),Number(p))}catch(h){try{wn.default.chownSync(String(e.absolute),Number(f),Number(p))}catch{c=c||h}}}s(c)})}[k6](e,t){let r=typeof e.mode=="number"?e.mode&4095:this.dmode,s=this[L0](String(e.absolute),r);if(s){this[Zs](s,e),t();return}if(e.mtime&&!this.noMtime)try{wn.default.utimesSync(String(e.absolute),e.atime||new Date,e.mtime)}catch{}if(this[xv](e))try{wn.default.chownSync(String(e.absolute),Number(this[kv](e)),Number(this[Qv](e)))}catch{}t(),e.resume()}[L0](e,t){try{return Mnt(cn(e),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cwd:this.cwd,mode:t})}catch(r){return r}}[Dv](e,t,r,s,a){if(this.preservePaths||!r.length)return s();let n=t;for(let c of r){n=ds.default.resolve(n,c);let[f,p]=vv(()=>wn.default.lstatSync(n));if(f)return s();if(p.isSymbolicLink())return a(new CT(n,ds.default.resolve(t,r.join("/"))))}s()}[lT](e,t,r,s){let a=`${r}Sync`;try{wn.default[a](t,String(e.absolute)),s(),e.resume()}catch(n){return this[Zs](n,e)}}},Knt=e=>{let t=new Ohe(e),r=e.file,s=j6.default.statSync(r),a=e.maxReadSize||16*1024*1024;new Ert(r,{readSize:a,size:s.size}).pipe(t)},znt=(e,t)=>{let r=new Y6(e),s=e.maxReadSize||16*1024*1024,a=e.file;return new Promise((n,c)=>{r.on("error",c),r.on("close",n),j6.default.stat(a,(f,p)=>{if(f)c(f);else{let h=new T6(a,{readSize:s,size:p.size});h.on("error",c),h.pipe(r)}})})},fUt=Ov(Knt,znt,e=>new Ohe(e),e=>new Y6(e),(e,t)=>{t?.length&&Ihe(e,t)}),Xnt=(e,t)=>{let r=new H6(e),s=!0,a,n;try{try{a=El.default.openSync(e.file,"r+")}catch(p){if(p?.code==="ENOENT")a=El.default.openSync(e.file,"w+");else throw p}let c=El.default.fstatSync(a),f=Buffer.alloc(512);e:for(n=0;nc.size)break;n+=h,e.mtimeCache&&p.mtime&&e.mtimeCache.set(String(p.path),p.mtime)}s=!1,Znt(e,r,n,a,t)}finally{if(s)try{El.default.closeSync(a)}catch{}}},Znt=(e,t,r,s,a)=>{let n=new che(e.file,{fd:s,start:r});t.pipe(n),eit(t,a)},$nt=(e,t)=>{t=Array.from(t);let r=new IT(e),s=(a,n,c)=>{let f=(S,x)=>{S?El.default.close(a,I=>c(S)):c(null,x)},p=0;if(n===0)return f(null,0);let h=0,E=Buffer.alloc(512),C=(S,x)=>{if(S||typeof x>"u")return f(S);if(h+=x,h<512&&x)return El.default.read(a,E,h,E.length-h,p+h,C);if(p===0&&E[0]===31&&E[1]===139)return f(new Error("cannot append to compressed archives"));if(h<512)return f(null,p);let I=new wm(E);if(!I.cksumValid)return f(null,p);let T=512*Math.ceil((I.size??0)/512);if(p+T+512>n||(p+=T+512,p>=n))return f(null,p);e.mtimeCache&&I.mtime&&e.mtimeCache.set(String(I.path),I.mtime),h=0,El.default.read(a,E,0,512,p,C)};El.default.read(a,E,0,512,p,C)};return new Promise((a,n)=>{r.on("error",n);let c="r+",f=(p,h)=>{if(p&&p.code==="ENOENT"&&c==="r+")return c="w+",El.default.open(e.file,c,f);if(p||!h)return n(p);El.default.fstat(h,(E,C)=>{if(E)return El.default.close(h,()=>n(E));s(h,C.size,(S,x)=>{if(S)return n(S);let I=new hT(e.file,{fd:h,start:x});r.pipe(I),I.on("error",n),I.on("close",a),tit(r,t)})})};El.default.open(e.file,c,f)})},eit=(e,t)=>{t.forEach(r=>{r.charAt(0)==="@"?yT({file:V6.default.resolve(e.cwd,r.slice(1)),sync:!0,noResume:!0,onReadEntry:s=>e.add(s)}):e.add(r)}),e.end()},tit=async(e,t)=>{for(let r=0;re.add(a)}):e.add(s)}e.end()},Sv=Ov(Xnt,$nt,()=>{throw new TypeError("file is required")},()=>{throw new TypeError("file is required")},(e,t)=>{if(!Srt(e))throw new TypeError("file is required");if(e.gzip||e.brotli||e.zstd||e.file.endsWith(".br")||e.file.endsWith(".tbr"))throw new TypeError("cannot append to compressed archives");if(!t?.length)throw new TypeError("no paths specified to add/replace")}),AUt=Ov(Sv.syncFile,Sv.asyncFile,Sv.syncNoFile,Sv.asyncNoFile,(e,t=[])=>{Sv.validate?.(e,t),rit(e)}),rit=e=>{let t=e.filter;e.mtimeCache||(e.mtimeCache=new Map),e.filter=t?(r,s)=>t(r,s)&&!((e.mtimeCache?.get(r)??s.mtime??0)>(s.mtime??0)):(r,s)=>!((e.mtimeCache?.get(r)??s.mtime??0)>(s.mtime??0))}});var J6,Mhe,_0,Mv,Uv,Uhe=Xe(()=>{J6=et(Ng()),Mhe=Ie("worker_threads"),_0=Symbol("kTaskInfo"),Mv=class{constructor(t,r){this.fn=t;this.limit=(0,J6.default)(r.poolSize)}run(t){return this.limit(()=>this.fn(t))}},Uv=class{constructor(t,r){this.source=t;this.workers=[];this.limit=(0,J6.default)(r.poolSize),this.cleanupInterval=setInterval(()=>{if(this.limit.pendingCount===0&&this.limit.activeCount===0){let s=this.workers.pop();s?s.terminate():clearInterval(this.cleanupInterval)}},5e3).unref()}createWorker(){this.cleanupInterval.refresh();let t=new Mhe.Worker(this.source,{eval:!0,execArgv:[...process.execArgv,"--unhandled-rejections=strict"]});return t.on("message",r=>{if(!t[_0])throw new Error("Assertion failed: Worker sent a result without having a task assigned");t[_0].resolve(r),t[_0]=null,t.unref(),this.workers.push(t)}),t.on("error",r=>{t[_0]?.reject(r),t[_0]=null}),t.on("exit",r=>{r!==0&&t[_0]?.reject(new Error(`Worker exited with code ${r}`)),t[_0]=null}),t}run(t){return this.limit(()=>{let r=this.workers.pop()??this.createWorker();return r.ref(),new Promise((s,a)=>{r[_0]={resolve:s,reject:a},r.postMessage(t)})})}}});var Hhe=G((JUt,_he)=>{var K6;_he.exports.getContent=()=>(typeof K6>"u"&&(K6=Ie("zlib").brotliDecompressSync(Buffer.from("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","base64")).toString()),K6)});var gs={};Vt(gs,{convertToZip:()=>ait,convertToZipWorker:()=>Z6,extractArchiveTo:()=>Whe,getDefaultTaskPool:()=>Ghe,getTaskPoolForConfiguration:()=>qhe,makeArchiveFromDirectory:()=>oit});function iit(e,t){switch(e){case"async":return new Mv(Z6,{poolSize:t});case"workers":return new Uv((0,X6.getContent)(),{poolSize:t});default:throw new Error(`Assertion failed: Unknown value ${e} for taskPoolMode`)}}function Ghe(){return typeof z6>"u"&&(z6=iit("workers",Ui.availableParallelism())),z6}function qhe(e){return typeof e>"u"?Ghe():Zl(sit,e,()=>{let t=e.get("taskPoolMode"),r=e.get("taskPoolConcurrency");switch(t){case"async":return new Mv(Z6,{poolSize:r});case"workers":return new Uv((0,X6.getContent)(),{poolSize:r});default:throw new Error(`Assertion failed: Unknown value ${t} for taskPoolMode`)}})}async function Z6(e){let{tmpFile:t,tgz:r,compressionLevel:s,extractBufferOpts:a}=e,n=new ps(t,{create:!0,level:s,stats:al.makeDefaultStats()}),c=Buffer.from(r.buffer,r.byteOffset,r.byteLength);return await Whe(c,n,a),n.saveAndClose(),t}async function oit(e,{baseFs:t=new Vn,prefixPath:r=vt.root,compressionLevel:s,inMemory:a=!1}={}){let n;if(a)n=new ps(null,{level:s});else{let f=await le.mktempPromise(),p=J.join(f,"archive.zip");n=new ps(p,{create:!0,level:s})}let c=J.resolve(vt.root,r);return await n.copyPromise(c,e,{baseFs:t,stableTime:!0,stableSort:!0}),n}async function ait(e,t={}){let r=await le.mktempPromise(),s=J.join(r,"archive.zip"),a=t.compressionLevel??t.configuration?.get("compressionLevel")??"mixed",n={prefixPath:t.prefixPath,stripComponents:t.stripComponents};return await(t.taskPool??qhe(t.configuration)).run({tmpFile:s,tgz:e,compressionLevel:a,extractBufferOpts:n}),new ps(s,{level:t.compressionLevel})}async function*lit(e){let t=new Bm,r=new jhe.PassThrough({objectMode:!0,autoDestroy:!0,emitClose:!0});t.on("entry",s=>{r.write(s)}),t.on("error",s=>{r.destroy(s)}),t.on("close",()=>{r.destroyed||r.end()}),t.end(e);for await(let s of r){let a=s;yield a,a.resume()}}async function Whe(e,t,{stripComponents:r=0,prefixPath:s=vt.dot}={}){function a(n){if(n.path[0]==="/")return!0;let c=n.path.split(/\//g);return!!(c.some(f=>f==="..")||c.length<=r)}for await(let n of lit(e)){if(a(n))continue;let c=J.normalize(fe.toPortablePath(n.path)).replace(/\/$/,"").split(/\//g);if(c.length<=r)continue;let f=c.slice(r).join("/"),p=J.join(s,f),h=420;switch((n.type==="Directory"||(n.mode??0)&73)&&(h|=73),n.type){case"Directory":t.mkdirpSync(J.dirname(p),{chmod:493,utimes:[Ai.SAFE_TIME,Ai.SAFE_TIME]}),t.mkdirSync(p,{mode:h}),t.utimesSync(p,Ai.SAFE_TIME,Ai.SAFE_TIME);break;case"OldFile":case"File":t.mkdirpSync(J.dirname(p),{chmod:493,utimes:[Ai.SAFE_TIME,Ai.SAFE_TIME]}),t.writeFileSync(p,await JE(n),{mode:h}),t.utimesSync(p,Ai.SAFE_TIME,Ai.SAFE_TIME);break;case"SymbolicLink":t.mkdirpSync(J.dirname(p),{chmod:493,utimes:[Ai.SAFE_TIME,Ai.SAFE_TIME]}),t.symlinkSync(n.linkpath,p),t.lutimesSync(p,Ai.SAFE_TIME,Ai.SAFE_TIME);break}}return t}var jhe,X6,z6,sit,Yhe=Xe(()=>{qe();Dt();nA();jhe=Ie("stream");Lhe();Uhe();kc();X6=et(Hhe());sit=new WeakMap});var Jhe=G(($6,Vhe)=>{(function(e,t){typeof $6=="object"?Vhe.exports=t():typeof define=="function"&&define.amd?define(t):e.treeify=t()})($6,function(){function e(a,n){var c=n?"\u2514":"\u251C";return a?c+="\u2500 ":c+="\u2500\u2500\u2510",c}function t(a,n){var c=[];for(var f in a)a.hasOwnProperty(f)&&(n&&typeof a[f]=="function"||c.push(f));return c}function r(a,n,c,f,p,h,E){var C="",S=0,x,I,T=f.slice(0);if(T.push([n,c])&&f.length>0&&(f.forEach(function(U,V){V>0&&(C+=(U[1]?" ":"\u2502")+" "),!I&&U[0]===n&&(I=!0)}),C+=e(a,c)+a,p&&(typeof n!="object"||n instanceof Date)&&(C+=": "+n),I&&(C+=" (circular ref.)"),E(C)),!I&&typeof n=="object"){var O=t(n,h);O.forEach(function(U){x=++S===O.length,r(U,n[U],x,T,p,h,E)})}}var s={};return s.asLines=function(a,n,c,f){var p=typeof c!="function"?c:!1;r(".",a,!1,[],n,p,f||c)},s.asTree=function(a,n,c){var f="";return r(".",a,!1,[],n,c,function(p){f+=p+` `}),f},s})});var Rs={};Vt(Rs,{emitList:()=>cit,emitTree:()=>Zhe,treeNodeToJson:()=>Xhe,treeNodeToTreeify:()=>zhe});function zhe(e,{configuration:t}){let r={},s=0,a=(n,c)=>{let f=Array.isArray(n)?n.entries():Object.entries(n);for(let[p,h]of f){if(!h)continue;let{label:E,value:C,children:S}=h,x=[];typeof E<"u"&&x.push(Jg(t,E,2)),typeof C<"u"&&x.push(jt(t,C[0],C[1])),x.length===0&&x.push(Jg(t,`${p}`,2));let I=x.join(": ").trim(),T=`\0${s++}\0`,O=c[`${T}${I}`]={};typeof S<"u"&&a(S,O)}};if(typeof e.children>"u")throw new Error("The root node must only contain children");return a(e.children,r),r}function Xhe(e){let t=r=>{if(typeof r.children>"u"){if(typeof r.value>"u")throw new Error("Assertion failed: Expected a value to be set if the children are missing");return Kg(r.value[0],r.value[1])}let s=Array.isArray(r.children)?r.children.entries():Object.entries(r.children??{}),a=Array.isArray(r.children)?[]:{};for(let[n,c]of s)c&&(a[uit(n)]=t(c));return typeof r.value>"u"?a:{value:Kg(r.value[0],r.value[1]),children:a}};return t(e)}function cit(e,{configuration:t,stdout:r,json:s}){let a=e.map(n=>({value:n}));Zhe({children:a},{configuration:t,stdout:r,json:s})}function Zhe(e,{configuration:t,stdout:r,json:s,separators:a=0}){if(s){let c=Array.isArray(e.children)?e.children.values():Object.values(e.children??{});for(let f of c)f&&r.write(`${JSON.stringify(Xhe(f))} `);return}let n=(0,Khe.asTree)(zhe(e,{configuration:t}),!1,!1);if(n=n.replace(/\0[0-9]+\0/g,""),a>=1&&(n=n.replace(/^([├└]─)/gm,`\u2502 $1`).replace(/^│\n/,"")),a>=2)for(let c=0;c<2;++c)n=n.replace(/^([│ ].{2}[├│ ].{2}[^\n]+\n)(([│ ]).{2}[├└].{2}[^\n]*\n[│ ].{2}[│ ].{2}[├└]─)/gm,`$1$3 \u2502 $2`).replace(/^│\n/,"");if(a>=3)throw new Error("Only the first two levels are accepted by treeUtils.emitTree");r.write(n)}function uit(e){return typeof e=="string"?e.replace(/^\0[0-9]+\0/,""):e}var Khe,$he=Xe(()=>{Khe=et(Jhe());Qc()});var wT,e0e=Xe(()=>{wT=class{constructor(t){this.releaseFunction=t;this.map=new Map}addOrCreate(t,r){let s=this.map.get(t);if(typeof s<"u"){if(s.refCount<=0)throw new Error(`Race condition in RefCountedMap. While adding a new key the refCount is: ${s.refCount} for ${JSON.stringify(t)}`);return s.refCount++,{value:s.value,release:()=>this.release(t)}}else{let a=r();return this.map.set(t,{refCount:1,value:a}),{value:a,release:()=>this.release(t)}}}release(t){let r=this.map.get(t);if(!r)throw new Error(`Unbalanced calls to release. No known instances of: ${JSON.stringify(t)}`);let s=r.refCount;if(s<=0)throw new Error(`Unbalanced calls to release. Too many release vs alloc refcount would become: ${s-1} of ${JSON.stringify(t)}`);s==1?(this.map.delete(t),this.releaseFunction(r.value)):r.refCount--}}});function _v(e){let t=e.match(fit);if(!t?.groups)throw new Error("Assertion failed: Expected the checksum to match the requested pattern");let r=t.groups.cacheVersion?parseInt(t.groups.cacheVersion):null;return{cacheKey:t.groups.cacheKey??null,cacheVersion:r,cacheSpec:t.groups.cacheSpec??null,hash:t.groups.hash}}var t0e,eG,tG,BT,Kr,fit,rG=Xe(()=>{qe();Dt();Dt();nA();t0e=Ie("crypto"),eG=et(Ie("fs"));e0e();Fc();E0();kc();Zo();tG=KE(process.env.YARN_CACHE_CHECKPOINT_OVERRIDE??process.env.YARN_CACHE_VERSION_OVERRIDE??9),BT=KE(process.env.YARN_CACHE_VERSION_OVERRIDE??10),Kr=class e{constructor(t,{configuration:r,immutable:s=r.get("enableImmutableCache"),check:a=!1}){this.markedFiles=new Set;this.mutexes=new Map;this.refCountedZipFsCache=new wT(t=>{t.discardAndClose()});this.cacheId=`-${(0,t0e.randomBytes)(8).toString("hex")}.tmp`;this.configuration=r,this.cwd=t,this.immutable=s,this.check=a;let{cacheSpec:n,cacheKey:c}=e.getCacheKey(r);this.cacheSpec=n,this.cacheKey=c}static async find(t,{immutable:r,check:s}={}){let a=new e(t.get("cacheFolder"),{configuration:t,immutable:r,check:s});return await a.setup(),a}static getCacheKey(t){let r=t.get("compressionLevel"),s=r!=="mixed"?`c${r}`:"";return{cacheKey:[BT,s].join(""),cacheSpec:s}}get mirrorCwd(){if(!this.configuration.get("enableMirror"))return null;let t=`${this.configuration.get("globalFolder")}/cache`;return t!==this.cwd?t:null}getVersionFilename(t){return`${oI(t)}-${this.cacheKey}.zip`}getChecksumFilename(t,r){let a=_v(r).hash.slice(0,10);return`${oI(t)}-${a}.zip`}isChecksumCompatible(t){if(t===null)return!1;let{cacheVersion:r,cacheSpec:s}=_v(t);if(r===null||r{let Ae=new ps,Ce=J.join(vt.root,d8(t));return Ae.mkdirSync(Ce,{recursive:!0}),Ae.writeJsonSync(J.join(Ce,Er.manifest),{name:fn(t),mocked:!0}),Ae},E=async(Ae,{isColdHit:Ce,controlPath:Ee=null})=>{if(Ee===null&&c.unstablePackages?.has(t.locatorHash))return{isValid:!0,hash:null};let d=r&&!Ce?_v(r).cacheKey:this.cacheKey,Se=!c.skipIntegrityCheck||!r?`${d}/${await vQ(Ae)}`:r;if(Ee!==null){let me=!c.skipIntegrityCheck||!r?`${this.cacheKey}/${await vQ(Ee)}`:r;if(Se!==me)throw new Lt(18,"The remote archive doesn't match the local checksum - has the local cache been corrupted?")}let Be=null;switch(r!==null&&Se!==r&&(this.check?Be="throw":_v(r).cacheKey!==_v(Se).cacheKey?Be="update":Be=this.configuration.get("checksumBehavior")),Be){case null:case"update":return{isValid:!0,hash:Se};case"ignore":return{isValid:!0,hash:r};case"reset":return{isValid:!1,hash:r};default:case"throw":throw new Lt(18,"The remote archive doesn't match the expected checksum")}},C=async Ae=>{if(!n)throw new Error(`Cache check required but no loader configured for ${Yr(this.configuration,t)}`);let Ce=await n(),Ee=Ce.getRealPath();Ce.saveAndClose(),await le.chmodPromise(Ee,420);let d=await E(Ae,{controlPath:Ee,isColdHit:!1});if(!d.isValid)throw new Error("Assertion failed: Expected a valid checksum");return d.hash},S=async()=>{if(f===null||!await le.existsPromise(f)){let Ae=await n(),Ce=Ae.getRealPath();return Ae.saveAndClose(),{source:"loader",path:Ce}}return{source:"mirror",path:f}},x=async()=>{if(!n)throw new Error(`Cache entry required but missing for ${Yr(this.configuration,t)}`);if(this.immutable)throw new Lt(56,`Cache entry required but missing for ${Yr(this.configuration,t)}`);let{path:Ae,source:Ce}=await S(),{hash:Ee}=await E(Ae,{isColdHit:!0}),d=this.getLocatorPath(t,Ee),Se=[];Ce!=="mirror"&&f!==null&&Se.push(async()=>{let me=`${f}${this.cacheId}`;await le.copyFilePromise(Ae,me,eG.default.constants.COPYFILE_FICLONE),await le.chmodPromise(me,420),await le.renamePromise(me,f)}),(!c.mirrorWriteOnly||f===null)&&Se.push(async()=>{let me=`${d}${this.cacheId}`;await le.copyFilePromise(Ae,me,eG.default.constants.COPYFILE_FICLONE),await le.chmodPromise(me,420),await le.renamePromise(me,d)});let Be=c.mirrorWriteOnly?f??d:d;return await Promise.all(Se.map(me=>me())),[!1,Be,Ee]},I=async()=>{let Ce=(async()=>{let Ee=c.unstablePackages?.has(t.locatorHash),d=Ee||!r||this.isChecksumCompatible(r)?this.getLocatorPath(t,r):null,Se=d!==null?this.markedFiles.has(d)||await p.existsPromise(d):!1,Be=!!c.mockedPackages?.has(t.locatorHash)&&(!this.check||!Se),me=Be||Se,ce=me?s:a;if(ce&&ce(),me){let Z=null,De=d;if(!Be)if(this.check)Z=await C(De);else{let Qe=await E(De,{isColdHit:!1});if(Qe.isValid)Z=Qe.hash;else return x()}return[Be,De,Z]}else{if(this.immutable&&Ee)throw new Lt(56,`Cache entry required but missing for ${Yr(this.configuration,t)}; consider defining ${pe.pretty(this.configuration,"supportedArchitectures",pe.Type.CODE)} to cache packages for multiple systems`);return x()}})();this.mutexes.set(t.locatorHash,Ce);try{return await Ce}finally{this.mutexes.delete(t.locatorHash)}};for(let Ae;Ae=this.mutexes.get(t.locatorHash);)await Ae;let[T,O,U]=await I();T||this.markedFiles.add(O);let V=()=>this.refCountedZipFsCache.addOrCreate(O,()=>T?h():new ps(O,{baseFs:p,readOnly:!0})),te,ie=new cE(()=>x4(()=>(te=V(),te.value),Ae=>`Failed to open the cache entry for ${Yr(this.configuration,t)}: ${Ae}`),J),ue=new Gf(O,{baseFs:ie,pathUtils:J}),ae=()=>{te?.release()},ge=c.unstablePackages?.has(t.locatorHash)?null:U;return[ue,ae,ge]}},fit=/^(?:(?(?[0-9]+)(?.*))\/)?(?.*)$/});var vT,r0e=Xe(()=>{vT=(r=>(r[r.SCRIPT=0]="SCRIPT",r[r.SHELLCODE=1]="SHELLCODE",r))(vT||{})});var Ait,_I,nG=Xe(()=>{Dt();vc();Np();Zo();Ait=[[/^(git(?:\+(?:https|ssh))?:\/\/.*(?:\.git)?)#(.*)$/,(e,t,r,s)=>`${r}#commit=${s}`],[/^https:\/\/((?:[^/]+?)@)?codeload\.github\.com\/([^/]+\/[^/]+)\/tar\.gz\/([0-9a-f]+)$/,(e,t,r="",s,a)=>`https://${r}github.com/${s}.git#commit=${a}`],[/^https:\/\/((?:[^/]+?)@)?github\.com\/([^/]+\/[^/]+?)(?:\.git)?#([0-9a-f]+)$/,(e,t,r="",s,a)=>`https://${r}github.com/${s}.git#commit=${a}`],[/^https?:\/\/[^/]+\/(?:[^/]+\/)*(?:@.+(?:\/|(?:%2f)))?([^/]+)\/(?:-|download)\/\1-[^/]+\.tgz(?:#|$)/,e=>`npm:${e}`],[/^https:\/\/npm\.pkg\.github\.com\/download\/(?:@[^/]+)\/(?:[^/]+)\/(?:[^/]+)\/(?:[0-9a-f]+)(?:#|$)/,e=>`npm:${e}`],[/^https:\/\/npm\.fontawesome\.com\/(?:@[^/]+)\/([^/]+)\/-\/([^/]+)\/\1-\2.tgz(?:#|$)/,e=>`npm:${e}`],[/^https?:\/\/[^/]+\/.*\/(@[^/]+)\/([^/]+)\/-\/\1\/\2-(?:[.\d\w-]+)\.tgz(?:#|$)/,(e,t)=>xQ({protocol:"npm:",source:null,selector:e,params:{__archiveUrl:t}})],[/^[^/]+\.tgz#[0-9a-f]+$/,e=>`npm:${e}`]],_I=class{constructor(t){this.resolver=t;this.resolutions=null}async setup(t,{report:r}){let s=J.join(t.cwd,Er.lockfile);if(!le.existsSync(s))return;let a=await le.readFilePromise(s,"utf8"),n=cs(a);if(Object.hasOwn(n,"__metadata"))return;let c=this.resolutions=new Map;for(let f of Object.keys(n)){let p=TB(f);if(!p){r.reportWarning(14,`Failed to parse the string "${f}" into a proper descriptor`);continue}let h=yl(p.range)?Mn(p,`npm:${p.range}`):p,{version:E,resolved:C}=n[f];if(!C)continue;let S;for(let[I,T]of Ait){let O=C.match(I);if(O){S=T(E,...O);break}}if(!S){r.reportWarning(14,`${oi(t.configuration,h)}: Only some patterns can be imported from legacy lockfiles (not "${C}")`);continue}let x=h;try{let I=Zg(h.range),T=TB(I.selector,!0);T&&(x=T)}catch{}c.set(h.descriptorHash,Js(x,S))}}supportsDescriptor(t,r){return this.resolutions?this.resolutions.has(t.descriptorHash):!1}supportsLocator(t,r){return!1}shouldPersistResolution(t,r){throw new Error("Assertion failed: This resolver doesn't support resolving locators to packages")}bindDescriptor(t,r,s){return t}getResolutionDependencies(t,r){return{}}async getCandidates(t,r,s){if(!this.resolutions)throw new Error("Assertion failed: The resolution store should have been setup");let a=this.resolutions.get(t.descriptorHash);if(!a)throw new Error("Assertion failed: The resolution should have been registered");let n=f8(a),c=s.project.configuration.normalizeDependency(n);return await this.resolver.getCandidates(c,r,s)}async getSatisfying(t,r,s,a){let[n]=await this.getCandidates(t,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(t,r){throw new Error("Assertion failed: This resolver doesn't support resolving locators to packages")}}});var uA,n0e=Xe(()=>{Fc();fv();Qc();uA=class extends yo{constructor({configuration:r,stdout:s,suggestInstall:a=!0}){super();this.errorCount=0;SB(this,{configuration:r}),this.configuration=r,this.stdout=s,this.suggestInstall=a}static async start(r,s){let a=new this(r);try{await s(a)}catch(n){a.reportExceptionOnce(n)}finally{await a.finalize()}return a}hasErrors(){return this.errorCount>0}exitCode(){return this.hasErrors()?1:0}reportCacheHit(r){}reportCacheMiss(r){}startSectionSync(r,s){return s()}async startSectionPromise(r,s){return await s()}startTimerSync(r,s,a){return(typeof s=="function"?s:a)()}async startTimerPromise(r,s,a){return await(typeof s=="function"?s:a)()}reportSeparator(){}reportInfo(r,s){}reportWarning(r,s){}reportError(r,s){this.errorCount+=1,this.stdout.write(`${jt(this.configuration,"\u27A4","redBright")} ${this.formatNameWithHyperlink(r)}: ${s} `)}reportProgress(r){return{...Promise.resolve().then(async()=>{for await(let{}of r);}),stop:()=>{}}}reportJson(r){}reportFold(r,s){}async finalize(){this.errorCount>0&&(this.stdout.write(` `),this.stdout.write(`${jt(this.configuration,"\u27A4","redBright")} Errors happened when preparing the environment required to run this command. `),this.suggestInstall&&this.stdout.write(`${jt(this.configuration,"\u27A4","redBright")} This might be caused by packages being missing from the lockfile, in which case running "yarn install" might help. `))}formatNameWithHyperlink(r){return Qj(r,{configuration:this.configuration,json:!1})}}});var HI,iG=Xe(()=>{Zo();HI=class{constructor(t){this.resolver=t}supportsDescriptor(t,r){return!!(r.project.storedResolutions.get(t.descriptorHash)||r.project.originalPackages.has(DQ(t).locatorHash))}supportsLocator(t,r){return!!(r.project.originalPackages.has(t.locatorHash)&&!r.project.lockfileNeedsRefresh)}shouldPersistResolution(t,r){throw new Error("The shouldPersistResolution method shouldn't be called on the lockfile resolver, which would always answer yes")}bindDescriptor(t,r,s){return t}getResolutionDependencies(t,r){return this.resolver.getResolutionDependencies(t,r)}async getCandidates(t,r,s){let a=s.project.storedResolutions.get(t.descriptorHash);if(a){let c=s.project.originalPackages.get(a);if(c)return[c]}let n=s.project.originalPackages.get(DQ(t).locatorHash);if(n)return[n];throw new Error("Resolution expected from the lockfile data")}async getSatisfying(t,r,s,a){let[n]=await this.getCandidates(t,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(t,r){let s=r.project.originalPackages.get(t.locatorHash);if(!s)throw new Error("The lockfile resolver isn't meant to resolve packages - they should already have been stored into a cache");return s}}});function Jp(){}function pit(e,t,r,s,a){for(var n=0,c=t.length,f=0,p=0;nx.length?T:x}),h.value=e.join(E)}else h.value=e.join(r.slice(f,f+h.count));f+=h.count,h.added||(p+=h.count)}}var S=t[c-1];return c>1&&typeof S.value=="string"&&(S.added||S.removed)&&e.equals("",S.value)&&(t[c-2].value+=S.value,t.pop()),t}function hit(e){return{newPos:e.newPos,components:e.components.slice(0)}}function dit(e,t){if(typeof e=="function")t.callback=e;else if(e)for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);return t}function o0e(e,t,r){return r=dit(r,{ignoreWhitespace:!0}),cG.diff(e,t,r)}function git(e,t,r){return uG.diff(e,t,r)}function ST(e){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?ST=function(t){return typeof t}:ST=function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},ST(e)}function sG(e){return Eit(e)||Iit(e)||Cit(e)||wit()}function Eit(e){if(Array.isArray(e))return oG(e)}function Iit(e){if(typeof Symbol<"u"&&Symbol.iterator in Object(e))return Array.from(e)}function Cit(e,t){if(e){if(typeof e=="string")return oG(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if(r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set")return Array.from(e);if(r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return oG(e,t)}}function oG(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,s=new Array(t);r"u"&&(c.context=4);var f=git(r,s,c);if(!f)return;f.push({value:"",lines:[]});function p(U){return U.map(function(V){return" "+V})}for(var h=[],E=0,C=0,S=[],x=1,I=1,T=function(V){var te=f[V],ie=te.lines||te.value.replace(/\n$/,"").split(` `);if(te.lines=ie,te.added||te.removed){var ue;if(!E){var ae=f[V-1];E=x,C=I,ae&&(S=c.context>0?p(ae.lines.slice(-c.context)):[],E-=S.length,C-=S.length)}(ue=S).push.apply(ue,sG(ie.map(function(me){return(te.added?"+":"-")+me}))),te.added?I+=ie.length:x+=ie.length}else{if(E)if(ie.length<=c.context*2&&V=f.length-2&&ie.length<=c.context){var d=/\n$/.test(r),Se=/\n$/.test(s),Be=ie.length==0&&S.length>Ee.oldLines;!d&&Be&&r.length>0&&S.splice(Ee.oldLines,0,"\\ No newline at end of file"),(!d&&!Be||!Se)&&S.push("\\ No newline at end of file")}h.push(Ee),E=0,C=0,S=[]}x+=ie.length,I+=ie.length}},O=0;O{Jp.prototype={diff:function(t,r){var s=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{},a=s.callback;typeof s=="function"&&(a=s,s={}),this.options=s;var n=this;function c(T){return a?(setTimeout(function(){a(void 0,T)},0),!0):T}t=this.castInput(t),r=this.castInput(r),t=this.removeEmpty(this.tokenize(t)),r=this.removeEmpty(this.tokenize(r));var f=r.length,p=t.length,h=1,E=f+p;s.maxEditLength&&(E=Math.min(E,s.maxEditLength));var C=[{newPos:-1,components:[]}],S=this.extractCommon(C[0],r,t,0);if(C[0].newPos+1>=f&&S+1>=p)return c([{value:this.join(r),count:r.length}]);function x(){for(var T=-1*h;T<=h;T+=2){var O=void 0,U=C[T-1],V=C[T+1],te=(V?V.newPos:0)-T;U&&(C[T-1]=void 0);var ie=U&&U.newPos+1=f&&te+1>=p)return c(pit(n,O.components,r,t,n.useLongestToken));C[T]=O}h++}if(a)(function T(){setTimeout(function(){if(h>E)return a();x()||T()},0)})();else for(;h<=E;){var I=x();if(I)return I}},pushComponent:function(t,r,s){var a=t[t.length-1];a&&a.added===r&&a.removed===s?t[t.length-1]={count:a.count+1,added:r,removed:s}:t.push({count:1,added:r,removed:s})},extractCommon:function(t,r,s,a){for(var n=r.length,c=s.length,f=t.newPos,p=f-a,h=0;f+1"u"?r:c}:s;return typeof e=="string"?e:JSON.stringify(aG(e,null,null,a),a," ")};Hv.equals=function(e,t){return Jp.prototype.equals.call(Hv,e.replace(/,([\r\n])/g,"$1"),t.replace(/,([\r\n])/g,"$1"))};lG=new Jp;lG.tokenize=function(e){return e.slice()};lG.join=lG.removeEmpty=function(e){return e}});var DT,l0e=Xe(()=>{Fc();DT=class{constructor(t){this.resolver=t}supportsDescriptor(t,r){return this.resolver.supportsDescriptor(t,r)}supportsLocator(t,r){return this.resolver.supportsLocator(t,r)}shouldPersistResolution(t,r){return this.resolver.shouldPersistResolution(t,r)}bindDescriptor(t,r,s){return this.resolver.bindDescriptor(t,r,s)}getResolutionDependencies(t,r){return this.resolver.getResolutionDependencies(t,r)}async getCandidates(t,r,s){throw new Lt(20,`This package doesn't seem to be present in your lockfile; run "yarn install" to update the lockfile`)}async getSatisfying(t,r,s,a){throw new Lt(20,`This package doesn't seem to be present in your lockfile; run "yarn install" to update the lockfile`)}async resolve(t,r){throw new Lt(20,`This package doesn't seem to be present in your lockfile; run "yarn install" to update the lockfile`)}}});var ki,AG=Xe(()=>{Fc();ki=class extends yo{reportCacheHit(t){}reportCacheMiss(t){}startSectionSync(t,r){return r()}async startSectionPromise(t,r){return await r()}startTimerSync(t,r,s){return(typeof r=="function"?r:s)()}async startTimerPromise(t,r,s){return await(typeof r=="function"?r:s)()}reportSeparator(){}reportInfo(t,r){}reportWarning(t,r){}reportError(t,r){}reportProgress(t){return{...Promise.resolve().then(async()=>{for await(let{}of t);}),stop:()=>{}}}reportJson(t){}reportFold(t,r){}async finalize(){}}});var c0e,jI,pG=Xe(()=>{Dt();c0e=et(wQ());cI();$g();Qc();E0();Np();Zo();jI=class{constructor(t,{project:r}){this.workspacesCwds=new Set;this.project=r,this.cwd=t}async setup(){this.manifest=await _t.tryFind(this.cwd)??new _t,this.relativeCwd=J.relative(this.project.cwd,this.cwd)||vt.dot;let t=this.manifest.name?this.manifest.name:ka(null,`${this.computeCandidateName()}-${fs(this.relativeCwd).substring(0,6)}`);this.anchoredDescriptor=Mn(t,`${Ii.protocol}${this.relativeCwd}`),this.anchoredLocator=Js(t,`${Ii.protocol}${this.relativeCwd}`);let r=this.manifest.workspaceDefinitions.map(({pattern:a})=>a);if(r.length===0)return;let s=await(0,c0e.default)(r,{cwd:fe.fromPortablePath(this.cwd),onlyDirectories:!0,ignore:["**/node_modules","**/.git","**/.yarn"]});s.sort(),await s.reduce(async(a,n)=>{let c=J.resolve(this.cwd,fe.toPortablePath(n)),f=await le.existsPromise(J.join(c,"package.json"));await a,f&&this.workspacesCwds.add(c)},Promise.resolve())}get anchoredPackage(){let t=this.project.storedPackages.get(this.anchoredLocator.locatorHash);if(!t)throw new Error(`Assertion failed: Expected workspace ${NB(this.project.configuration,this)} (${jt(this.project.configuration,J.join(this.cwd,Er.manifest),dt.PATH)}) to have been resolved. Run "yarn install" to update the lockfile`);return t}accepts(t){let r=t.indexOf(":"),s=r!==-1?t.slice(0,r+1):null,a=r!==-1?t.slice(r+1):t;if(s===Ii.protocol&&J.normalize(a)===this.relativeCwd||s===Ii.protocol&&(a==="*"||a==="^"||a==="~"))return!0;let n=yl(a);return n?s===Ii.protocol?n.test(this.manifest.version??"0.0.0"):this.project.configuration.get("enableTransparentWorkspaces")&&this.manifest.version!==null?n.test(this.manifest.version):!1:!1}computeCandidateName(){return this.cwd===this.project.cwd?"root-workspace":`${J.basename(this.cwd)}`||"unnamed-workspace"}getRecursiveWorkspaceDependencies({dependencies:t=_t.hardDependencies}={}){let r=new Set,s=a=>{for(let n of t)for(let c of a.manifest[n].values()){let f=this.project.tryWorkspaceByDescriptor(c);f===null||r.has(f)||(r.add(f),s(f))}};return s(this),r}getRecursiveWorkspaceDependents({dependencies:t=_t.hardDependencies}={}){let r=new Set,s=a=>{for(let n of this.project.workspaces)t.some(f=>[...n.manifest[f].values()].some(p=>{let h=this.project.tryWorkspaceByDescriptor(p);return h!==null&&RB(h.anchoredLocator,a.anchoredLocator)}))&&!r.has(n)&&(r.add(n),s(n))};return s(this),r}getRecursiveWorkspaceChildren(){let t=new Set([this]);for(let r of t)for(let s of r.workspacesCwds){let a=this.project.workspacesByCwd.get(s);a&&t.add(a)}return t.delete(this),Array.from(t)}async persistManifest(){let t={};this.manifest.exportTo(t);let r=J.join(this.cwd,_t.fileName),s=`${JSON.stringify(t,null,this.manifest.indent)} `;await le.changeFilePromise(r,s,{automaticNewlines:!0}),this.manifest.raw=t}}});function xit({project:e,allDescriptors:t,allResolutions:r,allPackages:s,accessibleLocators:a=new Set,optionalBuilds:n=new Set,peerRequirements:c=new Map,peerWarnings:f=[],peerRequirementNodes:p=new Map,volatileDescriptors:h=new Set}){let E=new Map,C=[],S=new Map,x=new Map,I=new Map,T=new Map,O=new Map,U=new Map(e.workspaces.map(ae=>{let ge=ae.anchoredLocator.locatorHash,Ae=s.get(ge);if(typeof Ae>"u")throw new Error("Assertion failed: The workspace should have an associated package");return[ge,xB(Ae)]})),V=()=>{let ae=le.mktempSync(),ge=J.join(ae,"stacktrace.log"),Ae=String(C.length+1).length,Ce=C.map((Ee,d)=>`${`${d+1}.`.padStart(Ae," ")} ${ml(Ee)} `).join("");throw le.writeFileSync(ge,Ce),le.detachTemp(ae),new Lt(45,`Encountered a stack overflow when resolving peer dependencies; cf ${fe.fromPortablePath(ge)}`)},te=ae=>{let ge=r.get(ae.descriptorHash);if(typeof ge>"u")throw new Error("Assertion failed: The resolution should have been registered");let Ae=s.get(ge);if(!Ae)throw new Error("Assertion failed: The package could not be found");return Ae},ie=(ae,ge,Ae,{top:Ce,optional:Ee})=>{C.length>1e3&&V(),C.push(ge);let d=ue(ae,ge,Ae,{top:Ce,optional:Ee});return C.pop(),d},ue=(ae,ge,Ae,{top:Ce,optional:Ee})=>{if(Ee||n.delete(ge.locatorHash),a.has(ge.locatorHash))return;a.add(ge.locatorHash);let d=s.get(ge.locatorHash);if(!d)throw new Error(`Assertion failed: The package (${Yr(e.configuration,ge)}) should have been registered`);let Se=new Set,Be=new Map,me=[],ce=[],Z=[],De=[];for(let Qe of Array.from(d.dependencies.values())){if(d.peerDependencies.has(Qe.identHash)&&d.locatorHash!==Ce)continue;if(Rp(Qe))throw new Error("Assertion failed: Virtual packages shouldn't be encountered when virtualizing a branch");h.delete(Qe.descriptorHash);let st=Ee;if(!st){let Re=d.dependenciesMeta.get(fn(Qe));if(typeof Re<"u"){let ct=Re.get(null);typeof ct<"u"&&ct.optional&&(st=!0)}}let _=r.get(Qe.descriptorHash);if(!_)throw new Error(`Assertion failed: The resolution (${oi(e.configuration,Qe)}) should have been registered`);let tt=U.get(_)||s.get(_);if(!tt)throw new Error(`Assertion failed: The package (${_}, resolved from ${oi(e.configuration,Qe)}) should have been registered`);if(tt.peerDependencies.size===0){ie(Qe,tt,new Map,{top:Ce,optional:st});continue}let Ne,ke,be=new Set,je=new Map;me.push(()=>{Ne=p8(Qe,ge.locatorHash),ke=h8(tt,ge.locatorHash),d.dependencies.set(Qe.identHash,Ne),r.set(Ne.descriptorHash,ke.locatorHash),t.set(Ne.descriptorHash,Ne),s.set(ke.locatorHash,ke),xp(T,ke.locatorHash).add(Ne.descriptorHash),Se.add(ke.locatorHash)}),ce.push(()=>{O.set(ke.locatorHash,je);for(let Re of ke.peerDependencies.values()){let Me=Zl(Be,Re.identHash,()=>{let P=Ae.get(Re.identHash)??null,w=d.dependencies.get(Re.identHash);return!w&&QB(ge,Re)&&(ae.identHash===ge.identHash?w=ae:(w=Mn(ge,ae.range),t.set(w.descriptorHash,w),r.set(w.descriptorHash,ge.locatorHash),h.delete(w.descriptorHash),P=null)),w||(w=Mn(Re,"missing:")),{subject:ge,ident:Re,provided:w,root:!P,requests:new Map,hash:`p${fs(ge.locatorHash,Re.identHash).slice(0,6)}`}}).provided;if(Me.range==="missing:"&&ke.dependencies.has(Re.identHash)){ke.peerDependencies.delete(Re.identHash);continue}if(je.set(Re.identHash,{requester:ke,descriptor:Re,meta:ke.peerDependenciesMeta.get(fn(Re)),children:new Map}),ke.dependencies.set(Re.identHash,Me),Rp(Me)){let P=r.get(Me.descriptorHash);xp(I,P).add(ke.locatorHash)}S.set(Me.identHash,Me),Me.range==="missing:"&&be.add(Me.identHash)}ke.dependencies=new Map(Vs(ke.dependencies,([Re,ct])=>fn(ct)))}),Z.push(()=>{if(!s.has(ke.locatorHash))return;let Re=E.get(tt.locatorHash);typeof Re=="number"&&Re>=2&&V();let ct=E.get(tt.locatorHash),Me=typeof ct<"u"?ct+1:1;E.set(tt.locatorHash,Me),ie(Ne,ke,je,{top:Ce,optional:st}),E.set(tt.locatorHash,Me-1)}),De.push(()=>{let Re=r.get(Ne.descriptorHash);if(typeof Re>"u")throw new Error("Assertion failed: Expected the descriptor to be registered");let ct=O.get(Re);if(typeof ct>"u")throw new Error("Assertion failed: Expected the peer requests to be registered");for(let Me of Be.values()){let P=ct.get(Me.ident.identHash);P&&(Me.requests.set(Ne.descriptorHash,P),p.set(Me.hash,Me),Me.root||Ae.get(Me.ident.identHash)?.children.set(Ne.descriptorHash,P))}if(s.has(ke.locatorHash))for(let Me of be)ke.dependencies.delete(Me)})}for(let Qe of[...me,...ce])Qe();for(let Qe of Se){Se.delete(Qe);let st=s.get(Qe),_=fs(sI(st).locatorHash,...Array.from(st.dependencies.values(),be=>{let je=be.range!=="missing:"?r.get(be.descriptorHash):"missing:";if(typeof je>"u")throw new Error(`Assertion failed: Expected the resolution for ${oi(e.configuration,be)} to have been registered`);return je===Ce?`${je} (top)`:je})),tt=x.get(_);if(typeof tt>"u"){x.set(_,st);continue}let Ne=xp(T,tt.locatorHash);for(let be of T.get(st.locatorHash)??[])r.set(be,tt.locatorHash),Ne.add(be);s.delete(st.locatorHash),a.delete(st.locatorHash),Se.delete(st.locatorHash);let ke=I.get(st.locatorHash);if(ke!==void 0){let be=xp(I,tt.locatorHash);for(let je of ke)be.add(je),Se.add(je)}}for(let Qe of[...Z,...De])Qe()};for(let ae of e.workspaces){let ge=ae.anchoredLocator;h.delete(ae.anchoredDescriptor.descriptorHash),ie(ae.anchoredDescriptor,ge,new Map,{top:ge.locatorHash,optional:!1})}for(let ae of p.values()){if(!ae.root)continue;let ge=s.get(ae.subject.locatorHash);if(typeof ge>"u")continue;for(let Ce of ae.requests.values()){let Ee=`p${fs(ae.subject.locatorHash,fn(ae.ident),Ce.requester.locatorHash).slice(0,6)}`;c.set(Ee,{subject:ae.subject.locatorHash,requested:ae.ident,rootRequester:Ce.requester.locatorHash,allRequesters:Array.from(OB(Ce),d=>d.requester.locatorHash)})}let Ae=[...OB(ae)];if(ae.provided.range!=="missing:"){let Ce=te(ae.provided),Ee=Ce.version??"0.0.0",d=Be=>{if(Be.startsWith(Ii.protocol)){if(!e.tryWorkspaceByLocator(Ce))return null;Be=Be.slice(Ii.protocol.length),(Be==="^"||Be==="~")&&(Be="*")}return Be},Se=!0;for(let Be of Ae){let me=d(Be.descriptor.range);if(me===null){Se=!1;continue}if(!tA(Ee,me)){Se=!1;let ce=`p${fs(ae.subject.locatorHash,fn(ae.ident),Be.requester.locatorHash).slice(0,6)}`;f.push({type:1,subject:ge,requested:ae.ident,requester:Be.requester,version:Ee,hash:ce,requirementCount:Ae.length})}}if(!Se){let Be=Ae.map(me=>d(me.descriptor.range));f.push({type:3,node:ae,range:Be.includes(null)?null:m8(Be),hash:ae.hash})}}else{let Ce=!0;for(let Ee of Ae)if(!Ee.meta?.optional){Ce=!1;let d=`p${fs(ae.subject.locatorHash,fn(ae.ident),Ee.requester.locatorHash).slice(0,6)}`;f.push({type:0,subject:ge,requested:ae.ident,requester:Ee.requester,hash:d})}Ce||f.push({type:2,node:ae,hash:ae.hash})}}}function*kit(e){let t=new Map;if("children"in e)t.set(e,e);else for(let r of e.requests.values())t.set(r,r);for(let[r,s]of t){yield{request:r,root:s};for(let a of r.children.values())t.has(a)||t.set(a,s)}}function Qit(e,t){let r=[],s=[],a=!1;for(let n of e.peerWarnings)if(!(n.type===1||n.type===0)){if(!e.tryWorkspaceByLocator(n.node.subject)){a=!0;continue}if(n.type===3){let c=e.storedResolutions.get(n.node.provided.descriptorHash);if(typeof c>"u")throw new Error("Assertion failed: Expected the descriptor to be registered");let f=e.storedPackages.get(c);if(typeof f>"u")throw new Error("Assertion failed: Expected the package to be registered");let p=A0(kit(n.node),({request:C,root:S})=>tA(f.version??"0.0.0",C.descriptor.range)?A0.skip:C===S?$i(e.configuration,C.requester):`${$i(e.configuration,C.requester)} (via ${$i(e.configuration,S.requester)})`),h=[...OB(n.node)].length>1?"and other dependencies request":"requests",E=n.range?aI(e.configuration,n.range):jt(e.configuration,"but they have non-overlapping ranges!","redBright");r.push(`${$i(e.configuration,n.node.ident)} is listed by your project with version ${FB(e.configuration,f.version??"0.0.0")} (${jt(e.configuration,n.hash,dt.CODE)}), which doesn't satisfy what ${p} ${h} (${E}).`)}if(n.type===2){let c=n.node.requests.size>1?" and other dependencies":"";s.push(`${Yr(e.configuration,n.node.subject)} doesn't provide ${$i(e.configuration,n.node.ident)} (${jt(e.configuration,n.hash,dt.CODE)}), requested by ${$i(e.configuration,n.node.requests.values().next().value.requester)}${c}.`)}}t.startSectionSync({reportFooter:()=>{t.reportWarning(86,`Some peer dependencies are incorrectly met by your project; run ${jt(e.configuration,"yarn explain peer-requirements ",dt.CODE)} for details, where ${jt(e.configuration,"",dt.CODE)} is the six-letter p-prefixed code.`)},skipIfEmpty:!0},()=>{for(let n of Vs(r,c=>XE.default(c)))t.reportWarning(60,n);for(let n of Vs(s,c=>XE.default(c)))t.reportWarning(2,n)}),a&&t.reportWarning(86,`Some peer dependencies are incorrectly met by dependencies; run ${jt(e.configuration,"yarn explain peer-requirements",dt.CODE)} for details.`)}var bT,PT,A0e,gG,dG,mG,xT,vit,Sit,u0e,Dit,bit,Pit,Oa,hG,kT,f0e,Rt,p0e=Xe(()=>{Dt();Dt();vc();Yt();bT=Ie("crypto");fG();zl();PT=et(Ng()),A0e=et(pi()),gG=Ie("util"),dG=et(Ie("v8")),mG=et(Ie("zlib"));rG();$B();nG();iG();cI();I8();Fc();l0e();fv();AG();$g();pG();OQ();Qc();E0();kc();hR();Oj();Np();Zo();xT=KE(process.env.YARN_LOCKFILE_VERSION_OVERRIDE??9),vit=3,Sit=/ *, */g,u0e=/\/$/,Dit=32,bit=(0,gG.promisify)(mG.default.gzip),Pit=(0,gG.promisify)(mG.default.gunzip),Oa=(r=>(r.UpdateLockfile="update-lockfile",r.SkipBuild="skip-build",r))(Oa||{}),hG={restoreLinkersCustomData:["linkersCustomData"],restoreResolutions:["accessibleLocators","conditionalLocators","disabledLocators","optionalBuilds","storedDescriptors","storedResolutions","storedPackages","lockFileChecksum"],restoreBuildState:["skippedBuilds","storedBuildState"]},kT=(a=>(a[a.NotProvided=0]="NotProvided",a[a.NotCompatible=1]="NotCompatible",a[a.NodeNotProvided=2]="NodeNotProvided",a[a.NodeNotCompatible=3]="NodeNotCompatible",a))(kT||{}),f0e=e=>fs(`${vit}`,e),Rt=class e{constructor(t,{configuration:r}){this.resolutionAliases=new Map;this.workspaces=[];this.workspacesByCwd=new Map;this.workspacesByIdent=new Map;this.storedResolutions=new Map;this.storedDescriptors=new Map;this.storedPackages=new Map;this.storedChecksums=new Map;this.storedBuildState=new Map;this.accessibleLocators=new Set;this.conditionalLocators=new Set;this.disabledLocators=new Set;this.originalPackages=new Map;this.optionalBuilds=new Set;this.skippedBuilds=new Set;this.lockfileLastVersion=null;this.lockfileNeedsRefresh=!1;this.peerRequirements=new Map;this.peerWarnings=[];this.peerRequirementNodes=new Map;this.linkersCustomData=new Map;this.lockFileChecksum=null;this.installStateChecksum=null;this.configuration=r,this.cwd=t}static async find(t,r){if(!t.projectCwd)throw new it(`No project found in ${r}`);let s=t.projectCwd,a=r,n=null;for(;n!==t.projectCwd;){if(n=a,le.existsSync(J.join(n,Er.manifest))){s=n;break}a=J.dirname(n)}let c=new e(t.projectCwd,{configuration:t});ze.telemetry?.reportProject(c.cwd),await c.setupResolutions(),await c.setupWorkspaces(),ze.telemetry?.reportWorkspaceCount(c.workspaces.length),ze.telemetry?.reportDependencyCount(c.workspaces.reduce((I,T)=>I+T.manifest.dependencies.size+T.manifest.devDependencies.size,0));let f=c.tryWorkspaceByCwd(s);if(f)return{project:c,workspace:f,locator:f.anchoredLocator};let p=await c.findLocatorForLocation(`${s}/`,{strict:!0});if(p)return{project:c,locator:p,workspace:null};let h=jt(t,c.cwd,dt.PATH),E=jt(t,J.relative(c.cwd,s),dt.PATH),C=`- If ${h} isn't intended to be a project, remove any yarn.lock and/or package.json file there.`,S=`- If ${h} is intended to be a project, it might be that you forgot to list ${E} in its workspace configuration.`,x=`- Finally, if ${h} is fine and you intend ${E} to be treated as a completely separate project (not even a workspace), create an empty yarn.lock file in it.`;throw new it(`The nearest package directory (${jt(t,s,dt.PATH)}) doesn't seem to be part of the project declared in ${jt(t,c.cwd,dt.PATH)}. ${[C,S,x].join(` `)}`)}async setupResolutions(){this.storedResolutions=new Map,this.storedDescriptors=new Map,this.storedPackages=new Map,this.lockFileChecksum=null;let t=J.join(this.cwd,Er.lockfile),r=this.configuration.get("defaultLanguageName");if(le.existsSync(t)){let s=await le.readFilePromise(t,"utf8");this.lockFileChecksum=f0e(s);let a=cs(s);if(a.__metadata){let n=a.__metadata.version,c=a.__metadata.cacheKey;this.lockfileLastVersion=n,this.lockfileNeedsRefresh=n"u")throw new Error(`Assertion failed: Expected the lockfile entry to have a resolution field (${f})`);let h=Tp(p.resolution,!0),E=new _t;E.load(p,{yamlCompatibilityMode:!0});let C=E.version,S=E.languageName||r,x=p.linkType.toUpperCase(),I=p.conditions??null,T=E.dependencies,O=E.peerDependencies,U=E.dependenciesMeta,V=E.peerDependenciesMeta,te=E.bin;if(p.checksum!=null){let ue=typeof c<"u"&&!p.checksum.includes("/")?`${c}/${p.checksum}`:p.checksum;this.storedChecksums.set(h.locatorHash,ue)}let ie={...h,version:C,languageName:S,linkType:x,conditions:I,dependencies:T,peerDependencies:O,dependenciesMeta:U,peerDependenciesMeta:V,bin:te};this.originalPackages.set(ie.locatorHash,ie);for(let ue of f.split(Sit)){let ae=I0(ue);n<=6&&(ae=this.configuration.normalizeDependency(ae),ae=Mn(ae,ae.range.replace(/^patch:[^@]+@(?!npm(:|%3A))/,"$1npm%3A"))),this.storedDescriptors.set(ae.descriptorHash,ae),this.storedResolutions.set(ae.descriptorHash,h.locatorHash)}}}else s.includes("yarn lockfile v1")&&(this.lockfileLastVersion=-1)}}async setupWorkspaces(){this.workspaces=[],this.workspacesByCwd=new Map,this.workspacesByIdent=new Map;let t=new Set,r=(0,PT.default)(4),s=async(a,n)=>{if(t.has(n))return a;t.add(n);let c=new jI(n,{project:this});await r(()=>c.setup());let f=a.then(()=>{this.addWorkspace(c)});return Array.from(c.workspacesCwds).reduce(s,f)};await s(Promise.resolve(),this.cwd)}addWorkspace(t){let r=this.workspacesByIdent.get(t.anchoredLocator.identHash);if(typeof r<"u")throw new Error(`Duplicate workspace name ${$i(this.configuration,t.anchoredLocator)}: ${fe.fromPortablePath(t.cwd)} conflicts with ${fe.fromPortablePath(r.cwd)}`);this.workspaces.push(t),this.workspacesByCwd.set(t.cwd,t),this.workspacesByIdent.set(t.anchoredLocator.identHash,t)}get topLevelWorkspace(){return this.getWorkspaceByCwd(this.cwd)}tryWorkspaceByCwd(t){J.isAbsolute(t)||(t=J.resolve(this.cwd,t)),t=J.normalize(t).replace(/\/+$/,"");let r=this.workspacesByCwd.get(t);return r||null}getWorkspaceByCwd(t){let r=this.tryWorkspaceByCwd(t);if(!r)throw new Error(`Workspace not found (${t})`);return r}tryWorkspaceByFilePath(t){let r=null;for(let s of this.workspaces)J.relative(s.cwd,t).startsWith("../")||r&&r.cwd.length>=s.cwd.length||(r=s);return r||null}getWorkspaceByFilePath(t){let r=this.tryWorkspaceByFilePath(t);if(!r)throw new Error(`Workspace not found (${t})`);return r}tryWorkspaceByIdent(t){let r=this.workspacesByIdent.get(t.identHash);return typeof r>"u"?null:r}getWorkspaceByIdent(t){let r=this.tryWorkspaceByIdent(t);if(!r)throw new Error(`Workspace not found (${$i(this.configuration,t)})`);return r}tryWorkspaceByDescriptor(t){if(t.range.startsWith(Ii.protocol)){let s=t.range.slice(Ii.protocol.length);if(s!=="^"&&s!=="~"&&s!=="*"&&!yl(s))return this.tryWorkspaceByCwd(s)}let r=this.tryWorkspaceByIdent(t);return r===null||(Rp(t)&&(t=kB(t)),!r.accepts(t.range))?null:r}getWorkspaceByDescriptor(t){let r=this.tryWorkspaceByDescriptor(t);if(r===null)throw new Error(`Workspace not found (${oi(this.configuration,t)})`);return r}tryWorkspaceByLocator(t){let r=this.tryWorkspaceByIdent(t);return r===null||(Hu(t)&&(t=sI(t)),r.anchoredLocator.locatorHash!==t.locatorHash)?null:r}getWorkspaceByLocator(t){let r=this.tryWorkspaceByLocator(t);if(!r)throw new Error(`Workspace not found (${Yr(this.configuration,t)})`);return r}deleteDescriptor(t){this.storedResolutions.delete(t),this.storedDescriptors.delete(t)}deleteLocator(t){this.originalPackages.delete(t),this.storedPackages.delete(t),this.accessibleLocators.delete(t)}forgetResolution(t){if("descriptorHash"in t){let r=this.storedResolutions.get(t.descriptorHash);this.deleteDescriptor(t.descriptorHash);let s=new Set(this.storedResolutions.values());typeof r<"u"&&!s.has(r)&&this.deleteLocator(r)}if("locatorHash"in t){this.deleteLocator(t.locatorHash);for(let[r,s]of this.storedResolutions)s===t.locatorHash&&this.deleteDescriptor(r)}}forgetTransientResolutions(){let t=this.configuration.makeResolver(),r=new Map;for(let[s,a]of this.storedResolutions.entries()){let n=r.get(a);n||r.set(a,n=new Set),n.add(s)}for(let s of this.originalPackages.values()){let a;try{a=t.shouldPersistResolution(s,{project:this,resolver:t})}catch{a=!1}if(!a){this.deleteLocator(s.locatorHash);let n=r.get(s.locatorHash);if(n){r.delete(s.locatorHash);for(let c of n)this.deleteDescriptor(c)}}}}forgetVirtualResolutions(){for(let t of this.storedPackages.values())for(let[r,s]of t.dependencies)Rp(s)&&t.dependencies.set(r,kB(s))}getDependencyMeta(t,r){let s={},n=this.topLevelWorkspace.manifest.dependenciesMeta.get(fn(t));if(!n)return s;let c=n.get(null);if(c&&Object.assign(s,c),r===null||!A0e.default.valid(r))return s;for(let[f,p]of n)f!==null&&f===r&&Object.assign(s,p);return s}async findLocatorForLocation(t,{strict:r=!1}={}){let s=new ki,a=this.configuration.getLinkers(),n={project:this,report:s};for(let c of a){let f=await c.findPackageLocator(t,n);if(f){if(r&&(await c.findPackageLocation(f,n)).replace(u0e,"")!==t.replace(u0e,""))continue;return f}}return null}async loadUserConfig(){let t=J.join(this.cwd,".pnp.cjs");await le.existsPromise(t)&&kp(t).setup();let r=J.join(this.cwd,"yarn.config.cjs");return await le.existsPromise(r)?kp(r):null}async preparePackage(t,{resolver:r,resolveOptions:s}){let a=await this.configuration.getPackageExtensions(),n=this.configuration.normalizePackage(t,{packageExtensions:a});for(let[c,f]of n.dependencies){let p=await this.configuration.reduceHook(E=>E.reduceDependency,f,this,n,f,{resolver:r,resolveOptions:s});if(!QB(f,p))throw new Error("Assertion failed: The descriptor ident cannot be changed through aliases");let h=r.bindDescriptor(p,n,s);n.dependencies.set(c,h)}return n}async resolveEverything(t){if(!this.workspacesByCwd||!this.workspacesByIdent)throw new Error("Workspaces must have been setup before calling this function");this.forgetVirtualResolutions();let r=new Map(this.originalPackages),s=[];t.lockfileOnly||this.forgetTransientResolutions();let a=t.resolver||this.configuration.makeResolver(),n=new _I(a);await n.setup(this,{report:t.report});let c=t.lockfileOnly?[new DT(a)]:[n,a],f=new em([new HI(a),...c]),p=new em([...c]),h=this.configuration.makeFetcher(),E=t.lockfileOnly?{project:this,report:t.report,resolver:f}:{project:this,report:t.report,resolver:f,fetchOptions:{project:this,cache:t.cache,checksums:this.storedChecksums,report:t.report,fetcher:h,cacheOptions:{mirrorWriteOnly:!0}}},C=new Map,S=new Map,x=new Map,I=new Map,T=new Map,O=new Map,U=this.topLevelWorkspace.anchoredLocator,V=new Set,te=[],ie=zH(),ue=this.configuration.getSupportedArchitectures();await t.report.startProgressPromise(yo.progressViaTitle(),async ce=>{let Z=async tt=>{let Ne=await VE(async()=>await f.resolve(tt,E),Re=>`${Yr(this.configuration,tt)}: ${Re}`);if(!RB(tt,Ne))throw new Error(`Assertion failed: The locator cannot be changed by the resolver (went from ${Yr(this.configuration,tt)} to ${Yr(this.configuration,Ne)})`);I.set(Ne.locatorHash,Ne),!r.delete(Ne.locatorHash)&&!this.tryWorkspaceByLocator(Ne)&&s.push(Ne);let be=await this.preparePackage(Ne,{resolver:f,resolveOptions:E}),je=Lu([...be.dependencies.values()].map(Re=>_(Re)));return te.push(je),je.catch(()=>{}),S.set(be.locatorHash,be),be},De=async tt=>{let Ne=T.get(tt.locatorHash);if(typeof Ne<"u")return Ne;let ke=Promise.resolve().then(()=>Z(tt));return T.set(tt.locatorHash,ke),ke},Qe=async(tt,Ne)=>{let ke=await _(Ne);return C.set(tt.descriptorHash,tt),x.set(tt.descriptorHash,ke.locatorHash),ke},st=async tt=>{ce.setTitle(oi(this.configuration,tt));let Ne=this.resolutionAliases.get(tt.descriptorHash);if(typeof Ne<"u")return Qe(tt,this.storedDescriptors.get(Ne));let ke=f.getResolutionDependencies(tt,E),be=Object.fromEntries(await Lu(Object.entries(ke).map(async([ct,Me])=>{let P=f.bindDescriptor(Me,U,E),w=await _(P);return V.add(w.locatorHash),[ct,w]}))),Re=(await VE(async()=>await f.getCandidates(tt,be,E),ct=>`${oi(this.configuration,tt)}: ${ct}`))[0];if(typeof Re>"u")throw new Lt(82,`${oi(this.configuration,tt)}: No candidates found`);if(t.checkResolutions){let{locators:ct}=await p.getSatisfying(tt,be,[Re],{...E,resolver:p});if(!ct.find(Me=>Me.locatorHash===Re.locatorHash))throw new Lt(78,`Invalid resolution ${DB(this.configuration,tt,Re)}`)}return C.set(tt.descriptorHash,tt),x.set(tt.descriptorHash,Re.locatorHash),De(Re)},_=tt=>{let Ne=O.get(tt.descriptorHash);if(typeof Ne<"u")return Ne;C.set(tt.descriptorHash,tt);let ke=Promise.resolve().then(()=>st(tt));return O.set(tt.descriptorHash,ke),ke};for(let tt of this.workspaces){let Ne=tt.anchoredDescriptor;te.push(_(Ne))}for(;te.length>0;){let tt=[...te];te.length=0,await Lu(tt)}});let ae=Xl(r.values(),ce=>this.tryWorkspaceByLocator(ce)?Xl.skip:ce);if(s.length>0||ae.length>0){let ce=new Set(this.workspaces.flatMap(tt=>{let Ne=S.get(tt.anchoredLocator.locatorHash);if(!Ne)throw new Error("Assertion failed: The workspace should have been resolved");return Array.from(Ne.dependencies.values(),ke=>{let be=x.get(ke.descriptorHash);if(!be)throw new Error("Assertion failed: The resolution should have been registered");return be})})),Z=tt=>ce.has(tt.locatorHash)?"0":"1",De=tt=>ml(tt),Qe=Vs(s,[Z,De]),st=Vs(ae,[Z,De]),_=t.report.getRecommendedLength();Qe.length>0&&t.report.reportInfo(85,`${jt(this.configuration,"+",dt.ADDED)} ${Zk(this.configuration,Qe,_)}`),st.length>0&&t.report.reportInfo(85,`${jt(this.configuration,"-",dt.REMOVED)} ${Zk(this.configuration,st,_)}`)}let ge=new Set(this.resolutionAliases.values()),Ae=new Set(S.keys()),Ce=new Set,Ee=new Map,d=[],Se=new Map;xit({project:this,accessibleLocators:Ce,volatileDescriptors:ge,optionalBuilds:Ae,peerRequirements:Ee,peerWarnings:d,peerRequirementNodes:Se,allDescriptors:C,allResolutions:x,allPackages:S});for(let ce of V)Ae.delete(ce);for(let ce of ge)C.delete(ce),x.delete(ce);let Be=new Set,me=new Set;for(let ce of S.values())ce.conditions!=null&&Ae.has(ce.locatorHash)&&(QQ(ce,ue)||(QQ(ce,ie)&&t.report.reportWarningOnce(77,`${Yr(this.configuration,ce)}: Your current architecture (${process.platform}-${process.arch}) is supported by this package, but is missing from the ${jt(this.configuration,"supportedArchitectures",dt.SETTING)} setting`),me.add(ce.locatorHash)),Be.add(ce.locatorHash));this.storedResolutions=x,this.storedDescriptors=C,this.storedPackages=S,this.accessibleLocators=Ce,this.conditionalLocators=Be,this.disabledLocators=me,this.originalPackages=I,this.optionalBuilds=Ae,this.peerRequirements=Ee,this.peerWarnings=d,this.peerRequirementNodes=Se}async fetchEverything({cache:t,report:r,fetcher:s,mode:a,persistProject:n=!0}){let c={mockedPackages:this.disabledLocators,unstablePackages:this.conditionalLocators},f=s||this.configuration.makeFetcher(),p={checksums:this.storedChecksums,project:this,cache:t,fetcher:f,report:r,cacheOptions:c},h=Array.from(new Set(Vs(this.storedResolutions.values(),[I=>{let T=this.storedPackages.get(I);if(!T)throw new Error("Assertion failed: The locator should have been registered");return ml(T)}])));a==="update-lockfile"&&(h=h.filter(I=>!this.storedChecksums.has(I)));let E=!1,C=yo.progressViaCounter(h.length);await r.reportProgress(C);let S=(0,PT.default)(Dit);if(await Lu(h.map(I=>S(async()=>{let T=this.storedPackages.get(I);if(!T)throw new Error("Assertion failed: The locator should have been registered");if(Hu(T))return;let O;try{O=await f.fetch(T,p)}catch(U){U.message=`${Yr(this.configuration,T)}: ${U.message}`,r.reportExceptionOnce(U),E=U;return}O.checksum!=null?this.storedChecksums.set(T.locatorHash,O.checksum):this.storedChecksums.delete(T.locatorHash),O.releaseFs&&O.releaseFs()}).finally(()=>{C.tick()}))),E)throw E;let x=n&&a!=="update-lockfile"?await this.cacheCleanup({cache:t,report:r}):null;if(r.cacheMisses.size>0||x){let T=(await Promise.all([...r.cacheMisses].map(async ae=>{let ge=this.storedPackages.get(ae),Ae=this.storedChecksums.get(ae)??null,Ce=t.getLocatorPath(ge,Ae);return(await le.statPromise(Ce)).size}))).reduce((ae,ge)=>ae+ge,0)-(x?.size??0),O=r.cacheMisses.size,U=x?.count??0,V=`${qk(O,{zero:"No new packages",one:"A package was",more:`${jt(this.configuration,O,dt.NUMBER)} packages were`})} added to the project`,te=`${qk(U,{zero:"none were",one:"one was",more:`${jt(this.configuration,U,dt.NUMBER)} were`})} removed`,ie=T!==0?` (${jt(this.configuration,T,dt.SIZE_DIFF)})`:"",ue=U>0?O>0?`${V}, and ${te}${ie}.`:`${V}, but ${te}${ie}.`:`${V}${ie}.`;r.reportInfo(13,ue)}}async linkEverything({cache:t,report:r,fetcher:s,mode:a}){let n={mockedPackages:this.disabledLocators,unstablePackages:this.conditionalLocators,skipIntegrityCheck:!0},c=s||this.configuration.makeFetcher(),f={checksums:this.storedChecksums,project:this,cache:t,fetcher:c,report:r,cacheOptions:n},p=this.configuration.getLinkers(),h={project:this,report:r},E=new Map(p.map(Be=>{let me=Be.makeInstaller(h),ce=Be.getCustomDataKey(),Z=this.linkersCustomData.get(ce);return typeof Z<"u"&&me.attachCustomData(Z),[Be,me]})),C=new Map,S=new Map,x=new Map,I=new Map(await Lu([...this.accessibleLocators].map(async Be=>{let me=this.storedPackages.get(Be);if(!me)throw new Error("Assertion failed: The locator should have been registered");return[Be,await c.fetch(me,f)]}))),T=[],O=new Set,U=[];for(let Be of this.accessibleLocators){let me=this.storedPackages.get(Be);if(typeof me>"u")throw new Error("Assertion failed: The locator should have been registered");let ce=I.get(me.locatorHash);if(typeof ce>"u")throw new Error("Assertion failed: The fetch result should have been registered");let Z=[],De=st=>{Z.push(st)},Qe=this.tryWorkspaceByLocator(me);if(Qe!==null){let st=[],{scripts:_}=Qe.manifest;for(let Ne of["preinstall","install","postinstall"])_.has(Ne)&&st.push({type:0,script:Ne});try{for(let[Ne,ke]of E)if(Ne.supportsPackage(me,h)&&(await ke.installPackage(me,ce,{holdFetchResult:De})).buildRequest!==null)throw new Error("Assertion failed: Linkers can't return build directives for workspaces; this responsibility befalls to the Yarn core")}finally{Z.length===0?ce.releaseFs?.():T.push(Lu(Z).catch(()=>{}).then(()=>{ce.releaseFs?.()}))}let tt=J.join(ce.packageFs.getRealPath(),ce.prefixPath);S.set(me.locatorHash,tt),!Hu(me)&&st.length>0&&x.set(me.locatorHash,{buildDirectives:st,buildLocations:[tt]})}else{let st=p.find(Ne=>Ne.supportsPackage(me,h));if(!st)throw new Lt(12,`${Yr(this.configuration,me)} isn't supported by any available linker`);let _=E.get(st);if(!_)throw new Error("Assertion failed: The installer should have been registered");let tt;try{tt=await _.installPackage(me,ce,{holdFetchResult:De})}finally{Z.length===0?ce.releaseFs?.():T.push(Lu(Z).then(()=>{}).then(()=>{ce.releaseFs?.()}))}C.set(me.locatorHash,st),S.set(me.locatorHash,tt.packageLocation),tt.buildRequest&&tt.packageLocation&&(tt.buildRequest.skipped?(O.add(me.locatorHash),this.skippedBuilds.has(me.locatorHash)||U.push([me,tt.buildRequest.explain])):x.set(me.locatorHash,{buildDirectives:tt.buildRequest.directives,buildLocations:[tt.packageLocation]}))}}let V=new Map;for(let Be of this.accessibleLocators){let me=this.storedPackages.get(Be);if(!me)throw new Error("Assertion failed: The locator should have been registered");let ce=this.tryWorkspaceByLocator(me)!==null,Z=async(De,Qe)=>{let st=S.get(me.locatorHash);if(typeof st>"u")throw new Error(`Assertion failed: The package (${Yr(this.configuration,me)}) should have been registered`);let _=[];for(let tt of me.dependencies.values()){let Ne=this.storedResolutions.get(tt.descriptorHash);if(typeof Ne>"u")throw new Error(`Assertion failed: The resolution (${oi(this.configuration,tt)}, from ${Yr(this.configuration,me)})should have been registered`);let ke=this.storedPackages.get(Ne);if(typeof ke>"u")throw new Error(`Assertion failed: The package (${Ne}, resolved from ${oi(this.configuration,tt)}) should have been registered`);let be=this.tryWorkspaceByLocator(ke)===null?C.get(Ne):null;if(typeof be>"u")throw new Error(`Assertion failed: The package (${Ne}, resolved from ${oi(this.configuration,tt)}) should have been registered`);be===De||be===null?S.get(ke.locatorHash)!==null&&_.push([tt,ke]):!ce&&st!==null&&CB(V,Ne).push(st)}st!==null&&await Qe.attachInternalDependencies(me,_)};if(ce)for(let[De,Qe]of E)De.supportsPackage(me,h)&&await Z(De,Qe);else{let De=C.get(me.locatorHash);if(!De)throw new Error("Assertion failed: The linker should have been found");let Qe=E.get(De);if(!Qe)throw new Error("Assertion failed: The installer should have been registered");await Z(De,Qe)}}for(let[Be,me]of V){let ce=this.storedPackages.get(Be);if(!ce)throw new Error("Assertion failed: The package should have been registered");let Z=C.get(ce.locatorHash);if(!Z)throw new Error("Assertion failed: The linker should have been found");let De=E.get(Z);if(!De)throw new Error("Assertion failed: The installer should have been registered");await De.attachExternalDependents(ce,me)}let te=new Map;for(let[Be,me]of E){let ce=await me.finalizeInstall();for(let Z of ce?.records??[])Z.buildRequest.skipped?(O.add(Z.locator.locatorHash),this.skippedBuilds.has(Z.locator.locatorHash)||U.push([Z.locator,Z.buildRequest.explain])):x.set(Z.locator.locatorHash,{buildDirectives:Z.buildRequest.directives,buildLocations:Z.buildLocations});typeof ce?.customData<"u"&&te.set(Be.getCustomDataKey(),ce.customData)}if(this.linkersCustomData=te,await Lu(T),a==="skip-build")return;for(let[,Be]of Vs(U,([me])=>ml(me)))Be(r);let ie=new Set(x.keys()),ue=(0,bT.createHash)("sha512");ue.update(process.versions.node),await this.configuration.triggerHook(Be=>Be.globalHashGeneration,this,Be=>{ue.update("\0"),ue.update(Be)});let ae=ue.digest("hex"),ge=new Map,Ae=Be=>{let me=ge.get(Be.locatorHash);if(typeof me<"u")return me;let ce=this.storedPackages.get(Be.locatorHash);if(typeof ce>"u")throw new Error("Assertion failed: The package should have been registered");let Z=(0,bT.createHash)("sha512");Z.update(Be.locatorHash),ge.set(Be.locatorHash,"");for(let De of ce.dependencies.values()){let Qe=this.storedResolutions.get(De.descriptorHash);if(typeof Qe>"u")throw new Error(`Assertion failed: The resolution (${oi(this.configuration,De)}) should have been registered`);let st=this.storedPackages.get(Qe);if(typeof st>"u")throw new Error("Assertion failed: The package should have been registered");Z.update(Ae(st))}return me=Z.digest("hex"),ge.set(Be.locatorHash,me),me},Ce=(Be,me)=>{let ce=(0,bT.createHash)("sha512");ce.update(ae),ce.update(Ae(Be));for(let Z of me)ce.update(Z);return ce.digest("hex")},Ee=new Map,d=!1,Se=Be=>{let me=new Set([Be.locatorHash]);for(let ce of me){let Z=this.storedPackages.get(ce);if(!Z)throw new Error("Assertion failed: The package should have been registered");for(let De of Z.dependencies.values()){let Qe=this.storedResolutions.get(De.descriptorHash);if(!Qe)throw new Error(`Assertion failed: The resolution (${oi(this.configuration,De)}) should have been registered`);if(Qe!==Be.locatorHash&&ie.has(Qe))return!1;let st=this.storedPackages.get(Qe);if(!st)throw new Error("Assertion failed: The package should have been registered");let _=this.tryWorkspaceByLocator(st);if(_){if(_.anchoredLocator.locatorHash!==Be.locatorHash&&ie.has(_.anchoredLocator.locatorHash))return!1;me.add(_.anchoredLocator.locatorHash)}me.add(Qe)}}return!0};for(;ie.size>0;){let Be=ie.size,me=[];for(let ce of ie){let Z=this.storedPackages.get(ce);if(!Z)throw new Error("Assertion failed: The package should have been registered");if(!Se(Z))continue;let De=x.get(Z.locatorHash);if(!De)throw new Error("Assertion failed: The build directive should have been registered");let Qe=Ce(Z,De.buildLocations);if(this.storedBuildState.get(Z.locatorHash)===Qe){Ee.set(Z.locatorHash,Qe),ie.delete(ce);continue}d||(await this.persistInstallStateFile(),d=!0),this.storedBuildState.has(Z.locatorHash)?r.reportInfo(8,`${Yr(this.configuration,Z)} must be rebuilt because its dependency tree changed`):r.reportInfo(7,`${Yr(this.configuration,Z)} must be built because it never has been before or the last one failed`);let st=De.buildLocations.map(async _=>{if(!J.isAbsolute(_))throw new Error(`Assertion failed: Expected the build location to be absolute (not ${_})`);for(let tt of De.buildDirectives){let Ne=`# This file contains the result of Yarn building a package (${ml(Z)}) `;switch(tt.type){case 0:Ne+=`# Script name: ${tt.script} `;break;case 1:Ne+=`# Script code: ${tt.script} `;break}let ke=null;if(!await le.mktempPromise(async je=>{let Re=J.join(je,"build.log"),{stdout:ct,stderr:Me}=this.configuration.getSubprocessStreams(Re,{header:Ne,prefix:Yr(this.configuration,Z),report:r}),P;try{switch(tt.type){case 0:P=await OR(Z,tt.script,[],{cwd:_,project:this,stdin:ke,stdout:ct,stderr:Me});break;case 1:P=await Rj(Z,tt.script,[],{cwd:_,project:this,stdin:ke,stdout:ct,stderr:Me});break}}catch(y){Me.write(y.stack),P=1}if(ct.end(),Me.end(),P===0)return!0;le.detachTemp(je);let w=`${Yr(this.configuration,Z)} couldn't be built successfully (exit code ${jt(this.configuration,P,dt.NUMBER)}, logs can be found here: ${jt(this.configuration,Re,dt.PATH)})`,b=this.optionalBuilds.has(Z.locatorHash);return b?r.reportInfo(9,w):r.reportError(9,w),Ipe&&r.reportFold(fe.fromPortablePath(Re),le.readFileSync(Re,"utf8")),b}))return!1}return!0});me.push(...st,Promise.allSettled(st).then(_=>{ie.delete(ce),_.every(tt=>tt.status==="fulfilled"&&tt.value===!0)&&Ee.set(Z.locatorHash,Qe)}))}if(await Lu(me),Be===ie.size){let ce=Array.from(ie).map(Z=>{let De=this.storedPackages.get(Z);if(!De)throw new Error("Assertion failed: The package should have been registered");return Yr(this.configuration,De)}).join(", ");r.reportError(3,`Some packages have circular dependencies that make their build order unsatisfiable - as a result they won't be built (affected packages are: ${ce})`);break}}this.storedBuildState=Ee,this.skippedBuilds=O}async installWithNewReport(t,r){return(await Ot.start({configuration:this.configuration,json:t.json,stdout:t.stdout,forceSectionAlignment:!0,includeLogs:!t.json&&!t.quiet,includeVersion:!0},async a=>{await this.install({...r,report:a})})).exitCode()}async install(t){let r=this.configuration.get("nodeLinker");ze.telemetry?.reportInstall(r);let s=!1;if(await t.report.startTimerPromise("Project validation",{skipIfEmpty:!0},async()=>{this.configuration.get("enableOfflineMode")&&t.report.reportWarning(90,"Offline work is enabled; Yarn won't fetch packages from the remote registry if it can avoid it"),await this.configuration.triggerHook(E=>E.validateProject,this,{reportWarning:(E,C)=>{t.report.reportWarning(E,C)},reportError:(E,C)=>{t.report.reportError(E,C),s=!0}})}),s)return;let a=await this.configuration.getPackageExtensions();for(let E of a.values())for(let[,C]of E)for(let S of C)S.status="inactive";let n=J.join(this.cwd,Er.lockfile),c=null;if(t.immutable)try{c=await le.readFilePromise(n,"utf8")}catch(E){throw E.code==="ENOENT"?new Lt(28,"The lockfile would have been created by this install, which is explicitly forbidden."):E}await t.report.startTimerPromise("Resolution step",async()=>{await this.resolveEverything(t)}),await t.report.startTimerPromise("Post-resolution validation",{skipIfEmpty:!0},async()=>{Qit(this,t.report);for(let[,E]of a)for(let[,C]of E)for(let S of C)if(S.userProvided){let x=jt(this.configuration,S,dt.PACKAGE_EXTENSION);switch(S.status){case"inactive":t.report.reportWarning(68,`${x}: No matching package in the dependency tree; you may not need this rule anymore.`);break;case"redundant":t.report.reportWarning(69,`${x}: This rule seems redundant when applied on the original package; the extension may have been applied upstream.`);break}}if(c!==null){let E=mg(c,this.generateLockfile());if(E!==c){let C=a0e(n,n,c,E,void 0,void 0,{maxEditLength:100});if(C){t.report.reportSeparator();for(let S of C.hunks){t.report.reportInfo(null,`@@ -${S.oldStart},${S.oldLines} +${S.newStart},${S.newLines} @@`);for(let x of S.lines)x.startsWith("+")?t.report.reportError(28,jt(this.configuration,x,dt.ADDED)):x.startsWith("-")?t.report.reportError(28,jt(this.configuration,x,dt.REMOVED)):t.report.reportInfo(null,jt(this.configuration,x,"grey"))}t.report.reportSeparator()}throw new Lt(28,"The lockfile would have been modified by this install, which is explicitly forbidden.")}}});for(let E of a.values())for(let[,C]of E)for(let S of C)S.userProvided&&S.status==="active"&&ze.telemetry?.reportPackageExtension(Kg(S,dt.PACKAGE_EXTENSION));await t.report.startTimerPromise("Fetch step",async()=>{await this.fetchEverything(t)});let f=t.immutable?[...new Set(this.configuration.get("immutablePatterns"))].sort():[],p=await Promise.all(f.map(async E=>SQ(E,{cwd:this.cwd})));(typeof t.persistProject>"u"||t.persistProject)&&await this.persist(),await t.report.startTimerPromise("Link step",async()=>{if(t.mode==="update-lockfile"){t.report.reportWarning(73,`Skipped due to ${jt(this.configuration,"mode=update-lockfile",dt.CODE)}`);return}await this.linkEverything(t);let E=await Promise.all(f.map(async C=>SQ(C,{cwd:this.cwd})));for(let C=0;C{await this.configuration.triggerHook(E=>E.validateProjectAfterInstall,this,{reportWarning:(E,C)=>{t.report.reportWarning(E,C)},reportError:(E,C)=>{t.report.reportError(E,C),h=!0}})}),!h&&await this.configuration.triggerHook(E=>E.afterAllInstalled,this,t)}generateLockfile(){let t=new Map;for(let[n,c]of this.storedResolutions.entries()){let f=t.get(c);f||t.set(c,f=new Set),f.add(n)}let r={},{cacheKey:s}=Kr.getCacheKey(this.configuration);r.__metadata={version:xT,cacheKey:s};for(let[n,c]of t.entries()){let f=this.originalPackages.get(n);if(!f)continue;let p=[];for(let C of c){let S=this.storedDescriptors.get(C);if(!S)throw new Error("Assertion failed: The descriptor should have been registered");p.push(S)}let h=p.map(C=>gl(C)).sort().join(", "),E=new _t;E.version=f.linkType==="HARD"?f.version:"0.0.0-use.local",E.languageName=f.languageName,E.dependencies=new Map(f.dependencies),E.peerDependencies=new Map(f.peerDependencies),E.dependenciesMeta=new Map(f.dependenciesMeta),E.peerDependenciesMeta=new Map(f.peerDependenciesMeta),E.bin=new Map(f.bin),r[h]={...E.exportTo({},{compatibilityMode:!1}),linkType:f.linkType.toLowerCase(),resolution:ml(f),checksum:this.storedChecksums.get(f.locatorHash),conditions:f.conditions||void 0}}return`${[`# This file is generated by running "yarn install" inside your project. `,`# Manual changes might be lost - proceed with caution! `].join("")} `+fl(r)}async persistLockfile(){let t=J.join(this.cwd,Er.lockfile),r="";try{r=await le.readFilePromise(t,"utf8")}catch{}let s=this.generateLockfile(),a=mg(r,s);a!==r&&(await le.writeFilePromise(t,a),this.lockFileChecksum=f0e(a),this.lockfileNeedsRefresh=!1)}async persistInstallStateFile(){let t=[];for(let c of Object.values(hG))t.push(...c);let r=Vg(this,t),s=dG.default.serialize(r),a=fs(s);if(this.installStateChecksum===a)return;let n=this.configuration.get("installStatePath");await le.mkdirPromise(J.dirname(n),{recursive:!0}),await le.writeFilePromise(n,await bit(s)),this.installStateChecksum=a}async restoreInstallState({restoreLinkersCustomData:t=!0,restoreResolutions:r=!0,restoreBuildState:s=!0}={}){let a=this.configuration.get("installStatePath"),n;try{let c=await Pit(await le.readFilePromise(a));n=dG.default.deserialize(c),this.installStateChecksum=fs(c)}catch{r&&await this.applyLightResolution();return}t&&typeof n.linkersCustomData<"u"&&(this.linkersCustomData=n.linkersCustomData),s&&Object.assign(this,Vg(n,hG.restoreBuildState)),r&&(n.lockFileChecksum===this.lockFileChecksum?Object.assign(this,Vg(n,hG.restoreResolutions)):await this.applyLightResolution())}async applyLightResolution(){await this.resolveEverything({lockfileOnly:!0,report:new ki}),await this.persistInstallStateFile()}async persist(){let t=(0,PT.default)(4);await Promise.all([this.persistLockfile(),...this.workspaces.map(r=>t(()=>r.persistManifest()))])}async cacheCleanup({cache:t,report:r}){if(this.configuration.get("enableGlobalCache"))return null;let s=new Set([".gitignore"]);if(!k8(t.cwd,this.cwd)||!await le.existsPromise(t.cwd))return null;let a=[];for(let c of await le.readdirPromise(t.cwd)){if(s.has(c))continue;let f=J.resolve(t.cwd,c);t.markedFiles.has(f)||(t.immutable?r.reportError(56,`${jt(this.configuration,J.basename(f),"magenta")} appears to be unused and would be marked for deletion, but the cache is immutable`):a.push(le.lstatPromise(f).then(async p=>(await le.removePromise(f),p.size))))}if(a.length===0)return null;let n=await Promise.all(a);return{count:a.length,size:n.reduce((c,f)=>c+f,0)}}}});function Rit(e){let s=Math.floor(e.timeNow/864e5),a=e.updateInterval*864e5,n=e.state.lastUpdate??e.timeNow+a+Math.floor(a*e.randomInitialInterval),c=n+a,f=e.state.lastTips??s*864e5,p=f+864e5+8*36e5-e.timeZone,h=c<=e.timeNow,E=p<=e.timeNow,C=null;return(h||E||!e.state.lastUpdate||!e.state.lastTips)&&(C={},C.lastUpdate=h?e.timeNow:n,C.lastTips=f,C.blocks=h?{}:e.state.blocks,C.displayedTips=e.state.displayedTips),{nextState:C,triggerUpdate:h,triggerTips:E,nextTips:E?s*864e5:f}}var GI,h0e=Xe(()=>{Dt();uv();E0();AR();kc();Np();GI=class{constructor(t,r){this.values=new Map;this.hits=new Map;this.enumerators=new Map;this.nextTips=0;this.displayedTips=[];this.shouldCommitTips=!1;this.configuration=t;let s=this.getRegistryPath();this.isNew=!le.existsSync(s),this.shouldShowTips=!1,this.sendReport(r),this.startBuffer()}commitTips(){this.shouldShowTips&&(this.shouldCommitTips=!0)}selectTip(t){let r=new Set(this.displayedTips),s=f=>f&&An?tA(An,f):!1,a=t.map((f,p)=>p).filter(f=>t[f]&&s(t[f]?.selector));if(a.length===0)return null;let n=a.filter(f=>!r.has(f));if(n.length===0){let f=Math.floor(a.length*.2);this.displayedTips=f>0?this.displayedTips.slice(-f):[],n=a.filter(p=>!r.has(p))}let c=n[Math.floor(Math.random()*n.length)];return this.displayedTips.push(c),this.commitTips(),t[c]}reportVersion(t){this.reportValue("version",t.replace(/-git\..*/,"-git"))}reportCommandName(t){this.reportValue("commandName",t||"")}reportPluginName(t){this.reportValue("pluginName",t)}reportProject(t){this.reportEnumerator("projectCount",t)}reportInstall(t){this.reportHit("installCount",t)}reportPackageExtension(t){this.reportValue("packageExtension",t)}reportWorkspaceCount(t){this.reportValue("workspaceCount",String(t))}reportDependencyCount(t){this.reportValue("dependencyCount",String(t))}reportValue(t,r){xp(this.values,t).add(r)}reportEnumerator(t,r){xp(this.enumerators,t).add(fs(r))}reportHit(t,r="*"){let s=P4(this.hits,t),a=Zl(s,r,()=>0);s.set(r,a+1)}getRegistryPath(){let t=this.configuration.get("globalFolder");return J.join(t,"telemetry.json")}sendReport(t){let r=this.getRegistryPath(),s;try{s=le.readJsonSync(r)}catch{s={}}let{nextState:a,triggerUpdate:n,triggerTips:c,nextTips:f}=Rit({state:s,timeNow:Date.now(),timeZone:new Date().getTimezoneOffset()*60*1e3,randomInitialInterval:Math.random(),updateInterval:this.configuration.get("telemetryInterval")});if(this.nextTips=f,this.displayedTips=s.displayedTips??[],a!==null)try{le.mkdirSync(J.dirname(r),{recursive:!0}),le.writeJsonSync(r,a)}catch{return!1}if(c&&this.configuration.get("enableTips")&&(this.shouldShowTips=!0),n){let p=s.blocks??{};if(Object.keys(p).length===0){let h=`https://browser-http-intake.logs.datadoghq.eu/v1/input/${t}?ddsource=yarn`,E=C=>KH(h,C,{configuration:this.configuration}).catch(()=>{});for(let[C,S]of Object.entries(s.blocks??{})){if(Object.keys(S).length===0)continue;let x=S;x.userId=C,x.reportType="primary";for(let O of Object.keys(x.enumerators??{}))x.enumerators[O]=x.enumerators[O].length;E(x);let I=new Map,T=20;for(let[O,U]of Object.entries(x.values))U.length>0&&I.set(O,U.slice(0,T));for(;I.size>0;){let O={};O.userId=C,O.reportType="secondary",O.metrics={};for(let[U,V]of I)O.metrics[U]=V.shift(),V.length===0&&I.delete(U);E(O)}}}}return!0}applyChanges(){let t=this.getRegistryPath(),r;try{r=le.readJsonSync(t)}catch{r={}}let s=this.configuration.get("telemetryUserId")??"*",a=r.blocks=r.blocks??{},n=a[s]=a[s]??{};for(let c of this.hits.keys()){let f=n.hits=n.hits??{},p=f[c]=f[c]??{};for(let[h,E]of this.hits.get(c))p[h]=(p[h]??0)+E}for(let c of["values","enumerators"])for(let f of this[c].keys()){let p=n[c]=n[c]??{};p[f]=[...new Set([...p[f]??[],...this[c].get(f)??[]])]}this.shouldCommitTips&&(r.lastTips=this.nextTips,r.displayedTips=this.displayedTips),le.mkdirSync(J.dirname(t),{recursive:!0}),le.writeJsonSync(t,r)}startBuffer(){process.on("exit",()=>{try{this.applyChanges()}catch{}})}}});var jv={};Vt(jv,{BuildDirectiveType:()=>vT,CACHE_CHECKPOINT:()=>tG,CACHE_VERSION:()=>BT,Cache:()=>Kr,Configuration:()=>ze,DEFAULT_RC_FILENAME:()=>rj,DurationUnit:()=>nj,FormatType:()=>kAe,InstallMode:()=>Oa,LEGACY_PLUGINS:()=>ZB,LOCKFILE_VERSION:()=>xT,LegacyMigrationResolver:()=>_I,LightReport:()=>uA,LinkType:()=>zE,LockfileResolver:()=>HI,Manifest:()=>_t,MessageName:()=>Ir,MultiFetcher:()=>uI,PackageExtensionStatus:()=>R4,PackageExtensionType:()=>Q4,PeerWarningType:()=>kT,Project:()=>Rt,Report:()=>yo,ReportError:()=>Lt,SettingsType:()=>SI,StreamReport:()=>Ot,TAG_REGEXP:()=>_p,TelemetryManager:()=>GI,ThrowReport:()=>ki,VirtualFetcher:()=>fI,WindowsLinkType:()=>ER,Workspace:()=>jI,WorkspaceFetcher:()=>AI,WorkspaceResolver:()=>Ii,YarnVersion:()=>An,execUtils:()=>qr,folderUtils:()=>NQ,formatUtils:()=>pe,hashUtils:()=>Ln,httpUtils:()=>nn,miscUtils:()=>Ge,nodeUtils:()=>Ui,parseMessageName:()=>jx,reportOptionDeprecations:()=>PI,scriptUtils:()=>Cn,semverUtils:()=>kr,stringifyMessageName:()=>Kf,structUtils:()=>j,tgzUtils:()=>gs,treeUtils:()=>Rs});var qe=Xe(()=>{dR();OQ();Qc();E0();AR();kc();hR();Oj();Np();Zo();Yhe();$he();rG();$B();$B();r0e();nG();n0e();iG();cI();Gx();E8();p0e();Fc();fv();h0e();AG();C8();w8();$g();pG();uv();Nae()});var I0e=G((X4t,qv)=>{"use strict";var Fit=process.env.TERM_PROGRAM==="Hyper",Nit=process.platform==="win32",m0e=process.platform==="linux",yG={ballotDisabled:"\u2612",ballotOff:"\u2610",ballotOn:"\u2611",bullet:"\u2022",bulletWhite:"\u25E6",fullBlock:"\u2588",heart:"\u2764",identicalTo:"\u2261",line:"\u2500",mark:"\u203B",middot:"\xB7",minus:"\uFF0D",multiplication:"\xD7",obelus:"\xF7",pencilDownRight:"\u270E",pencilRight:"\u270F",pencilUpRight:"\u2710",percent:"%",pilcrow2:"\u2761",pilcrow:"\xB6",plusMinus:"\xB1",section:"\xA7",starsOff:"\u2606",starsOn:"\u2605",upDownArrow:"\u2195"},y0e=Object.assign({},yG,{check:"\u221A",cross:"\xD7",ellipsisLarge:"...",ellipsis:"...",info:"i",question:"?",questionSmall:"?",pointer:">",pointerSmall:"\xBB",radioOff:"( )",radioOn:"(*)",warning:"\u203C"}),E0e=Object.assign({},yG,{ballotCross:"\u2718",check:"\u2714",cross:"\u2716",ellipsisLarge:"\u22EF",ellipsis:"\u2026",info:"\u2139",question:"?",questionFull:"\uFF1F",questionSmall:"\uFE56",pointer:m0e?"\u25B8":"\u276F",pointerSmall:m0e?"\u2023":"\u203A",radioOff:"\u25EF",radioOn:"\u25C9",warning:"\u26A0"});qv.exports=Nit&&!Fit?y0e:E0e;Reflect.defineProperty(qv.exports,"common",{enumerable:!1,value:yG});Reflect.defineProperty(qv.exports,"windows",{enumerable:!1,value:y0e});Reflect.defineProperty(qv.exports,"other",{enumerable:!1,value:E0e})});var zu=G((Z4t,EG)=>{"use strict";var Oit=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),Lit=/[\u001b\u009b][[\]#;?()]*(?:(?:(?:[^\W_]*;?[^\W_]*)\u0007)|(?:(?:[0-9]{1,4}(;[0-9]{0,4})*)?[~0-9=<>cf-nqrtyA-PRZ]))/g,C0e=()=>{let e={enabled:!0,visible:!0,styles:{},keys:{}};"FORCE_COLOR"in process.env&&(e.enabled=process.env.FORCE_COLOR!=="0");let t=n=>{let c=n.open=`\x1B[${n.codes[0]}m`,f=n.close=`\x1B[${n.codes[1]}m`,p=n.regex=new RegExp(`\\u001b\\[${n.codes[1]}m`,"g");return n.wrap=(h,E)=>{h.includes(f)&&(h=h.replace(p,f+c));let C=c+h+f;return E?C.replace(/\r*\n/g,`${f}$&${c}`):C},n},r=(n,c,f)=>typeof n=="function"?n(c):n.wrap(c,f),s=(n,c)=>{if(n===""||n==null)return"";if(e.enabled===!1)return n;if(e.visible===!1)return"";let f=""+n,p=f.includes(` `),h=c.length;for(h>0&&c.includes("unstyle")&&(c=[...new Set(["unstyle",...c])].reverse());h-- >0;)f=r(e.styles[c[h]],f,p);return f},a=(n,c,f)=>{e.styles[n]=t({name:n,codes:c}),(e.keys[f]||(e.keys[f]=[])).push(n),Reflect.defineProperty(e,n,{configurable:!0,enumerable:!0,set(h){e.alias(n,h)},get(){let h=E=>s(E,h.stack);return Reflect.setPrototypeOf(h,e),h.stack=this.stack?this.stack.concat(n):[n],h}})};return a("reset",[0,0],"modifier"),a("bold",[1,22],"modifier"),a("dim",[2,22],"modifier"),a("italic",[3,23],"modifier"),a("underline",[4,24],"modifier"),a("inverse",[7,27],"modifier"),a("hidden",[8,28],"modifier"),a("strikethrough",[9,29],"modifier"),a("black",[30,39],"color"),a("red",[31,39],"color"),a("green",[32,39],"color"),a("yellow",[33,39],"color"),a("blue",[34,39],"color"),a("magenta",[35,39],"color"),a("cyan",[36,39],"color"),a("white",[37,39],"color"),a("gray",[90,39],"color"),a("grey",[90,39],"color"),a("bgBlack",[40,49],"bg"),a("bgRed",[41,49],"bg"),a("bgGreen",[42,49],"bg"),a("bgYellow",[43,49],"bg"),a("bgBlue",[44,49],"bg"),a("bgMagenta",[45,49],"bg"),a("bgCyan",[46,49],"bg"),a("bgWhite",[47,49],"bg"),a("blackBright",[90,39],"bright"),a("redBright",[91,39],"bright"),a("greenBright",[92,39],"bright"),a("yellowBright",[93,39],"bright"),a("blueBright",[94,39],"bright"),a("magentaBright",[95,39],"bright"),a("cyanBright",[96,39],"bright"),a("whiteBright",[97,39],"bright"),a("bgBlackBright",[100,49],"bgBright"),a("bgRedBright",[101,49],"bgBright"),a("bgGreenBright",[102,49],"bgBright"),a("bgYellowBright",[103,49],"bgBright"),a("bgBlueBright",[104,49],"bgBright"),a("bgMagentaBright",[105,49],"bgBright"),a("bgCyanBright",[106,49],"bgBright"),a("bgWhiteBright",[107,49],"bgBright"),e.ansiRegex=Lit,e.hasColor=e.hasAnsi=n=>(e.ansiRegex.lastIndex=0,typeof n=="string"&&n!==""&&e.ansiRegex.test(n)),e.alias=(n,c)=>{let f=typeof c=="string"?e[c]:c;if(typeof f!="function")throw new TypeError("Expected alias to be the name of an existing color (string) or a function");f.stack||(Reflect.defineProperty(f,"name",{value:n}),e.styles[n]=f,f.stack=[n]),Reflect.defineProperty(e,n,{configurable:!0,enumerable:!0,set(p){e.alias(n,p)},get(){let p=h=>s(h,p.stack);return Reflect.setPrototypeOf(p,e),p.stack=this.stack?this.stack.concat(f.stack):f.stack,p}})},e.theme=n=>{if(!Oit(n))throw new TypeError("Expected theme to be an object");for(let c of Object.keys(n))e.alias(c,n[c]);return e},e.alias("unstyle",n=>typeof n=="string"&&n!==""?(e.ansiRegex.lastIndex=0,n.replace(e.ansiRegex,"")):""),e.alias("noop",n=>n),e.none=e.clear=e.noop,e.stripColor=e.unstyle,e.symbols=I0e(),e.define=a,e};EG.exports=C0e();EG.exports.create=C0e});var na=G(hn=>{"use strict";var Mit=Object.prototype.toString,Hc=zu(),w0e=!1,IG=[],B0e={yellow:"blue",cyan:"red",green:"magenta",black:"white",blue:"yellow",red:"cyan",magenta:"green",white:"black"};hn.longest=(e,t)=>e.reduce((r,s)=>Math.max(r,t?s[t].length:s.length),0);hn.hasColor=e=>!!e&&Hc.hasColor(e);var RT=hn.isObject=e=>e!==null&&typeof e=="object"&&!Array.isArray(e);hn.nativeType=e=>Mit.call(e).slice(8,-1).toLowerCase().replace(/\s/g,"");hn.isAsyncFn=e=>hn.nativeType(e)==="asyncfunction";hn.isPrimitive=e=>e!=null&&typeof e!="object"&&typeof e!="function";hn.resolve=(e,t,...r)=>typeof t=="function"?t.call(e,...r):t;hn.scrollDown=(e=[])=>[...e.slice(1),e[0]];hn.scrollUp=(e=[])=>[e.pop(),...e];hn.reorder=(e=[])=>{let t=e.slice();return t.sort((r,s)=>r.index>s.index?1:r.index{let s=e.length,a=r===s?0:r<0?s-1:r,n=e[t];e[t]=e[a],e[a]=n};hn.width=(e,t=80)=>{let r=e&&e.columns?e.columns:t;return e&&typeof e.getWindowSize=="function"&&(r=e.getWindowSize()[0]),process.platform==="win32"?r-1:r};hn.height=(e,t=20)=>{let r=e&&e.rows?e.rows:t;return e&&typeof e.getWindowSize=="function"&&(r=e.getWindowSize()[1]),r};hn.wordWrap=(e,t={})=>{if(!e)return e;typeof t=="number"&&(t={width:t});let{indent:r="",newline:s=` `+r,width:a=80}=t,n=(s+r).match(/[^\S\n]/g)||[];a-=n.length;let c=`.{1,${a}}([\\s\\u200B]+|$)|[^\\s\\u200B]+?([\\s\\u200B]+|$)`,f=e.trim(),p=new RegExp(c,"g"),h=f.match(p)||[];return h=h.map(E=>E.replace(/\n$/,"")),t.padEnd&&(h=h.map(E=>E.padEnd(a," "))),t.padStart&&(h=h.map(E=>E.padStart(a," "))),r+h.join(s)};hn.unmute=e=>{let t=e.stack.find(s=>Hc.keys.color.includes(s));return t?Hc[t]:e.stack.find(s=>s.slice(2)==="bg")?Hc[t.slice(2)]:s=>s};hn.pascal=e=>e?e[0].toUpperCase()+e.slice(1):"";hn.inverse=e=>{if(!e||!e.stack)return e;let t=e.stack.find(s=>Hc.keys.color.includes(s));if(t){let s=Hc["bg"+hn.pascal(t)];return s?s.black:e}let r=e.stack.find(s=>s.slice(0,2)==="bg");return r?Hc[r.slice(2).toLowerCase()]||e:Hc.none};hn.complement=e=>{if(!e||!e.stack)return e;let t=e.stack.find(s=>Hc.keys.color.includes(s)),r=e.stack.find(s=>s.slice(0,2)==="bg");if(t&&!r)return Hc[B0e[t]||t];if(r){let s=r.slice(2).toLowerCase(),a=B0e[s];return a&&Hc["bg"+hn.pascal(a)]||e}return Hc.none};hn.meridiem=e=>{let t=e.getHours(),r=e.getMinutes(),s=t>=12?"pm":"am";t=t%12;let a=t===0?12:t,n=r<10?"0"+r:r;return a+":"+n+" "+s};hn.set=(e={},t="",r)=>t.split(".").reduce((s,a,n,c)=>{let f=c.length-1>n?s[a]||{}:r;return!hn.isObject(f)&&n{let s=e[t]==null?t.split(".").reduce((a,n)=>a&&a[n],e):e[t];return s??r};hn.mixin=(e,t)=>{if(!RT(e))return t;if(!RT(t))return e;for(let r of Object.keys(t)){let s=Object.getOwnPropertyDescriptor(t,r);if(s.hasOwnProperty("value"))if(e.hasOwnProperty(r)&&RT(s.value)){let a=Object.getOwnPropertyDescriptor(e,r);RT(a.value)?e[r]=hn.merge({},e[r],t[r]):Reflect.defineProperty(e,r,s)}else Reflect.defineProperty(e,r,s);else Reflect.defineProperty(e,r,s)}return e};hn.merge=(...e)=>{let t={};for(let r of e)hn.mixin(t,r);return t};hn.mixinEmitter=(e,t)=>{let r=t.constructor.prototype;for(let s of Object.keys(r)){let a=r[s];typeof a=="function"?hn.define(e,s,a.bind(t)):hn.define(e,s,a)}};hn.onExit=e=>{let t=(r,s)=>{w0e||(w0e=!0,IG.forEach(a=>a()),r===!0&&process.exit(128+s))};IG.length===0&&(process.once("SIGTERM",t.bind(null,!0,15)),process.once("SIGINT",t.bind(null,!0,2)),process.once("exit",t)),IG.push(e)};hn.define=(e,t,r)=>{Reflect.defineProperty(e,t,{value:r})};hn.defineExport=(e,t,r)=>{let s;Reflect.defineProperty(e,t,{enumerable:!0,configurable:!0,set(a){s=a},get(){return s?s():r()}})}});var v0e=G(VI=>{"use strict";VI.ctrl={a:"first",b:"backward",c:"cancel",d:"deleteForward",e:"last",f:"forward",g:"reset",i:"tab",k:"cutForward",l:"reset",n:"newItem",m:"cancel",j:"submit",p:"search",r:"remove",s:"save",u:"undo",w:"cutLeft",x:"toggleCursor",v:"paste"};VI.shift={up:"shiftUp",down:"shiftDown",left:"shiftLeft",right:"shiftRight",tab:"prev"};VI.fn={up:"pageUp",down:"pageDown",left:"pageLeft",right:"pageRight",delete:"deleteForward"};VI.option={b:"backward",f:"forward",d:"cutRight",left:"cutLeft",up:"altUp",down:"altDown"};VI.keys={pageup:"pageUp",pagedown:"pageDown",home:"home",end:"end",cancel:"cancel",delete:"deleteForward",backspace:"delete",down:"down",enter:"submit",escape:"cancel",left:"left",space:"space",number:"number",return:"submit",right:"right",tab:"next",up:"up"}});var b0e=G((t3t,D0e)=>{"use strict";var S0e=Ie("readline"),Uit=v0e(),_it=/^(?:\x1b)([a-zA-Z0-9])$/,Hit=/^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/,jit={OP:"f1",OQ:"f2",OR:"f3",OS:"f4","[11~":"f1","[12~":"f2","[13~":"f3","[14~":"f4","[[A":"f1","[[B":"f2","[[C":"f3","[[D":"f4","[[E":"f5","[15~":"f5","[17~":"f6","[18~":"f7","[19~":"f8","[20~":"f9","[21~":"f10","[23~":"f11","[24~":"f12","[A":"up","[B":"down","[C":"right","[D":"left","[E":"clear","[F":"end","[H":"home",OA:"up",OB:"down",OC:"right",OD:"left",OE:"clear",OF:"end",OH:"home","[1~":"home","[2~":"insert","[3~":"delete","[4~":"end","[5~":"pageup","[6~":"pagedown","[[5~":"pageup","[[6~":"pagedown","[7~":"home","[8~":"end","[a":"up","[b":"down","[c":"right","[d":"left","[e":"clear","[2$":"insert","[3$":"delete","[5$":"pageup","[6$":"pagedown","[7$":"home","[8$":"end",Oa:"up",Ob:"down",Oc:"right",Od:"left",Oe:"clear","[2^":"insert","[3^":"delete","[5^":"pageup","[6^":"pagedown","[7^":"home","[8^":"end","[Z":"tab"};function Git(e){return["[a","[b","[c","[d","[e","[2$","[3$","[5$","[6$","[7$","[8$","[Z"].includes(e)}function qit(e){return["Oa","Ob","Oc","Od","Oe","[2^","[3^","[5^","[6^","[7^","[8^"].includes(e)}var TT=(e="",t={})=>{let r,s={name:t.name,ctrl:!1,meta:!1,shift:!1,option:!1,sequence:e,raw:e,...t};if(Buffer.isBuffer(e)?e[0]>127&&e[1]===void 0?(e[0]-=128,e="\x1B"+String(e)):e=String(e):e!==void 0&&typeof e!="string"?e=String(e):e||(e=s.sequence||""),s.sequence=s.sequence||e||s.name,e==="\r")s.raw=void 0,s.name="return";else if(e===` `)s.name="enter";else if(e===" ")s.name="tab";else if(e==="\b"||e==="\x7F"||e==="\x1B\x7F"||e==="\x1B\b")s.name="backspace",s.meta=e.charAt(0)==="\x1B";else if(e==="\x1B"||e==="\x1B\x1B")s.name="escape",s.meta=e.length===2;else if(e===" "||e==="\x1B ")s.name="space",s.meta=e.length===2;else if(e<="")s.name=String.fromCharCode(e.charCodeAt(0)+97-1),s.ctrl=!0;else if(e.length===1&&e>="0"&&e<="9")s.name="number";else if(e.length===1&&e>="a"&&e<="z")s.name=e;else if(e.length===1&&e>="A"&&e<="Z")s.name=e.toLowerCase(),s.shift=!0;else if(r=_it.exec(e))s.meta=!0,s.shift=/^[A-Z]$/.test(r[1]);else if(r=Hit.exec(e)){let a=[...e];a[0]==="\x1B"&&a[1]==="\x1B"&&(s.option=!0);let n=[r[1],r[2],r[4],r[6]].filter(Boolean).join(""),c=(r[3]||r[5]||1)-1;s.ctrl=!!(c&4),s.meta=!!(c&10),s.shift=!!(c&1),s.code=n,s.name=jit[n],s.shift=Git(n)||s.shift,s.ctrl=qit(n)||s.ctrl}return s};TT.listen=(e={},t)=>{let{stdin:r}=e;if(!r||r!==process.stdin&&!r.isTTY)throw new Error("Invalid stream passed");let s=S0e.createInterface({terminal:!0,input:r});S0e.emitKeypressEvents(r,s);let a=(f,p)=>t(f,TT(f,p),s),n=r.isRaw;return r.isTTY&&r.setRawMode(!0),r.on("keypress",a),s.resume(),()=>{r.isTTY&&r.setRawMode(n),r.removeListener("keypress",a),s.pause(),s.close()}};TT.action=(e,t,r)=>{let s={...Uit,...r};return t.ctrl?(t.action=s.ctrl[t.name],t):t.option&&s.option?(t.action=s.option[t.name],t):t.shift?(t.action=s.shift[t.name],t):(t.action=s.keys[t.name],t)};D0e.exports=TT});var x0e=G((r3t,P0e)=>{"use strict";P0e.exports=e=>{e.timers=e.timers||{};let t=e.options.timers;if(t)for(let r of Object.keys(t)){let s=t[r];typeof s=="number"&&(s={interval:s}),Wit(e,r,s)}};function Wit(e,t,r={}){let s=e.timers[t]={name:t,start:Date.now(),ms:0,tick:0},a=r.interval||120;s.frames=r.frames||[],s.loading=!0;let n=setInterval(()=>{s.ms=Date.now()-s.start,s.tick++,e.render()},a);return s.stop=()=>{s.loading=!1,clearInterval(n)},Reflect.defineProperty(s,"interval",{value:n}),e.once("close",()=>s.stop()),s.stop}});var Q0e=G((n3t,k0e)=>{"use strict";var{define:Yit,width:Vit}=na(),CG=class{constructor(t){let r=t.options;Yit(this,"_prompt",t),this.type=t.type,this.name=t.name,this.message="",this.header="",this.footer="",this.error="",this.hint="",this.input="",this.cursor=0,this.index=0,this.lines=0,this.tick=0,this.prompt="",this.buffer="",this.width=Vit(r.stdout||process.stdout),Object.assign(this,r),this.name=this.name||this.message,this.message=this.message||this.name,this.symbols=t.symbols,this.styles=t.styles,this.required=new Set,this.cancelled=!1,this.submitted=!1}clone(){let t={...this};return t.status=this.status,t.buffer=Buffer.from(t.buffer),delete t.clone,t}set color(t){this._color=t}get color(){let t=this.prompt.styles;if(this.cancelled)return t.cancelled;if(this.submitted)return t.submitted;let r=this._color||t[this.status];return typeof r=="function"?r:t.pending}set loading(t){this._loading=t}get loading(){return typeof this._loading=="boolean"?this._loading:this.loadingChoices?"choices":!1}get status(){return this.cancelled?"cancelled":this.submitted?"submitted":"pending"}};k0e.exports=CG});var T0e=G((i3t,R0e)=>{"use strict";var wG=na(),Io=zu(),BG={default:Io.noop,noop:Io.noop,set inverse(e){this._inverse=e},get inverse(){return this._inverse||wG.inverse(this.primary)},set complement(e){this._complement=e},get complement(){return this._complement||wG.complement(this.primary)},primary:Io.cyan,success:Io.green,danger:Io.magenta,strong:Io.bold,warning:Io.yellow,muted:Io.dim,disabled:Io.gray,dark:Io.dim.gray,underline:Io.underline,set info(e){this._info=e},get info(){return this._info||this.primary},set em(e){this._em=e},get em(){return this._em||this.primary.underline},set heading(e){this._heading=e},get heading(){return this._heading||this.muted.underline},set pending(e){this._pending=e},get pending(){return this._pending||this.primary},set submitted(e){this._submitted=e},get submitted(){return this._submitted||this.success},set cancelled(e){this._cancelled=e},get cancelled(){return this._cancelled||this.danger},set typing(e){this._typing=e},get typing(){return this._typing||this.dim},set placeholder(e){this._placeholder=e},get placeholder(){return this._placeholder||this.primary.dim},set highlight(e){this._highlight=e},get highlight(){return this._highlight||this.inverse}};BG.merge=(e={})=>{e.styles&&typeof e.styles.enabled=="boolean"&&(Io.enabled=e.styles.enabled),e.styles&&typeof e.styles.visible=="boolean"&&(Io.visible=e.styles.visible);let t=wG.merge({},BG,e.styles);delete t.merge;for(let r of Object.keys(Io))t.hasOwnProperty(r)||Reflect.defineProperty(t,r,{get:()=>Io[r]});for(let r of Object.keys(Io.styles))t.hasOwnProperty(r)||Reflect.defineProperty(t,r,{get:()=>Io[r]});return t};R0e.exports=BG});var N0e=G((s3t,F0e)=>{"use strict";var vG=process.platform==="win32",Kp=zu(),Jit=na(),SG={...Kp.symbols,upDownDoubleArrow:"\u21D5",upDownDoubleArrow2:"\u2B0D",upDownArrow:"\u2195",asterisk:"*",asterism:"\u2042",bulletWhite:"\u25E6",electricArrow:"\u2301",ellipsisLarge:"\u22EF",ellipsisSmall:"\u2026",fullBlock:"\u2588",identicalTo:"\u2261",indicator:Kp.symbols.check,leftAngle:"\u2039",mark:"\u203B",minus:"\u2212",multiplication:"\xD7",obelus:"\xF7",percent:"%",pilcrow:"\xB6",pilcrow2:"\u2761",pencilUpRight:"\u2710",pencilDownRight:"\u270E",pencilRight:"\u270F",plus:"+",plusMinus:"\xB1",pointRight:"\u261E",rightAngle:"\u203A",section:"\xA7",hexagon:{off:"\u2B21",on:"\u2B22",disabled:"\u2B22"},ballot:{on:"\u2611",off:"\u2610",disabled:"\u2612"},stars:{on:"\u2605",off:"\u2606",disabled:"\u2606"},folder:{on:"\u25BC",off:"\u25B6",disabled:"\u25B6"},prefix:{pending:Kp.symbols.question,submitted:Kp.symbols.check,cancelled:Kp.symbols.cross},separator:{pending:Kp.symbols.pointerSmall,submitted:Kp.symbols.middot,cancelled:Kp.symbols.middot},radio:{off:vG?"( )":"\u25EF",on:vG?"(*)":"\u25C9",disabled:vG?"(|)":"\u24BE"},numbers:["\u24EA","\u2460","\u2461","\u2462","\u2463","\u2464","\u2465","\u2466","\u2467","\u2468","\u2469","\u246A","\u246B","\u246C","\u246D","\u246E","\u246F","\u2470","\u2471","\u2472","\u2473","\u3251","\u3252","\u3253","\u3254","\u3255","\u3256","\u3257","\u3258","\u3259","\u325A","\u325B","\u325C","\u325D","\u325E","\u325F","\u32B1","\u32B2","\u32B3","\u32B4","\u32B5","\u32B6","\u32B7","\u32B8","\u32B9","\u32BA","\u32BB","\u32BC","\u32BD","\u32BE","\u32BF"]};SG.merge=e=>{let t=Jit.merge({},Kp.symbols,SG,e.symbols);return delete t.merge,t};F0e.exports=SG});var L0e=G((o3t,O0e)=>{"use strict";var Kit=T0e(),zit=N0e(),Xit=na();O0e.exports=e=>{e.options=Xit.merge({},e.options.theme,e.options),e.symbols=zit.merge(e.options),e.styles=Kit.merge(e.options)}});var j0e=G((_0e,H0e)=>{"use strict";var M0e=process.env.TERM_PROGRAM==="Apple_Terminal",Zit=zu(),DG=na(),Xu=H0e.exports=_0e,_i="\x1B[",U0e="\x07",bG=!1,H0=Xu.code={bell:U0e,beep:U0e,beginning:`${_i}G`,down:`${_i}J`,esc:_i,getPosition:`${_i}6n`,hide:`${_i}?25l`,line:`${_i}2K`,lineEnd:`${_i}K`,lineStart:`${_i}1K`,restorePosition:_i+(M0e?"8":"u"),savePosition:_i+(M0e?"7":"s"),screen:`${_i}2J`,show:`${_i}?25h`,up:`${_i}1J`},Sm=Xu.cursor={get hidden(){return bG},hide(){return bG=!0,H0.hide},show(){return bG=!1,H0.show},forward:(e=1)=>`${_i}${e}C`,backward:(e=1)=>`${_i}${e}D`,nextLine:(e=1)=>`${_i}E`.repeat(e),prevLine:(e=1)=>`${_i}F`.repeat(e),up:(e=1)=>e?`${_i}${e}A`:"",down:(e=1)=>e?`${_i}${e}B`:"",right:(e=1)=>e?`${_i}${e}C`:"",left:(e=1)=>e?`${_i}${e}D`:"",to(e,t){return t?`${_i}${t+1};${e+1}H`:`${_i}${e+1}G`},move(e=0,t=0){let r="";return r+=e<0?Sm.left(-e):e>0?Sm.right(e):"",r+=t<0?Sm.up(-t):t>0?Sm.down(t):"",r},restore(e={}){let{after:t,cursor:r,initial:s,input:a,prompt:n,size:c,value:f}=e;if(s=DG.isPrimitive(s)?String(s):"",a=DG.isPrimitive(a)?String(a):"",f=DG.isPrimitive(f)?String(f):"",c){let p=Xu.cursor.up(c)+Xu.cursor.to(n.length),h=a.length-r;return h>0&&(p+=Xu.cursor.left(h)),p}if(f||t){let p=!a&&s?-s.length:-a.length+r;return t&&(p-=t.length),a===""&&s&&!n.includes(s)&&(p+=s.length),Xu.cursor.move(p)}}},PG=Xu.erase={screen:H0.screen,up:H0.up,down:H0.down,line:H0.line,lineEnd:H0.lineEnd,lineStart:H0.lineStart,lines(e){let t="";for(let r=0;r{if(!t)return PG.line+Sm.to(0);let r=n=>[...Zit.unstyle(n)].length,s=e.split(/\r?\n/),a=0;for(let n of s)a+=1+Math.floor(Math.max(r(n)-1,0)/t);return(PG.line+Sm.prevLine()).repeat(a-1)+PG.line+Sm.to(0)}});var JI=G((a3t,q0e)=>{"use strict";var $it=Ie("events"),G0e=zu(),xG=b0e(),est=x0e(),tst=Q0e(),rst=L0e(),Cl=na(),Dm=j0e(),kG=class e extends $it{constructor(t={}){super(),this.name=t.name,this.type=t.type,this.options=t,rst(this),est(this),this.state=new tst(this),this.initial=[t.initial,t.default].find(r=>r!=null),this.stdout=t.stdout||process.stdout,this.stdin=t.stdin||process.stdin,this.scale=t.scale||1,this.term=this.options.term||process.env.TERM_PROGRAM,this.margin=ist(this.options.margin),this.setMaxListeners(0),nst(this)}async keypress(t,r={}){this.keypressed=!0;let s=xG.action(t,xG(t,r),this.options.actions);this.state.keypress=s,this.emit("keypress",t,s),this.emit("state",this.state.clone());let a=this.options[s.action]||this[s.action]||this.dispatch;if(typeof a=="function")return await a.call(this,t,s);this.alert()}alert(){delete this.state.alert,this.options.show===!1?this.emit("alert"):this.stdout.write(Dm.code.beep)}cursorHide(){this.stdout.write(Dm.cursor.hide()),Cl.onExit(()=>this.cursorShow())}cursorShow(){this.stdout.write(Dm.cursor.show())}write(t){t&&(this.stdout&&this.state.show!==!1&&this.stdout.write(t),this.state.buffer+=t)}clear(t=0){let r=this.state.buffer;this.state.buffer="",!(!r&&!t||this.options.show===!1)&&this.stdout.write(Dm.cursor.down(t)+Dm.clear(r,this.width))}restore(){if(this.state.closed||this.options.show===!1)return;let{prompt:t,after:r,rest:s}=this.sections(),{cursor:a,initial:n="",input:c="",value:f=""}=this,p=this.state.size=s.length,h={after:r,cursor:a,initial:n,input:c,prompt:t,size:p,value:f},E=Dm.cursor.restore(h);E&&this.stdout.write(E)}sections(){let{buffer:t,input:r,prompt:s}=this.state;s=G0e.unstyle(s);let a=G0e.unstyle(t),n=a.indexOf(s),c=a.slice(0,n),p=a.slice(n).split(` `),h=p[0],E=p[p.length-1],S=(s+(r?" "+r:"")).length,x=St.call(this,this.value),this.result=()=>s.call(this,this.value),typeof r.initial=="function"&&(this.initial=await r.initial.call(this,this)),typeof r.onRun=="function"&&await r.onRun.call(this,this),typeof r.onSubmit=="function"){let a=r.onSubmit.bind(this),n=this.submit.bind(this);delete this.options.onSubmit,this.submit=async()=>(await a(this.name,this.value,this),n())}await this.start(),await this.render()}render(){throw new Error("expected prompt to have a custom render method")}run(){return new Promise(async(t,r)=>{if(this.once("submit",t),this.once("cancel",r),await this.skip())return this.render=()=>{},this.submit();await this.initialize(),this.emit("run")})}async element(t,r,s){let{options:a,state:n,symbols:c,timers:f}=this,p=f&&f[t];n.timer=p;let h=a[t]||n[t]||c[t],E=r&&r[t]!=null?r[t]:await h;if(E==="")return E;let C=await this.resolve(E,n,r,s);return!C&&r&&r[t]?this.resolve(h,n,r,s):C}async prefix(){let t=await this.element("prefix")||this.symbols,r=this.timers&&this.timers.prefix,s=this.state;return s.timer=r,Cl.isObject(t)&&(t=t[s.status]||t.pending),Cl.hasColor(t)?t:(this.styles[s.status]||this.styles.pending)(t)}async message(){let t=await this.element("message");return Cl.hasColor(t)?t:this.styles.strong(t)}async separator(){let t=await this.element("separator")||this.symbols,r=this.timers&&this.timers.separator,s=this.state;s.timer=r;let a=t[s.status]||t.pending||s.separator,n=await this.resolve(a,s);return Cl.isObject(n)&&(n=n[s.status]||n.pending),Cl.hasColor(n)?n:this.styles.muted(n)}async pointer(t,r){let s=await this.element("pointer",t,r);if(typeof s=="string"&&Cl.hasColor(s))return s;if(s){let a=this.styles,n=this.index===r,c=n?a.primary:h=>h,f=await this.resolve(s[n?"on":"off"]||s,this.state),p=Cl.hasColor(f)?f:c(f);return n?p:" ".repeat(f.length)}}async indicator(t,r){let s=await this.element("indicator",t,r);if(typeof s=="string"&&Cl.hasColor(s))return s;if(s){let a=this.styles,n=t.enabled===!0,c=n?a.success:a.dark,f=s[n?"on":"off"]||s;return Cl.hasColor(f)?f:c(f)}return""}body(){return null}footer(){if(this.state.status==="pending")return this.element("footer")}header(){if(this.state.status==="pending")return this.element("header")}async hint(){if(this.state.status==="pending"&&!this.isValue(this.state.input)){let t=await this.element("hint");return Cl.hasColor(t)?t:this.styles.muted(t)}}error(t){return this.state.submitted?"":t||this.state.error}format(t){return t}result(t){return t}validate(t){return this.options.required===!0?this.isValue(t):!0}isValue(t){return t!=null&&t!==""}resolve(t,...r){return Cl.resolve(this,t,...r)}get base(){return e.prototype}get style(){return this.styles[this.state.status]}get height(){return this.options.rows||Cl.height(this.stdout,25)}get width(){return this.options.columns||Cl.width(this.stdout,80)}get size(){return{width:this.width,height:this.height}}set cursor(t){this.state.cursor=t}get cursor(){return this.state.cursor}set input(t){this.state.input=t}get input(){return this.state.input}set value(t){this.state.value=t}get value(){let{input:t,value:r}=this.state,s=[r,t].find(this.isValue.bind(this));return this.isValue(s)?s:this.initial}static get prompt(){return t=>new this(t).run()}};function nst(e){let t=a=>e[a]===void 0||typeof e[a]=="function",r=["actions","choices","initial","margin","roles","styles","symbols","theme","timers","value"],s=["body","footer","error","header","hint","indicator","message","prefix","separator","skip"];for(let a of Object.keys(e.options)){if(r.includes(a)||/^on[A-Z]/.test(a))continue;let n=e.options[a];typeof n=="function"&&t(a)?s.includes(a)||(e[a]=n.bind(e)):typeof e[a]!="function"&&(e[a]=n)}}function ist(e){typeof e=="number"&&(e=[e,e,e,e]);let t=[].concat(e||[]),r=a=>a%2===0?` `:" ",s=[];for(let a=0;a<4;a++){let n=r(a);t[a]?s.push(n.repeat(t[a])):s.push("")}return s}q0e.exports=kG});var V0e=G((l3t,Y0e)=>{"use strict";var sst=na(),W0e={default(e,t){return t},checkbox(e,t){throw new Error("checkbox role is not implemented yet")},editable(e,t){throw new Error("editable role is not implemented yet")},expandable(e,t){throw new Error("expandable role is not implemented yet")},heading(e,t){return t.disabled="",t.indicator=[t.indicator," "].find(r=>r!=null),t.message=t.message||"",t},input(e,t){throw new Error("input role is not implemented yet")},option(e,t){return W0e.default(e,t)},radio(e,t){throw new Error("radio role is not implemented yet")},separator(e,t){return t.disabled="",t.indicator=[t.indicator," "].find(r=>r!=null),t.message=t.message||e.symbols.line.repeat(5),t},spacer(e,t){return t}};Y0e.exports=(e,t={})=>{let r=sst.merge({},W0e,t.roles);return r[e]||r.default}});var Wv=G((c3t,z0e)=>{"use strict";var ost=zu(),ast=JI(),lst=V0e(),FT=na(),{reorder:QG,scrollUp:cst,scrollDown:ust,isObject:J0e,swap:fst}=FT,RG=class extends ast{constructor(t){super(t),this.cursorHide(),this.maxSelected=t.maxSelected||1/0,this.multiple=t.multiple||!1,this.initial=t.initial||0,this.delay=t.delay||0,this.longest=0,this.num=""}async initialize(){typeof this.options.initial=="function"&&(this.initial=await this.options.initial.call(this)),await this.reset(!0),await super.initialize()}async reset(){let{choices:t,initial:r,autofocus:s,suggest:a}=this.options;if(this.state._choices=[],this.state.choices=[],this.choices=await Promise.all(await this.toChoices(t)),this.choices.forEach(n=>n.enabled=!1),typeof a!="function"&&this.selectable.length===0)throw new Error("At least one choice must be selectable");J0e(r)&&(r=Object.keys(r)),Array.isArray(r)?(s!=null&&(this.index=this.findIndex(s)),r.forEach(n=>this.enable(this.find(n))),await this.render()):(s!=null&&(r=s),typeof r=="string"&&(r=this.findIndex(r)),typeof r=="number"&&r>-1&&(this.index=Math.max(0,Math.min(r,this.choices.length)),this.enable(this.find(this.index)))),this.isDisabled(this.focused)&&await this.down()}async toChoices(t,r){this.state.loadingChoices=!0;let s=[],a=0,n=async(c,f)=>{typeof c=="function"&&(c=await c.call(this)),c instanceof Promise&&(c=await c);for(let p=0;p(this.state.loadingChoices=!1,c))}async toChoice(t,r,s){if(typeof t=="function"&&(t=await t.call(this,this)),t instanceof Promise&&(t=await t),typeof t=="string"&&(t={name:t}),t.normalized)return t;t.normalized=!0;let a=t.value;if(t=lst(t.role,this.options)(this,t),typeof t.disabled=="string"&&!t.hint&&(t.hint=t.disabled,t.disabled=!0),t.disabled===!0&&t.hint==null&&(t.hint="(disabled)"),t.index!=null)return t;t.name=t.name||t.key||t.title||t.value||t.message,t.message=t.message||t.name||"",t.value=[t.value,t.name].find(this.isValue.bind(this)),t.input="",t.index=r,t.cursor=0,FT.define(t,"parent",s),t.level=s?s.level+1:1,t.indent==null&&(t.indent=s?s.indent+" ":t.indent||""),t.path=s?s.path+"."+t.name:t.name,t.enabled=!!(this.multiple&&!this.isDisabled(t)&&(t.enabled||this.isSelected(t))),this.isDisabled(t)||(this.longest=Math.max(this.longest,ost.unstyle(t.message).length));let c={...t};return t.reset=(f=c.input,p=c.value)=>{for(let h of Object.keys(c))t[h]=c[h];t.input=f,t.value=p},a==null&&typeof t.initial=="function"&&(t.input=await t.initial.call(this,this.state,t,r)),t}async onChoice(t,r){this.emit("choice",t,r,this),typeof t.onChoice=="function"&&await t.onChoice.call(this,this.state,t,r)}async addChoice(t,r,s){let a=await this.toChoice(t,r,s);return this.choices.push(a),this.index=this.choices.length-1,this.limit=this.choices.length,a}async newItem(t,r,s){let a={name:"New choice name?",editable:!0,newChoice:!0,...t},n=await this.addChoice(a,r,s);return n.updateChoice=()=>{delete n.newChoice,n.name=n.message=n.input,n.input="",n.cursor=0},this.render()}indent(t){return t.indent==null?t.level>1?" ".repeat(t.level-1):"":t.indent}dispatch(t,r){if(this.multiple&&this[r.name])return this[r.name]();this.alert()}focus(t,r){return typeof r!="boolean"&&(r=t.enabled),r&&!t.enabled&&this.selected.length>=this.maxSelected?this.alert():(this.index=t.index,t.enabled=r&&!this.isDisabled(t),t)}space(){return this.multiple?(this.toggle(this.focused),this.render()):this.alert()}a(){if(this.maxSelectedr.enabled);return this.choices.forEach(r=>r.enabled=!t),this.render()}i(){return this.choices.length-this.selected.length>this.maxSelected?this.alert():(this.choices.forEach(t=>t.enabled=!t.enabled),this.render())}g(t=this.focused){return this.choices.some(r=>!!r.parent)?(this.toggle(t.parent&&!t.choices?t.parent:t),this.render()):this.a()}toggle(t,r){if(!t.enabled&&this.selected.length>=this.maxSelected)return this.alert();typeof r!="boolean"&&(r=!t.enabled),t.enabled=r,t.choices&&t.choices.forEach(a=>this.toggle(a,r));let s=t.parent;for(;s;){let a=s.choices.filter(n=>this.isDisabled(n));s.enabled=a.every(n=>n.enabled===!0),s=s.parent}return K0e(this,this.choices),this.emit("toggle",t,this),t}enable(t){return this.selected.length>=this.maxSelected?this.alert():(t.enabled=!this.isDisabled(t),t.choices&&t.choices.forEach(this.enable.bind(this)),t)}disable(t){return t.enabled=!1,t.choices&&t.choices.forEach(this.disable.bind(this)),t}number(t){this.num+=t;let r=s=>{let a=Number(s);if(a>this.choices.length-1)return this.alert();let n=this.focused,c=this.choices.find(f=>a===f.index);if(!c.enabled&&this.selected.length>=this.maxSelected)return this.alert();if(this.visible.indexOf(c)===-1){let f=QG(this.choices),p=f.indexOf(c);if(n.index>p){let h=f.slice(p,p+this.limit),E=f.filter(C=>!h.includes(C));this.choices=h.concat(E)}else{let h=p-this.limit+1;this.choices=f.slice(h).concat(f.slice(0,h))}}return this.index=this.choices.indexOf(c),this.toggle(this.focused),this.render()};return clearTimeout(this.numberTimeout),new Promise(s=>{let a=this.choices.length,n=this.num,c=(f=!1,p)=>{clearTimeout(this.numberTimeout),f&&(p=r(n)),this.num="",s(p)};if(n==="0"||n.length===1&&+(n+"0")>a)return c(!0);if(Number(n)>a)return c(!1,this.alert());this.numberTimeout=setTimeout(()=>c(!0),this.delay)})}home(){return this.choices=QG(this.choices),this.index=0,this.render()}end(){let t=this.choices.length-this.limit,r=QG(this.choices);return this.choices=r.slice(t).concat(r.slice(0,t)),this.index=this.limit-1,this.render()}first(){return this.index=0,this.render()}last(){return this.index=this.visible.length-1,this.render()}prev(){return this.visible.length<=1?this.alert():this.up()}next(){return this.visible.length<=1?this.alert():this.down()}right(){return this.cursor>=this.input.length?this.alert():(this.cursor++,this.render())}left(){return this.cursor<=0?this.alert():(this.cursor--,this.render())}up(){let t=this.choices.length,r=this.visible.length,s=this.index;return this.options.scroll===!1&&s===0?this.alert():t>r&&s===0?this.scrollUp():(this.index=(s-1%t+t)%t,this.isDisabled()?this.up():this.render())}down(){let t=this.choices.length,r=this.visible.length,s=this.index;return this.options.scroll===!1&&s===r-1?this.alert():t>r&&s===r-1?this.scrollDown():(this.index=(s+1)%t,this.isDisabled()?this.down():this.render())}scrollUp(t=0){return this.choices=cst(this.choices),this.index=t,this.isDisabled()?this.up():this.render()}scrollDown(t=this.visible.length-1){return this.choices=ust(this.choices),this.index=t,this.isDisabled()?this.down():this.render()}async shiftUp(){if(this.options.sort===!0){this.sorting=!0,this.swap(this.index-1),await this.up(),this.sorting=!1;return}return this.scrollUp(this.index)}async shiftDown(){if(this.options.sort===!0){this.sorting=!0,this.swap(this.index+1),await this.down(),this.sorting=!1;return}return this.scrollDown(this.index)}pageUp(){return this.visible.length<=1?this.alert():(this.limit=Math.max(this.limit-1,0),this.index=Math.min(this.limit-1,this.index),this._limit=this.limit,this.isDisabled()?this.up():this.render())}pageDown(){return this.visible.length>=this.choices.length?this.alert():(this.index=Math.max(0,this.index),this.limit=Math.min(this.limit+1,this.choices.length),this._limit=this.limit,this.isDisabled()?this.down():this.render())}swap(t){fst(this.choices,this.index,t)}isDisabled(t=this.focused){return t&&["disabled","collapsed","hidden","completing","readonly"].some(s=>t[s]===!0)?!0:t&&t.role==="heading"}isEnabled(t=this.focused){if(Array.isArray(t))return t.every(r=>this.isEnabled(r));if(t.choices){let r=t.choices.filter(s=>!this.isDisabled(s));return t.enabled&&r.every(s=>this.isEnabled(s))}return t.enabled&&!this.isDisabled(t)}isChoice(t,r){return t.name===r||t.index===Number(r)}isSelected(t){return Array.isArray(this.initial)?this.initial.some(r=>this.isChoice(t,r)):this.isChoice(t,this.initial)}map(t=[],r="value"){return[].concat(t||[]).reduce((s,a)=>(s[a]=this.find(a,r),s),{})}filter(t,r){let a=typeof t=="function"?t:(f,p)=>[f.name,p].includes(t),c=(this.options.multiple?this.state._choices:this.choices).filter(a);return r?c.map(f=>f[r]):c}find(t,r){if(J0e(t))return r?t[r]:t;let a=typeof t=="function"?t:(c,f)=>[c.name,f].includes(t),n=this.choices.find(a);if(n)return r?n[r]:n}findIndex(t){return this.choices.indexOf(this.find(t))}async submit(){let t=this.focused;if(!t)return this.alert();if(t.newChoice)return t.input?(t.updateChoice(),this.render()):this.alert();if(this.choices.some(c=>c.newChoice))return this.alert();let{reorder:r,sort:s}=this.options,a=this.multiple===!0,n=this.selected;return n===void 0?this.alert():(Array.isArray(n)&&r!==!1&&s!==!0&&(n=FT.reorder(n)),this.value=a?n.map(c=>c.name):n.name,super.submit())}set choices(t=[]){this.state._choices=this.state._choices||[],this.state.choices=t;for(let r of t)this.state._choices.some(s=>s.name===r.name)||this.state._choices.push(r);if(!this._initial&&this.options.initial){this._initial=!0;let r=this.initial;if(typeof r=="string"||typeof r=="number"){let s=this.find(r);s&&(this.initial=s.index,this.focus(s,!0))}}}get choices(){return K0e(this,this.state.choices||[])}set visible(t){this.state.visible=t}get visible(){return(this.state.visible||this.choices).slice(0,this.limit)}set limit(t){this.state.limit=t}get limit(){let{state:t,options:r,choices:s}=this,a=t.limit||this._limit||r.limit||s.length;return Math.min(a,this.height)}set value(t){super.value=t}get value(){return typeof super.value!="string"&&super.value===this.initial?this.input:super.value}set index(t){this.state.index=t}get index(){return Math.max(0,this.state?this.state.index:0)}get enabled(){return this.filter(this.isEnabled.bind(this))}get focused(){let t=this.choices[this.index];return t&&this.state.submitted&&this.multiple!==!0&&(t.enabled=!0),t}get selectable(){return this.choices.filter(t=>!this.isDisabled(t))}get selected(){return this.multiple?this.enabled:this.focused}};function K0e(e,t){if(t instanceof Promise)return t;if(typeof t=="function"){if(FT.isAsyncFn(t))return t;t=t.call(e,e)}for(let r of t){if(Array.isArray(r.choices)){let s=r.choices.filter(a=>!e.isDisabled(a));r.enabled=s.every(a=>a.enabled===!0)}e.isDisabled(r)===!0&&delete r.enabled}return t}z0e.exports=RG});var j0=G((u3t,X0e)=>{"use strict";var Ast=Wv(),TG=na(),FG=class extends Ast{constructor(t){super(t),this.emptyError=this.options.emptyError||"No items were selected"}async dispatch(t,r){if(this.multiple)return this[r.name]?await this[r.name](t,r):await super.dispatch(t,r);this.alert()}separator(){if(this.options.separator)return super.separator();let t=this.styles.muted(this.symbols.ellipsis);return this.state.submitted?super.separator():t}pointer(t,r){return!this.multiple||this.options.pointer?super.pointer(t,r):""}indicator(t,r){return this.multiple?super.indicator(t,r):""}choiceMessage(t,r){let s=this.resolve(t.message,this.state,t,r);return t.role==="heading"&&!TG.hasColor(s)&&(s=this.styles.strong(s)),this.resolve(s,this.state,t,r)}choiceSeparator(){return":"}async renderChoice(t,r){await this.onChoice(t,r);let s=this.index===r,a=await this.pointer(t,r),n=await this.indicator(t,r)+(t.pad||""),c=await this.resolve(t.hint,this.state,t,r);c&&!TG.hasColor(c)&&(c=this.styles.muted(c));let f=this.indent(t),p=await this.choiceMessage(t,r),h=()=>[this.margin[3],f+a+n,p,this.margin[1],c].filter(Boolean).join(" ");return t.role==="heading"?h():t.disabled?(TG.hasColor(p)||(p=this.styles.disabled(p)),h()):(s&&(p=this.styles.em(p)),h())}async renderChoices(){if(this.state.loading==="choices")return this.styles.warning("Loading choices");if(this.state.submitted)return"";let t=this.visible.map(async(n,c)=>await this.renderChoice(n,c)),r=await Promise.all(t);r.length||r.push(this.styles.danger("No matching choices"));let s=this.margin[0]+r.join(` `),a;return this.options.choicesHeader&&(a=await this.resolve(this.options.choicesHeader,this.state)),[a,s].filter(Boolean).join(` `)}format(){return!this.state.submitted||this.state.cancelled?"":Array.isArray(this.selected)?this.selected.map(t=>this.styles.primary(t.name)).join(", "):this.styles.primary(this.selected.name)}async render(){let{submitted:t,size:r}=this.state,s="",a=await this.header(),n=await this.prefix(),c=await this.separator(),f=await this.message();this.options.promptLine!==!1&&(s=[n,f,c,""].join(" "),this.state.prompt=s);let p=await this.format(),h=await this.error()||await this.hint(),E=await this.renderChoices(),C=await this.footer();p&&(s+=p),h&&!s.includes(h)&&(s+=" "+h),t&&!p&&!E.trim()&&this.multiple&&this.emptyError!=null&&(s+=this.styles.danger(this.emptyError)),this.clear(r),this.write([a,s,E,C].filter(Boolean).join(` `)),this.write(this.margin[2]),this.restore()}};X0e.exports=FG});var $0e=G((f3t,Z0e)=>{"use strict";var pst=j0(),hst=(e,t)=>{let r=e.toLowerCase();return s=>{let n=s.toLowerCase().indexOf(r),c=t(s.slice(n,n+r.length));return n>=0?s.slice(0,n)+c+s.slice(n+r.length):s}},NG=class extends pst{constructor(t){super(t),this.cursorShow()}moveCursor(t){this.state.cursor+=t}dispatch(t){return this.append(t)}space(t){return this.options.multiple?super.space(t):this.append(t)}append(t){let{cursor:r,input:s}=this.state;return this.input=s.slice(0,r)+t+s.slice(r),this.moveCursor(1),this.complete()}delete(){let{cursor:t,input:r}=this.state;return r?(this.input=r.slice(0,t-1)+r.slice(t),this.moveCursor(-1),this.complete()):this.alert()}deleteForward(){let{cursor:t,input:r}=this.state;return r[t]===void 0?this.alert():(this.input=`${r}`.slice(0,t)+`${r}`.slice(t+1),this.complete())}number(t){return this.append(t)}async complete(){this.completing=!0,this.choices=await this.suggest(this.input,this.state._choices),this.state.limit=void 0,this.index=Math.min(Math.max(this.visible.length-1,0),this.index),await this.render(),this.completing=!1}suggest(t=this.input,r=this.state._choices){if(typeof this.options.suggest=="function")return this.options.suggest.call(this,t,r);let s=t.toLowerCase();return r.filter(a=>a.message.toLowerCase().includes(s))}pointer(){return""}format(){if(!this.focused)return this.input;if(this.options.multiple&&this.state.submitted)return this.selected.map(t=>this.styles.primary(t.message)).join(", ");if(this.state.submitted){let t=this.value=this.input=this.focused.value;return this.styles.primary(t)}return this.input}async render(){if(this.state.status!=="pending")return super.render();let t=this.options.highlight?this.options.highlight.bind(this):this.styles.placeholder,r=hst(this.input,t),s=this.choices;this.choices=s.map(a=>({...a,message:r(a.message)})),await super.render(),this.choices=s}submit(){return this.options.multiple&&(this.value=this.selected.map(t=>t.name)),super.submit()}};Z0e.exports=NG});var LG=G((A3t,ede)=>{"use strict";var OG=na();ede.exports=(e,t={})=>{e.cursorHide();let{input:r="",initial:s="",pos:a,showCursor:n=!0,color:c}=t,f=c||e.styles.placeholder,p=OG.inverse(e.styles.primary),h=T=>p(e.styles.black(T)),E=r,C=" ",S=h(C);if(e.blink&&e.blink.off===!0&&(h=T=>T,S=""),n&&a===0&&s===""&&r==="")return h(C);if(n&&a===0&&(r===s||r===""))return h(s[0])+f(s.slice(1));s=OG.isPrimitive(s)?`${s}`:"",r=OG.isPrimitive(r)?`${r}`:"";let x=s&&s.startsWith(r)&&s!==r,I=x?h(s[r.length]):S;if(a!==r.length&&n===!0&&(E=r.slice(0,a)+h(r[a])+r.slice(a+1),I=""),n===!1&&(I=""),x){let T=e.styles.unstyle(E+I);return E+I+f(s.slice(T.length))}return E+I}});var NT=G((p3t,tde)=>{"use strict";var dst=zu(),gst=j0(),mst=LG(),MG=class extends gst{constructor(t){super({...t,multiple:!0}),this.type="form",this.initial=this.options.initial,this.align=[this.options.align,"right"].find(r=>r!=null),this.emptyError="",this.values={}}async reset(t){return await super.reset(),t===!0&&(this._index=this.index),this.index=this._index,this.values={},this.choices.forEach(r=>r.reset&&r.reset()),this.render()}dispatch(t){return!!t&&this.append(t)}append(t){let r=this.focused;if(!r)return this.alert();let{cursor:s,input:a}=r;return r.value=r.input=a.slice(0,s)+t+a.slice(s),r.cursor++,this.render()}delete(){let t=this.focused;if(!t||t.cursor<=0)return this.alert();let{cursor:r,input:s}=t;return t.value=t.input=s.slice(0,r-1)+s.slice(r),t.cursor--,this.render()}deleteForward(){let t=this.focused;if(!t)return this.alert();let{cursor:r,input:s}=t;if(s[r]===void 0)return this.alert();let a=`${s}`.slice(0,r)+`${s}`.slice(r+1);return t.value=t.input=a,this.render()}right(){let t=this.focused;return t?t.cursor>=t.input.length?this.alert():(t.cursor++,this.render()):this.alert()}left(){let t=this.focused;return t?t.cursor<=0?this.alert():(t.cursor--,this.render()):this.alert()}space(t,r){return this.dispatch(t,r)}number(t,r){return this.dispatch(t,r)}next(){let t=this.focused;if(!t)return this.alert();let{initial:r,input:s}=t;return r&&r.startsWith(s)&&s!==r?(t.value=t.input=r,t.cursor=t.value.length,this.render()):super.next()}prev(){let t=this.focused;return t?t.cursor===0?super.prev():(t.value=t.input="",t.cursor=0,this.render()):this.alert()}separator(){return""}format(t){return this.state.submitted?"":super.format(t)}pointer(){return""}indicator(t){return t.input?"\u29BF":"\u2299"}async choiceSeparator(t,r){let s=await this.resolve(t.separator,this.state,t,r)||":";return s?" "+this.styles.disabled(s):""}async renderChoice(t,r){await this.onChoice(t,r);let{state:s,styles:a}=this,{cursor:n,initial:c="",name:f,hint:p,input:h=""}=t,{muted:E,submitted:C,primary:S,danger:x}=a,I=p,T=this.index===r,O=t.validate||(()=>!0),U=await this.choiceSeparator(t,r),V=t.message;this.align==="right"&&(V=V.padStart(this.longest+1," ")),this.align==="left"&&(V=V.padEnd(this.longest+1," "));let te=this.values[f]=h||c,ie=h?"success":"dark";await O.call(t,te,this.state)!==!0&&(ie="danger");let ue=a[ie],ae=ue(await this.indicator(t,r))+(t.pad||""),ge=this.indent(t),Ae=()=>[ge,ae,V+U,h,I].filter(Boolean).join(" ");if(s.submitted)return V=dst.unstyle(V),h=C(h),I="",Ae();if(t.format)h=await t.format.call(this,h,t,r);else{let Ce=this.styles.muted;h=mst(this,{input:h,initial:c,pos:n,showCursor:T,color:Ce})}return this.isValue(h)||(h=this.styles.muted(this.symbols.ellipsis)),t.result&&(this.values[f]=await t.result.call(this,te,t,r)),T&&(V=S(V)),t.error?h+=(h?" ":"")+x(t.error.trim()):t.hint&&(h+=(h?" ":"")+E(t.hint.trim())),Ae()}async submit(){return this.value=this.values,super.base.submit.call(this)}};tde.exports=MG});var UG=G((h3t,nde)=>{"use strict";var yst=NT(),Est=()=>{throw new Error("expected prompt to have a custom authenticate method")},rde=(e=Est)=>{class t extends yst{constructor(s){super(s)}async submit(){this.value=await e.call(this,this.values,this.state),super.base.submit.call(this)}static create(s){return rde(s)}}return t};nde.exports=rde()});var ode=G((d3t,sde)=>{"use strict";var Ist=UG();function Cst(e,t){return e.username===this.options.username&&e.password===this.options.password}var ide=(e=Cst)=>{let t=[{name:"username",message:"username"},{name:"password",message:"password",format(s){return this.options.showPassword?s:(this.state.submitted?this.styles.primary:this.styles.muted)(this.symbols.asterisk.repeat(s.length))}}];class r extends Ist.create(e){constructor(a){super({...a,choices:t})}static create(a){return ide(a)}}return r};sde.exports=ide()});var OT=G((g3t,ade)=>{"use strict";var wst=JI(),{isPrimitive:Bst,hasColor:vst}=na(),_G=class extends wst{constructor(t){super(t),this.cursorHide()}async initialize(){let t=await this.resolve(this.initial,this.state);this.input=await this.cast(t),await super.initialize()}dispatch(t){return this.isValue(t)?(this.input=t,this.submit()):this.alert()}format(t){let{styles:r,state:s}=this;return s.submitted?r.success(t):r.primary(t)}cast(t){return this.isTrue(t)}isTrue(t){return/^[ty1]/i.test(t)}isFalse(t){return/^[fn0]/i.test(t)}isValue(t){return Bst(t)&&(this.isTrue(t)||this.isFalse(t))}async hint(){if(this.state.status==="pending"){let t=await this.element("hint");return vst(t)?t:this.styles.muted(t)}}async render(){let{input:t,size:r}=this.state,s=await this.prefix(),a=await this.separator(),n=await this.message(),c=this.styles.muted(this.default),f=[s,n,c,a].filter(Boolean).join(" ");this.state.prompt=f;let p=await this.header(),h=this.value=this.cast(t),E=await this.format(h),C=await this.error()||await this.hint(),S=await this.footer();C&&!f.includes(C)&&(E+=" "+C),f+=" "+E,this.clear(r),this.write([p,f,S].filter(Boolean).join(` `)),this.restore()}set value(t){super.value=t}get value(){return this.cast(super.value)}};ade.exports=_G});var cde=G((m3t,lde)=>{"use strict";var Sst=OT(),HG=class extends Sst{constructor(t){super(t),this.default=this.options.default||(this.initial?"(Y/n)":"(y/N)")}};lde.exports=HG});var fde=G((y3t,ude)=>{"use strict";var Dst=j0(),bst=NT(),KI=bst.prototype,jG=class extends Dst{constructor(t){super({...t,multiple:!0}),this.align=[this.options.align,"left"].find(r=>r!=null),this.emptyError="",this.values={}}dispatch(t,r){let s=this.focused,a=s.parent||{};return!s.editable&&!a.editable&&(t==="a"||t==="i")?super[t]():KI.dispatch.call(this,t,r)}append(t,r){return KI.append.call(this,t,r)}delete(t,r){return KI.delete.call(this,t,r)}space(t){return this.focused.editable?this.append(t):super.space()}number(t){return this.focused.editable?this.append(t):super.number(t)}next(){return this.focused.editable?KI.next.call(this):super.next()}prev(){return this.focused.editable?KI.prev.call(this):super.prev()}async indicator(t,r){let s=t.indicator||"",a=t.editable?s:super.indicator(t,r);return await this.resolve(a,this.state,t,r)||""}indent(t){return t.role==="heading"?"":t.editable?" ":" "}async renderChoice(t,r){return t.indent="",t.editable?KI.renderChoice.call(this,t,r):super.renderChoice(t,r)}error(){return""}footer(){return this.state.error}async validate(){let t=!0;for(let r of this.choices){if(typeof r.validate!="function"||r.role==="heading")continue;let s=r.parent?this.value[r.parent.name]:this.value;if(r.editable?s=r.value===r.name?r.initial||"":r.value:this.isDisabled(r)||(s=r.enabled===!0),t=await r.validate(s,this.state),t!==!0)break}return t!==!0&&(this.state.error=typeof t=="string"?t:"Invalid Input"),t}submit(){if(this.focused.newChoice===!0)return super.submit();if(this.choices.some(t=>t.newChoice))return this.alert();this.value={};for(let t of this.choices){let r=t.parent?this.value[t.parent.name]:this.value;if(t.role==="heading"){this.value[t.name]={};continue}t.editable?r[t.name]=t.value===t.name?t.initial||"":t.value:this.isDisabled(t)||(r[t.name]=t.enabled===!0)}return this.base.submit.call(this)}};ude.exports=jG});var bm=G((E3t,Ade)=>{"use strict";var Pst=JI(),xst=LG(),{isPrimitive:kst}=na(),GG=class extends Pst{constructor(t){super(t),this.initial=kst(this.initial)?String(this.initial):"",this.initial&&this.cursorHide(),this.state.prevCursor=0,this.state.clipboard=[]}async keypress(t,r={}){let s=this.state.prevKeypress;return this.state.prevKeypress=r,this.options.multiline===!0&&r.name==="return"&&(!s||s.name!=="return")?this.append(` `,r):super.keypress(t,r)}moveCursor(t){this.cursor+=t}reset(){return this.input=this.value="",this.cursor=0,this.render()}dispatch(t,r){if(!t||r.ctrl||r.code)return this.alert();this.append(t)}append(t){let{cursor:r,input:s}=this.state;this.input=`${s}`.slice(0,r)+t+`${s}`.slice(r),this.moveCursor(String(t).length),this.render()}insert(t){this.append(t)}delete(){let{cursor:t,input:r}=this.state;if(t<=0)return this.alert();this.input=`${r}`.slice(0,t-1)+`${r}`.slice(t),this.moveCursor(-1),this.render()}deleteForward(){let{cursor:t,input:r}=this.state;if(r[t]===void 0)return this.alert();this.input=`${r}`.slice(0,t)+`${r}`.slice(t+1),this.render()}cutForward(){let t=this.cursor;if(this.input.length<=t)return this.alert();this.state.clipboard.push(this.input.slice(t)),this.input=this.input.slice(0,t),this.render()}cutLeft(){let t=this.cursor;if(t===0)return this.alert();let r=this.input.slice(0,t),s=this.input.slice(t),a=r.split(" ");this.state.clipboard.push(a.pop()),this.input=a.join(" "),this.cursor=this.input.length,this.input+=s,this.render()}paste(){if(!this.state.clipboard.length)return this.alert();this.insert(this.state.clipboard.pop()),this.render()}toggleCursor(){this.state.prevCursor?(this.cursor=this.state.prevCursor,this.state.prevCursor=0):(this.state.prevCursor=this.cursor,this.cursor=0),this.render()}first(){this.cursor=0,this.render()}last(){this.cursor=this.input.length-1,this.render()}next(){let t=this.initial!=null?String(this.initial):"";if(!t||!t.startsWith(this.input))return this.alert();this.input=this.initial,this.cursor=this.initial.length,this.render()}prev(){if(!this.input)return this.alert();this.reset()}backward(){return this.left()}forward(){return this.right()}right(){return this.cursor>=this.input.length?this.alert():(this.moveCursor(1),this.render())}left(){return this.cursor<=0?this.alert():(this.moveCursor(-1),this.render())}isValue(t){return!!t}async format(t=this.value){let r=await this.resolve(this.initial,this.state);return this.state.submitted?this.styles.submitted(t||r):xst(this,{input:t,initial:r,pos:this.cursor})}async render(){let t=this.state.size,r=await this.prefix(),s=await this.separator(),a=await this.message(),n=[r,a,s].filter(Boolean).join(" ");this.state.prompt=n;let c=await this.header(),f=await this.format(),p=await this.error()||await this.hint(),h=await this.footer();p&&!f.includes(p)&&(f+=" "+p),n+=" "+f,this.clear(t),this.write([c,n,h].filter(Boolean).join(` `)),this.restore()}};Ade.exports=GG});var hde=G((I3t,pde)=>{"use strict";var Qst=e=>e.filter((t,r)=>e.lastIndexOf(t)===r),LT=e=>Qst(e).filter(Boolean);pde.exports=(e,t={},r="")=>{let{past:s=[],present:a=""}=t,n,c;switch(e){case"prev":case"undo":return n=s.slice(0,s.length-1),c=s[s.length-1]||"",{past:LT([r,...n]),present:c};case"next":case"redo":return n=s.slice(1),c=s[0]||"",{past:LT([...n,r]),present:c};case"save":return{past:LT([...s,r]),present:""};case"remove":return c=LT(s.filter(f=>f!==r)),a="",c.length&&(a=c.pop()),{past:c,present:a};default:throw new Error(`Invalid action: "${e}"`)}}});var WG=G((C3t,gde)=>{"use strict";var Rst=bm(),dde=hde(),qG=class extends Rst{constructor(t){super(t);let r=this.options.history;if(r&&r.store){let s=r.values||this.initial;this.autosave=!!r.autosave,this.store=r.store,this.data=this.store.get("values")||{past:[],present:s},this.initial=this.data.present||this.data.past[this.data.past.length-1]}}completion(t){return this.store?(this.data=dde(t,this.data,this.input),this.data.present?(this.input=this.data.present,this.cursor=this.input.length,this.render()):this.alert()):this.alert()}altUp(){return this.completion("prev")}altDown(){return this.completion("next")}prev(){return this.save(),super.prev()}save(){this.store&&(this.data=dde("save",this.data,this.input),this.store.set("values",this.data))}submit(){return this.store&&this.autosave===!0&&this.save(),super.submit()}};gde.exports=qG});var yde=G((w3t,mde)=>{"use strict";var Tst=bm(),YG=class extends Tst{format(){return""}};mde.exports=YG});var Ide=G((B3t,Ede)=>{"use strict";var Fst=bm(),VG=class extends Fst{constructor(t={}){super(t),this.sep=this.options.separator||/, */,this.initial=t.initial||""}split(t=this.value){return t?String(t).split(this.sep):[]}format(){let t=this.state.submitted?this.styles.primary:r=>r;return this.list.map(t).join(", ")}async submit(t){let r=this.state.error||await this.validate(this.list,this.state);return r!==!0?(this.state.error=r,super.submit()):(this.value=this.list,super.submit())}get list(){return this.split()}};Ede.exports=VG});var wde=G((v3t,Cde)=>{"use strict";var Nst=j0(),JG=class extends Nst{constructor(t){super({...t,multiple:!0})}};Cde.exports=JG});var zG=G((S3t,Bde)=>{"use strict";var Ost=bm(),KG=class extends Ost{constructor(t={}){super({style:"number",...t}),this.min=this.isValue(t.min)?this.toNumber(t.min):-1/0,this.max=this.isValue(t.max)?this.toNumber(t.max):1/0,this.delay=t.delay!=null?t.delay:1e3,this.float=t.float!==!1,this.round=t.round===!0||t.float===!1,this.major=t.major||10,this.minor=t.minor||1,this.initial=t.initial!=null?t.initial:"",this.input=String(this.initial),this.cursor=this.input.length,this.cursorShow()}append(t){return!/[-+.]/.test(t)||t==="."&&this.input.includes(".")?this.alert("invalid number"):super.append(t)}number(t){return super.append(t)}next(){return this.input&&this.input!==this.initial?this.alert():this.isValue(this.initial)?(this.input=this.initial,this.cursor=String(this.initial).length,this.render()):this.alert()}up(t){let r=t||this.minor,s=this.toNumber(this.input);return s>this.max+r?this.alert():(this.input=`${s+r}`,this.render())}down(t){let r=t||this.minor,s=this.toNumber(this.input);return sthis.isValue(r));return this.value=this.toNumber(t||0),super.submit()}};Bde.exports=KG});var Sde=G((D3t,vde)=>{vde.exports=zG()});var bde=G((b3t,Dde)=>{"use strict";var Lst=bm(),XG=class extends Lst{constructor(t){super(t),this.cursorShow()}format(t=this.input){return this.keypressed?(this.state.submitted?this.styles.primary:this.styles.muted)(this.symbols.asterisk.repeat(t.length)):""}};Dde.exports=XG});var kde=G((P3t,xde)=>{"use strict";var Mst=zu(),Ust=Wv(),Pde=na(),ZG=class extends Ust{constructor(t={}){super(t),this.widths=[].concat(t.messageWidth||50),this.align=[].concat(t.align||"left"),this.linebreak=t.linebreak||!1,this.edgeLength=t.edgeLength||3,this.newline=t.newline||` `;let r=t.startNumber||1;typeof this.scale=="number"&&(this.scaleKey=!1,this.scale=Array(this.scale).fill(0).map((s,a)=>({name:a+r})))}async reset(){return this.tableized=!1,await super.reset(),this.render()}tableize(){if(this.tableized===!0)return;this.tableized=!0;let t=0;for(let r of this.choices){t=Math.max(t,r.message.length),r.scaleIndex=r.initial||2,r.scale=[];for(let s=0;s=this.scale.length-1?this.alert():(t.scaleIndex++,this.render())}left(){let t=this.focused;return t.scaleIndex<=0?this.alert():(t.scaleIndex--,this.render())}indent(){return""}format(){return this.state.submitted?this.choices.map(r=>this.styles.info(r.index)).join(", "):""}pointer(){return""}renderScaleKey(){return this.scaleKey===!1||this.state.submitted?"":["",...this.scale.map(s=>` ${s.name} - ${s.message}`)].map(s=>this.styles.muted(s)).join(` `)}renderScaleHeading(t){let r=this.scale.map(p=>p.name);typeof this.options.renderScaleHeading=="function"&&(r=this.options.renderScaleHeading.call(this,t));let s=this.scaleLength-r.join("").length,a=Math.round(s/(r.length-1)),c=r.map(p=>this.styles.strong(p)).join(" ".repeat(a)),f=" ".repeat(this.widths[0]);return this.margin[3]+f+this.margin[1]+c}scaleIndicator(t,r,s){if(typeof this.options.scaleIndicator=="function")return this.options.scaleIndicator.call(this,t,r,s);let a=t.scaleIndex===r.index;return r.disabled?this.styles.hint(this.symbols.radio.disabled):a?this.styles.success(this.symbols.radio.on):this.symbols.radio.off}renderScale(t,r){let s=t.scale.map(n=>this.scaleIndicator(t,n,r)),a=this.term==="Hyper"?"":" ";return s.join(a+this.symbols.line.repeat(this.edgeLength))}async renderChoice(t,r){await this.onChoice(t,r);let s=this.index===r,a=await this.pointer(t,r),n=await t.hint;n&&!Pde.hasColor(n)&&(n=this.styles.muted(n));let c=I=>this.margin[3]+I.replace(/\s+$/,"").padEnd(this.widths[0]," "),f=this.newline,p=this.indent(t),h=await this.resolve(t.message,this.state,t,r),E=await this.renderScale(t,r),C=this.margin[1]+this.margin[3];this.scaleLength=Mst.unstyle(E).length,this.widths[0]=Math.min(this.widths[0],this.width-this.scaleLength-C.length);let x=Pde.wordWrap(h,{width:this.widths[0],newline:f}).split(` `).map(I=>c(I)+this.margin[1]);return s&&(E=this.styles.info(E),x=x.map(I=>this.styles.info(I))),x[0]+=E,this.linebreak&&x.push(""),[p+a,x.join(` `)].filter(Boolean)}async renderChoices(){if(this.state.submitted)return"";this.tableize();let t=this.visible.map(async(a,n)=>await this.renderChoice(a,n)),r=await Promise.all(t),s=await this.renderScaleHeading();return this.margin[0]+[s,...r.map(a=>a.join(" "))].join(` `)}async render(){let{submitted:t,size:r}=this.state,s=await this.prefix(),a=await this.separator(),n=await this.message(),c="";this.options.promptLine!==!1&&(c=[s,n,a,""].join(" "),this.state.prompt=c);let f=await this.header(),p=await this.format(),h=await this.renderScaleKey(),E=await this.error()||await this.hint(),C=await this.renderChoices(),S=await this.footer(),x=this.emptyError;p&&(c+=p),E&&!c.includes(E)&&(c+=" "+E),t&&!p&&!C.trim()&&this.multiple&&x!=null&&(c+=this.styles.danger(x)),this.clear(r),this.write([f,c,h,C,S].filter(Boolean).join(` `)),this.state.submitted||this.write(this.margin[2]),this.restore()}submit(){this.value={};for(let t of this.choices)this.value[t.name]=t.scaleIndex;return this.base.submit.call(this)}};xde.exports=ZG});var Tde=G((x3t,Rde)=>{"use strict";var Qde=zu(),_st=(e="")=>typeof e=="string"?e.replace(/^['"]|['"]$/g,""):"",e5=class{constructor(t){this.name=t.key,this.field=t.field||{},this.value=_st(t.initial||this.field.initial||""),this.message=t.message||this.name,this.cursor=0,this.input="",this.lines=[]}},Hst=async(e={},t={},r=s=>s)=>{let s=new Set,a=e.fields||[],n=e.template,c=[],f=[],p=[],h=1;typeof n=="function"&&(n=await n());let E=-1,C=()=>n[++E],S=()=>n[E+1],x=I=>{I.line=h,c.push(I)};for(x({type:"bos",value:""});Eie.name===U.key);U.field=a.find(ie=>ie.name===U.key),te||(te=new e5(U),f.push(te)),te.lines.push(U.line-1);continue}let T=c[c.length-1];T.type==="text"&&T.line===h?T.value+=I:x({type:"text",value:I})}return x({type:"eos",value:""}),{input:n,tabstops:c,unique:s,keys:p,items:f}};Rde.exports=async e=>{let t=e.options,r=new Set(t.required===!0?[]:t.required||[]),s={...t.values,...t.initial},{tabstops:a,items:n,keys:c}=await Hst(t,s),f=$G("result",e,t),p=$G("format",e,t),h=$G("validate",e,t,!0),E=e.isValue.bind(e);return async(C={},S=!1)=>{let x=0;C.required=r,C.items=n,C.keys=c,C.output="";let I=async(V,te,ie,ue)=>{let ae=await h(V,te,ie,ue);return ae===!1?"Invalid field "+ie.name:ae};for(let V of a){let te=V.value,ie=V.key;if(V.type!=="template"){te&&(C.output+=te);continue}if(V.type==="template"){let ue=n.find(Ee=>Ee.name===ie);t.required===!0&&C.required.add(ue.name);let ae=[ue.input,C.values[ue.value],ue.value,te].find(E),Ae=(ue.field||{}).message||V.inner;if(S){let Ee=await I(C.values[ie],C,ue,x);if(Ee&&typeof Ee=="string"||Ee===!1){C.invalid.set(ie,Ee);continue}C.invalid.delete(ie);let d=await f(C.values[ie],C,ue,x);C.output+=Qde.unstyle(d);continue}ue.placeholder=!1;let Ce=te;te=await p(te,C,ue,x),ae!==te?(C.values[ie]=ae,te=e.styles.typing(ae),C.missing.delete(Ae)):(C.values[ie]=void 0,ae=`<${Ae}>`,te=e.styles.primary(ae),ue.placeholder=!0,C.required.has(ie)&&C.missing.add(Ae)),C.missing.has(Ae)&&C.validating&&(te=e.styles.warning(ae)),C.invalid.has(ie)&&C.validating&&(te=e.styles.danger(ae)),x===C.index&&(Ce!==te?te=e.styles.underline(te):te=e.styles.heading(Qde.unstyle(te))),x++}te&&(C.output+=te)}let T=C.output.split(` `).map(V=>" "+V),O=n.length,U=0;for(let V of n)C.invalid.has(V.name)&&V.lines.forEach(te=>{T[te][0]===" "&&(T[te]=C.styles.danger(C.symbols.bullet)+T[te].slice(1))}),e.isValue(C.values[V.name])&&U++;return C.completed=(U/O*100).toFixed(0),C.output=T.join(` `),C.output}};function $G(e,t,r,s){return(a,n,c,f)=>typeof c.field[e]=="function"?c.field[e].call(t,a,n,c,f):[s,a].find(p=>t.isValue(p))}});var Nde=G((k3t,Fde)=>{"use strict";var jst=zu(),Gst=Tde(),qst=JI(),t5=class extends qst{constructor(t){super(t),this.cursorHide(),this.reset(!0)}async initialize(){this.interpolate=await Gst(this),await super.initialize()}async reset(t){this.state.keys=[],this.state.invalid=new Map,this.state.missing=new Set,this.state.completed=0,this.state.values={},t!==!0&&(await this.initialize(),await this.render())}moveCursor(t){let r=this.getItem();this.cursor+=t,r.cursor+=t}dispatch(t,r){if(!r.code&&!r.ctrl&&t!=null&&this.getItem()){this.append(t,r);return}this.alert()}append(t,r){let s=this.getItem(),a=s.input.slice(0,this.cursor),n=s.input.slice(this.cursor);this.input=s.input=`${a}${t}${n}`,this.moveCursor(1),this.render()}delete(){let t=this.getItem();if(this.cursor<=0||!t.input)return this.alert();let r=t.input.slice(this.cursor),s=t.input.slice(0,this.cursor-1);this.input=t.input=`${s}${r}`,this.moveCursor(-1),this.render()}increment(t){return t>=this.state.keys.length-1?0:t+1}decrement(t){return t<=0?this.state.keys.length-1:t-1}first(){this.state.index=0,this.render()}last(){this.state.index=this.state.keys.length-1,this.render()}right(){if(this.cursor>=this.input.length)return this.alert();this.moveCursor(1),this.render()}left(){if(this.cursor<=0)return this.alert();this.moveCursor(-1),this.render()}prev(){this.state.index=this.decrement(this.state.index),this.getItem(),this.render()}next(){this.state.index=this.increment(this.state.index),this.getItem(),this.render()}up(){this.prev()}down(){this.next()}format(t){let r=this.state.completed<100?this.styles.warning:this.styles.success;return this.state.submitted===!0&&this.state.completed!==100&&(r=this.styles.danger),r(`${this.state.completed}% completed`)}async render(){let{index:t,keys:r=[],submitted:s,size:a}=this.state,n=[this.options.newline,` `].find(V=>V!=null),c=await this.prefix(),f=await this.separator(),p=await this.message(),h=[c,p,f].filter(Boolean).join(" ");this.state.prompt=h;let E=await this.header(),C=await this.error()||"",S=await this.hint()||"",x=s?"":await this.interpolate(this.state),I=this.state.key=r[t]||"",T=await this.format(I),O=await this.footer();T&&(h+=" "+T),S&&!T&&this.state.completed===0&&(h+=" "+S),this.clear(a);let U=[E,h,x,O,C.trim()];this.write(U.filter(Boolean).join(n)),this.restore()}getItem(t){let{items:r,keys:s,index:a}=this.state,n=r.find(c=>c.name===s[a]);return n&&n.input!=null&&(this.input=n.input,this.cursor=n.cursor),n}async submit(){typeof this.interpolate!="function"&&await this.initialize(),await this.interpolate(this.state,!0);let{invalid:t,missing:r,output:s,values:a}=this.state;if(t.size){let f="";for(let[p,h]of t)f+=`Invalid ${p}: ${h} `;return this.state.error=f,super.submit()}if(r.size)return this.state.error="Required: "+[...r.keys()].join(", "),super.submit();let c=jst.unstyle(s).split(` `).map(f=>f.slice(1)).join(` `);return this.value={values:a,result:c},super.submit()}};Fde.exports=t5});var Lde=G((Q3t,Ode)=>{"use strict";var Wst="(Use + to sort)",Yst=j0(),r5=class extends Yst{constructor(t){super({...t,reorder:!1,sort:!0,multiple:!0}),this.state.hint=[this.options.hint,Wst].find(this.isValue.bind(this))}indicator(){return""}async renderChoice(t,r){let s=await super.renderChoice(t,r),a=this.symbols.identicalTo+" ",n=this.index===r&&this.sorting?this.styles.muted(a):" ";return this.options.drag===!1&&(n=""),this.options.numbered===!0?n+`${r+1} - `+s:n+s}get selected(){return this.choices}submit(){return this.value=this.choices.map(t=>t.value),super.submit()}};Ode.exports=r5});var Ude=G((R3t,Mde)=>{"use strict";var Vst=Wv(),n5=class extends Vst{constructor(t={}){if(super(t),this.emptyError=t.emptyError||"No items were selected",this.term=process.env.TERM_PROGRAM,!this.options.header){let r=["","4 - Strongly Agree","3 - Agree","2 - Neutral","1 - Disagree","0 - Strongly Disagree",""];r=r.map(s=>this.styles.muted(s)),this.state.header=r.join(` `)}}async toChoices(...t){if(this.createdScales)return!1;this.createdScales=!0;let r=await super.toChoices(...t);for(let s of r)s.scale=Jst(5,this.options),s.scaleIdx=2;return r}dispatch(){this.alert()}space(){let t=this.focused,r=t.scale[t.scaleIdx],s=r.selected;return t.scale.forEach(a=>a.selected=!1),r.selected=!s,this.render()}indicator(){return""}pointer(){return""}separator(){return this.styles.muted(this.symbols.ellipsis)}right(){let t=this.focused;return t.scaleIdx>=t.scale.length-1?this.alert():(t.scaleIdx++,this.render())}left(){let t=this.focused;return t.scaleIdx<=0?this.alert():(t.scaleIdx--,this.render())}indent(){return" "}async renderChoice(t,r){await this.onChoice(t,r);let s=this.index===r,a=this.term==="Hyper",n=a?9:8,c=a?"":" ",f=this.symbols.line.repeat(n),p=" ".repeat(n+(a?0:1)),h=te=>(te?this.styles.success("\u25C9"):"\u25EF")+c,E=r+1+".",C=s?this.styles.heading:this.styles.noop,S=await this.resolve(t.message,this.state,t,r),x=this.indent(t),I=x+t.scale.map((te,ie)=>h(ie===t.scaleIdx)).join(f),T=te=>te===t.scaleIdx?C(te):te,O=x+t.scale.map((te,ie)=>T(ie)).join(p),U=()=>[E,S].filter(Boolean).join(" "),V=()=>[U(),I,O," "].filter(Boolean).join(` `);return s&&(I=this.styles.cyan(I),O=this.styles.cyan(O)),V()}async renderChoices(){if(this.state.submitted)return"";let t=this.visible.map(async(s,a)=>await this.renderChoice(s,a)),r=await Promise.all(t);return r.length||r.push(this.styles.danger("No matching choices")),r.join(` `)}format(){return this.state.submitted?this.choices.map(r=>this.styles.info(r.scaleIdx)).join(", "):""}async render(){let{submitted:t,size:r}=this.state,s=await this.prefix(),a=await this.separator(),n=await this.message(),c=[s,n,a].filter(Boolean).join(" ");this.state.prompt=c;let f=await this.header(),p=await this.format(),h=await this.error()||await this.hint(),E=await this.renderChoices(),C=await this.footer();(p||!h)&&(c+=" "+p),h&&!c.includes(h)&&(c+=" "+h),t&&!p&&!E&&this.multiple&&this.type!=="form"&&(c+=this.styles.danger(this.emptyError)),this.clear(r),this.write([c,f,E,C].filter(Boolean).join(` `)),this.restore()}submit(){this.value={};for(let t of this.choices)this.value[t.name]=t.scaleIdx;return this.base.submit.call(this)}};function Jst(e,t={}){if(Array.isArray(t.scale))return t.scale.map(s=>({...s}));let r=[];for(let s=1;s{_de.exports=WG()});var Gde=G((F3t,jde)=>{"use strict";var Kst=OT(),i5=class extends Kst{async initialize(){await super.initialize(),this.value=this.initial=!!this.options.initial,this.disabled=this.options.disabled||"no",this.enabled=this.options.enabled||"yes",await this.render()}reset(){this.value=this.initial,this.render()}delete(){this.alert()}toggle(){this.value=!this.value,this.render()}enable(){if(this.value===!0)return this.alert();this.value=!0,this.render()}disable(){if(this.value===!1)return this.alert();this.value=!1,this.render()}up(){this.toggle()}down(){this.toggle()}right(){this.toggle()}left(){this.toggle()}next(){this.toggle()}prev(){this.toggle()}dispatch(t="",r){switch(t.toLowerCase()){case" ":return this.toggle();case"1":case"y":case"t":return this.enable();case"0":case"n":case"f":return this.disable();default:return this.alert()}}format(){let t=s=>this.styles.primary.underline(s);return[this.value?this.disabled:t(this.disabled),this.value?t(this.enabled):this.enabled].join(this.styles.muted(" / "))}async render(){let{size:t}=this.state,r=await this.header(),s=await this.prefix(),a=await this.separator(),n=await this.message(),c=await this.format(),f=await this.error()||await this.hint(),p=await this.footer(),h=[s,n,a,c].join(" ");this.state.prompt=h,f&&!h.includes(f)&&(h+=" "+f),this.clear(t),this.write([r,h,p].filter(Boolean).join(` `)),this.write(this.margin[2]),this.restore()}};jde.exports=i5});var Wde=G((N3t,qde)=>{"use strict";var zst=j0(),s5=class extends zst{constructor(t){if(super(t),typeof this.options.correctChoice!="number"||this.options.correctChoice<0)throw new Error("Please specify the index of the correct answer from the list of choices")}async toChoices(t,r){let s=await super.toChoices(t,r);if(s.length<2)throw new Error("Please give at least two choices to the user");if(this.options.correctChoice>s.length)throw new Error("Please specify the index of the correct answer from the list of choices");return s}check(t){return t.index===this.options.correctChoice}async result(t){return{selectedAnswer:t,correctAnswer:this.options.choices[this.options.correctChoice].value,correct:await this.check(this.state)}}};qde.exports=s5});var Vde=G(o5=>{"use strict";var Yde=na(),Ts=(e,t)=>{Yde.defineExport(o5,e,t),Yde.defineExport(o5,e.toLowerCase(),t)};Ts("AutoComplete",()=>$0e());Ts("BasicAuth",()=>ode());Ts("Confirm",()=>cde());Ts("Editable",()=>fde());Ts("Form",()=>NT());Ts("Input",()=>WG());Ts("Invisible",()=>yde());Ts("List",()=>Ide());Ts("MultiSelect",()=>wde());Ts("Numeral",()=>Sde());Ts("Password",()=>bde());Ts("Scale",()=>kde());Ts("Select",()=>j0());Ts("Snippet",()=>Nde());Ts("Sort",()=>Lde());Ts("Survey",()=>Ude());Ts("Text",()=>Hde());Ts("Toggle",()=>Gde());Ts("Quiz",()=>Wde())});var Kde=G((L3t,Jde)=>{Jde.exports={ArrayPrompt:Wv(),AuthPrompt:UG(),BooleanPrompt:OT(),NumberPrompt:zG(),StringPrompt:bm()}});var Vv=G((M3t,Xde)=>{"use strict";var zde=Ie("assert"),l5=Ie("events"),G0=na(),Zu=class extends l5{constructor(t,r){super(),this.options=G0.merge({},t),this.answers={...r}}register(t,r){if(G0.isObject(t)){for(let a of Object.keys(t))this.register(a,t[a]);return this}zde.equal(typeof r,"function","expected a function");let s=t.toLowerCase();return r.prototype instanceof this.Prompt?this.prompts[s]=r:this.prompts[s]=r(this.Prompt,this),this}async prompt(t=[]){for(let r of[].concat(t))try{typeof r=="function"&&(r=await r.call(this)),await this.ask(G0.merge({},this.options,r))}catch(s){return Promise.reject(s)}return this.answers}async ask(t){typeof t=="function"&&(t=await t.call(this));let r=G0.merge({},this.options,t),{type:s,name:a}=t,{set:n,get:c}=G0;if(typeof s=="function"&&(s=await s.call(this,t,this.answers)),!s)return this.answers[a];zde(this.prompts[s],`Prompt "${s}" is not registered`);let f=new this.prompts[s](r),p=c(this.answers,a);f.state.answers=this.answers,f.enquirer=this,a&&f.on("submit",E=>{this.emit("answer",a,E,f),n(this.answers,a,E)});let h=f.emit.bind(f);return f.emit=(...E)=>(this.emit.call(this,...E),h(...E)),this.emit("prompt",f,this),r.autofill&&p!=null?(f.value=f.input=p,r.autofill==="show"&&await f.submit()):p=f.value=await f.run(),p}use(t){return t.call(this,this),this}set Prompt(t){this._Prompt=t}get Prompt(){return this._Prompt||this.constructor.Prompt}get prompts(){return this.constructor.prompts}static set Prompt(t){this._Prompt=t}static get Prompt(){return this._Prompt||JI()}static get prompts(){return Vde()}static get types(){return Kde()}static get prompt(){let t=(r,...s)=>{let a=new this(...s),n=a.emit.bind(a);return a.emit=(...c)=>(t.emit(...c),n(...c)),a.prompt(r)};return G0.mixinEmitter(t,new l5),t}};G0.mixinEmitter(Zu,new l5);var a5=Zu.prompts;for(let e of Object.keys(a5)){let t=e.toLowerCase(),r=s=>new a5[e](s).run();Zu.prompt[t]=r,Zu[t]=r,Zu[e]||Reflect.defineProperty(Zu,e,{get:()=>a5[e]})}var Yv=e=>{G0.defineExport(Zu,e,()=>Zu.types[e])};Yv("ArrayPrompt");Yv("AuthPrompt");Yv("BooleanPrompt");Yv("NumberPrompt");Yv("StringPrompt");Xde.exports=Zu});var sge=G((aHt,sot)=>{sot.exports={name:"@yarnpkg/cli",version:"4.14.1",license:"BSD-2-Clause",main:"./sources/index.ts",exports:{".":"./sources/index.ts","./polyfills":"./sources/polyfills.ts","./package.json":"./package.json"},dependencies:{"@yarnpkg/core":"workspace:^","@yarnpkg/fslib":"workspace:^","@yarnpkg/libzip":"workspace:^","@yarnpkg/parsers":"workspace:^","@yarnpkg/plugin-catalog":"workspace:^","@yarnpkg/plugin-compat":"workspace:^","@yarnpkg/plugin-constraints":"workspace:^","@yarnpkg/plugin-dlx":"workspace:^","@yarnpkg/plugin-essentials":"workspace:^","@yarnpkg/plugin-exec":"workspace:^","@yarnpkg/plugin-file":"workspace:^","@yarnpkg/plugin-git":"workspace:^","@yarnpkg/plugin-github":"workspace:^","@yarnpkg/plugin-http":"workspace:^","@yarnpkg/plugin-init":"workspace:^","@yarnpkg/plugin-interactive-tools":"workspace:^","@yarnpkg/plugin-jsr":"workspace:^","@yarnpkg/plugin-link":"workspace:^","@yarnpkg/plugin-nm":"workspace:^","@yarnpkg/plugin-npm":"workspace:^","@yarnpkg/plugin-npm-cli":"workspace:^","@yarnpkg/plugin-pack":"workspace:^","@yarnpkg/plugin-patch":"workspace:^","@yarnpkg/plugin-pnp":"workspace:^","@yarnpkg/plugin-pnpm":"workspace:^","@yarnpkg/plugin-stage":"workspace:^","@yarnpkg/plugin-typescript":"workspace:^","@yarnpkg/plugin-version":"workspace:^","@yarnpkg/plugin-workspace-tools":"workspace:^","@yarnpkg/shell":"workspace:^","ci-info":"^4.0.0",clipanion:"^4.0.0-rc.2",semver:"^7.1.2",tslib:"^2.4.0",typanion:"^3.14.0"},devDependencies:{"@types/semver":"^7.1.0","@yarnpkg/builder":"workspace:^","@yarnpkg/monorepo":"workspace:^","@yarnpkg/pnpify":"workspace:^"},peerDependencies:{"@yarnpkg/core":"workspace:^"},scripts:{postpack:"rm -rf lib",prepack:'run build:compile "$(pwd)"',"build:cli+hook":"run build:pnp:hook && builder build bundle","build:cli":"builder build bundle","run:cli":"builder run","update-local":"run build:cli --no-git-hash && rsync -a --delete bundles/ bin/"},publishConfig:{main:"./lib/index.js",bin:null,exports:{".":"./lib/index.js","./package.json":"./package.json"}},files:["/lib/**/*","!/lib/pluginConfiguration.*","!/lib/cli.*"],"@yarnpkg/builder":{bundles:{standard:["@yarnpkg/plugin-essentials","@yarnpkg/plugin-catalog","@yarnpkg/plugin-compat","@yarnpkg/plugin-constraints","@yarnpkg/plugin-dlx","@yarnpkg/plugin-exec","@yarnpkg/plugin-file","@yarnpkg/plugin-git","@yarnpkg/plugin-github","@yarnpkg/plugin-http","@yarnpkg/plugin-init","@yarnpkg/plugin-interactive-tools","@yarnpkg/plugin-jsr","@yarnpkg/plugin-link","@yarnpkg/plugin-nm","@yarnpkg/plugin-npm","@yarnpkg/plugin-npm-cli","@yarnpkg/plugin-pack","@yarnpkg/plugin-patch","@yarnpkg/plugin-pnp","@yarnpkg/plugin-pnpm","@yarnpkg/plugin-stage","@yarnpkg/plugin-typescript","@yarnpkg/plugin-version","@yarnpkg/plugin-workspace-tools"]}},repository:{type:"git",url:"git+https://github.com/yarnpkg/berry.git",directory:"packages/yarnpkg-cli"},engines:{node:">=18.12.0"}}});var B5=G((LGt,mge)=>{"use strict";mge.exports=function(t,r){r===!0&&(r=0);var s="";if(typeof t=="string")try{s=new URL(t).protocol}catch{}else t&&t.constructor===URL&&(s=t.protocol);var a=s.split(/\:|\+/).filter(Boolean);return typeof r=="number"?a[r]:a}});var Ege=G((MGt,yge)=>{"use strict";var Sot=B5();function Dot(e){var t={protocols:[],protocol:null,port:null,resource:"",host:"",user:"",password:"",pathname:"",hash:"",search:"",href:e,query:{},parse_failed:!1};try{var r=new URL(e);t.protocols=Sot(r),t.protocol=t.protocols[0],t.port=r.port,t.resource=r.hostname,t.host=r.host,t.user=r.username||"",t.password=r.password||"",t.pathname=r.pathname,t.hash=r.hash.slice(1),t.search=r.search.slice(1),t.href=r.href,t.query=Object.fromEntries(r.searchParams)}catch{t.protocols=["file"],t.protocol=t.protocols[0],t.port="",t.resource="",t.user="",t.pathname="",t.hash="",t.search="",t.href=e,t.query={},t.parse_failed=!0}return t}yge.exports=Dot});var wge=G((UGt,Cge)=>{"use strict";var bot=Ege();function Pot(e){return e&&typeof e=="object"&&"default"in e?e:{default:e}}var xot=Pot(bot),kot="text/plain",Qot="us-ascii",Ige=(e,t)=>t.some(r=>r instanceof RegExp?r.test(e):r===e),Rot=(e,{stripHash:t})=>{let r=/^data:(?[^,]*?),(?[^#]*?)(?:#(?.*))?$/.exec(e);if(!r)throw new Error(`Invalid URL: ${e}`);let{type:s,data:a,hash:n}=r.groups,c=s.split(";");n=t?"":n;let f=!1;c[c.length-1]==="base64"&&(c.pop(),f=!0);let p=(c.shift()||"").toLowerCase(),E=[...c.map(C=>{let[S,x=""]=C.split("=").map(I=>I.trim());return S==="charset"&&(x=x.toLowerCase(),x===Qot)?"":`${S}${x?`=${x}`:""}`}).filter(Boolean)];return f&&E.push("base64"),(E.length>0||p&&p!==kot)&&E.unshift(p),`data:${E.join(";")},${f?a.trim():a}${n?`#${n}`:""}`};function Tot(e,t){if(t={defaultProtocol:"http:",normalizeProtocol:!0,forceHttp:!1,forceHttps:!1,stripAuthentication:!0,stripHash:!1,stripTextFragment:!0,stripWWW:!0,removeQueryParameters:[/^utm_\w+/i],removeTrailingSlash:!0,removeSingleSlash:!0,removeDirectoryIndex:!1,sortQueryParameters:!0,...t},e=e.trim(),/^data:/i.test(e))return Rot(e,t);if(/^view-source:/i.test(e))throw new Error("`view-source:` is not supported as it is a non-standard protocol");let r=e.startsWith("//");!r&&/^\.*\//.test(e)||(e=e.replace(/^(?!(?:\w+:)?\/\/)|^\/\//,t.defaultProtocol));let a=new URL(e);if(t.forceHttp&&t.forceHttps)throw new Error("The `forceHttp` and `forceHttps` options cannot be used together");if(t.forceHttp&&a.protocol==="https:"&&(a.protocol="http:"),t.forceHttps&&a.protocol==="http:"&&(a.protocol="https:"),t.stripAuthentication&&(a.username="",a.password=""),t.stripHash?a.hash="":t.stripTextFragment&&(a.hash=a.hash.replace(/#?:~:text.*?$/i,"")),a.pathname){let c=/\b[a-z][a-z\d+\-.]{1,50}:\/\//g,f=0,p="";for(;;){let E=c.exec(a.pathname);if(!E)break;let C=E[0],S=E.index,x=a.pathname.slice(f,S);p+=x.replace(/\/{2,}/g,"/"),p+=C,f=S+C.length}let h=a.pathname.slice(f,a.pathname.length);p+=h.replace(/\/{2,}/g,"/"),a.pathname=p}if(a.pathname)try{a.pathname=decodeURI(a.pathname)}catch{}if(t.removeDirectoryIndex===!0&&(t.removeDirectoryIndex=[/^index\.[a-z]+$/]),Array.isArray(t.removeDirectoryIndex)&&t.removeDirectoryIndex.length>0){let c=a.pathname.split("/"),f=c[c.length-1];Ige(f,t.removeDirectoryIndex)&&(c=c.slice(0,-1),a.pathname=c.slice(1).join("/")+"/")}if(a.hostname&&(a.hostname=a.hostname.replace(/\.$/,""),t.stripWWW&&/^www\.(?!www\.)[a-z\-\d]{1,63}\.[a-z.\-\d]{2,63}$/.test(a.hostname)&&(a.hostname=a.hostname.replace(/^www\./,""))),Array.isArray(t.removeQueryParameters))for(let c of[...a.searchParams.keys()])Ige(c,t.removeQueryParameters)&&a.searchParams.delete(c);if(t.removeQueryParameters===!0&&(a.search=""),t.sortQueryParameters){a.searchParams.sort();try{a.search=decodeURIComponent(a.search)}catch{}}t.removeTrailingSlash&&(a.pathname=a.pathname.replace(/\/$/,""));let n=e;return e=a.toString(),!t.removeSingleSlash&&a.pathname==="/"&&!n.endsWith("/")&&a.hash===""&&(e=e.replace(/\/$/,"")),(t.removeTrailingSlash||a.pathname==="/")&&a.hash===""&&t.removeSingleSlash&&(e=e.replace(/\/$/,"")),r&&!t.normalizeProtocol&&(e=e.replace(/^http:\/\//,"//")),t.stripProtocol&&(e=e.replace(/^(?:https?:)?\/\//,"")),e}var v5=(e,t=!1)=>{let r=/^(?:([a-z_][a-z0-9_-]{0,31})@|https?:\/\/)([\w\.\-@]+)[\/:]([\~,\.\w,\-,\_,\/]+?(?:\.git|\/)?)$/,s=n=>{let c=new Error(n);throw c.subject_url=e,c};(typeof e!="string"||!e.trim())&&s("Invalid url."),e.length>v5.MAX_INPUT_LENGTH&&s("Input exceeds maximum length. If needed, change the value of parseUrl.MAX_INPUT_LENGTH."),t&&(typeof t!="object"&&(t={stripHash:!1}),e=Tot(e,t));let a=xot.default(e);if(a.parse_failed){let n=a.href.match(r);n?(a.protocols=["ssh"],a.protocol="ssh",a.resource=n[2],a.host=n[2],a.user=n[1],a.pathname=`/${n[3]}`,a.parse_failed=!1):s("URL parsing failed.")}return a};v5.MAX_INPUT_LENGTH=2048;Cge.exports=v5});var Sge=G((_Gt,vge)=>{"use strict";var Fot=B5();function Bge(e){if(Array.isArray(e))return e.indexOf("ssh")!==-1||e.indexOf("rsync")!==-1;if(typeof e!="string")return!1;var t=Fot(e);if(e=e.substring(e.indexOf("://")+3),Bge(t))return!0;var r=new RegExp(".([a-zA-Z\\d]+):(\\d+)/");return!e.match(r)&&e.indexOf("@"){"use strict";var Not=wge(),Dge=Sge();function Oot(e){var t=Not(e);return t.token="",t.password==="x-oauth-basic"?t.token=t.user:t.user==="x-token-auth"&&(t.token=t.password),Dge(t.protocols)||t.protocols.length===0&&Dge(e)?t.protocol="ssh":t.protocols.length?t.protocol=t.protocols[0]:(t.protocol="file",t.protocols=["file"]),t.href=t.href.replace(/\/$/,""),t}bge.exports=Oot});var kge=G((jGt,xge)=>{"use strict";var Lot=Pge();function S5(e){if(typeof e!="string")throw new Error("The url must be a string.");var t=/^([a-z\d-]{1,39})\/([-\.\w]{1,100})$/i;t.test(e)&&(e="https://github.com/"+e);var r=Lot(e),s=r.resource.split("."),a=null;switch(r.toString=function(O){return S5.stringify(this,O)},r.source=s.length>2?s.slice(1-s.length).join("."):r.source=r.resource,r.git_suffix=/\.git$/.test(r.pathname),r.name=decodeURIComponent((r.pathname||r.href).replace(/(^\/)|(\/$)/g,"").replace(/\.git$/,"")),r.owner=decodeURIComponent(r.user),r.source){case"git.cloudforge.com":r.owner=r.user,r.organization=s[0],r.source="cloudforge.com";break;case"visualstudio.com":if(r.resource==="vs-ssh.visualstudio.com"){a=r.name.split("/"),a.length===4&&(r.organization=a[1],r.owner=a[2],r.name=a[3],r.full_name=a[2]+"/"+a[3]);break}else{a=r.name.split("/"),a.length===2?(r.owner=a[1],r.name=a[1],r.full_name="_git/"+r.name):a.length===3?(r.name=a[2],a[0]==="DefaultCollection"?(r.owner=a[2],r.organization=a[0],r.full_name=r.organization+"/_git/"+r.name):(r.owner=a[0],r.full_name=r.owner+"/_git/"+r.name)):a.length===4&&(r.organization=a[0],r.owner=a[1],r.name=a[3],r.full_name=r.organization+"/"+r.owner+"/_git/"+r.name);break}case"dev.azure.com":case"azure.com":if(r.resource==="ssh.dev.azure.com"){a=r.name.split("/"),a.length===4&&(r.organization=a[1],r.owner=a[2],r.name=a[3]);break}else{a=r.name.split("/"),a.length===5?(r.organization=a[0],r.owner=a[1],r.name=a[4],r.full_name="_git/"+r.name):a.length===3?(r.name=a[2],a[0]==="DefaultCollection"?(r.owner=a[2],r.organization=a[0],r.full_name=r.organization+"/_git/"+r.name):(r.owner=a[0],r.full_name=r.owner+"/_git/"+r.name)):a.length===4&&(r.organization=a[0],r.owner=a[1],r.name=a[3],r.full_name=r.organization+"/"+r.owner+"/_git/"+r.name),r.query&&r.query.path&&(r.filepath=r.query.path.replace(/^\/+/g,"")),r.query&&r.query.version&&(r.ref=r.query.version.replace(/^GB/,""));break}default:a=r.name.split("/");var n=a.length-1;if(a.length>=2){var c=a.indexOf("-",2),f=a.indexOf("blob",2),p=a.indexOf("tree",2),h=a.indexOf("commit",2),E=a.indexOf("src",2),C=a.indexOf("raw",2),S=a.indexOf("edit",2);n=c>0?c-1:f>0?f-1:p>0?p-1:h>0?h-1:E>0?E-1:C>0?C-1:S>0?S-1:n,r.owner=a.slice(0,n).join("/"),r.name=a[n],h&&(r.commit=a[n+2])}r.ref="",r.filepathtype="",r.filepath="";var x=a.length>n&&a[n+1]==="-"?n+1:n;a.length>x+2&&["raw","src","blob","tree","edit"].indexOf(a[x+1])>=0&&(r.filepathtype=a[x+1],r.ref=a[x+2],a.length>x+3&&(r.filepath=a.slice(x+3).join("/"))),r.organization=r.owner;break}r.full_name||(r.full_name=r.owner,r.name&&(r.full_name&&(r.full_name+="/"),r.full_name+=r.name)),r.owner.startsWith("scm/")&&(r.source="bitbucket-server",r.owner=r.owner.replace("scm/",""),r.organization=r.owner,r.full_name=r.owner+"/"+r.name);var I=/(projects|users)\/(.*?)\/repos\/(.*?)((\/.*$)|$)/,T=I.exec(r.pathname);return T!=null&&(r.source="bitbucket-server",T[1]==="users"?r.owner="~"+T[2]:r.owner=T[2],r.organization=r.owner,r.name=T[3],a=T[4].split("/"),a.length>1&&(["raw","browse"].indexOf(a[1])>=0?(r.filepathtype=a[1],a.length>2&&(r.filepath=a.slice(2).join("/"))):a[1]==="commits"&&a.length>2&&(r.commit=a[2])),r.full_name=r.owner+"/"+r.name,r.query.at?r.ref=r.query.at:r.ref=""),r}S5.stringify=function(e,t){t=t||(e.protocols&&e.protocols.length?e.protocols.join("+"):e.protocol);var r=e.port?":"+e.port:"",s=e.user||"git",a=e.git_suffix?".git":"";switch(t){case"ssh":return r?"ssh://"+s+"@"+e.resource+r+"/"+e.full_name+a:s+"@"+e.resource+":"+e.full_name+a;case"git+ssh":case"ssh+git":case"ftp":case"ftps":return t+"://"+s+"@"+e.resource+r+"/"+e.full_name+a;case"http":case"https":var n=e.token?Mot(e):e.user&&(e.protocols.includes("http")||e.protocols.includes("https"))?e.user+"@":"";return t+"://"+n+e.resource+r+"/"+Uot(e)+a;default:return e.href}};function Mot(e){switch(e.source){case"bitbucket.org":return"x-token-auth:"+e.token+"@";default:return e.token+"@"}}function Uot(e){switch(e.source){case"bitbucket-server":return"scm/"+e.full_name;default:return""+e.full_name}}xge.exports=S5});function nat(e,t){return t===1&&rat.has(e[0])}function nS(e){let t=Array.isArray(e)?e:Ou(e);return t.map((s,a)=>eat.test(s)?`[${s}]`:tat.test(s)&&!nat(t,a)?`.${s}`:`[${JSON.stringify(s)}]`).join("").replace(/^\./,"")}function iat(e,t){let r=[];if(t.methodName!==null&&r.push(pe.pretty(e,t.methodName,pe.Type.CODE)),t.file!==null){let s=[];s.push(pe.pretty(e,t.file,pe.Type.PATH)),t.line!==null&&(s.push(pe.pretty(e,t.line,pe.Type.NUMBER)),t.column!==null&&s.push(pe.pretty(e,t.column,pe.Type.NUMBER))),r.push(`(${s.join(pe.pretty(e,":","grey"))})`)}return r.join(" ")}function jT(e,{manifestUpdates:t,reportedErrors:r},{fix:s}={}){let a=new Map,n=new Map,c=[...r.keys()].map(f=>[f,new Map]);for(let[f,p]of[...c,...t]){let h=r.get(f)?.map(x=>({text:x,fixable:!1}))??[],E=!1,C=e.getWorkspaceByCwd(f),S=C.manifest.exportTo({});for(let[x,I]of p){if(I.size>1){let T=[...I].map(([O,U])=>{let V=pe.pretty(e.configuration,O,pe.Type.INSPECT),te=U.size>0?iat(e.configuration,U.values().next().value):null;return te!==null?` ${V} at ${te}`:` ${V}`}).join("");h.push({text:`Conflict detected in constraint targeting ${pe.pretty(e.configuration,x,pe.Type.CODE)}; conflicting values are:${T}`,fixable:!1})}else{let[[T]]=I,O=Pa(S,x);if(JSON.stringify(O)===JSON.stringify(T))continue;if(!s){let U=typeof O>"u"?`Missing field ${pe.pretty(e.configuration,x,pe.Type.CODE)}; expected ${pe.pretty(e.configuration,T,pe.Type.INSPECT)}`:typeof T>"u"?`Extraneous field ${pe.pretty(e.configuration,x,pe.Type.CODE)} currently set to ${pe.pretty(e.configuration,O,pe.Type.INSPECT)}`:`Invalid field ${pe.pretty(e.configuration,x,pe.Type.CODE)}; expected ${pe.pretty(e.configuration,T,pe.Type.INSPECT)}, found ${pe.pretty(e.configuration,O,pe.Type.INSPECT)}`;h.push({text:U,fixable:!0});continue}typeof T>"u"?f0(S,x):Yg(S,x,T),E=!0}E&&a.set(C,S)}h.length>0&&n.set(C,h)}return{changedWorkspaces:a,remainingErrors:n}}function Wge(e,{configuration:t}){let r={children:[]};for(let[s,a]of e){let n=[];for(let f of a){let p=f.text.split(/\n/);f.fixable&&(p[0]=`${pe.pretty(t,"\u2699","gray")} ${p[0]}`),n.push({value:pe.tuple(pe.Type.NO_HINT,p[0]),children:p.slice(1).map(h=>({value:pe.tuple(pe.Type.NO_HINT,h)}))})}let c={value:pe.tuple(pe.Type.LOCATOR,s.anchoredLocator),children:Ge.sortMap(n,f=>f.value[1])};r.children.push(c)}return r.children=Ge.sortMap(r.children,s=>s.value[1]),r}var OC,eat,tat,rat,iS=Xe(()=>{qe();zl();OC=class{constructor(t){this.indexedFields=t;this.items=[];this.indexes={};this.clear()}clear(){this.items=[];for(let t of this.indexedFields)this.indexes[t]=new Map}insert(t){this.items.push(t);for(let r of this.indexedFields){let s=Object.hasOwn(t,r)?t[r]:void 0;if(typeof s>"u")continue;Ge.getArrayWithDefault(this.indexes[r],s).push(t)}return t}find(t){if(typeof t>"u")return this.items;let r=Object.entries(t);if(r.length===0)return this.items;let s=[],a;for(let[c,f]of r){let p=c,h=Object.hasOwn(this.indexes,p)?this.indexes[p]:void 0;if(typeof h>"u"){s.push([p,f]);continue}let E=new Set(h.get(f)??[]);if(E.size===0)return[];if(typeof a>"u")a=E;else for(let C of a)E.has(C)||a.delete(C);if(a.size===0)break}let n=[...a??[]];return s.length>0&&(n=n.filter(c=>{for(let[f,p]of s)if(!(typeof p<"u"?Object.hasOwn(c,f)&&c[f]===p:Object.hasOwn(c,f)===!1))return!1;return!0})),n}},eat=/^[0-9]+$/,tat=/^[a-zA-Z0-9_]+$/,rat=new Set(["scripts",..._t.allDependencies])});var Yge=G((q9t,G5)=>{var sat;(function(e){var t=function(){return{"append/2":[new e.type.Rule(new e.type.Term("append",[new e.type.Var("X"),new e.type.Var("L")]),new e.type.Term("foldl",[new e.type.Term("append",[]),new e.type.Var("X"),new e.type.Term("[]",[]),new e.type.Var("L")]))],"append/3":[new e.type.Rule(new e.type.Term("append",[new e.type.Term("[]",[]),new e.type.Var("X"),new e.type.Var("X")]),null),new e.type.Rule(new e.type.Term("append",[new e.type.Term(".",[new e.type.Var("H"),new e.type.Var("T")]),new e.type.Var("X"),new e.type.Term(".",[new e.type.Var("H"),new e.type.Var("S")])]),new e.type.Term("append",[new e.type.Var("T"),new e.type.Var("X"),new e.type.Var("S")]))],"member/2":[new e.type.Rule(new e.type.Term("member",[new e.type.Var("X"),new e.type.Term(".",[new e.type.Var("X"),new e.type.Var("_")])]),null),new e.type.Rule(new e.type.Term("member",[new e.type.Var("X"),new e.type.Term(".",[new e.type.Var("_"),new e.type.Var("Xs")])]),new e.type.Term("member",[new e.type.Var("X"),new e.type.Var("Xs")]))],"permutation/2":[new e.type.Rule(new e.type.Term("permutation",[new e.type.Term("[]",[]),new e.type.Term("[]",[])]),null),new e.type.Rule(new e.type.Term("permutation",[new e.type.Term(".",[new e.type.Var("H"),new e.type.Var("T")]),new e.type.Var("S")]),new e.type.Term(",",[new e.type.Term("permutation",[new e.type.Var("T"),new e.type.Var("P")]),new e.type.Term(",",[new e.type.Term("append",[new e.type.Var("X"),new e.type.Var("Y"),new e.type.Var("P")]),new e.type.Term("append",[new e.type.Var("X"),new e.type.Term(".",[new e.type.Var("H"),new e.type.Var("Y")]),new e.type.Var("S")])])]))],"maplist/2":[new e.type.Rule(new e.type.Term("maplist",[new e.type.Var("_"),new e.type.Term("[]",[])]),null),new e.type.Rule(new e.type.Term("maplist",[new e.type.Var("P"),new e.type.Term(".",[new e.type.Var("X"),new e.type.Var("Xs")])]),new e.type.Term(",",[new e.type.Term("call",[new e.type.Var("P"),new e.type.Var("X")]),new e.type.Term("maplist",[new e.type.Var("P"),new e.type.Var("Xs")])]))],"maplist/3":[new e.type.Rule(new e.type.Term("maplist",[new e.type.Var("_"),new e.type.Term("[]",[]),new e.type.Term("[]",[])]),null),new e.type.Rule(new e.type.Term("maplist",[new e.type.Var("P"),new e.type.Term(".",[new e.type.Var("A"),new e.type.Var("As")]),new e.type.Term(".",[new e.type.Var("B"),new e.type.Var("Bs")])]),new e.type.Term(",",[new e.type.Term("call",[new e.type.Var("P"),new e.type.Var("A"),new e.type.Var("B")]),new e.type.Term("maplist",[new e.type.Var("P"),new e.type.Var("As"),new e.type.Var("Bs")])]))],"maplist/4":[new e.type.Rule(new e.type.Term("maplist",[new e.type.Var("_"),new e.type.Term("[]",[]),new e.type.Term("[]",[]),new e.type.Term("[]",[])]),null),new e.type.Rule(new e.type.Term("maplist",[new e.type.Var("P"),new e.type.Term(".",[new e.type.Var("A"),new e.type.Var("As")]),new e.type.Term(".",[new e.type.Var("B"),new e.type.Var("Bs")]),new e.type.Term(".",[new e.type.Var("C"),new e.type.Var("Cs")])]),new e.type.Term(",",[new e.type.Term("call",[new e.type.Var("P"),new e.type.Var("A"),new e.type.Var("B"),new e.type.Var("C")]),new e.type.Term("maplist",[new e.type.Var("P"),new e.type.Var("As"),new e.type.Var("Bs"),new e.type.Var("Cs")])]))],"maplist/5":[new e.type.Rule(new e.type.Term("maplist",[new e.type.Var("_"),new e.type.Term("[]",[]),new e.type.Term("[]",[]),new e.type.Term("[]",[]),new e.type.Term("[]",[])]),null),new e.type.Rule(new e.type.Term("maplist",[new e.type.Var("P"),new e.type.Term(".",[new e.type.Var("A"),new e.type.Var("As")]),new e.type.Term(".",[new e.type.Var("B"),new e.type.Var("Bs")]),new e.type.Term(".",[new e.type.Var("C"),new e.type.Var("Cs")]),new e.type.Term(".",[new e.type.Var("D"),new e.type.Var("Ds")])]),new e.type.Term(",",[new e.type.Term("call",[new e.type.Var("P"),new e.type.Var("A"),new e.type.Var("B"),new e.type.Var("C"),new e.type.Var("D")]),new e.type.Term("maplist",[new e.type.Var("P"),new e.type.Var("As"),new e.type.Var("Bs"),new e.type.Var("Cs"),new e.type.Var("Ds")])]))],"maplist/6":[new e.type.Rule(new e.type.Term("maplist",[new e.type.Var("_"),new e.type.Term("[]",[]),new e.type.Term("[]",[]),new e.type.Term("[]",[]),new e.type.Term("[]",[]),new e.type.Term("[]",[])]),null),new e.type.Rule(new e.type.Term("maplist",[new e.type.Var("P"),new e.type.Term(".",[new e.type.Var("A"),new e.type.Var("As")]),new e.type.Term(".",[new e.type.Var("B"),new e.type.Var("Bs")]),new e.type.Term(".",[new e.type.Var("C"),new e.type.Var("Cs")]),new e.type.Term(".",[new e.type.Var("D"),new e.type.Var("Ds")]),new e.type.Term(".",[new e.type.Var("E"),new e.type.Var("Es")])]),new e.type.Term(",",[new e.type.Term("call",[new e.type.Var("P"),new e.type.Var("A"),new e.type.Var("B"),new e.type.Var("C"),new e.type.Var("D"),new e.type.Var("E")]),new e.type.Term("maplist",[new e.type.Var("P"),new e.type.Var("As"),new e.type.Var("Bs"),new e.type.Var("Cs"),new e.type.Var("Ds"),new e.type.Var("Es")])]))],"maplist/7":[new e.type.Rule(new e.type.Term("maplist",[new e.type.Var("_"),new e.type.Term("[]",[]),new e.type.Term("[]",[]),new e.type.Term("[]",[]),new e.type.Term("[]",[]),new e.type.Term("[]",[]),new e.type.Term("[]",[])]),null),new e.type.Rule(new e.type.Term("maplist",[new e.type.Var("P"),new e.type.Term(".",[new e.type.Var("A"),new e.type.Var("As")]),new e.type.Term(".",[new e.type.Var("B"),new e.type.Var("Bs")]),new e.type.Term(".",[new e.type.Var("C"),new e.type.Var("Cs")]),new e.type.Term(".",[new e.type.Var("D"),new e.type.Var("Ds")]),new e.type.Term(".",[new e.type.Var("E"),new e.type.Var("Es")]),new e.type.Term(".",[new e.type.Var("F"),new e.type.Var("Fs")])]),new e.type.Term(",",[new e.type.Term("call",[new e.type.Var("P"),new e.type.Var("A"),new e.type.Var("B"),new e.type.Var("C"),new e.type.Var("D"),new e.type.Var("E"),new e.type.Var("F")]),new e.type.Term("maplist",[new e.type.Var("P"),new e.type.Var("As"),new e.type.Var("Bs"),new e.type.Var("Cs"),new e.type.Var("Ds"),new e.type.Var("Es"),new e.type.Var("Fs")])]))],"maplist/8":[new e.type.Rule(new e.type.Term("maplist",[new e.type.Var("_"),new e.type.Term("[]",[]),new e.type.Term("[]",[]),new e.type.Term("[]",[]),new e.type.Term("[]",[]),new e.type.Term("[]",[]),new e.type.Term("[]",[]),new e.type.Term("[]",[])]),null),new e.type.Rule(new e.type.Term("maplist",[new e.type.Var("P"),new e.type.Term(".",[new e.type.Var("A"),new e.type.Var("As")]),new e.type.Term(".",[new e.type.Var("B"),new e.type.Var("Bs")]),new e.type.Term(".",[new e.type.Var("C"),new e.type.Var("Cs")]),new e.type.Term(".",[new e.type.Var("D"),new e.type.Var("Ds")]),new e.type.Term(".",[new e.type.Var("E"),new e.type.Var("Es")]),new e.type.Term(".",[new e.type.Var("F"),new e.type.Var("Fs")]),new e.type.Term(".",[new e.type.Var("G"),new e.type.Var("Gs")])]),new e.type.Term(",",[new e.type.Term("call",[new e.type.Var("P"),new e.type.Var("A"),new e.type.Var("B"),new e.type.Var("C"),new e.type.Var("D"),new e.type.Var("E"),new e.type.Var("F"),new e.type.Var("G")]),new e.type.Term("maplist",[new e.type.Var("P"),new e.type.Var("As"),new e.type.Var("Bs"),new e.type.Var("Cs"),new e.type.Var("Ds"),new e.type.Var("Es"),new e.type.Var("Fs"),new e.type.Var("Gs")])]))],"include/3":[new e.type.Rule(new e.type.Term("include",[new e.type.Var("_"),new e.type.Term("[]",[]),new e.type.Term("[]",[])]),null),new e.type.Rule(new e.type.Term("include",[new e.type.Var("P"),new e.type.Term(".",[new e.type.Var("H"),new e.type.Var("T")]),new e.type.Var("L")]),new e.type.Term(",",[new e.type.Term("=..",[new e.type.Var("P"),new e.type.Var("A")]),new e.type.Term(",",[new e.type.Term("append",[new e.type.Var("A"),new e.type.Term(".",[new e.type.Var("H"),new e.type.Term("[]",[])]),new e.type.Var("B")]),new e.type.Term(",",[new e.type.Term("=..",[new e.type.Var("F"),new e.type.Var("B")]),new e.type.Term(",",[new e.type.Term(";",[new e.type.Term(",",[new e.type.Term("call",[new e.type.Var("F")]),new e.type.Term(",",[new e.type.Term("=",[new e.type.Var("L"),new e.type.Term(".",[new e.type.Var("H"),new e.type.Var("S")])]),new e.type.Term("!",[])])]),new e.type.Term("=",[new e.type.Var("L"),new e.type.Var("S")])]),new e.type.Term("include",[new e.type.Var("P"),new e.type.Var("T"),new e.type.Var("S")])])])])]))],"exclude/3":[new e.type.Rule(new e.type.Term("exclude",[new e.type.Var("_"),new e.type.Term("[]",[]),new e.type.Term("[]",[])]),null),new e.type.Rule(new e.type.Term("exclude",[new e.type.Var("P"),new e.type.Term(".",[new e.type.Var("H"),new e.type.Var("T")]),new e.type.Var("S")]),new e.type.Term(",",[new e.type.Term("exclude",[new e.type.Var("P"),new e.type.Var("T"),new e.type.Var("E")]),new e.type.Term(",",[new e.type.Term("=..",[new e.type.Var("P"),new e.type.Var("L")]),new e.type.Term(",",[new e.type.Term("append",[new e.type.Var("L"),new e.type.Term(".",[new e.type.Var("H"),new e.type.Term("[]",[])]),new e.type.Var("Q")]),new e.type.Term(",",[new e.type.Term("=..",[new e.type.Var("R"),new e.type.Var("Q")]),new e.type.Term(";",[new e.type.Term(",",[new e.type.Term("call",[new e.type.Var("R")]),new e.type.Term(",",[new e.type.Term("!",[]),new e.type.Term("=",[new e.type.Var("S"),new e.type.Var("E")])])]),new e.type.Term("=",[new e.type.Var("S"),new e.type.Term(".",[new e.type.Var("H"),new e.type.Var("E")])])])])])])]))],"foldl/4":[new e.type.Rule(new e.type.Term("foldl",[new e.type.Var("_"),new e.type.Term("[]",[]),new e.type.Var("I"),new e.type.Var("I")]),null),new e.type.Rule(new e.type.Term("foldl",[new e.type.Var("P"),new e.type.Term(".",[new e.type.Var("H"),new e.type.Var("T")]),new e.type.Var("I"),new e.type.Var("R")]),new e.type.Term(",",[new e.type.Term("=..",[new e.type.Var("P"),new e.type.Var("L")]),new e.type.Term(",",[new e.type.Term("append",[new e.type.Var("L"),new e.type.Term(".",[new e.type.Var("I"),new e.type.Term(".",[new e.type.Var("H"),new e.type.Term(".",[new e.type.Var("X"),new e.type.Term("[]",[])])])]),new e.type.Var("L2")]),new e.type.Term(",",[new e.type.Term("=..",[new e.type.Var("P2"),new e.type.Var("L2")]),new e.type.Term(",",[new e.type.Term("call",[new e.type.Var("P2")]),new e.type.Term("foldl",[new e.type.Var("P"),new e.type.Var("T"),new e.type.Var("X"),new e.type.Var("R")])])])])]))],"select/3":[new e.type.Rule(new e.type.Term("select",[new e.type.Var("E"),new e.type.Term(".",[new e.type.Var("E"),new e.type.Var("Xs")]),new e.type.Var("Xs")]),null),new e.type.Rule(new e.type.Term("select",[new e.type.Var("E"),new e.type.Term(".",[new e.type.Var("X"),new e.type.Var("Xs")]),new e.type.Term(".",[new e.type.Var("X"),new e.type.Var("Ys")])]),new e.type.Term("select",[new e.type.Var("E"),new e.type.Var("Xs"),new e.type.Var("Ys")]))],"sum_list/2":[new e.type.Rule(new e.type.Term("sum_list",[new e.type.Term("[]",[]),new e.type.Num(0,!1)]),null),new e.type.Rule(new e.type.Term("sum_list",[new e.type.Term(".",[new e.type.Var("X"),new e.type.Var("Xs")]),new e.type.Var("S")]),new e.type.Term(",",[new e.type.Term("sum_list",[new e.type.Var("Xs"),new e.type.Var("Y")]),new e.type.Term("is",[new e.type.Var("S"),new e.type.Term("+",[new e.type.Var("X"),new e.type.Var("Y")])])]))],"max_list/2":[new e.type.Rule(new e.type.Term("max_list",[new e.type.Term(".",[new e.type.Var("X"),new e.type.Term("[]",[])]),new e.type.Var("X")]),null),new e.type.Rule(new e.type.Term("max_list",[new e.type.Term(".",[new e.type.Var("X"),new e.type.Var("Xs")]),new e.type.Var("S")]),new e.type.Term(",",[new e.type.Term("max_list",[new e.type.Var("Xs"),new e.type.Var("Y")]),new e.type.Term(";",[new e.type.Term(",",[new e.type.Term(">=",[new e.type.Var("X"),new e.type.Var("Y")]),new e.type.Term(",",[new e.type.Term("=",[new e.type.Var("S"),new e.type.Var("X")]),new e.type.Term("!",[])])]),new e.type.Term("=",[new e.type.Var("S"),new e.type.Var("Y")])])]))],"min_list/2":[new e.type.Rule(new e.type.Term("min_list",[new e.type.Term(".",[new e.type.Var("X"),new e.type.Term("[]",[])]),new e.type.Var("X")]),null),new e.type.Rule(new e.type.Term("min_list",[new e.type.Term(".",[new e.type.Var("X"),new e.type.Var("Xs")]),new e.type.Var("S")]),new e.type.Term(",",[new e.type.Term("min_list",[new e.type.Var("Xs"),new e.type.Var("Y")]),new e.type.Term(";",[new e.type.Term(",",[new e.type.Term("=<",[new e.type.Var("X"),new e.type.Var("Y")]),new e.type.Term(",",[new e.type.Term("=",[new e.type.Var("S"),new e.type.Var("X")]),new e.type.Term("!",[])])]),new e.type.Term("=",[new e.type.Var("S"),new e.type.Var("Y")])])]))],"prod_list/2":[new e.type.Rule(new e.type.Term("prod_list",[new e.type.Term("[]",[]),new e.type.Num(1,!1)]),null),new e.type.Rule(new e.type.Term("prod_list",[new e.type.Term(".",[new e.type.Var("X"),new e.type.Var("Xs")]),new e.type.Var("S")]),new e.type.Term(",",[new e.type.Term("prod_list",[new e.type.Var("Xs"),new e.type.Var("Y")]),new e.type.Term("is",[new e.type.Var("S"),new e.type.Term("*",[new e.type.Var("X"),new e.type.Var("Y")])])]))],"last/2":[new e.type.Rule(new e.type.Term("last",[new e.type.Term(".",[new e.type.Var("X"),new e.type.Term("[]",[])]),new e.type.Var("X")]),null),new e.type.Rule(new e.type.Term("last",[new e.type.Term(".",[new e.type.Var("_"),new e.type.Var("Xs")]),new e.type.Var("X")]),new e.type.Term("last",[new e.type.Var("Xs"),new e.type.Var("X")]))],"prefix/2":[new e.type.Rule(new e.type.Term("prefix",[new e.type.Var("Part"),new e.type.Var("Whole")]),new e.type.Term("append",[new e.type.Var("Part"),new e.type.Var("_"),new e.type.Var("Whole")]))],"nth0/3":[new e.type.Rule(new e.type.Term("nth0",[new e.type.Var("X"),new e.type.Var("Y"),new e.type.Var("Z")]),new e.type.Term(";",[new e.type.Term("->",[new e.type.Term("var",[new e.type.Var("X")]),new e.type.Term("nth",[new e.type.Num(0,!1),new e.type.Var("X"),new e.type.Var("Y"),new e.type.Var("Z"),new e.type.Var("_")])]),new e.type.Term(",",[new e.type.Term(">=",[new e.type.Var("X"),new e.type.Num(0,!1)]),new e.type.Term(",",[new e.type.Term("nth",[new e.type.Num(0,!1),new e.type.Var("X"),new e.type.Var("Y"),new e.type.Var("Z"),new e.type.Var("_")]),new e.type.Term("!",[])])])]))],"nth1/3":[new e.type.Rule(new e.type.Term("nth1",[new e.type.Var("X"),new e.type.Var("Y"),new e.type.Var("Z")]),new e.type.Term(";",[new e.type.Term("->",[new e.type.Term("var",[new e.type.Var("X")]),new e.type.Term("nth",[new e.type.Num(1,!1),new e.type.Var("X"),new e.type.Var("Y"),new e.type.Var("Z"),new e.type.Var("_")])]),new e.type.Term(",",[new e.type.Term(">",[new e.type.Var("X"),new e.type.Num(0,!1)]),new e.type.Term(",",[new e.type.Term("nth",[new e.type.Num(1,!1),new e.type.Var("X"),new e.type.Var("Y"),new e.type.Var("Z"),new e.type.Var("_")]),new e.type.Term("!",[])])])]))],"nth0/4":[new e.type.Rule(new e.type.Term("nth0",[new e.type.Var("X"),new e.type.Var("Y"),new e.type.Var("Z"),new e.type.Var("W")]),new e.type.Term(";",[new e.type.Term("->",[new e.type.Term("var",[new e.type.Var("X")]),new e.type.Term("nth",[new e.type.Num(0,!1),new e.type.Var("X"),new e.type.Var("Y"),new e.type.Var("Z"),new e.type.Var("W")])]),new e.type.Term(",",[new e.type.Term(">=",[new e.type.Var("X"),new e.type.Num(0,!1)]),new e.type.Term(",",[new e.type.Term("nth",[new e.type.Num(0,!1),new e.type.Var("X"),new e.type.Var("Y"),new e.type.Var("Z"),new e.type.Var("W")]),new e.type.Term("!",[])])])]))],"nth1/4":[new e.type.Rule(new e.type.Term("nth1",[new e.type.Var("X"),new e.type.Var("Y"),new e.type.Var("Z"),new e.type.Var("W")]),new e.type.Term(";",[new e.type.Term("->",[new e.type.Term("var",[new e.type.Var("X")]),new e.type.Term("nth",[new e.type.Num(1,!1),new e.type.Var("X"),new e.type.Var("Y"),new e.type.Var("Z"),new e.type.Var("W")])]),new e.type.Term(",",[new e.type.Term(">",[new e.type.Var("X"),new e.type.Num(0,!1)]),new e.type.Term(",",[new e.type.Term("nth",[new e.type.Num(1,!1),new e.type.Var("X"),new e.type.Var("Y"),new e.type.Var("Z"),new e.type.Var("W")]),new e.type.Term("!",[])])])]))],"nth/5":[new e.type.Rule(new e.type.Term("nth",[new e.type.Var("N"),new e.type.Var("N"),new e.type.Term(".",[new e.type.Var("X"),new e.type.Var("Xs")]),new e.type.Var("X"),new e.type.Var("Xs")]),null),new e.type.Rule(new e.type.Term("nth",[new e.type.Var("N"),new e.type.Var("O"),new e.type.Term(".",[new e.type.Var("X"),new e.type.Var("Xs")]),new e.type.Var("Y"),new e.type.Term(".",[new e.type.Var("X"),new e.type.Var("Ys")])]),new e.type.Term(",",[new e.type.Term("is",[new e.type.Var("M"),new e.type.Term("+",[new e.type.Var("N"),new e.type.Num(1,!1)])]),new e.type.Term("nth",[new e.type.Var("M"),new e.type.Var("O"),new e.type.Var("Xs"),new e.type.Var("Y"),new e.type.Var("Ys")])]))],"length/2":function(s,a,n){var c=n.args[0],f=n.args[1];if(!e.type.is_variable(f)&&!e.type.is_integer(f))s.throw_error(e.error.type("integer",f,n.indicator));else if(e.type.is_integer(f)&&f.value<0)s.throw_error(e.error.domain("not_less_than_zero",f,n.indicator));else{var p=new e.type.Term("length",[c,new e.type.Num(0,!1),f]);e.type.is_integer(f)&&(p=new e.type.Term(",",[p,new e.type.Term("!",[])])),s.prepend([new e.type.State(a.goal.replace(p),a.substitution,a)])}},"length/3":[new e.type.Rule(new e.type.Term("length",[new e.type.Term("[]",[]),new e.type.Var("N"),new e.type.Var("N")]),null),new e.type.Rule(new e.type.Term("length",[new e.type.Term(".",[new e.type.Var("_"),new e.type.Var("X")]),new e.type.Var("A"),new e.type.Var("N")]),new e.type.Term(",",[new e.type.Term("succ",[new e.type.Var("A"),new e.type.Var("B")]),new e.type.Term("length",[new e.type.Var("X"),new e.type.Var("B"),new e.type.Var("N")])]))],"replicate/3":function(s,a,n){var c=n.args[0],f=n.args[1],p=n.args[2];if(e.type.is_variable(f))s.throw_error(e.error.instantiation(n.indicator));else if(!e.type.is_integer(f))s.throw_error(e.error.type("integer",f,n.indicator));else if(f.value<0)s.throw_error(e.error.domain("not_less_than_zero",f,n.indicator));else if(!e.type.is_variable(p)&&!e.type.is_list(p))s.throw_error(e.error.type("list",p,n.indicator));else{for(var h=new e.type.Term("[]"),E=0;E0;C--)E[C].equals(E[C-1])&&E.splice(C,1);for(var S=new e.type.Term("[]"),C=E.length-1;C>=0;C--)S=new e.type.Term(".",[E[C],S]);s.prepend([new e.type.State(a.goal.replace(new e.type.Term("=",[S,f])),a.substitution,a)])}}},"msort/2":function(s,a,n){var c=n.args[0],f=n.args[1];if(e.type.is_variable(c))s.throw_error(e.error.instantiation(n.indicator));else if(!e.type.is_variable(f)&&!e.type.is_fully_list(f))s.throw_error(e.error.type("list",f,n.indicator));else{for(var p=[],h=c;h.indicator==="./2";)p.push(h.args[0]),h=h.args[1];if(e.type.is_variable(h))s.throw_error(e.error.instantiation(n.indicator));else if(!e.type.is_empty_list(h))s.throw_error(e.error.type("list",c,n.indicator));else{for(var E=p.sort(e.compare),C=new e.type.Term("[]"),S=E.length-1;S>=0;S--)C=new e.type.Term(".",[E[S],C]);s.prepend([new e.type.State(a.goal.replace(new e.type.Term("=",[C,f])),a.substitution,a)])}}},"keysort/2":function(s,a,n){var c=n.args[0],f=n.args[1];if(e.type.is_variable(c))s.throw_error(e.error.instantiation(n.indicator));else if(!e.type.is_variable(f)&&!e.type.is_fully_list(f))s.throw_error(e.error.type("list",f,n.indicator));else{for(var p=[],h,E=c;E.indicator==="./2";){if(h=E.args[0],e.type.is_variable(h)){s.throw_error(e.error.instantiation(n.indicator));return}else if(!e.type.is_term(h)||h.indicator!=="-/2"){s.throw_error(e.error.type("pair",h,n.indicator));return}h.args[0].pair=h.args[1],p.push(h.args[0]),E=E.args[1]}if(e.type.is_variable(E))s.throw_error(e.error.instantiation(n.indicator));else if(!e.type.is_empty_list(E))s.throw_error(e.error.type("list",c,n.indicator));else{for(var C=p.sort(e.compare),S=new e.type.Term("[]"),x=C.length-1;x>=0;x--)S=new e.type.Term(".",[new e.type.Term("-",[C[x],C[x].pair]),S]),delete C[x].pair;s.prepend([new e.type.State(a.goal.replace(new e.type.Term("=",[S,f])),a.substitution,a)])}}},"take/3":function(s,a,n){var c=n.args[0],f=n.args[1],p=n.args[2];if(e.type.is_variable(f)||e.type.is_variable(c))s.throw_error(e.error.instantiation(n.indicator));else if(!e.type.is_list(f))s.throw_error(e.error.type("list",f,n.indicator));else if(!e.type.is_integer(c))s.throw_error(e.error.type("integer",c,n.indicator));else if(!e.type.is_variable(p)&&!e.type.is_list(p))s.throw_error(e.error.type("list",p,n.indicator));else{for(var h=c.value,E=[],C=f;h>0&&C.indicator==="./2";)E.push(C.args[0]),C=C.args[1],h--;if(h===0){for(var S=new e.type.Term("[]"),h=E.length-1;h>=0;h--)S=new e.type.Term(".",[E[h],S]);s.prepend([new e.type.State(a.goal.replace(new e.type.Term("=",[S,p])),a.substitution,a)])}}},"drop/3":function(s,a,n){var c=n.args[0],f=n.args[1],p=n.args[2];if(e.type.is_variable(f)||e.type.is_variable(c))s.throw_error(e.error.instantiation(n.indicator));else if(!e.type.is_list(f))s.throw_error(e.error.type("list",f,n.indicator));else if(!e.type.is_integer(c))s.throw_error(e.error.type("integer",c,n.indicator));else if(!e.type.is_variable(p)&&!e.type.is_list(p))s.throw_error(e.error.type("list",p,n.indicator));else{for(var h=c.value,E=[],C=f;h>0&&C.indicator==="./2";)E.push(C.args[0]),C=C.args[1],h--;h===0&&s.prepend([new e.type.State(a.goal.replace(new e.type.Term("=",[C,p])),a.substitution,a)])}},"reverse/2":function(s,a,n){var c=n.args[0],f=n.args[1],p=e.type.is_instantiated_list(c),h=e.type.is_instantiated_list(f);if(e.type.is_variable(c)&&e.type.is_variable(f))s.throw_error(e.error.instantiation(n.indicator));else if(!e.type.is_variable(c)&&!e.type.is_fully_list(c))s.throw_error(e.error.type("list",c,n.indicator));else if(!e.type.is_variable(f)&&!e.type.is_fully_list(f))s.throw_error(e.error.type("list",f,n.indicator));else if(!p&&!h)s.throw_error(e.error.instantiation(n.indicator));else{for(var E=p?c:f,C=new e.type.Term("[]",[]);E.indicator==="./2";)C=new e.type.Term(".",[E.args[0],C]),E=E.args[1];s.prepend([new e.type.State(a.goal.replace(new e.type.Term("=",[C,p?f:c])),a.substitution,a)])}},"list_to_set/2":function(s,a,n){var c=n.args[0],f=n.args[1];if(e.type.is_variable(c))s.throw_error(e.error.instantiation(n.indicator));else{for(var p=c,h=[];p.indicator==="./2";)h.push(p.args[0]),p=p.args[1];if(e.type.is_variable(p))s.throw_error(e.error.instantiation(n.indicator));else if(!e.type.is_term(p)||p.indicator!=="[]/0")s.throw_error(e.error.type("list",c,n.indicator));else{for(var E=[],C=new e.type.Term("[]",[]),S,x=0;x=0;x--)C=new e.type.Term(".",[E[x],C]);s.prepend([new e.type.State(a.goal.replace(new e.type.Term("=",[f,C])),a.substitution,a)])}}}}},r=["append/2","append/3","member/2","permutation/2","maplist/2","maplist/3","maplist/4","maplist/5","maplist/6","maplist/7","maplist/8","include/3","exclude/3","foldl/4","sum_list/2","max_list/2","min_list/2","prod_list/2","last/2","prefix/2","nth0/3","nth1/3","nth0/4","nth1/4","length/2","replicate/3","select/3","sort/2","msort/2","keysort/2","take/3","drop/3","reverse/2","list_to_set/2"];typeof G5<"u"?G5.exports=function(s){e=s,new e.type.Module("lists",t(),r)}:new e.type.Module("lists",t(),r)})(sat)});var ame=G($r=>{"use strict";var km=process.platform==="win32",q5="aes-256-cbc",oat="sha256",Kge="The current environment doesn't support interactive reading from TTY.",ai=Ie("fs"),Vge=process.binding("tty_wrap").TTY,Y5=Ie("child_process"),W0=Ie("path"),V5={prompt:"> ",hideEchoBack:!1,mask:"*",limit:[],limitMessage:"Input another, please.$<( [)limit(])>",defaultInput:"",trueValue:[],falseValue:[],caseSensitive:!1,keepWhitespace:!1,encoding:"utf8",bufferSize:1024,print:void 0,history:!0,cd:!1,phContent:void 0,preCheck:void 0},Xp="none",ef,MC,Jge=!1,q0,qT,W5,aat=0,Z5="",xm=[],WT,zge=!1,J5=!1,sS=!1;function Xge(e){function t(r){return r.replace(/[^\w\u0080-\uFFFF]/g,function(s){return"#"+s.charCodeAt(0)+";"})}return qT.concat(function(r){var s=[];return Object.keys(r).forEach(function(a){r[a]==="boolean"?e[a]&&s.push("--"+a):r[a]==="string"&&e[a]&&s.push("--"+a,t(e[a]))}),s}({display:"string",displayOnly:"boolean",keyIn:"boolean",hideEchoBack:"boolean",mask:"string",limit:"string",caseSensitive:"boolean"}))}function lat(e,t){function r(U){var V,te="",ie;for(W5=W5||Ie("os").tmpdir();;){V=W0.join(W5,U+te);try{ie=ai.openSync(V,"wx")}catch(ue){if(ue.code==="EEXIST"){te++;continue}else throw ue}ai.closeSync(ie);break}return V}var s,a,n,c={},f,p,h=r("readline-sync.stdout"),E=r("readline-sync.stderr"),C=r("readline-sync.exit"),S=r("readline-sync.done"),x=Ie("crypto"),I,T,O;I=x.createHash(oat),I.update(""+process.pid+aat+++Math.random()),O=I.digest("hex"),T=x.createDecipher(q5,O),s=Xge(e),km?(a=process.env.ComSpec||"cmd.exe",process.env.Q='"',n=["/V:ON","/S","/C","(%Q%"+a+"%Q% /V:ON /S /C %Q%%Q%"+q0+"%Q%"+s.map(function(U){return" %Q%"+U+"%Q%"}).join("")+" & (echo !ERRORLEVEL!)>%Q%"+C+"%Q%%Q%) 2>%Q%"+E+"%Q% |%Q%"+process.execPath+"%Q% %Q%"+__dirname+"\\encrypt.js%Q% %Q%"+q5+"%Q% %Q%"+O+"%Q% >%Q%"+h+"%Q% & (echo 1)>%Q%"+S+"%Q%"]):(a="/bin/sh",n=["-c",'("'+q0+'"'+s.map(function(U){return" '"+U.replace(/'/g,"'\\''")+"'"}).join("")+'; echo $?>"'+C+'") 2>"'+E+'" |"'+process.execPath+'" "'+__dirname+'/encrypt.js" "'+q5+'" "'+O+'" >"'+h+'"; echo 1 >"'+S+'"']),sS&&sS("_execFileSync",s);try{Y5.spawn(a,n,t)}catch(U){c.error=new Error(U.message),c.error.method="_execFileSync - spawn",c.error.program=a,c.error.args=n}for(;ai.readFileSync(S,{encoding:e.encoding}).trim()!=="1";);return(f=ai.readFileSync(C,{encoding:e.encoding}).trim())==="0"?c.input=T.update(ai.readFileSync(h,{encoding:"binary"}),"hex",e.encoding)+T.final(e.encoding):(p=ai.readFileSync(E,{encoding:e.encoding}).trim(),c.error=new Error(Kge+(p?` `+p:"")),c.error.method="_execFileSync",c.error.program=a,c.error.args=n,c.error.extMessage=p,c.error.exitCode=+f),ai.unlinkSync(h),ai.unlinkSync(E),ai.unlinkSync(C),ai.unlinkSync(S),c}function cat(e){var t,r={},s,a={env:process.env,encoding:e.encoding};if(q0||(km?process.env.PSModulePath?(q0="powershell.exe",qT=["-ExecutionPolicy","Bypass","-File",__dirname+"\\read.ps1"]):(q0="cscript.exe",qT=["//nologo",__dirname+"\\read.cs.js"]):(q0="/bin/sh",qT=[__dirname+"/read.sh"])),km&&!process.env.PSModulePath&&(a.stdio=[process.stdin]),Y5.execFileSync){t=Xge(e),sS&&sS("execFileSync",t);try{r.input=Y5.execFileSync(q0,t,a)}catch(n){s=n.stderr?(n.stderr+"").trim():"",r.error=new Error(Kge+(s?` `+s:"")),r.error.method="execFileSync",r.error.program=q0,r.error.args=t,r.error.extMessage=s,r.error.exitCode=n.status,r.error.code=n.code,r.error.signal=n.signal}}else r=lat(e,a);return r.error||(r.input=r.input.replace(/^\s*'|'\s*$/g,""),e.display=""),r}function K5(e){var t="",r=e.display,s=!e.display&&e.keyIn&&e.hideEchoBack&&!e.mask;function a(){var n=cat(e);if(n.error)throw n.error;return n.input}return J5&&J5(e),function(){var n,c,f;function p(){return n||(n=process.binding("fs"),c=process.binding("constants")),n}if(typeof Xp=="string")if(Xp=null,km){if(f=function(h){var E=h.replace(/^\D+/,"").split("."),C=0;return(E[0]=+E[0])&&(C+=E[0]*1e4),(E[1]=+E[1])&&(C+=E[1]*100),(E[2]=+E[2])&&(C+=E[2]),C}(process.version),!(f>=20302&&f<40204||f>=5e4&&f<50100||f>=50600&&f<60200)&&process.stdin.isTTY)process.stdin.pause(),Xp=process.stdin.fd,MC=process.stdin._handle;else try{Xp=p().open("CONIN$",c.O_RDWR,parseInt("0666",8)),MC=new Vge(Xp,!0)}catch{}if(process.stdout.isTTY)ef=process.stdout.fd;else{try{ef=ai.openSync("\\\\.\\CON","w")}catch{}if(typeof ef!="number")try{ef=p().open("CONOUT$",c.O_RDWR,parseInt("0666",8))}catch{}}}else{if(process.stdin.isTTY){process.stdin.pause();try{Xp=ai.openSync("/dev/tty","r"),MC=process.stdin._handle}catch{}}else try{Xp=ai.openSync("/dev/tty","r"),MC=new Vge(Xp,!1)}catch{}if(process.stdout.isTTY)ef=process.stdout.fd;else try{ef=ai.openSync("/dev/tty","w")}catch{}}}(),function(){var n,c,f=!e.hideEchoBack&&!e.keyIn,p,h,E,C,S;WT="";function x(I){return I===Jge?!0:MC.setRawMode(I)!==0?!1:(Jge=I,!0)}if(zge||!MC||typeof ef!="number"&&(e.display||!f)){t=a();return}if(e.display&&(ai.writeSync(ef,e.display),e.display=""),!e.displayOnly){if(!x(!f)){t=a();return}for(h=e.keyIn?1:e.bufferSize,p=Buffer.allocUnsafe&&Buffer.alloc?Buffer.alloc(h):new Buffer(h),e.keyIn&&e.limit&&(c=new RegExp("[^"+e.limit+"]","g"+(e.caseSensitive?"":"i")));;){E=0;try{E=ai.readSync(Xp,p,0,h)}catch(I){if(I.code!=="EOF"){x(!1),t+=a();return}}if(E>0?(C=p.toString(e.encoding,0,E),WT+=C):(C=` `,WT+="\0"),C&&typeof(S=(C.match(/^(.*?)[\r\n]/)||[])[1])=="string"&&(C=S,n=!0),C&&(C=C.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g,"")),C&&c&&(C=C.replace(c,"")),C&&(f||(e.hideEchoBack?e.mask&&ai.writeSync(ef,new Array(C.length+1).join(e.mask)):ai.writeSync(ef,C)),t+=C),!e.keyIn&&n||e.keyIn&&t.length>=h)break}!f&&!s&&ai.writeSync(ef,` `),x(!1)}}(),e.print&&!s&&e.print(r+(e.displayOnly?"":(e.hideEchoBack?new Array(t.length+1).join(e.mask):t)+` `),e.encoding),e.displayOnly?"":Z5=e.keepWhitespace||e.keyIn?t:t.trim()}function uat(e,t){var r=[];function s(a){a!=null&&(Array.isArray(a)?a.forEach(s):(!t||t(a))&&r.push(a))}return s(e),r}function $5(e){return e.replace(/[\x00-\x7f]/g,function(t){return"\\x"+("00"+t.charCodeAt().toString(16)).substr(-2)})}function $s(){var e=Array.prototype.slice.call(arguments),t,r;return e.length&&typeof e[0]=="boolean"&&(r=e.shift(),r&&(t=Object.keys(V5),e.unshift(V5))),e.reduce(function(s,a){return a==null||(a.hasOwnProperty("noEchoBack")&&!a.hasOwnProperty("hideEchoBack")&&(a.hideEchoBack=a.noEchoBack,delete a.noEchoBack),a.hasOwnProperty("noTrim")&&!a.hasOwnProperty("keepWhitespace")&&(a.keepWhitespace=a.noTrim,delete a.noTrim),r||(t=Object.keys(a)),t.forEach(function(n){var c;if(a.hasOwnProperty(n))switch(c=a[n],n){case"mask":case"limitMessage":case"defaultInput":case"encoding":c=c!=null?c+"":"",c&&n!=="limitMessage"&&(c=c.replace(/[\r\n]/g,"")),s[n]=c;break;case"bufferSize":!isNaN(c=parseInt(c,10))&&typeof c=="number"&&(s[n]=c);break;case"displayOnly":case"keyIn":case"hideEchoBack":case"caseSensitive":case"keepWhitespace":case"history":case"cd":s[n]=!!c;break;case"limit":case"trueValue":case"falseValue":s[n]=uat(c,function(f){var p=typeof f;return p==="string"||p==="number"||p==="function"||f instanceof RegExp}).map(function(f){return typeof f=="string"?f.replace(/[\r\n]/g,""):f});break;case"print":case"phContent":case"preCheck":s[n]=typeof c=="function"?c:void 0;break;case"prompt":case"display":s[n]=c??"";break}})),s},{})}function z5(e,t,r){return t.some(function(s){var a=typeof s;return a==="string"?r?e===s:e.toLowerCase()===s.toLowerCase():a==="number"?parseFloat(e)===s:a==="function"?s(e):s instanceof RegExp?s.test(e):!1})}function e9(e,t){var r=W0.normalize(km?(process.env.HOMEDRIVE||"")+(process.env.HOMEPATH||""):process.env.HOME||"").replace(/[\/\\]+$/,"");return e=W0.normalize(e),t?e.replace(/^~(?=\/|\\|$)/,r):e.replace(new RegExp("^"+$5(r)+"(?=\\/|\\\\|$)",km?"i":""),"~")}function UC(e,t){var r="(?:\\(([\\s\\S]*?)\\))?(\\w+|.-.)(?:\\(([\\s\\S]*?)\\))?",s=new RegExp("(\\$)?(\\$<"+r+">)","g"),a=new RegExp("(\\$)?(\\$\\{"+r+"\\})","g");function n(c,f,p,h,E,C){var S;return f||typeof(S=t(E))!="string"?p:S?(h||"")+S+(C||""):""}return e.replace(s,n).replace(a,n)}function Zge(e,t,r){var s,a=[],n=-1,c=0,f="",p;function h(E,C){return C.length>3?(E.push(C[0]+"..."+C[C.length-1]),p=!0):C.length&&(E=E.concat(C)),E}return s=e.reduce(function(E,C){return E.concat((C+"").split(""))},[]).reduce(function(E,C){var S,x;return t||(C=C.toLowerCase()),S=/^\d$/.test(C)?1:/^[A-Z]$/.test(C)?2:/^[a-z]$/.test(C)?3:0,r&&S===0?f+=C:(x=C.charCodeAt(0),S&&S===n&&x===c+1?a.push(C):(E=h(E,a),a=[C],n=S),c=x),E},[]),s=h(s,a),f&&(s.push(f),p=!0),{values:s,suppressed:p}}function $ge(e,t){return e.join(e.length>2?", ":t?" / ":"/")}function eme(e,t){var r,s,a={},n;if(t.phContent&&(r=t.phContent(e,t)),typeof r!="string")switch(e){case"hideEchoBack":case"mask":case"defaultInput":case"caseSensitive":case"keepWhitespace":case"encoding":case"bufferSize":case"history":case"cd":r=t.hasOwnProperty(e)?typeof t[e]=="boolean"?t[e]?"on":"off":t[e]+"":"";break;case"limit":case"trueValue":case"falseValue":s=t[t.hasOwnProperty(e+"Src")?e+"Src":e],t.keyIn?(a=Zge(s,t.caseSensitive),s=a.values):s=s.filter(function(c){var f=typeof c;return f==="string"||f==="number"}),r=$ge(s,a.suppressed);break;case"limitCount":case"limitCountNotZero":r=t[t.hasOwnProperty("limitSrc")?"limitSrc":"limit"].length,r=r||e!=="limitCountNotZero"?r+"":"";break;case"lastInput":r=Z5;break;case"cwd":case"CWD":case"cwdHome":r=process.cwd(),e==="CWD"?r=W0.basename(r):e==="cwdHome"&&(r=e9(r));break;case"date":case"time":case"localeDate":case"localeTime":r=new Date()["to"+e.replace(/^./,function(c){return c.toUpperCase()})+"String"]();break;default:typeof(n=(e.match(/^history_m(\d+)$/)||[])[1])=="string"&&(r=xm[xm.length-n]||"")}return r}function tme(e){var t=/^(.)-(.)$/.exec(e),r="",s,a,n,c;if(!t)return null;for(s=t[1].charCodeAt(0),a=t[2].charCodeAt(0),c=s And the length must be: $`,trueValue:null,falseValue:null,caseSensitive:!0},t,{history:!1,cd:!1,phContent:function(x){return x==="charlist"?r.text:x==="length"?s+"..."+a:null}}),c,f,p,h,E,C,S;for(t=t||{},c=UC(t.charlist?t.charlist+"":"$",tme),(isNaN(s=parseInt(t.min,10))||typeof s!="number")&&(s=12),(isNaN(a=parseInt(t.max,10))||typeof a!="number")&&(a=24),h=new RegExp("^["+$5(c)+"]{"+s+","+a+"}$"),r=Zge([c],n.caseSensitive,!0),r.text=$ge(r.values,r.suppressed),f=t.confirmMessage!=null?t.confirmMessage:"Reinput a same one to confirm it: ",p=t.unmatchMessage!=null?t.unmatchMessage:"It differs from first one. Hit only the Enter key if you want to retry from first one.",e==null&&(e="Input new password: "),E=n.limitMessage;!S;)n.limit=h,n.limitMessage=E,C=$r.question(e,n),n.limit=[C,""],n.limitMessage=p,S=$r.question(f,n);return C};function ime(e,t,r){var s;function a(n){return s=r(n),!isNaN(s)&&typeof s=="number"}return $r.question(e,$s({limitMessage:"Input valid number, please."},t,{limit:a,cd:!1})),s}$r.questionInt=function(e,t){return ime(e,t,function(r){return parseInt(r,10)})};$r.questionFloat=function(e,t){return ime(e,t,parseFloat)};$r.questionPath=function(e,t){var r,s="",a=$s({hideEchoBack:!1,limitMessage:`$Input valid path, please.$<( Min:)min>$<( Max:)max>`,history:!0,cd:!0},t,{keepWhitespace:!1,limit:function(n){var c,f,p;n=e9(n,!0),s="";function h(E){E.split(/\/|\\/).reduce(function(C,S){var x=W0.resolve(C+=S+W0.sep);if(!ai.existsSync(x))ai.mkdirSync(x);else if(!ai.statSync(x).isDirectory())throw new Error("Non directory already exists: "+x);return C},"")}try{if(c=ai.existsSync(n),r=c?ai.realpathSync(n):W0.resolve(n),!t.hasOwnProperty("exists")&&!c||typeof t.exists=="boolean"&&t.exists!==c)return s=(c?"Already exists":"No such file or directory")+": "+r,!1;if(!c&&t.create&&(t.isDirectory?h(r):(h(W0.dirname(r)),ai.closeSync(ai.openSync(r,"w"))),r=ai.realpathSync(r)),c&&(t.min||t.max||t.isFile||t.isDirectory)){if(f=ai.statSync(r),t.isFile&&!f.isFile())return s="Not file: "+r,!1;if(t.isDirectory&&!f.isDirectory())return s="Not directory: "+r,!1;if(t.min&&f.size<+t.min||t.max&&f.size>+t.max)return s="Size "+f.size+" is out of range: "+r,!1}if(typeof t.validate=="function"&&(p=t.validate(r))!==!0)return typeof p=="string"&&(s=p),!1}catch(E){return s=E+"",!1}return!0},phContent:function(n){return n==="error"?s:n!=="min"&&n!=="max"?null:t.hasOwnProperty(n)?t[n]+"":""}});return t=t||{},e==null&&(e='Input path (you can "cd" and "pwd"): '),$r.question(e,a),r};function sme(e,t){var r={},s={};return typeof e=="object"?(Object.keys(e).forEach(function(a){typeof e[a]=="function"&&(s[t.caseSensitive?a:a.toLowerCase()]=e[a])}),r.preCheck=function(a){var n;return r.args=X5(a),n=r.args[0]||"",t.caseSensitive||(n=n.toLowerCase()),r.hRes=n!=="_"&&s.hasOwnProperty(n)?s[n].apply(a,r.args.slice(1)):s.hasOwnProperty("_")?s._.apply(a,r.args):null,{res:a,forceNext:!1}},s.hasOwnProperty("_")||(r.limit=function(){var a=r.args[0]||"";return t.caseSensitive||(a=a.toLowerCase()),s.hasOwnProperty(a)})):r.preCheck=function(a){return r.args=X5(a),r.hRes=typeof e=="function"?e.apply(a,r.args):!0,{res:a,forceNext:!1}},r}$r.promptCL=function(e,t){var r=$s({hideEchoBack:!1,limitMessage:"Requested command is not available.",caseSensitive:!1,history:!0},t),s=sme(e,r);return r.limit=s.limit,r.preCheck=s.preCheck,$r.prompt(r),s.args};$r.promptLoop=function(e,t){for(var r=$s({hideEchoBack:!1,trueValue:null,falseValue:null,caseSensitive:!1,history:!0},t);!e($r.prompt(r)););};$r.promptCLLoop=function(e,t){var r=$s({hideEchoBack:!1,limitMessage:"Requested command is not available.",caseSensitive:!1,history:!0},t),s=sme(e,r);for(r.limit=s.limit,r.preCheck=s.preCheck;$r.prompt(r),!s.hRes;);};$r.promptSimShell=function(e){return $r.prompt($s({hideEchoBack:!1,history:!0},e,{prompt:function(){return km?"$>":(process.env.USER||"")+(process.env.HOSTNAME?"@"+process.env.HOSTNAME.replace(/\..*$/,""):"")+":$$ "}()}))};function ome(e,t,r){var s;return e==null&&(e="Are you sure? "),(!t||t.guide!==!1)&&(e+="")&&(e=e.replace(/\s*:?\s*$/,"")+" [y/n]: "),s=$r.keyIn(e,$s(t,{hideEchoBack:!1,limit:r,trueValue:"y",falseValue:"n",caseSensitive:!1})),typeof s=="boolean"?s:""}$r.keyInYN=function(e,t){return ome(e,t)};$r.keyInYNStrict=function(e,t){return ome(e,t,"yn")};$r.keyInPause=function(e,t){e==null&&(e="Continue..."),(!t||t.guide!==!1)&&(e+="")&&(e=e.replace(/\s+$/,"")+" (Hit any key)"),$r.keyIn(e,$s({limit:null},t,{hideEchoBack:!0,mask:""}))};$r.keyInSelect=function(e,t,r){var s=$s({hideEchoBack:!1},r,{trueValue:null,falseValue:null,caseSensitive:!1,phContent:function(p){return p==="itemsCount"?e.length+"":p==="firstItem"?(e[0]+"").trim():p==="lastItem"?(e[e.length-1]+"").trim():null}}),a="",n={},c=49,f=` `;if(!Array.isArray(e)||!e.length||e.length>35)throw"`items` must be Array (max length: 35).";return e.forEach(function(p,h){var E=String.fromCharCode(c);a+=E,n[E]=h,f+="["+E+"] "+(p+"").trim()+` `,c=c===57?97:c+1}),(!r||r.cancel!==!1)&&(a+="0",n[0]=-1,f+="[0] "+(r&&r.cancel!=null&&typeof r.cancel!="boolean"?(r.cancel+"").trim():"CANCEL")+` `),s.limit=a,f+=` `,t==null&&(t="Choose one from list: "),(t+="")&&((!r||r.guide!==!1)&&(t=t.replace(/\s*:?\s*$/,"")+" [$]: "),f+=t),n[$r.keyIn(f,s).toLowerCase()]};$r.getRawInput=function(){return WT};function oS(e,t){var r;return t.length&&(r={},r[e]=t[0]),$r.setDefaultOptions(r)[e]}$r.setPrint=function(){return oS("print",arguments)};$r.setPrompt=function(){return oS("prompt",arguments)};$r.setEncoding=function(){return oS("encoding",arguments)};$r.setMask=function(){return oS("mask",arguments)};$r.setBufferSize=function(){return oS("bufferSize",arguments)}});var t9=G((Y9t,rc)=>{(function(){var e={major:0,minor:2,patch:66,status:"beta"};tau_file_system={files:{},open:function(w,b,y){var F=tau_file_system.files[w];if(!F){if(y==="read")return null;F={path:w,text:"",type:b,get:function(z,X){return X===this.text.length||X>this.text.length?"end_of_file":this.text.substring(X,X+z)},put:function(z,X){return X==="end_of_file"?(this.text+=z,!0):X==="past_end_of_file"?null:(this.text=this.text.substring(0,X)+z+this.text.substring(X+z.length),!0)},get_byte:function(z){if(z==="end_of_stream")return-1;var X=Math.floor(z/2);if(this.text.length<=X)return-1;var $=n(this.text[Math.floor(z/2)],0);return z%2===0?$&255:$/256>>>0},put_byte:function(z,X){var $=X==="end_of_stream"?this.text.length:Math.floor(X/2);if(this.text.length<$)return null;var se=this.text.length===$?-1:n(this.text[Math.floor(X/2)],0);return X%2===0?(se=se/256>>>0,se=(se&255)<<8|z&255):(se=se&255,se=(z&255)<<8|se&255),this.text.length===$?this.text+=c(se):this.text=this.text.substring(0,$)+c(se)+this.text.substring($+1),!0},flush:function(){return!0},close:function(){var z=tau_file_system.files[this.path];return z?!0:null}},tau_file_system.files[w]=F}return y==="write"&&(F.text=""),F}},tau_user_input={buffer:"",get:function(w,b){for(var y;tau_user_input.buffer.length\?\@\^\~\\]+|'(?:[^']*?(?:\\(?:x?\d+)?\\)*(?:'')*(?:\\')*)*')/,number:/^(?:0o[0-7]+|0x[0-9a-fA-F]+|0b[01]+|0'(?:''|\\[abfnrtv\\'"`]|\\x?\d+\\|[^\\])|\d+(?:\.\d+(?:[eE][+-]?\d+)?)?)/,string:/^(?:"([^"]|""|\\")*"|`([^`]|``|\\`)*`)/,l_brace:/^(?:\[)/,r_brace:/^(?:\])/,l_bracket:/^(?:\{)/,r_bracket:/^(?:\})/,bar:/^(?:\|)/,l_paren:/^(?:\()/,r_paren:/^(?:\))/};function O(w,b){return w.get_flag("char_conversion").id==="on"?b.replace(/./g,function(y){return w.get_char_conversion(y)}):b}function U(w){this.thread=w,this.text="",this.tokens=[]}U.prototype.set_last_tokens=function(w){return this.tokens=w},U.prototype.new_text=function(w){this.text=w,this.tokens=[]},U.prototype.get_tokens=function(w){var b,y=0,F=0,z=0,X=[],$=!1;if(w){var se=this.tokens[w-1];y=se.len,b=O(this.thread,this.text.substr(se.len)),F=se.line,z=se.start}else b=this.text;if(/^\s*$/.test(b))return null;for(;b!=="";){var xe=[],Fe=!1;if(/^\n/.exec(b)!==null){F++,z=0,y++,b=b.replace(/\n/,""),$=!0;continue}for(var ut in T)if(T.hasOwnProperty(ut)){var Ct=T[ut].exec(b);Ct&&xe.push({value:Ct[0],name:ut,matches:Ct})}if(!xe.length)return this.set_last_tokens([{value:b,matches:[],name:"lexical",line:F,start:z}]);var se=r(xe,function(Pr,Cr){return Pr.value.length>=Cr.value.length?Pr:Cr});switch(se.start=z,se.line=F,b=b.replace(se.value,""),z+=se.value.length,y+=se.value.length,se.name){case"atom":se.raw=se.value,se.value.charAt(0)==="'"&&(se.value=S(se.value.substr(1,se.value.length-2),"'"),se.value===null&&(se.name="lexical",se.value="unknown escape sequence"));break;case"number":se.float=se.value.substring(0,2)!=="0x"&&se.value.match(/[.eE]/)!==null&&se.value!=="0'.",se.value=I(se.value),se.blank=Fe;break;case"string":var qt=se.value.charAt(0);se.value=S(se.value.substr(1,se.value.length-2),qt),se.value===null&&(se.name="lexical",se.value="unknown escape sequence");break;case"whitespace":var ir=X[X.length-1];ir&&(ir.space=!0),Fe=!0;continue;case"r_bracket":X.length>0&&X[X.length-1].name==="l_bracket"&&(se=X.pop(),se.name="atom",se.value="{}",se.raw="{}",se.space=!1);break;case"r_brace":X.length>0&&X[X.length-1].name==="l_brace"&&(se=X.pop(),se.name="atom",se.value="[]",se.raw="[]",se.space=!1);break}se.len=y,X.push(se),Fe=!1}var Pt=this.set_last_tokens(X);return Pt.length===0?null:Pt};function V(w,b,y,F,z){if(!b[y])return{type:f,value:P.error.syntax(b[y-1],"expression expected",!0)};var X;if(F==="0"){var $=b[y];switch($.name){case"number":return{type:p,len:y+1,value:new P.type.Num($.value,$.float)};case"variable":return{type:p,len:y+1,value:new P.type.Var($.value)};case"string":var se;switch(w.get_flag("double_quotes").id){case"atom":se=new _($.value,[]);break;case"codes":se=new _("[]",[]);for(var xe=$.value.length-1;xe>=0;xe--)se=new _(".",[new P.type.Num(n($.value,xe),!1),se]);break;case"chars":se=new _("[]",[]);for(var xe=$.value.length-1;xe>=0;xe--)se=new _(".",[new P.type.Term($.value.charAt(xe),[]),se]);break}return{type:p,len:y+1,value:se};case"l_paren":var Pt=V(w,b,y+1,w.__get_max_priority(),!0);return Pt.type!==p?Pt:b[Pt.len]&&b[Pt.len].name==="r_paren"?(Pt.len++,Pt):{type:f,derived:!0,value:P.error.syntax(b[Pt.len]?b[Pt.len]:b[Pt.len-1],") or operator expected",!b[Pt.len])};case"l_bracket":var Pt=V(w,b,y+1,w.__get_max_priority(),!0);return Pt.type!==p?Pt:b[Pt.len]&&b[Pt.len].name==="r_bracket"?(Pt.len++,Pt.value=new _("{}",[Pt.value]),Pt):{type:f,derived:!0,value:P.error.syntax(b[Pt.len]?b[Pt.len]:b[Pt.len-1],"} or operator expected",!b[Pt.len])}}var Fe=te(w,b,y,z);return Fe.type===p||Fe.derived||(Fe=ie(w,b,y),Fe.type===p||Fe.derived)?Fe:{type:f,derived:!1,value:P.error.syntax(b[y],"unexpected token")}}var ut=w.__get_max_priority(),Ct=w.__get_next_priority(F),qt=y;if(b[y].name==="atom"&&b[y+1]&&(b[y].space||b[y+1].name!=="l_paren")){var $=b[y++],ir=w.__lookup_operator_classes(F,$.value);if(ir&&ir.indexOf("fy")>-1){var Pt=V(w,b,y,F,z);if(Pt.type!==f)return $.value==="-"&&!$.space&&P.type.is_number(Pt.value)?{value:new P.type.Num(-Pt.value.value,Pt.value.is_float),len:Pt.len,type:p}:{value:new P.type.Term($.value,[Pt.value]),len:Pt.len,type:p};X=Pt}else if(ir&&ir.indexOf("fx")>-1){var Pt=V(w,b,y,Ct,z);if(Pt.type!==f)return{value:new P.type.Term($.value,[Pt.value]),len:Pt.len,type:p};X=Pt}}y=qt;var Pt=V(w,b,y,Ct,z);if(Pt.type===p){y=Pt.len;var $=b[y];if(b[y]&&(b[y].name==="atom"&&w.__lookup_operator_classes(F,$.value)||b[y].name==="bar"&&w.__lookup_operator_classes(F,"|"))){var gn=Ct,Pr=F,ir=w.__lookup_operator_classes(F,$.value);if(ir.indexOf("xf")>-1)return{value:new P.type.Term($.value,[Pt.value]),len:++Pt.len,type:p};if(ir.indexOf("xfx")>-1){var Cr=V(w,b,y+1,gn,z);return Cr.type===p?{value:new P.type.Term($.value,[Pt.value,Cr.value]),len:Cr.len,type:p}:(Cr.derived=!0,Cr)}else if(ir.indexOf("xfy")>-1){var Cr=V(w,b,y+1,Pr,z);return Cr.type===p?{value:new P.type.Term($.value,[Pt.value,Cr.value]),len:Cr.len,type:p}:(Cr.derived=!0,Cr)}else if(Pt.type!==f)for(;;){y=Pt.len;var $=b[y];if($&&$.name==="atom"&&w.__lookup_operator_classes(F,$.value)){var ir=w.__lookup_operator_classes(F,$.value);if(ir.indexOf("yf")>-1)Pt={value:new P.type.Term($.value,[Pt.value]),len:++y,type:p};else if(ir.indexOf("yfx")>-1){var Cr=V(w,b,++y,gn,z);if(Cr.type===f)return Cr.derived=!0,Cr;y=Cr.len,Pt={value:new P.type.Term($.value,[Pt.value,Cr.value]),len:y,type:p}}else break}else break}}else X={type:f,value:P.error.syntax(b[Pt.len-1],"operator expected")};return Pt}return Pt}function te(w,b,y,F){if(!b[y]||b[y].name==="atom"&&b[y].raw==="."&&!F&&(b[y].space||!b[y+1]||b[y+1].name!=="l_paren"))return{type:f,derived:!1,value:P.error.syntax(b[y-1],"unfounded token")};var z=b[y],X=[];if(b[y].name==="atom"&&b[y].raw!==","){if(y++,b[y-1].space)return{type:p,len:y,value:new P.type.Term(z.value,X)};if(b[y]&&b[y].name==="l_paren"){if(b[y+1]&&b[y+1].name==="r_paren")return{type:f,derived:!0,value:P.error.syntax(b[y+1],"argument expected")};var $=V(w,b,++y,"999",!0);if($.type===f)return $.derived?$:{type:f,derived:!0,value:P.error.syntax(b[y]?b[y]:b[y-1],"argument expected",!b[y])};for(X.push($.value),y=$.len;b[y]&&b[y].name==="atom"&&b[y].value===",";){if($=V(w,b,y+1,"999",!0),$.type===f)return $.derived?$:{type:f,derived:!0,value:P.error.syntax(b[y+1]?b[y+1]:b[y],"argument expected",!b[y+1])};X.push($.value),y=$.len}if(b[y]&&b[y].name==="r_paren")y++;else return{type:f,derived:!0,value:P.error.syntax(b[y]?b[y]:b[y-1],", or ) expected",!b[y])}}return{type:p,len:y,value:new P.type.Term(z.value,X)}}return{type:f,derived:!1,value:P.error.syntax(b[y],"term expected")}}function ie(w,b,y){if(!b[y])return{type:f,derived:!1,value:P.error.syntax(b[y-1],"[ expected")};if(b[y]&&b[y].name==="l_brace"){var F=V(w,b,++y,"999",!0),z=[F.value],X=void 0;if(F.type===f)return b[y]&&b[y].name==="r_brace"?{type:p,len:y+1,value:new P.type.Term("[]",[])}:{type:f,derived:!0,value:P.error.syntax(b[y],"] expected")};for(y=F.len;b[y]&&b[y].name==="atom"&&b[y].value===",";){if(F=V(w,b,y+1,"999",!0),F.type===f)return F.derived?F:{type:f,derived:!0,value:P.error.syntax(b[y+1]?b[y+1]:b[y],"argument expected",!b[y+1])};z.push(F.value),y=F.len}var $=!1;if(b[y]&&b[y].name==="bar"){if($=!0,F=V(w,b,y+1,"999",!0),F.type===f)return F.derived?F:{type:f,derived:!0,value:P.error.syntax(b[y+1]?b[y+1]:b[y],"argument expected",!b[y+1])};X=F.value,y=F.len}return b[y]&&b[y].name==="r_brace"?{type:p,len:y+1,value:d(z,X)}:{type:f,derived:!0,value:P.error.syntax(b[y]?b[y]:b[y-1],$?"] expected":", or | or ] expected",!b[y])}}return{type:f,derived:!1,value:P.error.syntax(b[y],"list expected")}}function ue(w,b,y){var F=b[y].line,z=V(w,b,y,w.__get_max_priority(),!1),X=null,$;if(z.type!==f)if(y=z.len,b[y]&&b[y].name==="atom"&&b[y].raw===".")if(y++,P.type.is_term(z.value)){if(z.value.indicator===":-/2"?(X=new P.type.Rule(z.value.args[0],Ee(z.value.args[1])),$={value:X,len:y,type:p}):z.value.indicator==="-->/2"?(X=Ae(new P.type.Rule(z.value.args[0],z.value.args[1]),w),X.body=Ee(X.body),$={value:X,len:y,type:P.type.is_rule(X)?p:f}):(X=new P.type.Rule(z.value,null),$={value:X,len:y,type:p}),X){var se=X.singleton_variables();se.length>0&&w.throw_warning(P.warning.singleton(se,X.head.indicator,F))}return $}else return{type:f,value:P.error.syntax(b[y],"callable expected")};else return{type:f,value:P.error.syntax(b[y]?b[y]:b[y-1],". or operator expected")};return z}function ae(w,b,y){y=y||{},y.from=y.from?y.from:"$tau-js",y.reconsult=y.reconsult!==void 0?y.reconsult:!0;var F=new U(w),z={},X;F.new_text(b);var $=0,se=F.get_tokens($);do{if(se===null||!se[$])break;var xe=ue(w,se,$);if(xe.type===f)return new _("throw",[xe.value]);if(xe.value.body===null&&xe.value.head.indicator==="?-/1"){var Fe=new ct(w.session);Fe.add_goal(xe.value.head.args[0]),Fe.answer(function(Ct){P.type.is_error(Ct)?w.throw_warning(Ct.args[0]):(Ct===!1||Ct===null)&&w.throw_warning(P.warning.failed_goal(xe.value.head.args[0],xe.len))}),$=xe.len;var ut=!0}else if(xe.value.body===null&&xe.value.head.indicator===":-/1"){var ut=w.run_directive(xe.value.head.args[0]);$=xe.len,xe.value.head.args[0].indicator==="char_conversion/2"&&(se=F.get_tokens($),$=0)}else{X=xe.value.head.indicator,y.reconsult!==!1&&z[X]!==!0&&!w.is_multifile_predicate(X)&&(w.session.rules[X]=a(w.session.rules[X]||[],function(qt){return qt.dynamic}),z[X]=!0);var ut=w.add_rule(xe.value,y);$=xe.len}if(!ut)return ut}while(!0);return!0}function ge(w,b){var y=new U(w);y.new_text(b);var F=0;do{var z=y.get_tokens(F);if(z===null)break;var X=V(w,z,0,w.__get_max_priority(),!1);if(X.type!==f){var $=X.len,se=$;if(z[$]&&z[$].name==="atom"&&z[$].raw===".")w.add_goal(Ee(X.value));else{var xe=z[$];return new _("throw",[P.error.syntax(xe||z[$-1],". or operator expected",!xe)])}F=X.len+1}else return new _("throw",[X.value])}while(!0);return!0}function Ae(w,b){w=w.rename(b);var y=b.next_free_variable(),F=Ce(w.body,y,b);return F.error?F.value:(w.body=F.value,w.head.args=w.head.args.concat([y,F.variable]),w.head=new _(w.head.id,w.head.args),w)}function Ce(w,b,y){var F;if(P.type.is_term(w)&&w.indicator==="!/0")return{value:w,variable:b,error:!1};if(P.type.is_term(w)&&w.indicator===",/2"){var z=Ce(w.args[0],b,y);if(z.error)return z;var X=Ce(w.args[1],z.variable,y);return X.error?X:{value:new _(",",[z.value,X.value]),variable:X.variable,error:!1}}else{if(P.type.is_term(w)&&w.indicator==="{}/1")return{value:w.args[0],variable:b,error:!1};if(P.type.is_empty_list(w))return{value:new _("true",[]),variable:b,error:!1};if(P.type.is_list(w)){F=y.next_free_variable();for(var $=w,se;$.indicator==="./2";)se=$,$=$.args[1];return P.type.is_variable($)?{value:P.error.instantiation("DCG"),variable:b,error:!0}:P.type.is_empty_list($)?(se.args[1]=F,{value:new _("=",[b,w]),variable:F,error:!1}):{value:P.error.type("list",w,"DCG"),variable:b,error:!0}}else return P.type.is_callable(w)?(F=y.next_free_variable(),w.args=w.args.concat([b,F]),w=new _(w.id,w.args),{value:w,variable:F,error:!1}):{value:P.error.type("callable",w,"DCG"),variable:b,error:!0}}}function Ee(w){return P.type.is_variable(w)?new _("call",[w]):P.type.is_term(w)&&[",/2",";/2","->/2"].indexOf(w.indicator)!==-1?new _(w.id,[Ee(w.args[0]),Ee(w.args[1])]):w}function d(w,b){for(var y=b||new P.type.Term("[]",[]),F=w.length-1;F>=0;F--)y=new P.type.Term(".",[w[F],y]);return y}function Se(w,b){for(var y=w.length-1;y>=0;y--)w[y]===b&&w.splice(y,1)}function Be(w){for(var b={},y=[],F=0;F=0;b--)if(w.charAt(b)==="/")return new _("/",[new _(w.substring(0,b)),new Qe(parseInt(w.substring(b+1)),!1)])}function De(w){this.id=w}function Qe(w,b){this.is_float=b!==void 0?b:parseInt(w)!==w,this.value=this.is_float?w:parseInt(w)}var st=0;function _(w,b,y){this.ref=y||++st,this.id=w,this.args=b||[],this.indicator=w+"/"+this.args.length}var tt=0;function Ne(w,b,y,F,z,X){this.id=tt++,this.stream=w,this.mode=b,this.alias=y,this.type=F!==void 0?F:"text",this.reposition=z!==void 0?z:!0,this.eof_action=X!==void 0?X:"eof_code",this.position=this.mode==="append"?"end_of_stream":0,this.output=this.mode==="write"||this.mode==="append",this.input=this.mode==="read"}function ke(w){w=w||{},this.links=w}function be(w,b,y){b=b||new ke,y=y||null,this.goal=w,this.substitution=b,this.parent=y}function je(w,b,y){this.head=w,this.body=b,this.dynamic=y||!1}function Re(w){w=w===void 0||w<=0?1e3:w,this.rules={},this.src_predicates={},this.rename=0,this.modules=[],this.thread=new ct(this),this.total_threads=1,this.renamed_variables={},this.public_predicates={},this.multifile_predicates={},this.limit=w,this.streams={user_input:new Ne(typeof rc<"u"&&rc.exports?nodejs_user_input:tau_user_input,"read","user_input","text",!1,"reset"),user_output:new Ne(typeof rc<"u"&&rc.exports?nodejs_user_output:tau_user_output,"write","user_output","text",!1,"eof_code")},this.file_system=typeof rc<"u"&&rc.exports?nodejs_file_system:tau_file_system,this.standard_input=this.streams.user_input,this.standard_output=this.streams.user_output,this.current_input=this.streams.user_input,this.current_output=this.streams.user_output,this.format_success=function(b){return b.substitution},this.format_error=function(b){return b.goal},this.flag={bounded:P.flag.bounded.value,max_integer:P.flag.max_integer.value,min_integer:P.flag.min_integer.value,integer_rounding_function:P.flag.integer_rounding_function.value,char_conversion:P.flag.char_conversion.value,debug:P.flag.debug.value,max_arity:P.flag.max_arity.value,unknown:P.flag.unknown.value,double_quotes:P.flag.double_quotes.value,occurs_check:P.flag.occurs_check.value,dialect:P.flag.dialect.value,version_data:P.flag.version_data.value,nodejs:P.flag.nodejs.value},this.__loaded_modules=[],this.__char_conversion={},this.__operators={1200:{":-":["fx","xfx"],"-->":["xfx"],"?-":["fx"]},1100:{";":["xfy"]},1050:{"->":["xfy"]},1e3:{",":["xfy"]},900:{"\\+":["fy"]},700:{"=":["xfx"],"\\=":["xfx"],"==":["xfx"],"\\==":["xfx"],"@<":["xfx"],"@=<":["xfx"],"@>":["xfx"],"@>=":["xfx"],"=..":["xfx"],is:["xfx"],"=:=":["xfx"],"=\\=":["xfx"],"<":["xfx"],"=<":["xfx"],">":["xfx"],">=":["xfx"]},600:{":":["xfy"]},500:{"+":["yfx"],"-":["yfx"],"/\\":["yfx"],"\\/":["yfx"]},400:{"*":["yfx"],"/":["yfx"],"//":["yfx"],rem:["yfx"],mod:["yfx"],"<<":["yfx"],">>":["yfx"]},200:{"**":["xfx"],"^":["xfy"],"-":["fy"],"+":["fy"],"\\":["fy"]}}}function ct(w){this.epoch=Date.now(),this.session=w,this.session.total_threads++,this.total_steps=0,this.cpu_time=0,this.cpu_time_last=0,this.points=[],this.debugger=!1,this.debugger_states=[],this.level="top_level/0",this.__calls=[],this.current_limit=this.session.limit,this.warnings=[]}function Me(w,b,y){this.id=w,this.rules=b,this.exports=y,P.module[w]=this}Me.prototype.exports_predicate=function(w){return this.exports.indexOf(w)!==-1},De.prototype.unify=function(w,b){if(b&&t(w.variables(),this.id)!==-1&&!P.type.is_variable(w))return null;var y={};return y[this.id]=w,new ke(y)},Qe.prototype.unify=function(w,b){return P.type.is_number(w)&&this.value===w.value&&this.is_float===w.is_float?new ke:null},_.prototype.unify=function(w,b){if(P.type.is_term(w)&&this.indicator===w.indicator){for(var y=new ke,F=0;F=0){var F=this.args[0].value,z=Math.floor(F/26),X=F%26;return"ABCDEFGHIJKLMNOPQRSTUVWXYZ"[X]+(z!==0?z:"")}switch(this.indicator){case"[]/0":case"{}/0":case"!/0":return this.id;case"{}/1":return"{"+this.args[0].toString(w)+"}";case"./2":for(var $="["+this.args[0].toString(w),se=this.args[1];se.indicator==="./2";)$+=", "+se.args[0].toString(w),se=se.args[1];return se.indicator!=="[]/0"&&($+="|"+se.toString(w)),$+="]",$;case",/2":return"("+this.args[0].toString(w)+", "+this.args[1].toString(w)+")";default:var xe=this.id,Fe=w.session?w.session.lookup_operator(this.id,this.args.length):null;if(w.session===void 0||w.ignore_ops||Fe===null)return w.quoted&&!/^(!|,|;|[a-z][0-9a-zA-Z_]*)$/.test(xe)&&xe!=="{}"&&xe!=="[]"&&(xe="'"+x(xe)+"'"),xe+(this.args.length?"("+s(this.args,function(ir){return ir.toString(w)}).join(", ")+")":"");var ut=Fe.priority>b.priority||Fe.priority===b.priority&&(Fe.class==="xfy"&&this.indicator!==b.indicator||Fe.class==="yfx"&&this.indicator!==b.indicator||this.indicator===b.indicator&&Fe.class==="yfx"&&y==="right"||this.indicator===b.indicator&&Fe.class==="xfy"&&y==="left");Fe.indicator=this.indicator;var Ct=ut?"(":"",qt=ut?")":"";return this.args.length===0?"("+this.id+")":["fy","fx"].indexOf(Fe.class)!==-1?Ct+xe+" "+this.args[0].toString(w,Fe)+qt:["yf","xf"].indexOf(Fe.class)!==-1?Ct+this.args[0].toString(w,Fe)+" "+xe+qt:Ct+this.args[0].toString(w,Fe,"left")+" "+this.id+" "+this.args[1].toString(w,Fe,"right")+qt}},Ne.prototype.toString=function(w){return"("+this.id+")"},ke.prototype.toString=function(w){var b="{";for(var y in this.links)this.links.hasOwnProperty(y)&&(b!=="{"&&(b+=", "),b+=y+"/"+this.links[y].toString(w));return b+="}",b},be.prototype.toString=function(w){return this.goal===null?"<"+this.substitution.toString(w)+">":"<"+this.goal.toString(w)+", "+this.substitution.toString(w)+">"},je.prototype.toString=function(w){return this.body?this.head.toString(w)+" :- "+this.body.toString(w)+".":this.head.toString(w)+"."},Re.prototype.toString=function(w){for(var b="",y=0;y=0;z--)F=new _(".",[b[z],F]);return F}return new _(this.id,s(this.args,function(X){return X.apply(w)}),this.ref)},Ne.prototype.apply=function(w){return this},je.prototype.apply=function(w){return new je(this.head.apply(w),this.body!==null?this.body.apply(w):null)},ke.prototype.apply=function(w){var b,y={};for(b in this.links)this.links.hasOwnProperty(b)&&(y[b]=this.links[b].apply(w));return new ke(y)},_.prototype.select=function(){for(var w=this;w.indicator===",/2";)w=w.args[0];return w},_.prototype.replace=function(w){return this.indicator===",/2"?this.args[0].indicator===",/2"?new _(",",[this.args[0].replace(w),this.args[1]]):w===null?this.args[1]:new _(",",[w,this.args[1]]):w},_.prototype.search=function(w){if(P.type.is_term(w)&&w.ref!==void 0&&this.ref===w.ref)return!0;for(var b=0;bb&&F0&&(b=this.head_point().substitution.domain());t(b,P.format_variable(this.session.rename))!==-1;)this.session.rename++;if(w.id==="_")return new De(P.format_variable(this.session.rename));this.session.renamed_variables[w.id]=P.format_variable(this.session.rename)}return new De(this.session.renamed_variables[w.id])},Re.prototype.next_free_variable=function(){return this.thread.next_free_variable()},ct.prototype.next_free_variable=function(){this.session.rename++;var w=[];for(this.points.length>0&&(w=this.head_point().substitution.domain());t(w,P.format_variable(this.session.rename))!==-1;)this.session.rename++;return new De(P.format_variable(this.session.rename))},Re.prototype.is_public_predicate=function(w){return!this.public_predicates.hasOwnProperty(w)||this.public_predicates[w]===!0},ct.prototype.is_public_predicate=function(w){return this.session.is_public_predicate(w)},Re.prototype.is_multifile_predicate=function(w){return this.multifile_predicates.hasOwnProperty(w)&&this.multifile_predicates[w]===!0},ct.prototype.is_multifile_predicate=function(w){return this.session.is_multifile_predicate(w)},Re.prototype.prepend=function(w){return this.thread.prepend(w)},ct.prototype.prepend=function(w){for(var b=w.length-1;b>=0;b--)this.points.push(w[b])},Re.prototype.success=function(w,b){return this.thread.success(w,b)},ct.prototype.success=function(w,y){var y=typeof y>"u"?w:y;this.prepend([new be(w.goal.replace(null),w.substitution,y)])},Re.prototype.throw_error=function(w){return this.thread.throw_error(w)},ct.prototype.throw_error=function(w){this.prepend([new be(new _("throw",[w]),new ke,null,null)])},Re.prototype.step_rule=function(w,b){return this.thread.step_rule(w,b)},ct.prototype.step_rule=function(w,b){var y=b.indicator;if(w==="user"&&(w=null),w===null&&this.session.rules.hasOwnProperty(y))return this.session.rules[y];for(var F=w===null?this.session.modules:t(this.session.modules,w)===-1?[]:[w],z=0;z1)&&this.again()},Re.prototype.answers=function(w,b,y){return this.thread.answers(w,b,y)},ct.prototype.answers=function(w,b,y){var F=b||1e3,z=this;if(b<=0){y&&y();return}this.answer(function(X){w(X),X!==!1?setTimeout(function(){z.answers(w,b-1,y)},1):y&&y()})},Re.prototype.again=function(w){return this.thread.again(w)},ct.prototype.again=function(w){for(var b,y=Date.now();this.__calls.length>0;){for(this.warnings=[],w!==!1&&(this.current_limit=this.session.limit);this.current_limit>0&&this.points.length>0&&this.head_point().goal!==null&&!P.type.is_error(this.head_point().goal);)if(this.current_limit--,this.step()===!0)return;var F=Date.now();this.cpu_time_last=F-y,this.cpu_time+=this.cpu_time_last;var z=this.__calls.shift();this.current_limit<=0?z(null):this.points.length===0?z(!1):P.type.is_error(this.head_point().goal)?(b=this.session.format_error(this.points.pop()),this.points=[],z(b)):(this.debugger&&this.debugger_states.push(this.head_point()),b=this.session.format_success(this.points.pop()),z(b))}},Re.prototype.unfold=function(w){if(w.body===null)return!1;var b=w.head,y=w.body,F=y.select(),z=new ct(this),X=[];z.add_goal(F),z.step();for(var $=z.points.length-1;$>=0;$--){var se=z.points[$],xe=b.apply(se.substitution),Fe=y.replace(se.goal);Fe!==null&&(Fe=Fe.apply(se.substitution)),X.push(new je(xe,Fe))}var ut=this.rules[b.indicator],Ct=t(ut,w);return X.length>0&&Ct!==-1?(ut.splice.apply(ut,[Ct,1].concat(X)),!0):!1},ct.prototype.unfold=function(w){return this.session.unfold(w)},De.prototype.interpret=function(w){return P.error.instantiation(w.level)},Qe.prototype.interpret=function(w){return this},_.prototype.interpret=function(w){return P.type.is_unitary_list(this)?this.args[0].interpret(w):P.operate(w,this)},De.prototype.compare=function(w){return this.idw.id?1:0},Qe.prototype.compare=function(w){if(this.value===w.value&&this.is_float===w.is_float)return 0;if(this.valuew.value)return 1},_.prototype.compare=function(w){if(this.args.lengthw.args.length||this.args.length===w.args.length&&this.id>w.id)return 1;for(var b=0;bF)return 1;if(w.constructor===Qe){if(w.is_float&&b.is_float)return 0;if(w.is_float)return-1;if(b.is_float)return 1}return 0},is_substitution:function(w){return w instanceof ke},is_state:function(w){return w instanceof be},is_rule:function(w){return w instanceof je},is_variable:function(w){return w instanceof De},is_stream:function(w){return w instanceof Ne},is_anonymous_var:function(w){return w instanceof De&&w.id==="_"},is_callable:function(w){return w instanceof _},is_number:function(w){return w instanceof Qe},is_integer:function(w){return w instanceof Qe&&!w.is_float},is_float:function(w){return w instanceof Qe&&w.is_float},is_term:function(w){return w instanceof _},is_atom:function(w){return w instanceof _&&w.args.length===0},is_ground:function(w){if(w instanceof De)return!1;if(w instanceof _){for(var b=0;b0},is_list:function(w){return w instanceof _&&(w.indicator==="[]/0"||w.indicator==="./2")},is_empty_list:function(w){return w instanceof _&&w.indicator==="[]/0"},is_non_empty_list:function(w){return w instanceof _&&w.indicator==="./2"},is_fully_list:function(w){for(;w instanceof _&&w.indicator==="./2";)w=w.args[1];return w instanceof De||w instanceof _&&w.indicator==="[]/0"},is_instantiated_list:function(w){for(;w instanceof _&&w.indicator==="./2";)w=w.args[1];return w instanceof _&&w.indicator==="[]/0"},is_unitary_list:function(w){return w instanceof _&&w.indicator==="./2"&&w.args[1]instanceof _&&w.args[1].indicator==="[]/0"},is_character:function(w){return w instanceof _&&(w.id.length===1||w.id.length>0&&w.id.length<=2&&n(w.id,0)>=65536)},is_character_code:function(w){return w instanceof Qe&&!w.is_float&&w.value>=0&&w.value<=1114111},is_byte:function(w){return w instanceof Qe&&!w.is_float&&w.value>=0&&w.value<=255},is_operator:function(w){return w instanceof _&&P.arithmetic.evaluation[w.indicator]},is_directive:function(w){return w instanceof _&&P.directive[w.indicator]!==void 0},is_builtin:function(w){return w instanceof _&&P.predicate[w.indicator]!==void 0},is_error:function(w){return w instanceof _&&w.indicator==="throw/1"},is_predicate_indicator:function(w){return w instanceof _&&w.indicator==="//2"&&w.args[0]instanceof _&&w.args[0].args.length===0&&w.args[1]instanceof Qe&&w.args[1].is_float===!1},is_flag:function(w){return w instanceof _&&w.args.length===0&&P.flag[w.id]!==void 0},is_value_flag:function(w,b){if(!P.type.is_flag(w))return!1;for(var y in P.flag[w.id].allowed)if(P.flag[w.id].allowed.hasOwnProperty(y)&&P.flag[w.id].allowed[y].equals(b))return!0;return!1},is_io_mode:function(w){return P.type.is_atom(w)&&["read","write","append"].indexOf(w.id)!==-1},is_stream_option:function(w){return P.type.is_term(w)&&(w.indicator==="alias/1"&&P.type.is_atom(w.args[0])||w.indicator==="reposition/1"&&P.type.is_atom(w.args[0])&&(w.args[0].id==="true"||w.args[0].id==="false")||w.indicator==="type/1"&&P.type.is_atom(w.args[0])&&(w.args[0].id==="text"||w.args[0].id==="binary")||w.indicator==="eof_action/1"&&P.type.is_atom(w.args[0])&&(w.args[0].id==="error"||w.args[0].id==="eof_code"||w.args[0].id==="reset"))},is_stream_position:function(w){return P.type.is_integer(w)&&w.value>=0||P.type.is_atom(w)&&(w.id==="end_of_stream"||w.id==="past_end_of_stream")},is_stream_property:function(w){return P.type.is_term(w)&&(w.indicator==="input/0"||w.indicator==="output/0"||w.indicator==="alias/1"&&(P.type.is_variable(w.args[0])||P.type.is_atom(w.args[0]))||w.indicator==="file_name/1"&&(P.type.is_variable(w.args[0])||P.type.is_atom(w.args[0]))||w.indicator==="position/1"&&(P.type.is_variable(w.args[0])||P.type.is_stream_position(w.args[0]))||w.indicator==="reposition/1"&&(P.type.is_variable(w.args[0])||P.type.is_atom(w.args[0])&&(w.args[0].id==="true"||w.args[0].id==="false"))||w.indicator==="type/1"&&(P.type.is_variable(w.args[0])||P.type.is_atom(w.args[0])&&(w.args[0].id==="text"||w.args[0].id==="binary"))||w.indicator==="mode/1"&&(P.type.is_variable(w.args[0])||P.type.is_atom(w.args[0])&&(w.args[0].id==="read"||w.args[0].id==="write"||w.args[0].id==="append"))||w.indicator==="eof_action/1"&&(P.type.is_variable(w.args[0])||P.type.is_atom(w.args[0])&&(w.args[0].id==="error"||w.args[0].id==="eof_code"||w.args[0].id==="reset"))||w.indicator==="end_of_stream/1"&&(P.type.is_variable(w.args[0])||P.type.is_atom(w.args[0])&&(w.args[0].id==="at"||w.args[0].id==="past"||w.args[0].id==="not")))},is_streamable:function(w){return w.__proto__.stream!==void 0},is_read_option:function(w){return P.type.is_term(w)&&["variables/1","variable_names/1","singletons/1"].indexOf(w.indicator)!==-1},is_write_option:function(w){return P.type.is_term(w)&&(w.indicator==="quoted/1"&&P.type.is_atom(w.args[0])&&(w.args[0].id==="true"||w.args[0].id==="false")||w.indicator==="ignore_ops/1"&&P.type.is_atom(w.args[0])&&(w.args[0].id==="true"||w.args[0].id==="false")||w.indicator==="numbervars/1"&&P.type.is_atom(w.args[0])&&(w.args[0].id==="true"||w.args[0].id==="false"))},is_close_option:function(w){return P.type.is_term(w)&&w.indicator==="force/1"&&P.type.is_atom(w.args[0])&&(w.args[0].id==="true"||w.args[0].id==="false")},is_modifiable_flag:function(w){return P.type.is_flag(w)&&P.flag[w.id].changeable},is_module:function(w){return w instanceof _&&w.indicator==="library/1"&&w.args[0]instanceof _&&w.args[0].args.length===0&&P.module[w.args[0].id]!==void 0}},arithmetic:{evaluation:{"e/0":{type_args:null,type_result:!0,fn:function(w){return Math.E}},"pi/0":{type_args:null,type_result:!0,fn:function(w){return Math.PI}},"tau/0":{type_args:null,type_result:!0,fn:function(w){return 2*Math.PI}},"epsilon/0":{type_args:null,type_result:!0,fn:function(w){return Number.EPSILON}},"+/1":{type_args:null,type_result:null,fn:function(w,b){return w}},"-/1":{type_args:null,type_result:null,fn:function(w,b){return-w}},"\\/1":{type_args:!1,type_result:!1,fn:function(w,b){return~w}},"abs/1":{type_args:null,type_result:null,fn:function(w,b){return Math.abs(w)}},"sign/1":{type_args:null,type_result:null,fn:function(w,b){return Math.sign(w)}},"float_integer_part/1":{type_args:!0,type_result:!1,fn:function(w,b){return parseInt(w)}},"float_fractional_part/1":{type_args:!0,type_result:!0,fn:function(w,b){return w-parseInt(w)}},"float/1":{type_args:null,type_result:!0,fn:function(w,b){return parseFloat(w)}},"floor/1":{type_args:!0,type_result:!1,fn:function(w,b){return Math.floor(w)}},"truncate/1":{type_args:!0,type_result:!1,fn:function(w,b){return parseInt(w)}},"round/1":{type_args:!0,type_result:!1,fn:function(w,b){return Math.round(w)}},"ceiling/1":{type_args:!0,type_result:!1,fn:function(w,b){return Math.ceil(w)}},"sin/1":{type_args:null,type_result:!0,fn:function(w,b){return Math.sin(w)}},"cos/1":{type_args:null,type_result:!0,fn:function(w,b){return Math.cos(w)}},"tan/1":{type_args:null,type_result:!0,fn:function(w,b){return Math.tan(w)}},"asin/1":{type_args:null,type_result:!0,fn:function(w,b){return Math.asin(w)}},"acos/1":{type_args:null,type_result:!0,fn:function(w,b){return Math.acos(w)}},"atan/1":{type_args:null,type_result:!0,fn:function(w,b){return Math.atan(w)}},"atan2/2":{type_args:null,type_result:!0,fn:function(w,b,y){return Math.atan2(w,b)}},"exp/1":{type_args:null,type_result:!0,fn:function(w,b){return Math.exp(w)}},"sqrt/1":{type_args:null,type_result:!0,fn:function(w,b){return Math.sqrt(w)}},"log/1":{type_args:null,type_result:!0,fn:function(w,b){return w>0?Math.log(w):P.error.evaluation("undefined",b.__call_indicator)}},"+/2":{type_args:null,type_result:null,fn:function(w,b,y){return w+b}},"-/2":{type_args:null,type_result:null,fn:function(w,b,y){return w-b}},"*/2":{type_args:null,type_result:null,fn:function(w,b,y){return w*b}},"//2":{type_args:null,type_result:!0,fn:function(w,b,y){return b?w/b:P.error.evaluation("zero_division",y.__call_indicator)}},"///2":{type_args:!1,type_result:!1,fn:function(w,b,y){return b?parseInt(w/b):P.error.evaluation("zero_division",y.__call_indicator)}},"**/2":{type_args:null,type_result:!0,fn:function(w,b,y){return Math.pow(w,b)}},"^/2":{type_args:null,type_result:null,fn:function(w,b,y){return Math.pow(w,b)}},"<>/2":{type_args:!1,type_result:!1,fn:function(w,b,y){return w>>b}},"/\\/2":{type_args:!1,type_result:!1,fn:function(w,b,y){return w&b}},"\\//2":{type_args:!1,type_result:!1,fn:function(w,b,y){return w|b}},"xor/2":{type_args:!1,type_result:!1,fn:function(w,b,y){return w^b}},"rem/2":{type_args:!1,type_result:!1,fn:function(w,b,y){return b?w%b:P.error.evaluation("zero_division",y.__call_indicator)}},"mod/2":{type_args:!1,type_result:!1,fn:function(w,b,y){return b?w-parseInt(w/b)*b:P.error.evaluation("zero_division",y.__call_indicator)}},"max/2":{type_args:null,type_result:null,fn:function(w,b,y){return Math.max(w,b)}},"min/2":{type_args:null,type_result:null,fn:function(w,b,y){return Math.min(w,b)}}}},directive:{"dynamic/1":function(w,b){var y=b.args[0];if(P.type.is_variable(y))w.throw_error(P.error.instantiation(b.indicator));else if(!P.type.is_compound(y)||y.indicator!=="//2")w.throw_error(P.error.type("predicate_indicator",y,b.indicator));else if(P.type.is_variable(y.args[0])||P.type.is_variable(y.args[1]))w.throw_error(P.error.instantiation(b.indicator));else if(!P.type.is_atom(y.args[0]))w.throw_error(P.error.type("atom",y.args[0],b.indicator));else if(!P.type.is_integer(y.args[1]))w.throw_error(P.error.type("integer",y.args[1],b.indicator));else{var F=b.args[0].args[0].id+"/"+b.args[0].args[1].value;w.session.public_predicates[F]=!0,w.session.rules[F]||(w.session.rules[F]=[])}},"multifile/1":function(w,b){var y=b.args[0];P.type.is_variable(y)?w.throw_error(P.error.instantiation(b.indicator)):!P.type.is_compound(y)||y.indicator!=="//2"?w.throw_error(P.error.type("predicate_indicator",y,b.indicator)):P.type.is_variable(y.args[0])||P.type.is_variable(y.args[1])?w.throw_error(P.error.instantiation(b.indicator)):P.type.is_atom(y.args[0])?P.type.is_integer(y.args[1])?w.session.multifile_predicates[b.args[0].args[0].id+"/"+b.args[0].args[1].value]=!0:w.throw_error(P.error.type("integer",y.args[1],b.indicator)):w.throw_error(P.error.type("atom",y.args[0],b.indicator))},"set_prolog_flag/2":function(w,b){var y=b.args[0],F=b.args[1];P.type.is_variable(y)||P.type.is_variable(F)?w.throw_error(P.error.instantiation(b.indicator)):P.type.is_atom(y)?P.type.is_flag(y)?P.type.is_value_flag(y,F)?P.type.is_modifiable_flag(y)?w.session.flag[y.id]=F:w.throw_error(P.error.permission("modify","flag",y)):w.throw_error(P.error.domain("flag_value",new _("+",[y,F]),b.indicator)):w.throw_error(P.error.domain("prolog_flag",y,b.indicator)):w.throw_error(P.error.type("atom",y,b.indicator))},"use_module/1":function(w,b){var y=b.args[0];if(P.type.is_variable(y))w.throw_error(P.error.instantiation(b.indicator));else if(!P.type.is_term(y))w.throw_error(P.error.type("term",y,b.indicator));else if(P.type.is_module(y)){var F=y.args[0].id;t(w.session.modules,F)===-1&&w.session.modules.push(F)}},"char_conversion/2":function(w,b){var y=b.args[0],F=b.args[1];P.type.is_variable(y)||P.type.is_variable(F)?w.throw_error(P.error.instantiation(b.indicator)):P.type.is_character(y)?P.type.is_character(F)?y.id===F.id?delete w.session.__char_conversion[y.id]:w.session.__char_conversion[y.id]=F.id:w.throw_error(P.error.type("character",F,b.indicator)):w.throw_error(P.error.type("character",y,b.indicator))},"op/3":function(w,b){var y=b.args[0],F=b.args[1],z=b.args[2];if(P.type.is_variable(y)||P.type.is_variable(F)||P.type.is_variable(z))w.throw_error(P.error.instantiation(b.indicator));else if(!P.type.is_integer(y))w.throw_error(P.error.type("integer",y,b.indicator));else if(!P.type.is_atom(F))w.throw_error(P.error.type("atom",F,b.indicator));else if(!P.type.is_atom(z))w.throw_error(P.error.type("atom",z,b.indicator));else if(y.value<0||y.value>1200)w.throw_error(P.error.domain("operator_priority",y,b.indicator));else if(z.id===",")w.throw_error(P.error.permission("modify","operator",z,b.indicator));else if(z.id==="|"&&(y.value<1001||F.id.length!==3))w.throw_error(P.error.permission("modify","operator",z,b.indicator));else if(["fy","fx","yf","xf","xfx","yfx","xfy"].indexOf(F.id)===-1)w.throw_error(P.error.domain("operator_specifier",F,b.indicator));else{var X={prefix:null,infix:null,postfix:null};for(var $ in w.session.__operators)if(w.session.__operators.hasOwnProperty($)){var se=w.session.__operators[$][z.id];se&&(t(se,"fx")!==-1&&(X.prefix={priority:$,type:"fx"}),t(se,"fy")!==-1&&(X.prefix={priority:$,type:"fy"}),t(se,"xf")!==-1&&(X.postfix={priority:$,type:"xf"}),t(se,"yf")!==-1&&(X.postfix={priority:$,type:"yf"}),t(se,"xfx")!==-1&&(X.infix={priority:$,type:"xfx"}),t(se,"xfy")!==-1&&(X.infix={priority:$,type:"xfy"}),t(se,"yfx")!==-1&&(X.infix={priority:$,type:"yfx"}))}var xe;switch(F.id){case"fy":case"fx":xe="prefix";break;case"yf":case"xf":xe="postfix";break;default:xe="infix";break}if(((X.prefix&&xe==="prefix"||X.postfix&&xe==="postfix"||X.infix&&xe==="infix")&&X[xe].type!==F.id||X.infix&&xe==="postfix"||X.postfix&&xe==="infix")&&y.value!==0)w.throw_error(P.error.permission("create","operator",z,b.indicator));else return X[xe]&&(Se(w.session.__operators[X[xe].priority][z.id],F.id),w.session.__operators[X[xe].priority][z.id].length===0&&delete w.session.__operators[X[xe].priority][z.id]),y.value>0&&(w.session.__operators[y.value]||(w.session.__operators[y.value.toString()]={}),w.session.__operators[y.value][z.id]||(w.session.__operators[y.value][z.id]=[]),w.session.__operators[y.value][z.id].push(F.id)),!0}}},predicate:{"op/3":function(w,b,y){P.directive["op/3"](w,y)&&w.success(b)},"current_op/3":function(w,b,y){var F=y.args[0],z=y.args[1],X=y.args[2],$=[];for(var se in w.session.__operators)for(var xe in w.session.__operators[se])for(var Fe=0;Fe/2"){var F=w.points,z=w.session.format_success,X=w.session.format_error;w.session.format_success=function(Fe){return Fe.substitution},w.session.format_error=function(Fe){return Fe.goal},w.points=[new be(y.args[0].args[0],b.substitution,b)];var $=function(Fe){w.points=F,w.session.format_success=z,w.session.format_error=X,Fe===!1?w.prepend([new be(b.goal.replace(y.args[1]),b.substitution,b)]):P.type.is_error(Fe)?w.throw_error(Fe.args[0]):Fe===null?(w.prepend([b]),w.__calls.shift()(null)):w.prepend([new be(b.goal.replace(y.args[0].args[1]).apply(Fe),b.substitution.apply(Fe),b)])};w.__calls.unshift($)}else{var se=new be(b.goal.replace(y.args[0]),b.substitution,b),xe=new be(b.goal.replace(y.args[1]),b.substitution,b);w.prepend([se,xe])}},"!/0":function(w,b,y){var F,z,X=[];for(F=b,z=null;F.parent!==null&&F.parent.goal.search(y);)if(z=F,F=F.parent,F.goal!==null){var $=F.goal.select();if($&&$.id==="call"&&$.search(y)){F=z;break}}for(var se=w.points.length-1;se>=0;se--){for(var xe=w.points[se],Fe=xe.parent;Fe!==null&&Fe!==F.parent;)Fe=Fe.parent;Fe===null&&Fe!==F.parent&&X.push(xe)}w.points=X.reverse(),w.success(b)},"\\+/1":function(w,b,y){var F=y.args[0];P.type.is_variable(F)?w.throw_error(P.error.instantiation(w.level)):P.type.is_callable(F)?w.prepend([new be(b.goal.replace(new _(",",[new _(",",[new _("call",[F]),new _("!",[])]),new _("fail",[])])),b.substitution,b),new be(b.goal.replace(null),b.substitution,b)]):w.throw_error(P.error.type("callable",F,w.level))},"->/2":function(w,b,y){var F=b.goal.replace(new _(",",[y.args[0],new _(",",[new _("!"),y.args[1]])]));w.prepend([new be(F,b.substitution,b)])},"fail/0":function(w,b,y){},"false/0":function(w,b,y){},"true/0":function(w,b,y){w.success(b)},"call/1":ce(1),"call/2":ce(2),"call/3":ce(3),"call/4":ce(4),"call/5":ce(5),"call/6":ce(6),"call/7":ce(7),"call/8":ce(8),"once/1":function(w,b,y){var F=y.args[0];w.prepend([new be(b.goal.replace(new _(",",[new _("call",[F]),new _("!",[])])),b.substitution,b)])},"forall/2":function(w,b,y){var F=y.args[0],z=y.args[1];w.prepend([new be(b.goal.replace(new _("\\+",[new _(",",[new _("call",[F]),new _("\\+",[new _("call",[z])])])])),b.substitution,b)])},"repeat/0":function(w,b,y){w.prepend([new be(b.goal.replace(null),b.substitution,b),b])},"throw/1":function(w,b,y){P.type.is_variable(y.args[0])?w.throw_error(P.error.instantiation(w.level)):w.throw_error(y.args[0])},"catch/3":function(w,b,y){var F=w.points;w.points=[],w.prepend([new be(y.args[0],b.substitution,b)]);var z=w.session.format_success,X=w.session.format_error;w.session.format_success=function(se){return se.substitution},w.session.format_error=function(se){return se.goal};var $=function(se){var xe=w.points;if(w.points=F,w.session.format_success=z,w.session.format_error=X,P.type.is_error(se)){for(var Fe=[],ut=w.points.length-1;ut>=0;ut--){for(var ir=w.points[ut],Ct=ir.parent;Ct!==null&&Ct!==b.parent;)Ct=Ct.parent;Ct===null&&Ct!==b.parent&&Fe.push(ir)}w.points=Fe;var qt=w.get_flag("occurs_check").indicator==="true/0",ir=new be,Pt=P.unify(se.args[0],y.args[1],qt);Pt!==null?(ir.substitution=b.substitution.apply(Pt),ir.goal=b.goal.replace(y.args[2]).apply(Pt),ir.parent=b,w.prepend([ir])):w.throw_error(se.args[0])}else if(se!==!1){for(var gn=se===null?[]:[new be(b.goal.apply(se).replace(null),b.substitution.apply(se),b)],Pr=[],ut=xe.length-1;ut>=0;ut--){Pr.push(xe[ut]);var Cr=xe[ut].goal!==null?xe[ut].goal.select():null;if(P.type.is_term(Cr)&&Cr.indicator==="!/0")break}var Or=s(Pr,function(on){return on.goal===null&&(on.goal=new _("true",[])),on=new be(b.goal.replace(new _("catch",[on.goal,y.args[1],y.args[2]])),b.substitution.apply(on.substitution),on.parent),on.exclude=y.args[0].variables(),on}).reverse();w.prepend(Or),w.prepend(gn),se===null&&(this.current_limit=0,w.__calls.shift()(null))}};w.__calls.unshift($)},"=/2":function(w,b,y){var F=w.get_flag("occurs_check").indicator==="true/0",z=new be,X=P.unify(y.args[0],y.args[1],F);X!==null&&(z.goal=b.goal.apply(X).replace(null),z.substitution=b.substitution.apply(X),z.parent=b,w.prepend([z]))},"unify_with_occurs_check/2":function(w,b,y){var F=new be,z=P.unify(y.args[0],y.args[1],!0);z!==null&&(F.goal=b.goal.apply(z).replace(null),F.substitution=b.substitution.apply(z),F.parent=b,w.prepend([F]))},"\\=/2":function(w,b,y){var F=w.get_flag("occurs_check").indicator==="true/0",z=P.unify(y.args[0],y.args[1],F);z===null&&w.success(b)},"subsumes_term/2":function(w,b,y){var F=w.get_flag("occurs_check").indicator==="true/0",z=P.unify(y.args[1],y.args[0],F);z!==null&&y.args[1].apply(z).equals(y.args[1])&&w.success(b)},"findall/3":function(w,b,y){var F=y.args[0],z=y.args[1],X=y.args[2];if(P.type.is_variable(z))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_callable(z))w.throw_error(P.error.type("callable",z,y.indicator));else if(!P.type.is_variable(X)&&!P.type.is_list(X))w.throw_error(P.error.type("list",X,y.indicator));else{var $=w.next_free_variable(),se=new _(",",[z,new _("=",[$,F])]),xe=w.points,Fe=w.session.limit,ut=w.session.format_success;w.session.format_success=function(ir){return ir.substitution},w.add_goal(se,!0,b);var Ct=[],qt=function(ir){if(ir!==!1&&ir!==null&&!P.type.is_error(ir))w.__calls.unshift(qt),Ct.push(ir.links[$.id]),w.session.limit=w.current_limit;else if(w.points=xe,w.session.limit=Fe,w.session.format_success=ut,P.type.is_error(ir))w.throw_error(ir.args[0]);else if(w.current_limit>0){for(var Pt=new _("[]"),gn=Ct.length-1;gn>=0;gn--)Pt=new _(".",[Ct[gn],Pt]);w.prepend([new be(b.goal.replace(new _("=",[X,Pt])),b.substitution,b)])}};w.__calls.unshift(qt)}},"bagof/3":function(w,b,y){var F,z=y.args[0],X=y.args[1],$=y.args[2];if(P.type.is_variable(X))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_callable(X))w.throw_error(P.error.type("callable",X,y.indicator));else if(!P.type.is_variable($)&&!P.type.is_list($))w.throw_error(P.error.type("list",$,y.indicator));else{var se=w.next_free_variable(),xe;X.indicator==="^/2"?(xe=X.args[0].variables(),X=X.args[1]):xe=[],xe=xe.concat(z.variables());for(var Fe=X.variables().filter(function(Or){return t(xe,Or)===-1}),ut=new _("[]"),Ct=Fe.length-1;Ct>=0;Ct--)ut=new _(".",[new De(Fe[Ct]),ut]);var qt=new _(",",[X,new _("=",[se,new _(",",[ut,z])])]),ir=w.points,Pt=w.session.limit,gn=w.session.format_success;w.session.format_success=function(Or){return Or.substitution},w.add_goal(qt,!0,b);var Pr=[],Cr=function(Or){if(Or!==!1&&Or!==null&&!P.type.is_error(Or)){w.__calls.unshift(Cr);var on=!1,li=Or.links[se.id].args[0],Do=Or.links[se.id].args[1];for(var ns in Pr)if(Pr.hasOwnProperty(ns)){var so=Pr[ns];if(so.variables.equals(li)){so.answers.push(Do),on=!0;break}}on||Pr.push({variables:li,answers:[Do]}),w.session.limit=w.current_limit}else if(w.points=ir,w.session.limit=Pt,w.session.format_success=gn,P.type.is_error(Or))w.throw_error(Or.args[0]);else if(w.current_limit>0){for(var bo=[],ji=0;ji=0;Po--)oo=new _(".",[Or[Po],oo]);bo.push(new be(b.goal.replace(new _(",",[new _("=",[ut,Pr[ji].variables]),new _("=",[$,oo])])),b.substitution,b))}w.prepend(bo)}};w.__calls.unshift(Cr)}},"setof/3":function(w,b,y){var F,z=y.args[0],X=y.args[1],$=y.args[2];if(P.type.is_variable(X))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_callable(X))w.throw_error(P.error.type("callable",X,y.indicator));else if(!P.type.is_variable($)&&!P.type.is_list($))w.throw_error(P.error.type("list",$,y.indicator));else{var se=w.next_free_variable(),xe;X.indicator==="^/2"?(xe=X.args[0].variables(),X=X.args[1]):xe=[],xe=xe.concat(z.variables());for(var Fe=X.variables().filter(function(Or){return t(xe,Or)===-1}),ut=new _("[]"),Ct=Fe.length-1;Ct>=0;Ct--)ut=new _(".",[new De(Fe[Ct]),ut]);var qt=new _(",",[X,new _("=",[se,new _(",",[ut,z])])]),ir=w.points,Pt=w.session.limit,gn=w.session.format_success;w.session.format_success=function(Or){return Or.substitution},w.add_goal(qt,!0,b);var Pr=[],Cr=function(Or){if(Or!==!1&&Or!==null&&!P.type.is_error(Or)){w.__calls.unshift(Cr);var on=!1,li=Or.links[se.id].args[0],Do=Or.links[se.id].args[1];for(var ns in Pr)if(Pr.hasOwnProperty(ns)){var so=Pr[ns];if(so.variables.equals(li)){so.answers.push(Do),on=!0;break}}on||Pr.push({variables:li,answers:[Do]}),w.session.limit=w.current_limit}else if(w.points=ir,w.session.limit=Pt,w.session.format_success=gn,P.type.is_error(Or))w.throw_error(Or.args[0]);else if(w.current_limit>0){for(var bo=[],ji=0;ji=0;Po--)oo=new _(".",[Or[Po],oo]);bo.push(new be(b.goal.replace(new _(",",[new _("=",[ut,Pr[ji].variables]),new _("=",[$,oo])])),b.substitution,b))}w.prepend(bo)}};w.__calls.unshift(Cr)}},"functor/3":function(w,b,y){var F,z=y.args[0],X=y.args[1],$=y.args[2];if(P.type.is_variable(z)&&(P.type.is_variable(X)||P.type.is_variable($)))w.throw_error(P.error.instantiation("functor/3"));else if(!P.type.is_variable($)&&!P.type.is_integer($))w.throw_error(P.error.type("integer",y.args[2],"functor/3"));else if(!P.type.is_variable(X)&&!P.type.is_atomic(X))w.throw_error(P.error.type("atomic",y.args[1],"functor/3"));else if(P.type.is_integer(X)&&P.type.is_integer($)&&$.value!==0)w.throw_error(P.error.type("atom",y.args[1],"functor/3"));else if(P.type.is_variable(z)){if(y.args[2].value>=0){for(var se=[],xe=0;xe<$.value;xe++)se.push(w.next_free_variable());var Fe=P.type.is_integer(X)?X:new _(X.id,se);w.prepend([new be(b.goal.replace(new _("=",[z,Fe])),b.substitution,b)])}}else{var ut=P.type.is_integer(z)?z:new _(z.id,[]),Ct=P.type.is_integer(z)?new Qe(0,!1):new Qe(z.args.length,!1),qt=new _(",",[new _("=",[ut,X]),new _("=",[Ct,$])]);w.prepend([new be(b.goal.replace(qt),b.substitution,b)])}},"arg/3":function(w,b,y){if(P.type.is_variable(y.args[0])||P.type.is_variable(y.args[1]))w.throw_error(P.error.instantiation(y.indicator));else if(y.args[0].value<0)w.throw_error(P.error.domain("not_less_than_zero",y.args[0],y.indicator));else if(!P.type.is_compound(y.args[1]))w.throw_error(P.error.type("compound",y.args[1],y.indicator));else{var F=y.args[0].value;if(F>0&&F<=y.args[1].args.length){var z=new _("=",[y.args[1].args[F-1],y.args[2]]);w.prepend([new be(b.goal.replace(z),b.substitution,b)])}}},"=../2":function(w,b,y){var F;if(P.type.is_variable(y.args[0])&&(P.type.is_variable(y.args[1])||P.type.is_non_empty_list(y.args[1])&&P.type.is_variable(y.args[1].args[0])))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_fully_list(y.args[1]))w.throw_error(P.error.type("list",y.args[1],y.indicator));else if(P.type.is_variable(y.args[0])){if(!P.type.is_variable(y.args[1])){var X=[];for(F=y.args[1].args[1];F.indicator==="./2";)X.push(F.args[0]),F=F.args[1];P.type.is_variable(y.args[0])&&P.type.is_variable(F)?w.throw_error(P.error.instantiation(y.indicator)):X.length===0&&P.type.is_compound(y.args[1].args[0])?w.throw_error(P.error.type("atomic",y.args[1].args[0],y.indicator)):X.length>0&&(P.type.is_compound(y.args[1].args[0])||P.type.is_number(y.args[1].args[0]))?w.throw_error(P.error.type("atom",y.args[1].args[0],y.indicator)):X.length===0?w.prepend([new be(b.goal.replace(new _("=",[y.args[1].args[0],y.args[0]],b)),b.substitution,b)]):w.prepend([new be(b.goal.replace(new _("=",[new _(y.args[1].args[0].id,X),y.args[0]])),b.substitution,b)])}}else{if(P.type.is_atomic(y.args[0]))F=new _(".",[y.args[0],new _("[]")]);else{F=new _("[]");for(var z=y.args[0].args.length-1;z>=0;z--)F=new _(".",[y.args[0].args[z],F]);F=new _(".",[new _(y.args[0].id),F])}w.prepend([new be(b.goal.replace(new _("=",[F,y.args[1]])),b.substitution,b)])}},"copy_term/2":function(w,b,y){var F=y.args[0].rename(w);w.prepend([new be(b.goal.replace(new _("=",[F,y.args[1]])),b.substitution,b.parent)])},"term_variables/2":function(w,b,y){var F=y.args[0],z=y.args[1];if(!P.type.is_fully_list(z))w.throw_error(P.error.type("list",z,y.indicator));else{var X=d(s(Be(F.variables()),function($){return new De($)}));w.prepend([new be(b.goal.replace(new _("=",[z,X])),b.substitution,b)])}},"clause/2":function(w,b,y){if(P.type.is_variable(y.args[0]))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_callable(y.args[0]))w.throw_error(P.error.type("callable",y.args[0],y.indicator));else if(!P.type.is_variable(y.args[1])&&!P.type.is_callable(y.args[1]))w.throw_error(P.error.type("callable",y.args[1],y.indicator));else if(w.session.rules[y.args[0].indicator]!==void 0)if(w.is_public_predicate(y.args[0].indicator)){var F=[];for(var z in w.session.rules[y.args[0].indicator])if(w.session.rules[y.args[0].indicator].hasOwnProperty(z)){var X=w.session.rules[y.args[0].indicator][z];w.session.renamed_variables={},X=X.rename(w),X.body===null&&(X.body=new _("true"));var $=new _(",",[new _("=",[X.head,y.args[0]]),new _("=",[X.body,y.args[1]])]);F.push(new be(b.goal.replace($),b.substitution,b))}w.prepend(F)}else w.throw_error(P.error.permission("access","private_procedure",y.args[0].indicator,y.indicator))},"current_predicate/1":function(w,b,y){var F=y.args[0];if(!P.type.is_variable(F)&&(!P.type.is_compound(F)||F.indicator!=="//2"))w.throw_error(P.error.type("predicate_indicator",F,y.indicator));else if(!P.type.is_variable(F)&&!P.type.is_variable(F.args[0])&&!P.type.is_atom(F.args[0]))w.throw_error(P.error.type("atom",F.args[0],y.indicator));else if(!P.type.is_variable(F)&&!P.type.is_variable(F.args[1])&&!P.type.is_integer(F.args[1]))w.throw_error(P.error.type("integer",F.args[1],y.indicator));else{var z=[];for(var X in w.session.rules)if(w.session.rules.hasOwnProperty(X)){var $=X.lastIndexOf("/"),se=X.substr(0,$),xe=parseInt(X.substr($+1,X.length-($+1))),Fe=new _("/",[new _(se),new Qe(xe,!1)]),ut=new _("=",[Fe,F]);z.push(new be(b.goal.replace(ut),b.substitution,b))}w.prepend(z)}},"asserta/1":function(w,b,y){if(P.type.is_variable(y.args[0]))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_callable(y.args[0]))w.throw_error(P.error.type("callable",y.args[0],y.indicator));else{var F,z;y.args[0].indicator===":-/2"?(F=y.args[0].args[0],z=Ee(y.args[0].args[1])):(F=y.args[0],z=null),P.type.is_callable(F)?z!==null&&!P.type.is_callable(z)?w.throw_error(P.error.type("callable",z,y.indicator)):w.is_public_predicate(F.indicator)?(w.session.rules[F.indicator]===void 0&&(w.session.rules[F.indicator]=[]),w.session.public_predicates[F.indicator]=!0,w.session.rules[F.indicator]=[new je(F,z,!0)].concat(w.session.rules[F.indicator]),w.success(b)):w.throw_error(P.error.permission("modify","static_procedure",F.indicator,y.indicator)):w.throw_error(P.error.type("callable",F,y.indicator))}},"assertz/1":function(w,b,y){if(P.type.is_variable(y.args[0]))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_callable(y.args[0]))w.throw_error(P.error.type("callable",y.args[0],y.indicator));else{var F,z;y.args[0].indicator===":-/2"?(F=y.args[0].args[0],z=Ee(y.args[0].args[1])):(F=y.args[0],z=null),P.type.is_callable(F)?z!==null&&!P.type.is_callable(z)?w.throw_error(P.error.type("callable",z,y.indicator)):w.is_public_predicate(F.indicator)?(w.session.rules[F.indicator]===void 0&&(w.session.rules[F.indicator]=[]),w.session.public_predicates[F.indicator]=!0,w.session.rules[F.indicator].push(new je(F,z,!0)),w.success(b)):w.throw_error(P.error.permission("modify","static_procedure",F.indicator,y.indicator)):w.throw_error(P.error.type("callable",F,y.indicator))}},"retract/1":function(w,b,y){if(P.type.is_variable(y.args[0]))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_callable(y.args[0]))w.throw_error(P.error.type("callable",y.args[0],y.indicator));else{var F,z;if(y.args[0].indicator===":-/2"?(F=y.args[0].args[0],z=y.args[0].args[1]):(F=y.args[0],z=new _("true")),typeof b.retract>"u")if(w.is_public_predicate(F.indicator)){if(w.session.rules[F.indicator]!==void 0){for(var X=[],$=0;$w.get_flag("max_arity").value)w.throw_error(P.error.representation("max_arity",y.indicator));else{var F=y.args[0].args[0].id+"/"+y.args[0].args[1].value;w.is_public_predicate(F)?(delete w.session.rules[F],w.success(b)):w.throw_error(P.error.permission("modify","static_procedure",F,y.indicator))}},"atom_length/2":function(w,b,y){if(P.type.is_variable(y.args[0]))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_atom(y.args[0]))w.throw_error(P.error.type("atom",y.args[0],y.indicator));else if(!P.type.is_variable(y.args[1])&&!P.type.is_integer(y.args[1]))w.throw_error(P.error.type("integer",y.args[1],y.indicator));else if(P.type.is_integer(y.args[1])&&y.args[1].value<0)w.throw_error(P.error.domain("not_less_than_zero",y.args[1],y.indicator));else{var F=new Qe(y.args[0].id.length,!1);w.prepend([new be(b.goal.replace(new _("=",[F,y.args[1]])),b.substitution,b)])}},"atom_concat/3":function(w,b,y){var F,z,X=y.args[0],$=y.args[1],se=y.args[2];if(P.type.is_variable(se)&&(P.type.is_variable(X)||P.type.is_variable($)))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(X)&&!P.type.is_atom(X))w.throw_error(P.error.type("atom",X,y.indicator));else if(!P.type.is_variable($)&&!P.type.is_atom($))w.throw_error(P.error.type("atom",$,y.indicator));else if(!P.type.is_variable(se)&&!P.type.is_atom(se))w.throw_error(P.error.type("atom",se,y.indicator));else{var xe=P.type.is_variable(X),Fe=P.type.is_variable($);if(!xe&&!Fe)z=new _("=",[se,new _(X.id+$.id)]),w.prepend([new be(b.goal.replace(z),b.substitution,b)]);else if(xe&&!Fe)F=se.id.substr(0,se.id.length-$.id.length),F+$.id===se.id&&(z=new _("=",[X,new _(F)]),w.prepend([new be(b.goal.replace(z),b.substitution,b)]));else if(Fe&&!xe)F=se.id.substr(X.id.length),X.id+F===se.id&&(z=new _("=",[$,new _(F)]),w.prepend([new be(b.goal.replace(z),b.substitution,b)]));else{for(var ut=[],Ct=0;Ct<=se.id.length;Ct++){var qt=new _(se.id.substr(0,Ct)),ir=new _(se.id.substr(Ct));z=new _(",",[new _("=",[qt,X]),new _("=",[ir,$])]),ut.push(new be(b.goal.replace(z),b.substitution,b))}w.prepend(ut)}}},"sub_atom/5":function(w,b,y){var F,z=y.args[0],X=y.args[1],$=y.args[2],se=y.args[3],xe=y.args[4];if(P.type.is_variable(z))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(X)&&!P.type.is_integer(X))w.throw_error(P.error.type("integer",X,y.indicator));else if(!P.type.is_variable($)&&!P.type.is_integer($))w.throw_error(P.error.type("integer",$,y.indicator));else if(!P.type.is_variable(se)&&!P.type.is_integer(se))w.throw_error(P.error.type("integer",se,y.indicator));else if(P.type.is_integer(X)&&X.value<0)w.throw_error(P.error.domain("not_less_than_zero",X,y.indicator));else if(P.type.is_integer($)&&$.value<0)w.throw_error(P.error.domain("not_less_than_zero",$,y.indicator));else if(P.type.is_integer(se)&&se.value<0)w.throw_error(P.error.domain("not_less_than_zero",se,y.indicator));else{var Fe=[],ut=[],Ct=[];if(P.type.is_variable(X))for(F=0;F<=z.id.length;F++)Fe.push(F);else Fe.push(X.value);if(P.type.is_variable($))for(F=0;F<=z.id.length;F++)ut.push(F);else ut.push($.value);if(P.type.is_variable(se))for(F=0;F<=z.id.length;F++)Ct.push(F);else Ct.push(se.value);var qt=[];for(var ir in Fe)if(Fe.hasOwnProperty(ir)){F=Fe[ir];for(var Pt in ut)if(ut.hasOwnProperty(Pt)){var gn=ut[Pt],Pr=z.id.length-F-gn;if(t(Ct,Pr)!==-1&&F+gn+Pr===z.id.length){var Cr=z.id.substr(F,gn);if(z.id===z.id.substr(0,F)+Cr+z.id.substr(F+gn,Pr)){var Or=new _("=",[new _(Cr),xe]),on=new _("=",[X,new Qe(F)]),li=new _("=",[$,new Qe(gn)]),Do=new _("=",[se,new Qe(Pr)]),ns=new _(",",[new _(",",[new _(",",[on,li]),Do]),Or]);qt.push(new be(b.goal.replace(ns),b.substitution,b))}}}}w.prepend(qt)}},"atom_chars/2":function(w,b,y){var F=y.args[0],z=y.args[1];if(P.type.is_variable(F)&&P.type.is_variable(z))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(F)&&!P.type.is_atom(F))w.throw_error(P.error.type("atom",F,y.indicator));else if(P.type.is_variable(F)){for(var se=z,xe=P.type.is_variable(F),Fe="";se.indicator==="./2";){if(P.type.is_character(se.args[0]))Fe+=se.args[0].id;else if(P.type.is_variable(se.args[0])&&xe){w.throw_error(P.error.instantiation(y.indicator));return}else if(!P.type.is_variable(se.args[0])){w.throw_error(P.error.type("character",se.args[0],y.indicator));return}se=se.args[1]}P.type.is_variable(se)&&xe?w.throw_error(P.error.instantiation(y.indicator)):!P.type.is_empty_list(se)&&!P.type.is_variable(se)?w.throw_error(P.error.type("list",z,y.indicator)):w.prepend([new be(b.goal.replace(new _("=",[new _(Fe),F])),b.substitution,b)])}else{for(var X=new _("[]"),$=F.id.length-1;$>=0;$--)X=new _(".",[new _(F.id.charAt($)),X]);w.prepend([new be(b.goal.replace(new _("=",[z,X])),b.substitution,b)])}},"atom_codes/2":function(w,b,y){var F=y.args[0],z=y.args[1];if(P.type.is_variable(F)&&P.type.is_variable(z))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(F)&&!P.type.is_atom(F))w.throw_error(P.error.type("atom",F,y.indicator));else if(P.type.is_variable(F)){for(var se=z,xe=P.type.is_variable(F),Fe="";se.indicator==="./2";){if(P.type.is_character_code(se.args[0]))Fe+=c(se.args[0].value);else if(P.type.is_variable(se.args[0])&&xe){w.throw_error(P.error.instantiation(y.indicator));return}else if(!P.type.is_variable(se.args[0])){w.throw_error(P.error.representation("character_code",y.indicator));return}se=se.args[1]}P.type.is_variable(se)&&xe?w.throw_error(P.error.instantiation(y.indicator)):!P.type.is_empty_list(se)&&!P.type.is_variable(se)?w.throw_error(P.error.type("list",z,y.indicator)):w.prepend([new be(b.goal.replace(new _("=",[new _(Fe),F])),b.substitution,b)])}else{for(var X=new _("[]"),$=F.id.length-1;$>=0;$--)X=new _(".",[new Qe(n(F.id,$),!1),X]);w.prepend([new be(b.goal.replace(new _("=",[z,X])),b.substitution,b)])}},"char_code/2":function(w,b,y){var F=y.args[0],z=y.args[1];if(P.type.is_variable(F)&&P.type.is_variable(z))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(F)&&!P.type.is_character(F))w.throw_error(P.error.type("character",F,y.indicator));else if(!P.type.is_variable(z)&&!P.type.is_integer(z))w.throw_error(P.error.type("integer",z,y.indicator));else if(!P.type.is_variable(z)&&!P.type.is_character_code(z))w.throw_error(P.error.representation("character_code",y.indicator));else if(P.type.is_variable(z)){var X=new Qe(n(F.id,0),!1);w.prepend([new be(b.goal.replace(new _("=",[X,z])),b.substitution,b)])}else{var $=new _(c(z.value));w.prepend([new be(b.goal.replace(new _("=",[$,F])),b.substitution,b)])}},"number_chars/2":function(w,b,y){var F,z=y.args[0],X=y.args[1];if(P.type.is_variable(z)&&P.type.is_variable(X))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(z)&&!P.type.is_number(z))w.throw_error(P.error.type("number",z,y.indicator));else if(!P.type.is_variable(X)&&!P.type.is_list(X))w.throw_error(P.error.type("list",X,y.indicator));else{var $=P.type.is_variable(z);if(!P.type.is_variable(X)){var se=X,xe=!0;for(F="";se.indicator==="./2";){if(P.type.is_character(se.args[0]))F+=se.args[0].id;else if(P.type.is_variable(se.args[0]))xe=!1;else if(!P.type.is_variable(se.args[0])){w.throw_error(P.error.type("character",se.args[0],y.indicator));return}se=se.args[1]}if(xe=xe&&P.type.is_empty_list(se),!P.type.is_empty_list(se)&&!P.type.is_variable(se)){w.throw_error(P.error.type("list",X,y.indicator));return}if(!xe&&$){w.throw_error(P.error.instantiation(y.indicator));return}else if(xe)if(P.type.is_variable(se)&&$){w.throw_error(P.error.instantiation(y.indicator));return}else{var Fe=w.parse(F),ut=Fe.value;!P.type.is_number(ut)||Fe.tokens[Fe.tokens.length-1].space?w.throw_error(P.error.syntax_by_predicate("parseable_number",y.indicator)):w.prepend([new be(b.goal.replace(new _("=",[z,ut])),b.substitution,b)]);return}}if(!$){F=z.toString();for(var Ct=new _("[]"),qt=F.length-1;qt>=0;qt--)Ct=new _(".",[new _(F.charAt(qt)),Ct]);w.prepend([new be(b.goal.replace(new _("=",[X,Ct])),b.substitution,b)])}}},"number_codes/2":function(w,b,y){var F,z=y.args[0],X=y.args[1];if(P.type.is_variable(z)&&P.type.is_variable(X))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(z)&&!P.type.is_number(z))w.throw_error(P.error.type("number",z,y.indicator));else if(!P.type.is_variable(X)&&!P.type.is_list(X))w.throw_error(P.error.type("list",X,y.indicator));else{var $=P.type.is_variable(z);if(!P.type.is_variable(X)){var se=X,xe=!0;for(F="";se.indicator==="./2";){if(P.type.is_character_code(se.args[0]))F+=c(se.args[0].value);else if(P.type.is_variable(se.args[0]))xe=!1;else if(!P.type.is_variable(se.args[0])){w.throw_error(P.error.type("character_code",se.args[0],y.indicator));return}se=se.args[1]}if(xe=xe&&P.type.is_empty_list(se),!P.type.is_empty_list(se)&&!P.type.is_variable(se)){w.throw_error(P.error.type("list",X,y.indicator));return}if(!xe&&$){w.throw_error(P.error.instantiation(y.indicator));return}else if(xe)if(P.type.is_variable(se)&&$){w.throw_error(P.error.instantiation(y.indicator));return}else{var Fe=w.parse(F),ut=Fe.value;!P.type.is_number(ut)||Fe.tokens[Fe.tokens.length-1].space?w.throw_error(P.error.syntax_by_predicate("parseable_number",y.indicator)):w.prepend([new be(b.goal.replace(new _("=",[z,ut])),b.substitution,b)]);return}}if(!$){F=z.toString();for(var Ct=new _("[]"),qt=F.length-1;qt>=0;qt--)Ct=new _(".",[new Qe(n(F,qt),!1),Ct]);w.prepend([new be(b.goal.replace(new _("=",[X,Ct])),b.substitution,b)])}}},"upcase_atom/2":function(w,b,y){var F=y.args[0],z=y.args[1];P.type.is_variable(F)?w.throw_error(P.error.instantiation(y.indicator)):P.type.is_atom(F)?!P.type.is_variable(z)&&!P.type.is_atom(z)?w.throw_error(P.error.type("atom",z,y.indicator)):w.prepend([new be(b.goal.replace(new _("=",[z,new _(F.id.toUpperCase(),[])])),b.substitution,b)]):w.throw_error(P.error.type("atom",F,y.indicator))},"downcase_atom/2":function(w,b,y){var F=y.args[0],z=y.args[1];P.type.is_variable(F)?w.throw_error(P.error.instantiation(y.indicator)):P.type.is_atom(F)?!P.type.is_variable(z)&&!P.type.is_atom(z)?w.throw_error(P.error.type("atom",z,y.indicator)):w.prepend([new be(b.goal.replace(new _("=",[z,new _(F.id.toLowerCase(),[])])),b.substitution,b)]):w.throw_error(P.error.type("atom",F,y.indicator))},"atomic_list_concat/2":function(w,b,y){var F=y.args[0],z=y.args[1];w.prepend([new be(b.goal.replace(new _("atomic_list_concat",[F,new _("",[]),z])),b.substitution,b)])},"atomic_list_concat/3":function(w,b,y){var F=y.args[0],z=y.args[1],X=y.args[2];if(P.type.is_variable(z)||P.type.is_variable(F)&&P.type.is_variable(X))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_variable(F)&&!P.type.is_list(F))w.throw_error(P.error.type("list",F,y.indicator));else if(!P.type.is_variable(X)&&!P.type.is_atom(X))w.throw_error(P.error.type("atom",X,y.indicator));else if(P.type.is_variable(X)){for(var se="",xe=F;P.type.is_term(xe)&&xe.indicator==="./2";){if(!P.type.is_atom(xe.args[0])&&!P.type.is_number(xe.args[0])){w.throw_error(P.error.type("atomic",xe.args[0],y.indicator));return}se!==""&&(se+=z.id),P.type.is_atom(xe.args[0])?se+=xe.args[0].id:se+=""+xe.args[0].value,xe=xe.args[1]}se=new _(se,[]),P.type.is_variable(xe)?w.throw_error(P.error.instantiation(y.indicator)):!P.type.is_term(xe)||xe.indicator!=="[]/0"?w.throw_error(P.error.type("list",F,y.indicator)):w.prepend([new be(b.goal.replace(new _("=",[se,X])),b.substitution,b)])}else{var $=d(s(X.id.split(z.id),function(Fe){return new _(Fe,[])}));w.prepend([new be(b.goal.replace(new _("=",[$,F])),b.substitution,b)])}},"@=/2":function(w,b,y){P.compare(y.args[0],y.args[1])>0&&w.success(b)},"@>=/2":function(w,b,y){P.compare(y.args[0],y.args[1])>=0&&w.success(b)},"compare/3":function(w,b,y){var F=y.args[0],z=y.args[1],X=y.args[2];if(!P.type.is_variable(F)&&!P.type.is_atom(F))w.throw_error(P.error.type("atom",F,y.indicator));else if(P.type.is_atom(F)&&["<",">","="].indexOf(F.id)===-1)w.throw_error(P.type.domain("order",F,y.indicator));else{var $=P.compare(z,X);$=$===0?"=":$===-1?"<":">",w.prepend([new be(b.goal.replace(new _("=",[F,new _($,[])])),b.substitution,b)])}},"is/2":function(w,b,y){var F=y.args[1].interpret(w);P.type.is_number(F)?w.prepend([new be(b.goal.replace(new _("=",[y.args[0],F],w.level)),b.substitution,b)]):w.throw_error(F)},"between/3":function(w,b,y){var F=y.args[0],z=y.args[1],X=y.args[2];if(P.type.is_variable(F)||P.type.is_variable(z))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_integer(F))w.throw_error(P.error.type("integer",F,y.indicator));else if(!P.type.is_integer(z))w.throw_error(P.error.type("integer",z,y.indicator));else if(!P.type.is_variable(X)&&!P.type.is_integer(X))w.throw_error(P.error.type("integer",X,y.indicator));else if(P.type.is_variable(X)){var $=[new be(b.goal.replace(new _("=",[X,F])),b.substitution,b)];F.value=X.value&&w.success(b)},"succ/2":function(w,b,y){var F=y.args[0],z=y.args[1];P.type.is_variable(F)&&P.type.is_variable(z)?w.throw_error(P.error.instantiation(y.indicator)):!P.type.is_variable(F)&&!P.type.is_integer(F)?w.throw_error(P.error.type("integer",F,y.indicator)):!P.type.is_variable(z)&&!P.type.is_integer(z)?w.throw_error(P.error.type("integer",z,y.indicator)):!P.type.is_variable(F)&&F.value<0?w.throw_error(P.error.domain("not_less_than_zero",F,y.indicator)):!P.type.is_variable(z)&&z.value<0?w.throw_error(P.error.domain("not_less_than_zero",z,y.indicator)):(P.type.is_variable(z)||z.value>0)&&(P.type.is_variable(F)?w.prepend([new be(b.goal.replace(new _("=",[F,new Qe(z.value-1,!1)])),b.substitution,b)]):w.prepend([new be(b.goal.replace(new _("=",[z,new Qe(F.value+1,!1)])),b.substitution,b)]))},"=:=/2":function(w,b,y){var F=P.arithmetic_compare(w,y.args[0],y.args[1]);P.type.is_term(F)?w.throw_error(F):F===0&&w.success(b)},"=\\=/2":function(w,b,y){var F=P.arithmetic_compare(w,y.args[0],y.args[1]);P.type.is_term(F)?w.throw_error(F):F!==0&&w.success(b)},"/2":function(w,b,y){var F=P.arithmetic_compare(w,y.args[0],y.args[1]);P.type.is_term(F)?w.throw_error(F):F>0&&w.success(b)},">=/2":function(w,b,y){var F=P.arithmetic_compare(w,y.args[0],y.args[1]);P.type.is_term(F)?w.throw_error(F):F>=0&&w.success(b)},"var/1":function(w,b,y){P.type.is_variable(y.args[0])&&w.success(b)},"atom/1":function(w,b,y){P.type.is_atom(y.args[0])&&w.success(b)},"atomic/1":function(w,b,y){P.type.is_atomic(y.args[0])&&w.success(b)},"compound/1":function(w,b,y){P.type.is_compound(y.args[0])&&w.success(b)},"integer/1":function(w,b,y){P.type.is_integer(y.args[0])&&w.success(b)},"float/1":function(w,b,y){P.type.is_float(y.args[0])&&w.success(b)},"number/1":function(w,b,y){P.type.is_number(y.args[0])&&w.success(b)},"nonvar/1":function(w,b,y){P.type.is_variable(y.args[0])||w.success(b)},"ground/1":function(w,b,y){y.variables().length===0&&w.success(b)},"acyclic_term/1":function(w,b,y){for(var F=b.substitution.apply(b.substitution),z=y.args[0].variables(),X=0;X0?Pt[Pt.length-1]:null,Pt!==null&&(qt=V(w,Pt,0,w.__get_max_priority(),!1))}if(qt.type===p&&qt.len===Pt.length-1&&gn.value==="."){qt=qt.value.rename(w);var Pr=new _("=",[z,qt]);if(se.variables){var Cr=d(s(Be(qt.variables()),function(Or){return new De(Or)}));Pr=new _(",",[Pr,new _("=",[se.variables,Cr])])}if(se.variable_names){var Cr=d(s(Be(qt.variables()),function(on){var li;for(li in w.session.renamed_variables)if(w.session.renamed_variables.hasOwnProperty(li)&&w.session.renamed_variables[li]===on)break;return new _("=",[new _(li,[]),new De(on)])}));Pr=new _(",",[Pr,new _("=",[se.variable_names,Cr])])}if(se.singletons){var Cr=d(s(new je(qt,null).singleton_variables(),function(on){var li;for(li in w.session.renamed_variables)if(w.session.renamed_variables.hasOwnProperty(li)&&w.session.renamed_variables[li]===on)break;return new _("=",[new _(li,[]),new De(on)])}));Pr=new _(",",[Pr,new _("=",[se.singletons,Cr])])}w.prepend([new be(b.goal.replace(Pr),b.substitution,b)])}else qt.type===p?w.throw_error(P.error.syntax(Pt[qt.len],"unexpected token",!1)):w.throw_error(qt.value)}}},"write/1":function(w,b,y){var F=y.args[0];w.prepend([new be(b.goal.replace(new _(",",[new _("current_output",[new De("S")]),new _("write",[new De("S"),F])])),b.substitution,b)])},"write/2":function(w,b,y){var F=y.args[0],z=y.args[1];w.prepend([new be(b.goal.replace(new _("write_term",[F,z,new _(".",[new _("quoted",[new _("false",[])]),new _(".",[new _("ignore_ops",[new _("false")]),new _(".",[new _("numbervars",[new _("true")]),new _("[]",[])])])])])),b.substitution,b)])},"writeq/1":function(w,b,y){var F=y.args[0];w.prepend([new be(b.goal.replace(new _(",",[new _("current_output",[new De("S")]),new _("writeq",[new De("S"),F])])),b.substitution,b)])},"writeq/2":function(w,b,y){var F=y.args[0],z=y.args[1];w.prepend([new be(b.goal.replace(new _("write_term",[F,z,new _(".",[new _("quoted",[new _("true",[])]),new _(".",[new _("ignore_ops",[new _("false")]),new _(".",[new _("numbervars",[new _("true")]),new _("[]",[])])])])])),b.substitution,b)])},"write_canonical/1":function(w,b,y){var F=y.args[0];w.prepend([new be(b.goal.replace(new _(",",[new _("current_output",[new De("S")]),new _("write_canonical",[new De("S"),F])])),b.substitution,b)])},"write_canonical/2":function(w,b,y){var F=y.args[0],z=y.args[1];w.prepend([new be(b.goal.replace(new _("write_term",[F,z,new _(".",[new _("quoted",[new _("true",[])]),new _(".",[new _("ignore_ops",[new _("true")]),new _(".",[new _("numbervars",[new _("false")]),new _("[]",[])])])])])),b.substitution,b)])},"write_term/2":function(w,b,y){var F=y.args[0],z=y.args[1];w.prepend([new be(b.goal.replace(new _(",",[new _("current_output",[new De("S")]),new _("write_term",[new De("S"),F,z])])),b.substitution,b)])},"write_term/3":function(w,b,y){var F=y.args[0],z=y.args[1],X=y.args[2],$=P.type.is_stream(F)?F:w.get_stream_by_alias(F.id);if(P.type.is_variable(F)||P.type.is_variable(X))w.throw_error(P.error.instantiation(y.indicator));else if(!P.type.is_list(X))w.throw_error(P.error.type("list",X,y.indicator));else if(!P.type.is_stream(F)&&!P.type.is_atom(F))w.throw_error(P.error.domain("stream_or_alias",F,y.indicator));else if(!P.type.is_stream($)||$.stream===null)w.throw_error(P.error.existence("stream",F,y.indicator));else if($.input)w.throw_error(P.error.permission("output","stream",F,y.indicator));else if($.type==="binary")w.throw_error(P.error.permission("output","binary_stream",F,y.indicator));else if($.position==="past_end_of_stream"&&$.eof_action==="error")w.throw_error(P.error.permission("output","past_end_of_stream",F,y.indicator));else{for(var se={},xe=X,Fe;P.type.is_term(xe)&&xe.indicator==="./2";){if(Fe=xe.args[0],P.type.is_variable(Fe)){w.throw_error(P.error.instantiation(y.indicator));return}else if(!P.type.is_write_option(Fe)){w.throw_error(P.error.domain("write_option",Fe,y.indicator));return}se[Fe.id]=Fe.args[0].id==="true",xe=xe.args[1]}if(xe.indicator!=="[]/0"){P.type.is_variable(xe)?w.throw_error(P.error.instantiation(y.indicator)):w.throw_error(P.error.type("list",X,y.indicator));return}else{se.session=w.session;var ut=z.toString(se);$.stream.put(ut,$.position),typeof $.position=="number"&&($.position+=ut.length),w.success(b)}}},"halt/0":function(w,b,y){w.points=[]},"halt/1":function(w,b,y){var F=y.args[0];P.type.is_variable(F)?w.throw_error(P.error.instantiation(y.indicator)):P.type.is_integer(F)?w.points=[]:w.throw_error(P.error.type("integer",F,y.indicator))},"current_prolog_flag/2":function(w,b,y){var F=y.args[0],z=y.args[1];if(!P.type.is_variable(F)&&!P.type.is_atom(F))w.throw_error(P.error.type("atom",F,y.indicator));else if(!P.type.is_variable(F)&&!P.type.is_flag(F))w.throw_error(P.error.domain("prolog_flag",F,y.indicator));else{var X=[];for(var $ in P.flag)if(P.flag.hasOwnProperty($)){var se=new _(",",[new _("=",[new _($),F]),new _("=",[w.get_flag($),z])]);X.push(new be(b.goal.replace(se),b.substitution,b))}w.prepend(X)}},"set_prolog_flag/2":function(w,b,y){var F=y.args[0],z=y.args[1];P.type.is_variable(F)||P.type.is_variable(z)?w.throw_error(P.error.instantiation(y.indicator)):P.type.is_atom(F)?P.type.is_flag(F)?P.type.is_value_flag(F,z)?P.type.is_modifiable_flag(F)?(w.session.flag[F.id]=z,w.success(b)):w.throw_error(P.error.permission("modify","flag",F)):w.throw_error(P.error.domain("flag_value",new _("+",[F,z]),y.indicator)):w.throw_error(P.error.domain("prolog_flag",F,y.indicator)):w.throw_error(P.error.type("atom",F,y.indicator))}},flag:{bounded:{allowed:[new _("true"),new _("false")],value:new _("true"),changeable:!1},max_integer:{allowed:[new Qe(Number.MAX_SAFE_INTEGER)],value:new Qe(Number.MAX_SAFE_INTEGER),changeable:!1},min_integer:{allowed:[new Qe(Number.MIN_SAFE_INTEGER)],value:new Qe(Number.MIN_SAFE_INTEGER),changeable:!1},integer_rounding_function:{allowed:[new _("down"),new _("toward_zero")],value:new _("toward_zero"),changeable:!1},char_conversion:{allowed:[new _("on"),new _("off")],value:new _("on"),changeable:!0},debug:{allowed:[new _("on"),new _("off")],value:new _("off"),changeable:!0},max_arity:{allowed:[new _("unbounded")],value:new _("unbounded"),changeable:!1},unknown:{allowed:[new _("error"),new _("fail"),new _("warning")],value:new _("error"),changeable:!0},double_quotes:{allowed:[new _("chars"),new _("codes"),new _("atom")],value:new _("codes"),changeable:!0},occurs_check:{allowed:[new _("false"),new _("true")],value:new _("false"),changeable:!0},dialect:{allowed:[new _("tau")],value:new _("tau"),changeable:!1},version_data:{allowed:[new _("tau",[new Qe(e.major,!1),new Qe(e.minor,!1),new Qe(e.patch,!1),new _(e.status)])],value:new _("tau",[new Qe(e.major,!1),new Qe(e.minor,!1),new Qe(e.patch,!1),new _(e.status)]),changeable:!1},nodejs:{allowed:[new _("yes"),new _("no")],value:new _(typeof rc<"u"&&rc.exports?"yes":"no"),changeable:!1}},unify:function(w,b,y){y=y===void 0?!1:y;for(var F=[{left:w,right:b}],z={};F.length!==0;){var X=F.pop();if(w=X.left,b=X.right,P.type.is_term(w)&&P.type.is_term(b)){if(w.indicator!==b.indicator)return null;for(var $=0;$z.value?1:0:z}else return F},operate:function(w,b){if(P.type.is_operator(b)){for(var y=P.type.is_operator(b),F=[],z,X=!1,$=0;$w.get_flag("max_integer").value||z0?w.start+w.matches[0].length:w.start,z=y?new _("token_not_found"):new _("found",[new _(w.value.toString())]),X=new _(".",[new _("line",[new Qe(w.line+1)]),new _(".",[new _("column",[new Qe(F+1)]),new _(".",[z,new _("[]",[])])])]);return new _("error",[new _("syntax_error",[new _(b)]),X])},syntax_by_predicate:function(w,b){return new _("error",[new _("syntax_error",[new _(w)]),Z(b)])}},warning:{singleton:function(w,b,y){for(var F=new _("[]"),z=w.length-1;z>=0;z--)F=new _(".",[new De(w[z]),F]);return new _("warning",[new _("singleton_variables",[F,Z(b)]),new _(".",[new _("line",[new Qe(y,!1)]),new _("[]")])])},failed_goal:function(w,b){return new _("warning",[new _("failed_goal",[w]),new _(".",[new _("line",[new Qe(b,!1)]),new _("[]")])])}},format_variable:function(w){return"_"+w},format_answer:function(w,b,F){b instanceof Re&&(b=b.thread);var F=F||{};if(F.session=b?b.session:void 0,P.type.is_error(w))return"uncaught exception: "+w.args[0].toString();if(w===!1)return"false.";if(w===null)return"limit exceeded ;";var z=0,X="";if(P.type.is_substitution(w)){var $=w.domain(!0);w=w.filter(function(Fe,ut){return!P.type.is_variable(ut)||$.indexOf(ut.id)!==-1&&Fe!==ut.id})}for(var se in w.links)w.links.hasOwnProperty(se)&&(z++,X!==""&&(X+=", "),X+=se.toString(F)+" = "+w.links[se].toString(F));var xe=typeof b>"u"||b.points.length>0?" ;":".";return z===0?"true"+xe:X+xe},flatten_error:function(w){if(!P.type.is_error(w))return null;w=w.args[0];var b={};return b.type=w.args[0].id,b.thrown=b.type==="syntax_error"?null:w.args[1].id,b.expected=null,b.found=null,b.representation=null,b.existence=null,b.existence_type=null,b.line=null,b.column=null,b.permission_operation=null,b.permission_type=null,b.evaluation_type=null,b.type==="type_error"||b.type==="domain_error"?(b.expected=w.args[0].args[0].id,b.found=w.args[0].args[1].toString()):b.type==="syntax_error"?w.args[1].indicator==="./2"?(b.expected=w.args[0].args[0].id,b.found=w.args[1].args[1].args[1].args[0],b.found=b.found.id==="token_not_found"?b.found.id:b.found.args[0].id,b.line=w.args[1].args[0].args[0].value,b.column=w.args[1].args[1].args[0].args[0].value):b.thrown=w.args[1].id:b.type==="permission_error"?(b.found=w.args[0].args[2].toString(),b.permission_operation=w.args[0].args[0].id,b.permission_type=w.args[0].args[1].id):b.type==="evaluation_error"?b.evaluation_type=w.args[0].args[0].id:b.type==="representation_error"?b.representation=w.args[0].args[0].id:b.type==="existence_error"&&(b.existence=w.args[0].args[1].toString(),b.existence_type=w.args[0].args[0].id),b},create:function(w){return new P.type.Session(w)}};typeof rc<"u"?rc.exports=P:window.pl=P})()});function lme(e,t,r){e.prepend(r.map(s=>new wl.default.type.State(t.goal.replace(s),t.substitution,t)))}function r9(e){let t=ume.get(e.session);if(t==null)throw new Error("Assertion failed: A project should have been registered for the active session");return t}function fme(e,t){ume.set(e,t),e.consult(`:- use_module(library(${pat.id})).`)}var wl,cme,Y0,fat,Aat,ume,pat,Ame=Xe(()=>{qe();zl();wl=et(t9()),cme=et(Ie("vm")),{is_atom:Y0,is_variable:fat,is_instantiated_list:Aat}=wl.default.type;ume=new WeakMap;pat=new wl.default.type.Module("constraints",{"project_workspaces_by_descriptor/3":(e,t,r)=>{let[s,a,n]=r.args;if(!Y0(s)||!Y0(a)){e.throw_error(wl.default.error.instantiation(r.indicator));return}let c=j.parseIdent(s.id),f=j.makeDescriptor(c,a.id),h=r9(e).tryWorkspaceByDescriptor(f);fat(n)&&h!==null&&lme(e,t,[new wl.default.type.Term("=",[n,new wl.default.type.Term(String(h.relativeCwd))])]),Y0(n)&&h!==null&&h.relativeCwd===n.id&&e.success(t)},"workspace_field/3":(e,t,r)=>{let[s,a,n]=r.args;if(!Y0(s)||!Y0(a)){e.throw_error(wl.default.error.instantiation(r.indicator));return}let f=r9(e).tryWorkspaceByCwd(s.id);if(f==null)return;let p=Pa(f.manifest.raw,a.id);typeof p>"u"||lme(e,t,[new wl.default.type.Term("=",[n,new wl.default.type.Term(typeof p=="object"?JSON.stringify(p):p)])])},"workspace_field_test/3":(e,t,r)=>{let[s,a,n]=r.args;e.prepend([new wl.default.type.State(t.goal.replace(new wl.default.type.Term("workspace_field_test",[s,a,n,new wl.default.type.Term("[]",[])])),t.substitution,t)])},"workspace_field_test/4":(e,t,r)=>{let[s,a,n,c]=r.args;if(!Y0(s)||!Y0(a)||!Y0(n)||!Aat(c)){e.throw_error(wl.default.error.instantiation(r.indicator));return}let p=r9(e).tryWorkspaceByCwd(s.id);if(p==null)return;let h=Pa(p.manifest.raw,a.id);if(typeof h>"u")return;let E={$$:h};for(let[S,x]of c.toJavaScript().entries())E[`$${S}`]=x;cme.default.runInNewContext(n.id,E)&&e.success(t)}},["project_workspaces_by_descriptor/3","workspace_field/3","workspace_field_test/3","workspace_field_test/4"])});var aS={};Vt(aS,{Constraints:()=>i9,DependencyType:()=>gme});function Co(e){if(e instanceof _C.default.type.Num)return e.value;if(e instanceof _C.default.type.Term)switch(e.indicator){case"throw/1":return Co(e.args[0]);case"error/1":return Co(e.args[0]);case"error/2":if(e.args[0]instanceof _C.default.type.Term&&e.args[0].indicator==="syntax_error/1")return Object.assign(Co(e.args[0]),...Co(e.args[1]));{let t=Co(e.args[0]);return t.message+=` (in ${Co(e.args[1])})`,t}case"syntax_error/1":return new Lt(43,`Syntax error: ${Co(e.args[0])}`);case"existence_error/2":return new Lt(44,`Existence error: ${Co(e.args[0])} ${Co(e.args[1])} not found`);case"instantiation_error/0":return new Lt(75,"Instantiation error: an argument is variable when an instantiated argument was expected");case"line/1":return{line:Co(e.args[0])};case"column/1":return{column:Co(e.args[0])};case"found/1":return{found:Co(e.args[0])};case"./2":return[Co(e.args[0])].concat(Co(e.args[1]));case"//2":return`${Co(e.args[0])}/${Co(e.args[1])}`;default:return e.id}throw`couldn't pretty print because of unsupported node ${e}`}function hme(e){let t;try{t=Co(e)}catch(r){throw typeof r=="string"?new Lt(42,`Unknown error: ${e} (note: ${r})`):r}return typeof t.line<"u"&&typeof t.column<"u"&&(t.message+=` at line ${t.line}, column ${t.column}`),t}function Qm(e){return e.id==="null"?null:`${e.toJavaScript()}`}function hat(e){if(e.id==="null")return null;{let t=e.toJavaScript();if(typeof t!="string")return JSON.stringify(t);try{return JSON.stringify(JSON.parse(t))}catch{return JSON.stringify(t)}}}function V0(e){return typeof e=="string"?`'${e}'`:"[]"}var dme,_C,gme,pme,n9,i9,lS=Xe(()=>{qe();qe();Dt();dme=et(Yge()),_C=et(t9());iS();Ame();(0,dme.default)(_C.default);gme=(s=>(s.Dependencies="dependencies",s.DevDependencies="devDependencies",s.PeerDependencies="peerDependencies",s))(gme||{}),pme=["dependencies","devDependencies","peerDependencies"];n9=class{constructor(t,r){let s=1e3*t.workspaces.length;this.session=_C.default.create(s),fme(this.session,t),this.session.consult(":- use_module(library(lists))."),this.session.consult(r)}fetchNextAnswer(){return new Promise(t=>{this.session.answer(r=>{t(r)})})}async*makeQuery(t){let r=this.session.query(t);if(r!==!0)throw hme(r);for(;;){let s=await this.fetchNextAnswer();if(s===null)throw new Lt(79,"Resolution limit exceeded");if(!s)break;if(s.id==="throw")throw hme(s);yield s}}};i9=class e{constructor(t){this.source="";this.project=t;let r=t.configuration.get("constraintsPath");le.existsSync(r)&&(this.source=le.readFileSync(r,"utf8"))}static async find(t){return new e(t)}getProjectDatabase(){let t="";for(let r of pme)t+=`dependency_type(${r}). `;for(let r of this.project.workspacesByCwd.values()){let s=r.relativeCwd;t+=`workspace(${V0(s)}). `,t+=`workspace_ident(${V0(s)}, ${V0(j.stringifyIdent(r.anchoredLocator))}). `,t+=`workspace_version(${V0(s)}, ${V0(r.manifest.version)}). `;for(let a of pme)for(let n of r.manifest[a].values())t+=`workspace_has_dependency(${V0(s)}, ${V0(j.stringifyIdent(n))}, ${V0(n.range)}, ${a}). `}return t+=`workspace(_) :- false. `,t+=`workspace_ident(_, _) :- false. `,t+=`workspace_version(_, _) :- false. `,t+=`workspace_has_dependency(_, _, _, _) :- false. `,t}getDeclarations(){let t="";return t+=`gen_enforced_dependency(_, _, _, _) :- false. `,t+=`gen_enforced_field(_, _, _) :- false. `,t}get fullSource(){return`${this.getProjectDatabase()} ${this.source} ${this.getDeclarations()}`}createSession(){return new n9(this.project,this.fullSource)}async processClassic(){let t=this.createSession();return{enforcedDependencies:await this.genEnforcedDependencies(t),enforcedFields:await this.genEnforcedFields(t)}}async process(){let{enforcedDependencies:t,enforcedFields:r}=await this.processClassic(),s=new Map;for(let{workspace:a,dependencyIdent:n,dependencyRange:c,dependencyType:f}of t){let p=nS([f,j.stringifyIdent(n)]),h=Ge.getMapWithDefault(s,a.cwd);Ge.getMapWithDefault(h,p).set(c??void 0,new Set)}for(let{workspace:a,fieldPath:n,fieldValue:c}of r){let f=nS(n),p=Ge.getMapWithDefault(s,a.cwd);Ge.getMapWithDefault(p,f).set(JSON.parse(c)??void 0,new Set)}return{manifestUpdates:s,reportedErrors:new Map}}async genEnforcedDependencies(t){let r=[];for await(let s of t.makeQuery("workspace(WorkspaceCwd), dependency_type(DependencyType), gen_enforced_dependency(WorkspaceCwd, DependencyIdent, DependencyRange, DependencyType).")){let a=J.resolve(this.project.cwd,Qm(s.links.WorkspaceCwd)),n=Qm(s.links.DependencyIdent),c=Qm(s.links.DependencyRange),f=Qm(s.links.DependencyType);if(a===null||n===null)throw new Error("Invalid rule");let p=this.project.getWorkspaceByCwd(a),h=j.parseIdent(n);r.push({workspace:p,dependencyIdent:h,dependencyRange:c,dependencyType:f})}return Ge.sortMap(r,[({dependencyRange:s})=>s!==null?"0":"1",({workspace:s})=>j.stringifyIdent(s.anchoredLocator),({dependencyIdent:s})=>j.stringifyIdent(s)])}async genEnforcedFields(t){let r=[];for await(let s of t.makeQuery("workspace(WorkspaceCwd), gen_enforced_field(WorkspaceCwd, FieldPath, FieldValue).")){let a=J.resolve(this.project.cwd,Qm(s.links.WorkspaceCwd)),n=Qm(s.links.FieldPath),c=hat(s.links.FieldValue);if(a===null||n===null)throw new Error("Invalid rule");let f=this.project.getWorkspaceByCwd(a);r.push({workspace:f,fieldPath:n,fieldValue:c})}return Ge.sortMap(r,[({workspace:s})=>j.stringifyIdent(s.anchoredLocator),({fieldPath:s})=>s])}async*query(t){let r=this.createSession();for await(let s of r.makeQuery(t)){let a={};for(let[n,c]of Object.entries(s.links))n!=="_"&&(a[n]=Qm(c));yield a}}}});var Sme=G(KT=>{"use strict";Object.defineProperty(KT,"__esModule",{value:!0});function BS(e){let t=[...e.caches],r=t.shift();return r===void 0?vme():{get(s,a,n={miss:()=>Promise.resolve()}){return r.get(s,a,n).catch(()=>BS({caches:t}).get(s,a,n))},set(s,a){return r.set(s,a).catch(()=>BS({caches:t}).set(s,a))},delete(s){return r.delete(s).catch(()=>BS({caches:t}).delete(s))},clear(){return r.clear().catch(()=>BS({caches:t}).clear())}}}function vme(){return{get(e,t,r={miss:()=>Promise.resolve()}){return t().then(a=>Promise.all([a,r.miss(a)])).then(([a])=>a)},set(e,t){return Promise.resolve(t)},delete(e){return Promise.resolve()},clear(){return Promise.resolve()}}}KT.createFallbackableCache=BS;KT.createNullCache=vme});var bme=G((xYt,Dme)=>{Dme.exports=Sme()});var Pme=G(y9=>{"use strict";Object.defineProperty(y9,"__esModule",{value:!0});function Tat(e={serializable:!0}){let t={};return{get(r,s,a={miss:()=>Promise.resolve()}){let n=JSON.stringify(r);if(n in t)return Promise.resolve(e.serializable?JSON.parse(t[n]):t[n]);let c=s(),f=a&&a.miss||(()=>Promise.resolve());return c.then(p=>f(p)).then(()=>c)},set(r,s){return t[JSON.stringify(r)]=e.serializable?JSON.stringify(s):s,Promise.resolve(s)},delete(r){return delete t[JSON.stringify(r)],Promise.resolve()},clear(){return t={},Promise.resolve()}}}y9.createInMemoryCache=Tat});var kme=G((QYt,xme)=>{xme.exports=Pme()});var Rme=G(tf=>{"use strict";Object.defineProperty(tf,"__esModule",{value:!0});function Fat(e,t,r){let s={"x-algolia-api-key":r,"x-algolia-application-id":t};return{headers(){return e===E9.WithinHeaders?s:{}},queryParameters(){return e===E9.WithinQueryParameters?s:{}}}}function Nat(e){let t=0,r=()=>(t++,new Promise(s=>{setTimeout(()=>{s(e(r))},Math.min(100*t,1e3))}));return e(r)}function Qme(e,t=(r,s)=>Promise.resolve()){return Object.assign(e,{wait(r){return Qme(e.then(s=>Promise.all([t(s,r),s])).then(s=>s[1]))}})}function Oat(e){let t=e.length-1;for(t;t>0;t--){let r=Math.floor(Math.random()*(t+1)),s=e[t];e[t]=e[r],e[r]=s}return e}function Lat(e,t){return t&&Object.keys(t).forEach(r=>{e[r]=t[r](e)}),e}function Mat(e,...t){let r=0;return e.replace(/%s/g,()=>encodeURIComponent(t[r++]))}var Uat="4.22.1",_at=e=>()=>e.transporter.requester.destroy(),E9={WithinQueryParameters:0,WithinHeaders:1};tf.AuthMode=E9;tf.addMethods=Lat;tf.createAuth=Fat;tf.createRetryablePromise=Nat;tf.createWaitablePromise=Qme;tf.destroy=_at;tf.encode=Mat;tf.shuffle=Oat;tf.version=Uat});var vS=G((TYt,Tme)=>{Tme.exports=Rme()});var Fme=G(I9=>{"use strict";Object.defineProperty(I9,"__esModule",{value:!0});var Hat={Delete:"DELETE",Get:"GET",Post:"POST",Put:"PUT"};I9.MethodEnum=Hat});var SS=G((NYt,Nme)=>{Nme.exports=Fme()});var zme=G(Yi=>{"use strict";Object.defineProperty(Yi,"__esModule",{value:!0});var Lme=SS();function C9(e,t){let r=e||{},s=r.data||{};return Object.keys(r).forEach(a=>{["timeout","headers","queryParameters","data","cacheable"].indexOf(a)===-1&&(s[a]=r[a])}),{data:Object.entries(s).length>0?s:void 0,timeout:r.timeout||t,headers:r.headers||{},queryParameters:r.queryParameters||{},cacheable:r.cacheable}}var DS={Read:1,Write:2,Any:3},zC={Up:1,Down:2,Timeouted:3},Mme=2*60*1e3;function B9(e,t=zC.Up){return{...e,status:t,lastUpdate:Date.now()}}function Ume(e){return e.status===zC.Up||Date.now()-e.lastUpdate>Mme}function _me(e){return e.status===zC.Timeouted&&Date.now()-e.lastUpdate<=Mme}function v9(e){return typeof e=="string"?{protocol:"https",url:e,accept:DS.Any}:{protocol:e.protocol||"https",url:e.url,accept:e.accept||DS.Any}}function jat(e,t){return Promise.all(t.map(r=>e.get(r,()=>Promise.resolve(B9(r))))).then(r=>{let s=r.filter(f=>Ume(f)),a=r.filter(f=>_me(f)),n=[...s,...a],c=n.length>0?n.map(f=>v9(f)):t;return{getTimeout(f,p){return(a.length===0&&f===0?1:a.length+3+f)*p},statelessHosts:c}})}var Gat=({isTimedOut:e,status:t})=>!e&&~~t===0,qat=e=>{let t=e.status;return e.isTimedOut||Gat(e)||~~(t/100)!==2&&~~(t/100)!==4},Wat=({status:e})=>~~(e/100)===2,Yat=(e,t)=>qat(e)?t.onRetry(e):Wat(e)?t.onSuccess(e):t.onFail(e);function Ome(e,t,r,s){let a=[],n=Wme(r,s),c=Yme(e,s),f=r.method,p=r.method!==Lme.MethodEnum.Get?{}:{...r.data,...s.data},h={"x-algolia-agent":e.userAgent.value,...e.queryParameters,...p,...s.queryParameters},E=0,C=(S,x)=>{let I=S.pop();if(I===void 0)throw Kme(w9(a));let T={data:n,headers:c,method:f,url:Gme(I,r.path,h),connectTimeout:x(E,e.timeouts.connect),responseTimeout:x(E,s.timeout)},O=V=>{let te={request:T,response:V,host:I,triesLeft:S.length};return a.push(te),te},U={onSuccess:V=>Hme(V),onRetry(V){let te=O(V);return V.isTimedOut&&E++,Promise.all([e.logger.info("Retryable failure",S9(te)),e.hostsCache.set(I,B9(I,V.isTimedOut?zC.Timeouted:zC.Down))]).then(()=>C(S,x))},onFail(V){throw O(V),jme(V,w9(a))}};return e.requester.send(T).then(V=>Yat(V,U))};return jat(e.hostsCache,t).then(S=>C([...S.statelessHosts].reverse(),S.getTimeout))}function Vat(e){let{hostsCache:t,logger:r,requester:s,requestsCache:a,responsesCache:n,timeouts:c,userAgent:f,hosts:p,queryParameters:h,headers:E}=e,C={hostsCache:t,logger:r,requester:s,requestsCache:a,responsesCache:n,timeouts:c,userAgent:f,headers:E,queryParameters:h,hosts:p.map(S=>v9(S)),read(S,x){let I=C9(x,C.timeouts.read),T=()=>Ome(C,C.hosts.filter(V=>(V.accept&DS.Read)!==0),S,I);if((I.cacheable!==void 0?I.cacheable:S.cacheable)!==!0)return T();let U={request:S,mappedRequestOptions:I,transporter:{queryParameters:C.queryParameters,headers:C.headers}};return C.responsesCache.get(U,()=>C.requestsCache.get(U,()=>C.requestsCache.set(U,T()).then(V=>Promise.all([C.requestsCache.delete(U),V]),V=>Promise.all([C.requestsCache.delete(U),Promise.reject(V)])).then(([V,te])=>te)),{miss:V=>C.responsesCache.set(U,V)})},write(S,x){return Ome(C,C.hosts.filter(I=>(I.accept&DS.Write)!==0),S,C9(x,C.timeouts.write))}};return C}function Jat(e){let t={value:`Algolia for JavaScript (${e})`,add(r){let s=`; ${r.segment}${r.version!==void 0?` (${r.version})`:""}`;return t.value.indexOf(s)===-1&&(t.value=`${t.value}${s}`),t}};return t}function Hme(e){try{return JSON.parse(e.content)}catch(t){throw Jme(t.message,e)}}function jme({content:e,status:t},r){let s=e;try{s=JSON.parse(e).message}catch{}return Vme(s,t,r)}function Kat(e,...t){let r=0;return e.replace(/%s/g,()=>encodeURIComponent(t[r++]))}function Gme(e,t,r){let s=qme(r),a=`${e.protocol}://${e.url}/${t.charAt(0)==="/"?t.substr(1):t}`;return s.length&&(a+=`?${s}`),a}function qme(e){let t=r=>Object.prototype.toString.call(r)==="[object Object]"||Object.prototype.toString.call(r)==="[object Array]";return Object.keys(e).map(r=>Kat("%s=%s",r,t(e[r])?JSON.stringify(e[r]):e[r])).join("&")}function Wme(e,t){if(e.method===Lme.MethodEnum.Get||e.data===void 0&&t.data===void 0)return;let r=Array.isArray(e.data)?e.data:{...e.data,...t.data};return JSON.stringify(r)}function Yme(e,t){let r={...e.headers,...t.headers},s={};return Object.keys(r).forEach(a=>{let n=r[a];s[a.toLowerCase()]=n}),s}function w9(e){return e.map(t=>S9(t))}function S9(e){let t=e.request.headers["x-algolia-api-key"]?{"x-algolia-api-key":"*****"}:{};return{...e,request:{...e.request,headers:{...e.request.headers,...t}}}}function Vme(e,t,r){return{name:"ApiError",message:e,status:t,transporterStackTrace:r}}function Jme(e,t){return{name:"DeserializationError",message:e,response:t}}function Kme(e){return{name:"RetryError",message:"Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.",transporterStackTrace:e}}Yi.CallEnum=DS;Yi.HostStatusEnum=zC;Yi.createApiError=Vme;Yi.createDeserializationError=Jme;Yi.createMappedRequestOptions=C9;Yi.createRetryError=Kme;Yi.createStatefulHost=B9;Yi.createStatelessHost=v9;Yi.createTransporter=Vat;Yi.createUserAgent=Jat;Yi.deserializeFailure=jme;Yi.deserializeSuccess=Hme;Yi.isStatefulHostTimeouted=_me;Yi.isStatefulHostUp=Ume;Yi.serializeData=Wme;Yi.serializeHeaders=Yme;Yi.serializeQueryParameters=qme;Yi.serializeUrl=Gme;Yi.stackFrameWithoutCredentials=S9;Yi.stackTraceWithoutCredentials=w9});var bS=G((LYt,Xme)=>{Xme.exports=zme()});var Zme=G(K0=>{"use strict";Object.defineProperty(K0,"__esModule",{value:!0});var XC=vS(),zat=bS(),PS=SS(),Xat=e=>{let t=e.region||"us",r=XC.createAuth(XC.AuthMode.WithinHeaders,e.appId,e.apiKey),s=zat.createTransporter({hosts:[{url:`analytics.${t}.algolia.com`}],...e,headers:{...r.headers(),"content-type":"application/json",...e.headers},queryParameters:{...r.queryParameters(),...e.queryParameters}}),a=e.appId;return XC.addMethods({appId:a,transporter:s},e.methods)},Zat=e=>(t,r)=>e.transporter.write({method:PS.MethodEnum.Post,path:"2/abtests",data:t},r),$at=e=>(t,r)=>e.transporter.write({method:PS.MethodEnum.Delete,path:XC.encode("2/abtests/%s",t)},r),elt=e=>(t,r)=>e.transporter.read({method:PS.MethodEnum.Get,path:XC.encode("2/abtests/%s",t)},r),tlt=e=>t=>e.transporter.read({method:PS.MethodEnum.Get,path:"2/abtests"},t),rlt=e=>(t,r)=>e.transporter.write({method:PS.MethodEnum.Post,path:XC.encode("2/abtests/%s/stop",t)},r);K0.addABTest=Zat;K0.createAnalyticsClient=Xat;K0.deleteABTest=$at;K0.getABTest=elt;K0.getABTests=tlt;K0.stopABTest=rlt});var eye=G((UYt,$me)=>{$me.exports=Zme()});var rye=G(xS=>{"use strict";Object.defineProperty(xS,"__esModule",{value:!0});var D9=vS(),nlt=bS(),tye=SS(),ilt=e=>{let t=e.region||"us",r=D9.createAuth(D9.AuthMode.WithinHeaders,e.appId,e.apiKey),s=nlt.createTransporter({hosts:[{url:`personalization.${t}.algolia.com`}],...e,headers:{...r.headers(),"content-type":"application/json",...e.headers},queryParameters:{...r.queryParameters(),...e.queryParameters}});return D9.addMethods({appId:e.appId,transporter:s},e.methods)},slt=e=>t=>e.transporter.read({method:tye.MethodEnum.Get,path:"1/strategies/personalization"},t),olt=e=>(t,r)=>e.transporter.write({method:tye.MethodEnum.Post,path:"1/strategies/personalization",data:t},r);xS.createPersonalizationClient=ilt;xS.getPersonalizationStrategy=slt;xS.setPersonalizationStrategy=olt});var iye=G((HYt,nye)=>{nye.exports=rye()});var yye=G(Ft=>{"use strict";Object.defineProperty(Ft,"__esModule",{value:!0});var Jt=vS(),Bl=bS(),br=SS(),alt=Ie("crypto");function zT(e){let t=r=>e.request(r).then(s=>{if(e.batch!==void 0&&e.batch(s.hits),!e.shouldStop(s))return s.cursor?t({cursor:s.cursor}):t({page:(r.page||0)+1})});return t({})}var llt=e=>{let t=e.appId,r=Jt.createAuth(e.authMode!==void 0?e.authMode:Jt.AuthMode.WithinHeaders,t,e.apiKey),s=Bl.createTransporter({hosts:[{url:`${t}-dsn.algolia.net`,accept:Bl.CallEnum.Read},{url:`${t}.algolia.net`,accept:Bl.CallEnum.Write}].concat(Jt.shuffle([{url:`${t}-1.algolianet.com`},{url:`${t}-2.algolianet.com`},{url:`${t}-3.algolianet.com`}])),...e,headers:{...r.headers(),"content-type":"application/x-www-form-urlencoded",...e.headers},queryParameters:{...r.queryParameters(),...e.queryParameters}}),a={transporter:s,appId:t,addAlgoliaAgent(n,c){s.userAgent.add({segment:n,version:c})},clearCache(){return Promise.all([s.requestsCache.clear(),s.responsesCache.clear()]).then(()=>{})}};return Jt.addMethods(a,e.methods)};function sye(){return{name:"MissingObjectIDError",message:"All objects must have an unique objectID (like a primary key) to be valid. Algolia is also able to generate objectIDs automatically but *it's not recommended*. To do it, use the `{'autoGenerateObjectIDIfNotExist': true}` option."}}function oye(){return{name:"ObjectNotFoundError",message:"Object not found."}}function aye(){return{name:"ValidUntilNotFoundError",message:"ValidUntil not found in given secured api key."}}var clt=e=>(t,r)=>{let{queryParameters:s,...a}=r||{},n={acl:t,...s!==void 0?{queryParameters:s}:{}},c=(f,p)=>Jt.createRetryablePromise(h=>kS(e)(f.key,p).catch(E=>{if(E.status!==404)throw E;return h()}));return Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:"1/keys",data:n},a),c)},ult=e=>(t,r,s)=>{let a=Bl.createMappedRequestOptions(s);return a.queryParameters["X-Algolia-User-ID"]=t,e.transporter.write({method:br.MethodEnum.Post,path:"1/clusters/mapping",data:{cluster:r}},a)},flt=e=>(t,r,s)=>e.transporter.write({method:br.MethodEnum.Post,path:"1/clusters/mapping/batch",data:{users:t,cluster:r}},s),Alt=e=>(t,r)=>Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode("/1/dictionaries/%s/batch",t),data:{clearExistingDictionaryEntries:!0,requests:{action:"addEntry",body:[]}}},r),(s,a)=>ZC(e)(s.taskID,a)),XT=e=>(t,r,s)=>{let a=(n,c)=>QS(e)(t,{methods:{waitTask:ms}}).waitTask(n.taskID,c);return Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode("1/indexes/%s/operation",t),data:{operation:"copy",destination:r}},s),a)},plt=e=>(t,r,s)=>XT(e)(t,r,{...s,scope:[$T.Rules]}),hlt=e=>(t,r,s)=>XT(e)(t,r,{...s,scope:[$T.Settings]}),dlt=e=>(t,r,s)=>XT(e)(t,r,{...s,scope:[$T.Synonyms]}),glt=e=>(t,r)=>t.method===br.MethodEnum.Get?e.transporter.read(t,r):e.transporter.write(t,r),mlt=e=>(t,r)=>{let s=(a,n)=>Jt.createRetryablePromise(c=>kS(e)(t,n).then(c).catch(f=>{if(f.status!==404)throw f}));return Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Delete,path:Jt.encode("1/keys/%s",t)},r),s)},ylt=e=>(t,r,s)=>{let a=r.map(n=>({action:"deleteEntry",body:{objectID:n}}));return Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode("/1/dictionaries/%s/batch",t),data:{clearExistingDictionaryEntries:!1,requests:a}},s),(n,c)=>ZC(e)(n.taskID,c))},Elt=()=>(e,t)=>{let r=Bl.serializeQueryParameters(t),s=alt.createHmac("sha256",e).update(r).digest("hex");return Buffer.from(s+r).toString("base64")},kS=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Get,path:Jt.encode("1/keys/%s",t)},r),lye=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Get,path:Jt.encode("1/task/%s",t.toString())},r),Ilt=e=>t=>e.transporter.read({method:br.MethodEnum.Get,path:"/1/dictionaries/*/settings"},t),Clt=e=>t=>e.transporter.read({method:br.MethodEnum.Get,path:"1/logs"},t),wlt=()=>e=>{let t=Buffer.from(e,"base64").toString("ascii"),r=/validUntil=(\d+)/,s=t.match(r);if(s===null)throw aye();return parseInt(s[1],10)-Math.round(new Date().getTime()/1e3)},Blt=e=>t=>e.transporter.read({method:br.MethodEnum.Get,path:"1/clusters/mapping/top"},t),vlt=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Get,path:Jt.encode("1/clusters/mapping/%s",t)},r),Slt=e=>t=>{let{retrieveMappings:r,...s}=t||{};return r===!0&&(s.getClusters=!0),e.transporter.read({method:br.MethodEnum.Get,path:"1/clusters/mapping/pending"},s)},QS=e=>(t,r={})=>{let s={transporter:e.transporter,appId:e.appId,indexName:t};return Jt.addMethods(s,r.methods)},Dlt=e=>t=>e.transporter.read({method:br.MethodEnum.Get,path:"1/keys"},t),blt=e=>t=>e.transporter.read({method:br.MethodEnum.Get,path:"1/clusters"},t),Plt=e=>t=>e.transporter.read({method:br.MethodEnum.Get,path:"1/indexes"},t),xlt=e=>t=>e.transporter.read({method:br.MethodEnum.Get,path:"1/clusters/mapping"},t),klt=e=>(t,r,s)=>{let a=(n,c)=>QS(e)(t,{methods:{waitTask:ms}}).waitTask(n.taskID,c);return Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode("1/indexes/%s/operation",t),data:{operation:"move",destination:r}},s),a)},Qlt=e=>(t,r)=>{let s=(a,n)=>Promise.all(Object.keys(a.taskID).map(c=>QS(e)(c,{methods:{waitTask:ms}}).waitTask(a.taskID[c],n)));return Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:"1/indexes/*/batch",data:{requests:t}},r),s)},Rlt=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Post,path:"1/indexes/*/objects",data:{requests:t}},r),Tlt=e=>(t,r)=>{let s=t.map(a=>({...a,params:Bl.serializeQueryParameters(a.params||{})}));return e.transporter.read({method:br.MethodEnum.Post,path:"1/indexes/*/queries",data:{requests:s},cacheable:!0},r)},Flt=e=>(t,r)=>Promise.all(t.map(s=>{let{facetName:a,facetQuery:n,...c}=s.params;return QS(e)(s.indexName,{methods:{searchForFacetValues:dye}}).searchForFacetValues(a,n,{...r,...c})})),Nlt=e=>(t,r)=>{let s=Bl.createMappedRequestOptions(r);return s.queryParameters["X-Algolia-User-ID"]=t,e.transporter.write({method:br.MethodEnum.Delete,path:"1/clusters/mapping"},s)},Olt=e=>(t,r,s)=>{let a=r.map(n=>({action:"addEntry",body:n}));return Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode("/1/dictionaries/%s/batch",t),data:{clearExistingDictionaryEntries:!0,requests:a}},s),(n,c)=>ZC(e)(n.taskID,c))},Llt=e=>(t,r)=>{let s=(a,n)=>Jt.createRetryablePromise(c=>kS(e)(t,n).catch(f=>{if(f.status!==404)throw f;return c()}));return Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode("1/keys/%s/restore",t)},r),s)},Mlt=e=>(t,r,s)=>{let a=r.map(n=>({action:"addEntry",body:n}));return Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode("/1/dictionaries/%s/batch",t),data:{clearExistingDictionaryEntries:!1,requests:a}},s),(n,c)=>ZC(e)(n.taskID,c))},Ult=e=>(t,r,s)=>e.transporter.read({method:br.MethodEnum.Post,path:Jt.encode("/1/dictionaries/%s/search",t),data:{query:r},cacheable:!0},s),_lt=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Post,path:"1/clusters/mapping/search",data:{query:t}},r),Hlt=e=>(t,r)=>Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Put,path:"/1/dictionaries/*/settings",data:t},r),(s,a)=>ZC(e)(s.taskID,a)),jlt=e=>(t,r)=>{let s=Object.assign({},r),{queryParameters:a,...n}=r||{},c=a?{queryParameters:a}:{},f=["acl","indexes","referers","restrictSources","queryParameters","description","maxQueriesPerIPPerHour","maxHitsPerQuery"],p=E=>Object.keys(s).filter(C=>f.indexOf(C)!==-1).every(C=>{if(Array.isArray(E[C])&&Array.isArray(s[C])){let S=E[C];return S.length===s[C].length&&S.every((x,I)=>x===s[C][I])}else return E[C]===s[C]}),h=(E,C)=>Jt.createRetryablePromise(S=>kS(e)(t,C).then(x=>p(x)?Promise.resolve():S()));return Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Put,path:Jt.encode("1/keys/%s",t),data:c},n),h)},ZC=e=>(t,r)=>Jt.createRetryablePromise(s=>lye(e)(t,r).then(a=>a.status!=="published"?s():void 0)),cye=e=>(t,r)=>{let s=(a,n)=>ms(e)(a.taskID,n);return Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode("1/indexes/%s/batch",e.indexName),data:{requests:t}},r),s)},Glt=e=>t=>zT({shouldStop:r=>r.cursor===void 0,...t,request:r=>e.transporter.read({method:br.MethodEnum.Post,path:Jt.encode("1/indexes/%s/browse",e.indexName),data:r},t)}),qlt=e=>t=>{let r={hitsPerPage:1e3,...t};return zT({shouldStop:s=>s.hits.length({...a,hits:a.hits.map(n=>(delete n._highlightResult,n))}))}})},Wlt=e=>t=>{let r={hitsPerPage:1e3,...t};return zT({shouldStop:s=>s.hits.length({...a,hits:a.hits.map(n=>(delete n._highlightResult,n))}))}})},ZT=e=>(t,r,s)=>{let{batchSize:a,...n}=s||{},c={taskIDs:[],objectIDs:[]},f=(p=0)=>{let h=[],E;for(E=p;E({action:r,body:C})),n).then(C=>(c.objectIDs=c.objectIDs.concat(C.objectIDs),c.taskIDs.push(C.taskID),E++,f(E)))};return Jt.createWaitablePromise(f(),(p,h)=>Promise.all(p.taskIDs.map(E=>ms(e)(E,h))))},Ylt=e=>t=>Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode("1/indexes/%s/clear",e.indexName)},t),(r,s)=>ms(e)(r.taskID,s)),Vlt=e=>t=>{let{forwardToReplicas:r,...s}=t||{},a=Bl.createMappedRequestOptions(s);return r&&(a.queryParameters.forwardToReplicas=1),Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode("1/indexes/%s/rules/clear",e.indexName)},a),(n,c)=>ms(e)(n.taskID,c))},Jlt=e=>t=>{let{forwardToReplicas:r,...s}=t||{},a=Bl.createMappedRequestOptions(s);return r&&(a.queryParameters.forwardToReplicas=1),Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode("1/indexes/%s/synonyms/clear",e.indexName)},a),(n,c)=>ms(e)(n.taskID,c))},Klt=e=>(t,r)=>Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode("1/indexes/%s/deleteByQuery",e.indexName),data:t},r),(s,a)=>ms(e)(s.taskID,a)),zlt=e=>t=>Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Delete,path:Jt.encode("1/indexes/%s",e.indexName)},t),(r,s)=>ms(e)(r.taskID,s)),Xlt=e=>(t,r)=>Jt.createWaitablePromise(uye(e)([t],r).then(s=>({taskID:s.taskIDs[0]})),(s,a)=>ms(e)(s.taskID,a)),uye=e=>(t,r)=>{let s=t.map(a=>({objectID:a}));return ZT(e)(s,Tm.DeleteObject,r)},Zlt=e=>(t,r)=>{let{forwardToReplicas:s,...a}=r||{},n=Bl.createMappedRequestOptions(a);return s&&(n.queryParameters.forwardToReplicas=1),Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Delete,path:Jt.encode("1/indexes/%s/rules/%s",e.indexName,t)},n),(c,f)=>ms(e)(c.taskID,f))},$lt=e=>(t,r)=>{let{forwardToReplicas:s,...a}=r||{},n=Bl.createMappedRequestOptions(a);return s&&(n.queryParameters.forwardToReplicas=1),Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Delete,path:Jt.encode("1/indexes/%s/synonyms/%s",e.indexName,t)},n),(c,f)=>ms(e)(c.taskID,f))},ect=e=>t=>fye(e)(t).then(()=>!0).catch(r=>{if(r.status!==404)throw r;return!1}),tct=e=>(t,r,s)=>e.transporter.read({method:br.MethodEnum.Post,path:Jt.encode("1/answers/%s/prediction",e.indexName),data:{query:t,queryLanguages:r},cacheable:!0},s),rct=e=>(t,r)=>{let{query:s,paginate:a,...n}=r||{},c=0,f=()=>hye(e)(s||"",{...n,page:c}).then(p=>{for(let[h,E]of Object.entries(p.hits))if(t(E))return{object:E,position:parseInt(h,10),page:c};if(c++,a===!1||c>=p.nbPages)throw oye();return f()});return f()},nct=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Get,path:Jt.encode("1/indexes/%s/%s",e.indexName,t)},r),ict=()=>(e,t)=>{for(let[r,s]of Object.entries(e.hits))if(s.objectID===t)return parseInt(r,10);return-1},sct=e=>(t,r)=>{let{attributesToRetrieve:s,...a}=r||{},n=t.map(c=>({indexName:e.indexName,objectID:c,...s?{attributesToRetrieve:s}:{}}));return e.transporter.read({method:br.MethodEnum.Post,path:"1/indexes/*/objects",data:{requests:n}},a)},oct=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Get,path:Jt.encode("1/indexes/%s/rules/%s",e.indexName,t)},r),fye=e=>t=>e.transporter.read({method:br.MethodEnum.Get,path:Jt.encode("1/indexes/%s/settings",e.indexName),data:{getVersion:2}},t),act=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Get,path:Jt.encode("1/indexes/%s/synonyms/%s",e.indexName,t)},r),Aye=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Get,path:Jt.encode("1/indexes/%s/task/%s",e.indexName,t.toString())},r),lct=e=>(t,r)=>Jt.createWaitablePromise(pye(e)([t],r).then(s=>({objectID:s.objectIDs[0],taskID:s.taskIDs[0]})),(s,a)=>ms(e)(s.taskID,a)),pye=e=>(t,r)=>{let{createIfNotExists:s,...a}=r||{},n=s?Tm.PartialUpdateObject:Tm.PartialUpdateObjectNoCreate;return ZT(e)(t,n,a)},cct=e=>(t,r)=>{let{safe:s,autoGenerateObjectIDIfNotExist:a,batchSize:n,...c}=r||{},f=(I,T,O,U)=>Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode("1/indexes/%s/operation",I),data:{operation:O,destination:T}},U),(V,te)=>ms(e)(V.taskID,te)),p=Math.random().toString(36).substring(7),h=`${e.indexName}_tmp_${p}`,E=b9({appId:e.appId,transporter:e.transporter,indexName:h}),C=[],S=f(e.indexName,h,"copy",{...c,scope:["settings","synonyms","rules"]});C.push(S);let x=(s?S.wait(c):S).then(()=>{let I=E(t,{...c,autoGenerateObjectIDIfNotExist:a,batchSize:n});return C.push(I),s?I.wait(c):I}).then(()=>{let I=f(h,e.indexName,"move",c);return C.push(I),s?I.wait(c):I}).then(()=>Promise.all(C)).then(([I,T,O])=>({objectIDs:T.objectIDs,taskIDs:[I.taskID,...T.taskIDs,O.taskID]}));return Jt.createWaitablePromise(x,(I,T)=>Promise.all(C.map(O=>O.wait(T))))},uct=e=>(t,r)=>P9(e)(t,{...r,clearExistingRules:!0}),fct=e=>(t,r)=>x9(e)(t,{...r,clearExistingSynonyms:!0}),Act=e=>(t,r)=>Jt.createWaitablePromise(b9(e)([t],r).then(s=>({objectID:s.objectIDs[0],taskID:s.taskIDs[0]})),(s,a)=>ms(e)(s.taskID,a)),b9=e=>(t,r)=>{let{autoGenerateObjectIDIfNotExist:s,...a}=r||{},n=s?Tm.AddObject:Tm.UpdateObject;if(n===Tm.UpdateObject){for(let c of t)if(c.objectID===void 0)return Jt.createWaitablePromise(Promise.reject(sye()))}return ZT(e)(t,n,a)},pct=e=>(t,r)=>P9(e)([t],r),P9=e=>(t,r)=>{let{forwardToReplicas:s,clearExistingRules:a,...n}=r||{},c=Bl.createMappedRequestOptions(n);return s&&(c.queryParameters.forwardToReplicas=1),a&&(c.queryParameters.clearExistingRules=1),Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode("1/indexes/%s/rules/batch",e.indexName),data:t},c),(f,p)=>ms(e)(f.taskID,p))},hct=e=>(t,r)=>x9(e)([t],r),x9=e=>(t,r)=>{let{forwardToReplicas:s,clearExistingSynonyms:a,replaceExistingSynonyms:n,...c}=r||{},f=Bl.createMappedRequestOptions(c);return s&&(f.queryParameters.forwardToReplicas=1),(n||a)&&(f.queryParameters.replaceExistingSynonyms=1),Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Post,path:Jt.encode("1/indexes/%s/synonyms/batch",e.indexName),data:t},f),(p,h)=>ms(e)(p.taskID,h))},hye=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Post,path:Jt.encode("1/indexes/%s/query",e.indexName),data:{query:t},cacheable:!0},r),dye=e=>(t,r,s)=>e.transporter.read({method:br.MethodEnum.Post,path:Jt.encode("1/indexes/%s/facets/%s/query",e.indexName,t),data:{facetQuery:r},cacheable:!0},s),gye=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Post,path:Jt.encode("1/indexes/%s/rules/search",e.indexName),data:{query:t}},r),mye=e=>(t,r)=>e.transporter.read({method:br.MethodEnum.Post,path:Jt.encode("1/indexes/%s/synonyms/search",e.indexName),data:{query:t}},r),dct=e=>(t,r)=>{let{forwardToReplicas:s,...a}=r||{},n=Bl.createMappedRequestOptions(a);return s&&(n.queryParameters.forwardToReplicas=1),Jt.createWaitablePromise(e.transporter.write({method:br.MethodEnum.Put,path:Jt.encode("1/indexes/%s/settings",e.indexName),data:t},n),(c,f)=>ms(e)(c.taskID,f))},ms=e=>(t,r)=>Jt.createRetryablePromise(s=>Aye(e)(t,r).then(a=>a.status!=="published"?s():void 0)),gct={AddObject:"addObject",Analytics:"analytics",Browser:"browse",DeleteIndex:"deleteIndex",DeleteObject:"deleteObject",EditSettings:"editSettings",Inference:"inference",ListIndexes:"listIndexes",Logs:"logs",Personalization:"personalization",Recommendation:"recommendation",Search:"search",SeeUnretrievableAttributes:"seeUnretrievableAttributes",Settings:"settings",Usage:"usage"},Tm={AddObject:"addObject",UpdateObject:"updateObject",PartialUpdateObject:"partialUpdateObject",PartialUpdateObjectNoCreate:"partialUpdateObjectNoCreate",DeleteObject:"deleteObject",DeleteIndex:"delete",ClearIndex:"clear"},$T={Settings:"settings",Synonyms:"synonyms",Rules:"rules"},mct={None:"none",StopIfEnoughMatches:"stopIfEnoughMatches"},yct={Synonym:"synonym",OneWaySynonym:"oneWaySynonym",AltCorrection1:"altCorrection1",AltCorrection2:"altCorrection2",Placeholder:"placeholder"};Ft.ApiKeyACLEnum=gct;Ft.BatchActionEnum=Tm;Ft.ScopeEnum=$T;Ft.StrategyEnum=mct;Ft.SynonymEnum=yct;Ft.addApiKey=clt;Ft.assignUserID=ult;Ft.assignUserIDs=flt;Ft.batch=cye;Ft.browseObjects=Glt;Ft.browseRules=qlt;Ft.browseSynonyms=Wlt;Ft.chunkedBatch=ZT;Ft.clearDictionaryEntries=Alt;Ft.clearObjects=Ylt;Ft.clearRules=Vlt;Ft.clearSynonyms=Jlt;Ft.copyIndex=XT;Ft.copyRules=plt;Ft.copySettings=hlt;Ft.copySynonyms=dlt;Ft.createBrowsablePromise=zT;Ft.createMissingObjectIDError=sye;Ft.createObjectNotFoundError=oye;Ft.createSearchClient=llt;Ft.createValidUntilNotFoundError=aye;Ft.customRequest=glt;Ft.deleteApiKey=mlt;Ft.deleteBy=Klt;Ft.deleteDictionaryEntries=ylt;Ft.deleteIndex=zlt;Ft.deleteObject=Xlt;Ft.deleteObjects=uye;Ft.deleteRule=Zlt;Ft.deleteSynonym=$lt;Ft.exists=ect;Ft.findAnswers=tct;Ft.findObject=rct;Ft.generateSecuredApiKey=Elt;Ft.getApiKey=kS;Ft.getAppTask=lye;Ft.getDictionarySettings=Ilt;Ft.getLogs=Clt;Ft.getObject=nct;Ft.getObjectPosition=ict;Ft.getObjects=sct;Ft.getRule=oct;Ft.getSecuredApiKeyRemainingValidity=wlt;Ft.getSettings=fye;Ft.getSynonym=act;Ft.getTask=Aye;Ft.getTopUserIDs=Blt;Ft.getUserID=vlt;Ft.hasPendingMappings=Slt;Ft.initIndex=QS;Ft.listApiKeys=Dlt;Ft.listClusters=blt;Ft.listIndices=Plt;Ft.listUserIDs=xlt;Ft.moveIndex=klt;Ft.multipleBatch=Qlt;Ft.multipleGetObjects=Rlt;Ft.multipleQueries=Tlt;Ft.multipleSearchForFacetValues=Flt;Ft.partialUpdateObject=lct;Ft.partialUpdateObjects=pye;Ft.removeUserID=Nlt;Ft.replaceAllObjects=cct;Ft.replaceAllRules=uct;Ft.replaceAllSynonyms=fct;Ft.replaceDictionaryEntries=Olt;Ft.restoreApiKey=Llt;Ft.saveDictionaryEntries=Mlt;Ft.saveObject=Act;Ft.saveObjects=b9;Ft.saveRule=pct;Ft.saveRules=P9;Ft.saveSynonym=hct;Ft.saveSynonyms=x9;Ft.search=hye;Ft.searchDictionaryEntries=Ult;Ft.searchForFacetValues=dye;Ft.searchRules=gye;Ft.searchSynonyms=mye;Ft.searchUserIDs=_lt;Ft.setDictionarySettings=Hlt;Ft.setSettings=dct;Ft.updateApiKey=jlt;Ft.waitAppTask=ZC;Ft.waitTask=ms});var Iye=G((GYt,Eye)=>{Eye.exports=yye()});var Cye=G(eF=>{"use strict";Object.defineProperty(eF,"__esModule",{value:!0});function Ect(){return{debug(e,t){return Promise.resolve()},info(e,t){return Promise.resolve()},error(e,t){return Promise.resolve()}}}var Ict={Debug:1,Info:2,Error:3};eF.LogLevelEnum=Ict;eF.createNullLogger=Ect});var Bye=G((WYt,wye)=>{wye.exports=Cye()});var bye=G(k9=>{"use strict";Object.defineProperty(k9,"__esModule",{value:!0});var vye=Ie("http"),Sye=Ie("https"),Cct=Ie("url"),Dye={keepAlive:!0},wct=new vye.Agent(Dye),Bct=new Sye.Agent(Dye);function vct({agent:e,httpAgent:t,httpsAgent:r,requesterOptions:s={}}={}){let a=t||e||wct,n=r||e||Bct;return{send(c){return new Promise(f=>{let p=Cct.parse(c.url),h=p.query===null?p.pathname:`${p.pathname}?${p.query}`,E={...s,agent:p.protocol==="https:"?n:a,hostname:p.hostname,path:h,method:c.method,headers:{...s&&s.headers?s.headers:{},...c.headers},...p.port!==void 0?{port:p.port||""}:{}},C=(p.protocol==="https:"?Sye:vye).request(E,T=>{let O=[];T.on("data",U=>{O=O.concat(U)}),T.on("end",()=>{clearTimeout(x),clearTimeout(I),f({status:T.statusCode||0,content:Buffer.concat(O).toString(),isTimedOut:!1})})}),S=(T,O)=>setTimeout(()=>{C.abort(),f({status:0,content:O,isTimedOut:!0})},T*1e3),x=S(c.connectTimeout,"Connection timeout"),I;C.on("error",T=>{clearTimeout(x),clearTimeout(I),f({status:0,content:T.message,isTimedOut:!1})}),C.once("response",()=>{clearTimeout(x),I=S(c.responseTimeout,"Socket timeout")}),c.data!==void 0&&C.write(c.data),C.end()})},destroy(){return a.destroy(),n.destroy(),Promise.resolve()}}}k9.createNodeHttpRequester=vct});var xye=G((VYt,Pye)=>{Pye.exports=bye()});var Tye=G((JYt,Rye)=>{"use strict";var kye=bme(),Sct=kme(),$C=eye(),R9=vS(),Q9=iye(),Gt=Iye(),Dct=Bye(),bct=xye(),Pct=bS();function Qye(e,t,r){let s={appId:e,apiKey:t,timeouts:{connect:2,read:5,write:30},requester:bct.createNodeHttpRequester(),logger:Dct.createNullLogger(),responsesCache:kye.createNullCache(),requestsCache:kye.createNullCache(),hostsCache:Sct.createInMemoryCache(),userAgent:Pct.createUserAgent(R9.version).add({segment:"Node.js",version:process.versions.node})},a={...s,...r},n=()=>c=>Q9.createPersonalizationClient({...s,...c,methods:{getPersonalizationStrategy:Q9.getPersonalizationStrategy,setPersonalizationStrategy:Q9.setPersonalizationStrategy}});return Gt.createSearchClient({...a,methods:{search:Gt.multipleQueries,searchForFacetValues:Gt.multipleSearchForFacetValues,multipleBatch:Gt.multipleBatch,multipleGetObjects:Gt.multipleGetObjects,multipleQueries:Gt.multipleQueries,copyIndex:Gt.copyIndex,copySettings:Gt.copySettings,copyRules:Gt.copyRules,copySynonyms:Gt.copySynonyms,moveIndex:Gt.moveIndex,listIndices:Gt.listIndices,getLogs:Gt.getLogs,listClusters:Gt.listClusters,multipleSearchForFacetValues:Gt.multipleSearchForFacetValues,getApiKey:Gt.getApiKey,addApiKey:Gt.addApiKey,listApiKeys:Gt.listApiKeys,updateApiKey:Gt.updateApiKey,deleteApiKey:Gt.deleteApiKey,restoreApiKey:Gt.restoreApiKey,assignUserID:Gt.assignUserID,assignUserIDs:Gt.assignUserIDs,getUserID:Gt.getUserID,searchUserIDs:Gt.searchUserIDs,listUserIDs:Gt.listUserIDs,getTopUserIDs:Gt.getTopUserIDs,removeUserID:Gt.removeUserID,hasPendingMappings:Gt.hasPendingMappings,generateSecuredApiKey:Gt.generateSecuredApiKey,getSecuredApiKeyRemainingValidity:Gt.getSecuredApiKeyRemainingValidity,destroy:R9.destroy,clearDictionaryEntries:Gt.clearDictionaryEntries,deleteDictionaryEntries:Gt.deleteDictionaryEntries,getDictionarySettings:Gt.getDictionarySettings,getAppTask:Gt.getAppTask,replaceDictionaryEntries:Gt.replaceDictionaryEntries,saveDictionaryEntries:Gt.saveDictionaryEntries,searchDictionaryEntries:Gt.searchDictionaryEntries,setDictionarySettings:Gt.setDictionarySettings,waitAppTask:Gt.waitAppTask,customRequest:Gt.customRequest,initIndex:c=>f=>Gt.initIndex(c)(f,{methods:{batch:Gt.batch,delete:Gt.deleteIndex,findAnswers:Gt.findAnswers,getObject:Gt.getObject,getObjects:Gt.getObjects,saveObject:Gt.saveObject,saveObjects:Gt.saveObjects,search:Gt.search,searchForFacetValues:Gt.searchForFacetValues,waitTask:Gt.waitTask,setSettings:Gt.setSettings,getSettings:Gt.getSettings,partialUpdateObject:Gt.partialUpdateObject,partialUpdateObjects:Gt.partialUpdateObjects,deleteObject:Gt.deleteObject,deleteObjects:Gt.deleteObjects,deleteBy:Gt.deleteBy,clearObjects:Gt.clearObjects,browseObjects:Gt.browseObjects,getObjectPosition:Gt.getObjectPosition,findObject:Gt.findObject,exists:Gt.exists,saveSynonym:Gt.saveSynonym,saveSynonyms:Gt.saveSynonyms,getSynonym:Gt.getSynonym,searchSynonyms:Gt.searchSynonyms,browseSynonyms:Gt.browseSynonyms,deleteSynonym:Gt.deleteSynonym,clearSynonyms:Gt.clearSynonyms,replaceAllObjects:Gt.replaceAllObjects,replaceAllSynonyms:Gt.replaceAllSynonyms,searchRules:Gt.searchRules,getRule:Gt.getRule,deleteRule:Gt.deleteRule,saveRule:Gt.saveRule,saveRules:Gt.saveRules,replaceAllRules:Gt.replaceAllRules,browseRules:Gt.browseRules,clearRules:Gt.clearRules}}),initAnalytics:()=>c=>$C.createAnalyticsClient({...s,...c,methods:{addABTest:$C.addABTest,getABTest:$C.getABTest,getABTests:$C.getABTests,stopABTest:$C.stopABTest,deleteABTest:$C.deleteABTest}}),initPersonalization:n,initRecommendation:()=>c=>(a.logger.info("The `initRecommendation` method is deprecated. Use `initPersonalization` instead."),n()(c))}})}Qye.version=R9.version;Rye.exports=Qye});var F9=G((KYt,T9)=>{var Fye=Tye();T9.exports=Fye;T9.exports.default=Fye});var L9=G((XYt,Lye)=>{"use strict";var Oye=Object.getOwnPropertySymbols,kct=Object.prototype.hasOwnProperty,Qct=Object.prototype.propertyIsEnumerable;function Rct(e){if(e==null)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}function Tct(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de",Object.getOwnPropertyNames(e)[0]==="5")return!1;for(var t={},r=0;r<10;r++)t["_"+String.fromCharCode(r)]=r;var s=Object.getOwnPropertyNames(t).map(function(n){return t[n]});if(s.join("")!=="0123456789")return!1;var a={};return"abcdefghijklmnopqrst".split("").forEach(function(n){a[n]=n}),Object.keys(Object.assign({},a)).join("")==="abcdefghijklmnopqrst"}catch{return!1}}Lye.exports=Tct()?Object.assign:function(e,t){for(var r,s=Rct(e),a,n=1;n{"use strict";var U9=L9(),ew=60103,_ye=60106;Pn.Fragment=60107;Pn.StrictMode=60108;Pn.Profiler=60114;var Hye=60109,jye=60110,Gye=60112;Pn.Suspense=60113;var qye=60115,Wye=60116;typeof Symbol=="function"&&Symbol.for&&(jc=Symbol.for,ew=jc("react.element"),_ye=jc("react.portal"),Pn.Fragment=jc("react.fragment"),Pn.StrictMode=jc("react.strict_mode"),Pn.Profiler=jc("react.profiler"),Hye=jc("react.provider"),jye=jc("react.context"),Gye=jc("react.forward_ref"),Pn.Suspense=jc("react.suspense"),qye=jc("react.memo"),Wye=jc("react.lazy"));var jc,Mye=typeof Symbol=="function"&&Symbol.iterator;function Fct(e){return e===null||typeof e!="object"?null:(e=Mye&&e[Mye]||e["@@iterator"],typeof e=="function"?e:null)}function RS(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,r=1;r{"use strict";eEe.exports=$ye()});var nF=G((eVt,tEe)=>{function Uct(e){var t=typeof e;return e!=null&&(t=="object"||t=="function")}tEe.exports=Uct});var nEe=G((tVt,rEe)=>{var _ct=typeof global=="object"&&global&&global.Object===Object&&global;rEe.exports=_ct});var q9=G((rVt,iEe)=>{var Hct=nEe(),jct=typeof self=="object"&&self&&self.Object===Object&&self,Gct=Hct||jct||Function("return this")();iEe.exports=Gct});var oEe=G((nVt,sEe)=>{var qct=q9(),Wct=function(){return qct.Date.now()};sEe.exports=Wct});var lEe=G((iVt,aEe)=>{var Yct=/\s/;function Vct(e){for(var t=e.length;t--&&Yct.test(e.charAt(t)););return t}aEe.exports=Vct});var uEe=G((sVt,cEe)=>{var Jct=lEe(),Kct=/^\s+/;function zct(e){return e&&e.slice(0,Jct(e)+1).replace(Kct,"")}cEe.exports=zct});var W9=G((oVt,fEe)=>{var Xct=q9(),Zct=Xct.Symbol;fEe.exports=Zct});var dEe=G((aVt,hEe)=>{var AEe=W9(),pEe=Object.prototype,$ct=pEe.hasOwnProperty,eut=pEe.toString,TS=AEe?AEe.toStringTag:void 0;function tut(e){var t=$ct.call(e,TS),r=e[TS];try{e[TS]=void 0;var s=!0}catch{}var a=eut.call(e);return s&&(t?e[TS]=r:delete e[TS]),a}hEe.exports=tut});var mEe=G((lVt,gEe)=>{var rut=Object.prototype,nut=rut.toString;function iut(e){return nut.call(e)}gEe.exports=iut});var CEe=G((cVt,IEe)=>{var yEe=W9(),sut=dEe(),out=mEe(),aut="[object Null]",lut="[object Undefined]",EEe=yEe?yEe.toStringTag:void 0;function cut(e){return e==null?e===void 0?lut:aut:EEe&&EEe in Object(e)?sut(e):out(e)}IEe.exports=cut});var BEe=G((uVt,wEe)=>{function uut(e){return e!=null&&typeof e=="object"}wEe.exports=uut});var SEe=G((fVt,vEe)=>{var fut=CEe(),Aut=BEe(),put="[object Symbol]";function hut(e){return typeof e=="symbol"||Aut(e)&&fut(e)==put}vEe.exports=hut});var xEe=G((AVt,PEe)=>{var dut=uEe(),DEe=nF(),gut=SEe(),bEe=NaN,mut=/^[-+]0x[0-9a-f]+$/i,yut=/^0b[01]+$/i,Eut=/^0o[0-7]+$/i,Iut=parseInt;function Cut(e){if(typeof e=="number")return e;if(gut(e))return bEe;if(DEe(e)){var t=typeof e.valueOf=="function"?e.valueOf():e;e=DEe(t)?t+"":t}if(typeof e!="string")return e===0?e:+e;e=dut(e);var r=yut.test(e);return r||Eut.test(e)?Iut(e.slice(2),r?2:8):mut.test(e)?bEe:+e}PEe.exports=Cut});var REe=G((pVt,QEe)=>{var wut=nF(),Y9=oEe(),kEe=xEe(),But="Expected a function",vut=Math.max,Sut=Math.min;function Dut(e,t,r){var s,a,n,c,f,p,h=0,E=!1,C=!1,S=!0;if(typeof e!="function")throw new TypeError(But);t=kEe(t)||0,wut(r)&&(E=!!r.leading,C="maxWait"in r,n=C?vut(kEe(r.maxWait)||0,t):n,S="trailing"in r?!!r.trailing:S);function x(ae){var ge=s,Ae=a;return s=a=void 0,h=ae,c=e.apply(Ae,ge),c}function I(ae){return h=ae,f=setTimeout(U,t),E?x(ae):c}function T(ae){var ge=ae-p,Ae=ae-h,Ce=t-ge;return C?Sut(Ce,n-Ae):Ce}function O(ae){var ge=ae-p,Ae=ae-h;return p===void 0||ge>=t||ge<0||C&&Ae>=n}function U(){var ae=Y9();if(O(ae))return V(ae);f=setTimeout(U,T(ae))}function V(ae){return f=void 0,S&&s?x(ae):(s=a=void 0,c)}function te(){f!==void 0&&clearTimeout(f),h=0,s=p=a=f=void 0}function ie(){return f===void 0?c:V(Y9())}function ue(){var ae=Y9(),ge=O(ae);if(s=arguments,a=this,p=ae,ge){if(f===void 0)return I(p);if(C)return clearTimeout(f),f=setTimeout(U,t),x(p)}return f===void 0&&(f=setTimeout(U,t)),c}return ue.cancel=te,ue.flush=ie,ue}QEe.exports=Dut});var FEe=G((hVt,TEe)=>{var but=REe(),Put=nF(),xut="Expected a function";function kut(e,t,r){var s=!0,a=!0;if(typeof e!="function")throw new TypeError(xut);return Put(r)&&(s="leading"in r?!!r.leading:s,a="trailing"in r?!!r.trailing:a),but(e,t,{leading:s,maxWait:t,trailing:a})}TEe.exports=kut});var J9=G((dVt,V9)=>{"use strict";var Bn=V9.exports;V9.exports.default=Bn;var $n="\x1B[",NS="\x1B]",rw="\x07",iF=";",NEe=process.env.TERM_PROGRAM==="Apple_Terminal";Bn.cursorTo=(e,t)=>{if(typeof e!="number")throw new TypeError("The `x` argument is required");return typeof t!="number"?$n+(e+1)+"G":$n+(t+1)+";"+(e+1)+"H"};Bn.cursorMove=(e,t)=>{if(typeof e!="number")throw new TypeError("The `x` argument is required");let r="";return e<0?r+=$n+-e+"D":e>0&&(r+=$n+e+"C"),t<0?r+=$n+-t+"A":t>0&&(r+=$n+t+"B"),r};Bn.cursorUp=(e=1)=>$n+e+"A";Bn.cursorDown=(e=1)=>$n+e+"B";Bn.cursorForward=(e=1)=>$n+e+"C";Bn.cursorBackward=(e=1)=>$n+e+"D";Bn.cursorLeft=$n+"G";Bn.cursorSavePosition=NEe?"\x1B7":$n+"s";Bn.cursorRestorePosition=NEe?"\x1B8":$n+"u";Bn.cursorGetPosition=$n+"6n";Bn.cursorNextLine=$n+"E";Bn.cursorPrevLine=$n+"F";Bn.cursorHide=$n+"?25l";Bn.cursorShow=$n+"?25h";Bn.eraseLines=e=>{let t="";for(let r=0;r[NS,"8",iF,iF,t,rw,e,NS,"8",iF,iF,rw].join("");Bn.image=(e,t={})=>{let r=`${NS}1337;File=inline=1`;return t.width&&(r+=`;width=${t.width}`),t.height&&(r+=`;height=${t.height}`),t.preserveAspectRatio===!1&&(r+=";preserveAspectRatio=0"),r+":"+e.toString("base64")+rw};Bn.iTerm={setCwd:(e=process.cwd())=>`${NS}50;CurrentDir=${e}${rw}`,annotation:(e,t={})=>{let r=`${NS}1337;`,s=typeof t.x<"u",a=typeof t.y<"u";if((s||a)&&!(s&&a&&typeof t.length<"u"))throw new Error("`x`, `y` and `length` must be defined when `x` or `y` is defined");return e=e.replace(/\|/g,""),r+=t.isHidden?"AddHiddenAnnotation=":"AddAnnotation=",t.length>0?r+=(s?[e,t.length,t.x,t.y]:[t.length,e]).join("|"):r+=e,r+rw}}});var LEe=G((gVt,K9)=>{"use strict";var OEe=(e,t)=>{for(let r of Reflect.ownKeys(t))Object.defineProperty(e,r,Object.getOwnPropertyDescriptor(t,r));return e};K9.exports=OEe;K9.exports.default=OEe});var UEe=G((mVt,oF)=>{"use strict";var Qut=LEe(),sF=new WeakMap,MEe=(e,t={})=>{if(typeof e!="function")throw new TypeError("Expected a function");let r,s=0,a=e.displayName||e.name||"",n=function(...c){if(sF.set(n,++s),s===1)r=e.apply(this,c),e=null;else if(t.throw===!0)throw new Error(`Function \`${a}\` can only be called once`);return r};return Qut(n,e),sF.set(n,s),n};oF.exports=MEe;oF.exports.default=MEe;oF.exports.callCount=e=>{if(!sF.has(e))throw new Error(`The given function \`${e.name}\` is not wrapped by the \`onetime\` package`);return sF.get(e)}});var _Ee=G((yVt,aF)=>{aF.exports=["SIGABRT","SIGALRM","SIGHUP","SIGINT","SIGTERM"];process.platform!=="win32"&&aF.exports.push("SIGVTALRM","SIGXCPU","SIGXFSZ","SIGUSR2","SIGTRAP","SIGSYS","SIGQUIT","SIGIOT");process.platform==="linux"&&aF.exports.push("SIGIO","SIGPOLL","SIGPWR","SIGSTKFLT","SIGUNUSED")});var Z9=G((EVt,sw)=>{var Qi=global.process,Fm=function(e){return e&&typeof e=="object"&&typeof e.removeListener=="function"&&typeof e.emit=="function"&&typeof e.reallyExit=="function"&&typeof e.listeners=="function"&&typeof e.kill=="function"&&typeof e.pid=="number"&&typeof e.on=="function"};Fm(Qi)?(HEe=Ie("assert"),nw=_Ee(),jEe=/^win/i.test(Qi.platform),OS=Ie("events"),typeof OS!="function"&&(OS=OS.EventEmitter),Qi.__signal_exit_emitter__?eo=Qi.__signal_exit_emitter__:(eo=Qi.__signal_exit_emitter__=new OS,eo.count=0,eo.emitted={}),eo.infinite||(eo.setMaxListeners(1/0),eo.infinite=!0),sw.exports=function(e,t){if(!Fm(global.process))return function(){};HEe.equal(typeof e,"function","a callback must be provided for exit handler"),iw===!1&&z9();var r="exit";t&&t.alwaysLast&&(r="afterexit");var s=function(){eo.removeListener(r,e),eo.listeners("exit").length===0&&eo.listeners("afterexit").length===0&&lF()};return eo.on(r,e),s},lF=function(){!iw||!Fm(global.process)||(iw=!1,nw.forEach(function(t){try{Qi.removeListener(t,cF[t])}catch{}}),Qi.emit=uF,Qi.reallyExit=X9,eo.count-=1)},sw.exports.unload=lF,Nm=function(t,r,s){eo.emitted[t]||(eo.emitted[t]=!0,eo.emit(t,r,s))},cF={},nw.forEach(function(e){cF[e]=function(){if(Fm(global.process)){var r=Qi.listeners(e);r.length===eo.count&&(lF(),Nm("exit",null,e),Nm("afterexit",null,e),jEe&&e==="SIGHUP"&&(e="SIGINT"),Qi.kill(Qi.pid,e))}}}),sw.exports.signals=function(){return nw},iw=!1,z9=function(){iw||!Fm(global.process)||(iw=!0,eo.count+=1,nw=nw.filter(function(t){try{return Qi.on(t,cF[t]),!0}catch{return!1}}),Qi.emit=qEe,Qi.reallyExit=GEe)},sw.exports.load=z9,X9=Qi.reallyExit,GEe=function(t){Fm(global.process)&&(Qi.exitCode=t||0,Nm("exit",Qi.exitCode,null),Nm("afterexit",Qi.exitCode,null),X9.call(Qi,Qi.exitCode))},uF=Qi.emit,qEe=function(t,r){if(t==="exit"&&Fm(global.process)){r!==void 0&&(Qi.exitCode=r);var s=uF.apply(this,arguments);return Nm("exit",Qi.exitCode,null),Nm("afterexit",Qi.exitCode,null),s}else return uF.apply(this,arguments)}):sw.exports=function(){return function(){}};var HEe,nw,jEe,OS,eo,lF,Nm,cF,iw,z9,X9,GEe,uF,qEe});var YEe=G((IVt,WEe)=>{"use strict";var Rut=UEe(),Tut=Z9();WEe.exports=Rut(()=>{Tut(()=>{process.stderr.write("\x1B[?25h")},{alwaysLast:!0})})});var $9=G(ow=>{"use strict";var Fut=YEe(),fF=!1;ow.show=(e=process.stderr)=>{e.isTTY&&(fF=!1,e.write("\x1B[?25h"))};ow.hide=(e=process.stderr)=>{e.isTTY&&(Fut(),fF=!0,e.write("\x1B[?25l"))};ow.toggle=(e,t)=>{e!==void 0&&(fF=e),fF?ow.show(t):ow.hide(t)}});var zEe=G(LS=>{"use strict";var KEe=LS&&LS.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(LS,"__esModule",{value:!0});var VEe=KEe(J9()),JEe=KEe($9()),Nut=(e,{showCursor:t=!1}={})=>{let r=0,s="",a=!1,n=c=>{!t&&!a&&(JEe.default.hide(),a=!0);let f=c+` `;f!==s&&(s=f,e.write(VEe.default.eraseLines(r)+f),r=f.split(` `).length)};return n.clear=()=>{e.write(VEe.default.eraseLines(r)),s="",r=0},n.done=()=>{s="",r=0,t||(JEe.default.show(),a=!1)},n};LS.default={create:Nut}});var XEe=G((BVt,Out)=>{Out.exports=[{name:"AppVeyor",constant:"APPVEYOR",env:"APPVEYOR",pr:"APPVEYOR_PULL_REQUEST_NUMBER"},{name:"Azure Pipelines",constant:"AZURE_PIPELINES",env:"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI",pr:"SYSTEM_PULLREQUEST_PULLREQUESTID"},{name:"Bamboo",constant:"BAMBOO",env:"bamboo_planKey"},{name:"Bitbucket Pipelines",constant:"BITBUCKET",env:"BITBUCKET_COMMIT",pr:"BITBUCKET_PR_ID"},{name:"Bitrise",constant:"BITRISE",env:"BITRISE_IO",pr:"BITRISE_PULL_REQUEST"},{name:"Buddy",constant:"BUDDY",env:"BUDDY_WORKSPACE_ID",pr:"BUDDY_EXECUTION_PULL_REQUEST_ID"},{name:"Buildkite",constant:"BUILDKITE",env:"BUILDKITE",pr:{env:"BUILDKITE_PULL_REQUEST",ne:"false"}},{name:"CircleCI",constant:"CIRCLE",env:"CIRCLECI",pr:"CIRCLE_PULL_REQUEST"},{name:"Cirrus CI",constant:"CIRRUS",env:"CIRRUS_CI",pr:"CIRRUS_PR"},{name:"AWS CodeBuild",constant:"CODEBUILD",env:"CODEBUILD_BUILD_ARN"},{name:"Codeship",constant:"CODESHIP",env:{CI_NAME:"codeship"}},{name:"Drone",constant:"DRONE",env:"DRONE",pr:{DRONE_BUILD_EVENT:"pull_request"}},{name:"dsari",constant:"DSARI",env:"DSARI"},{name:"GitLab CI",constant:"GITLAB",env:"GITLAB_CI"},{name:"GoCD",constant:"GOCD",env:"GO_PIPELINE_LABEL"},{name:"Hudson",constant:"HUDSON",env:"HUDSON_URL"},{name:"Jenkins",constant:"JENKINS",env:["JENKINS_URL","BUILD_ID"],pr:{any:["ghprbPullId","CHANGE_ID"]}},{name:"Magnum CI",constant:"MAGNUM",env:"MAGNUM"},{name:"Netlify CI",constant:"NETLIFY",env:"NETLIFY_BUILD_BASE",pr:{env:"PULL_REQUEST",ne:"false"}},{name:"Sail CI",constant:"SAIL",env:"SAILCI",pr:"SAIL_PULL_REQUEST_NUMBER"},{name:"Semaphore",constant:"SEMAPHORE",env:"SEMAPHORE",pr:"PULL_REQUEST_NUMBER"},{name:"Shippable",constant:"SHIPPABLE",env:"SHIPPABLE",pr:{IS_PULL_REQUEST:"true"}},{name:"Solano CI",constant:"SOLANO",env:"TDDIUM",pr:"TDDIUM_PR_ID"},{name:"Strider CD",constant:"STRIDER",env:"STRIDER"},{name:"TaskCluster",constant:"TASKCLUSTER",env:["TASK_ID","RUN_ID"]},{name:"TeamCity",constant:"TEAMCITY",env:"TEAMCITY_VERSION"},{name:"Travis CI",constant:"TRAVIS",env:"TRAVIS",pr:{env:"TRAVIS_PULL_REQUEST",ne:"false"}}]});var eIe=G(nc=>{"use strict";var $Ee=XEe(),AA=process.env;Object.defineProperty(nc,"_vendors",{value:$Ee.map(function(e){return e.constant})});nc.name=null;nc.isPR=null;$Ee.forEach(function(e){var t=Array.isArray(e.env)?e.env:[e.env],r=t.every(function(s){return ZEe(s)});if(nc[e.constant]=r,r)switch(nc.name=e.name,typeof e.pr){case"string":nc.isPR=!!AA[e.pr];break;case"object":"env"in e.pr?nc.isPR=e.pr.env in AA&&AA[e.pr.env]!==e.pr.ne:"any"in e.pr?nc.isPR=e.pr.any.some(function(s){return!!AA[s]}):nc.isPR=ZEe(e.pr);break;default:nc.isPR=null}});nc.isCI=!!(AA.CI||AA.CONTINUOUS_INTEGRATION||AA.BUILD_NUMBER||AA.RUN_ID||nc.name);function ZEe(e){return typeof e=="string"?!!AA[e]:Object.keys(e).every(function(t){return AA[t]===e[t]})}});var rIe=G((SVt,tIe)=>{"use strict";tIe.exports=eIe().isCI});var iIe=G((DVt,nIe)=>{"use strict";var Lut=e=>{let t=new Set;do for(let r of Reflect.ownKeys(e))t.add([e,r]);while((e=Reflect.getPrototypeOf(e))&&e!==Object.prototype);return t};nIe.exports=(e,{include:t,exclude:r}={})=>{let s=a=>{let n=c=>typeof c=="string"?a===c:c.test(a);return t?t.some(n):r?!r.some(n):!0};for(let[a,n]of Lut(e.constructor.prototype)){if(n==="constructor"||!s(n))continue;let c=Reflect.getOwnPropertyDescriptor(a,n);c&&typeof c.value=="function"&&(e[n]=e[n].bind(e))}return e}});var uIe=G(Kn=>{"use strict";var lw,_S,dF,oq;typeof performance=="object"&&typeof performance.now=="function"?(sIe=performance,Kn.unstable_now=function(){return sIe.now()}):(eq=Date,oIe=eq.now(),Kn.unstable_now=function(){return eq.now()-oIe});var sIe,eq,oIe;typeof window>"u"||typeof MessageChannel!="function"?(aw=null,tq=null,rq=function(){if(aw!==null)try{var e=Kn.unstable_now();aw(!0,e),aw=null}catch(t){throw setTimeout(rq,0),t}},lw=function(e){aw!==null?setTimeout(lw,0,e):(aw=e,setTimeout(rq,0))},_S=function(e,t){tq=setTimeout(e,t)},dF=function(){clearTimeout(tq)},Kn.unstable_shouldYield=function(){return!1},oq=Kn.unstable_forceFrameRate=function(){}):(aIe=window.setTimeout,lIe=window.clearTimeout,typeof console<"u"&&(cIe=window.cancelAnimationFrame,typeof window.requestAnimationFrame!="function"&&console.error("This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills"),typeof cIe!="function"&&console.error("This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills")),MS=!1,US=null,AF=-1,nq=5,iq=0,Kn.unstable_shouldYield=function(){return Kn.unstable_now()>=iq},oq=function(){},Kn.unstable_forceFrameRate=function(e){0>e||125>>1,a=e[s];if(a!==void 0&&0hF(c,r))p!==void 0&&0>hF(p,c)?(e[s]=p,e[f]=r,s=f):(e[s]=c,e[n]=r,s=n);else if(p!==void 0&&0>hF(p,r))e[s]=p,e[f]=r,s=f;else break e}}return t}return null}function hF(e,t){var r=e.sortIndex-t.sortIndex;return r!==0?r:e.id-t.id}var pA=[],z0=[],Mut=1,Gc=null,ia=3,mF=!1,Om=!1,HS=!1;function lq(e){for(var t=rf(z0);t!==null;){if(t.callback===null)gF(z0);else if(t.startTime<=e)gF(z0),t.sortIndex=t.expirationTime,aq(pA,t);else break;t=rf(z0)}}function cq(e){if(HS=!1,lq(e),!Om)if(rf(pA)!==null)Om=!0,lw(uq);else{var t=rf(z0);t!==null&&_S(cq,t.startTime-e)}}function uq(e,t){Om=!1,HS&&(HS=!1,dF()),mF=!0;var r=ia;try{for(lq(t),Gc=rf(pA);Gc!==null&&(!(Gc.expirationTime>t)||e&&!Kn.unstable_shouldYield());){var s=Gc.callback;if(typeof s=="function"){Gc.callback=null,ia=Gc.priorityLevel;var a=s(Gc.expirationTime<=t);t=Kn.unstable_now(),typeof a=="function"?Gc.callback=a:Gc===rf(pA)&&gF(pA),lq(t)}else gF(pA);Gc=rf(pA)}if(Gc!==null)var n=!0;else{var c=rf(z0);c!==null&&_S(cq,c.startTime-t),n=!1}return n}finally{Gc=null,ia=r,mF=!1}}var Uut=oq;Kn.unstable_IdlePriority=5;Kn.unstable_ImmediatePriority=1;Kn.unstable_LowPriority=4;Kn.unstable_NormalPriority=3;Kn.unstable_Profiling=null;Kn.unstable_UserBlockingPriority=2;Kn.unstable_cancelCallback=function(e){e.callback=null};Kn.unstable_continueExecution=function(){Om||mF||(Om=!0,lw(uq))};Kn.unstable_getCurrentPriorityLevel=function(){return ia};Kn.unstable_getFirstCallbackNode=function(){return rf(pA)};Kn.unstable_next=function(e){switch(ia){case 1:case 2:case 3:var t=3;break;default:t=ia}var r=ia;ia=t;try{return e()}finally{ia=r}};Kn.unstable_pauseExecution=function(){};Kn.unstable_requestPaint=Uut;Kn.unstable_runWithPriority=function(e,t){switch(e){case 1:case 2:case 3:case 4:case 5:break;default:e=3}var r=ia;ia=e;try{return t()}finally{ia=r}};Kn.unstable_scheduleCallback=function(e,t,r){var s=Kn.unstable_now();switch(typeof r=="object"&&r!==null?(r=r.delay,r=typeof r=="number"&&0s?(e.sortIndex=r,aq(z0,e),rf(pA)===null&&e===rf(z0)&&(HS?dF():HS=!0,_S(cq,r-s))):(e.sortIndex=a,aq(pA,e),Om||mF||(Om=!0,lw(uq))),e};Kn.unstable_wrapCallback=function(e){var t=ia;return function(){var r=ia;ia=t;try{return e.apply(this,arguments)}finally{ia=r}}}});var fq=G((PVt,fIe)=>{"use strict";fIe.exports=uIe()});var AIe=G((xVt,jS)=>{jS.exports=function(t){var r={},s=L9(),a=dn(),n=fq();function c(v){for(var D="https://reactjs.org/docs/error-decoder.html?invariant="+v,Q=1;Q_e||Y[ve]!==ne[_e])return` `+Y[ve].replace(" at new "," at ");while(1<=ve&&0<=_e);break}}}finally{we=!1,Error.prepareStackTrace=Q}return(v=v?v.displayName||v.name:"")?lc(v):""}var cc=[],Oi=-1;function co(v){return{current:v}}function Tt(v){0>Oi||(v.current=cc[Oi],cc[Oi]=null,Oi--)}function Qn(v,D){Oi++,cc[Oi]=v.current,v.current=D}var pa={},Gi=co(pa),Li=co(!1),qa=pa;function mn(v,D){var Q=v.type.contextTypes;if(!Q)return pa;var H=v.stateNode;if(H&&H.__reactInternalMemoizedUnmaskedChildContext===D)return H.__reactInternalMemoizedMaskedChildContext;var Y={},ne;for(ne in Q)Y[ne]=D[ne];return H&&(v=v.stateNode,v.__reactInternalMemoizedUnmaskedChildContext=D,v.__reactInternalMemoizedMaskedChildContext=Y),Y}function Xn(v){return v=v.childContextTypes,v!=null}function uu(){Tt(Li),Tt(Gi)}function mh(v,D,Q){if(Gi.current!==pa)throw Error(c(168));Qn(Gi,D),Qn(Li,Q)}function Wa(v,D,Q){var H=v.stateNode;if(v=D.childContextTypes,typeof H.getChildContext!="function")return Q;H=H.getChildContext();for(var Y in H)if(!(Y in v))throw Error(c(108,d(D)||"Unknown",Y));return s({},Q,H)}function Ya(v){return v=(v=v.stateNode)&&v.__reactInternalMemoizedMergedChildContext||pa,qa=Gi.current,Qn(Gi,v),Qn(Li,Li.current),!0}function Va(v,D,Q){var H=v.stateNode;if(!H)throw Error(c(169));Q?(v=Wa(v,D,qa),H.__reactInternalMemoizedMergedChildContext=v,Tt(Li),Tt(Gi),Qn(Gi,v)):Tt(Li),Qn(Li,Q)}var $e=null,Ja=null,mf=n.unstable_now;mf();var uc=0,vn=8;function ha(v){if(1&v)return vn=15,1;if(2&v)return vn=14,2;if(4&v)return vn=13,4;var D=24&v;return D!==0?(vn=12,D):v&32?(vn=11,32):(D=192&v,D!==0?(vn=10,D):v&256?(vn=9,256):(D=3584&v,D!==0?(vn=8,D):v&4096?(vn=7,4096):(D=4186112&v,D!==0?(vn=6,D):(D=62914560&v,D!==0?(vn=5,D):v&67108864?(vn=4,67108864):v&134217728?(vn=3,134217728):(D=805306368&v,D!==0?(vn=2,D):1073741824&v?(vn=1,1073741824):(vn=8,v))))))}function UA(v){switch(v){case 99:return 15;case 98:return 10;case 97:case 96:return 8;case 95:return 2;default:return 0}}function _A(v){switch(v){case 15:case 14:return 99;case 13:case 12:case 11:case 10:return 98;case 9:case 8:case 7:case 6:case 4:case 5:return 97;case 3:case 2:case 1:return 95;case 0:return 90;default:throw Error(c(358,v))}}function da(v,D){var Q=v.pendingLanes;if(Q===0)return vn=0;var H=0,Y=0,ne=v.expiredLanes,ve=v.suspendedLanes,_e=v.pingedLanes;if(ne!==0)H=ne,Y=vn=15;else if(ne=Q&134217727,ne!==0){var ht=ne&~ve;ht!==0?(H=ha(ht),Y=vn):(_e&=ne,_e!==0&&(H=ha(_e),Y=vn))}else ne=Q&~ve,ne!==0?(H=ha(ne),Y=vn):_e!==0&&(H=ha(_e),Y=vn);if(H===0)return 0;if(H=31-is(H),H=Q&((0>H?0:1<Q;Q++)D.push(v);return D}function Ka(v,D,Q){v.pendingLanes|=D;var H=D-1;v.suspendedLanes&=H,v.pingedLanes&=H,v=v.eventTimes,D=31-is(D),v[D]=Q}var is=Math.clz32?Math.clz32:Ac,fc=Math.log,fu=Math.LN2;function Ac(v){return v===0?32:31-(fc(v)/fu|0)|0}var za=n.unstable_runWithPriority,Mi=n.unstable_scheduleCallback,Bs=n.unstable_cancelCallback,Ql=n.unstable_shouldYield,yf=n.unstable_requestPaint,pc=n.unstable_now,Bi=n.unstable_getCurrentPriorityLevel,Tn=n.unstable_ImmediatePriority,hc=n.unstable_UserBlockingPriority,Ke=n.unstable_NormalPriority,ot=n.unstable_LowPriority,St=n.unstable_IdlePriority,lr={},ee=yf!==void 0?yf:function(){},ye=null,Oe=null,mt=!1,Et=pc(),bt=1e4>Et?pc:function(){return pc()-Et};function tr(){switch(Bi()){case Tn:return 99;case hc:return 98;case Ke:return 97;case ot:return 96;case St:return 95;default:throw Error(c(332))}}function pn(v){switch(v){case 99:return Tn;case 98:return hc;case 97:return Ke;case 96:return ot;case 95:return St;default:throw Error(c(332))}}function ci(v,D){return v=pn(v),za(v,D)}function qi(v,D,Q){return v=pn(v),Mi(v,D,Q)}function Fn(){if(Oe!==null){var v=Oe;Oe=null,Bs(v)}Xa()}function Xa(){if(!mt&&ye!==null){mt=!0;var v=0;try{var D=ye;ci(99,function(){for(;vNn?(Hn=Qr,Qr=null):Hn=Qr.sibling;var zr=Zt(rt,Qr,gt[Nn],Xt);if(zr===null){Qr===null&&(Qr=Hn);break}v&&Qr&&zr.alternate===null&&D(rt,Qr),We=ne(zr,We,Nn),ti===null?Dr=zr:ti.sibling=zr,ti=zr,Qr=Hn}if(Nn===gt.length)return Q(rt,Qr),Dr;if(Qr===null){for(;NnNn?(Hn=Qr,Qr=null):Hn=Qr.sibling;var ui=Zt(rt,Qr,zr.value,Xt);if(ui===null){Qr===null&&(Qr=Hn);break}v&&Qr&&ui.alternate===null&&D(rt,Qr),We=ne(ui,We,Nn),ti===null?Dr=ui:ti.sibling=ui,ti=ui,Qr=Hn}if(zr.done)return Q(rt,Qr),Dr;if(Qr===null){for(;!zr.done;Nn++,zr=gt.next())zr=Lr(rt,zr.value,Xt),zr!==null&&(We=ne(zr,We,Nn),ti===null?Dr=zr:ti.sibling=zr,ti=zr);return Dr}for(Qr=H(rt,Qr);!zr.done;Nn++,zr=gt.next())zr=Zn(Qr,rt,Nn,zr.value,Xt),zr!==null&&(v&&zr.alternate!==null&&Qr.delete(zr.key===null?Nn:zr.key),We=ne(zr,We,Nn),ti===null?Dr=zr:ti.sibling=zr,ti=zr);return v&&Qr.forEach(function(vu){return D(rt,vu)}),Dr}return function(rt,We,gt,Xt){var Dr=typeof gt=="object"&>!==null&>.type===E&>.key===null;Dr&&(gt=gt.props.children);var ti=typeof gt=="object"&>!==null;if(ti)switch(gt.$$typeof){case p:e:{for(ti=gt.key,Dr=We;Dr!==null;){if(Dr.key===ti){switch(Dr.tag){case 7:if(gt.type===E){Q(rt,Dr.sibling),We=Y(Dr,gt.props.children),We.return=rt,rt=We;break e}break;default:if(Dr.elementType===gt.type){Q(rt,Dr.sibling),We=Y(Dr,gt.props),We.ref=yt(rt,Dr,gt),We.return=rt,rt=We;break e}}Q(rt,Dr);break}else D(rt,Dr);Dr=Dr.sibling}gt.type===E?(We=Tf(gt.props.children,rt.mode,Xt,gt.key),We.return=rt,rt=We):(Xt=ng(gt.type,gt.key,gt.props,null,rt.mode,Xt),Xt.ref=yt(rt,We,gt),Xt.return=rt,rt=Xt)}return ve(rt);case h:e:{for(Dr=gt.key;We!==null;){if(We.key===Dr)if(We.tag===4&&We.stateNode.containerInfo===gt.containerInfo&&We.stateNode.implementation===gt.implementation){Q(rt,We.sibling),We=Y(We,gt.children||[]),We.return=rt,rt=We;break e}else{Q(rt,We);break}else D(rt,We);We=We.sibling}We=Lo(gt,rt.mode,Xt),We.return=rt,rt=We}return ve(rt)}if(typeof gt=="string"||typeof gt=="number")return gt=""+gt,We!==null&&We.tag===6?(Q(rt,We.sibling),We=Y(We,gt),We.return=rt,rt=We):(Q(rt,We),We=E2(gt,rt.mode,Xt),We.return=rt,rt=We),ve(rt);if(If(gt))return Ei(rt,We,gt,Xt);if(Ee(gt))return il(rt,We,gt,Xt);if(ti&&pu(rt,gt),typeof gt>"u"&&!Dr)switch(rt.tag){case 1:case 22:case 0:case 11:case 15:throw Error(c(152,d(rt.type)||"Component"))}return Q(rt,We)}}var Od=Dy(!0),Y1=Dy(!1),Bh={},ur=co(Bh),zi=co(Bh),Cf=co(Bh);function Za(v){if(v===Bh)throw Error(c(174));return v}function Ld(v,D){Qn(Cf,D),Qn(zi,v),Qn(ur,Bh),v=st(D),Tt(ur),Qn(ur,v)}function hu(){Tt(ur),Tt(zi),Tt(Cf)}function wf(v){var D=Za(Cf.current),Q=Za(ur.current);D=_(Q,v.type,D),Q!==D&&(Qn(zi,v),Qn(ur,D))}function wt(v){zi.current===v&&(Tt(ur),Tt(zi))}var mi=co(0);function WA(v){for(var D=v;D!==null;){if(D.tag===13){var Q=D.memoizedState;if(Q!==null&&(Q=Q.dehydrated,Q===null||dr(Q)||xo(Q)))return D}else if(D.tag===19&&D.memoizedProps.revealOrder!==void 0){if(D.flags&64)return D}else if(D.child!==null){D.child.return=D,D=D.child;continue}if(D===v)break;for(;D.sibling===null;){if(D.return===null||D.return===v)return null;D=D.return}D.sibling.return=D.return,D=D.sibling}return null}var $a=null,ma=null,el=!1;function Md(v,D){var Q=nl(5,null,null,0);Q.elementType="DELETED",Q.type="DELETED",Q.stateNode=D,Q.return=v,Q.flags=8,v.lastEffect!==null?(v.lastEffect.nextEffect=Q,v.lastEffect=Q):v.firstEffect=v.lastEffect=Q}function vh(v,D){switch(v.tag){case 5:return D=Aa(D,v.type,v.pendingProps),D!==null?(v.stateNode=D,!0):!1;case 6:return D=OA(D,v.pendingProps),D!==null?(v.stateNode=D,!0):!1;case 13:return!1;default:return!1}}function Ud(v){if(el){var D=ma;if(D){var Q=D;if(!vh(v,D)){if(D=Ga(Q),!D||!vh(v,D)){v.flags=v.flags&-1025|2,el=!1,$a=v;return}Md($a,Q)}$a=v,ma=Ue(D)}else v.flags=v.flags&-1025|2,el=!1,$a=v}}function by(v){for(v=v.return;v!==null&&v.tag!==5&&v.tag!==3&&v.tag!==13;)v=v.return;$a=v}function YA(v){if(!X||v!==$a)return!1;if(!el)return by(v),el=!0,!1;var D=v.type;if(v.tag!==5||D!=="head"&&D!=="body"&&!ct(D,v.memoizedProps))for(D=ma;D;)Md(v,D),D=Ga(D);if(by(v),v.tag===13){if(!X)throw Error(c(316));if(v=v.memoizedState,v=v!==null?v.dehydrated:null,!v)throw Error(c(317));ma=LA(v)}else ma=$a?Ga(v.stateNode):null;return!0}function _d(){X&&(ma=$a=null,el=!1)}var du=[];function gu(){for(var v=0;vne))throw Error(c(301));ne+=1,Pi=ss=null,D.updateQueue=null,Bf.current=re,v=Q(H,Y)}while(vf)}if(Bf.current=kt,D=ss!==null&&ss.next!==null,mu=0,Pi=ss=qn=null,VA=!1,D)throw Error(c(300));return v}function os(){var v={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};return Pi===null?qn.memoizedState=Pi=v:Pi=Pi.next=v,Pi}function Nl(){if(ss===null){var v=qn.alternate;v=v!==null?v.memoizedState:null}else v=ss.next;var D=Pi===null?qn.memoizedState:Pi.next;if(D!==null)Pi=D,ss=v;else{if(v===null)throw Error(c(310));ss=v,v={memoizedState:ss.memoizedState,baseState:ss.baseState,baseQueue:ss.baseQueue,queue:ss.queue,next:null},Pi===null?qn.memoizedState=Pi=v:Pi=Pi.next=v}return Pi}function Fo(v,D){return typeof D=="function"?D(v):D}function Sf(v){var D=Nl(),Q=D.queue;if(Q===null)throw Error(c(311));Q.lastRenderedReducer=v;var H=ss,Y=H.baseQueue,ne=Q.pending;if(ne!==null){if(Y!==null){var ve=Y.next;Y.next=ne.next,ne.next=ve}H.baseQueue=Y=ne,Q.pending=null}if(Y!==null){Y=Y.next,H=H.baseState;var _e=ve=ne=null,ht=Y;do{var Wt=ht.lane;if((mu&Wt)===Wt)_e!==null&&(_e=_e.next={lane:0,action:ht.action,eagerReducer:ht.eagerReducer,eagerState:ht.eagerState,next:null}),H=ht.eagerReducer===v?ht.eagerState:v(H,ht.action);else{var Sr={lane:Wt,action:ht.action,eagerReducer:ht.eagerReducer,eagerState:ht.eagerState,next:null};_e===null?(ve=_e=Sr,ne=H):_e=_e.next=Sr,qn.lanes|=Wt,zd|=Wt}ht=ht.next}while(ht!==null&&ht!==Y);_e===null?ne=H:_e.next=ve,ko(H,D.memoizedState)||(Je=!0),D.memoizedState=H,D.baseState=ne,D.baseQueue=_e,Q.lastRenderedState=H}return[D.memoizedState,Q.dispatch]}function Df(v){var D=Nl(),Q=D.queue;if(Q===null)throw Error(c(311));Q.lastRenderedReducer=v;var H=Q.dispatch,Y=Q.pending,ne=D.memoizedState;if(Y!==null){Q.pending=null;var ve=Y=Y.next;do ne=v(ne,ve.action),ve=ve.next;while(ve!==Y);ko(ne,D.memoizedState)||(Je=!0),D.memoizedState=ne,D.baseQueue===null&&(D.baseState=ne),Q.lastRenderedState=ne}return[ne,H]}function Ol(v,D,Q){var H=D._getVersion;H=H(D._source);var Y=y?D._workInProgressVersionPrimary:D._workInProgressVersionSecondary;if(Y!==null?v=Y===H:(v=v.mutableReadLanes,(v=(mu&v)===v)&&(y?D._workInProgressVersionPrimary=H:D._workInProgressVersionSecondary=H,du.push(D))),v)return Q(D._source);throw du.push(D),Error(c(350))}function En(v,D,Q,H){var Y=fo;if(Y===null)throw Error(c(349));var ne=D._getVersion,ve=ne(D._source),_e=Bf.current,ht=_e.useState(function(){return Ol(Y,D,Q)}),Wt=ht[1],Sr=ht[0];ht=Pi;var Lr=v.memoizedState,Zt=Lr.refs,Zn=Zt.getSnapshot,Ei=Lr.source;Lr=Lr.subscribe;var il=qn;return v.memoizedState={refs:Zt,source:D,subscribe:H},_e.useEffect(function(){Zt.getSnapshot=Q,Zt.setSnapshot=Wt;var rt=ne(D._source);if(!ko(ve,rt)){rt=Q(D._source),ko(Sr,rt)||(Wt(rt),rt=Ds(il),Y.mutableReadLanes|=rt&Y.pendingLanes),rt=Y.mutableReadLanes,Y.entangledLanes|=rt;for(var We=Y.entanglements,gt=rt;0Q?98:Q,function(){v(!0)}),ci(97c2&&(D.flags|=64,Y=!0,$A(H,!1),D.lanes=33554432)}else{if(!Y)if(v=WA(ne),v!==null){if(D.flags|=64,Y=!0,v=v.updateQueue,v!==null&&(D.updateQueue=v,D.flags|=4),$A(H,!0),H.tail===null&&H.tailMode==="hidden"&&!ne.alternate&&!el)return D=D.lastEffect=H.lastEffect,D!==null&&(D.nextEffect=null),null}else 2*bt()-H.renderingStartTime>c2&&Q!==1073741824&&(D.flags|=64,Y=!0,$A(H,!1),D.lanes=33554432);H.isBackwards?(ne.sibling=D.child,D.child=ne):(v=H.last,v!==null?v.sibling=ne:D.child=ne,H.last=ne)}return H.tail!==null?(v=H.tail,H.rendering=v,H.tail=v.sibling,H.lastEffect=D.lastEffect,H.renderingStartTime=bt(),v.sibling=null,D=mi.current,Qn(mi,Y?D&1|2:D&1),v):null;case 23:case 24:return d2(),v!==null&&v.memoizedState!==null!=(D.memoizedState!==null)&&H.mode!=="unstable-defer-without-hiding"&&(D.flags|=4),null}throw Error(c(156,D.tag))}function bL(v){switch(v.tag){case 1:Xn(v.type)&&uu();var D=v.flags;return D&4096?(v.flags=D&-4097|64,v):null;case 3:if(hu(),Tt(Li),Tt(Gi),gu(),D=v.flags,D&64)throw Error(c(285));return v.flags=D&-4097|64,v;case 5:return wt(v),null;case 13:return Tt(mi),D=v.flags,D&4096?(v.flags=D&-4097|64,v):null;case 19:return Tt(mi),null;case 4:return hu(),null;case 10:return Fd(v),null;case 23:case 24:return d2(),null;default:return null}}function qd(v,D){try{var Q="",H=D;do Q+=W1(H),H=H.return;while(H);var Y=Q}catch(ne){Y=` Error generating stack: `+ne.message+` `+ne.stack}return{value:v,source:D,stack:Y}}function Wd(v,D){try{console.error(D.value)}catch(Q){setTimeout(function(){throw Q})}}var PL=typeof WeakMap=="function"?WeakMap:Map;function z1(v,D,Q){Q=Tl(-1,Q),Q.tag=3,Q.payload={element:null};var H=D.value;return Q.callback=function(){Gy||(Gy=!0,u2=H),Wd(v,D)},Q}function Yd(v,D,Q){Q=Tl(-1,Q),Q.tag=3;var H=v.type.getDerivedStateFromError;if(typeof H=="function"){var Y=D.value;Q.payload=function(){return Wd(v,D),H(Y)}}var ne=v.stateNode;return ne!==null&&typeof ne.componentDidCatch=="function"&&(Q.callback=function(){typeof H!="function"&&(gc===null?gc=new Set([this]):gc.add(this),Wd(v,D));var ve=D.stack;this.componentDidCatch(D.value,{componentStack:ve!==null?ve:""})}),Q}var xL=typeof WeakSet=="function"?WeakSet:Set;function X1(v){var D=v.ref;if(D!==null)if(typeof D=="function")try{D(null)}catch(Q){Rf(v,Q)}else D.current=null}function Ry(v,D){switch(D.tag){case 0:case 11:case 15:case 22:return;case 1:if(D.flags&256&&v!==null){var Q=v.memoizedProps,H=v.memoizedState;v=D.stateNode,D=v.getSnapshotBeforeUpdate(D.elementType===D.type?Q:Qo(D.type,Q),H),v.__reactInternalSnapshotBeforeUpdate=D}return;case 3:F&&D.flags&256&&Ns(D.stateNode.containerInfo);return;case 5:case 6:case 4:case 17:return}throw Error(c(163))}function Qh(v,D){if(D=D.updateQueue,D=D!==null?D.lastEffect:null,D!==null){var Q=D=D.next;do{if((Q.tag&v)===v){var H=Q.destroy;Q.destroy=void 0,H!==void 0&&H()}Q=Q.next}while(Q!==D)}}function uP(v,D,Q){switch(Q.tag){case 0:case 11:case 15:case 22:if(D=Q.updateQueue,D=D!==null?D.lastEffect:null,D!==null){v=D=D.next;do{if((v.tag&3)===3){var H=v.create;v.destroy=H()}v=v.next}while(v!==D)}if(D=Q.updateQueue,D=D!==null?D.lastEffect:null,D!==null){v=D=D.next;do{var Y=v;H=Y.next,Y=Y.tag,Y&4&&Y&1&&(vP(Q,v),ML(Q,v)),v=H}while(v!==D)}return;case 1:v=Q.stateNode,Q.flags&4&&(D===null?v.componentDidMount():(H=Q.elementType===Q.type?D.memoizedProps:Qo(Q.type,D.memoizedProps),v.componentDidUpdate(H,D.memoizedState,v.__reactInternalSnapshotBeforeUpdate))),D=Q.updateQueue,D!==null&&vy(Q,D,v);return;case 3:if(D=Q.updateQueue,D!==null){if(v=null,Q.child!==null)switch(Q.child.tag){case 5:v=Qe(Q.child.stateNode);break;case 1:v=Q.child.stateNode}vy(Q,D,v)}return;case 5:v=Q.stateNode,D===null&&Q.flags&4&&so(v,Q.type,Q.memoizedProps,Q);return;case 6:return;case 4:return;case 12:return;case 13:X&&Q.memoizedState===null&&(Q=Q.alternate,Q!==null&&(Q=Q.memoizedState,Q!==null&&(Q=Q.dehydrated,Q!==null&&lu(Q))));return;case 19:case 17:case 20:case 21:case 23:case 24:return}throw Error(c(163))}function fP(v,D){if(F)for(var Q=v;;){if(Q.tag===5){var H=Q.stateNode;D?dh(H):ao(Q.stateNode,Q.memoizedProps)}else if(Q.tag===6)H=Q.stateNode,D?gh(H):Gn(H,Q.memoizedProps);else if((Q.tag!==23&&Q.tag!==24||Q.memoizedState===null||Q===v)&&Q.child!==null){Q.child.return=Q,Q=Q.child;continue}if(Q===v)break;for(;Q.sibling===null;){if(Q.return===null||Q.return===v)return;Q=Q.return}Q.sibling.return=Q.return,Q=Q.sibling}}function Ty(v,D){if(Ja&&typeof Ja.onCommitFiberUnmount=="function")try{Ja.onCommitFiberUnmount($e,D)}catch{}switch(D.tag){case 0:case 11:case 14:case 15:case 22:if(v=D.updateQueue,v!==null&&(v=v.lastEffect,v!==null)){var Q=v=v.next;do{var H=Q,Y=H.destroy;if(H=H.tag,Y!==void 0)if(H&4)vP(D,Q);else{H=D;try{Y()}catch(ne){Rf(H,ne)}}Q=Q.next}while(Q!==v)}break;case 1:if(X1(D),v=D.stateNode,typeof v.componentWillUnmount=="function")try{v.props=D.memoizedProps,v.state=D.memoizedState,v.componentWillUnmount()}catch(ne){Rf(D,ne)}break;case 5:X1(D);break;case 4:F?dP(v,D):z&&z&&(D=D.stateNode.containerInfo,v=su(D),FA(D,v))}}function AP(v,D){for(var Q=D;;)if(Ty(v,Q),Q.child===null||F&&Q.tag===4){if(Q===D)break;for(;Q.sibling===null;){if(Q.return===null||Q.return===D)return;Q=Q.return}Q.sibling.return=Q.return,Q=Q.sibling}else Q.child.return=Q,Q=Q.child}function Fy(v){v.alternate=null,v.child=null,v.dependencies=null,v.firstEffect=null,v.lastEffect=null,v.memoizedProps=null,v.memoizedState=null,v.pendingProps=null,v.return=null,v.updateQueue=null}function pP(v){return v.tag===5||v.tag===3||v.tag===4}function hP(v){if(F){e:{for(var D=v.return;D!==null;){if(pP(D))break e;D=D.return}throw Error(c(160))}var Q=D;switch(D=Q.stateNode,Q.tag){case 5:var H=!1;break;case 3:D=D.containerInfo,H=!0;break;case 4:D=D.containerInfo,H=!0;break;default:throw Error(c(161))}Q.flags&16&&(df(D),Q.flags&=-17);e:t:for(Q=v;;){for(;Q.sibling===null;){if(Q.return===null||pP(Q.return)){Q=null;break e}Q=Q.return}for(Q.sibling.return=Q.return,Q=Q.sibling;Q.tag!==5&&Q.tag!==6&&Q.tag!==18;){if(Q.flags&2||Q.child===null||Q.tag===4)continue t;Q.child.return=Q,Q=Q.child}if(!(Q.flags&2)){Q=Q.stateNode;break e}}H?Z1(v,Q,D):$1(v,Q,D)}}function Z1(v,D,Q){var H=v.tag,Y=H===5||H===6;if(Y)v=Y?v.stateNode:v.stateNode.instance,D?oo(Q,v,D):Do(Q,v);else if(H!==4&&(v=v.child,v!==null))for(Z1(v,D,Q),v=v.sibling;v!==null;)Z1(v,D,Q),v=v.sibling}function $1(v,D,Q){var H=v.tag,Y=H===5||H===6;if(Y)v=Y?v.stateNode:v.stateNode.instance,D?ji(Q,v,D):li(Q,v);else if(H!==4&&(v=v.child,v!==null))for($1(v,D,Q),v=v.sibling;v!==null;)$1(v,D,Q),v=v.sibling}function dP(v,D){for(var Q=D,H=!1,Y,ne;;){if(!H){H=Q.return;e:for(;;){if(H===null)throw Error(c(160));switch(Y=H.stateNode,H.tag){case 5:ne=!1;break e;case 3:Y=Y.containerInfo,ne=!0;break e;case 4:Y=Y.containerInfo,ne=!0;break e}H=H.return}H=!0}if(Q.tag===5||Q.tag===6)AP(v,Q),ne?TA(Y,Q.stateNode):Po(Y,Q.stateNode);else if(Q.tag===4){if(Q.child!==null){Y=Q.stateNode.containerInfo,ne=!0,Q.child.return=Q,Q=Q.child;continue}}else if(Ty(v,Q),Q.child!==null){Q.child.return=Q,Q=Q.child;continue}if(Q===D)break;for(;Q.sibling===null;){if(Q.return===null||Q.return===D)return;Q=Q.return,Q.tag===4&&(H=!1)}Q.sibling.return=Q.return,Q=Q.sibling}}function e2(v,D){if(F){switch(D.tag){case 0:case 11:case 14:case 15:case 22:Qh(3,D);return;case 1:return;case 5:var Q=D.stateNode;if(Q!=null){var H=D.memoizedProps;v=v!==null?v.memoizedProps:H;var Y=D.type,ne=D.updateQueue;D.updateQueue=null,ne!==null&&bo(Q,ne,Y,v,H,D)}return;case 6:if(D.stateNode===null)throw Error(c(162));Q=D.memoizedProps,ns(D.stateNode,v!==null?v.memoizedProps:Q,Q);return;case 3:X&&(D=D.stateNode,D.hydrate&&(D.hydrate=!1,MA(D.containerInfo)));return;case 12:return;case 13:gP(D),Vd(D);return;case 19:Vd(D);return;case 17:return;case 23:case 24:fP(D,D.memoizedState!==null);return}throw Error(c(163))}switch(D.tag){case 0:case 11:case 14:case 15:case 22:Qh(3,D);return;case 12:return;case 13:gP(D),Vd(D);return;case 19:Vd(D);return;case 3:X&&(Q=D.stateNode,Q.hydrate&&(Q.hydrate=!1,MA(Q.containerInfo)));break;case 23:case 24:return}e:if(z){switch(D.tag){case 1:case 5:case 6:case 20:break e;case 3:case 4:D=D.stateNode,FA(D.containerInfo,D.pendingChildren);break e}throw Error(c(163))}}function gP(v){v.memoizedState!==null&&(l2=bt(),F&&fP(v.child,!0))}function Vd(v){var D=v.updateQueue;if(D!==null){v.updateQueue=null;var Q=v.stateNode;Q===null&&(Q=v.stateNode=new xL),D.forEach(function(H){var Y=_L.bind(null,v,H);Q.has(H)||(Q.add(H),H.then(Y,Y))})}}function kL(v,D){return v!==null&&(v=v.memoizedState,v===null||v.dehydrated!==null)?(D=D.memoizedState,D!==null&&D.dehydrated===null):!1}var Ny=0,Oy=1,Ly=2,Jd=3,My=4;if(typeof Symbol=="function"&&Symbol.for){var Kd=Symbol.for;Ny=Kd("selector.component"),Oy=Kd("selector.has_pseudo_class"),Ly=Kd("selector.role"),Jd=Kd("selector.test_id"),My=Kd("selector.text")}function Uy(v){var D=$(v);if(D!=null){if(typeof D.memoizedProps["data-testname"]!="string")throw Error(c(364));return D}if(v=ir(v),v===null)throw Error(c(362));return v.stateNode.current}function Pf(v,D){switch(D.$$typeof){case Ny:if(v.type===D.value)return!0;break;case Oy:e:{D=D.value,v=[v,0];for(var Q=0;Q";case Oy:return":has("+(xf(v)||"")+")";case Ly:return'[role="'+v.value+'"]';case My:return'"'+v.value+'"';case Jd:return'[data-testname="'+v.value+'"]';default:throw Error(c(365,v))}}function t2(v,D){var Q=[];v=[v,0];for(var H=0;HY&&(Y=ve),Q&=~ne}if(Q=Y,Q=bt()-Q,Q=(120>Q?120:480>Q?480:1080>Q?1080:1920>Q?1920:3e3>Q?3e3:4320>Q?4320:1960*RL(Q/1960))-Q,10 component higher in the tree to provide a loading indicator or placeholder to display.`)}Ss!==5&&(Ss=2),ht=qd(ht,_e),Zt=ve;do{switch(Zt.tag){case 3:ne=ht,Zt.flags|=4096,D&=-D,Zt.lanes|=D;var ti=z1(Zt,ne,D);By(Zt,ti);break e;case 1:ne=ht;var Qr=Zt.type,Nn=Zt.stateNode;if(!(Zt.flags&64)&&(typeof Qr.getDerivedStateFromError=="function"||Nn!==null&&typeof Nn.componentDidCatch=="function"&&(gc===null||!gc.has(Nn)))){Zt.flags|=4096,D&=-D,Zt.lanes|=D;var Hn=Yd(Zt,ne,D);By(Zt,Hn);break e}}Zt=Zt.return}while(Zt!==null)}BP(Q)}catch(zr){D=zr,Xi===Q&&Q!==null&&(Xi=Q=Q.return);continue}break}while(!0)}function CP(){var v=Hy.current;return Hy.current=kt,v===null?kt:v}function rg(v,D){var Q=xr;xr|=16;var H=CP();fo===v&&Ms===D||Nh(v,D);do try{FL();break}catch(Y){IP(v,Y)}while(!0);if(Rd(),xr=Q,Hy.current=H,Xi!==null)throw Error(c(261));return fo=null,Ms=0,Ss}function FL(){for(;Xi!==null;)wP(Xi)}function NL(){for(;Xi!==null&&!Ql();)wP(Xi)}function wP(v){var D=bP(v.alternate,v,ep);v.memoizedProps=v.pendingProps,D===null?BP(v):Xi=D,n2.current=null}function BP(v){var D=v;do{var Q=D.alternate;if(v=D.return,D.flags&2048){if(Q=bL(D),Q!==null){Q.flags&=2047,Xi=Q;return}v!==null&&(v.firstEffect=v.lastEffect=null,v.flags|=2048)}else{if(Q=DL(Q,D,ep),Q!==null){Xi=Q;return}if(Q=D,Q.tag!==24&&Q.tag!==23||Q.memoizedState===null||ep&1073741824||!(Q.mode&4)){for(var H=0,Y=Q.child;Y!==null;)H|=Y.lanes|Y.childLanes,Y=Y.sibling;Q.childLanes=H}v!==null&&!(v.flags&2048)&&(v.firstEffect===null&&(v.firstEffect=D.firstEffect),D.lastEffect!==null&&(v.lastEffect!==null&&(v.lastEffect.nextEffect=D.firstEffect),v.lastEffect=D.lastEffect),1bt()-l2?Nh(v,0):o2|=Q),Ia(v,D)}function _L(v,D){var Q=v.stateNode;Q!==null&&Q.delete(D),D=0,D===0&&(D=v.mode,D&2?D&4?(Cu===0&&(Cu=Rh),D=Rn(62914560&~Cu),D===0&&(D=4194304)):D=tr()===99?1:2:D=1),Q=Oo(),v=Yy(v,D),v!==null&&(Ka(v,D,Q),Ia(v,Q))}var bP;bP=function(v,D,Q){var H=D.lanes;if(v!==null)if(v.memoizedProps!==D.pendingProps||Li.current)Je=!0;else if(Q&H)Je=!!(v.flags&16384);else{switch(Je=!1,D.tag){case 3:ky(D),_d();break;case 5:wf(D);break;case 1:Xn(D.type)&&Ya(D);break;case 4:Ld(D,D.stateNode.containerInfo);break;case 10:Td(D,D.memoizedProps.value);break;case 13:if(D.memoizedState!==null)return Q&D.child.childLanes?J1(v,D,Q):(Qn(mi,mi.current&1),D=Wn(v,D,Q),D!==null?D.sibling:null);Qn(mi,mi.current&1);break;case 19:if(H=(Q&D.childLanes)!==0,v.flags&64){if(H)return cP(v,D,Q);D.flags|=64}var Y=D.memoizedState;if(Y!==null&&(Y.rendering=null,Y.tail=null,Y.lastEffect=null),Qn(mi,mi.current),H)break;return null;case 23:case 24:return D.lanes=0,yi(v,D,Q)}return Wn(v,D,Q)}else Je=!1;switch(D.lanes=0,D.tag){case 2:if(H=D.type,v!==null&&(v.alternate=null,D.alternate=null,D.flags|=2),v=D.pendingProps,Y=mn(D,Gi.current),Ef(D,Q),Y=jd(null,D,H,v,Y,Q),D.flags|=1,typeof Y=="object"&&Y!==null&&typeof Y.render=="function"&&Y.$$typeof===void 0){if(D.tag=1,D.memoizedState=null,D.updateQueue=null,Xn(H)){var ne=!0;Ya(D)}else ne=!1;D.memoizedState=Y.state!==null&&Y.state!==void 0?Y.state:null,wh(D);var ve=H.getDerivedStateFromProps;typeof ve=="function"&&jA(D,H,ve,v),Y.updater=GA,D.stateNode=Y,Y._reactInternals=D,To(D,H,v,Q),D=V1(null,D,H,!0,ne,Q)}else D.tag=0,pt(null,D,Y,Q),D=D.child;return D;case 16:Y=D.elementType;e:{switch(v!==null&&(v.alternate=null,D.alternate=null,D.flags|=2),v=D.pendingProps,ne=Y._init,Y=ne(Y._payload),D.type=Y,ne=D.tag=jL(Y),v=Qo(Y,v),ne){case 0:D=zA(null,D,Y,v,Q);break e;case 1:D=lP(null,D,Y,v,Q);break e;case 11:D=gr(null,D,Y,v,Q);break e;case 14:D=vr(null,D,Y,Qo(Y.type,v),H,Q);break e}throw Error(c(306,Y,""))}return D;case 0:return H=D.type,Y=D.pendingProps,Y=D.elementType===H?Y:Qo(H,Y),zA(v,D,H,Y,Q);case 1:return H=D.type,Y=D.pendingProps,Y=D.elementType===H?Y:Qo(H,Y),lP(v,D,H,Y,Q);case 3:if(ky(D),H=D.updateQueue,v===null||H===null)throw Error(c(282));if(H=D.pendingProps,Y=D.memoizedState,Y=Y!==null?Y.element:null,Nd(v,D),HA(D,H,null,Q),H=D.memoizedState.element,H===Y)_d(),D=Wn(v,D,Q);else{if(Y=D.stateNode,(ne=Y.hydrate)&&(X?(ma=Ue(D.stateNode.containerInfo),$a=D,ne=el=!0):ne=!1),ne){if(X&&(v=Y.mutableSourceEagerHydrationData,v!=null))for(Y=0;Y=Wt&&ne>=Lr&&Y<=Sr&&ve<=Zt){v.splice(D,1);break}else if(H!==Wt||Q.width!==ht.width||Ztve){if(!(ne!==Lr||Q.height!==ht.height||SrY)){Wt>H&&(ht.width+=Wt-H,ht.x=H),Srne&&(ht.height+=Lr-ne,ht.y=ne),ZtQ&&(Q=ve)),ve ")+` No matching component was found for: `)+v.join(" > ")}return null},r.getPublicRootInstance=function(v){if(v=v.current,!v.child)return null;switch(v.child.tag){case 5:return Qe(v.child.stateNode);default:return v.child.stateNode}},r.injectIntoDevTools=function(v){if(v={bundleType:v.bundleType,version:v.version,rendererPackageName:v.rendererPackageName,rendererConfig:v.rendererConfig,overrideHookState:null,overrideHookStateDeletePath:null,overrideHookStateRenamePath:null,overrideProps:null,overridePropsDeletePath:null,overridePropsRenamePath:null,setSuspenseHandler:null,scheduleUpdate:null,currentDispatcherRef:f.ReactCurrentDispatcher,findHostInstanceByFiber:WL,findFiberByHostInstance:v.findFiberByHostInstance||YL,findHostInstancesForRefresh:null,scheduleRefresh:null,scheduleRoot:null,setRefreshHandler:null,getCurrentFiber:null},typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u")v=!1;else{var D=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(!D.isDisabled&&D.supportsFiber)try{$e=D.inject(v),Ja=D}catch{}v=!0}return v},r.observeVisibleRects=function(v,D,Q,H){if(!qt)throw Error(c(363));v=r2(v,D);var Y=on(v,Q,H).disconnect;return{disconnect:function(){Y()}}},r.registerMutableSourceForHydration=function(v,D){var Q=D._getVersion;Q=Q(D._source),v.mutableSourceEagerHydrationData==null?v.mutableSourceEagerHydrationData=[D,Q]:v.mutableSourceEagerHydrationData.push(D,Q)},r.runWithPriority=function(v,D){var Q=uc;try{return uc=v,D()}finally{uc=Q}},r.shouldSuspend=function(){return!1},r.unbatchedUpdates=function(v,D){var Q=xr;xr&=-2,xr|=8;try{return v(D)}finally{xr=Q,xr===0&&(kf(),Fn())}},r.updateContainer=function(v,D,Q,H){var Y=D.current,ne=Oo(),ve=Ds(Y);e:if(Q){Q=Q._reactInternals;t:{if(Se(Q)!==Q||Q.tag!==1)throw Error(c(170));var _e=Q;do{switch(_e.tag){case 3:_e=_e.stateNode.context;break t;case 1:if(Xn(_e.type)){_e=_e.stateNode.__reactInternalMemoizedMergedChildContext;break t}}_e=_e.return}while(_e!==null);throw Error(c(171))}if(Q.tag===1){var ht=Q.type;if(Xn(ht)){Q=Wa(Q,ht,_e);break e}}Q=_e}else Q=pa;return D.context===null?D.context=Q:D.pendingContext=Q,D=Tl(ne,ve),D.payload={element:v},H=H===void 0?null:H,H!==null&&(D.callback=H),Fl(Y,D),Ul(Y,ve,ne),ve},r}});var hIe=G((kVt,pIe)=>{"use strict";pIe.exports=AIe()});var gIe=G((QVt,dIe)=>{"use strict";var _ut={ALIGN_COUNT:8,ALIGN_AUTO:0,ALIGN_FLEX_START:1,ALIGN_CENTER:2,ALIGN_FLEX_END:3,ALIGN_STRETCH:4,ALIGN_BASELINE:5,ALIGN_SPACE_BETWEEN:6,ALIGN_SPACE_AROUND:7,DIMENSION_COUNT:2,DIMENSION_WIDTH:0,DIMENSION_HEIGHT:1,DIRECTION_COUNT:3,DIRECTION_INHERIT:0,DIRECTION_LTR:1,DIRECTION_RTL:2,DISPLAY_COUNT:2,DISPLAY_FLEX:0,DISPLAY_NONE:1,EDGE_COUNT:9,EDGE_LEFT:0,EDGE_TOP:1,EDGE_RIGHT:2,EDGE_BOTTOM:3,EDGE_START:4,EDGE_END:5,EDGE_HORIZONTAL:6,EDGE_VERTICAL:7,EDGE_ALL:8,EXPERIMENTAL_FEATURE_COUNT:1,EXPERIMENTAL_FEATURE_WEB_FLEX_BASIS:0,FLEX_DIRECTION_COUNT:4,FLEX_DIRECTION_COLUMN:0,FLEX_DIRECTION_COLUMN_REVERSE:1,FLEX_DIRECTION_ROW:2,FLEX_DIRECTION_ROW_REVERSE:3,JUSTIFY_COUNT:6,JUSTIFY_FLEX_START:0,JUSTIFY_CENTER:1,JUSTIFY_FLEX_END:2,JUSTIFY_SPACE_BETWEEN:3,JUSTIFY_SPACE_AROUND:4,JUSTIFY_SPACE_EVENLY:5,LOG_LEVEL_COUNT:6,LOG_LEVEL_ERROR:0,LOG_LEVEL_WARN:1,LOG_LEVEL_INFO:2,LOG_LEVEL_DEBUG:3,LOG_LEVEL_VERBOSE:4,LOG_LEVEL_FATAL:5,MEASURE_MODE_COUNT:3,MEASURE_MODE_UNDEFINED:0,MEASURE_MODE_EXACTLY:1,MEASURE_MODE_AT_MOST:2,NODE_TYPE_COUNT:2,NODE_TYPE_DEFAULT:0,NODE_TYPE_TEXT:1,OVERFLOW_COUNT:3,OVERFLOW_VISIBLE:0,OVERFLOW_HIDDEN:1,OVERFLOW_SCROLL:2,POSITION_TYPE_COUNT:2,POSITION_TYPE_RELATIVE:0,POSITION_TYPE_ABSOLUTE:1,PRINT_OPTIONS_COUNT:3,PRINT_OPTIONS_LAYOUT:1,PRINT_OPTIONS_STYLE:2,PRINT_OPTIONS_CHILDREN:4,UNIT_COUNT:4,UNIT_UNDEFINED:0,UNIT_POINT:1,UNIT_PERCENT:2,UNIT_AUTO:3,WRAP_COUNT:3,WRAP_NO_WRAP:0,WRAP_WRAP:1,WRAP_WRAP_REVERSE:2};dIe.exports=_ut});var IIe=G((RVt,EIe)=>{"use strict";var Hut=Object.assign||function(e){for(var t=1;t"}}]),e}(),mIe=function(){yF(e,null,[{key:"fromJS",value:function(r){var s=r.width,a=r.height;return new e(s,a)}}]);function e(t,r){pq(this,e),this.width=t,this.height=r}return yF(e,[{key:"fromJS",value:function(r){r(this.width,this.height)}},{key:"toString",value:function(){return""}}]),e}(),yIe=function(){function e(t,r){pq(this,e),this.unit=t,this.value=r}return yF(e,[{key:"fromJS",value:function(r){r(this.unit,this.value)}},{key:"toString",value:function(){switch(this.unit){case nf.UNIT_POINT:return String(this.value);case nf.UNIT_PERCENT:return this.value+"%";case nf.UNIT_AUTO:return"auto";default:return this.value+"?"}}},{key:"valueOf",value:function(){return this.value}}]),e}();EIe.exports=function(e,t){function r(c,f,p){var h=c[f];c[f]=function(){for(var E=arguments.length,C=Array(E),S=0;S1?C-1:0),x=1;x1&&arguments[1]!==void 0?arguments[1]:NaN,p=arguments.length>2&&arguments[2]!==void 0?arguments[2]:NaN,h=arguments.length>3&&arguments[3]!==void 0?arguments[3]:nf.DIRECTION_LTR;return c.call(this,f,p,h)}),Hut({Config:t.Config,Node:t.Node,Layout:e("Layout",jut),Size:e("Size",mIe),Value:e("Value",yIe),getInstanceCount:function(){return t.getInstanceCount.apply(t,arguments)}},nf)}});var CIe=G((exports,module)=>{(function(e,t){typeof define=="function"&&define.amd?define([],function(){return t}):typeof module=="object"&&module.exports?module.exports=t:(e.nbind=e.nbind||{}).init=t})(exports,function(Module,cb){typeof Module=="function"&&(cb=Module,Module={}),Module.onRuntimeInitialized=function(e,t){return function(){e&&e.apply(this,arguments);try{Module.ccall("nbind_init")}catch(r){t(r);return}t(null,{bind:Module._nbind_value,reflect:Module.NBind.reflect,queryType:Module.NBind.queryType,toggleLightGC:Module.toggleLightGC,lib:Module})}}(Module.onRuntimeInitialized,cb);var Module;Module||(Module=(typeof Module<"u"?Module:null)||{});var moduleOverrides={};for(var key in Module)Module.hasOwnProperty(key)&&(moduleOverrides[key]=Module[key]);var ENVIRONMENT_IS_WEB=!1,ENVIRONMENT_IS_WORKER=!1,ENVIRONMENT_IS_NODE=!1,ENVIRONMENT_IS_SHELL=!1;if(Module.ENVIRONMENT)if(Module.ENVIRONMENT==="WEB")ENVIRONMENT_IS_WEB=!0;else if(Module.ENVIRONMENT==="WORKER")ENVIRONMENT_IS_WORKER=!0;else if(Module.ENVIRONMENT==="NODE")ENVIRONMENT_IS_NODE=!0;else if(Module.ENVIRONMENT==="SHELL")ENVIRONMENT_IS_SHELL=!0;else throw new Error("The provided Module['ENVIRONMENT'] value is not valid. It must be one of: WEB|WORKER|NODE|SHELL.");else ENVIRONMENT_IS_WEB=typeof window=="object",ENVIRONMENT_IS_WORKER=typeof importScripts=="function",ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof Ie=="function"&&!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_WORKER,ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER;if(ENVIRONMENT_IS_NODE){Module.print||(Module.print=console.log),Module.printErr||(Module.printErr=console.warn);var nodeFS,nodePath;Module.read=function(t,r){nodeFS||(nodeFS={}("")),nodePath||(nodePath={}("")),t=nodePath.normalize(t);var s=nodeFS.readFileSync(t);return r?s:s.toString()},Module.readBinary=function(t){var r=Module.read(t,!0);return r.buffer||(r=new Uint8Array(r)),assert(r.buffer),r},Module.load=function(t){globalEval(read(t))},Module.thisProgram||(process.argv.length>1?Module.thisProgram=process.argv[1].replace(/\\/g,"/"):Module.thisProgram="unknown-program"),Module.arguments=process.argv.slice(2),typeof module<"u"&&(module.exports=Module),Module.inspect=function(){return"[Emscripten Module object]"}}else if(ENVIRONMENT_IS_SHELL)Module.print||(Module.print=print),typeof printErr<"u"&&(Module.printErr=printErr),typeof read<"u"?Module.read=read:Module.read=function(){throw"no read() available"},Module.readBinary=function(t){if(typeof readbuffer=="function")return new Uint8Array(readbuffer(t));var r=read(t,"binary");return assert(typeof r=="object"),r},typeof scriptArgs<"u"?Module.arguments=scriptArgs:typeof arguments<"u"&&(Module.arguments=arguments),typeof quit=="function"&&(Module.quit=function(e,t){quit(e)});else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(Module.read=function(t){var r=new XMLHttpRequest;return r.open("GET",t,!1),r.send(null),r.responseText},ENVIRONMENT_IS_WORKER&&(Module.readBinary=function(t){var r=new XMLHttpRequest;return r.open("GET",t,!1),r.responseType="arraybuffer",r.send(null),new Uint8Array(r.response)}),Module.readAsync=function(t,r,s){var a=new XMLHttpRequest;a.open("GET",t,!0),a.responseType="arraybuffer",a.onload=function(){a.status==200||a.status==0&&a.response?r(a.response):s()},a.onerror=s,a.send(null)},typeof arguments<"u"&&(Module.arguments=arguments),typeof console<"u")Module.print||(Module.print=function(t){console.log(t)}),Module.printErr||(Module.printErr=function(t){console.warn(t)});else{var TRY_USE_DUMP=!1;Module.print||(Module.print=TRY_USE_DUMP&&typeof dump<"u"?function(e){dump(e)}:function(e){})}ENVIRONMENT_IS_WORKER&&(Module.load=importScripts),typeof Module.setWindowTitle>"u"&&(Module.setWindowTitle=function(e){document.title=e})}else throw"Unknown runtime environment. Where are we?";function globalEval(e){eval.call(null,e)}!Module.load&&Module.read&&(Module.load=function(t){globalEval(Module.read(t))}),Module.print||(Module.print=function(){}),Module.printErr||(Module.printErr=Module.print),Module.arguments||(Module.arguments=[]),Module.thisProgram||(Module.thisProgram="./this.program"),Module.quit||(Module.quit=function(e,t){throw t}),Module.print=Module.print,Module.printErr=Module.printErr,Module.preRun=[],Module.postRun=[];for(var key in moduleOverrides)moduleOverrides.hasOwnProperty(key)&&(Module[key]=moduleOverrides[key]);moduleOverrides=void 0;var Runtime={setTempRet0:function(e){return tempRet0=e,e},getTempRet0:function(){return tempRet0},stackSave:function(){return STACKTOP},stackRestore:function(e){STACKTOP=e},getNativeTypeSize:function(e){switch(e){case"i1":case"i8":return 1;case"i16":return 2;case"i32":return 4;case"i64":return 8;case"float":return 4;case"double":return 8;default:{if(e[e.length-1]==="*")return Runtime.QUANTUM_SIZE;if(e[0]==="i"){var t=parseInt(e.substr(1));return assert(t%8===0),t/8}else return 0}}},getNativeFieldSize:function(e){return Math.max(Runtime.getNativeTypeSize(e),Runtime.QUANTUM_SIZE)},STACK_ALIGN:16,prepVararg:function(e,t){return t==="double"||t==="i64"?e&7&&(assert((e&7)===4),e+=4):assert((e&3)===0),e},getAlignSize:function(e,t,r){return!r&&(e=="i64"||e=="double")?8:e?Math.min(t||(e?Runtime.getNativeFieldSize(e):0),Runtime.QUANTUM_SIZE):Math.min(t,8)},dynCall:function(e,t,r){return r&&r.length?Module["dynCall_"+e].apply(null,[t].concat(r)):Module["dynCall_"+e].call(null,t)},functionPointers:[],addFunction:function(e){for(var t=0;t>2],r=(t+e+15|0)&-16;if(HEAP32[DYNAMICTOP_PTR>>2]=r,r>=TOTAL_MEMORY){var s=enlargeMemory();if(!s)return HEAP32[DYNAMICTOP_PTR>>2]=t,0}return t},alignMemory:function(e,t){var r=e=Math.ceil(e/(t||16))*(t||16);return r},makeBigInt:function(e,t,r){var s=r?+(e>>>0)+ +(t>>>0)*4294967296:+(e>>>0)+ +(t|0)*4294967296;return s},GLOBAL_BASE:8,QUANTUM_SIZE:4,__dummy__:0};Module.Runtime=Runtime;var ABORT=0,EXITSTATUS=0;function assert(e,t){e||abort("Assertion failed: "+t)}function getCFunc(ident){var func=Module["_"+ident];if(!func)try{func=eval("_"+ident)}catch(e){}return assert(func,"Cannot call unknown function "+ident+" (perhaps LLVM optimizations or closure removed it?)"),func}var cwrap,ccall;(function(){var JSfuncs={stackSave:function(){Runtime.stackSave()},stackRestore:function(){Runtime.stackRestore()},arrayToC:function(e){var t=Runtime.stackAlloc(e.length);return writeArrayToMemory(e,t),t},stringToC:function(e){var t=0;if(e!=null&&e!==0){var r=(e.length<<2)+1;t=Runtime.stackAlloc(r),stringToUTF8(e,t,r)}return t}},toC={string:JSfuncs.stringToC,array:JSfuncs.arrayToC};ccall=function(t,r,s,a,n){var c=getCFunc(t),f=[],p=0;if(a)for(var h=0;h>0]=t;break;case"i8":HEAP8[e>>0]=t;break;case"i16":HEAP16[e>>1]=t;break;case"i32":HEAP32[e>>2]=t;break;case"i64":tempI64=[t>>>0,(tempDouble=t,+Math_abs(tempDouble)>=1?tempDouble>0?(Math_min(+Math_floor(tempDouble/4294967296),4294967295)|0)>>>0:~~+Math_ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[e>>2]=tempI64[0],HEAP32[e+4>>2]=tempI64[1];break;case"float":HEAPF32[e>>2]=t;break;case"double":HEAPF64[e>>3]=t;break;default:abort("invalid type for setValue: "+r)}}Module.setValue=setValue;function getValue(e,t,r){switch(t=t||"i8",t.charAt(t.length-1)==="*"&&(t="i32"),t){case"i1":return HEAP8[e>>0];case"i8":return HEAP8[e>>0];case"i16":return HEAP16[e>>1];case"i32":return HEAP32[e>>2];case"i64":return HEAP32[e>>2];case"float":return HEAPF32[e>>2];case"double":return HEAPF64[e>>3];default:abort("invalid type for setValue: "+t)}return null}Module.getValue=getValue;var ALLOC_NORMAL=0,ALLOC_STACK=1,ALLOC_STATIC=2,ALLOC_DYNAMIC=3,ALLOC_NONE=4;Module.ALLOC_NORMAL=ALLOC_NORMAL,Module.ALLOC_STACK=ALLOC_STACK,Module.ALLOC_STATIC=ALLOC_STATIC,Module.ALLOC_DYNAMIC=ALLOC_DYNAMIC,Module.ALLOC_NONE=ALLOC_NONE;function allocate(e,t,r,s){var a,n;typeof e=="number"?(a=!0,n=e):(a=!1,n=e.length);var c=typeof t=="string"?t:null,f;if(r==ALLOC_NONE?f=s:f=[typeof _malloc=="function"?_malloc:Runtime.staticAlloc,Runtime.stackAlloc,Runtime.staticAlloc,Runtime.dynamicAlloc][r===void 0?ALLOC_STATIC:r](Math.max(n,c?1:t.length)),a){var s=f,p;for(assert((f&3)==0),p=f+(n&-4);s>2]=0;for(p=f+n;s>0]=0;return f}if(c==="i8")return e.subarray||e.slice?HEAPU8.set(e,f):HEAPU8.set(new Uint8Array(e),f),f;for(var h=0,E,C,S;h>0],r|=s,!(s==0&&!t||(a++,t&&a==t)););t||(t=a);var n="";if(r<128){for(var c=1024,f;t>0;)f=String.fromCharCode.apply(String,HEAPU8.subarray(e,e+Math.min(t,c))),n=n?n+f:f,e+=c,t-=c;return n}return Module.UTF8ToString(e)}Module.Pointer_stringify=Pointer_stringify;function AsciiToString(e){for(var t="";;){var r=HEAP8[e++>>0];if(!r)return t;t+=String.fromCharCode(r)}}Module.AsciiToString=AsciiToString;function stringToAscii(e,t){return writeAsciiToMemory(e,t,!1)}Module.stringToAscii=stringToAscii;var UTF8Decoder=typeof TextDecoder<"u"?new TextDecoder("utf8"):void 0;function UTF8ArrayToString(e,t){for(var r=t;e[r];)++r;if(r-t>16&&e.subarray&&UTF8Decoder)return UTF8Decoder.decode(e.subarray(t,r));for(var s,a,n,c,f,p,h="";;){if(s=e[t++],!s)return h;if(!(s&128)){h+=String.fromCharCode(s);continue}if(a=e[t++]&63,(s&224)==192){h+=String.fromCharCode((s&31)<<6|a);continue}if(n=e[t++]&63,(s&240)==224?s=(s&15)<<12|a<<6|n:(c=e[t++]&63,(s&248)==240?s=(s&7)<<18|a<<12|n<<6|c:(f=e[t++]&63,(s&252)==248?s=(s&3)<<24|a<<18|n<<12|c<<6|f:(p=e[t++]&63,s=(s&1)<<30|a<<24|n<<18|c<<12|f<<6|p))),s<65536)h+=String.fromCharCode(s);else{var E=s-65536;h+=String.fromCharCode(55296|E>>10,56320|E&1023)}}}Module.UTF8ArrayToString=UTF8ArrayToString;function UTF8ToString(e){return UTF8ArrayToString(HEAPU8,e)}Module.UTF8ToString=UTF8ToString;function stringToUTF8Array(e,t,r,s){if(!(s>0))return 0;for(var a=r,n=r+s-1,c=0;c=55296&&f<=57343&&(f=65536+((f&1023)<<10)|e.charCodeAt(++c)&1023),f<=127){if(r>=n)break;t[r++]=f}else if(f<=2047){if(r+1>=n)break;t[r++]=192|f>>6,t[r++]=128|f&63}else if(f<=65535){if(r+2>=n)break;t[r++]=224|f>>12,t[r++]=128|f>>6&63,t[r++]=128|f&63}else if(f<=2097151){if(r+3>=n)break;t[r++]=240|f>>18,t[r++]=128|f>>12&63,t[r++]=128|f>>6&63,t[r++]=128|f&63}else if(f<=67108863){if(r+4>=n)break;t[r++]=248|f>>24,t[r++]=128|f>>18&63,t[r++]=128|f>>12&63,t[r++]=128|f>>6&63,t[r++]=128|f&63}else{if(r+5>=n)break;t[r++]=252|f>>30,t[r++]=128|f>>24&63,t[r++]=128|f>>18&63,t[r++]=128|f>>12&63,t[r++]=128|f>>6&63,t[r++]=128|f&63}}return t[r]=0,r-a}Module.stringToUTF8Array=stringToUTF8Array;function stringToUTF8(e,t,r){return stringToUTF8Array(e,HEAPU8,t,r)}Module.stringToUTF8=stringToUTF8;function lengthBytesUTF8(e){for(var t=0,r=0;r=55296&&s<=57343&&(s=65536+((s&1023)<<10)|e.charCodeAt(++r)&1023),s<=127?++t:s<=2047?t+=2:s<=65535?t+=3:s<=2097151?t+=4:s<=67108863?t+=5:t+=6}return t}Module.lengthBytesUTF8=lengthBytesUTF8;var UTF16Decoder=typeof TextDecoder<"u"?new TextDecoder("utf-16le"):void 0;function demangle(e){var t=Module.___cxa_demangle||Module.__cxa_demangle;if(t){try{var r=e.substr(1),s=lengthBytesUTF8(r)+1,a=_malloc(s);stringToUTF8(r,a,s);var n=_malloc(4),c=t(a,0,0,n);if(getValue(n,"i32")===0&&c)return Pointer_stringify(c)}catch{}finally{a&&_free(a),n&&_free(n),c&&_free(c)}return e}return Runtime.warnOnce("warning: build with -s DEMANGLE_SUPPORT=1 to link in libcxxabi demangling"),e}function demangleAll(e){var t=/__Z[\w\d_]+/g;return e.replace(t,function(r){var s=demangle(r);return r===s?r:r+" ["+s+"]"})}function jsStackTrace(){var e=new Error;if(!e.stack){try{throw new Error(0)}catch(t){e=t}if(!e.stack)return"(no stack trace available)"}return e.stack.toString()}function stackTrace(){var e=jsStackTrace();return Module.extraStackTrace&&(e+=` `+Module.extraStackTrace()),demangleAll(e)}Module.stackTrace=stackTrace;var HEAP,buffer,HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateGlobalBufferViews(){Module.HEAP8=HEAP8=new Int8Array(buffer),Module.HEAP16=HEAP16=new Int16Array(buffer),Module.HEAP32=HEAP32=new Int32Array(buffer),Module.HEAPU8=HEAPU8=new Uint8Array(buffer),Module.HEAPU16=HEAPU16=new Uint16Array(buffer),Module.HEAPU32=HEAPU32=new Uint32Array(buffer),Module.HEAPF32=HEAPF32=new Float32Array(buffer),Module.HEAPF64=HEAPF64=new Float64Array(buffer)}var STATIC_BASE,STATICTOP,staticSealed,STACK_BASE,STACKTOP,STACK_MAX,DYNAMIC_BASE,DYNAMICTOP_PTR;STATIC_BASE=STATICTOP=STACK_BASE=STACKTOP=STACK_MAX=DYNAMIC_BASE=DYNAMICTOP_PTR=0,staticSealed=!1;function abortOnCannotGrowMemory(){abort("Cannot enlarge memory arrays. Either (1) compile with -s TOTAL_MEMORY=X with X higher than the current value "+TOTAL_MEMORY+", (2) compile with -s ALLOW_MEMORY_GROWTH=1 which allows increasing the size at runtime but prevents some optimizations, (3) set Module.TOTAL_MEMORY to a higher value before the program runs, or (4) if you want malloc to return NULL (0) instead of this abort, compile with -s ABORTING_MALLOC=0 ")}function enlargeMemory(){abortOnCannotGrowMemory()}var TOTAL_STACK=Module.TOTAL_STACK||5242880,TOTAL_MEMORY=Module.TOTAL_MEMORY||134217728;TOTAL_MEMORY0;){var t=e.shift();if(typeof t=="function"){t();continue}var r=t.func;typeof r=="number"?t.arg===void 0?Module.dynCall_v(r):Module.dynCall_vi(r,t.arg):r(t.arg===void 0?null:t.arg)}}var __ATPRERUN__=[],__ATINIT__=[],__ATMAIN__=[],__ATEXIT__=[],__ATPOSTRUN__=[],runtimeInitialized=!1,runtimeExited=!1;function preRun(){if(Module.preRun)for(typeof Module.preRun=="function"&&(Module.preRun=[Module.preRun]);Module.preRun.length;)addOnPreRun(Module.preRun.shift());callRuntimeCallbacks(__ATPRERUN__)}function ensureInitRuntime(){runtimeInitialized||(runtimeInitialized=!0,callRuntimeCallbacks(__ATINIT__))}function preMain(){callRuntimeCallbacks(__ATMAIN__)}function exitRuntime(){callRuntimeCallbacks(__ATEXIT__),runtimeExited=!0}function postRun(){if(Module.postRun)for(typeof Module.postRun=="function"&&(Module.postRun=[Module.postRun]);Module.postRun.length;)addOnPostRun(Module.postRun.shift());callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(e){__ATPRERUN__.unshift(e)}Module.addOnPreRun=addOnPreRun;function addOnInit(e){__ATINIT__.unshift(e)}Module.addOnInit=addOnInit;function addOnPreMain(e){__ATMAIN__.unshift(e)}Module.addOnPreMain=addOnPreMain;function addOnExit(e){__ATEXIT__.unshift(e)}Module.addOnExit=addOnExit;function addOnPostRun(e){__ATPOSTRUN__.unshift(e)}Module.addOnPostRun=addOnPostRun;function intArrayFromString(e,t,r){var s=r>0?r:lengthBytesUTF8(e)+1,a=new Array(s),n=stringToUTF8Array(e,a,0,a.length);return t&&(a.length=n),a}Module.intArrayFromString=intArrayFromString;function intArrayToString(e){for(var t=[],r=0;r255&&(s&=255),t.push(String.fromCharCode(s))}return t.join("")}Module.intArrayToString=intArrayToString;function writeStringToMemory(e,t,r){Runtime.warnOnce("writeStringToMemory is deprecated and should not be called! Use stringToUTF8() instead!");var s,a;r&&(a=t+lengthBytesUTF8(e),s=HEAP8[a]),stringToUTF8(e,t,1/0),r&&(HEAP8[a]=s)}Module.writeStringToMemory=writeStringToMemory;function writeArrayToMemory(e,t){HEAP8.set(e,t)}Module.writeArrayToMemory=writeArrayToMemory;function writeAsciiToMemory(e,t,r){for(var s=0;s>0]=e.charCodeAt(s);r||(HEAP8[t>>0]=0)}if(Module.writeAsciiToMemory=writeAsciiToMemory,(!Math.imul||Math.imul(4294967295,5)!==-5)&&(Math.imul=function e(t,r){var s=t>>>16,a=t&65535,n=r>>>16,c=r&65535;return a*c+(s*c+a*n<<16)|0}),Math.imul=Math.imul,!Math.fround){var froundBuffer=new Float32Array(1);Math.fround=function(e){return froundBuffer[0]=e,froundBuffer[0]}}Math.fround=Math.fround,Math.clz32||(Math.clz32=function(e){e=e>>>0;for(var t=0;t<32;t++)if(e&1<<31-t)return t;return 32}),Math.clz32=Math.clz32,Math.trunc||(Math.trunc=function(e){return e<0?Math.ceil(e):Math.floor(e)}),Math.trunc=Math.trunc;var Math_abs=Math.abs,Math_cos=Math.cos,Math_sin=Math.sin,Math_tan=Math.tan,Math_acos=Math.acos,Math_asin=Math.asin,Math_atan=Math.atan,Math_atan2=Math.atan2,Math_exp=Math.exp,Math_log=Math.log,Math_sqrt=Math.sqrt,Math_ceil=Math.ceil,Math_floor=Math.floor,Math_pow=Math.pow,Math_imul=Math.imul,Math_fround=Math.fround,Math_round=Math.round,Math_min=Math.min,Math_clz32=Math.clz32,Math_trunc=Math.trunc,runDependencies=0,runDependencyWatcher=null,dependenciesFulfilled=null;function getUniqueRunDependency(e){return e}function addRunDependency(e){runDependencies++,Module.monitorRunDependencies&&Module.monitorRunDependencies(runDependencies)}Module.addRunDependency=addRunDependency;function removeRunDependency(e){if(runDependencies--,Module.monitorRunDependencies&&Module.monitorRunDependencies(runDependencies),runDependencies==0&&(runDependencyWatcher!==null&&(clearInterval(runDependencyWatcher),runDependencyWatcher=null),dependenciesFulfilled)){var t=dependenciesFulfilled;dependenciesFulfilled=null,t()}}Module.removeRunDependency=removeRunDependency,Module.preloadedImages={},Module.preloadedAudios={};var ASM_CONSTS=[function(e,t,r,s,a,n,c,f){return _nbind.callbackSignatureList[e].apply(this,arguments)}];function _emscripten_asm_const_iiiiiiii(e,t,r,s,a,n,c,f){return ASM_CONSTS[e](t,r,s,a,n,c,f)}function _emscripten_asm_const_iiiii(e,t,r,s,a){return ASM_CONSTS[e](t,r,s,a)}function _emscripten_asm_const_iiidddddd(e,t,r,s,a,n,c,f,p){return ASM_CONSTS[e](t,r,s,a,n,c,f,p)}function _emscripten_asm_const_iiididi(e,t,r,s,a,n,c){return ASM_CONSTS[e](t,r,s,a,n,c)}function _emscripten_asm_const_iiii(e,t,r,s){return ASM_CONSTS[e](t,r,s)}function _emscripten_asm_const_iiiid(e,t,r,s,a){return ASM_CONSTS[e](t,r,s,a)}function _emscripten_asm_const_iiiiii(e,t,r,s,a,n){return ASM_CONSTS[e](t,r,s,a,n)}STATIC_BASE=Runtime.GLOBAL_BASE,STATICTOP=STATIC_BASE+12800,__ATINIT__.push({func:function(){__GLOBAL__sub_I_Yoga_cpp()}},{func:function(){__GLOBAL__sub_I_nbind_cc()}},{func:function(){__GLOBAL__sub_I_common_cc()}},{func:function(){__GLOBAL__sub_I_Binding_cc()}}),allocate([0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192,127,0,0,192,127,0,0,192,127,0,0,192,127,3,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,3,0,0,0,0,0,192,127,3,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192,127,0,0,192,127,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192,127,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192,127,0,0,192,127,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,0,0,128,191,0,0,128,191,0,0,192,127,0,0,0,0,0,0,0,0,0,0,128,63,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,3,0,0,0,1,0,0,0,2,0,0,0,0,0,0,0,190,12,0,0,200,12,0,0,208,12,0,0,216,12,0,0,230,12,0,0,242,12,0,0,1,0,0,0,3,0,0,0,0,0,0,0,2,0,0,0,0,0,192,127,3,0,0,0,180,45,0,0,181,45,0,0,182,45,0,0,181,45,0,0,182,45,0,0,0,0,0,0,0,0,0,0,1,0,0,0,2,0,0,0,3,0,0,0,1,0,0,0,4,0,0,0,183,45,0,0,181,45,0,0,181,45,0,0,181,45,0,0,181,45,0,0,181,45,0,0,181,45,0,0,184,45,0,0,185,45,0,0,181,45,0,0,181,45,0,0,182,45,0,0,186,45,0,0,185,45,0,0,148,4,0,0,3,0,0,0,187,45,0,0,164,4,0,0,188,45,0,0,2,0,0,0,189,45,0,0,164,4,0,0,188,45,0,0,185,45,0,0,164,4,0,0,185,45,0,0,164,4,0,0,188,45,0,0,181,45,0,0,182,45,0,0,181,45,0,0,0,0,0,0,0,0,0,0,1,0,0,0,5,0,0,0,6,0,0,0,1,0,0,0,7,0,0,0,183,45,0,0,182,45,0,0,181,45,0,0,190,45,0,0,190,45,0,0,182,45,0,0,182,45,0,0,185,45,0,0,181,45,0,0,185,45,0,0,182,45,0,0,181,45,0,0,185,45,0,0,182,45,0,0,185,45,0,0,48,5,0,0,3,0,0,0,56,5,0,0,1,0,0,0,189,45,0,0,185,45,0,0,164,4,0,0,76,5,0,0,2,0,0,0,191,45,0,0,186,45,0,0,182,45,0,0,185,45,0,0,192,45,0,0,185,45,0,0,182,45,0,0,186,45,0,0,185,45,0,0,76,5,0,0,76,5,0,0,136,5,0,0,182,45,0,0,181,45,0,0,2,0,0,0,190,45,0,0,136,5,0,0,56,19,0,0,156,5,0,0,2,0,0,0,184,45,0,0,0,0,0,0,0,0,0,0,1,0,0,0,8,0,0,0,9,0,0,0,1,0,0,0,10,0,0,0,204,5,0,0,181,45,0,0,181,45,0,0,2,0,0,0,180,45,0,0,204,5,0,0,2,0,0,0,195,45,0,0,236,5,0,0,97,19,0,0,198,45,0,0,211,45,0,0,212,45,0,0,213,45,0,0,214,45,0,0,215,45,0,0,188,45,0,0,182,45,0,0,216,45,0,0,217,45,0,0,218,45,0,0,219,45,0,0,192,45,0,0,181,45,0,0,0,0,0,0,185,45,0,0,110,19,0,0,186,45,0,0,115,19,0,0,221,45,0,0,120,19,0,0,148,4,0,0,132,19,0,0,96,6,0,0,145,19,0,0,222,45,0,0,164,19,0,0,223,45,0,0,173,19,0,0,0,0,0,0,3,0,0,0,104,6,0,0,1,0,0,0,187,45,0,0,0,0,0,0,0,0,0,0,1,0,0,0,11,0,0,0,12,0,0,0,1,0,0,0,13,0,0,0,185,45,0,0,224,45,0,0,164,6,0,0,188,45,0,0,172,6,0,0,180,6,0,0,2,0,0,0,188,6,0,0,7,0,0,0,224,45,0,0,7,0,0,0,164,6,0,0,1,0,0,0,213,45,0,0,185,45,0,0,224,45,0,0,172,6,0,0,185,45,0,0,224,45,0,0,164,6,0,0,185,45,0,0,224,45,0,0,211,45,0,0,211,45,0,0,222,45,0,0,211,45,0,0,224,45,0,0,222,45,0,0,211,45,0,0,224,45,0,0,172,6,0,0,222,45,0,0,211,45,0,0,224,45,0,0,188,45,0,0,222,45,0,0,211,45,0,0,40,7,0,0,188,45,0,0,2,0,0,0,224,45,0,0,185,45,0,0,188,45,0,0,188,45,0,0,188,45,0,0,188,45,0,0,222,45,0,0,224,45,0,0,148,4,0,0,185,45,0,0,148,4,0,0,148,4,0,0,148,4,0,0,148,4,0,0,148,4,0,0,185,45,0,0,164,6,0,0,148,4,0,0,0,0,0,0,0,0,0,0,1,0,0,0,14,0,0,0,15,0,0,0,1,0,0,0,16,0,0,0,148,7,0,0,2,0,0,0,225,45,0,0,183,45,0,0,188,45,0,0,168,7,0,0,5,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,2,0,0,0,234,45,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,148,45,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28,9,0,0,5,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,2,0,0,0,242,45,0,0,0,4,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,67,111,117,108,100,32,110,111,116,32,97,108,108,111,99,97,116,101,32,109,101,109,111,114,121,32,102,111,114,32,110,111,100,101,0,67,97,110,110,111,116,32,114,101,115,101,116,32,97,32,110,111,100,101,32,119,104,105,99,104,32,115,116,105,108,108,32,104,97,115,32,99,104,105,108,100,114,101,110,32,97,116,116,97,99,104,101,100,0,67,97,110,110,111,116,32,114,101,115,101,116,32,97,32,110,111,100,101,32,115,116,105,108,108,32,97,116,116,97,99,104,101,100,32,116,111,32,97,32,112,97,114,101,110,116,0,67,111,117,108,100,32,110,111,116,32,97,108,108,111,99,97,116,101,32,109,101,109,111,114,121,32,102,111,114,32,99,111,110,102,105,103,0,67,97,110,110,111,116,32,115,101,116,32,109,101,97,115,117,114,101,32,102,117,110,99,116,105,111,110,58,32,78,111,100,101,115,32,119,105,116,104,32,109,101,97,115,117,114,101,32,102,117,110,99,116,105,111,110,115,32,99,97,110,110,111,116,32,104,97,118,101,32,99,104,105,108,100,114,101,110,46,0,67,104,105,108,100,32,97,108,114,101,97,100,121,32,104,97,115,32,97,32,112,97,114,101,110,116,44,32,105,116,32,109,117,115,116,32,98,101,32,114,101,109,111,118,101,100,32,102,105,114,115,116,46,0,67,97,110,110,111,116,32,97,100,100,32,99,104,105,108,100,58,32,78,111,100,101,115,32,119,105,116,104,32,109,101,97,115,117,114,101,32,102,117,110,99,116,105,111,110,115,32,99,97,110,110,111,116,32,104,97,118,101,32,99,104,105,108,100,114,101,110,46,0,79,110,108,121,32,108,101,97,102,32,110,111,100,101,115,32,119,105,116,104,32,99,117,115,116,111,109,32,109,101,97,115,117,114,101,32,102,117,110,99,116,105,111,110,115,115,104,111,117,108,100,32,109,97,110,117,97,108,108,121,32,109,97,114,107,32,116,104,101,109,115,101,108,118,101,115,32,97,115,32,100,105,114,116,121,0,67,97,110,110,111,116,32,103,101,116,32,108,97,121,111,117,116,32,112,114,111,112,101,114,116,105,101,115,32,111,102,32,109,117,108,116,105,45,101,100,103,101,32,115,104,111,114,116,104,97,110,100,115,0,37,115,37,100,46,123,91,115,107,105,112,112,101,100,93,32,0,119,109,58,32,37,115,44,32,104,109,58,32,37,115,44,32,97,119,58,32,37,102,32,97,104,58,32,37,102,32,61,62,32,100,58,32,40,37,102,44,32,37,102,41,32,37,115,10,0,37,115,37,100,46,123,37,115,0,42,0,119,109,58,32,37,115,44,32,104,109,58,32,37,115,44,32,97,119,58,32,37,102,32,97,104,58,32,37,102,32,37,115,10,0,37,115,37,100,46,125,37,115,0,119,109,58,32,37,115,44,32,104,109,58,32,37,115,44,32,100,58,32,40,37,102,44,32,37,102,41,32,37,115,10,0,79,117,116,32,111,102,32,99,97,99,104,101,32,101,110,116,114,105,101,115,33,10,0,83,99,97,108,101,32,102,97,99,116,111,114,32,115,104,111,117,108,100,32,110,111,116,32,98,101,32,108,101,115,115,32,116,104,97,110,32,122,101,114,111,0,105,110,105,116,105,97,108,0,37,115,10,0,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,0,85,78,68,69,70,73,78,69,68,0,69,88,65,67,84,76,89,0,65,84,95,77,79,83,84,0,76,65,89,95,85,78,68,69,70,73,78,69,68,0,76,65,89,95,69,88,65,67,84,76,89,0,76,65,89,95,65,84,95,77,79,83,84,0,97,118,97,105,108,97,98,108,101,87,105,100,116,104,32,105,115,32,105,110,100,101,102,105,110,105,116,101,32,115,111,32,119,105,100,116,104,77,101,97,115,117,114,101,77,111,100,101,32,109,117,115,116,32,98,101,32,89,71,77,101,97,115,117,114,101,77,111,100,101,85,110,100,101,102,105,110,101,100,0,97,118,97,105,108,97,98,108,101,72,101,105,103,104,116,32,105,115,32,105,110,100,101,102,105,110,105,116,101,32,115,111,32,104,101,105,103,104,116,77,101,97,115,117,114,101,77,111,100,101,32,109,117,115,116,32,98,101,32,89,71,77,101,97,115,117,114,101,77,111,100,101,85,110,100,101,102,105,110,101,100,0,102,108,101,120,0,115,116,114,101,116,99,104,0,109,117,108,116,105,108,105,110,101,45,115,116,114,101,116,99,104,0,69,120,112,101,99,116,101,100,32,110,111,100,101,32,116,111,32,104,97,118,101,32,99,117,115,116,111,109,32,109,101,97,115,117,114,101,32,102,117,110,99,116,105,111,110,0,109,101,97,115,117,114,101,0,69,120,112,101,99,116,32,99,117,115,116,111,109,32,98,97,115,101,108,105,110,101,32,102,117,110,99,116,105,111,110,32,116,111,32,110,111,116,32,114,101,116,117,114,110,32,78,97,78,0,97,98,115,45,109,101,97,115,117,114,101,0,97,98,115,45,108,97,121,111,117,116,0,78,111,100,101,0,99,114,101,97,116,101,68,101,102,97,117,108,116,0,99,114,101,97,116,101,87,105,116,104,67,111,110,102,105,103,0,100,101,115,116,114,111,121,0,114,101,115,101,116,0,99,111,112,121,83,116,121,108,101,0,115,101,116,80,111,115,105,116,105,111,110,84,121,112,101,0,115,101,116,80,111,115,105,116,105,111,110,0,115,101,116,80,111,115,105,116,105,111,110,80,101,114,99,101,110,116,0,115,101,116,65,108,105,103,110,67,111,110,116,101,110,116,0,115,101,116,65,108,105,103,110,73,116,101,109,115,0,115,101,116,65,108,105,103,110,83,101,108,102,0,115,101,116,70,108,101,120,68,105,114,101,99,116,105,111,110,0,115,101,116,70,108,101,120,87,114,97,112,0,115,101,116,74,117,115,116,105,102,121,67,111,110,116,101,110,116,0,115,101,116,77,97,114,103,105,110,0,115,101,116,77,97,114,103,105,110,80,101,114,99,101,110,116,0,115,101,116,77,97,114,103,105,110,65,117,116,111,0,115,101,116,79,118,101,114,102,108,111,119,0,115,101,116,68,105,115,112,108,97,121,0,115,101,116,70,108,101,120,0,115,101,116,70,108,101,120,66,97,115,105,115,0,115,101,116,70,108,101,120,66,97,115,105,115,80,101,114,99,101,110,116,0,115,101,116,70,108,101,120,71,114,111,119,0,115,101,116,70,108,101,120,83,104,114,105,110,107,0,115,101,116,87,105,100,116,104,0,115,101,116,87,105,100,116,104,80,101,114,99,101,110,116,0,115,101,116,87,105,100,116,104,65,117,116,111,0,115,101,116,72,101,105,103,104,116,0,115,101,116,72,101,105,103,104,116,80,101,114,99,101,110,116,0,115,101,116,72,101,105,103,104,116,65,117,116,111,0,115,101,116,77,105,110,87,105,100,116,104,0,115,101,116,77,105,110,87,105,100,116,104,80,101,114,99,101,110,116,0,115,101,116,77,105,110,72,101,105,103,104,116,0,115,101,116,77,105,110,72,101,105,103,104,116,80,101,114,99,101,110,116,0,115,101,116,77,97,120,87,105,100,116,104,0,115,101,116,77,97,120,87,105,100,116,104,80,101,114,99,101,110,116,0,115,101,116,77,97,120,72,101,105,103,104,116,0,115,101,116,77,97,120,72,101,105,103,104,116,80,101,114,99,101,110,116,0,115,101,116,65,115,112,101,99,116,82,97,116,105,111,0,115,101,116,66,111,114,100,101,114,0,115,101,116,80,97,100,100,105,110,103,0,115,101,116,80,97,100,100,105,110,103,80,101,114,99,101,110,116,0,103,101,116,80,111,115,105,116,105,111,110,84,121,112,101,0,103,101,116,80,111,115,105,116,105,111,110,0,103,101,116,65,108,105,103,110,67,111,110,116,101,110,116,0,103,101,116,65,108,105,103,110,73,116,101,109,115,0,103,101,116,65,108,105,103,110,83,101,108,102,0,103,101,116,70,108,101,120,68,105,114,101,99,116,105,111,110,0,103,101,116,70,108,101,120,87,114,97,112,0,103,101,116,74,117,115,116,105,102,121,67,111,110,116,101,110,116,0,103,101,116,77,97,114,103,105,110,0,103,101,116,70,108,101,120,66,97,115,105,115,0,103,101,116,70,108,101,120,71,114,111,119,0,103,101,116,70,108,101,120,83,104,114,105,110,107,0,103,101,116,87,105,100,116,104,0,103,101,116,72,101,105,103,104,116,0,103,101,116,77,105,110,87,105,100,116,104,0,103,101,116,77,105,110,72,101,105,103,104,116,0,103,101,116,77,97,120,87,105,100,116,104,0,103,101,116,77,97,120,72,101,105,103,104,116,0,103,101,116,65,115,112,101,99,116,82,97,116,105,111,0,103,101,116,66,111,114,100,101,114,0,103,101,116,79,118,101,114,102,108,111,119,0,103,101,116,68,105,115,112,108,97,121,0,103,101,116,80,97,100,100,105,110,103,0,105,110,115,101,114,116,67,104,105,108,100,0,114,101,109,111,118,101,67,104,105,108,100,0,103,101,116,67,104,105,108,100,67,111,117,110,116,0,103,101,116,80,97,114,101,110,116,0,103,101,116,67,104,105,108,100,0,115,101,116,77,101,97,115,117,114,101,70,117,110,99,0,117,110,115,101,116,77,101,97,115,117,114,101,70,117,110,99,0,109,97,114,107,68,105,114,116,121,0,105,115,68,105,114,116,121,0,99,97,108,99,117,108,97,116,101,76,97,121,111,117,116,0,103,101,116,67,111,109,112,117,116,101,100,76,101,102,116,0,103,101,116,67,111,109,112,117,116,101,100,82,105,103,104,116,0,103,101,116,67,111,109,112,117,116,101,100,84,111,112,0,103,101,116,67,111,109,112,117,116,101,100,66,111,116,116,111,109,0,103,101,116,67,111,109,112,117,116,101,100,87,105,100,116,104,0,103,101,116,67,111,109,112,117,116,101,100,72,101,105,103,104,116,0,103,101,116,67,111,109,112,117,116,101,100,76,97,121,111,117,116,0,103,101,116,67,111,109,112,117,116,101,100,77,97,114,103,105,110,0,103,101,116,67,111,109,112,117,116,101,100,66,111,114,100,101,114,0,103,101,116,67,111,109,112,117,116,101,100,80,97,100,100,105,110,103,0,67,111,110,102,105,103,0,99,114,101,97,116,101,0,115,101,116,69,120,112,101,114,105,109,101,110,116,97,108,70,101,97,116,117,114,101,69,110,97,98,108,101,100,0,115,101,116,80,111,105,110,116,83,99,97,108,101,70,97,99,116,111,114,0,105,115,69,120,112,101,114,105,109,101,110,116,97,108,70,101,97,116,117,114,101,69,110,97,98,108,101,100,0,86,97,108,117,101,0,76,97,121,111,117,116,0,83,105,122,101,0,103,101,116,73,110,115,116,97,110,99,101,67,111,117,110,116,0,73,110,116,54,52,0,1,1,1,2,2,4,4,4,4,8,8,4,8,118,111,105,100,0,98,111,111,108,0,115,116,100,58,58,115,116,114,105,110,103,0,99,98,70,117,110,99,116,105,111,110,32,38,0,99,111,110,115,116,32,99,98,70,117,110,99,116,105,111,110,32,38,0,69,120,116,101,114,110,97,108,0,66,117,102,102,101,114,0,78,66,105,110,100,73,68,0,78,66,105,110,100,0,98,105,110,100,95,118,97,108,117,101,0,114,101,102,108,101,99,116,0,113,117,101,114,121,84,121,112,101,0,108,97,108,108,111,99,0,108,114,101,115,101,116,0,123,114,101,116,117,114,110,40,95,110,98,105,110,100,46,99,97,108,108,98,97,99,107,83,105,103,110,97,116,117,114,101,76,105,115,116,91,36,48,93,46,97,112,112,108,121,40,116,104,105,115,44,97,114,103,117,109,101,110,116,115,41,41,59,125,0,95,110,98,105,110,100,95,110,101,119,0,17,0,10,0,17,17,17,0,0,0,0,5,0,0,0,0,0,0,9,0,0,0,0,11,0,0,0,0,0,0,0,0,17,0,15,10,17,17,17,3,10,7,0,1,19,9,11,11,0,0,9,6,11,0,0,11,0,6,17,0,0,0,17,17,17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,17,0,10,10,17,17,17,0,10,0,0,2,0,9,11,0,0,0,9,0,11,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,12,0,0,0,0,9,12,0,0,0,0,0,12,0,0,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,0,0,0,0,0,0,0,0,0,0,0,13,0,0,0,4,13,0,0,0,0,9,14,0,0,0,0,0,14,0,0,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16,0,0,0,0,0,0,0,0,0,0,0,15,0,0,0,0,15,0,0,0,0,9,16,0,0,0,0,0,16,0,0,16,0,0,18,0,0,0,18,18,18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,18,0,0,0,18,18,18,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,10,0,0,0,0,9,11,0,0,0,0,0,11,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,12,0,0,0,0,9,12,0,0,0,0,0,12,0,0,12,0,0,45,43,32,32,32,48,88,48,120,0,40,110,117,108,108,41,0,45,48,88,43,48,88,32,48,88,45,48,120,43,48,120,32,48,120,0,105,110,102,0,73,78,70,0,110,97,110,0,78,65,78,0,48,49,50,51,52,53,54,55,56,57,65,66,67,68,69,70,46,0,84,33,34,25,13,1,2,3,17,75,28,12,16,4,11,29,18,30,39,104,110,111,112,113,98,32,5,6,15,19,20,21,26,8,22,7,40,36,23,24,9,10,14,27,31,37,35,131,130,125,38,42,43,60,61,62,63,67,71,74,77,88,89,90,91,92,93,94,95,96,97,99,100,101,102,103,105,106,107,108,114,115,116,121,122,123,124,0,73,108,108,101,103,97,108,32,98,121,116,101,32,115,101,113,117,101,110,99,101,0,68,111,109,97,105,110,32,101,114,114,111,114,0,82,101,115,117,108,116,32,110,111,116,32,114,101,112,114,101,115,101,110,116,97,98,108,101,0,78,111,116,32,97,32,116,116,121,0,80,101,114,109,105,115,115,105,111,110,32,100,101,110,105,101,100,0,79,112,101,114,97,116,105,111,110,32,110,111,116,32,112,101,114,109,105,116,116,101,100,0,78,111,32,115,117,99,104,32,102,105,108,101,32,111,114,32,100,105,114,101,99,116,111,114,121,0,78,111,32,115,117,99,104,32,112,114,111,99,101,115,115,0,70,105,108,101,32,101,120,105,115,116,115,0,86,97,108,117,101,32,116,111,111,32,108,97,114,103,101,32,102,111,114,32,100,97,116,97,32,116,121,112,101,0,78,111,32,115,112,97,99,101,32,108,101,102,116,32,111,110,32,100,101,118,105,99,101,0,79,117,116,32,111,102,32,109,101,109,111,114,121,0,82,101,115,111,117,114,99,101,32,98,117,115,121,0,73,110,116,101,114,114,117,112,116,101,100,32,115,121,115,116,101,109,32,99,97,108,108,0,82,101,115,111,117,114,99,101,32,116,101,109,112,111,114,97,114,105,108,121,32,117,110,97,118,97,105,108,97,98,108,101,0,73,110,118,97,108,105,100,32,115,101,101,107,0,67,114,111,115,115,45,100,101,118,105,99,101,32,108,105,110,107,0,82,101,97,100,45,111,110,108,121,32,102,105,108,101,32,115,121,115,116,101,109,0,68,105,114,101,99,116,111,114,121,32,110,111,116,32,101,109,112,116,121,0,67,111,110,110,101,99,116,105,111,110,32,114,101,115,101,116,32,98,121,32,112,101,101,114,0,79,112,101,114,97,116,105,111,110,32,116,105,109,101,100,32,111,117,116,0,67,111,110,110,101,99,116,105,111,110,32,114,101,102,117,115,101,100,0,72,111,115,116,32,105,115,32,100,111,119,110,0,72,111,115,116,32,105,115,32,117,110,114,101,97,99,104,97,98,108,101,0,65,100,100,114,101,115,115,32,105,110,32,117,115,101,0,66,114,111,107,101,110,32,112,105,112,101,0,73,47,79,32,101,114,114,111,114,0,78,111,32,115,117,99,104,32,100,101,118,105,99,101,32,111,114,32,97,100,100,114,101,115,115,0,66,108,111,99,107,32,100,101,118,105,99,101,32,114,101,113,117,105,114,101,100,0,78,111,32,115,117,99,104,32,100,101,118,105,99,101,0,78,111,116,32,97,32,100,105,114,101,99,116,111,114,121,0,73,115,32,97,32,100,105,114,101,99,116,111,114,121,0,84,101,120,116,32,102,105,108,101,32,98,117,115,121,0,69,120,101,99,32,102,111,114,109,97,116,32,101,114,114,111,114,0,73,110,118,97,108,105,100,32,97,114,103,117,109,101,110,116,0,65,114,103,117,109,101,110,116,32,108,105,115,116,32,116,111,111,32,108,111,110,103,0,83,121,109,98,111,108,105,99,32,108,105,110,107,32,108,111,111,112,0,70,105,108,101,110,97,109,101,32,116,111,111,32,108,111,110,103,0,84,111,111,32,109,97,110,121,32,111,112,101,110,32,102,105,108,101,115,32,105,110,32,115,121,115,116,101,109,0,78,111,32,102,105,108,101,32,100,101,115,99,114,105,112,116,111,114,115,32,97,118,97,105,108,97,98,108,101,0,66,97,100,32,102,105,108,101,32,100,101,115,99,114,105,112,116,111,114,0,78,111,32,99,104,105,108,100,32,112,114,111,99,101,115,115,0,66,97,100,32,97,100,100,114,101,115,115,0,70,105,108,101,32,116,111,111,32,108,97,114,103,101,0,84,111,111,32,109,97,110,121,32,108,105,110,107,115,0,78,111,32,108,111,99,107,115,32,97,118,97,105,108,97,98,108,101,0,82,101,115,111,117,114,99,101,32,100,101,97,100,108,111,99,107,32,119,111,117,108,100,32,111,99,99,117,114,0,83,116,97,116,101,32,110,111,116,32,114,101,99,111,118,101,114,97,98,108,101,0,80,114,101,118,105,111,117,115,32,111,119,110,101,114,32,100,105,101,100,0,79,112,101,114,97,116,105,111,110,32,99,97,110,99,101,108,101,100,0,70,117,110,99,116,105,111,110,32,110,111,116,32,105,109,112,108,101,109,101,110,116,101,100,0,78,111,32,109,101,115,115,97,103,101,32,111,102,32,100,101,115,105,114,101,100,32,116,121,112,101,0,73,100,101,110,116,105,102,105,101,114,32,114,101,109,111,118,101,100,0,68,101,118,105,99,101,32,110,111,116,32,97,32,115,116,114,101,97,109,0,78,111,32,100,97,116,97,32,97,118,97,105,108,97,98,108,101,0,68,101,118,105,99,101,32,116,105,109,101,111,117,116,0,79,117,116,32,111,102,32,115,116,114,101,97,109,115,32,114,101,115,111,117,114,99,101,115,0,76,105,110,107,32,104,97,115,32,98,101,101,110,32,115,101,118,101,114,101,100,0,80,114,111,116,111,99,111,108,32,101,114,114,111,114,0,66,97,100,32,109,101,115,115,97,103,101,0,70,105,108,101,32,100,101,115,99,114,105,112,116,111,114,32,105,110,32,98,97,100,32,115,116,97,116,101,0,78,111,116,32,97,32,115,111,99,107,101,116,0,68,101,115,116,105,110,97,116,105,111,110,32,97,100,100,114,101,115,115,32,114,101,113,117,105,114,101,100,0,77,101,115,115,97,103,101,32,116,111,111,32,108,97,114,103,101,0,80,114,111,116,111,99,111,108,32,119,114,111,110,103,32,116,121,112,101,32,102,111,114,32,115,111,99,107,101,116,0,80,114,111,116,111,99,111,108,32,110,111,116,32,97,118,97,105,108,97,98,108,101,0,80,114,111,116,111,99,111,108,32,110,111,116,32,115,117,112,112,111,114,116,101,100,0,83,111,99,107,101,116,32,116,121,112,101,32,110,111,116,32,115,117,112,112,111,114,116,101,100,0,78,111,116,32,115,117,112,112,111,114,116,101,100,0,80,114,111,116,111,99,111,108,32,102,97,109,105,108,121,32,110,111,116,32,115,117,112,112,111,114,116,101,100,0,65,100,100,114,101,115,115,32,102,97,109,105,108,121,32,110,111,116,32,115,117,112,112,111,114,116,101,100,32,98,121,32,112,114,111,116,111,99,111,108,0,65,100,100,114,101,115,115,32,110,111,116,32,97,118,97,105,108,97,98,108,101,0,78,101,116,119,111,114,107,32,105,115,32,100,111,119,110,0,78,101,116,119,111,114,107,32,117,110,114,101,97,99,104,97,98,108,101,0,67,111,110,110,101,99,116,105,111,110,32,114,101,115,101,116,32,98,121,32,110,101,116,119,111,114,107,0,67,111,110,110,101,99,116,105,111,110,32,97,98,111,114,116,101,100,0,78,111,32,98,117,102,102,101,114,32,115,112,97,99,101,32,97,118,97,105,108,97,98,108,101,0,83,111,99,107,101,116,32,105,115,32,99,111,110,110,101,99,116,101,100,0,83,111,99,107,101,116,32,110,111,116,32,99,111,110,110,101,99,116,101,100,0,67,97,110,110,111,116,32,115,101,110,100,32,97,102,116,101,114,32,115,111,99,107,101,116,32,115,104,117,116,100,111,119,110,0,79,112,101,114,97,116,105,111,110,32,97,108,114,101,97,100,121,32,105,110,32,112,114,111,103,114,101,115,115,0,79,112,101,114,97,116,105,111,110,32,105,110,32,112,114,111,103,114,101,115,115,0,83,116,97,108,101,32,102,105,108,101,32,104,97,110,100,108,101,0,82,101,109,111,116,101,32,73,47,79,32,101,114,114,111,114,0,81,117,111,116,97,32,101,120,99,101,101,100,101,100,0,78,111,32,109,101,100,105,117,109,32,102,111,117,110,100,0,87,114,111,110,103,32,109,101,100,105,117,109,32,116,121,112,101,0,78,111,32,101,114,114,111,114,32,105,110,102,111,114,109,97,116,105,111,110,0,0],"i8",ALLOC_NONE,Runtime.GLOBAL_BASE);var tempDoublePtr=STATICTOP;STATICTOP+=16;function _atexit(e,t){__ATEXIT__.unshift({func:e,arg:t})}function ___cxa_atexit(){return _atexit.apply(null,arguments)}function _abort(){Module.abort()}function __ZN8facebook4yoga14YGNodeToStringEPNSt3__212basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEP6YGNode14YGPrintOptionsj(){Module.printErr("missing function: _ZN8facebook4yoga14YGNodeToStringEPNSt3__212basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEP6YGNode14YGPrintOptionsj"),abort(-1)}function __decorate(e,t,r,s){var a=arguments.length,n=a<3?t:s===null?s=Object.getOwnPropertyDescriptor(t,r):s,c;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")n=Reflect.decorate(e,t,r,s);else for(var f=e.length-1;f>=0;f--)(c=e[f])&&(n=(a<3?c(n):a>3?c(t,r,n):c(t,r))||n);return a>3&&n&&Object.defineProperty(t,r,n),n}function _defineHidden(e){return function(t,r){Object.defineProperty(t,r,{configurable:!1,enumerable:!1,value:e,writable:!0})}}var _nbind={};function __nbind_free_external(e){_nbind.externalList[e].dereference(e)}function __nbind_reference_external(e){_nbind.externalList[e].reference()}function _llvm_stackrestore(e){var t=_llvm_stacksave,r=t.LLVM_SAVEDSTACKS[e];t.LLVM_SAVEDSTACKS.splice(e,1),Runtime.stackRestore(r)}function __nbind_register_pool(e,t,r,s){_nbind.Pool.pageSize=e,_nbind.Pool.usedPtr=t/4,_nbind.Pool.rootPtr=r,_nbind.Pool.pagePtr=s/4,HEAP32[t/4]=16909060,HEAP8[t]==1&&(_nbind.bigEndian=!0),HEAP32[t/4]=0,_nbind.makeTypeKindTbl=(n={},n[1024]=_nbind.PrimitiveType,n[64]=_nbind.Int64Type,n[2048]=_nbind.BindClass,n[3072]=_nbind.BindClassPtr,n[4096]=_nbind.SharedClassPtr,n[5120]=_nbind.ArrayType,n[6144]=_nbind.ArrayType,n[7168]=_nbind.CStringType,n[9216]=_nbind.CallbackType,n[10240]=_nbind.BindType,n),_nbind.makeTypeNameTbl={Buffer:_nbind.BufferType,External:_nbind.ExternalType,Int64:_nbind.Int64Type,_nbind_new:_nbind.CreateValueType,bool:_nbind.BooleanType,"cbFunction &":_nbind.CallbackType,"const cbFunction &":_nbind.CallbackType,"const std::string &":_nbind.StringType,"std::string":_nbind.StringType},Module.toggleLightGC=_nbind.toggleLightGC,_nbind.callUpcast=Module.dynCall_ii;var a=_nbind.makeType(_nbind.constructType,{flags:2048,id:0,name:""});a.proto=Module,_nbind.BindClass.list.push(a);var n}function _emscripten_set_main_loop_timing(e,t){if(Browser.mainLoop.timingMode=e,Browser.mainLoop.timingValue=t,!Browser.mainLoop.func)return 1;if(e==0)Browser.mainLoop.scheduler=function(){var c=Math.max(0,Browser.mainLoop.tickStartTime+t-_emscripten_get_now())|0;setTimeout(Browser.mainLoop.runner,c)},Browser.mainLoop.method="timeout";else if(e==1)Browser.mainLoop.scheduler=function(){Browser.requestAnimationFrame(Browser.mainLoop.runner)},Browser.mainLoop.method="rAF";else if(e==2){if(!window.setImmediate){let n=function(c){c.source===window&&c.data===s&&(c.stopPropagation(),r.shift()())};var a=n,r=[],s="setimmediate";window.addEventListener("message",n,!0),window.setImmediate=function(f){r.push(f),ENVIRONMENT_IS_WORKER?(Module.setImmediates===void 0&&(Module.setImmediates=[]),Module.setImmediates.push(f),window.postMessage({target:s})):window.postMessage(s,"*")}}Browser.mainLoop.scheduler=function(){window.setImmediate(Browser.mainLoop.runner)},Browser.mainLoop.method="immediate"}return 0}function _emscripten_get_now(){abort()}function _emscripten_set_main_loop(e,t,r,s,a){Module.noExitRuntime=!0,assert(!Browser.mainLoop.func,"emscripten_set_main_loop: there can only be one main loop function at once: call emscripten_cancel_main_loop to cancel the previous one before setting a new one with different parameters."),Browser.mainLoop.func=e,Browser.mainLoop.arg=s;var n;typeof s<"u"?n=function(){Module.dynCall_vi(e,s)}:n=function(){Module.dynCall_v(e)};var c=Browser.mainLoop.currentlyRunningMainloop;if(Browser.mainLoop.runner=function(){if(!ABORT){if(Browser.mainLoop.queue.length>0){var p=Date.now(),h=Browser.mainLoop.queue.shift();if(h.func(h.arg),Browser.mainLoop.remainingBlockers){var E=Browser.mainLoop.remainingBlockers,C=E%1==0?E-1:Math.floor(E);h.counted?Browser.mainLoop.remainingBlockers=C:(C=C+.5,Browser.mainLoop.remainingBlockers=(8*E+C)/9)}if(console.log('main loop blocker "'+h.name+'" took '+(Date.now()-p)+" ms"),Browser.mainLoop.updateStatus(),c1&&Browser.mainLoop.currentFrameNumber%Browser.mainLoop.timingValue!=0){Browser.mainLoop.scheduler();return}else Browser.mainLoop.timingMode==0&&(Browser.mainLoop.tickStartTime=_emscripten_get_now());Browser.mainLoop.method==="timeout"&&Module.ctx&&(Module.printErr("Looks like you are rendering without using requestAnimationFrame for the main loop. You should use 0 for the frame rate in emscripten_set_main_loop in order to use requestAnimationFrame, as that can greatly improve your frame rates!"),Browser.mainLoop.method=""),Browser.mainLoop.runIter(n),!(c0?_emscripten_set_main_loop_timing(0,1e3/t):_emscripten_set_main_loop_timing(1,1),Browser.mainLoop.scheduler()),r)throw"SimulateInfiniteLoop"}var Browser={mainLoop:{scheduler:null,method:"",currentlyRunningMainloop:0,func:null,arg:0,timingMode:0,timingValue:0,currentFrameNumber:0,queue:[],pause:function(){Browser.mainLoop.scheduler=null,Browser.mainLoop.currentlyRunningMainloop++},resume:function(){Browser.mainLoop.currentlyRunningMainloop++;var e=Browser.mainLoop.timingMode,t=Browser.mainLoop.timingValue,r=Browser.mainLoop.func;Browser.mainLoop.func=null,_emscripten_set_main_loop(r,0,!1,Browser.mainLoop.arg,!0),_emscripten_set_main_loop_timing(e,t),Browser.mainLoop.scheduler()},updateStatus:function(){if(Module.setStatus){var e=Module.statusMessage||"Please wait...",t=Browser.mainLoop.remainingBlockers,r=Browser.mainLoop.expectedBlockers;t?t"u"&&(console.log("warning: Browser does not support creating object URLs. Built-in browser image decoding will not be available."),Module.noImageDecoding=!0);var e={};e.canHandle=function(n){return!Module.noImageDecoding&&/\.(jpg|jpeg|png|bmp)$/i.test(n)},e.handle=function(n,c,f,p){var h=null;if(Browser.hasBlobConstructor)try{h=new Blob([n],{type:Browser.getMimetype(c)}),h.size!==n.length&&(h=new Blob([new Uint8Array(n).buffer],{type:Browser.getMimetype(c)}))}catch(x){Runtime.warnOnce("Blob constructor present but fails: "+x+"; falling back to blob builder")}if(!h){var E=new Browser.BlobBuilder;E.append(new Uint8Array(n).buffer),h=E.getBlob()}var C=Browser.URLObject.createObjectURL(h),S=new Image;S.onload=function(){assert(S.complete,"Image "+c+" could not be decoded");var I=document.createElement("canvas");I.width=S.width,I.height=S.height;var T=I.getContext("2d");T.drawImage(S,0,0),Module.preloadedImages[c]=I,Browser.URLObject.revokeObjectURL(C),f&&f(n)},S.onerror=function(I){console.log("Image "+C+" could not be decoded"),p&&p()},S.src=C},Module.preloadPlugins.push(e);var t={};t.canHandle=function(n){return!Module.noAudioDecoding&&n.substr(-4)in{".ogg":1,".wav":1,".mp3":1}},t.handle=function(n,c,f,p){var h=!1;function E(T){h||(h=!0,Module.preloadedAudios[c]=T,f&&f(n))}function C(){h||(h=!0,Module.preloadedAudios[c]=new Audio,p&&p())}if(Browser.hasBlobConstructor){try{var S=new Blob([n],{type:Browser.getMimetype(c)})}catch{return C()}var x=Browser.URLObject.createObjectURL(S),I=new Audio;I.addEventListener("canplaythrough",function(){E(I)},!1),I.onerror=function(O){if(h)return;console.log("warning: browser could not fully decode audio "+c+", trying slower base64 approach");function U(V){for(var te="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",ie="=",ue="",ae=0,ge=0,Ae=0;Ae=6;){var Ce=ae>>ge-6&63;ge-=6,ue+=te[Ce]}return ge==2?(ue+=te[(ae&3)<<4],ue+=ie+ie):ge==4&&(ue+=te[(ae&15)<<2],ue+=ie),ue}I.src="data:audio/x-"+c.substr(-3)+";base64,"+U(n),E(I)},I.src=x,Browser.safeSetTimeout(function(){E(I)},1e4)}else return C()},Module.preloadPlugins.push(t);function r(){Browser.pointerLock=document.pointerLockElement===Module.canvas||document.mozPointerLockElement===Module.canvas||document.webkitPointerLockElement===Module.canvas||document.msPointerLockElement===Module.canvas}var s=Module.canvas;s&&(s.requestPointerLock=s.requestPointerLock||s.mozRequestPointerLock||s.webkitRequestPointerLock||s.msRequestPointerLock||function(){},s.exitPointerLock=document.exitPointerLock||document.mozExitPointerLock||document.webkitExitPointerLock||document.msExitPointerLock||function(){},s.exitPointerLock=s.exitPointerLock.bind(document),document.addEventListener("pointerlockchange",r,!1),document.addEventListener("mozpointerlockchange",r,!1),document.addEventListener("webkitpointerlockchange",r,!1),document.addEventListener("mspointerlockchange",r,!1),Module.elementPointerLock&&s.addEventListener("click",function(a){!Browser.pointerLock&&Module.canvas.requestPointerLock&&(Module.canvas.requestPointerLock(),a.preventDefault())},!1))},createContext:function(e,t,r,s){if(t&&Module.ctx&&e==Module.canvas)return Module.ctx;var a,n;if(t){var c={antialias:!1,alpha:!1};if(s)for(var f in s)c[f]=s[f];n=GL.createContext(e,c),n&&(a=GL.getContext(n).GLctx)}else a=e.getContext("2d");return a?(r&&(t||assert(typeof GLctx>"u","cannot set in module if GLctx is used, but we are a non-GL context that would replace it"),Module.ctx=a,t&&GL.makeContextCurrent(n),Module.useWebGL=t,Browser.moduleContextCreatedCallbacks.forEach(function(p){p()}),Browser.init()),a):null},destroyContext:function(e,t,r){},fullscreenHandlersInstalled:!1,lockPointer:void 0,resizeCanvas:void 0,requestFullscreen:function(e,t,r){Browser.lockPointer=e,Browser.resizeCanvas=t,Browser.vrDevice=r,typeof Browser.lockPointer>"u"&&(Browser.lockPointer=!0),typeof Browser.resizeCanvas>"u"&&(Browser.resizeCanvas=!1),typeof Browser.vrDevice>"u"&&(Browser.vrDevice=null);var s=Module.canvas;function a(){Browser.isFullscreen=!1;var c=s.parentNode;(document.fullscreenElement||document.mozFullScreenElement||document.msFullscreenElement||document.webkitFullscreenElement||document.webkitCurrentFullScreenElement)===c?(s.exitFullscreen=document.exitFullscreen||document.cancelFullScreen||document.mozCancelFullScreen||document.msExitFullscreen||document.webkitCancelFullScreen||function(){},s.exitFullscreen=s.exitFullscreen.bind(document),Browser.lockPointer&&s.requestPointerLock(),Browser.isFullscreen=!0,Browser.resizeCanvas&&Browser.setFullscreenCanvasSize()):(c.parentNode.insertBefore(s,c),c.parentNode.removeChild(c),Browser.resizeCanvas&&Browser.setWindowedCanvasSize()),Module.onFullScreen&&Module.onFullScreen(Browser.isFullscreen),Module.onFullscreen&&Module.onFullscreen(Browser.isFullscreen),Browser.updateCanvasDimensions(s)}Browser.fullscreenHandlersInstalled||(Browser.fullscreenHandlersInstalled=!0,document.addEventListener("fullscreenchange",a,!1),document.addEventListener("mozfullscreenchange",a,!1),document.addEventListener("webkitfullscreenchange",a,!1),document.addEventListener("MSFullscreenChange",a,!1));var n=document.createElement("div");s.parentNode.insertBefore(n,s),n.appendChild(s),n.requestFullscreen=n.requestFullscreen||n.mozRequestFullScreen||n.msRequestFullscreen||(n.webkitRequestFullscreen?function(){n.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT)}:null)||(n.webkitRequestFullScreen?function(){n.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT)}:null),r?n.requestFullscreen({vrDisplay:r}):n.requestFullscreen()},requestFullScreen:function(e,t,r){return Module.printErr("Browser.requestFullScreen() is deprecated. Please call Browser.requestFullscreen instead."),Browser.requestFullScreen=function(s,a,n){return Browser.requestFullscreen(s,a,n)},Browser.requestFullscreen(e,t,r)},nextRAF:0,fakeRequestAnimationFrame:function(e){var t=Date.now();if(Browser.nextRAF===0)Browser.nextRAF=t+1e3/60;else for(;t+2>=Browser.nextRAF;)Browser.nextRAF+=1e3/60;var r=Math.max(Browser.nextRAF-t,0);setTimeout(e,r)},requestAnimationFrame:function e(t){typeof window>"u"?Browser.fakeRequestAnimationFrame(t):(window.requestAnimationFrame||(window.requestAnimationFrame=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame||window.oRequestAnimationFrame||Browser.fakeRequestAnimationFrame),window.requestAnimationFrame(t))},safeCallback:function(e){return function(){if(!ABORT)return e.apply(null,arguments)}},allowAsyncCallbacks:!0,queuedAsyncCallbacks:[],pauseAsyncCallbacks:function(){Browser.allowAsyncCallbacks=!1},resumeAsyncCallbacks:function(){if(Browser.allowAsyncCallbacks=!0,Browser.queuedAsyncCallbacks.length>0){var e=Browser.queuedAsyncCallbacks;Browser.queuedAsyncCallbacks=[],e.forEach(function(t){t()})}},safeRequestAnimationFrame:function(e){return Browser.requestAnimationFrame(function(){ABORT||(Browser.allowAsyncCallbacks?e():Browser.queuedAsyncCallbacks.push(e))})},safeSetTimeout:function(e,t){return Module.noExitRuntime=!0,setTimeout(function(){ABORT||(Browser.allowAsyncCallbacks?e():Browser.queuedAsyncCallbacks.push(e))},t)},safeSetInterval:function(e,t){return Module.noExitRuntime=!0,setInterval(function(){ABORT||Browser.allowAsyncCallbacks&&e()},t)},getMimetype:function(e){return{jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",bmp:"image/bmp",ogg:"audio/ogg",wav:"audio/wav",mp3:"audio/mpeg"}[e.substr(e.lastIndexOf(".")+1)]},getUserMedia:function(e){window.getUserMedia||(window.getUserMedia=navigator.getUserMedia||navigator.mozGetUserMedia),window.getUserMedia(e)},getMovementX:function(e){return e.movementX||e.mozMovementX||e.webkitMovementX||0},getMovementY:function(e){return e.movementY||e.mozMovementY||e.webkitMovementY||0},getMouseWheelDelta:function(e){var t=0;switch(e.type){case"DOMMouseScroll":t=e.detail;break;case"mousewheel":t=e.wheelDelta;break;case"wheel":t=e.deltaY;break;default:throw"unrecognized mouse wheel event: "+e.type}return t},mouseX:0,mouseY:0,mouseMovementX:0,mouseMovementY:0,touches:{},lastTouches:{},calculateMouseEvent:function(e){if(Browser.pointerLock)e.type!="mousemove"&&"mozMovementX"in e?Browser.mouseMovementX=Browser.mouseMovementY=0:(Browser.mouseMovementX=Browser.getMovementX(e),Browser.mouseMovementY=Browser.getMovementY(e)),typeof SDL<"u"?(Browser.mouseX=SDL.mouseX+Browser.mouseMovementX,Browser.mouseY=SDL.mouseY+Browser.mouseMovementY):(Browser.mouseX+=Browser.mouseMovementX,Browser.mouseY+=Browser.mouseMovementY);else{var t=Module.canvas.getBoundingClientRect(),r=Module.canvas.width,s=Module.canvas.height,a=typeof window.scrollX<"u"?window.scrollX:window.pageXOffset,n=typeof window.scrollY<"u"?window.scrollY:window.pageYOffset;if(e.type==="touchstart"||e.type==="touchend"||e.type==="touchmove"){var c=e.touch;if(c===void 0)return;var f=c.pageX-(a+t.left),p=c.pageY-(n+t.top);f=f*(r/t.width),p=p*(s/t.height);var h={x:f,y:p};if(e.type==="touchstart")Browser.lastTouches[c.identifier]=h,Browser.touches[c.identifier]=h;else if(e.type==="touchend"||e.type==="touchmove"){var E=Browser.touches[c.identifier];E||(E=h),Browser.lastTouches[c.identifier]=E,Browser.touches[c.identifier]=h}return}var C=e.pageX-(a+t.left),S=e.pageY-(n+t.top);C=C*(r/t.width),S=S*(s/t.height),Browser.mouseMovementX=C-Browser.mouseX,Browser.mouseMovementY=S-Browser.mouseY,Browser.mouseX=C,Browser.mouseY=S}},asyncLoad:function(e,t,r,s){var a=s?"":"al "+e;Module.readAsync(e,function(n){assert(n,'Loading data file "'+e+'" failed (no arrayBuffer).'),t(new Uint8Array(n)),a&&removeRunDependency(a)},function(n){if(r)r();else throw'Loading data file "'+e+'" failed.'}),a&&addRunDependency(a)},resizeListeners:[],updateResizeListeners:function(){var e=Module.canvas;Browser.resizeListeners.forEach(function(t){t(e.width,e.height)})},setCanvasSize:function(e,t,r){var s=Module.canvas;Browser.updateCanvasDimensions(s,e,t),r||Browser.updateResizeListeners()},windowedWidth:0,windowedHeight:0,setFullscreenCanvasSize:function(){if(typeof SDL<"u"){var e=HEAPU32[SDL.screen+Runtime.QUANTUM_SIZE*0>>2];e=e|8388608,HEAP32[SDL.screen+Runtime.QUANTUM_SIZE*0>>2]=e}Browser.updateResizeListeners()},setWindowedCanvasSize:function(){if(typeof SDL<"u"){var e=HEAPU32[SDL.screen+Runtime.QUANTUM_SIZE*0>>2];e=e&-8388609,HEAP32[SDL.screen+Runtime.QUANTUM_SIZE*0>>2]=e}Browser.updateResizeListeners()},updateCanvasDimensions:function(e,t,r){t&&r?(e.widthNative=t,e.heightNative=r):(t=e.widthNative,r=e.heightNative);var s=t,a=r;if(Module.forcedAspectRatio&&Module.forcedAspectRatio>0&&(s/a>2];return t},getStr:function(){var e=Pointer_stringify(SYSCALLS.get());return e},get64:function(){var e=SYSCALLS.get(),t=SYSCALLS.get();return e>=0?assert(t===0):assert(t===-1),e},getZero:function(){assert(SYSCALLS.get()===0)}};function ___syscall6(e,t){SYSCALLS.varargs=t;try{var r=SYSCALLS.getStreamFromFD();return FS.close(r),0}catch(s){return(typeof FS>"u"||!(s instanceof FS.ErrnoError))&&abort(s),-s.errno}}function ___syscall54(e,t){SYSCALLS.varargs=t;try{return 0}catch(r){return(typeof FS>"u"||!(r instanceof FS.ErrnoError))&&abort(r),-r.errno}}function _typeModule(e){var t=[[0,1,"X"],[1,1,"const X"],[128,1,"X *"],[256,1,"X &"],[384,1,"X &&"],[512,1,"std::shared_ptr"],[640,1,"std::unique_ptr"],[5120,1,"std::vector"],[6144,2,"std::array"],[9216,-1,"std::function"]];function r(p,h,E,C,S,x){if(h==1){var I=C&896;(I==128||I==256||I==384)&&(p="X const")}var T;return x?T=E.replace("X",p).replace("Y",S):T=p.replace("X",E).replace("Y",S),T.replace(/([*&]) (?=[*&])/g,"$1")}function s(p,h,E,C,S){throw new Error(p+" type "+E.replace("X",h+"?")+(C?" with flag "+C:"")+" in "+S)}function a(p,h,E,C,S,x,I,T){x===void 0&&(x="X"),T===void 0&&(T=1);var O=E(p);if(O)return O;var U=C(p),V=U.placeholderFlag,te=t[V];I&&te&&(x=r(I[2],I[0],x,te[0],"?",!0));var ie;V==0&&(ie="Unbound"),V>=10&&(ie="Corrupt"),T>20&&(ie="Deeply nested"),ie&&s(ie,p,x,V,S||"?");var ue=U.paramList[0],ae=a(ue,h,E,C,S,x,te,T+1),ge,Ae={flags:te[0],id:p,name:"",paramList:[ae]},Ce=[],Ee="?";switch(U.placeholderFlag){case 1:ge=ae.spec;break;case 2:if((ae.flags&15360)==1024&&ae.spec.ptrSize==1){Ae.flags=7168;break}case 3:case 6:case 5:ge=ae.spec,ae.flags&15360;break;case 8:Ee=""+U.paramList[1],Ae.paramList.push(U.paramList[1]);break;case 9:for(var d=0,Se=U.paramList[1];d>2]=e),e}function _llvm_stacksave(){var e=_llvm_stacksave;return e.LLVM_SAVEDSTACKS||(e.LLVM_SAVEDSTACKS=[]),e.LLVM_SAVEDSTACKS.push(Runtime.stackSave()),e.LLVM_SAVEDSTACKS.length-1}function ___syscall140(e,t){SYSCALLS.varargs=t;try{var r=SYSCALLS.getStreamFromFD(),s=SYSCALLS.get(),a=SYSCALLS.get(),n=SYSCALLS.get(),c=SYSCALLS.get(),f=a;return FS.llseek(r,f,c),HEAP32[n>>2]=r.position,r.getdents&&f===0&&c===0&&(r.getdents=null),0}catch(p){return(typeof FS>"u"||!(p instanceof FS.ErrnoError))&&abort(p),-p.errno}}function ___syscall146(e,t){SYSCALLS.varargs=t;try{var r=SYSCALLS.get(),s=SYSCALLS.get(),a=SYSCALLS.get(),n=0;___syscall146.buffer||(___syscall146.buffers=[null,[],[]],___syscall146.printChar=function(E,C){var S=___syscall146.buffers[E];assert(S),C===0||C===10?((E===1?Module.print:Module.printErr)(UTF8ArrayToString(S,0)),S.length=0):S.push(C)});for(var c=0;c>2],p=HEAP32[s+(c*8+4)>>2],h=0;h"u"||!(E instanceof FS.ErrnoError))&&abort(E),-E.errno}}function __nbind_finish(){for(var e=0,t=_nbind.BindClass.list;ee.pageSize/2||t>e.pageSize-r){var s=_nbind.typeNameTbl.NBind.proto;return s.lalloc(t)}else return HEAPU32[e.usedPtr]=r+t,e.rootPtr+r},e.lreset=function(t,r){var s=HEAPU32[e.pagePtr];if(s){var a=_nbind.typeNameTbl.NBind.proto;a.lreset(t,r)}else HEAPU32[e.usedPtr]=t},e}();_nbind.Pool=Pool;function constructType(e,t){var r=e==10240?_nbind.makeTypeNameTbl[t.name]||_nbind.BindType:_nbind.makeTypeKindTbl[e],s=new r(t);return typeIdTbl[t.id]=s,_nbind.typeNameTbl[t.name]=s,s}_nbind.constructType=constructType;function getType(e){return typeIdTbl[e]}_nbind.getType=getType;function queryType(e){var t=HEAPU8[e],r=_nbind.structureList[t][1];e/=4,r<0&&(++e,r=HEAPU32[e]+1);var s=Array.prototype.slice.call(HEAPU32.subarray(e+1,e+1+r));return t==9&&(s=[s[0],s.slice(1)]),{paramList:s,placeholderFlag:t}}_nbind.queryType=queryType;function getTypes(e,t){return e.map(function(r){return typeof r=="number"?_nbind.getComplexType(r,constructType,getType,queryType,t):_nbind.typeNameTbl[r]})}_nbind.getTypes=getTypes;function readTypeIdList(e,t){return Array.prototype.slice.call(HEAPU32,e/4,e/4+t)}_nbind.readTypeIdList=readTypeIdList;function readAsciiString(e){for(var t=e;HEAPU8[t++];);return String.fromCharCode.apply("",HEAPU8.subarray(e,t-1))}_nbind.readAsciiString=readAsciiString;function readPolicyList(e){var t={};if(e)for(;;){var r=HEAPU32[e/4];if(!r)break;t[readAsciiString(r)]=!0,e+=4}return t}_nbind.readPolicyList=readPolicyList;function getDynCall(e,t){var r={float32_t:"d",float64_t:"d",int64_t:"d",uint64_t:"d",void:"v"},s=e.map(function(n){return r[n.name]||"i"}).join(""),a=Module["dynCall_"+s];if(!a)throw new Error("dynCall_"+s+" not found for "+t+"("+e.map(function(n){return n.name}).join(", ")+")");return a}_nbind.getDynCall=getDynCall;function addMethod(e,t,r,s){var a=e[t];e.hasOwnProperty(t)&&a?((a.arity||a.arity===0)&&(a=_nbind.makeOverloader(a,a.arity),e[t]=a),a.addMethod(r,s)):(r.arity=s,e[t]=r)}_nbind.addMethod=addMethod;function throwError(e){throw new Error(e)}_nbind.throwError=throwError,_nbind.bigEndian=!1,_a=_typeModule(_typeModule),_nbind.Type=_a.Type,_nbind.makeType=_a.makeType,_nbind.getComplexType=_a.getComplexType,_nbind.structureList=_a.structureList;var BindType=function(e){__extends(t,e);function t(){var r=e!==null&&e.apply(this,arguments)||this;return r.heap=HEAPU32,r.ptrSize=4,r}return t.prototype.needsWireRead=function(r){return!!this.wireRead||!!this.makeWireRead},t.prototype.needsWireWrite=function(r){return!!this.wireWrite||!!this.makeWireWrite},t}(_nbind.Type);_nbind.BindType=BindType;var PrimitiveType=function(e){__extends(t,e);function t(r){var s=e.call(this,r)||this,a=r.flags&32?{32:HEAPF32,64:HEAPF64}:r.flags&8?{8:HEAPU8,16:HEAPU16,32:HEAPU32}:{8:HEAP8,16:HEAP16,32:HEAP32};return s.heap=a[r.ptrSize*8],s.ptrSize=r.ptrSize,s}return t.prototype.needsWireWrite=function(r){return!!r&&!!r.Strict},t.prototype.makeWireWrite=function(r,s){return s&&s.Strict&&function(a){if(typeof a=="number")return a;throw new Error("Type mismatch")}},t}(BindType);_nbind.PrimitiveType=PrimitiveType;function pushCString(e,t){if(e==null){if(t&&t.Nullable)return 0;throw new Error("Type mismatch")}if(t&&t.Strict){if(typeof e!="string")throw new Error("Type mismatch")}else e=e.toString();var r=Module.lengthBytesUTF8(e)+1,s=_nbind.Pool.lalloc(r);return Module.stringToUTF8Array(e,HEAPU8,s,r),s}_nbind.pushCString=pushCString;function popCString(e){return e===0?null:Module.Pointer_stringify(e)}_nbind.popCString=popCString;var CStringType=function(e){__extends(t,e);function t(){var r=e!==null&&e.apply(this,arguments)||this;return r.wireRead=popCString,r.wireWrite=pushCString,r.readResources=[_nbind.resources.pool],r.writeResources=[_nbind.resources.pool],r}return t.prototype.makeWireWrite=function(r,s){return function(a){return pushCString(a,s)}},t}(BindType);_nbind.CStringType=CStringType;var BooleanType=function(e){__extends(t,e);function t(){var r=e!==null&&e.apply(this,arguments)||this;return r.wireRead=function(s){return!!s},r}return t.prototype.needsWireWrite=function(r){return!!r&&!!r.Strict},t.prototype.makeWireRead=function(r){return"!!("+r+")"},t.prototype.makeWireWrite=function(r,s){return s&&s.Strict&&function(a){if(typeof a=="boolean")return a;throw new Error("Type mismatch")}||r},t}(BindType);_nbind.BooleanType=BooleanType;var Wrapper=function(){function e(){}return e.prototype.persist=function(){this.__nbindState|=1},e}();_nbind.Wrapper=Wrapper;function makeBound(e,t){var r=function(s){__extends(a,s);function a(n,c,f,p){var h=s.call(this)||this;if(!(h instanceof a))return new(Function.prototype.bind.apply(a,Array.prototype.concat.apply([null],arguments)));var E=c,C=f,S=p;if(n!==_nbind.ptrMarker){var x=h.__nbindConstructor.apply(h,arguments);E=4608,S=HEAPU32[x/4],C=HEAPU32[x/4+1]}var I={configurable:!0,enumerable:!1,value:null,writable:!1},T={__nbindFlags:E,__nbindPtr:C};S&&(T.__nbindShared=S,_nbind.mark(h));for(var O=0,U=Object.keys(T);O>=1;var r=_nbind.valueList[e];return _nbind.valueList[e]=firstFreeValue,firstFreeValue=e,r}else{if(t)return _nbind.popShared(e,t);throw new Error("Invalid value slot "+e)}}_nbind.popValue=popValue;var valueBase=18446744073709552e3;function push64(e){return typeof e=="number"?e:pushValue(e)*4096+valueBase}function pop64(e){return e=3?c=Buffer.from(n):c=new Buffer(n),c.copy(s)}else getBuffer(s).set(n)}}_nbind.commitBuffer=commitBuffer;var dirtyList=[],gcTimer=0;function sweep(){for(var e=0,t=dirtyList;e>2]=DYNAMIC_BASE,staticSealed=!0;function invoke_viiiii(e,t,r,s,a,n){try{Module.dynCall_viiiii(e,t,r,s,a,n)}catch(c){if(typeof c!="number"&&c!=="longjmp")throw c;Module.setThrew(1,0)}}function invoke_vif(e,t,r){try{Module.dynCall_vif(e,t,r)}catch(s){if(typeof s!="number"&&s!=="longjmp")throw s;Module.setThrew(1,0)}}function invoke_vid(e,t,r){try{Module.dynCall_vid(e,t,r)}catch(s){if(typeof s!="number"&&s!=="longjmp")throw s;Module.setThrew(1,0)}}function invoke_fiff(e,t,r,s){try{return Module.dynCall_fiff(e,t,r,s)}catch(a){if(typeof a!="number"&&a!=="longjmp")throw a;Module.setThrew(1,0)}}function invoke_vi(e,t){try{Module.dynCall_vi(e,t)}catch(r){if(typeof r!="number"&&r!=="longjmp")throw r;Module.setThrew(1,0)}}function invoke_vii(e,t,r){try{Module.dynCall_vii(e,t,r)}catch(s){if(typeof s!="number"&&s!=="longjmp")throw s;Module.setThrew(1,0)}}function invoke_ii(e,t){try{return Module.dynCall_ii(e,t)}catch(r){if(typeof r!="number"&&r!=="longjmp")throw r;Module.setThrew(1,0)}}function invoke_viddi(e,t,r,s,a){try{Module.dynCall_viddi(e,t,r,s,a)}catch(n){if(typeof n!="number"&&n!=="longjmp")throw n;Module.setThrew(1,0)}}function invoke_vidd(e,t,r,s){try{Module.dynCall_vidd(e,t,r,s)}catch(a){if(typeof a!="number"&&a!=="longjmp")throw a;Module.setThrew(1,0)}}function invoke_iiii(e,t,r,s){try{return Module.dynCall_iiii(e,t,r,s)}catch(a){if(typeof a!="number"&&a!=="longjmp")throw a;Module.setThrew(1,0)}}function invoke_diii(e,t,r,s){try{return Module.dynCall_diii(e,t,r,s)}catch(a){if(typeof a!="number"&&a!=="longjmp")throw a;Module.setThrew(1,0)}}function invoke_di(e,t){try{return Module.dynCall_di(e,t)}catch(r){if(typeof r!="number"&&r!=="longjmp")throw r;Module.setThrew(1,0)}}function invoke_iid(e,t,r){try{return Module.dynCall_iid(e,t,r)}catch(s){if(typeof s!="number"&&s!=="longjmp")throw s;Module.setThrew(1,0)}}function invoke_iii(e,t,r){try{return Module.dynCall_iii(e,t,r)}catch(s){if(typeof s!="number"&&s!=="longjmp")throw s;Module.setThrew(1,0)}}function invoke_viiddi(e,t,r,s,a,n){try{Module.dynCall_viiddi(e,t,r,s,a,n)}catch(c){if(typeof c!="number"&&c!=="longjmp")throw c;Module.setThrew(1,0)}}function invoke_viiiiii(e,t,r,s,a,n,c){try{Module.dynCall_viiiiii(e,t,r,s,a,n,c)}catch(f){if(typeof f!="number"&&f!=="longjmp")throw f;Module.setThrew(1,0)}}function invoke_dii(e,t,r){try{return Module.dynCall_dii(e,t,r)}catch(s){if(typeof s!="number"&&s!=="longjmp")throw s;Module.setThrew(1,0)}}function invoke_i(e){try{return Module.dynCall_i(e)}catch(t){if(typeof t!="number"&&t!=="longjmp")throw t;Module.setThrew(1,0)}}function invoke_iiiiii(e,t,r,s,a,n){try{return Module.dynCall_iiiiii(e,t,r,s,a,n)}catch(c){if(typeof c!="number"&&c!=="longjmp")throw c;Module.setThrew(1,0)}}function invoke_viiid(e,t,r,s,a){try{Module.dynCall_viiid(e,t,r,s,a)}catch(n){if(typeof n!="number"&&n!=="longjmp")throw n;Module.setThrew(1,0)}}function invoke_viififi(e,t,r,s,a,n,c){try{Module.dynCall_viififi(e,t,r,s,a,n,c)}catch(f){if(typeof f!="number"&&f!=="longjmp")throw f;Module.setThrew(1,0)}}function invoke_viii(e,t,r,s){try{Module.dynCall_viii(e,t,r,s)}catch(a){if(typeof a!="number"&&a!=="longjmp")throw a;Module.setThrew(1,0)}}function invoke_v(e){try{Module.dynCall_v(e)}catch(t){if(typeof t!="number"&&t!=="longjmp")throw t;Module.setThrew(1,0)}}function invoke_viid(e,t,r,s){try{Module.dynCall_viid(e,t,r,s)}catch(a){if(typeof a!="number"&&a!=="longjmp")throw a;Module.setThrew(1,0)}}function invoke_idd(e,t,r){try{return Module.dynCall_idd(e,t,r)}catch(s){if(typeof s!="number"&&s!=="longjmp")throw s;Module.setThrew(1,0)}}function invoke_viiii(e,t,r,s,a){try{Module.dynCall_viiii(e,t,r,s,a)}catch(n){if(typeof n!="number"&&n!=="longjmp")throw n;Module.setThrew(1,0)}}Module.asmGlobalArg={Math,Int8Array,Int16Array,Int32Array,Uint8Array,Uint16Array,Uint32Array,Float32Array,Float64Array,NaN:NaN,Infinity:1/0},Module.asmLibraryArg={abort,assert,enlargeMemory,getTotalMemory,abortOnCannotGrowMemory,invoke_viiiii,invoke_vif,invoke_vid,invoke_fiff,invoke_vi,invoke_vii,invoke_ii,invoke_viddi,invoke_vidd,invoke_iiii,invoke_diii,invoke_di,invoke_iid,invoke_iii,invoke_viiddi,invoke_viiiiii,invoke_dii,invoke_i,invoke_iiiiii,invoke_viiid,invoke_viififi,invoke_viii,invoke_v,invoke_viid,invoke_idd,invoke_viiii,_emscripten_asm_const_iiiii,_emscripten_asm_const_iiidddddd,_emscripten_asm_const_iiiid,__nbind_reference_external,_emscripten_asm_const_iiiiiiii,_removeAccessorPrefix,_typeModule,__nbind_register_pool,__decorate,_llvm_stackrestore,___cxa_atexit,__extends,__nbind_get_value_object,__ZN8facebook4yoga14YGNodeToStringEPNSt3__212basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEP6YGNode14YGPrintOptionsj,_emscripten_set_main_loop_timing,__nbind_register_primitive,__nbind_register_type,_emscripten_memcpy_big,__nbind_register_function,___setErrNo,__nbind_register_class,__nbind_finish,_abort,_nbind_value,_llvm_stacksave,___syscall54,_defineHidden,_emscripten_set_main_loop,_emscripten_get_now,__nbind_register_callback_signature,_emscripten_asm_const_iiiiii,__nbind_free_external,_emscripten_asm_const_iiii,_emscripten_asm_const_iiididi,___syscall6,_atexit,___syscall140,___syscall146,DYNAMICTOP_PTR,tempDoublePtr,ABORT,STACKTOP,STACK_MAX,cttz_i8,___dso_handle};var asm=function(e,t,r){var s=new e.Int8Array(r),a=new e.Int16Array(r),n=new e.Int32Array(r),c=new e.Uint8Array(r),f=new e.Uint16Array(r),p=new e.Uint32Array(r),h=new e.Float32Array(r),E=new e.Float64Array(r),C=t.DYNAMICTOP_PTR|0,S=t.tempDoublePtr|0,x=t.ABORT|0,I=t.STACKTOP|0,T=t.STACK_MAX|0,O=t.cttz_i8|0,U=t.___dso_handle|0,V=0,te=0,ie=0,ue=0,ae=e.NaN,ge=e.Infinity,Ae=0,Ce=0,Ee=0,d=0,Se=0,Be=0,me=e.Math.floor,ce=e.Math.abs,Z=e.Math.sqrt,De=e.Math.pow,Qe=e.Math.cos,st=e.Math.sin,_=e.Math.tan,tt=e.Math.acos,Ne=e.Math.asin,ke=e.Math.atan,be=e.Math.atan2,je=e.Math.exp,Re=e.Math.log,ct=e.Math.ceil,Me=e.Math.imul,P=e.Math.min,w=e.Math.max,b=e.Math.clz32,y=e.Math.fround,F=t.abort,z=t.assert,X=t.enlargeMemory,$=t.getTotalMemory,se=t.abortOnCannotGrowMemory,xe=t.invoke_viiiii,Fe=t.invoke_vif,ut=t.invoke_vid,Ct=t.invoke_fiff,qt=t.invoke_vi,ir=t.invoke_vii,Pt=t.invoke_ii,gn=t.invoke_viddi,Pr=t.invoke_vidd,Cr=t.invoke_iiii,Or=t.invoke_diii,on=t.invoke_di,li=t.invoke_iid,Do=t.invoke_iii,ns=t.invoke_viiddi,so=t.invoke_viiiiii,bo=t.invoke_dii,ji=t.invoke_i,oo=t.invoke_iiiiii,Po=t.invoke_viiid,TA=t.invoke_viififi,df=t.invoke_viii,dh=t.invoke_v,gh=t.invoke_viid,ao=t.invoke_idd,Gn=t.invoke_viiii,Ns=t._emscripten_asm_const_iiiii,lo=t._emscripten_asm_const_iiidddddd,su=t._emscripten_asm_const_iiiid,ou=t.__nbind_reference_external,au=t._emscripten_asm_const_iiiiiiii,FA=t._removeAccessorPrefix,NA=t._typeModule,fa=t.__nbind_register_pool,Aa=t.__decorate,OA=t._llvm_stackrestore,dr=t.___cxa_atexit,xo=t.__extends,Ga=t.__nbind_get_value_object,Ue=t.__ZN8facebook4yoga14YGNodeToStringEPNSt3__212basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEP6YGNode14YGPrintOptionsj,wr=t._emscripten_set_main_loop_timing,gf=t.__nbind_register_primitive,LA=t.__nbind_register_type,MA=t._emscripten_memcpy_big,lu=t.__nbind_register_function,cu=t.___setErrNo,lc=t.__nbind_register_class,we=t.__nbind_finish,Nt=t._abort,cc=t._nbind_value,Oi=t._llvm_stacksave,co=t.___syscall54,Tt=t._defineHidden,Qn=t._emscripten_set_main_loop,pa=t._emscripten_get_now,Gi=t.__nbind_register_callback_signature,Li=t._emscripten_asm_const_iiiiii,qa=t.__nbind_free_external,mn=t._emscripten_asm_const_iiii,Xn=t._emscripten_asm_const_iiididi,uu=t.___syscall6,mh=t._atexit,Wa=t.___syscall140,Ya=t.___syscall146,Va=y(0);let $e=y(0);function Ja(o){o=o|0;var l=0;return l=I,I=I+o|0,I=I+15&-16,l|0}function mf(){return I|0}function uc(o){o=o|0,I=o}function vn(o,l){o=o|0,l=l|0,I=o,T=l}function ha(o,l){o=o|0,l=l|0,V||(V=o,te=l)}function UA(o){o=o|0,Be=o}function _A(){return Be|0}function da(){var o=0,l=0;Rr(8104,8,400)|0,Rr(8504,408,540)|0,o=9044,l=o+44|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));s[9088]=0,s[9089]=1,n[2273]=0,n[2274]=948,n[2275]=948,dr(17,8104,U|0)|0}function kl(o){o=o|0,mt(o+948|0)}function Ut(o){return o=y(o),((fP(o)|0)&2147483647)>>>0>2139095040|0}function Rn(o,l,u){o=o|0,l=l|0,u=u|0;e:do if(n[o+(l<<3)+4>>2]|0)o=o+(l<<3)|0;else{if((l|2|0)==3&&n[o+60>>2]|0){o=o+56|0;break}switch(l|0){case 0:case 2:case 4:case 5:{if(n[o+52>>2]|0){o=o+48|0;break e}break}default:}if(n[o+68>>2]|0){o=o+64|0;break}else{o=(l|1|0)==5?948:u;break}}while(!1);return o|0}function ga(o){o=o|0;var l=0;return l=_P(1e3)|0,Ka(o,(l|0)!=0,2456),n[2276]=(n[2276]|0)+1,Rr(l|0,8104,1e3)|0,s[o+2>>0]|0&&(n[l+4>>2]=2,n[l+12>>2]=4),n[l+976>>2]=o,l|0}function Ka(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0;g=I,I=I+16|0,A=g,l||(n[A>>2]=u,Gd(o,5,3197,A)),I=g}function is(){return ga(956)|0}function fc(o){o=o|0;var l=0;return l=Kt(1e3)|0,fu(l,o),Ka(n[o+976>>2]|0,1,2456),n[2276]=(n[2276]|0)+1,n[l+944>>2]=0,l|0}function fu(o,l){o=o|0,l=l|0;var u=0;Rr(o|0,l|0,948)|0,xy(o+948|0,l+948|0),u=o+960|0,o=l+960|0,l=u+40|0;do n[u>>2]=n[o>>2],u=u+4|0,o=o+4|0;while((u|0)<(l|0))}function Ac(o){o=o|0;var l=0,u=0,A=0,g=0;if(l=o+944|0,u=n[l>>2]|0,u|0&&(za(u+948|0,o)|0,n[l>>2]=0),u=Mi(o)|0,u|0){l=0;do n[(Bs(o,l)|0)+944>>2]=0,l=l+1|0;while((l|0)!=(u|0))}u=o+948|0,A=n[u>>2]|0,g=o+952|0,l=n[g>>2]|0,(l|0)!=(A|0)&&(n[g>>2]=l+(~((l+-4-A|0)>>>2)<<2)),Ql(u),HP(o),n[2276]=(n[2276]|0)+-1}function za(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0;A=n[o>>2]|0,k=o+4|0,u=n[k>>2]|0,m=u;e:do if((A|0)==(u|0))g=A,B=4;else for(o=A;;){if((n[o>>2]|0)==(l|0)){g=o,B=4;break e}if(o=o+4|0,(o|0)==(u|0)){o=0;break}}while(!1);return(B|0)==4&&((g|0)!=(u|0)?(A=g+4|0,o=m-A|0,l=o>>2,l&&(B2(g|0,A|0,o|0)|0,u=n[k>>2]|0),o=g+(l<<2)|0,(u|0)==(o|0)||(n[k>>2]=u+(~((u+-4-o|0)>>>2)<<2)),o=1):o=0),o|0}function Mi(o){return o=o|0,(n[o+952>>2]|0)-(n[o+948>>2]|0)>>2|0}function Bs(o,l){o=o|0,l=l|0;var u=0;return u=n[o+948>>2]|0,(n[o+952>>2]|0)-u>>2>>>0>l>>>0?o=n[u+(l<<2)>>2]|0:o=0,o|0}function Ql(o){o=o|0;var l=0,u=0,A=0,g=0;A=I,I=I+32|0,l=A,g=n[o>>2]|0,u=(n[o+4>>2]|0)-g|0,((n[o+8>>2]|0)-g|0)>>>0>u>>>0&&(g=u>>2,Ty(l,g,g,o+8|0),AP(o,l),Fy(l)),I=A}function yf(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0;M=Mi(o)|0;do if(M|0){if((n[(Bs(o,0)|0)+944>>2]|0)==(o|0)){if(!(za(o+948|0,l)|0))break;Rr(l+400|0,8504,540)|0,n[l+944>>2]=0,Oe(o);break}B=n[(n[o+976>>2]|0)+12>>2]|0,k=o+948|0,R=(B|0)==0,u=0,m=0;do A=n[(n[k>>2]|0)+(m<<2)>>2]|0,(A|0)==(l|0)?Oe(o):(g=fc(A)|0,n[(n[k>>2]|0)+(u<<2)>>2]=g,n[g+944>>2]=o,R||eU[B&15](A,g,o,u),u=u+1|0),m=m+1|0;while((m|0)!=(M|0));if(u>>>0>>0){R=o+948|0,k=o+952|0,B=u,u=n[k>>2]|0;do m=(n[R>>2]|0)+(B<<2)|0,A=m+4|0,g=u-A|0,l=g>>2,l&&(B2(m|0,A|0,g|0)|0,u=n[k>>2]|0),g=u,A=m+(l<<2)|0,(g|0)!=(A|0)&&(u=g+(~((g+-4-A|0)>>>2)<<2)|0,n[k>>2]=u),B=B+1|0;while((B|0)!=(M|0))}}while(!1)}function pc(o){o=o|0;var l=0,u=0,A=0,g=0;Bi(o,(Mi(o)|0)==0,2491),Bi(o,(n[o+944>>2]|0)==0,2545),l=o+948|0,u=n[l>>2]|0,A=o+952|0,g=n[A>>2]|0,(g|0)!=(u|0)&&(n[A>>2]=g+(~((g+-4-u|0)>>>2)<<2)),Ql(l),l=o+976|0,u=n[l>>2]|0,Rr(o|0,8104,1e3)|0,s[u+2>>0]|0&&(n[o+4>>2]=2,n[o+12>>2]=4),n[l>>2]=u}function Bi(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0;g=I,I=I+16|0,A=g,l||(n[A>>2]=u,No(o,5,3197,A)),I=g}function Tn(){return n[2276]|0}function hc(){var o=0;return o=_P(20)|0,Ke((o|0)!=0,2592),n[2277]=(n[2277]|0)+1,n[o>>2]=n[239],n[o+4>>2]=n[240],n[o+8>>2]=n[241],n[o+12>>2]=n[242],n[o+16>>2]=n[243],o|0}function Ke(o,l){o=o|0,l=l|0;var u=0,A=0;A=I,I=I+16|0,u=A,o||(n[u>>2]=l,No(0,5,3197,u)),I=A}function ot(o){o=o|0,HP(o),n[2277]=(n[2277]|0)+-1}function St(o,l){o=o|0,l=l|0;var u=0;l?(Bi(o,(Mi(o)|0)==0,2629),u=1):(u=0,l=0),n[o+964>>2]=l,n[o+988>>2]=u}function lr(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;A=I,I=I+16|0,m=A+8|0,g=A+4|0,B=A,n[g>>2]=l,Bi(o,(n[l+944>>2]|0)==0,2709),Bi(o,(n[o+964>>2]|0)==0,2763),ee(o),l=o+948|0,n[B>>2]=(n[l>>2]|0)+(u<<2),n[m>>2]=n[B>>2],ye(l,m,g)|0,n[(n[g>>2]|0)+944>>2]=o,Oe(o),I=A}function ee(o){o=o|0;var l=0,u=0,A=0,g=0,m=0,B=0,k=0;if(u=Mi(o)|0,u|0&&(n[(Bs(o,0)|0)+944>>2]|0)!=(o|0)){A=n[(n[o+976>>2]|0)+12>>2]|0,g=o+948|0,m=(A|0)==0,l=0;do B=n[(n[g>>2]|0)+(l<<2)>>2]|0,k=fc(B)|0,n[(n[g>>2]|0)+(l<<2)>>2]=k,n[k+944>>2]=o,m||eU[A&15](B,k,o,l),l=l+1|0;while((l|0)!=(u|0))}}function ye(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0,Le=0,Te=0,nt=0,Ze=0;nt=I,I=I+64|0,q=nt+52|0,k=nt+48|0,oe=nt+28|0,Ve=nt+24|0,Le=nt+20|0,Te=nt,A=n[o>>2]|0,m=A,l=A+((n[l>>2]|0)-m>>2<<2)|0,A=o+4|0,g=n[A>>2]|0,B=o+8|0;do if(g>>>0<(n[B>>2]|0)>>>0){if((l|0)==(g|0)){n[l>>2]=n[u>>2],n[A>>2]=(n[A>>2]|0)+4;break}pP(o,l,g,l+4|0),l>>>0<=u>>>0&&(u=(n[A>>2]|0)>>>0>u>>>0?u+4|0:u),n[l>>2]=n[u>>2]}else{A=(g-m>>2)+1|0,g=N(o)|0,g>>>0>>0&&an(o),L=n[o>>2]|0,M=(n[B>>2]|0)-L|0,m=M>>1,Ty(Te,M>>2>>>0>>1>>>0?m>>>0>>0?A:m:g,l-L>>2,o+8|0),L=Te+8|0,A=n[L>>2]|0,m=Te+12|0,M=n[m>>2]|0,B=M,R=A;do if((A|0)==(M|0)){if(M=Te+4|0,A=n[M>>2]|0,Ze=n[Te>>2]|0,g=Ze,A>>>0<=Ze>>>0){A=B-g>>1,A=A|0?A:1,Ty(oe,A,A>>>2,n[Te+16>>2]|0),n[Ve>>2]=n[M>>2],n[Le>>2]=n[L>>2],n[k>>2]=n[Ve>>2],n[q>>2]=n[Le>>2],Z1(oe,k,q),A=n[Te>>2]|0,n[Te>>2]=n[oe>>2],n[oe>>2]=A,A=oe+4|0,Ze=n[M>>2]|0,n[M>>2]=n[A>>2],n[A>>2]=Ze,A=oe+8|0,Ze=n[L>>2]|0,n[L>>2]=n[A>>2],n[A>>2]=Ze,A=oe+12|0,Ze=n[m>>2]|0,n[m>>2]=n[A>>2],n[A>>2]=Ze,Fy(oe),A=n[L>>2]|0;break}m=A,B=((m-g>>2)+1|0)/-2|0,k=A+(B<<2)|0,g=R-m|0,m=g>>2,m&&(B2(k|0,A|0,g|0)|0,A=n[M>>2]|0),Ze=k+(m<<2)|0,n[L>>2]=Ze,n[M>>2]=A+(B<<2),A=Ze}while(!1);n[A>>2]=n[u>>2],n[L>>2]=(n[L>>2]|0)+4,l=hP(o,Te,l)|0,Fy(Te)}while(!1);return I=nt,l|0}function Oe(o){o=o|0;var l=0;do{if(l=o+984|0,s[l>>0]|0)break;s[l>>0]=1,h[o+504>>2]=y(ae),o=n[o+944>>2]|0}while(o|0)}function mt(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-4-A|0)>>>2)<<2)),It(u))}function Et(o){return o=o|0,n[o+944>>2]|0}function bt(o){o=o|0,Bi(o,(n[o+964>>2]|0)!=0,2832),Oe(o)}function tr(o){return o=o|0,(s[o+984>>0]|0)!=0|0}function pn(o,l){o=o|0,l=l|0,K8e(o,l,400)|0&&(Rr(o|0,l|0,400)|0,Oe(o))}function ci(o){o=o|0;var l=$e;return l=y(h[o+44>>2]),o=Ut(l)|0,y(o?y(0):l)}function qi(o){o=o|0;var l=$e;return l=y(h[o+48>>2]),Ut(l)|0&&(l=s[(n[o+976>>2]|0)+2>>0]|0?y(1):y(0)),y(l)}function Fn(o,l){o=o|0,l=l|0,n[o+980>>2]=l}function Xa(o){return o=o|0,n[o+980>>2]|0}function Iy(o,l){o=o|0,l=l|0;var u=0;u=o+4|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function q1(o){return o=o|0,n[o+4>>2]|0}function ko(o,l){o=o|0,l=l|0;var u=0;u=o+8|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function Cy(o){return o=o|0,n[o+8>>2]|0}function yh(o,l){o=o|0,l=l|0;var u=0;u=o+12|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function W1(o){return o=o|0,n[o+12>>2]|0}function Qo(o,l){o=o|0,l=l|0;var u=0;u=o+16|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function Eh(o){return o=o|0,n[o+16>>2]|0}function Ih(o,l){o=o|0,l=l|0;var u=0;u=o+20|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function Au(o){return o=o|0,n[o+20>>2]|0}function Ch(o,l){o=o|0,l=l|0;var u=0;u=o+24|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function Rd(o){return o=o|0,n[o+24>>2]|0}function Td(o,l){o=o|0,l=l|0;var u=0;u=o+28|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function Fd(o){return o=o|0,n[o+28>>2]|0}function wy(o,l){o=o|0,l=l|0;var u=0;u=o+32|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function Ef(o){return o=o|0,n[o+32>>2]|0}function Ro(o,l){o=o|0,l=l|0;var u=0;u=o+36|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function Rl(o){return o=o|0,n[o+36>>2]|0}function wh(o,l){o=o|0,l=y(l);var u=0;u=o+40|0,y(h[u>>2])!=l&&(h[u>>2]=l,Oe(o))}function Nd(o,l){o=o|0,l=y(l);var u=0;u=o+44|0,y(h[u>>2])!=l&&(h[u>>2]=l,Oe(o))}function Tl(o,l){o=o|0,l=y(l);var u=0;u=o+48|0,y(h[u>>2])!=l&&(h[u>>2]=l,Oe(o))}function Fl(o,l){o=o|0,l=y(l);var u=0,A=0,g=0,m=0;m=Ut(l)|0,u=(m^1)&1,A=o+52|0,g=o+56|0,m|y(h[A>>2])==l&&(n[g>>2]|0)==(u|0)||(h[A>>2]=l,n[g>>2]=u,Oe(o))}function By(o,l){o=o|0,l=y(l);var u=0,A=0;A=o+52|0,u=o+56|0,y(h[A>>2])==l&&(n[u>>2]|0)==2||(h[A>>2]=l,A=Ut(l)|0,n[u>>2]=A?3:2,Oe(o))}function HA(o,l){o=o|0,l=l|0;var u=0,A=0;A=l+52|0,u=n[A+4>>2]|0,l=o,n[l>>2]=n[A>>2],n[l+4>>2]=u}function vy(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,g=0,m=0;m=Ut(u)|0,A=(m^1)&1,g=o+132+(l<<3)|0,l=o+132+(l<<3)+4|0,m|y(h[g>>2])==u&&(n[l>>2]|0)==(A|0)||(h[g>>2]=u,n[l>>2]=A,Oe(o))}function Sy(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,g=0,m=0;m=Ut(u)|0,A=m?0:2,g=o+132+(l<<3)|0,l=o+132+(l<<3)+4|0,m|y(h[g>>2])==u&&(n[l>>2]|0)==(A|0)||(h[g>>2]=u,n[l>>2]=A,Oe(o))}function jA(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=l+132+(u<<3)|0,l=n[A+4>>2]|0,u=o,n[u>>2]=n[A>>2],n[u+4>>2]=l}function GA(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,g=0,m=0;m=Ut(u)|0,A=(m^1)&1,g=o+60+(l<<3)|0,l=o+60+(l<<3)+4|0,m|y(h[g>>2])==u&&(n[l>>2]|0)==(A|0)||(h[g>>2]=u,n[l>>2]=A,Oe(o))}function W(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,g=0,m=0;m=Ut(u)|0,A=m?0:2,g=o+60+(l<<3)|0,l=o+60+(l<<3)+4|0,m|y(h[g>>2])==u&&(n[l>>2]|0)==(A|0)||(h[g>>2]=u,n[l>>2]=A,Oe(o))}function xt(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=l+60+(u<<3)|0,l=n[A+4>>2]|0,u=o,n[u>>2]=n[A>>2],n[u+4>>2]=l}function qA(o,l){o=o|0,l=l|0;var u=0;u=o+60+(l<<3)+4|0,(n[u>>2]|0)!=3&&(h[o+60+(l<<3)>>2]=y(ae),n[u>>2]=3,Oe(o))}function To(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,g=0,m=0;m=Ut(u)|0,A=(m^1)&1,g=o+204+(l<<3)|0,l=o+204+(l<<3)+4|0,m|y(h[g>>2])==u&&(n[l>>2]|0)==(A|0)||(h[g>>2]=u,n[l>>2]=A,Oe(o))}function If(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,g=0,m=0;m=Ut(u)|0,A=m?0:2,g=o+204+(l<<3)|0,l=o+204+(l<<3)+4|0,m|y(h[g>>2])==u&&(n[l>>2]|0)==(A|0)||(h[g>>2]=u,n[l>>2]=A,Oe(o))}function yt(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=l+204+(u<<3)|0,l=n[A+4>>2]|0,u=o,n[u>>2]=n[A>>2],n[u+4>>2]=l}function pu(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,g=0,m=0;m=Ut(u)|0,A=(m^1)&1,g=o+276+(l<<3)|0,l=o+276+(l<<3)+4|0,m|y(h[g>>2])==u&&(n[l>>2]|0)==(A|0)||(h[g>>2]=u,n[l>>2]=A,Oe(o))}function Dy(o,l){return o=o|0,l=l|0,y(h[o+276+(l<<3)>>2])}function Od(o,l){o=o|0,l=y(l);var u=0,A=0,g=0,m=0;m=Ut(l)|0,u=(m^1)&1,A=o+348|0,g=o+352|0,m|y(h[A>>2])==l&&(n[g>>2]|0)==(u|0)||(h[A>>2]=l,n[g>>2]=u,Oe(o))}function Y1(o,l){o=o|0,l=y(l);var u=0,A=0;A=o+348|0,u=o+352|0,y(h[A>>2])==l&&(n[u>>2]|0)==2||(h[A>>2]=l,A=Ut(l)|0,n[u>>2]=A?3:2,Oe(o))}function Bh(o){o=o|0;var l=0;l=o+352|0,(n[l>>2]|0)!=3&&(h[o+348>>2]=y(ae),n[l>>2]=3,Oe(o))}function ur(o,l){o=o|0,l=l|0;var u=0,A=0;A=l+348|0,u=n[A+4>>2]|0,l=o,n[l>>2]=n[A>>2],n[l+4>>2]=u}function zi(o,l){o=o|0,l=y(l);var u=0,A=0,g=0,m=0;m=Ut(l)|0,u=(m^1)&1,A=o+356|0,g=o+360|0,m|y(h[A>>2])==l&&(n[g>>2]|0)==(u|0)||(h[A>>2]=l,n[g>>2]=u,Oe(o))}function Cf(o,l){o=o|0,l=y(l);var u=0,A=0;A=o+356|0,u=o+360|0,y(h[A>>2])==l&&(n[u>>2]|0)==2||(h[A>>2]=l,A=Ut(l)|0,n[u>>2]=A?3:2,Oe(o))}function Za(o){o=o|0;var l=0;l=o+360|0,(n[l>>2]|0)!=3&&(h[o+356>>2]=y(ae),n[l>>2]=3,Oe(o))}function Ld(o,l){o=o|0,l=l|0;var u=0,A=0;A=l+356|0,u=n[A+4>>2]|0,l=o,n[l>>2]=n[A>>2],n[l+4>>2]=u}function hu(o,l){o=o|0,l=y(l);var u=0,A=0,g=0,m=0;m=Ut(l)|0,u=(m^1)&1,A=o+364|0,g=o+368|0,m|y(h[A>>2])==l&&(n[g>>2]|0)==(u|0)||(h[A>>2]=l,n[g>>2]=u,Oe(o))}function wf(o,l){o=o|0,l=y(l);var u=0,A=0,g=0,m=0;m=Ut(l)|0,u=m?0:2,A=o+364|0,g=o+368|0,m|y(h[A>>2])==l&&(n[g>>2]|0)==(u|0)||(h[A>>2]=l,n[g>>2]=u,Oe(o))}function wt(o,l){o=o|0,l=l|0;var u=0,A=0;A=l+364|0,u=n[A+4>>2]|0,l=o,n[l>>2]=n[A>>2],n[l+4>>2]=u}function mi(o,l){o=o|0,l=y(l);var u=0,A=0,g=0,m=0;m=Ut(l)|0,u=(m^1)&1,A=o+372|0,g=o+376|0,m|y(h[A>>2])==l&&(n[g>>2]|0)==(u|0)||(h[A>>2]=l,n[g>>2]=u,Oe(o))}function WA(o,l){o=o|0,l=y(l);var u=0,A=0,g=0,m=0;m=Ut(l)|0,u=m?0:2,A=o+372|0,g=o+376|0,m|y(h[A>>2])==l&&(n[g>>2]|0)==(u|0)||(h[A>>2]=l,n[g>>2]=u,Oe(o))}function $a(o,l){o=o|0,l=l|0;var u=0,A=0;A=l+372|0,u=n[A+4>>2]|0,l=o,n[l>>2]=n[A>>2],n[l+4>>2]=u}function ma(o,l){o=o|0,l=y(l);var u=0,A=0,g=0,m=0;m=Ut(l)|0,u=(m^1)&1,A=o+380|0,g=o+384|0,m|y(h[A>>2])==l&&(n[g>>2]|0)==(u|0)||(h[A>>2]=l,n[g>>2]=u,Oe(o))}function el(o,l){o=o|0,l=y(l);var u=0,A=0,g=0,m=0;m=Ut(l)|0,u=m?0:2,A=o+380|0,g=o+384|0,m|y(h[A>>2])==l&&(n[g>>2]|0)==(u|0)||(h[A>>2]=l,n[g>>2]=u,Oe(o))}function Md(o,l){o=o|0,l=l|0;var u=0,A=0;A=l+380|0,u=n[A+4>>2]|0,l=o,n[l>>2]=n[A>>2],n[l+4>>2]=u}function vh(o,l){o=o|0,l=y(l);var u=0,A=0,g=0,m=0;m=Ut(l)|0,u=(m^1)&1,A=o+388|0,g=o+392|0,m|y(h[A>>2])==l&&(n[g>>2]|0)==(u|0)||(h[A>>2]=l,n[g>>2]=u,Oe(o))}function Ud(o,l){o=o|0,l=y(l);var u=0,A=0,g=0,m=0;m=Ut(l)|0,u=m?0:2,A=o+388|0,g=o+392|0,m|y(h[A>>2])==l&&(n[g>>2]|0)==(u|0)||(h[A>>2]=l,n[g>>2]=u,Oe(o))}function by(o,l){o=o|0,l=l|0;var u=0,A=0;A=l+388|0,u=n[A+4>>2]|0,l=o,n[l>>2]=n[A>>2],n[l+4>>2]=u}function YA(o,l){o=o|0,l=y(l);var u=0;u=o+396|0,y(h[u>>2])!=l&&(h[u>>2]=l,Oe(o))}function _d(o){return o=o|0,y(h[o+396>>2])}function du(o){return o=o|0,y(h[o+400>>2])}function gu(o){return o=o|0,y(h[o+404>>2])}function Bf(o){return o=o|0,y(h[o+408>>2])}function Os(o){return o=o|0,y(h[o+412>>2])}function mu(o){return o=o|0,y(h[o+416>>2])}function qn(o){return o=o|0,y(h[o+420>>2])}function ss(o,l){switch(o=o|0,l=l|0,Bi(o,(l|0)<6,2918),l|0){case 0:{l=(n[o+496>>2]|0)==2?5:4;break}case 2:{l=(n[o+496>>2]|0)==2?4:5;break}default:}return y(h[o+424+(l<<2)>>2])}function Pi(o,l){switch(o=o|0,l=l|0,Bi(o,(l|0)<6,2918),l|0){case 0:{l=(n[o+496>>2]|0)==2?5:4;break}case 2:{l=(n[o+496>>2]|0)==2?4:5;break}default:}return y(h[o+448+(l<<2)>>2])}function VA(o,l){switch(o=o|0,l=l|0,Bi(o,(l|0)<6,2918),l|0){case 0:{l=(n[o+496>>2]|0)==2?5:4;break}case 2:{l=(n[o+496>>2]|0)==2?4:5;break}default:}return y(h[o+472+(l<<2)>>2])}function vf(o,l){o=o|0,l=l|0;var u=0,A=$e;return u=n[o+4>>2]|0,(u|0)==(n[l+4>>2]|0)?u?(A=y(h[o>>2]),o=y(ce(y(A-y(h[l>>2]))))>2]=0,n[A+4>>2]=0,n[A+8>>2]=0,Ue(A|0,o|0,l|0,0),No(o,3,(s[A+11>>0]|0)<0?n[A>>2]|0:A,u),yHe(A),I=u}function os(o,l,u,A){o=y(o),l=y(l),u=u|0,A=A|0;var g=$e;o=y(o*l),g=y(JM(o,y(1)));do if(yn(g,y(0))|0)o=y(o-g);else{if(o=y(o-g),yn(g,y(1))|0){o=y(o+y(1));break}if(u){o=y(o+y(1));break}A||(g>y(.5)?g=y(1):(A=yn(g,y(.5))|0,g=y(A?1:0)),o=y(o+g))}while(!1);return y(o/l)}function Nl(o,l,u,A,g,m,B,k,R,M,L,q,oe){o=o|0,l=y(l),u=u|0,A=y(A),g=g|0,m=y(m),B=B|0,k=y(k),R=y(R),M=y(M),L=y(L),q=y(q),oe=oe|0;var Ve=0,Le=$e,Te=$e,nt=$e,Ze=$e,ft=$e,He=$e;return R>2]),Le!=y(0))?(nt=y(os(l,Le,0,0)),Ze=y(os(A,Le,0,0)),Te=y(os(m,Le,0,0)),Le=y(os(k,Le,0,0))):(Te=m,nt=l,Le=k,Ze=A),(g|0)==(o|0)?Ve=yn(Te,nt)|0:Ve=0,(B|0)==(u|0)?oe=yn(Le,Ze)|0:oe=0,!Ve&&(ft=y(l-L),!(Fo(o,ft,R)|0))&&!(Sf(o,ft,g,R)|0)?Ve=Df(o,ft,g,m,R)|0:Ve=1,!oe&&(He=y(A-q),!(Fo(u,He,M)|0))&&!(Sf(u,He,B,M)|0)?oe=Df(u,He,B,k,M)|0:oe=1,oe=Ve&oe),oe|0}function Fo(o,l,u){return o=o|0,l=y(l),u=y(u),(o|0)==1?o=yn(l,u)|0:o=0,o|0}function Sf(o,l,u,A){return o=o|0,l=y(l),u=u|0,A=y(A),(o|0)==2&(u|0)==0?l>=A?o=1:o=yn(l,A)|0:o=0,o|0}function Df(o,l,u,A,g){return o=o|0,l=y(l),u=u|0,A=y(A),g=y(g),(o|0)==2&(u|0)==2&A>l?g<=l?o=1:o=yn(l,g)|0:o=0,o|0}function Ol(o,l,u,A,g,m,B,k,R,M,L){o=o|0,l=y(l),u=y(u),A=A|0,g=g|0,m=m|0,B=y(B),k=y(k),R=R|0,M=M|0,L=L|0;var q=0,oe=0,Ve=0,Le=0,Te=$e,nt=$e,Ze=0,ft=0,He=0,Ye=0,Mt=0,Gr=0,fr=0,$t=0,Tr=0,Hr=0,cr=0,jn=$e,Mo=$e,Uo=$e,_o=0,ol=0;cr=I,I=I+160|0,$t=cr+152|0,fr=cr+120|0,Gr=cr+104|0,He=cr+72|0,Le=cr+56|0,Mt=cr+8|0,ft=cr,Ye=(n[2279]|0)+1|0,n[2279]=Ye,Tr=o+984|0,s[Tr>>0]|0&&(n[o+512>>2]|0)!=(n[2278]|0)?Ze=4:(n[o+516>>2]|0)==(A|0)?Hr=0:Ze=4,(Ze|0)==4&&(n[o+520>>2]=0,n[o+924>>2]=-1,n[o+928>>2]=-1,h[o+932>>2]=y(-1),h[o+936>>2]=y(-1),Hr=1);e:do if(n[o+964>>2]|0)if(Te=y(En(o,2,B)),nt=y(En(o,0,B)),q=o+916|0,Uo=y(h[q>>2]),Mo=y(h[o+920>>2]),jn=y(h[o+932>>2]),Nl(g,l,m,u,n[o+924>>2]|0,Uo,n[o+928>>2]|0,Mo,jn,y(h[o+936>>2]),Te,nt,L)|0)Ze=22;else if(Ve=n[o+520>>2]|0,!Ve)Ze=21;else for(oe=0;;){if(q=o+524+(oe*24|0)|0,jn=y(h[q>>2]),Mo=y(h[o+524+(oe*24|0)+4>>2]),Uo=y(h[o+524+(oe*24|0)+16>>2]),Nl(g,l,m,u,n[o+524+(oe*24|0)+8>>2]|0,jn,n[o+524+(oe*24|0)+12>>2]|0,Mo,Uo,y(h[o+524+(oe*24|0)+20>>2]),Te,nt,L)|0){Ze=22;break e}if(oe=oe+1|0,oe>>>0>=Ve>>>0){Ze=21;break}}else{if(R){if(q=o+916|0,!(yn(y(h[q>>2]),l)|0)){Ze=21;break}if(!(yn(y(h[o+920>>2]),u)|0)){Ze=21;break}if((n[o+924>>2]|0)!=(g|0)){Ze=21;break}q=(n[o+928>>2]|0)==(m|0)?q:0,Ze=22;break}if(Ve=n[o+520>>2]|0,!Ve)Ze=21;else for(oe=0;;){if(q=o+524+(oe*24|0)|0,yn(y(h[q>>2]),l)|0&&yn(y(h[o+524+(oe*24|0)+4>>2]),u)|0&&(n[o+524+(oe*24|0)+8>>2]|0)==(g|0)&&(n[o+524+(oe*24|0)+12>>2]|0)==(m|0)){Ze=22;break e}if(oe=oe+1|0,oe>>>0>=Ve>>>0){Ze=21;break}}}while(!1);do if((Ze|0)==21)s[11697]|0?(q=0,Ze=28):(q=0,Ze=31);else if((Ze|0)==22){if(oe=(s[11697]|0)!=0,!((q|0)!=0&(Hr^1)))if(oe){Ze=28;break}else{Ze=31;break}Le=q+16|0,n[o+908>>2]=n[Le>>2],Ve=q+20|0,n[o+912>>2]=n[Ve>>2],(s[11698]|0)==0|oe^1||(n[ft>>2]=yu(Ye)|0,n[ft+4>>2]=Ye,No(o,4,2972,ft),oe=n[o+972>>2]|0,oe|0&&op[oe&127](o),g=ya(g,R)|0,m=ya(m,R)|0,ol=+y(h[Le>>2]),_o=+y(h[Ve>>2]),n[Mt>>2]=g,n[Mt+4>>2]=m,E[Mt+8>>3]=+l,E[Mt+16>>3]=+u,E[Mt+24>>3]=ol,E[Mt+32>>3]=_o,n[Mt+40>>2]=M,No(o,4,2989,Mt))}while(!1);return(Ze|0)==28&&(oe=yu(Ye)|0,n[Le>>2]=oe,n[Le+4>>2]=Ye,n[Le+8>>2]=Hr?3047:11699,No(o,4,3038,Le),oe=n[o+972>>2]|0,oe|0&&op[oe&127](o),Mt=ya(g,R)|0,Ze=ya(m,R)|0,n[He>>2]=Mt,n[He+4>>2]=Ze,E[He+8>>3]=+l,E[He+16>>3]=+u,n[He+24>>2]=M,No(o,4,3049,He),Ze=31),(Ze|0)==31&&(Ls(o,l,u,A,g,m,B,k,R,L),s[11697]|0&&(oe=n[2279]|0,Mt=yu(oe)|0,n[Gr>>2]=Mt,n[Gr+4>>2]=oe,n[Gr+8>>2]=Hr?3047:11699,No(o,4,3083,Gr),oe=n[o+972>>2]|0,oe|0&&op[oe&127](o),Mt=ya(g,R)|0,Gr=ya(m,R)|0,_o=+y(h[o+908>>2]),ol=+y(h[o+912>>2]),n[fr>>2]=Mt,n[fr+4>>2]=Gr,E[fr+8>>3]=_o,E[fr+16>>3]=ol,n[fr+24>>2]=M,No(o,4,3092,fr)),n[o+516>>2]=A,q||(oe=o+520|0,q=n[oe>>2]|0,(q|0)==16&&(s[11697]|0&&No(o,4,3124,$t),n[oe>>2]=0,q=0),R?q=o+916|0:(n[oe>>2]=q+1,q=o+524+(q*24|0)|0),h[q>>2]=l,h[q+4>>2]=u,n[q+8>>2]=g,n[q+12>>2]=m,n[q+16>>2]=n[o+908>>2],n[q+20>>2]=n[o+912>>2],q=0)),R&&(n[o+416>>2]=n[o+908>>2],n[o+420>>2]=n[o+912>>2],s[o+985>>0]=1,s[Tr>>0]=0),n[2279]=(n[2279]|0)+-1,n[o+512>>2]=n[2278],I=cr,Hr|(q|0)==0|0}function En(o,l,u){o=o|0,l=l|0,u=y(u);var A=$e;return A=y(K(o,l,u)),y(A+y(re(o,l,u)))}function No(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;m=I,I=I+16|0,g=m,n[g>>2]=A,o?A=n[o+976>>2]|0:A=0,bh(A,o,l,u,g),I=m}function yu(o){return o=o|0,(o>>>0>60?3201:3201+(60-o)|0)|0}function ya(o,l){o=o|0,l=l|0;var u=0,A=0,g=0;return g=I,I=I+32|0,u=g+12|0,A=g,n[u>>2]=n[254],n[u+4>>2]=n[255],n[u+8>>2]=n[256],n[A>>2]=n[257],n[A+4>>2]=n[258],n[A+8>>2]=n[259],(o|0)>2?o=11699:o=n[(l?A:u)+(o<<2)>>2]|0,I=g,o|0}function Ls(o,l,u,A,g,m,B,k,R,M){o=o|0,l=y(l),u=y(u),A=A|0,g=g|0,m=m|0,B=y(B),k=y(k),R=R|0,M=M|0;var L=0,q=0,oe=0,Ve=0,Le=$e,Te=$e,nt=$e,Ze=$e,ft=$e,He=$e,Ye=$e,Mt=0,Gr=0,fr=0,$t=$e,Tr=$e,Hr=0,cr=$e,jn=0,Mo=0,Uo=0,_o=0,ol=0,qh=0,Wh=0,mc=0,Yh=0,Of=0,Lf=0,Vh=0,Jh=0,Kh=0,ln=0,yc=0,zh=0,Du=0,Xh=$e,Zh=$e,Mf=$e,Uf=$e,bu=$e,Ao=0,ql=0,wa=0,Ec=0,lp=0,cp=$e,_f=$e,up=$e,fp=$e,po=$e,Hs=$e,Ic=0,Yn=$e,Ap=$e,Ho=$e,Pu=$e,jo=$e,xu=$e,pp=0,hp=0,ku=$e,ho=$e,Cc=0,dp=0,gp=0,mp=0,Nr=$e,fi=0,js=0,Go=0,go=0,Mr=0,Ar=0,wc=0,zt=$e,yp=0,vi=0;wc=I,I=I+16|0,Ao=wc+12|0,ql=wc+8|0,wa=wc+4|0,Ec=wc,Bi(o,(g|0)==0|(Ut(l)|0)^1,3326),Bi(o,(m|0)==0|(Ut(u)|0)^1,3406),js=pt(o,A)|0,n[o+496>>2]=js,Mr=gr(2,js)|0,Ar=gr(0,js)|0,h[o+440>>2]=y(K(o,Mr,B)),h[o+444>>2]=y(re(o,Mr,B)),h[o+428>>2]=y(K(o,Ar,B)),h[o+436>>2]=y(re(o,Ar,B)),h[o+464>>2]=y(vr(o,Mr)),h[o+468>>2]=y(_n(o,Mr)),h[o+452>>2]=y(vr(o,Ar)),h[o+460>>2]=y(_n(o,Ar)),h[o+488>>2]=y(yi(o,Mr,B)),h[o+492>>2]=y(vs(o,Mr,B)),h[o+476>>2]=y(yi(o,Ar,B)),h[o+484>>2]=y(vs(o,Ar,B));do if(n[o+964>>2]|0)zA(o,l,u,g,m,B,k);else{if(Go=o+948|0,go=(n[o+952>>2]|0)-(n[Go>>2]|0)>>2,!go){lP(o,l,u,g,m,B,k);break}if(!R&&V1(o,l,u,g,m,B,k)|0)break;ee(o),yc=o+508|0,s[yc>>0]=0,Mr=gr(n[o+4>>2]|0,js)|0,Ar=ky(Mr,js)|0,fi=de(Mr)|0,zh=n[o+8>>2]|0,dp=o+28|0,Du=(n[dp>>2]|0)!=0,jo=fi?B:k,ku=fi?k:B,Xh=y(xh(o,Mr,B)),Zh=y(J1(o,Mr,B)),Le=y(xh(o,Ar,B)),xu=y(tl(o,Mr,B)),ho=y(tl(o,Ar,B)),fr=fi?g:m,Cc=fi?m:g,Nr=fi?xu:ho,ft=fi?ho:xu,Pu=y(En(o,2,B)),Ze=y(En(o,0,B)),Te=y(y(Zr(o+364|0,B))-Nr),nt=y(y(Zr(o+380|0,B))-Nr),He=y(y(Zr(o+372|0,k))-ft),Ye=y(y(Zr(o+388|0,k))-ft),Mf=fi?Te:He,Uf=fi?nt:Ye,Pu=y(l-Pu),l=y(Pu-Nr),Ut(l)|0?Nr=l:Nr=y(ri(y(fg(l,nt)),Te)),Ap=y(u-Ze),l=y(Ap-ft),Ut(l)|0?Ho=l:Ho=y(ri(y(fg(l,Ye)),He)),Te=fi?Nr:Ho,Yn=fi?Ho:Nr;e:do if((fr|0)==1)for(A=0,q=0;;){if(L=Bs(o,q)|0,!A)y(XA(L))>y(0)&&y(kh(L))>y(0)?A=L:A=0;else if(K1(L)|0){Ve=0;break e}if(q=q+1|0,q>>>0>=go>>>0){Ve=A;break}}else Ve=0;while(!1);Mt=Ve+500|0,Gr=Ve+504|0,A=0,L=0,l=y(0),oe=0;do{if(q=n[(n[Go>>2]|0)+(oe<<2)>>2]|0,(n[q+36>>2]|0)==1)Qy(q),s[q+985>>0]=1,s[q+984>>0]=0;else{bf(q),R&&Dh(q,pt(q,js)|0,Te,Yn,Nr);do if((n[q+24>>2]|0)!=1)if((q|0)==(Ve|0)){n[Mt>>2]=n[2278],h[Gr>>2]=y(0);break}else{cP(o,q,Nr,g,Ho,Nr,Ho,m,js,M);break}else L|0&&(n[L+960>>2]=q),n[q+960>>2]=0,L=q,A=A|0?A:q;while(!1);Hs=y(h[q+504>>2]),l=y(l+y(Hs+y(En(q,Mr,Nr))))}oe=oe+1|0}while((oe|0)!=(go|0));for(Uo=l>Te,Ic=Du&((fr|0)==2&Uo)?1:fr,jn=(Cc|0)==1,ol=jn&(R^1),qh=(Ic|0)==1,Wh=(Ic|0)==2,mc=976+(Mr<<2)|0,Yh=(Cc|2|0)==2,Kh=jn&(Du^1),Of=1040+(Ar<<2)|0,Lf=1040+(Mr<<2)|0,Vh=976+(Ar<<2)|0,Jh=(Cc|0)!=1,Uo=Du&((fr|0)!=0&Uo),Mo=o+976|0,jn=jn^1,l=Te,Hr=0,_o=0,Hs=y(0),bu=y(0);;){e:do if(Hr>>>0>>0)for(Gr=n[Go>>2]|0,oe=0,Ye=y(0),He=y(0),nt=y(0),Te=y(0),q=0,L=0,Ve=Hr;;){if(Mt=n[Gr+(Ve<<2)>>2]|0,(n[Mt+36>>2]|0)!=1&&(n[Mt+940>>2]=_o,(n[Mt+24>>2]|0)!=1)){if(Ze=y(En(Mt,Mr,Nr)),ln=n[mc>>2]|0,u=y(Zr(Mt+380+(ln<<3)|0,jo)),ft=y(h[Mt+504>>2]),u=y(fg(u,ft)),u=y(ri(y(Zr(Mt+364+(ln<<3)|0,jo)),u)),Du&(oe|0)!=0&y(Ze+y(He+u))>l){m=oe,Ze=Ye,fr=Ve;break e}Ze=y(Ze+u),u=y(He+Ze),Ze=y(Ye+Ze),K1(Mt)|0&&(nt=y(nt+y(XA(Mt))),Te=y(Te-y(ft*y(kh(Mt))))),L|0&&(n[L+960>>2]=Mt),n[Mt+960>>2]=0,oe=oe+1|0,L=Mt,q=q|0?q:Mt}else Ze=Ye,u=He;if(Ve=Ve+1|0,Ve>>>0>>0)Ye=Ze,He=u;else{m=oe,fr=Ve;break}}else m=0,Ze=y(0),nt=y(0),Te=y(0),q=0,fr=Hr;while(!1);ln=nt>y(0)&nty(0)&TeUf&((Ut(Uf)|0)^1))l=Uf,ln=51;else if(s[(n[Mo>>2]|0)+3>>0]|0)ln=51;else{if($t!=y(0)&&y(XA(o))!=y(0)){ln=53;break}l=Ze,ln=53}while(!1);if((ln|0)==51&&(ln=0,Ut(l)|0?ln=53:(Tr=y(l-Ze),cr=l)),(ln|0)==53&&(ln=0,Ze>2]|0,Ve=Try(0),He=y(Tr/$t),nt=y(0),Ze=y(0),l=y(0),L=q;do u=y(Zr(L+380+(oe<<3)|0,jo)),Te=y(Zr(L+364+(oe<<3)|0,jo)),Te=y(fg(u,y(ri(Te,y(h[L+504>>2]))))),Ve?(u=y(Te*y(kh(L))),u!=y(-0)&&(zt=y(Te-y(ft*u)),cp=y(Wn(L,Mr,zt,cr,Nr)),zt!=cp)&&(nt=y(nt-y(cp-Te)),l=y(l+u))):Mt&&(_f=y(XA(L)),_f!=y(0))&&(zt=y(Te+y(He*_f)),up=y(Wn(L,Mr,zt,cr,Nr)),zt!=up)&&(nt=y(nt-y(up-Te)),Ze=y(Ze-_f)),L=n[L+960>>2]|0;while(L|0);if(l=y(Ye+l),Te=y(Tr+nt),lp)l=y(0);else{ft=y($t+Ze),Ve=n[mc>>2]|0,Mt=Tey(0),ft=y(Te/ft),l=y(0);do{zt=y(Zr(q+380+(Ve<<3)|0,jo)),nt=y(Zr(q+364+(Ve<<3)|0,jo)),nt=y(fg(zt,y(ri(nt,y(h[q+504>>2]))))),Mt?(zt=y(nt*y(kh(q))),Te=y(-zt),zt!=y(-0)?(zt=y(He*Te),Te=y(Wn(q,Mr,y(nt+(Gr?Te:zt)),cr,Nr))):Te=nt):oe&&(fp=y(XA(q)),fp!=y(0))?Te=y(Wn(q,Mr,y(nt+y(ft*fp)),cr,Nr)):Te=nt,l=y(l-y(Te-nt)),Ze=y(En(q,Mr,Nr)),u=y(En(q,Ar,Nr)),Te=y(Te+Ze),h[ql>>2]=Te,n[Ec>>2]=1,nt=y(h[q+396>>2]);e:do if(Ut(nt)|0){L=Ut(Yn)|0;do if(!L){if(Uo|(uo(q,Ar,Yn)|0|jn)||(as(o,q)|0)!=4||(n[(Ll(q,Ar)|0)+4>>2]|0)==3||(n[(Ml(q,Ar)|0)+4>>2]|0)==3)break;h[Ao>>2]=Yn,n[wa>>2]=1;break e}while(!1);if(uo(q,Ar,Yn)|0){L=n[q+992+(n[Vh>>2]<<2)>>2]|0,zt=y(u+y(Zr(L,Yn))),h[Ao>>2]=zt,L=Jh&(n[L+4>>2]|0)==2,n[wa>>2]=((Ut(zt)|0|L)^1)&1;break}else{h[Ao>>2]=Yn,n[wa>>2]=L?0:2;break}}else zt=y(Te-Ze),$t=y(zt/nt),zt=y(nt*zt),n[wa>>2]=1,h[Ao>>2]=y(u+(fi?$t:zt));while(!1);Eu(q,Mr,cr,Nr,Ec,ql),Eu(q,Ar,Yn,Nr,wa,Ao);do if(!(uo(q,Ar,Yn)|0)&&(as(o,q)|0)==4){if((n[(Ll(q,Ar)|0)+4>>2]|0)==3){L=0;break}L=(n[(Ml(q,Ar)|0)+4>>2]|0)!=3}else L=0;while(!1);zt=y(h[ql>>2]),$t=y(h[Ao>>2]),yp=n[Ec>>2]|0,vi=n[wa>>2]|0,Ol(q,fi?zt:$t,fi?$t:zt,js,fi?yp:vi,fi?vi:yp,Nr,Ho,R&(L^1),3488,M)|0,s[yc>>0]=s[yc>>0]|s[q+508>>0],q=n[q+960>>2]|0}while(q|0)}}else l=y(0);if(l=y(Tr+l),vi=l>0]=vi|c[yc>>0],Wh&l>y(0)?(L=n[mc>>2]|0,n[o+364+(L<<3)+4>>2]|0&&(po=y(Zr(o+364+(L<<3)|0,jo)),po>=y(0))?Te=y(ri(y(0),y(po-y(cr-l)))):Te=y(0)):Te=l,Mt=Hr>>>0>>0,Mt){Ve=n[Go>>2]|0,oe=Hr,L=0;do q=n[Ve+(oe<<2)>>2]|0,n[q+24>>2]|0||(L=((n[(Ll(q,Mr)|0)+4>>2]|0)==3&1)+L|0,L=L+((n[(Ml(q,Mr)|0)+4>>2]|0)==3&1)|0),oe=oe+1|0;while((oe|0)!=(fr|0));L?(Ze=y(0),u=y(0)):ln=101}else ln=101;e:do if((ln|0)==101)switch(ln=0,zh|0){case 1:{L=0,Ze=y(Te*y(.5)),u=y(0);break e}case 2:{L=0,Ze=Te,u=y(0);break e}case 3:{if(m>>>0<=1){L=0,Ze=y(0),u=y(0);break e}u=y((m+-1|0)>>>0),L=0,Ze=y(0),u=y(y(ri(Te,y(0)))/u);break e}case 5:{u=y(Te/y((m+1|0)>>>0)),L=0,Ze=u;break e}case 4:{u=y(Te/y(m>>>0)),L=0,Ze=y(u*y(.5));break e}default:{L=0,Ze=y(0),u=y(0);break e}}while(!1);if(l=y(Xh+Ze),Mt){nt=y(Te/y(L|0)),oe=n[Go>>2]|0,q=Hr,Te=y(0);do{L=n[oe+(q<<2)>>2]|0;e:do if((n[L+36>>2]|0)!=1){switch(n[L+24>>2]|0){case 1:{if(Ea(L,Mr)|0){if(!R)break e;zt=y(ZA(L,Mr,cr)),zt=y(zt+y(vr(o,Mr))),zt=y(zt+y(K(L,Mr,Nr))),h[L+400+(n[Lf>>2]<<2)>>2]=zt;break e}break}case 0:if(vi=(n[(Ll(L,Mr)|0)+4>>2]|0)==3,zt=y(nt+l),l=vi?zt:l,R&&(vi=L+400+(n[Lf>>2]<<2)|0,h[vi>>2]=y(l+y(h[vi>>2]))),vi=(n[(Ml(L,Mr)|0)+4>>2]|0)==3,zt=y(nt+l),l=vi?zt:l,ol){zt=y(u+y(En(L,Mr,Nr))),Te=Yn,l=y(l+y(zt+y(h[L+504>>2])));break e}else{l=y(l+y(u+y($A(L,Mr,Nr)))),Te=y(ri(Te,y($A(L,Ar,Nr))));break e}default:}R&&(zt=y(Ze+y(vr(o,Mr))),vi=L+400+(n[Lf>>2]<<2)|0,h[vi>>2]=y(zt+y(h[vi>>2])))}while(!1);q=q+1|0}while((q|0)!=(fr|0))}else Te=y(0);if(u=y(Zh+l),Yh?Ze=y(y(Wn(o,Ar,y(ho+Te),ku,B))-ho):Ze=Yn,nt=y(y(Wn(o,Ar,y(ho+(Kh?Yn:Te)),ku,B))-ho),Mt&R){q=Hr;do{oe=n[(n[Go>>2]|0)+(q<<2)>>2]|0;do if((n[oe+36>>2]|0)!=1){if((n[oe+24>>2]|0)==1){if(Ea(oe,Ar)|0){if(zt=y(ZA(oe,Ar,Yn)),zt=y(zt+y(vr(o,Ar))),zt=y(zt+y(K(oe,Ar,Nr))),L=n[Of>>2]|0,h[oe+400+(L<<2)>>2]=zt,!(Ut(zt)|0))break}else L=n[Of>>2]|0;zt=y(vr(o,Ar)),h[oe+400+(L<<2)>>2]=y(zt+y(K(oe,Ar,Nr)));break}L=as(o,oe)|0;do if((L|0)==4){if((n[(Ll(oe,Ar)|0)+4>>2]|0)==3){ln=139;break}if((n[(Ml(oe,Ar)|0)+4>>2]|0)==3){ln=139;break}if(uo(oe,Ar,Yn)|0){l=Le;break}yp=n[oe+908+(n[mc>>2]<<2)>>2]|0,n[Ao>>2]=yp,l=y(h[oe+396>>2]),vi=Ut(l)|0,Te=(n[S>>2]=yp,y(h[S>>2])),vi?l=nt:(Tr=y(En(oe,Ar,Nr)),zt=y(Te/l),l=y(l*Te),l=y(Tr+(fi?zt:l))),h[ql>>2]=l,h[Ao>>2]=y(y(En(oe,Mr,Nr))+Te),n[wa>>2]=1,n[Ec>>2]=1,Eu(oe,Mr,cr,Nr,wa,Ao),Eu(oe,Ar,Yn,Nr,Ec,ql),l=y(h[Ao>>2]),Tr=y(h[ql>>2]),zt=fi?l:Tr,l=fi?Tr:l,vi=((Ut(zt)|0)^1)&1,Ol(oe,zt,l,js,vi,((Ut(l)|0)^1)&1,Nr,Ho,1,3493,M)|0,l=Le}else ln=139;while(!1);e:do if((ln|0)==139){ln=0,l=y(Ze-y($A(oe,Ar,Nr)));do if((n[(Ll(oe,Ar)|0)+4>>2]|0)==3){if((n[(Ml(oe,Ar)|0)+4>>2]|0)!=3)break;l=y(Le+y(ri(y(0),y(l*y(.5)))));break e}while(!1);if((n[(Ml(oe,Ar)|0)+4>>2]|0)==3){l=Le;break}if((n[(Ll(oe,Ar)|0)+4>>2]|0)==3){l=y(Le+y(ri(y(0),l)));break}switch(L|0){case 1:{l=Le;break e}case 2:{l=y(Le+y(l*y(.5)));break e}default:{l=y(Le+l);break e}}}while(!1);zt=y(Hs+l),vi=oe+400+(n[Of>>2]<<2)|0,h[vi>>2]=y(zt+y(h[vi>>2]))}while(!1);q=q+1|0}while((q|0)!=(fr|0))}if(Hs=y(Hs+nt),bu=y(ri(bu,u)),m=_o+1|0,fr>>>0>=go>>>0)break;l=cr,Hr=fr,_o=m}do if(R){if(L=m>>>0>1,!L&&!(DL(o)|0))break;if(!(Ut(Yn)|0)){l=y(Yn-Hs);e:do switch(n[o+12>>2]|0){case 3:{Le=y(Le+l),He=y(0);break}case 2:{Le=y(Le+y(l*y(.5))),He=y(0);break}case 4:{Yn>Hs?He=y(l/y(m>>>0)):He=y(0);break}case 7:if(Yn>Hs){Le=y(Le+y(l/y(m<<1>>>0))),He=y(l/y(m>>>0)),He=L?He:y(0);break e}else{Le=y(Le+y(l*y(.5))),He=y(0);break e}case 6:{He=y(l/y(_o>>>0)),He=Yn>Hs&L?He:y(0);break}default:He=y(0)}while(!1);if(m|0)for(Mt=1040+(Ar<<2)|0,Gr=976+(Ar<<2)|0,Ve=0,q=0;;){e:do if(q>>>0>>0)for(Te=y(0),nt=y(0),l=y(0),oe=q;;){L=n[(n[Go>>2]|0)+(oe<<2)>>2]|0;do if((n[L+36>>2]|0)!=1&&!(n[L+24>>2]|0)){if((n[L+940>>2]|0)!=(Ve|0))break e;if(bL(L,Ar)|0&&(zt=y(h[L+908+(n[Gr>>2]<<2)>>2]),l=y(ri(l,y(zt+y(En(L,Ar,Nr)))))),(as(o,L)|0)!=5)break;po=y(qd(L)),po=y(po+y(K(L,0,Nr))),zt=y(h[L+912>>2]),zt=y(y(zt+y(En(L,0,Nr)))-po),po=y(ri(nt,po)),zt=y(ri(Te,zt)),Te=zt,nt=po,l=y(ri(l,y(po+zt)))}while(!1);if(L=oe+1|0,L>>>0>>0)oe=L;else{oe=L;break}}else nt=y(0),l=y(0),oe=q;while(!1);if(ft=y(He+l),u=Le,Le=y(Le+ft),q>>>0>>0){Ze=y(u+nt),L=q;do{q=n[(n[Go>>2]|0)+(L<<2)>>2]|0;e:do if((n[q+36>>2]|0)!=1&&!(n[q+24>>2]|0))switch(as(o,q)|0){case 1:{zt=y(u+y(K(q,Ar,Nr))),h[q+400+(n[Mt>>2]<<2)>>2]=zt;break e}case 3:{zt=y(y(Le-y(re(q,Ar,Nr)))-y(h[q+908+(n[Gr>>2]<<2)>>2])),h[q+400+(n[Mt>>2]<<2)>>2]=zt;break e}case 2:{zt=y(u+y(y(ft-y(h[q+908+(n[Gr>>2]<<2)>>2]))*y(.5))),h[q+400+(n[Mt>>2]<<2)>>2]=zt;break e}case 4:{if(zt=y(u+y(K(q,Ar,Nr))),h[q+400+(n[Mt>>2]<<2)>>2]=zt,uo(q,Ar,Yn)|0||(fi?(Te=y(h[q+908>>2]),l=y(Te+y(En(q,Mr,Nr))),nt=ft):(nt=y(h[q+912>>2]),nt=y(nt+y(En(q,Ar,Nr))),l=ft,Te=y(h[q+908>>2])),yn(l,Te)|0&&yn(nt,y(h[q+912>>2]))|0))break e;Ol(q,l,nt,js,1,1,Nr,Ho,1,3501,M)|0;break e}case 5:{h[q+404>>2]=y(y(Ze-y(qd(q)))+y(ZA(q,0,Yn)));break e}default:break e}while(!1);L=L+1|0}while((L|0)!=(oe|0))}if(Ve=Ve+1|0,(Ve|0)==(m|0))break;q=oe}}}while(!1);if(h[o+908>>2]=y(Wn(o,2,Pu,B,B)),h[o+912>>2]=y(Wn(o,0,Ap,k,B)),Ic|0&&(pp=n[o+32>>2]|0,hp=(Ic|0)==2,!(hp&(pp|0)!=2))?hp&(pp|0)==2&&(l=y(xu+cr),l=y(ri(y(fg(l,y(Wd(o,Mr,bu,jo)))),xu)),ln=198):(l=y(Wn(o,Mr,bu,jo,B)),ln=198),(ln|0)==198&&(h[o+908+(n[976+(Mr<<2)>>2]<<2)>>2]=l),Cc|0&&(gp=n[o+32>>2]|0,mp=(Cc|0)==2,!(mp&(gp|0)!=2))?mp&(gp|0)==2&&(l=y(ho+Yn),l=y(ri(y(fg(l,y(Wd(o,Ar,y(ho+Hs),ku)))),ho)),ln=204):(l=y(Wn(o,Ar,y(ho+Hs),ku,B)),ln=204),(ln|0)==204&&(h[o+908+(n[976+(Ar<<2)>>2]<<2)>>2]=l),R){if((n[dp>>2]|0)==2){q=976+(Ar<<2)|0,oe=1040+(Ar<<2)|0,L=0;do Ve=Bs(o,L)|0,n[Ve+24>>2]|0||(yp=n[q>>2]|0,zt=y(h[o+908+(yp<<2)>>2]),vi=Ve+400+(n[oe>>2]<<2)|0,zt=y(zt-y(h[vi>>2])),h[vi>>2]=y(zt-y(h[Ve+908+(yp<<2)>>2]))),L=L+1|0;while((L|0)!=(go|0))}if(A|0){L=fi?Ic:g;do PL(o,A,Nr,L,Ho,js,M),A=n[A+960>>2]|0;while(A|0)}if(L=(Mr|2|0)==3,q=(Ar|2|0)==3,L|q){A=0;do oe=n[(n[Go>>2]|0)+(A<<2)>>2]|0,(n[oe+36>>2]|0)!=1&&(L&&z1(o,oe,Mr),q&&z1(o,oe,Ar)),A=A+1|0;while((A|0)!=(go|0))}}}while(!1);I=wc}function Sh(o,l){o=o|0,l=y(l);var u=0;Ka(o,l>=y(0),3147),u=l==y(0),h[o+4>>2]=u?y(0):l}function JA(o,l,u,A){o=o|0,l=y(l),u=y(u),A=A|0;var g=$e,m=$e,B=0,k=0,R=0;n[2278]=(n[2278]|0)+1,bf(o),uo(o,2,l)|0?(g=y(Zr(n[o+992>>2]|0,l)),R=1,g=y(g+y(En(o,2,l)))):(g=y(Zr(o+380|0,l)),g>=y(0)?R=2:(R=((Ut(l)|0)^1)&1,g=l)),uo(o,0,u)|0?(m=y(Zr(n[o+996>>2]|0,u)),k=1,m=y(m+y(En(o,0,l)))):(m=y(Zr(o+388|0,u)),m>=y(0)?k=2:(k=((Ut(u)|0)^1)&1,m=u)),B=o+976|0,Ol(o,g,m,A,R,k,l,u,1,3189,n[B>>2]|0)|0&&(Dh(o,n[o+496>>2]|0,l,u,l),KA(o,y(h[(n[B>>2]|0)+4>>2]),y(0),y(0)),s[11696]|0)&&Hd(o,7)}function bf(o){o=o|0;var l=0,u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;k=I,I=I+32|0,B=k+24|0,m=k+16|0,A=k+8|0,g=k,u=0;do l=o+380+(u<<3)|0,n[o+380+(u<<3)+4>>2]|0&&(R=l,M=n[R+4>>2]|0,L=A,n[L>>2]=n[R>>2],n[L+4>>2]=M,L=o+364+(u<<3)|0,M=n[L+4>>2]|0,R=g,n[R>>2]=n[L>>2],n[R+4>>2]=M,n[m>>2]=n[A>>2],n[m+4>>2]=n[A+4>>2],n[B>>2]=n[g>>2],n[B+4>>2]=n[g+4>>2],vf(m,B)|0)||(l=o+348+(u<<3)|0),n[o+992+(u<<2)>>2]=l,u=u+1|0;while((u|0)!=2);I=k}function uo(o,l,u){o=o|0,l=l|0,u=y(u);var A=0;switch(o=n[o+992+(n[976+(l<<2)>>2]<<2)>>2]|0,n[o+4>>2]|0){case 0:case 3:{o=0;break}case 1:{y(h[o>>2])>2])>2]|0){case 2:{l=y(y(y(h[o>>2])*l)/y(100));break}case 1:{l=y(h[o>>2]);break}default:l=y(ae)}return y(l)}function Dh(o,l,u,A,g){o=o|0,l=l|0,u=y(u),A=y(A),g=y(g);var m=0,B=$e;l=n[o+944>>2]|0?l:1,m=gr(n[o+4>>2]|0,l)|0,l=ky(m,l)|0,u=y(uP(o,m,u)),A=y(uP(o,l,A)),B=y(u+y(K(o,m,g))),h[o+400+(n[1040+(m<<2)>>2]<<2)>>2]=B,u=y(u+y(re(o,m,g))),h[o+400+(n[1e3+(m<<2)>>2]<<2)>>2]=u,u=y(A+y(K(o,l,g))),h[o+400+(n[1040+(l<<2)>>2]<<2)>>2]=u,g=y(A+y(re(o,l,g))),h[o+400+(n[1e3+(l<<2)>>2]<<2)>>2]=g}function KA(o,l,u,A){o=o|0,l=y(l),u=y(u),A=y(A);var g=0,m=0,B=$e,k=$e,R=0,M=0,L=$e,q=0,oe=$e,Ve=$e,Le=$e,Te=$e;if(l!=y(0)&&(g=o+400|0,Te=y(h[g>>2]),m=o+404|0,Le=y(h[m>>2]),q=o+416|0,Ve=y(h[q>>2]),M=o+420|0,B=y(h[M>>2]),oe=y(Te+u),L=y(Le+A),A=y(oe+Ve),k=y(L+B),R=(n[o+988>>2]|0)==1,h[g>>2]=y(os(Te,l,0,R)),h[m>>2]=y(os(Le,l,0,R)),u=y(JM(y(Ve*l),y(1))),yn(u,y(0))|0?m=0:m=(yn(u,y(1))|0)^1,u=y(JM(y(B*l),y(1))),yn(u,y(0))|0?g=0:g=(yn(u,y(1))|0)^1,Te=y(os(A,l,R&m,R&(m^1))),h[q>>2]=y(Te-y(os(oe,l,0,R))),Te=y(os(k,l,R&g,R&(g^1))),h[M>>2]=y(Te-y(os(L,l,0,R))),m=(n[o+952>>2]|0)-(n[o+948>>2]|0)>>2,m|0)){g=0;do KA(Bs(o,g)|0,l,oe,L),g=g+1|0;while((g|0)!=(m|0))}}function Py(o,l,u,A,g){switch(o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,u|0){case 5:case 0:{o=_X(n[489]|0,A,g)|0;break}default:o=hHe(A,g)|0}return o|0}function Gd(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;g=I,I=I+16|0,m=g,n[m>>2]=A,bh(o,0,l,u,m),I=g}function bh(o,l,u,A,g){if(o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,o=o|0?o:956,aZ[n[o+8>>2]&1](o,l,u,A,g)|0,(u|0)==5)Nt();else return}function dc(o,l,u){o=o|0,l=l|0,u=u|0,s[o+l>>0]=u&1}function xy(o,l){o=o|0,l=l|0;var u=0,A=0;n[o>>2]=0,n[o+4>>2]=0,n[o+8>>2]=0,u=l+4|0,A=(n[u>>2]|0)-(n[l>>2]|0)>>2,A|0&&(Ph(o,A),kt(o,n[l>>2]|0,n[u>>2]|0,A))}function Ph(o,l){o=o|0,l=l|0;var u=0;if((N(o)|0)>>>0>>0&&an(o),l>>>0>1073741823)Nt();else{u=Kt(l<<2)|0,n[o+4>>2]=u,n[o>>2]=u,n[o+8>>2]=u+(l<<2);return}}function kt(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,A=o+4|0,o=u-l|0,(o|0)>0&&(Rr(n[A>>2]|0,l|0,o|0)|0,n[A>>2]=(n[A>>2]|0)+(o>>>2<<2))}function N(o){return o=o|0,1073741823}function K(o,l,u){return o=o|0,l=l|0,u=y(u),de(l)|0&&n[o+96>>2]|0?o=o+92|0:o=Rn(o+60|0,n[1040+(l<<2)>>2]|0,992)|0,y(Je(o,u))}function re(o,l,u){return o=o|0,l=l|0,u=y(u),de(l)|0&&n[o+104>>2]|0?o=o+100|0:o=Rn(o+60|0,n[1e3+(l<<2)>>2]|0,992)|0,y(Je(o,u))}function de(o){return o=o|0,(o|1|0)==3|0}function Je(o,l){return o=o|0,l=y(l),(n[o+4>>2]|0)==3?l=y(0):l=y(Zr(o,l)),y(l)}function pt(o,l){return o=o|0,l=l|0,o=n[o>>2]|0,(o|0?o:(l|0)>1?l:1)|0}function gr(o,l){o=o|0,l=l|0;var u=0;e:do if((l|0)==2){switch(o|0){case 2:{o=3;break e}case 3:break;default:{u=4;break e}}o=2}else u=4;while(!1);return o|0}function vr(o,l){o=o|0,l=l|0;var u=$e;return de(l)|0&&n[o+312>>2]|0&&(u=y(h[o+308>>2]),u>=y(0))||(u=y(ri(y(h[(Rn(o+276|0,n[1040+(l<<2)>>2]|0,992)|0)>>2]),y(0)))),y(u)}function _n(o,l){o=o|0,l=l|0;var u=$e;return de(l)|0&&n[o+320>>2]|0&&(u=y(h[o+316>>2]),u>=y(0))||(u=y(ri(y(h[(Rn(o+276|0,n[1e3+(l<<2)>>2]|0,992)|0)>>2]),y(0)))),y(u)}function yi(o,l,u){o=o|0,l=l|0,u=y(u);var A=$e;return de(l)|0&&n[o+240>>2]|0&&(A=y(Zr(o+236|0,u)),A>=y(0))||(A=y(ri(y(Zr(Rn(o+204|0,n[1040+(l<<2)>>2]|0,992)|0,u)),y(0)))),y(A)}function vs(o,l,u){o=o|0,l=l|0,u=y(u);var A=$e;return de(l)|0&&n[o+248>>2]|0&&(A=y(Zr(o+244|0,u)),A>=y(0))||(A=y(ri(y(Zr(Rn(o+204|0,n[1e3+(l<<2)>>2]|0,992)|0,u)),y(0)))),y(A)}function zA(o,l,u,A,g,m,B){o=o|0,l=y(l),u=y(u),A=A|0,g=g|0,m=y(m),B=y(B);var k=$e,R=$e,M=$e,L=$e,q=$e,oe=$e,Ve=0,Le=0,Te=0;Te=I,I=I+16|0,Ve=Te,Le=o+964|0,Bi(o,(n[Le>>2]|0)!=0,3519),k=y(tl(o,2,l)),R=y(tl(o,0,l)),M=y(En(o,2,l)),L=y(En(o,0,l)),Ut(l)|0?q=l:q=y(ri(y(0),y(y(l-M)-k))),Ut(u)|0?oe=u:oe=y(ri(y(0),y(y(u-L)-R))),(A|0)==1&(g|0)==1?(h[o+908>>2]=y(Wn(o,2,y(l-M),m,m)),l=y(Wn(o,0,y(u-L),B,m))):(lZ[n[Le>>2]&1](Ve,o,q,A,oe,g),q=y(k+y(h[Ve>>2])),oe=y(l-M),h[o+908>>2]=y(Wn(o,2,(A|2|0)==2?q:oe,m,m)),oe=y(R+y(h[Ve+4>>2])),l=y(u-L),l=y(Wn(o,0,(g|2|0)==2?oe:l,B,m))),h[o+912>>2]=l,I=Te}function lP(o,l,u,A,g,m,B){o=o|0,l=y(l),u=y(u),A=A|0,g=g|0,m=y(m),B=y(B);var k=$e,R=$e,M=$e,L=$e;M=y(tl(o,2,m)),k=y(tl(o,0,m)),L=y(En(o,2,m)),R=y(En(o,0,m)),l=y(l-L),h[o+908>>2]=y(Wn(o,2,(A|2|0)==2?M:l,m,m)),u=y(u-R),h[o+912>>2]=y(Wn(o,0,(g|2|0)==2?k:u,B,m))}function V1(o,l,u,A,g,m,B){o=o|0,l=y(l),u=y(u),A=A|0,g=g|0,m=y(m),B=y(B);var k=0,R=$e,M=$e;return k=(A|0)==2,!(l<=y(0)&k)&&!(u<=y(0)&(g|0)==2)&&!((A|0)==1&(g|0)==1)?o=0:(R=y(En(o,0,m)),M=y(En(o,2,m)),k=l>2]=y(Wn(o,2,k?y(0):l,m,m)),l=y(u-R),k=u>2]=y(Wn(o,0,k?y(0):l,B,m)),o=1),o|0}function ky(o,l){return o=o|0,l=l|0,Yd(o)|0?o=gr(2,l)|0:o=0,o|0}function xh(o,l,u){return o=o|0,l=l|0,u=y(u),u=y(yi(o,l,u)),y(u+y(vr(o,l)))}function J1(o,l,u){return o=o|0,l=l|0,u=y(u),u=y(vs(o,l,u)),y(u+y(_n(o,l)))}function tl(o,l,u){o=o|0,l=l|0,u=y(u);var A=$e;return A=y(xh(o,l,u)),y(A+y(J1(o,l,u)))}function K1(o){return o=o|0,n[o+24>>2]|0?o=0:y(XA(o))!=y(0)?o=1:o=y(kh(o))!=y(0),o|0}function XA(o){o=o|0;var l=$e;if(n[o+944>>2]|0){if(l=y(h[o+44>>2]),Ut(l)|0)return l=y(h[o+40>>2]),o=l>y(0)&((Ut(l)|0)^1),y(o?l:y(0))}else l=y(0);return y(l)}function kh(o){o=o|0;var l=$e,u=0,A=$e;do if(n[o+944>>2]|0){if(l=y(h[o+48>>2]),Ut(l)|0){if(u=s[(n[o+976>>2]|0)+2>>0]|0,!(u<<24>>24)&&(A=y(h[o+40>>2]),A>24?y(1):y(0)}}else l=y(0);while(!1);return y(l)}function Qy(o){o=o|0;var l=0,u=0;if(nE(o+400|0,0,540)|0,s[o+985>>0]=1,ee(o),u=Mi(o)|0,u|0){l=o+948|0,o=0;do Qy(n[(n[l>>2]|0)+(o<<2)>>2]|0),o=o+1|0;while((o|0)!=(u|0))}}function cP(o,l,u,A,g,m,B,k,R,M){o=o|0,l=l|0,u=y(u),A=A|0,g=y(g),m=y(m),B=y(B),k=k|0,R=R|0,M=M|0;var L=0,q=$e,oe=0,Ve=0,Le=$e,Te=$e,nt=0,Ze=$e,ft=0,He=$e,Ye=0,Mt=0,Gr=0,fr=0,$t=0,Tr=0,Hr=0,cr=0,jn=0,Mo=0;jn=I,I=I+16|0,Gr=jn+12|0,fr=jn+8|0,$t=jn+4|0,Tr=jn,cr=gr(n[o+4>>2]|0,R)|0,Ye=de(cr)|0,q=y(Zr(xL(l)|0,Ye?m:B)),Mt=uo(l,2,m)|0,Hr=uo(l,0,B)|0;do if(!(Ut(q)|0)&&!(Ut(Ye?u:g)|0)){if(L=l+504|0,!(Ut(y(h[L>>2]))|0)&&(!(X1(n[l+976>>2]|0,0)|0)||(n[l+500>>2]|0)==(n[2278]|0)))break;h[L>>2]=y(ri(q,y(tl(l,cr,m))))}else oe=7;while(!1);do if((oe|0)==7){if(ft=Ye^1,!(ft|Mt^1)){B=y(Zr(n[l+992>>2]|0,m)),h[l+504>>2]=y(ri(B,y(tl(l,2,m))));break}if(!(Ye|Hr^1)){B=y(Zr(n[l+996>>2]|0,B)),h[l+504>>2]=y(ri(B,y(tl(l,0,m))));break}h[Gr>>2]=y(ae),h[fr>>2]=y(ae),n[$t>>2]=0,n[Tr>>2]=0,Ze=y(En(l,2,m)),He=y(En(l,0,m)),Mt?(Le=y(Ze+y(Zr(n[l+992>>2]|0,m))),h[Gr>>2]=Le,n[$t>>2]=1,Ve=1):(Ve=0,Le=y(ae)),Hr?(q=y(He+y(Zr(n[l+996>>2]|0,B))),h[fr>>2]=q,n[Tr>>2]=1,L=1):(L=0,q=y(ae)),oe=n[o+32>>2]|0,Ye&(oe|0)==2?oe=2:Ut(Le)|0&&!(Ut(u)|0)&&(h[Gr>>2]=u,n[$t>>2]=2,Ve=2,Le=u),!((oe|0)==2&ft)&&Ut(q)|0&&!(Ut(g)|0)&&(h[fr>>2]=g,n[Tr>>2]=2,L=2,q=g),Te=y(h[l+396>>2]),nt=Ut(Te)|0;do if(nt)oe=Ve;else{if((Ve|0)==1&ft){h[fr>>2]=y(y(Le-Ze)/Te),n[Tr>>2]=1,L=1,oe=1;break}Ye&(L|0)==1?(h[Gr>>2]=y(Te*y(q-He)),n[$t>>2]=1,L=1,oe=1):oe=Ve}while(!1);Mo=Ut(u)|0,Ve=(as(o,l)|0)!=4,!(Ye|Mt|((A|0)!=1|Mo)|(Ve|(oe|0)==1))&&(h[Gr>>2]=u,n[$t>>2]=1,!nt)&&(h[fr>>2]=y(y(u-Ze)/Te),n[Tr>>2]=1,L=1),!(Hr|ft|((k|0)!=1|(Ut(g)|0))|(Ve|(L|0)==1))&&(h[fr>>2]=g,n[Tr>>2]=1,!nt)&&(h[Gr>>2]=y(Te*y(g-He)),n[$t>>2]=1),Eu(l,2,m,m,$t,Gr),Eu(l,0,B,m,Tr,fr),u=y(h[Gr>>2]),g=y(h[fr>>2]),Ol(l,u,g,R,n[$t>>2]|0,n[Tr>>2]|0,m,B,0,3565,M)|0,B=y(h[l+908+(n[976+(cr<<2)>>2]<<2)>>2]),h[l+504>>2]=y(ri(B,y(tl(l,cr,m))))}while(!1);n[l+500>>2]=n[2278],I=jn}function Wn(o,l,u,A,g){return o=o|0,l=l|0,u=y(u),A=y(A),g=y(g),A=y(Wd(o,l,u,A)),y(ri(A,y(tl(o,l,g))))}function as(o,l){return o=o|0,l=l|0,l=l+20|0,l=n[(n[l>>2]|0?l:o+16|0)>>2]|0,(l|0)==5&&Yd(n[o+4>>2]|0)|0&&(l=1),l|0}function Ll(o,l){return o=o|0,l=l|0,de(l)|0&&n[o+96>>2]|0?l=4:l=n[1040+(l<<2)>>2]|0,o+60+(l<<3)|0}function Ml(o,l){return o=o|0,l=l|0,de(l)|0&&n[o+104>>2]|0?l=5:l=n[1e3+(l<<2)>>2]|0,o+60+(l<<3)|0}function Eu(o,l,u,A,g,m){switch(o=o|0,l=l|0,u=y(u),A=y(A),g=g|0,m=m|0,u=y(Zr(o+380+(n[976+(l<<2)>>2]<<3)|0,u)),u=y(u+y(En(o,l,A))),n[g>>2]|0){case 2:case 1:{g=Ut(u)|0,A=y(h[m>>2]),h[m>>2]=g|A>2]=2,h[m>>2]=u);break}default:}}function Ea(o,l){return o=o|0,l=l|0,o=o+132|0,de(l)|0&&n[(Rn(o,4,948)|0)+4>>2]|0?o=1:o=(n[(Rn(o,n[1040+(l<<2)>>2]|0,948)|0)+4>>2]|0)!=0,o|0}function ZA(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,g=0;return o=o+132|0,de(l)|0&&(A=Rn(o,4,948)|0,(n[A+4>>2]|0)!=0)?g=4:(A=Rn(o,n[1040+(l<<2)>>2]|0,948)|0,n[A+4>>2]|0?g=4:u=y(0)),(g|0)==4&&(u=y(Zr(A,u))),y(u)}function $A(o,l,u){o=o|0,l=l|0,u=y(u);var A=$e;return A=y(h[o+908+(n[976+(l<<2)>>2]<<2)>>2]),A=y(A+y(K(o,l,u))),y(A+y(re(o,l,u)))}function DL(o){o=o|0;var l=0,u=0,A=0;e:do if(Yd(n[o+4>>2]|0)|0)l=0;else if((n[o+16>>2]|0)!=5)if(u=Mi(o)|0,!u)l=0;else for(l=0;;){if(A=Bs(o,l)|0,!(n[A+24>>2]|0)&&(n[A+20>>2]|0)==5){l=1;break e}if(l=l+1|0,l>>>0>=u>>>0){l=0;break}}else l=1;while(!1);return l|0}function bL(o,l){o=o|0,l=l|0;var u=$e;return u=y(h[o+908+(n[976+(l<<2)>>2]<<2)>>2]),u>=y(0)&((Ut(u)|0)^1)|0}function qd(o){o=o|0;var l=$e,u=0,A=0,g=0,m=0,B=0,k=0,R=$e;if(u=n[o+968>>2]|0,u)R=y(h[o+908>>2]),l=y(h[o+912>>2]),l=y(nZ[u&0](o,R,l)),Bi(o,(Ut(l)|0)^1,3573);else{m=Mi(o)|0;do if(m|0){for(u=0,g=0;;){if(A=Bs(o,g)|0,n[A+940>>2]|0){B=8;break}if((n[A+24>>2]|0)!=1)if(k=(as(o,A)|0)==5,k){u=A;break}else u=u|0?u:A;if(g=g+1|0,g>>>0>=m>>>0){B=8;break}}if((B|0)==8&&!u)break;return l=y(qd(u)),y(l+y(h[u+404>>2]))}while(!1);l=y(h[o+912>>2])}return y(l)}function Wd(o,l,u,A){o=o|0,l=l|0,u=y(u),A=y(A);var g=$e,m=0;return Yd(l)|0?(l=1,m=3):de(l)|0?(l=0,m=3):(A=y(ae),g=y(ae)),(m|0)==3&&(g=y(Zr(o+364+(l<<3)|0,A)),A=y(Zr(o+380+(l<<3)|0,A))),m=A=y(0)&((Ut(A)|0)^1)),u=m?A:u,m=g>=y(0)&((Ut(g)|0)^1)&u>2]|0,m)|0,Le=ky(nt,m)|0,Te=de(nt)|0,q=y(En(l,2,u)),oe=y(En(l,0,u)),uo(l,2,u)|0?k=y(q+y(Zr(n[l+992>>2]|0,u))):Ea(l,2)|0&&Ry(l,2)|0?(k=y(h[o+908>>2]),R=y(vr(o,2)),R=y(k-y(R+y(_n(o,2)))),k=y(ZA(l,2,u)),k=y(Wn(l,2,y(R-y(k+y(Qh(l,2,u)))),u,u))):k=y(ae),uo(l,0,g)|0?R=y(oe+y(Zr(n[l+996>>2]|0,g))):Ea(l,0)|0&&Ry(l,0)|0?(R=y(h[o+912>>2]),ft=y(vr(o,0)),ft=y(R-y(ft+y(_n(o,0)))),R=y(ZA(l,0,g)),R=y(Wn(l,0,y(ft-y(R+y(Qh(l,0,g)))),g,u))):R=y(ae),M=Ut(k)|0,L=Ut(R)|0;do if(M^L&&(Ve=y(h[l+396>>2]),!(Ut(Ve)|0)))if(M){k=y(q+y(y(R-oe)*Ve));break}else{ft=y(oe+y(y(k-q)/Ve)),R=L?ft:R;break}while(!1);L=Ut(k)|0,M=Ut(R)|0,L|M&&(He=(L^1)&1,A=u>y(0)&((A|0)!=0&L),k=Te?k:A?u:k,Ol(l,k,R,m,Te?He:A?2:He,L&(M^1)&1,k,R,0,3623,B)|0,k=y(h[l+908>>2]),k=y(k+y(En(l,2,u))),R=y(h[l+912>>2]),R=y(R+y(En(l,0,u)))),Ol(l,k,R,m,1,1,k,R,1,3635,B)|0,Ry(l,nt)|0&&!(Ea(l,nt)|0)?(He=n[976+(nt<<2)>>2]|0,ft=y(h[o+908+(He<<2)>>2]),ft=y(ft-y(h[l+908+(He<<2)>>2])),ft=y(ft-y(_n(o,nt))),ft=y(ft-y(re(l,nt,u))),ft=y(ft-y(Qh(l,nt,Te?u:g))),h[l+400+(n[1040+(nt<<2)>>2]<<2)>>2]=ft):Ze=21;do if((Ze|0)==21){if(!(Ea(l,nt)|0)&&(n[o+8>>2]|0)==1){He=n[976+(nt<<2)>>2]|0,ft=y(h[o+908+(He<<2)>>2]),ft=y(y(ft-y(h[l+908+(He<<2)>>2]))*y(.5)),h[l+400+(n[1040+(nt<<2)>>2]<<2)>>2]=ft;break}!(Ea(l,nt)|0)&&(n[o+8>>2]|0)==2&&(He=n[976+(nt<<2)>>2]|0,ft=y(h[o+908+(He<<2)>>2]),ft=y(ft-y(h[l+908+(He<<2)>>2])),h[l+400+(n[1040+(nt<<2)>>2]<<2)>>2]=ft)}while(!1);Ry(l,Le)|0&&!(Ea(l,Le)|0)?(He=n[976+(Le<<2)>>2]|0,ft=y(h[o+908+(He<<2)>>2]),ft=y(ft-y(h[l+908+(He<<2)>>2])),ft=y(ft-y(_n(o,Le))),ft=y(ft-y(re(l,Le,u))),ft=y(ft-y(Qh(l,Le,Te?g:u))),h[l+400+(n[1040+(Le<<2)>>2]<<2)>>2]=ft):Ze=30;do if((Ze|0)==30&&!(Ea(l,Le)|0)){if((as(o,l)|0)==2){He=n[976+(Le<<2)>>2]|0,ft=y(h[o+908+(He<<2)>>2]),ft=y(y(ft-y(h[l+908+(He<<2)>>2]))*y(.5)),h[l+400+(n[1040+(Le<<2)>>2]<<2)>>2]=ft;break}He=(as(o,l)|0)==3,He^(n[o+28>>2]|0)==2&&(He=n[976+(Le<<2)>>2]|0,ft=y(h[o+908+(He<<2)>>2]),ft=y(ft-y(h[l+908+(He<<2)>>2])),h[l+400+(n[1040+(Le<<2)>>2]<<2)>>2]=ft)}while(!1)}function z1(o,l,u){o=o|0,l=l|0,u=u|0;var A=$e,g=0;g=n[976+(u<<2)>>2]|0,A=y(h[l+908+(g<<2)>>2]),A=y(y(h[o+908+(g<<2)>>2])-A),A=y(A-y(h[l+400+(n[1040+(u<<2)>>2]<<2)>>2])),h[l+400+(n[1e3+(u<<2)>>2]<<2)>>2]=A}function Yd(o){return o=o|0,(o|1|0)==1|0}function xL(o){o=o|0;var l=$e;switch(n[o+56>>2]|0){case 0:case 3:{l=y(h[o+40>>2]),l>y(0)&((Ut(l)|0)^1)?o=s[(n[o+976>>2]|0)+2>>0]|0?1056:992:o=1056;break}default:o=o+52|0}return o|0}function X1(o,l){return o=o|0,l=l|0,(s[o+l>>0]|0)!=0|0}function Ry(o,l){return o=o|0,l=l|0,o=o+132|0,de(l)|0&&n[(Rn(o,5,948)|0)+4>>2]|0?o=1:o=(n[(Rn(o,n[1e3+(l<<2)>>2]|0,948)|0)+4>>2]|0)!=0,o|0}function Qh(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,g=0;return o=o+132|0,de(l)|0&&(A=Rn(o,5,948)|0,(n[A+4>>2]|0)!=0)?g=4:(A=Rn(o,n[1e3+(l<<2)>>2]|0,948)|0,n[A+4>>2]|0?g=4:u=y(0)),(g|0)==4&&(u=y(Zr(A,u))),y(u)}function uP(o,l,u){return o=o|0,l=l|0,u=y(u),Ea(o,l)|0?u=y(ZA(o,l,u)):u=y(-y(Qh(o,l,u))),y(u)}function fP(o){return o=y(o),h[S>>2]=o,n[S>>2]|0|0}function Ty(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>1073741823)Nt();else{g=Kt(l<<2)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<2)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<2)}function AP(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>2)<<2)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function Fy(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-4-l|0)>>>2)<<2)),o=n[o>>2]|0,o|0&&It(o)}function pP(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0;if(B=o+4|0,k=n[B>>2]|0,g=k-A|0,m=g>>2,o=l+(m<<2)|0,o>>>0>>0){A=k;do n[A>>2]=n[o>>2],o=o+4|0,A=(n[B>>2]|0)+4|0,n[B>>2]=A;while(o>>>0>>0)}m|0&&B2(k+(0-m<<2)|0,l|0,g|0)|0}function hP(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0;return k=l+4|0,R=n[k>>2]|0,g=n[o>>2]|0,B=u,m=B-g|0,A=R+(0-(m>>2)<<2)|0,n[k>>2]=A,(m|0)>0&&Rr(A|0,g|0,m|0)|0,g=o+4|0,m=l+8|0,A=(n[g>>2]|0)-B|0,(A|0)>0&&(Rr(n[m>>2]|0,u|0,A|0)|0,n[m>>2]=(n[m>>2]|0)+(A>>>2<<2)),B=n[o>>2]|0,n[o>>2]=n[k>>2],n[k>>2]=B,B=n[g>>2]|0,n[g>>2]=n[m>>2],n[m>>2]=B,B=o+8|0,u=l+12|0,o=n[B>>2]|0,n[B>>2]=n[u>>2],n[u>>2]=o,n[l>>2]=n[k>>2],R|0}function Z1(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;if(B=n[l>>2]|0,m=n[u>>2]|0,(B|0)!=(m|0)){g=o+8|0,u=((m+-4-B|0)>>>2)+1|0,o=B,A=n[g>>2]|0;do n[A>>2]=n[o>>2],A=(n[g>>2]|0)+4|0,n[g>>2]=A,o=o+4|0;while((o|0)!=(m|0));n[l>>2]=B+(u<<2)}}function $1(){da()}function dP(){var o=0;return o=Kt(4)|0,e2(o),o|0}function e2(o){o=o|0,n[o>>2]=hc()|0}function gP(o){o=o|0,o|0&&(Vd(o),It(o))}function Vd(o){o=o|0,ot(n[o>>2]|0)}function kL(o,l,u){o=o|0,l=l|0,u=u|0,dc(n[o>>2]|0,l,u)}function Ny(o,l){o=o|0,l=y(l),Sh(n[o>>2]|0,l)}function Oy(o,l){return o=o|0,l=l|0,X1(n[o>>2]|0,l)|0}function Ly(){var o=0;return o=Kt(8)|0,Jd(o,0),o|0}function Jd(o,l){o=o|0,l=l|0,l?l=ga(n[l>>2]|0)|0:l=is()|0,n[o>>2]=l,n[o+4>>2]=0,Fn(l,o)}function My(o){o=o|0;var l=0;return l=Kt(8)|0,Jd(l,o),l|0}function Kd(o){o=o|0,o|0&&(Uy(o),It(o))}function Uy(o){o=o|0;var l=0;Ac(n[o>>2]|0),l=o+4|0,o=n[l>>2]|0,n[l>>2]=0,o|0&&(Pf(o),It(o))}function Pf(o){o=o|0,xf(o)}function xf(o){o=o|0,o=n[o>>2]|0,o|0&&qa(o|0)}function t2(o){return o=o|0,Xa(o)|0}function r2(o){o=o|0;var l=0,u=0;u=o+4|0,l=n[u>>2]|0,n[u>>2]=0,l|0&&(Pf(l),It(l)),pc(n[o>>2]|0)}function _y(o,l){o=o|0,l=l|0,pn(n[o>>2]|0,n[l>>2]|0)}function QL(o,l){o=o|0,l=l|0,Ch(n[o>>2]|0,l)}function RL(o,l,u){o=o|0,l=l|0,u=+u,vy(n[o>>2]|0,l,y(u))}function Hy(o,l,u){o=o|0,l=l|0,u=+u,Sy(n[o>>2]|0,l,y(u))}function n2(o,l){o=o|0,l=l|0,yh(n[o>>2]|0,l)}function i2(o,l){o=o|0,l=l|0,Qo(n[o>>2]|0,l)}function xr(o,l){o=o|0,l=l|0,Ih(n[o>>2]|0,l)}function fo(o,l){o=o|0,l=l|0,Iy(n[o>>2]|0,l)}function Xi(o,l){o=o|0,l=l|0,Td(n[o>>2]|0,l)}function Ms(o,l){o=o|0,l=l|0,ko(n[o>>2]|0,l)}function ep(o,l,u){o=o|0,l=l|0,u=+u,GA(n[o>>2]|0,l,y(u))}function s2(o,l,u){o=o|0,l=l|0,u=+u,W(n[o>>2]|0,l,y(u))}function Ss(o,l){o=o|0,l=l|0,qA(n[o>>2]|0,l)}function jy(o,l){o=o|0,l=l|0,wy(n[o>>2]|0,l)}function Rh(o,l){o=o|0,l=l|0,Ro(n[o>>2]|0,l)}function zd(o,l){o=o|0,l=+l,wh(n[o>>2]|0,y(l))}function Th(o,l){o=o|0,l=+l,Fl(n[o>>2]|0,y(l))}function o2(o,l){o=o|0,l=+l,By(n[o>>2]|0,y(l))}function a2(o,l){o=o|0,l=+l,Nd(n[o>>2]|0,y(l))}function l2(o,l){o=o|0,l=+l,Tl(n[o>>2]|0,y(l))}function c2(o,l){o=o|0,l=+l,Od(n[o>>2]|0,y(l))}function kf(o,l){o=o|0,l=+l,Y1(n[o>>2]|0,y(l))}function sr(o){o=o|0,Bh(n[o>>2]|0)}function Gy(o,l){o=o|0,l=+l,zi(n[o>>2]|0,y(l))}function u2(o,l){o=o|0,l=+l,Cf(n[o>>2]|0,y(l))}function gc(o){o=o|0,Za(n[o>>2]|0)}function Qf(o,l){o=o|0,l=+l,hu(n[o>>2]|0,y(l))}function Xd(o,l){o=o|0,l=+l,wf(n[o>>2]|0,y(l))}function Zd(o,l){o=o|0,l=+l,mi(n[o>>2]|0,y(l))}function f2(o,l){o=o|0,l=+l,WA(n[o>>2]|0,y(l))}function A2(o,l){o=o|0,l=+l,ma(n[o>>2]|0,y(l))}function Iu(o,l){o=o|0,l=+l,el(n[o>>2]|0,y(l))}function $d(o,l){o=o|0,l=+l,vh(n[o>>2]|0,y(l))}function p2(o,l){o=o|0,l=+l,Ud(n[o>>2]|0,y(l))}function qy(o,l){o=o|0,l=+l,YA(n[o>>2]|0,y(l))}function Cu(o,l,u){o=o|0,l=l|0,u=+u,pu(n[o>>2]|0,l,y(u))}function Wy(o,l,u){o=o|0,l=l|0,u=+u,To(n[o>>2]|0,l,y(u))}function eg(o,l,u){o=o|0,l=l|0,u=+u,If(n[o>>2]|0,l,y(u))}function tg(o){return o=o|0,Rd(n[o>>2]|0)|0}function Oo(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0;A=I,I=I+16|0,g=A,jA(g,n[l>>2]|0,u),Ds(o,g),I=A}function Ds(o,l){o=o|0,l=l|0,Ul(o,n[l+4>>2]|0,+y(h[l>>2]))}function Ul(o,l,u){o=o|0,l=l|0,u=+u,n[o>>2]=l,E[o+8>>3]=u}function Yy(o){return o=o|0,W1(n[o>>2]|0)|0}function Ia(o){return o=o|0,Eh(n[o>>2]|0)|0}function mP(o){return o=o|0,Au(n[o>>2]|0)|0}function Fh(o){return o=o|0,q1(n[o>>2]|0)|0}function h2(o){return o=o|0,Fd(n[o>>2]|0)|0}function TL(o){return o=o|0,Cy(n[o>>2]|0)|0}function yP(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0;A=I,I=I+16|0,g=A,xt(g,n[l>>2]|0,u),Ds(o,g),I=A}function EP(o){return o=o|0,Ef(n[o>>2]|0)|0}function Vy(o){return o=o|0,Rl(n[o>>2]|0)|0}function d2(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,HA(A,n[l>>2]|0),Ds(o,A),I=u}function Nh(o){return o=o|0,+ +y(ci(n[o>>2]|0))}function IP(o){return o=o|0,+ +y(qi(n[o>>2]|0))}function CP(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,ur(A,n[l>>2]|0),Ds(o,A),I=u}function rg(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,Ld(A,n[l>>2]|0),Ds(o,A),I=u}function FL(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,wt(A,n[l>>2]|0),Ds(o,A),I=u}function NL(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,$a(A,n[l>>2]|0),Ds(o,A),I=u}function wP(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,Md(A,n[l>>2]|0),Ds(o,A),I=u}function BP(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,by(A,n[l>>2]|0),Ds(o,A),I=u}function tp(o){return o=o|0,+ +y(_d(n[o>>2]|0))}function OL(o,l){return o=o|0,l=l|0,+ +y(Dy(n[o>>2]|0,l))}function LL(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0;A=I,I=I+16|0,g=A,yt(g,n[l>>2]|0,u),Ds(o,g),I=A}function wu(o,l,u){o=o|0,l=l|0,u=u|0,lr(n[o>>2]|0,n[l>>2]|0,u)}function ML(o,l){o=o|0,l=l|0,yf(n[o>>2]|0,n[l>>2]|0)}function vP(o){return o=o|0,Mi(n[o>>2]|0)|0}function UL(o){return o=o|0,o=Et(n[o>>2]|0)|0,o?o=t2(o)|0:o=0,o|0}function SP(o,l){return o=o|0,l=l|0,o=Bs(n[o>>2]|0,l)|0,o?o=t2(o)|0:o=0,o|0}function Rf(o,l){o=o|0,l=l|0;var u=0,A=0;A=Kt(4)|0,DP(A,l),u=o+4|0,l=n[u>>2]|0,n[u>>2]=A,l|0&&(Pf(l),It(l)),St(n[o>>2]|0,1)}function DP(o,l){o=o|0,l=l|0,qL(o,l)}function _L(o,l,u,A,g,m){o=o|0,l=l|0,u=y(u),A=A|0,g=y(g),m=m|0;var B=0,k=0;B=I,I=I+16|0,k=B,bP(k,Xa(l)|0,+u,A,+g,m),h[o>>2]=y(+E[k>>3]),h[o+4>>2]=y(+E[k+8>>3]),I=B}function bP(o,l,u,A,g,m){o=o|0,l=l|0,u=+u,A=A|0,g=+g,m=m|0;var B=0,k=0,R=0,M=0,L=0;B=I,I=I+32|0,L=B+8|0,M=B+20|0,R=B,k=B+16|0,E[L>>3]=u,n[M>>2]=A,E[R>>3]=g,n[k>>2]=m,Jy(o,n[l+4>>2]|0,L,M,R,k),I=B}function Jy(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0;var B=0,k=0;B=I,I=I+16|0,k=B,Hl(k),l=Us(l)|0,PP(o,l,+E[u>>3],n[A>>2]|0,+E[g>>3],n[m>>2]|0),jl(k),I=B}function Us(o){return o=o|0,n[o>>2]|0}function PP(o,l,u,A,g,m){o=o|0,l=l|0,u=+u,A=A|0,g=+g,m=m|0;var B=0;B=Ca(g2()|0)|0,u=+rl(u),A=Ky(A)|0,g=+rl(g),HL(o,Xn(0,B|0,l|0,+u,A|0,+g,Ky(m)|0)|0)}function g2(){var o=0;return s[7608]|0||(y2(9120),o=7608,n[o>>2]=1,n[o+4>>2]=0),9120}function Ca(o){return o=o|0,n[o+8>>2]|0}function rl(o){return o=+o,+ +Tf(o)}function Ky(o){return o=o|0,ng(o)|0}function HL(o,l){o=o|0,l=l|0;var u=0,A=0,g=0;g=I,I=I+32|0,u=g,A=l,A&1?(nl(u,0),Ga(A|0,u|0)|0,m2(o,u),jL(u)):(n[o>>2]=n[l>>2],n[o+4>>2]=n[l+4>>2],n[o+8>>2]=n[l+8>>2],n[o+12>>2]=n[l+12>>2]),I=g}function nl(o,l){o=o|0,l=l|0,Bu(o,l),n[o+8>>2]=0,s[o+24>>0]=0}function m2(o,l){o=o|0,l=l|0,l=l+8|0,n[o>>2]=n[l>>2],n[o+4>>2]=n[l+4>>2],n[o+8>>2]=n[l+8>>2],n[o+12>>2]=n[l+12>>2]}function jL(o){o=o|0,s[o+24>>0]=0}function Bu(o,l){o=o|0,l=l|0,n[o>>2]=l}function ng(o){return o=o|0,o|0}function Tf(o){return o=+o,+o}function y2(o){o=o|0,Lo(o,E2()|0,4)}function E2(){return 1064}function Lo(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u,n[o+8>>2]=Gi(l|0,u+1|0)|0}function qL(o,l){o=o|0,l=l|0,l=n[l>>2]|0,n[o>>2]=l,ou(l|0)}function xP(o){o=o|0;var l=0,u=0;u=o+4|0,l=n[u>>2]|0,n[u>>2]=0,l|0&&(Pf(l),It(l)),St(n[o>>2]|0,0)}function kP(o){o=o|0,bt(n[o>>2]|0)}function zy(o){return o=o|0,tr(n[o>>2]|0)|0}function WL(o,l,u,A){o=o|0,l=+l,u=+u,A=A|0,JA(n[o>>2]|0,y(l),y(u),A)}function YL(o){return o=o|0,+ +y(du(n[o>>2]|0))}function v(o){return o=o|0,+ +y(Bf(n[o>>2]|0))}function D(o){return o=o|0,+ +y(gu(n[o>>2]|0))}function Q(o){return o=o|0,+ +y(Os(n[o>>2]|0))}function H(o){return o=o|0,+ +y(mu(n[o>>2]|0))}function Y(o){return o=o|0,+ +y(qn(n[o>>2]|0))}function ne(o,l){o=o|0,l=l|0,E[o>>3]=+y(du(n[l>>2]|0)),E[o+8>>3]=+y(Bf(n[l>>2]|0)),E[o+16>>3]=+y(gu(n[l>>2]|0)),E[o+24>>3]=+y(Os(n[l>>2]|0)),E[o+32>>3]=+y(mu(n[l>>2]|0)),E[o+40>>3]=+y(qn(n[l>>2]|0))}function ve(o,l){return o=o|0,l=l|0,+ +y(ss(n[o>>2]|0,l))}function _e(o,l){return o=o|0,l=l|0,+ +y(Pi(n[o>>2]|0,l))}function ht(o,l){return o=o|0,l=l|0,+ +y(VA(n[o>>2]|0,l))}function Wt(){return Tn()|0}function Sr(){Lr(),Zt(),Zn(),Ei(),il(),rt()}function Lr(){eUe(11713,4938,1)}function Zt(){EMe(10448)}function Zn(){eMe(10408)}function Ei(){BLe(10324)}function il(){QNe(10096)}function rt(){We(9132)}function We(o){o=o|0;var l=0,u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0,Le=0,Te=0,nt=0,Ze=0,ft=0,He=0,Ye=0,Mt=0,Gr=0,fr=0,$t=0,Tr=0,Hr=0,cr=0,jn=0,Mo=0,Uo=0,_o=0,ol=0,qh=0,Wh=0,mc=0,Yh=0,Of=0,Lf=0,Vh=0,Jh=0,Kh=0,ln=0,yc=0,zh=0,Du=0,Xh=0,Zh=0,Mf=0,Uf=0,bu=0,Ao=0,ql=0,wa=0,Ec=0,lp=0,cp=0,_f=0,up=0,fp=0,po=0,Hs=0,Ic=0,Yn=0,Ap=0,Ho=0,Pu=0,jo=0,xu=0,pp=0,hp=0,ku=0,ho=0,Cc=0,dp=0,gp=0,mp=0,Nr=0,fi=0,js=0,Go=0,go=0,Mr=0,Ar=0,wc=0;l=I,I=I+672|0,u=l+656|0,wc=l+648|0,Ar=l+640|0,Mr=l+632|0,go=l+624|0,Go=l+616|0,js=l+608|0,fi=l+600|0,Nr=l+592|0,mp=l+584|0,gp=l+576|0,dp=l+568|0,Cc=l+560|0,ho=l+552|0,ku=l+544|0,hp=l+536|0,pp=l+528|0,xu=l+520|0,jo=l+512|0,Pu=l+504|0,Ho=l+496|0,Ap=l+488|0,Yn=l+480|0,Ic=l+472|0,Hs=l+464|0,po=l+456|0,fp=l+448|0,up=l+440|0,_f=l+432|0,cp=l+424|0,lp=l+416|0,Ec=l+408|0,wa=l+400|0,ql=l+392|0,Ao=l+384|0,bu=l+376|0,Uf=l+368|0,Mf=l+360|0,Zh=l+352|0,Xh=l+344|0,Du=l+336|0,zh=l+328|0,yc=l+320|0,ln=l+312|0,Kh=l+304|0,Jh=l+296|0,Vh=l+288|0,Lf=l+280|0,Of=l+272|0,Yh=l+264|0,mc=l+256|0,Wh=l+248|0,qh=l+240|0,ol=l+232|0,_o=l+224|0,Uo=l+216|0,Mo=l+208|0,jn=l+200|0,cr=l+192|0,Hr=l+184|0,Tr=l+176|0,$t=l+168|0,fr=l+160|0,Gr=l+152|0,Mt=l+144|0,Ye=l+136|0,He=l+128|0,ft=l+120|0,Ze=l+112|0,nt=l+104|0,Te=l+96|0,Le=l+88|0,Ve=l+80|0,oe=l+72|0,q=l+64|0,L=l+56|0,M=l+48|0,R=l+40|0,k=l+32|0,B=l+24|0,m=l+16|0,g=l+8|0,A=l,gt(o,3646),Xt(o,3651,2)|0,Dr(o,3665,2)|0,ti(o,3682,18)|0,n[wc>>2]=19,n[wc+4>>2]=0,n[u>>2]=n[wc>>2],n[u+4>>2]=n[wc+4>>2],Qr(o,3690,u)|0,n[Ar>>2]=1,n[Ar+4>>2]=0,n[u>>2]=n[Ar>>2],n[u+4>>2]=n[Ar+4>>2],Nn(o,3696,u)|0,n[Mr>>2]=2,n[Mr+4>>2]=0,n[u>>2]=n[Mr>>2],n[u+4>>2]=n[Mr+4>>2],Hn(o,3706,u)|0,n[go>>2]=1,n[go+4>>2]=0,n[u>>2]=n[go>>2],n[u+4>>2]=n[go+4>>2],zr(o,3722,u)|0,n[Go>>2]=2,n[Go+4>>2]=0,n[u>>2]=n[Go>>2],n[u+4>>2]=n[Go+4>>2],zr(o,3734,u)|0,n[js>>2]=3,n[js+4>>2]=0,n[u>>2]=n[js>>2],n[u+4>>2]=n[js+4>>2],Hn(o,3753,u)|0,n[fi>>2]=4,n[fi+4>>2]=0,n[u>>2]=n[fi>>2],n[u+4>>2]=n[fi+4>>2],Hn(o,3769,u)|0,n[Nr>>2]=5,n[Nr+4>>2]=0,n[u>>2]=n[Nr>>2],n[u+4>>2]=n[Nr+4>>2],Hn(o,3783,u)|0,n[mp>>2]=6,n[mp+4>>2]=0,n[u>>2]=n[mp>>2],n[u+4>>2]=n[mp+4>>2],Hn(o,3796,u)|0,n[gp>>2]=7,n[gp+4>>2]=0,n[u>>2]=n[gp>>2],n[u+4>>2]=n[gp+4>>2],Hn(o,3813,u)|0,n[dp>>2]=8,n[dp+4>>2]=0,n[u>>2]=n[dp>>2],n[u+4>>2]=n[dp+4>>2],Hn(o,3825,u)|0,n[Cc>>2]=3,n[Cc+4>>2]=0,n[u>>2]=n[Cc>>2],n[u+4>>2]=n[Cc+4>>2],zr(o,3843,u)|0,n[ho>>2]=4,n[ho+4>>2]=0,n[u>>2]=n[ho>>2],n[u+4>>2]=n[ho+4>>2],zr(o,3853,u)|0,n[ku>>2]=9,n[ku+4>>2]=0,n[u>>2]=n[ku>>2],n[u+4>>2]=n[ku+4>>2],Hn(o,3870,u)|0,n[hp>>2]=10,n[hp+4>>2]=0,n[u>>2]=n[hp>>2],n[u+4>>2]=n[hp+4>>2],Hn(o,3884,u)|0,n[pp>>2]=11,n[pp+4>>2]=0,n[u>>2]=n[pp>>2],n[u+4>>2]=n[pp+4>>2],Hn(o,3896,u)|0,n[xu>>2]=1,n[xu+4>>2]=0,n[u>>2]=n[xu>>2],n[u+4>>2]=n[xu+4>>2],ui(o,3907,u)|0,n[jo>>2]=2,n[jo+4>>2]=0,n[u>>2]=n[jo>>2],n[u+4>>2]=n[jo+4>>2],ui(o,3915,u)|0,n[Pu>>2]=3,n[Pu+4>>2]=0,n[u>>2]=n[Pu>>2],n[u+4>>2]=n[Pu+4>>2],ui(o,3928,u)|0,n[Ho>>2]=4,n[Ho+4>>2]=0,n[u>>2]=n[Ho>>2],n[u+4>>2]=n[Ho+4>>2],ui(o,3948,u)|0,n[Ap>>2]=5,n[Ap+4>>2]=0,n[u>>2]=n[Ap>>2],n[u+4>>2]=n[Ap+4>>2],ui(o,3960,u)|0,n[Yn>>2]=6,n[Yn+4>>2]=0,n[u>>2]=n[Yn>>2],n[u+4>>2]=n[Yn+4>>2],ui(o,3974,u)|0,n[Ic>>2]=7,n[Ic+4>>2]=0,n[u>>2]=n[Ic>>2],n[u+4>>2]=n[Ic+4>>2],ui(o,3983,u)|0,n[Hs>>2]=20,n[Hs+4>>2]=0,n[u>>2]=n[Hs>>2],n[u+4>>2]=n[Hs+4>>2],Qr(o,3999,u)|0,n[po>>2]=8,n[po+4>>2]=0,n[u>>2]=n[po>>2],n[u+4>>2]=n[po+4>>2],ui(o,4012,u)|0,n[fp>>2]=9,n[fp+4>>2]=0,n[u>>2]=n[fp>>2],n[u+4>>2]=n[fp+4>>2],ui(o,4022,u)|0,n[up>>2]=21,n[up+4>>2]=0,n[u>>2]=n[up>>2],n[u+4>>2]=n[up+4>>2],Qr(o,4039,u)|0,n[_f>>2]=10,n[_f+4>>2]=0,n[u>>2]=n[_f>>2],n[u+4>>2]=n[_f+4>>2],ui(o,4053,u)|0,n[cp>>2]=11,n[cp+4>>2]=0,n[u>>2]=n[cp>>2],n[u+4>>2]=n[cp+4>>2],ui(o,4065,u)|0,n[lp>>2]=12,n[lp+4>>2]=0,n[u>>2]=n[lp>>2],n[u+4>>2]=n[lp+4>>2],ui(o,4084,u)|0,n[Ec>>2]=13,n[Ec+4>>2]=0,n[u>>2]=n[Ec>>2],n[u+4>>2]=n[Ec+4>>2],ui(o,4097,u)|0,n[wa>>2]=14,n[wa+4>>2]=0,n[u>>2]=n[wa>>2],n[u+4>>2]=n[wa+4>>2],ui(o,4117,u)|0,n[ql>>2]=15,n[ql+4>>2]=0,n[u>>2]=n[ql>>2],n[u+4>>2]=n[ql+4>>2],ui(o,4129,u)|0,n[Ao>>2]=16,n[Ao+4>>2]=0,n[u>>2]=n[Ao>>2],n[u+4>>2]=n[Ao+4>>2],ui(o,4148,u)|0,n[bu>>2]=17,n[bu+4>>2]=0,n[u>>2]=n[bu>>2],n[u+4>>2]=n[bu+4>>2],ui(o,4161,u)|0,n[Uf>>2]=18,n[Uf+4>>2]=0,n[u>>2]=n[Uf>>2],n[u+4>>2]=n[Uf+4>>2],ui(o,4181,u)|0,n[Mf>>2]=5,n[Mf+4>>2]=0,n[u>>2]=n[Mf>>2],n[u+4>>2]=n[Mf+4>>2],zr(o,4196,u)|0,n[Zh>>2]=6,n[Zh+4>>2]=0,n[u>>2]=n[Zh>>2],n[u+4>>2]=n[Zh+4>>2],zr(o,4206,u)|0,n[Xh>>2]=7,n[Xh+4>>2]=0,n[u>>2]=n[Xh>>2],n[u+4>>2]=n[Xh+4>>2],zr(o,4217,u)|0,n[Du>>2]=3,n[Du+4>>2]=0,n[u>>2]=n[Du>>2],n[u+4>>2]=n[Du+4>>2],vu(o,4235,u)|0,n[zh>>2]=1,n[zh+4>>2]=0,n[u>>2]=n[zh>>2],n[u+4>>2]=n[zh+4>>2],VL(o,4251,u)|0,n[yc>>2]=4,n[yc+4>>2]=0,n[u>>2]=n[yc>>2],n[u+4>>2]=n[yc+4>>2],vu(o,4263,u)|0,n[ln>>2]=5,n[ln+4>>2]=0,n[u>>2]=n[ln>>2],n[u+4>>2]=n[ln+4>>2],vu(o,4279,u)|0,n[Kh>>2]=6,n[Kh+4>>2]=0,n[u>>2]=n[Kh>>2],n[u+4>>2]=n[Kh+4>>2],vu(o,4293,u)|0,n[Jh>>2]=7,n[Jh+4>>2]=0,n[u>>2]=n[Jh>>2],n[u+4>>2]=n[Jh+4>>2],vu(o,4306,u)|0,n[Vh>>2]=8,n[Vh+4>>2]=0,n[u>>2]=n[Vh>>2],n[u+4>>2]=n[Vh+4>>2],vu(o,4323,u)|0,n[Lf>>2]=9,n[Lf+4>>2]=0,n[u>>2]=n[Lf>>2],n[u+4>>2]=n[Lf+4>>2],vu(o,4335,u)|0,n[Of>>2]=2,n[Of+4>>2]=0,n[u>>2]=n[Of>>2],n[u+4>>2]=n[Of+4>>2],VL(o,4353,u)|0,n[Yh>>2]=12,n[Yh+4>>2]=0,n[u>>2]=n[Yh>>2],n[u+4>>2]=n[Yh+4>>2],ig(o,4363,u)|0,n[mc>>2]=1,n[mc+4>>2]=0,n[u>>2]=n[mc>>2],n[u+4>>2]=n[mc+4>>2],rp(o,4376,u)|0,n[Wh>>2]=2,n[Wh+4>>2]=0,n[u>>2]=n[Wh>>2],n[u+4>>2]=n[Wh+4>>2],rp(o,4388,u)|0,n[qh>>2]=13,n[qh+4>>2]=0,n[u>>2]=n[qh>>2],n[u+4>>2]=n[qh+4>>2],ig(o,4402,u)|0,n[ol>>2]=14,n[ol+4>>2]=0,n[u>>2]=n[ol>>2],n[u+4>>2]=n[ol+4>>2],ig(o,4411,u)|0,n[_o>>2]=15,n[_o+4>>2]=0,n[u>>2]=n[_o>>2],n[u+4>>2]=n[_o+4>>2],ig(o,4421,u)|0,n[Uo>>2]=16,n[Uo+4>>2]=0,n[u>>2]=n[Uo>>2],n[u+4>>2]=n[Uo+4>>2],ig(o,4433,u)|0,n[Mo>>2]=17,n[Mo+4>>2]=0,n[u>>2]=n[Mo>>2],n[u+4>>2]=n[Mo+4>>2],ig(o,4446,u)|0,n[jn>>2]=18,n[jn+4>>2]=0,n[u>>2]=n[jn>>2],n[u+4>>2]=n[jn+4>>2],ig(o,4458,u)|0,n[cr>>2]=3,n[cr+4>>2]=0,n[u>>2]=n[cr>>2],n[u+4>>2]=n[cr+4>>2],rp(o,4471,u)|0,n[Hr>>2]=1,n[Hr+4>>2]=0,n[u>>2]=n[Hr>>2],n[u+4>>2]=n[Hr+4>>2],QP(o,4486,u)|0,n[Tr>>2]=10,n[Tr+4>>2]=0,n[u>>2]=n[Tr>>2],n[u+4>>2]=n[Tr+4>>2],vu(o,4496,u)|0,n[$t>>2]=11,n[$t+4>>2]=0,n[u>>2]=n[$t>>2],n[u+4>>2]=n[$t+4>>2],vu(o,4508,u)|0,n[fr>>2]=3,n[fr+4>>2]=0,n[u>>2]=n[fr>>2],n[u+4>>2]=n[fr+4>>2],VL(o,4519,u)|0,n[Gr>>2]=4,n[Gr+4>>2]=0,n[u>>2]=n[Gr>>2],n[u+4>>2]=n[Gr+4>>2],lPe(o,4530,u)|0,n[Mt>>2]=19,n[Mt+4>>2]=0,n[u>>2]=n[Mt>>2],n[u+4>>2]=n[Mt+4>>2],cPe(o,4542,u)|0,n[Ye>>2]=12,n[Ye+4>>2]=0,n[u>>2]=n[Ye>>2],n[u+4>>2]=n[Ye+4>>2],uPe(o,4554,u)|0,n[He>>2]=13,n[He+4>>2]=0,n[u>>2]=n[He>>2],n[u+4>>2]=n[He+4>>2],fPe(o,4568,u)|0,n[ft>>2]=2,n[ft+4>>2]=0,n[u>>2]=n[ft>>2],n[u+4>>2]=n[ft+4>>2],APe(o,4578,u)|0,n[Ze>>2]=20,n[Ze+4>>2]=0,n[u>>2]=n[Ze>>2],n[u+4>>2]=n[Ze+4>>2],pPe(o,4587,u)|0,n[nt>>2]=22,n[nt+4>>2]=0,n[u>>2]=n[nt>>2],n[u+4>>2]=n[nt+4>>2],Qr(o,4602,u)|0,n[Te>>2]=23,n[Te+4>>2]=0,n[u>>2]=n[Te>>2],n[u+4>>2]=n[Te+4>>2],Qr(o,4619,u)|0,n[Le>>2]=14,n[Le+4>>2]=0,n[u>>2]=n[Le>>2],n[u+4>>2]=n[Le+4>>2],hPe(o,4629,u)|0,n[Ve>>2]=1,n[Ve+4>>2]=0,n[u>>2]=n[Ve>>2],n[u+4>>2]=n[Ve+4>>2],dPe(o,4637,u)|0,n[oe>>2]=4,n[oe+4>>2]=0,n[u>>2]=n[oe>>2],n[u+4>>2]=n[oe+4>>2],rp(o,4653,u)|0,n[q>>2]=5,n[q+4>>2]=0,n[u>>2]=n[q>>2],n[u+4>>2]=n[q+4>>2],rp(o,4669,u)|0,n[L>>2]=6,n[L+4>>2]=0,n[u>>2]=n[L>>2],n[u+4>>2]=n[L+4>>2],rp(o,4686,u)|0,n[M>>2]=7,n[M+4>>2]=0,n[u>>2]=n[M>>2],n[u+4>>2]=n[M+4>>2],rp(o,4701,u)|0,n[R>>2]=8,n[R+4>>2]=0,n[u>>2]=n[R>>2],n[u+4>>2]=n[R+4>>2],rp(o,4719,u)|0,n[k>>2]=9,n[k+4>>2]=0,n[u>>2]=n[k>>2],n[u+4>>2]=n[k+4>>2],rp(o,4736,u)|0,n[B>>2]=21,n[B+4>>2]=0,n[u>>2]=n[B>>2],n[u+4>>2]=n[B+4>>2],gPe(o,4754,u)|0,n[m>>2]=2,n[m+4>>2]=0,n[u>>2]=n[m>>2],n[u+4>>2]=n[m+4>>2],QP(o,4772,u)|0,n[g>>2]=3,n[g+4>>2]=0,n[u>>2]=n[g>>2],n[u+4>>2]=n[g+4>>2],QP(o,4790,u)|0,n[A>>2]=4,n[A+4>>2]=0,n[u>>2]=n[A>>2],n[u+4>>2]=n[A+4>>2],QP(o,4808,u)|0,I=l}function gt(o,l){o=o|0,l=l|0;var u=0;u=wNe()|0,n[o>>2]=u,BNe(u,l),Hh(n[o>>2]|0)}function Xt(o,l,u){return o=o|0,l=l|0,u=u|0,lNe(o,Sn(l)|0,u,0),o|0}function Dr(o,l,u){return o=o|0,l=l|0,u=u|0,YFe(o,Sn(l)|0,u,0),o|0}function ti(o,l,u){return o=o|0,l=l|0,u=u|0,RFe(o,Sn(l)|0,u,0),o|0}function Qr(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],gFe(o,l,g),I=A,o|0}function Nn(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],XTe(o,l,g),I=A,o|0}function Hn(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],NTe(o,l,g),I=A,o|0}function zr(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],ETe(o,l,g),I=A,o|0}function ui(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],nTe(o,l,g),I=A,o|0}function vu(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],HRe(o,l,g),I=A,o|0}function VL(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],SRe(o,l,g),I=A,o|0}function ig(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],XQe(o,l,g),I=A,o|0}function rp(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],NQe(o,l,g),I=A,o|0}function QP(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],EQe(o,l,g),I=A,o|0}function lPe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],nQe(o,l,g),I=A,o|0}function cPe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],Hke(o,l,g),I=A,o|0}function uPe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],Dke(o,l,g),I=A,o|0}function fPe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],fke(o,l,g),I=A,o|0}function APe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],Jxe(o,l,g),I=A,o|0}function pPe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],Qxe(o,l,g),I=A,o|0}function hPe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],dxe(o,l,g),I=A,o|0}function dPe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],XPe(o,l,g),I=A,o|0}function gPe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],mPe(o,l,g),I=A,o|0}function mPe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],yPe(o,u,g,1),I=A}function Sn(o){return o=o|0,o|0}function yPe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=JL()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=EPe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,IPe(m,A)|0,A),I=g}function JL(){var o=0,l=0;if(s[7616]|0||(LK(9136),dr(24,9136,U|0)|0,l=7616,n[l>>2]=1,n[l+4>>2]=0),!(_r(9136)|0)){o=9136,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));LK(9136)}return 9136}function EPe(o){return o=o|0,0}function IPe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=JL()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],OK(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(BPe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function Dn(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0;var B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0;B=I,I=I+32|0,oe=B+24|0,q=B+20|0,R=B+16|0,L=B+12|0,M=B+8|0,k=B+4|0,Ve=B,n[q>>2]=l,n[R>>2]=u,n[L>>2]=A,n[M>>2]=g,n[k>>2]=m,m=o+28|0,n[Ve>>2]=n[m>>2],n[oe>>2]=n[Ve>>2],CPe(o+24|0,oe,q,L,M,R,k)|0,n[m>>2]=n[n[m>>2]>>2],I=B}function CPe(o,l,u,A,g,m,B){return o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0,B=B|0,o=wPe(l)|0,l=Kt(24)|0,NK(l+4|0,n[u>>2]|0,n[A>>2]|0,n[g>>2]|0,n[m>>2]|0,n[B>>2]|0),n[l>>2]=n[o>>2],n[o>>2]=l,l|0}function wPe(o){return o=o|0,n[o>>2]|0}function NK(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0,n[o>>2]=l,n[o+4>>2]=u,n[o+8>>2]=A,n[o+12>>2]=g,n[o+16>>2]=m}function yr(o,l){return o=o|0,l=l|0,l|o|0}function OK(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function BPe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=vPe(o)|0,m>>>0>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,SPe(k,oe>>>0>>1>>>0?q>>>0>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],OK(m,A,u),n[R>>2]=(n[R>>2]|0)+12,DPe(o,k),bPe(k),I=M;return}}function vPe(o){return o=o|0,357913941}function SPe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function DPe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function bPe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function LK(o){o=o|0,kPe(o)}function PPe(o){o=o|0,xPe(o+24|0)}function _r(o){return o=o|0,n[o>>2]|0}function xPe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function kPe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,3,l,QPe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function tn(){return 9228}function QPe(){return 1140}function RPe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0;return u=I,I=I+16|0,A=u+8|0,g=u,m=TPe(o)|0,o=n[m+4>>2]|0,n[g>>2]=n[m>>2],n[g+4>>2]=o,n[A>>2]=n[g>>2],n[A+4>>2]=n[g+4>>2],l=FPe(l,A)|0,I=u,l|0}function rn(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0,n[o>>2]=l,n[o+4>>2]=u,n[o+8>>2]=A,n[o+12>>2]=g,n[o+16>>2]=m}function TPe(o){return o=o|0,(n[(JL()|0)+24>>2]|0)+(o*12|0)|0}function FPe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0;return g=I,I=I+48|0,A=g,u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),ap[u&31](A,o),A=NPe(A)|0,I=g,A|0}function NPe(o){o=o|0;var l=0,u=0,A=0,g=0;return g=I,I=I+32|0,l=g+12|0,u=g,A=KL(MK()|0)|0,A?(zL(l,A),XL(u,l),OPe(o,u),o=ZL(l)|0):o=LPe(o)|0,I=g,o|0}function MK(){var o=0;return s[7632]|0||(VPe(9184),dr(25,9184,U|0)|0,o=7632,n[o>>2]=1,n[o+4>>2]=0),9184}function KL(o){return o=o|0,n[o+36>>2]|0}function zL(o,l){o=o|0,l=l|0,n[o>>2]=l,n[o+4>>2]=o,n[o+8>>2]=0}function XL(o,l){o=o|0,l=l|0,n[o>>2]=n[l>>2],n[o+4>>2]=n[l+4>>2],n[o+8>>2]=0}function OPe(o,l){o=o|0,l=l|0,HPe(l,o,o+8|0,o+16|0,o+24|0,o+32|0,o+40|0)|0}function ZL(o){return o=o|0,n[(n[o+4>>2]|0)+8>>2]|0}function LPe(o){o=o|0;var l=0,u=0,A=0,g=0,m=0,B=0,k=0,R=0;R=I,I=I+16|0,u=R+4|0,A=R,g=_l(8)|0,m=g,B=Kt(48)|0,k=B,l=k+48|0;do n[k>>2]=n[o>>2],k=k+4|0,o=o+4|0;while((k|0)<(l|0));return l=m+4|0,n[l>>2]=B,k=Kt(8)|0,B=n[l>>2]|0,n[A>>2]=0,n[u>>2]=n[A>>2],UK(k,B,u),n[g>>2]=k,I=R,m|0}function UK(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,u=Kt(16)|0,n[u+4>>2]=0,n[u+8>>2]=0,n[u>>2]=1092,n[u+12>>2]=l,n[o+4>>2]=u}function MPe(o){o=o|0,rE(o),It(o)}function UPe(o){o=o|0,o=n[o+12>>2]|0,o|0&&It(o)}function _Pe(o){o=o|0,It(o)}function HPe(o,l,u,A,g,m,B){return o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0,B=B|0,m=jPe(n[o>>2]|0,l,u,A,g,m,B)|0,B=o+4|0,n[(n[B>>2]|0)+8>>2]=m,n[(n[B>>2]|0)+8>>2]|0}function jPe(o,l,u,A,g,m,B){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0,B=B|0;var k=0,R=0;return k=I,I=I+16|0,R=k,Hl(R),o=Us(o)|0,B=GPe(o,+E[l>>3],+E[u>>3],+E[A>>3],+E[g>>3],+E[m>>3],+E[B>>3])|0,jl(R),I=k,B|0}function GPe(o,l,u,A,g,m,B){o=o|0,l=+l,u=+u,A=+A,g=+g,m=+m,B=+B;var k=0;return k=Ca(qPe()|0)|0,l=+rl(l),u=+rl(u),A=+rl(A),g=+rl(g),m=+rl(m),lo(0,k|0,o|0,+l,+u,+A,+g,+m,+ +rl(B))|0}function qPe(){var o=0;return s[7624]|0||(WPe(9172),o=7624,n[o>>2]=1,n[o+4>>2]=0),9172}function WPe(o){o=o|0,Lo(o,YPe()|0,6)}function YPe(){return 1112}function VPe(o){o=o|0,Oh(o)}function JPe(o){o=o|0,_K(o+24|0),HK(o+16|0)}function _K(o){o=o|0,zPe(o)}function HK(o){o=o|0,KPe(o)}function KPe(o){o=o|0;var l=0,u=0;if(l=n[o>>2]|0,l|0)do u=l,l=n[l>>2]|0,It(u);while(l|0);n[o>>2]=0}function zPe(o){o=o|0;var l=0,u=0;if(l=n[o>>2]|0,l|0)do u=l,l=n[l>>2]|0,It(u);while(l|0);n[o>>2]=0}function Oh(o){o=o|0;var l=0;n[o+16>>2]=0,n[o+20>>2]=0,l=o+24|0,n[l>>2]=0,n[o+28>>2]=l,n[o+36>>2]=0,s[o+40>>0]=0,s[o+41>>0]=0}function XPe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],ZPe(o,u,g,0),I=A}function ZPe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=$L()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=$Pe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,exe(m,A)|0,A),I=g}function $L(){var o=0,l=0;if(s[7640]|0||(GK(9232),dr(26,9232,U|0)|0,l=7640,n[l>>2]=1,n[l+4>>2]=0),!(_r(9232)|0)){o=9232,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));GK(9232)}return 9232}function $Pe(o){return o=o|0,0}function exe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=$L()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],jK(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(txe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function jK(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function txe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=rxe(o)|0,m>>>0>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,nxe(k,oe>>>0>>1>>>0?q>>>0>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],jK(m,A,u),n[R>>2]=(n[R>>2]|0)+12,ixe(o,k),sxe(k),I=M;return}}function rxe(o){return o=o|0,357913941}function nxe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function ixe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function sxe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function GK(o){o=o|0,lxe(o)}function oxe(o){o=o|0,axe(o+24|0)}function axe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function lxe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,1,l,cxe()|0,3),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function cxe(){return 1144}function uxe(o,l,u,A,g){o=o|0,l=l|0,u=+u,A=+A,g=g|0;var m=0,B=0,k=0,R=0;m=I,I=I+16|0,B=m+8|0,k=m,R=fxe(o)|0,o=n[R+4>>2]|0,n[k>>2]=n[R>>2],n[k+4>>2]=o,n[B>>2]=n[k>>2],n[B+4>>2]=n[k+4>>2],Axe(l,B,u,A,g),I=m}function fxe(o){return o=o|0,(n[($L()|0)+24>>2]|0)+(o*12|0)|0}function Axe(o,l,u,A,g){o=o|0,l=l|0,u=+u,A=+A,g=g|0;var m=0,B=0,k=0,R=0,M=0;M=I,I=I+16|0,B=M+2|0,k=M+1|0,R=M,m=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(m=n[(n[o>>2]|0)+m>>2]|0),Ff(B,u),u=+Nf(B,u),Ff(k,A),A=+Nf(k,A),np(R,g),R=ip(R,g)|0,iZ[m&1](o,u,A,R),I=M}function Ff(o,l){o=o|0,l=+l}function Nf(o,l){return o=o|0,l=+l,+ +hxe(l)}function np(o,l){o=o|0,l=l|0}function ip(o,l){return o=o|0,l=l|0,pxe(l)|0}function pxe(o){return o=o|0,o|0}function hxe(o){return o=+o,+o}function dxe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],gxe(o,u,g,1),I=A}function gxe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=eM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=mxe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,yxe(m,A)|0,A),I=g}function eM(){var o=0,l=0;if(s[7648]|0||(WK(9268),dr(27,9268,U|0)|0,l=7648,n[l>>2]=1,n[l+4>>2]=0),!(_r(9268)|0)){o=9268,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));WK(9268)}return 9268}function mxe(o){return o=o|0,0}function yxe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=eM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],qK(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(Exe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function qK(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function Exe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=Ixe(o)|0,m>>>0>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,Cxe(k,oe>>>0>>1>>>0?q>>>0>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],qK(m,A,u),n[R>>2]=(n[R>>2]|0)+12,wxe(o,k),Bxe(k),I=M;return}}function Ixe(o){return o=o|0,357913941}function Cxe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function wxe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function Bxe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function WK(o){o=o|0,Dxe(o)}function vxe(o){o=o|0,Sxe(o+24|0)}function Sxe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function Dxe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,4,l,bxe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function bxe(){return 1160}function Pxe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0;return u=I,I=I+16|0,A=u+8|0,g=u,m=xxe(o)|0,o=n[m+4>>2]|0,n[g>>2]=n[m>>2],n[g+4>>2]=o,n[A>>2]=n[g>>2],n[A+4>>2]=n[g+4>>2],l=kxe(l,A)|0,I=u,l|0}function xxe(o){return o=o|0,(n[(eM()|0)+24>>2]|0)+(o*12|0)|0}function kxe(o,l){o=o|0,l=l|0;var u=0;return u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),YK(pg[u&31](o)|0)|0}function YK(o){return o=o|0,o&1|0}function Qxe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],Rxe(o,u,g,0),I=A}function Rxe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=tM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=Txe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,Fxe(m,A)|0,A),I=g}function tM(){var o=0,l=0;if(s[7656]|0||(JK(9304),dr(28,9304,U|0)|0,l=7656,n[l>>2]=1,n[l+4>>2]=0),!(_r(9304)|0)){o=9304,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));JK(9304)}return 9304}function Txe(o){return o=o|0,0}function Fxe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=tM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],VK(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(Nxe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function VK(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function Nxe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=Oxe(o)|0,m>>>0>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,Lxe(k,oe>>>0>>1>>>0?q>>>0>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],VK(m,A,u),n[R>>2]=(n[R>>2]|0)+12,Mxe(o,k),Uxe(k),I=M;return}}function Oxe(o){return o=o|0,357913941}function Lxe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function Mxe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function Uxe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function JK(o){o=o|0,jxe(o)}function _xe(o){o=o|0,Hxe(o+24|0)}function Hxe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function jxe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,5,l,Gxe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function Gxe(){return 1164}function qxe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;A=I,I=I+16|0,g=A+8|0,m=A,B=Wxe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],Yxe(l,g,u),I=A}function Wxe(o){return o=o|0,(n[(tM()|0)+24>>2]|0)+(o*12|0)|0}function Yxe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0;m=I,I=I+16|0,g=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),Lh(g,u),u=Mh(g,u)|0,ap[A&31](o,u),Uh(g),I=m}function Lh(o,l){o=o|0,l=l|0,Vxe(o,l)}function Mh(o,l){return o=o|0,l=l|0,o|0}function Uh(o){o=o|0,Pf(o)}function Vxe(o,l){o=o|0,l=l|0,rM(o,l)}function rM(o,l){o=o|0,l=l|0,n[o>>2]=l}function Jxe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],Kxe(o,u,g,0),I=A}function Kxe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=nM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=zxe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,Xxe(m,A)|0,A),I=g}function nM(){var o=0,l=0;if(s[7664]|0||(zK(9340),dr(29,9340,U|0)|0,l=7664,n[l>>2]=1,n[l+4>>2]=0),!(_r(9340)|0)){o=9340,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));zK(9340)}return 9340}function zxe(o){return o=o|0,0}function Xxe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=nM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],KK(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(Zxe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function KK(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function Zxe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=$xe(o)|0,m>>>0>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,eke(k,oe>>>0>>1>>>0?q>>>0>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],KK(m,A,u),n[R>>2]=(n[R>>2]|0)+12,tke(o,k),rke(k),I=M;return}}function $xe(o){return o=o|0,357913941}function eke(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function tke(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function rke(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function zK(o){o=o|0,ske(o)}function nke(o){o=o|0,ike(o+24|0)}function ike(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function ske(o){o=o|0;var l=0;l=tn()|0,rn(o,2,4,l,oke()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function oke(){return 1180}function ake(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=lke(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],u=cke(l,g,u)|0,I=A,u|0}function lke(o){return o=o|0,(n[(nM()|0)+24>>2]|0)+(o*12|0)|0}function cke(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0;return m=I,I=I+16|0,g=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),sg(g,u),g=og(g,u)|0,g=RP($M[A&15](o,g)|0)|0,I=m,g|0}function sg(o,l){o=o|0,l=l|0}function og(o,l){return o=o|0,l=l|0,uke(l)|0}function RP(o){return o=o|0,o|0}function uke(o){return o=o|0,o|0}function fke(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],Ake(o,u,g,0),I=A}function Ake(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=iM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=pke(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,hke(m,A)|0,A),I=g}function iM(){var o=0,l=0;if(s[7672]|0||(ZK(9376),dr(30,9376,U|0)|0,l=7672,n[l>>2]=1,n[l+4>>2]=0),!(_r(9376)|0)){o=9376,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));ZK(9376)}return 9376}function pke(o){return o=o|0,0}function hke(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=iM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],XK(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(dke(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function XK(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function dke(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=gke(o)|0,m>>>0>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,mke(k,oe>>>0>>1>>>0?q>>>0>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],XK(m,A,u),n[R>>2]=(n[R>>2]|0)+12,yke(o,k),Eke(k),I=M;return}}function gke(o){return o=o|0,357913941}function mke(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function yke(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function Eke(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function ZK(o){o=o|0,wke(o)}function Ike(o){o=o|0,Cke(o+24|0)}function Cke(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function wke(o){o=o|0;var l=0;l=tn()|0,rn(o,2,5,l,$K()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function $K(){return 1196}function Bke(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0;return u=I,I=I+16|0,A=u+8|0,g=u,m=vke(o)|0,o=n[m+4>>2]|0,n[g>>2]=n[m>>2],n[g+4>>2]=o,n[A>>2]=n[g>>2],n[A+4>>2]=n[g+4>>2],l=Ske(l,A)|0,I=u,l|0}function vke(o){return o=o|0,(n[(iM()|0)+24>>2]|0)+(o*12|0)|0}function Ske(o,l){o=o|0,l=l|0;var u=0;return u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),RP(pg[u&31](o)|0)|0}function Dke(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],bke(o,u,g,1),I=A}function bke(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=sM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=Pke(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,xke(m,A)|0,A),I=g}function sM(){var o=0,l=0;if(s[7680]|0||(tz(9412),dr(31,9412,U|0)|0,l=7680,n[l>>2]=1,n[l+4>>2]=0),!(_r(9412)|0)){o=9412,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));tz(9412)}return 9412}function Pke(o){return o=o|0,0}function xke(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=sM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],ez(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(kke(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function ez(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function kke(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=Qke(o)|0,m>>>0>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,Rke(k,oe>>>0>>1>>>0?q>>>0>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],ez(m,A,u),n[R>>2]=(n[R>>2]|0)+12,Tke(o,k),Fke(k),I=M;return}}function Qke(o){return o=o|0,357913941}function Rke(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function Tke(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function Fke(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function tz(o){o=o|0,Lke(o)}function Nke(o){o=o|0,Oke(o+24|0)}function Oke(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function Lke(o){o=o|0;var l=0;l=tn()|0,rn(o,2,6,l,rz()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function rz(){return 1200}function Mke(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0;return u=I,I=I+16|0,A=u+8|0,g=u,m=Uke(o)|0,o=n[m+4>>2]|0,n[g>>2]=n[m>>2],n[g+4>>2]=o,n[A>>2]=n[g>>2],n[A+4>>2]=n[g+4>>2],l=_ke(l,A)|0,I=u,l|0}function Uke(o){return o=o|0,(n[(sM()|0)+24>>2]|0)+(o*12|0)|0}function _ke(o,l){o=o|0,l=l|0;var u=0;return u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),TP(pg[u&31](o)|0)|0}function TP(o){return o=o|0,o|0}function Hke(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],jke(o,u,g,0),I=A}function jke(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=oM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=Gke(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,qke(m,A)|0,A),I=g}function oM(){var o=0,l=0;if(s[7688]|0||(iz(9448),dr(32,9448,U|0)|0,l=7688,n[l>>2]=1,n[l+4>>2]=0),!(_r(9448)|0)){o=9448,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));iz(9448)}return 9448}function Gke(o){return o=o|0,0}function qke(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=oM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],nz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(Wke(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function nz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function Wke(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=Yke(o)|0,m>>>0>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,Vke(k,oe>>>0>>1>>>0?q>>>0>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],nz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,Jke(o,k),Kke(k),I=M;return}}function Yke(o){return o=o|0,357913941}function Vke(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function Jke(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function Kke(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function iz(o){o=o|0,Zke(o)}function zke(o){o=o|0,Xke(o+24|0)}function Xke(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function Zke(o){o=o|0;var l=0;l=tn()|0,rn(o,2,6,l,sz()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function sz(){return 1204}function $ke(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;A=I,I=I+16|0,g=A+8|0,m=A,B=eQe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],tQe(l,g,u),I=A}function eQe(o){return o=o|0,(n[(oM()|0)+24>>2]|0)+(o*12|0)|0}function tQe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0;m=I,I=I+16|0,g=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),aM(g,u),g=lM(g,u)|0,ap[A&31](o,g),I=m}function aM(o,l){o=o|0,l=l|0}function lM(o,l){return o=o|0,l=l|0,rQe(l)|0}function rQe(o){return o=o|0,o|0}function nQe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],iQe(o,u,g,0),I=A}function iQe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=cM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=sQe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,oQe(m,A)|0,A),I=g}function cM(){var o=0,l=0;if(s[7696]|0||(az(9484),dr(33,9484,U|0)|0,l=7696,n[l>>2]=1,n[l+4>>2]=0),!(_r(9484)|0)){o=9484,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));az(9484)}return 9484}function sQe(o){return o=o|0,0}function oQe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=cM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],oz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(aQe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function oz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function aQe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=lQe(o)|0,m>>>0>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,cQe(k,oe>>>0>>1>>>0?q>>>0>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],oz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,uQe(o,k),fQe(k),I=M;return}}function lQe(o){return o=o|0,357913941}function cQe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function uQe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function fQe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function az(o){o=o|0,hQe(o)}function AQe(o){o=o|0,pQe(o+24|0)}function pQe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function hQe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,1,l,dQe()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function dQe(){return 1212}function gQe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0;g=I,I=I+16|0,m=g+8|0,B=g,k=mQe(o)|0,o=n[k+4>>2]|0,n[B>>2]=n[k>>2],n[B+4>>2]=o,n[m>>2]=n[B>>2],n[m+4>>2]=n[B+4>>2],yQe(l,m,u,A),I=g}function mQe(o){return o=o|0,(n[(cM()|0)+24>>2]|0)+(o*12|0)|0}function yQe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0;k=I,I=I+16|0,m=k+1|0,B=k,g=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(g=n[(n[o>>2]|0)+g>>2]|0),aM(m,u),m=lM(m,u)|0,sg(B,A),B=og(B,A)|0,D2[g&15](o,m,B),I=k}function EQe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],IQe(o,u,g,1),I=A}function IQe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=uM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=CQe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,wQe(m,A)|0,A),I=g}function uM(){var o=0,l=0;if(s[7704]|0||(cz(9520),dr(34,9520,U|0)|0,l=7704,n[l>>2]=1,n[l+4>>2]=0),!(_r(9520)|0)){o=9520,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));cz(9520)}return 9520}function CQe(o){return o=o|0,0}function wQe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=uM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],lz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(BQe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function lz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function BQe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=vQe(o)|0,m>>>0>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,SQe(k,oe>>>0>>1>>>0?q>>>0>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],lz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,DQe(o,k),bQe(k),I=M;return}}function vQe(o){return o=o|0,357913941}function SQe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function DQe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function bQe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function cz(o){o=o|0,kQe(o)}function PQe(o){o=o|0,xQe(o+24|0)}function xQe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function kQe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,1,l,QQe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function QQe(){return 1224}function RQe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;return g=I,I=I+16|0,m=g+8|0,B=g,k=TQe(o)|0,o=n[k+4>>2]|0,n[B>>2]=n[k>>2],n[B+4>>2]=o,n[m>>2]=n[B>>2],n[m+4>>2]=n[B+4>>2],A=+FQe(l,m,u),I=g,+A}function TQe(o){return o=o|0,(n[(uM()|0)+24>>2]|0)+(o*12|0)|0}function FQe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return m=I,I=I+16|0,g=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),np(g,u),g=ip(g,u)|0,B=+Tf(+oZ[A&7](o,g)),I=m,+B}function NQe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],OQe(o,u,g,1),I=A}function OQe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=fM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=LQe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,MQe(m,A)|0,A),I=g}function fM(){var o=0,l=0;if(s[7712]|0||(fz(9556),dr(35,9556,U|0)|0,l=7712,n[l>>2]=1,n[l+4>>2]=0),!(_r(9556)|0)){o=9556,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));fz(9556)}return 9556}function LQe(o){return o=o|0,0}function MQe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=fM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],uz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(UQe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function uz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function UQe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=_Qe(o)|0,m>>>0>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,HQe(k,oe>>>0>>1>>>0?q>>>0>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],uz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,jQe(o,k),GQe(k),I=M;return}}function _Qe(o){return o=o|0,357913941}function HQe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function jQe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function GQe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function fz(o){o=o|0,YQe(o)}function qQe(o){o=o|0,WQe(o+24|0)}function WQe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function YQe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,5,l,VQe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function VQe(){return 1232}function JQe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=KQe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],u=+zQe(l,g),I=A,+u}function KQe(o){return o=o|0,(n[(fM()|0)+24>>2]|0)+(o*12|0)|0}function zQe(o,l){o=o|0,l=l|0;var u=0;return u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),+ +Tf(+sZ[u&15](o))}function XQe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],ZQe(o,u,g,1),I=A}function ZQe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=AM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=$Qe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,eRe(m,A)|0,A),I=g}function AM(){var o=0,l=0;if(s[7720]|0||(pz(9592),dr(36,9592,U|0)|0,l=7720,n[l>>2]=1,n[l+4>>2]=0),!(_r(9592)|0)){o=9592,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));pz(9592)}return 9592}function $Qe(o){return o=o|0,0}function eRe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=AM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],Az(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(tRe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function Az(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function tRe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=rRe(o)|0,m>>>0>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,nRe(k,oe>>>0>>1>>>0?q>>>0>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],Az(m,A,u),n[R>>2]=(n[R>>2]|0)+12,iRe(o,k),sRe(k),I=M;return}}function rRe(o){return o=o|0,357913941}function nRe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function iRe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function sRe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function pz(o){o=o|0,lRe(o)}function oRe(o){o=o|0,aRe(o+24|0)}function aRe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function lRe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,7,l,cRe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function cRe(){return 1276}function uRe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0;return u=I,I=I+16|0,A=u+8|0,g=u,m=fRe(o)|0,o=n[m+4>>2]|0,n[g>>2]=n[m>>2],n[g+4>>2]=o,n[A>>2]=n[g>>2],n[A+4>>2]=n[g+4>>2],l=ARe(l,A)|0,I=u,l|0}function fRe(o){return o=o|0,(n[(AM()|0)+24>>2]|0)+(o*12|0)|0}function ARe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0;return g=I,I=I+16|0,A=g,u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),ap[u&31](A,o),A=hz(A)|0,I=g,A|0}function hz(o){o=o|0;var l=0,u=0,A=0,g=0;return g=I,I=I+32|0,l=g+12|0,u=g,A=KL(dz()|0)|0,A?(zL(l,A),XL(u,l),pRe(o,u),o=ZL(l)|0):o=hRe(o)|0,I=g,o|0}function dz(){var o=0;return s[7736]|0||(vRe(9640),dr(25,9640,U|0)|0,o=7736,n[o>>2]=1,n[o+4>>2]=0),9640}function pRe(o,l){o=o|0,l=l|0,yRe(l,o,o+8|0)|0}function hRe(o){o=o|0;var l=0,u=0,A=0,g=0,m=0,B=0,k=0;return u=I,I=I+16|0,g=u+4|0,B=u,A=_l(8)|0,l=A,k=Kt(16)|0,n[k>>2]=n[o>>2],n[k+4>>2]=n[o+4>>2],n[k+8>>2]=n[o+8>>2],n[k+12>>2]=n[o+12>>2],m=l+4|0,n[m>>2]=k,o=Kt(8)|0,m=n[m>>2]|0,n[B>>2]=0,n[g>>2]=n[B>>2],pM(o,m,g),n[A>>2]=o,I=u,l|0}function pM(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,u=Kt(16)|0,n[u+4>>2]=0,n[u+8>>2]=0,n[u>>2]=1244,n[u+12>>2]=l,n[o+4>>2]=u}function dRe(o){o=o|0,rE(o),It(o)}function gRe(o){o=o|0,o=n[o+12>>2]|0,o|0&&It(o)}function mRe(o){o=o|0,It(o)}function yRe(o,l,u){return o=o|0,l=l|0,u=u|0,l=ERe(n[o>>2]|0,l,u)|0,u=o+4|0,n[(n[u>>2]|0)+8>>2]=l,n[(n[u>>2]|0)+8>>2]|0}function ERe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0;return A=I,I=I+16|0,g=A,Hl(g),o=Us(o)|0,u=IRe(o,n[l>>2]|0,+E[u>>3])|0,jl(g),I=A,u|0}function IRe(o,l,u){o=o|0,l=l|0,u=+u;var A=0;return A=Ca(CRe()|0)|0,l=Ky(l)|0,su(0,A|0,o|0,l|0,+ +rl(u))|0}function CRe(){var o=0;return s[7728]|0||(wRe(9628),o=7728,n[o>>2]=1,n[o+4>>2]=0),9628}function wRe(o){o=o|0,Lo(o,BRe()|0,2)}function BRe(){return 1264}function vRe(o){o=o|0,Oh(o)}function SRe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],DRe(o,u,g,1),I=A}function DRe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=hM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=bRe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,PRe(m,A)|0,A),I=g}function hM(){var o=0,l=0;if(s[7744]|0||(mz(9684),dr(37,9684,U|0)|0,l=7744,n[l>>2]=1,n[l+4>>2]=0),!(_r(9684)|0)){o=9684,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));mz(9684)}return 9684}function bRe(o){return o=o|0,0}function PRe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=hM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],gz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(xRe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function gz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function xRe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=kRe(o)|0,m>>>0>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,QRe(k,oe>>>0>>1>>>0?q>>>0>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],gz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,RRe(o,k),TRe(k),I=M;return}}function kRe(o){return o=o|0,357913941}function QRe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function RRe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function TRe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function mz(o){o=o|0,ORe(o)}function FRe(o){o=o|0,NRe(o+24|0)}function NRe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function ORe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,5,l,LRe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function LRe(){return 1280}function MRe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=URe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],u=_Re(l,g,u)|0,I=A,u|0}function URe(o){return o=o|0,(n[(hM()|0)+24>>2]|0)+(o*12|0)|0}function _Re(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return B=I,I=I+32|0,g=B,m=B+16|0,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),np(m,u),m=ip(m,u)|0,D2[A&15](g,o,m),m=hz(g)|0,I=B,m|0}function HRe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],jRe(o,u,g,1),I=A}function jRe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=dM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=GRe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,qRe(m,A)|0,A),I=g}function dM(){var o=0,l=0;if(s[7752]|0||(Ez(9720),dr(38,9720,U|0)|0,l=7752,n[l>>2]=1,n[l+4>>2]=0),!(_r(9720)|0)){o=9720,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Ez(9720)}return 9720}function GRe(o){return o=o|0,0}function qRe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=dM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],yz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(WRe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function yz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function WRe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=YRe(o)|0,m>>>0>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,VRe(k,oe>>>0>>1>>>0?q>>>0>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],yz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,JRe(o,k),KRe(k),I=M;return}}function YRe(o){return o=o|0,357913941}function VRe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function JRe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function KRe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function Ez(o){o=o|0,ZRe(o)}function zRe(o){o=o|0,XRe(o+24|0)}function XRe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function ZRe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,8,l,$Re()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function $Re(){return 1288}function eTe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0;return u=I,I=I+16|0,A=u+8|0,g=u,m=tTe(o)|0,o=n[m+4>>2]|0,n[g>>2]=n[m>>2],n[g+4>>2]=o,n[A>>2]=n[g>>2],n[A+4>>2]=n[g+4>>2],l=rTe(l,A)|0,I=u,l|0}function tTe(o){return o=o|0,(n[(dM()|0)+24>>2]|0)+(o*12|0)|0}function rTe(o,l){o=o|0,l=l|0;var u=0;return u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),ng(pg[u&31](o)|0)|0}function nTe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],iTe(o,u,g,0),I=A}function iTe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=gM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=sTe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,oTe(m,A)|0,A),I=g}function gM(){var o=0,l=0;if(s[7760]|0||(Cz(9756),dr(39,9756,U|0)|0,l=7760,n[l>>2]=1,n[l+4>>2]=0),!(_r(9756)|0)){o=9756,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Cz(9756)}return 9756}function sTe(o){return o=o|0,0}function oTe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=gM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],Iz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(aTe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function Iz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function aTe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=lTe(o)|0,m>>>0>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,cTe(k,oe>>>0>>1>>>0?q>>>0>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],Iz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,uTe(o,k),fTe(k),I=M;return}}function lTe(o){return o=o|0,357913941}function cTe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function uTe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function fTe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function Cz(o){o=o|0,hTe(o)}function ATe(o){o=o|0,pTe(o+24|0)}function pTe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function hTe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,8,l,dTe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function dTe(){return 1292}function gTe(o,l,u){o=o|0,l=l|0,u=+u;var A=0,g=0,m=0,B=0;A=I,I=I+16|0,g=A+8|0,m=A,B=mTe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],yTe(l,g,u),I=A}function mTe(o){return o=o|0,(n[(gM()|0)+24>>2]|0)+(o*12|0)|0}function yTe(o,l,u){o=o|0,l=l|0,u=+u;var A=0,g=0,m=0;m=I,I=I+16|0,g=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),Ff(g,u),u=+Nf(g,u),rZ[A&31](o,u),I=m}function ETe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],ITe(o,u,g,0),I=A}function ITe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=mM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=CTe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,wTe(m,A)|0,A),I=g}function mM(){var o=0,l=0;if(s[7768]|0||(Bz(9792),dr(40,9792,U|0)|0,l=7768,n[l>>2]=1,n[l+4>>2]=0),!(_r(9792)|0)){o=9792,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Bz(9792)}return 9792}function CTe(o){return o=o|0,0}function wTe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=mM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],wz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(BTe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function wz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function BTe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=vTe(o)|0,m>>>0>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,STe(k,oe>>>0>>1>>>0?q>>>0>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],wz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,DTe(o,k),bTe(k),I=M;return}}function vTe(o){return o=o|0,357913941}function STe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function DTe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function bTe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function Bz(o){o=o|0,kTe(o)}function PTe(o){o=o|0,xTe(o+24|0)}function xTe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function kTe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,1,l,QTe()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function QTe(){return 1300}function RTe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=+A;var g=0,m=0,B=0,k=0;g=I,I=I+16|0,m=g+8|0,B=g,k=TTe(o)|0,o=n[k+4>>2]|0,n[B>>2]=n[k>>2],n[B+4>>2]=o,n[m>>2]=n[B>>2],n[m+4>>2]=n[B+4>>2],FTe(l,m,u,A),I=g}function TTe(o){return o=o|0,(n[(mM()|0)+24>>2]|0)+(o*12|0)|0}function FTe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=+A;var g=0,m=0,B=0,k=0;k=I,I=I+16|0,m=k+1|0,B=k,g=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(g=n[(n[o>>2]|0)+g>>2]|0),np(m,u),m=ip(m,u)|0,Ff(B,A),A=+Nf(B,A),uZ[g&15](o,m,A),I=k}function NTe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],OTe(o,u,g,0),I=A}function OTe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=yM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=LTe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,MTe(m,A)|0,A),I=g}function yM(){var o=0,l=0;if(s[7776]|0||(Sz(9828),dr(41,9828,U|0)|0,l=7776,n[l>>2]=1,n[l+4>>2]=0),!(_r(9828)|0)){o=9828,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Sz(9828)}return 9828}function LTe(o){return o=o|0,0}function MTe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=yM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],vz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(UTe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function vz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function UTe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=_Te(o)|0,m>>>0>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,HTe(k,oe>>>0>>1>>>0?q>>>0>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],vz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,jTe(o,k),GTe(k),I=M;return}}function _Te(o){return o=o|0,357913941}function HTe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function jTe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function GTe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function Sz(o){o=o|0,YTe(o)}function qTe(o){o=o|0,WTe(o+24|0)}function WTe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function YTe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,7,l,VTe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function VTe(){return 1312}function JTe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;A=I,I=I+16|0,g=A+8|0,m=A,B=KTe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],zTe(l,g,u),I=A}function KTe(o){return o=o|0,(n[(yM()|0)+24>>2]|0)+(o*12|0)|0}function zTe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0;m=I,I=I+16|0,g=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),np(g,u),g=ip(g,u)|0,ap[A&31](o,g),I=m}function XTe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],ZTe(o,u,g,0),I=A}function ZTe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=EM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=$Te(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,eFe(m,A)|0,A),I=g}function EM(){var o=0,l=0;if(s[7784]|0||(bz(9864),dr(42,9864,U|0)|0,l=7784,n[l>>2]=1,n[l+4>>2]=0),!(_r(9864)|0)){o=9864,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));bz(9864)}return 9864}function $Te(o){return o=o|0,0}function eFe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=EM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],Dz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(tFe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function Dz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function tFe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=rFe(o)|0,m>>>0>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,nFe(k,oe>>>0>>1>>>0?q>>>0>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],Dz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,iFe(o,k),sFe(k),I=M;return}}function rFe(o){return o=o|0,357913941}function nFe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function iFe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function sFe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function bz(o){o=o|0,lFe(o)}function oFe(o){o=o|0,aFe(o+24|0)}function aFe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function lFe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,8,l,cFe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function cFe(){return 1320}function uFe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;A=I,I=I+16|0,g=A+8|0,m=A,B=fFe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],AFe(l,g,u),I=A}function fFe(o){return o=o|0,(n[(EM()|0)+24>>2]|0)+(o*12|0)|0}function AFe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0;m=I,I=I+16|0,g=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),pFe(g,u),g=hFe(g,u)|0,ap[A&31](o,g),I=m}function pFe(o,l){o=o|0,l=l|0}function hFe(o,l){return o=o|0,l=l|0,dFe(l)|0}function dFe(o){return o=o|0,o|0}function gFe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],mFe(o,u,g,0),I=A}function mFe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=IM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=yFe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,EFe(m,A)|0,A),I=g}function IM(){var o=0,l=0;if(s[7792]|0||(xz(9900),dr(43,9900,U|0)|0,l=7792,n[l>>2]=1,n[l+4>>2]=0),!(_r(9900)|0)){o=9900,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));xz(9900)}return 9900}function yFe(o){return o=o|0,0}function EFe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=IM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],Pz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(IFe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function Pz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function IFe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=CFe(o)|0,m>>>0>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,wFe(k,oe>>>0>>1>>>0?q>>>0>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],Pz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,BFe(o,k),vFe(k),I=M;return}}function CFe(o){return o=o|0,357913941}function wFe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function BFe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function vFe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function xz(o){o=o|0,bFe(o)}function SFe(o){o=o|0,DFe(o+24|0)}function DFe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function bFe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,22,l,PFe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function PFe(){return 1344}function xFe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0;u=I,I=I+16|0,A=u+8|0,g=u,m=kFe(o)|0,o=n[m+4>>2]|0,n[g>>2]=n[m>>2],n[g+4>>2]=o,n[A>>2]=n[g>>2],n[A+4>>2]=n[g+4>>2],QFe(l,A),I=u}function kFe(o){return o=o|0,(n[(IM()|0)+24>>2]|0)+(o*12|0)|0}function QFe(o,l){o=o|0,l=l|0;var u=0;u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),op[u&127](o)}function RFe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;m=n[o>>2]|0,g=CM()|0,o=TFe(u)|0,Dn(m,l,g,o,FFe(u,A)|0,A)}function CM(){var o=0,l=0;if(s[7800]|0||(Qz(9936),dr(44,9936,U|0)|0,l=7800,n[l>>2]=1,n[l+4>>2]=0),!(_r(9936)|0)){o=9936,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Qz(9936)}return 9936}function TFe(o){return o=o|0,o|0}function FFe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return k=I,I=I+16|0,g=k,m=k+4|0,n[g>>2]=o,R=CM()|0,B=R+24|0,l=yr(l,4)|0,n[m>>2]=l,u=R+28|0,A=n[u>>2]|0,A>>>0<(n[R+32>>2]|0)>>>0?(kz(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(NFe(B,g,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function kz(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function NFe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;if(k=I,I=I+32|0,g=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=OFe(o)|0,A>>>0>>0)an(o);else{R=n[o>>2]|0,L=(n[o+8>>2]|0)-R|0,M=L>>2,LFe(g,L>>3>>>0>>1>>>0?M>>>0>>0?B:M:A,(n[m>>2]|0)-R>>3,o+8|0),B=g+8|0,kz(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,MFe(o,g),UFe(g),I=k;return}}function OFe(o){return o=o|0,536870911}function LFe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{g=Kt(l<<3)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<3)}function MFe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>3)<<3)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function UFe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&It(o)}function Qz(o){o=o|0,jFe(o)}function _Fe(o){o=o|0,HFe(o+24|0)}function HFe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function jFe(o){o=o|0;var l=0;l=tn()|0,rn(o,1,23,l,sz()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function GFe(o,l){o=o|0,l=l|0,WFe(n[(qFe(o)|0)>>2]|0,l)}function qFe(o){return o=o|0,(n[(CM()|0)+24>>2]|0)+(o<<3)|0}function WFe(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,aM(A,l),l=lM(A,l)|0,op[o&127](l),I=u}function YFe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;m=n[o>>2]|0,g=wM()|0,o=VFe(u)|0,Dn(m,l,g,o,JFe(u,A)|0,A)}function wM(){var o=0,l=0;if(s[7808]|0||(Tz(9972),dr(45,9972,U|0)|0,l=7808,n[l>>2]=1,n[l+4>>2]=0),!(_r(9972)|0)){o=9972,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Tz(9972)}return 9972}function VFe(o){return o=o|0,o|0}function JFe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return k=I,I=I+16|0,g=k,m=k+4|0,n[g>>2]=o,R=wM()|0,B=R+24|0,l=yr(l,4)|0,n[m>>2]=l,u=R+28|0,A=n[u>>2]|0,A>>>0<(n[R+32>>2]|0)>>>0?(Rz(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(KFe(B,g,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function Rz(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function KFe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;if(k=I,I=I+32|0,g=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=zFe(o)|0,A>>>0>>0)an(o);else{R=n[o>>2]|0,L=(n[o+8>>2]|0)-R|0,M=L>>2,XFe(g,L>>3>>>0>>1>>>0?M>>>0>>0?B:M:A,(n[m>>2]|0)-R>>3,o+8|0),B=g+8|0,Rz(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,ZFe(o,g),$Fe(g),I=k;return}}function zFe(o){return o=o|0,536870911}function XFe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{g=Kt(l<<3)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<3)}function ZFe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>3)<<3)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function $Fe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&It(o)}function Tz(o){o=o|0,rNe(o)}function eNe(o){o=o|0,tNe(o+24|0)}function tNe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function rNe(o){o=o|0;var l=0;l=tn()|0,rn(o,1,9,l,nNe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function nNe(){return 1348}function iNe(o,l){return o=o|0,l=l|0,oNe(n[(sNe(o)|0)>>2]|0,l)|0}function sNe(o){return o=o|0,(n[(wM()|0)+24>>2]|0)+(o<<3)|0}function oNe(o,l){o=o|0,l=l|0;var u=0,A=0;return u=I,I=I+16|0,A=u,Fz(A,l),l=Nz(A,l)|0,l=RP(pg[o&31](l)|0)|0,I=u,l|0}function Fz(o,l){o=o|0,l=l|0}function Nz(o,l){return o=o|0,l=l|0,aNe(l)|0}function aNe(o){return o=o|0,o|0}function lNe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;m=n[o>>2]|0,g=BM()|0,o=cNe(u)|0,Dn(m,l,g,o,uNe(u,A)|0,A)}function BM(){var o=0,l=0;if(s[7816]|0||(Lz(10008),dr(46,10008,U|0)|0,l=7816,n[l>>2]=1,n[l+4>>2]=0),!(_r(10008)|0)){o=10008,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Lz(10008)}return 10008}function cNe(o){return o=o|0,o|0}function uNe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return k=I,I=I+16|0,g=k,m=k+4|0,n[g>>2]=o,R=BM()|0,B=R+24|0,l=yr(l,4)|0,n[m>>2]=l,u=R+28|0,A=n[u>>2]|0,A>>>0<(n[R+32>>2]|0)>>>0?(Oz(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(fNe(B,g,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function Oz(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function fNe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;if(k=I,I=I+32|0,g=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=ANe(o)|0,A>>>0>>0)an(o);else{R=n[o>>2]|0,L=(n[o+8>>2]|0)-R|0,M=L>>2,pNe(g,L>>3>>>0>>1>>>0?M>>>0>>0?B:M:A,(n[m>>2]|0)-R>>3,o+8|0),B=g+8|0,Oz(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,hNe(o,g),dNe(g),I=k;return}}function ANe(o){return o=o|0,536870911}function pNe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{g=Kt(l<<3)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<3)}function hNe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>3)<<3)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function dNe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&It(o)}function Lz(o){o=o|0,yNe(o)}function gNe(o){o=o|0,mNe(o+24|0)}function mNe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function yNe(o){o=o|0;var l=0;l=tn()|0,rn(o,1,15,l,$K()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function ENe(o){return o=o|0,CNe(n[(INe(o)|0)>>2]|0)|0}function INe(o){return o=o|0,(n[(BM()|0)+24>>2]|0)+(o<<3)|0}function CNe(o){return o=o|0,RP(VP[o&7]()|0)|0}function wNe(){var o=0;return s[7832]|0||(kNe(10052),dr(25,10052,U|0)|0,o=7832,n[o>>2]=1,n[o+4>>2]=0),10052}function BNe(o,l){o=o|0,l=l|0,n[o>>2]=vNe()|0,n[o+4>>2]=SNe()|0,n[o+12>>2]=l,n[o+8>>2]=DNe()|0,n[o+32>>2]=2}function vNe(){return 11709}function SNe(){return 1188}function DNe(){return FP()|0}function bNe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(_h(A,896)|0)==512?u|0&&(PNe(u),It(u)):l|0&&(Uy(l),It(l))}function _h(o,l){return o=o|0,l=l|0,l&o|0}function PNe(o){o=o|0,o=n[o+4>>2]|0,o|0&&jh(o)}function FP(){var o=0;return s[7824]|0||(n[2511]=xNe()|0,n[2512]=0,o=7824,n[o>>2]=1,n[o+4>>2]=0),10044}function xNe(){return 0}function kNe(o){o=o|0,Oh(o)}function QNe(o){o=o|0;var l=0,u=0,A=0,g=0,m=0;l=I,I=I+32|0,u=l+24|0,m=l+16|0,g=l+8|0,A=l,RNe(o,4827),TNe(o,4834,3)|0,FNe(o,3682,47)|0,n[m>>2]=9,n[m+4>>2]=0,n[u>>2]=n[m>>2],n[u+4>>2]=n[m+4>>2],NNe(o,4841,u)|0,n[g>>2]=1,n[g+4>>2]=0,n[u>>2]=n[g>>2],n[u+4>>2]=n[g+4>>2],ONe(o,4871,u)|0,n[A>>2]=10,n[A+4>>2]=0,n[u>>2]=n[A>>2],n[u+4>>2]=n[A+4>>2],LNe(o,4891,u)|0,I=l}function RNe(o,l){o=o|0,l=l|0;var u=0;u=dLe()|0,n[o>>2]=u,gLe(u,l),Hh(n[o>>2]|0)}function TNe(o,l,u){return o=o|0,l=l|0,u=u|0,$Oe(o,Sn(l)|0,u,0),o|0}function FNe(o,l,u){return o=o|0,l=l|0,u=u|0,MOe(o,Sn(l)|0,u,0),o|0}function NNe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],EOe(o,l,g),I=A,o|0}function ONe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],eOe(o,l,g),I=A,o|0}function LNe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],MNe(o,l,g),I=A,o|0}function MNe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],UNe(o,u,g,1),I=A}function UNe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=vM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=_Ne(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,HNe(m,A)|0,A),I=g}function vM(){var o=0,l=0;if(s[7840]|0||(Uz(10100),dr(48,10100,U|0)|0,l=7840,n[l>>2]=1,n[l+4>>2]=0),!(_r(10100)|0)){o=10100,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Uz(10100)}return 10100}function _Ne(o){return o=o|0,0}function HNe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=vM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],Mz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(jNe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function Mz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function jNe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=GNe(o)|0,m>>>0>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,qNe(k,oe>>>0>>1>>>0?q>>>0>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],Mz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,WNe(o,k),YNe(k),I=M;return}}function GNe(o){return o=o|0,357913941}function qNe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function WNe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function YNe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function Uz(o){o=o|0,KNe(o)}function VNe(o){o=o|0,JNe(o+24|0)}function JNe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function KNe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,6,l,zNe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function zNe(){return 1364}function XNe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;return A=I,I=I+16|0,g=A+8|0,m=A,B=ZNe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],u=$Ne(l,g,u)|0,I=A,u|0}function ZNe(o){return o=o|0,(n[(vM()|0)+24>>2]|0)+(o*12|0)|0}function $Ne(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0;return m=I,I=I+16|0,g=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),np(g,u),g=ip(g,u)|0,g=YK($M[A&15](o,g)|0)|0,I=m,g|0}function eOe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],tOe(o,u,g,0),I=A}function tOe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=SM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=rOe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,nOe(m,A)|0,A),I=g}function SM(){var o=0,l=0;if(s[7848]|0||(Hz(10136),dr(49,10136,U|0)|0,l=7848,n[l>>2]=1,n[l+4>>2]=0),!(_r(10136)|0)){o=10136,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Hz(10136)}return 10136}function rOe(o){return o=o|0,0}function nOe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=SM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],_z(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(iOe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function _z(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function iOe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=sOe(o)|0,m>>>0>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,oOe(k,oe>>>0>>1>>>0?q>>>0>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],_z(m,A,u),n[R>>2]=(n[R>>2]|0)+12,aOe(o,k),lOe(k),I=M;return}}function sOe(o){return o=o|0,357913941}function oOe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function aOe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function lOe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function Hz(o){o=o|0,fOe(o)}function cOe(o){o=o|0,uOe(o+24|0)}function uOe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function fOe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,9,l,AOe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function AOe(){return 1372}function pOe(o,l,u){o=o|0,l=l|0,u=+u;var A=0,g=0,m=0,B=0;A=I,I=I+16|0,g=A+8|0,m=A,B=hOe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],dOe(l,g,u),I=A}function hOe(o){return o=o|0,(n[(SM()|0)+24>>2]|0)+(o*12|0)|0}function dOe(o,l,u){o=o|0,l=l|0,u=+u;var A=0,g=0,m=0,B=$e;m=I,I=I+16|0,g=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),gOe(g,u),B=y(mOe(g,u)),tZ[A&1](o,B),I=m}function gOe(o,l){o=o|0,l=+l}function mOe(o,l){return o=o|0,l=+l,y(yOe(l))}function yOe(o){return o=+o,y(o)}function EOe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,g=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Sn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[g>>2]=n[m>>2],n[g+4>>2]=n[m+4>>2],IOe(o,u,g,0),I=A}function IOe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0,R=0,M=0,L=0;g=I,I=I+32|0,m=g+16|0,L=g+8|0,k=g,M=n[u>>2]|0,R=n[u+4>>2]|0,B=n[o>>2]|0,o=DM()|0,n[L>>2]=M,n[L+4>>2]=R,n[m>>2]=n[L>>2],n[m+4>>2]=n[L+4>>2],u=COe(m)|0,n[k>>2]=M,n[k+4>>2]=R,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],Dn(B,l,o,u,wOe(m,A)|0,A),I=g}function DM(){var o=0,l=0;if(s[7856]|0||(Gz(10172),dr(50,10172,U|0)|0,l=7856,n[l>>2]=1,n[l+4>>2]=0),!(_r(10172)|0)){o=10172,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Gz(10172)}return 10172}function COe(o){return o=o|0,0}function wOe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0;return L=I,I=I+32|0,g=L+24|0,B=L+16|0,k=L,R=L+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,q=DM()|0,M=q+24|0,o=yr(l,4)|0,n[R>>2]=o,l=q+28|0,u=n[l>>2]|0,u>>>0<(n[q+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[g>>2]=n[B>>2],n[g+4>>2]=n[B+4>>2],jz(u,g,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(BOe(M,k,R),o=n[l>>2]|0),I=L,((o-(n[M>>2]|0)|0)/12|0)+-1|0}function jz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function BOe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;if(M=I,I=I+48|0,A=M+32|0,B=M+24|0,k=M,R=o+4|0,g=(((n[R>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=vOe(o)|0,m>>>0>>0)an(o);else{L=n[o>>2]|0,oe=((n[o+8>>2]|0)-L|0)/12|0,q=oe<<1,SOe(k,oe>>>0>>1>>>0?q>>>0>>0?g:q:m,((n[R>>2]|0)-L|0)/12|0,o+8|0),R=k+8|0,m=n[R>>2]|0,g=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=g,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],jz(m,A,u),n[R>>2]=(n[R>>2]|0)+12,DOe(o,k),bOe(k),I=M;return}}function vOe(o){return o=o|0,357913941}function SOe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{g=Kt(l*12|0)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l*12|0)}function DOe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((g|0)/-12|0)*12|0)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function bOe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&It(o)}function Gz(o){o=o|0,kOe(o)}function POe(o){o=o|0,xOe(o+24|0)}function xOe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),It(u))}function kOe(o){o=o|0;var l=0;l=tn()|0,rn(o,2,3,l,QOe()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function QOe(){return 1380}function ROe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0;g=I,I=I+16|0,m=g+8|0,B=g,k=TOe(o)|0,o=n[k+4>>2]|0,n[B>>2]=n[k>>2],n[B+4>>2]=o,n[m>>2]=n[B>>2],n[m+4>>2]=n[B+4>>2],FOe(l,m,u,A),I=g}function TOe(o){return o=o|0,(n[(DM()|0)+24>>2]|0)+(o*12|0)|0}function FOe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0;k=I,I=I+16|0,m=k+1|0,B=k,g=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(g=n[(n[o>>2]|0)+g>>2]|0),np(m,u),m=ip(m,u)|0,NOe(B,A),B=OOe(B,A)|0,D2[g&15](o,m,B),I=k}function NOe(o,l){o=o|0,l=l|0}function OOe(o,l){return o=o|0,l=l|0,LOe(l)|0}function LOe(o){return o=o|0,(o|0)!=0|0}function MOe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;m=n[o>>2]|0,g=bM()|0,o=UOe(u)|0,Dn(m,l,g,o,_Oe(u,A)|0,A)}function bM(){var o=0,l=0;if(s[7864]|0||(Wz(10208),dr(51,10208,U|0)|0,l=7864,n[l>>2]=1,n[l+4>>2]=0),!(_r(10208)|0)){o=10208,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Wz(10208)}return 10208}function UOe(o){return o=o|0,o|0}function _Oe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return k=I,I=I+16|0,g=k,m=k+4|0,n[g>>2]=o,R=bM()|0,B=R+24|0,l=yr(l,4)|0,n[m>>2]=l,u=R+28|0,A=n[u>>2]|0,A>>>0<(n[R+32>>2]|0)>>>0?(qz(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(HOe(B,g,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function qz(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function HOe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;if(k=I,I=I+32|0,g=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=jOe(o)|0,A>>>0>>0)an(o);else{R=n[o>>2]|0,L=(n[o+8>>2]|0)-R|0,M=L>>2,GOe(g,L>>3>>>0>>1>>>0?M>>>0>>0?B:M:A,(n[m>>2]|0)-R>>3,o+8|0),B=g+8|0,qz(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,qOe(o,g),WOe(g),I=k;return}}function jOe(o){return o=o|0,536870911}function GOe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{g=Kt(l<<3)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<3)}function qOe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>3)<<3)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function WOe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&It(o)}function Wz(o){o=o|0,JOe(o)}function YOe(o){o=o|0,VOe(o+24|0)}function VOe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function JOe(o){o=o|0;var l=0;l=tn()|0,rn(o,1,24,l,KOe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function KOe(){return 1392}function zOe(o,l){o=o|0,l=l|0,ZOe(n[(XOe(o)|0)>>2]|0,l)}function XOe(o){return o=o|0,(n[(bM()|0)+24>>2]|0)+(o<<3)|0}function ZOe(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,Fz(A,l),l=Nz(A,l)|0,op[o&127](l),I=u}function $Oe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;m=n[o>>2]|0,g=PM()|0,o=eLe(u)|0,Dn(m,l,g,o,tLe(u,A)|0,A)}function PM(){var o=0,l=0;if(s[7872]|0||(Vz(10244),dr(52,10244,U|0)|0,l=7872,n[l>>2]=1,n[l+4>>2]=0),!(_r(10244)|0)){o=10244,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Vz(10244)}return 10244}function eLe(o){return o=o|0,o|0}function tLe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return k=I,I=I+16|0,g=k,m=k+4|0,n[g>>2]=o,R=PM()|0,B=R+24|0,l=yr(l,4)|0,n[m>>2]=l,u=R+28|0,A=n[u>>2]|0,A>>>0<(n[R+32>>2]|0)>>>0?(Yz(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(rLe(B,g,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function Yz(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function rLe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;if(k=I,I=I+32|0,g=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=nLe(o)|0,A>>>0>>0)an(o);else{R=n[o>>2]|0,L=(n[o+8>>2]|0)-R|0,M=L>>2,iLe(g,L>>3>>>0>>1>>>0?M>>>0>>0?B:M:A,(n[m>>2]|0)-R>>3,o+8|0),B=g+8|0,Yz(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,sLe(o,g),oLe(g),I=k;return}}function nLe(o){return o=o|0,536870911}function iLe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{g=Kt(l<<3)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<3)}function sLe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>3)<<3)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function oLe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&It(o)}function Vz(o){o=o|0,cLe(o)}function aLe(o){o=o|0,lLe(o+24|0)}function lLe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function cLe(o){o=o|0;var l=0;l=tn()|0,rn(o,1,16,l,uLe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function uLe(){return 1400}function fLe(o){return o=o|0,pLe(n[(ALe(o)|0)>>2]|0)|0}function ALe(o){return o=o|0,(n[(PM()|0)+24>>2]|0)+(o<<3)|0}function pLe(o){return o=o|0,hLe(VP[o&7]()|0)|0}function hLe(o){return o=o|0,o|0}function dLe(){var o=0;return s[7880]|0||(wLe(10280),dr(25,10280,U|0)|0,o=7880,n[o>>2]=1,n[o+4>>2]=0),10280}function gLe(o,l){o=o|0,l=l|0,n[o>>2]=mLe()|0,n[o+4>>2]=yLe()|0,n[o+12>>2]=l,n[o+8>>2]=ELe()|0,n[o+32>>2]=4}function mLe(){return 11711}function yLe(){return 1356}function ELe(){return FP()|0}function ILe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(_h(A,896)|0)==512?u|0&&(CLe(u),It(u)):l|0&&(Vd(l),It(l))}function CLe(o){o=o|0,o=n[o+4>>2]|0,o|0&&jh(o)}function wLe(o){o=o|0,Oh(o)}function BLe(o){o=o|0,vLe(o,4920),SLe(o)|0,DLe(o)|0}function vLe(o,l){o=o|0,l=l|0;var u=0;u=dz()|0,n[o>>2]=u,VLe(u,l),Hh(n[o>>2]|0)}function SLe(o){o=o|0;var l=0;return l=n[o>>2]|0,ag(l,LLe()|0),o|0}function DLe(o){o=o|0;var l=0;return l=n[o>>2]|0,ag(l,bLe()|0),o|0}function bLe(){var o=0;return s[7888]|0||(Jz(10328),dr(53,10328,U|0)|0,o=7888,n[o>>2]=1,n[o+4>>2]=0),_r(10328)|0||Jz(10328),10328}function ag(o,l){o=o|0,l=l|0,Dn(o,0,l,0,0,0)}function Jz(o){o=o|0,kLe(o),lg(o,10)}function PLe(o){o=o|0,xLe(o+24|0)}function xLe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function kLe(o){o=o|0;var l=0;l=tn()|0,rn(o,5,1,l,FLe()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function QLe(o,l,u){o=o|0,l=l|0,u=+u,RLe(o,l,u)}function lg(o,l){o=o|0,l=l|0,n[o+20>>2]=l}function RLe(o,l,u){o=o|0,l=l|0,u=+u;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+16|0,m=A+8|0,k=A+13|0,g=A,B=A+12|0,np(k,l),n[m>>2]=ip(k,l)|0,Ff(B,u),E[g>>3]=+Nf(B,u),TLe(o,m,g),I=A}function TLe(o,l,u){o=o|0,l=l|0,u=u|0,Ul(o+8|0,n[l>>2]|0,+E[u>>3]),s[o+24>>0]=1}function FLe(){return 1404}function NLe(o,l){return o=o|0,l=+l,OLe(o,l)|0}function OLe(o,l){o=o|0,l=+l;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return A=I,I=I+16|0,m=A+4|0,B=A+8|0,k=A,g=_l(8)|0,u=g,R=Kt(16)|0,np(m,o),o=ip(m,o)|0,Ff(B,l),Ul(R,o,+Nf(B,l)),B=u+4|0,n[B>>2]=R,o=Kt(8)|0,B=n[B>>2]|0,n[k>>2]=0,n[m>>2]=n[k>>2],pM(o,B,m),n[g>>2]=o,I=A,u|0}function LLe(){var o=0;return s[7896]|0||(Kz(10364),dr(54,10364,U|0)|0,o=7896,n[o>>2]=1,n[o+4>>2]=0),_r(10364)|0||Kz(10364),10364}function Kz(o){o=o|0,_Le(o),lg(o,55)}function MLe(o){o=o|0,ULe(o+24|0)}function ULe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function _Le(o){o=o|0;var l=0;l=tn()|0,rn(o,5,4,l,qLe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function HLe(o){o=o|0,jLe(o)}function jLe(o){o=o|0,GLe(o)}function GLe(o){o=o|0,zz(o+8|0),s[o+24>>0]=1}function zz(o){o=o|0,n[o>>2]=0,E[o+8>>3]=0}function qLe(){return 1424}function WLe(){return YLe()|0}function YLe(){var o=0,l=0,u=0,A=0,g=0,m=0,B=0;return l=I,I=I+16|0,g=l+4|0,B=l,u=_l(8)|0,o=u,A=Kt(16)|0,zz(A),m=o+4|0,n[m>>2]=A,A=Kt(8)|0,m=n[m>>2]|0,n[B>>2]=0,n[g>>2]=n[B>>2],pM(A,m,g),n[u>>2]=A,I=l,o|0}function VLe(o,l){o=o|0,l=l|0,n[o>>2]=JLe()|0,n[o+4>>2]=KLe()|0,n[o+12>>2]=l,n[o+8>>2]=zLe()|0,n[o+32>>2]=5}function JLe(){return 11710}function KLe(){return 1416}function zLe(){return NP()|0}function XLe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(_h(A,896)|0)==512?u|0&&(ZLe(u),It(u)):l|0&&It(l)}function ZLe(o){o=o|0,o=n[o+4>>2]|0,o|0&&jh(o)}function NP(){var o=0;return s[7904]|0||(n[2600]=$Le()|0,n[2601]=0,o=7904,n[o>>2]=1,n[o+4>>2]=0),10400}function $Le(){return n[357]|0}function eMe(o){o=o|0,tMe(o,4926),rMe(o)|0}function tMe(o,l){o=o|0,l=l|0;var u=0;u=MK()|0,n[o>>2]=u,pMe(u,l),Hh(n[o>>2]|0)}function rMe(o){o=o|0;var l=0;return l=n[o>>2]|0,ag(l,nMe()|0),o|0}function nMe(){var o=0;return s[7912]|0||(Xz(10412),dr(56,10412,U|0)|0,o=7912,n[o>>2]=1,n[o+4>>2]=0),_r(10412)|0||Xz(10412),10412}function Xz(o){o=o|0,oMe(o),lg(o,57)}function iMe(o){o=o|0,sMe(o+24|0)}function sMe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function oMe(o){o=o|0;var l=0;l=tn()|0,rn(o,5,5,l,uMe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function aMe(o){o=o|0,lMe(o)}function lMe(o){o=o|0,cMe(o)}function cMe(o){o=o|0;var l=0,u=0;l=o+8|0,u=l+48|0;do n[l>>2]=0,l=l+4|0;while((l|0)<(u|0));s[o+56>>0]=1}function uMe(){return 1432}function fMe(){return AMe()|0}function AMe(){var o=0,l=0,u=0,A=0,g=0,m=0,B=0,k=0;B=I,I=I+16|0,o=B+4|0,l=B,u=_l(8)|0,A=u,g=Kt(48)|0,m=g,k=m+48|0;do n[m>>2]=0,m=m+4|0;while((m|0)<(k|0));return m=A+4|0,n[m>>2]=g,k=Kt(8)|0,m=n[m>>2]|0,n[l>>2]=0,n[o>>2]=n[l>>2],UK(k,m,o),n[u>>2]=k,I=B,A|0}function pMe(o,l){o=o|0,l=l|0,n[o>>2]=hMe()|0,n[o+4>>2]=dMe()|0,n[o+12>>2]=l,n[o+8>>2]=gMe()|0,n[o+32>>2]=6}function hMe(){return 11704}function dMe(){return 1436}function gMe(){return NP()|0}function mMe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(_h(A,896)|0)==512?u|0&&(yMe(u),It(u)):l|0&&It(l)}function yMe(o){o=o|0,o=n[o+4>>2]|0,o|0&&jh(o)}function EMe(o){o=o|0,IMe(o,4933),CMe(o)|0,wMe(o)|0}function IMe(o,l){o=o|0,l=l|0;var u=0;u=YMe()|0,n[o>>2]=u,VMe(u,l),Hh(n[o>>2]|0)}function CMe(o){o=o|0;var l=0;return l=n[o>>2]|0,ag(l,OMe()|0),o|0}function wMe(o){o=o|0;var l=0;return l=n[o>>2]|0,ag(l,BMe()|0),o|0}function BMe(){var o=0;return s[7920]|0||(Zz(10452),dr(58,10452,U|0)|0,o=7920,n[o>>2]=1,n[o+4>>2]=0),_r(10452)|0||Zz(10452),10452}function Zz(o){o=o|0,DMe(o),lg(o,1)}function vMe(o){o=o|0,SMe(o+24|0)}function SMe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function DMe(o){o=o|0;var l=0;l=tn()|0,rn(o,5,1,l,kMe()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function bMe(o,l,u){o=o|0,l=+l,u=+u,PMe(o,l,u)}function PMe(o,l,u){o=o|0,l=+l,u=+u;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+32|0,m=A+8|0,k=A+17|0,g=A,B=A+16|0,Ff(k,l),E[m>>3]=+Nf(k,l),Ff(B,u),E[g>>3]=+Nf(B,u),xMe(o,m,g),I=A}function xMe(o,l,u){o=o|0,l=l|0,u=u|0,$z(o+8|0,+E[l>>3],+E[u>>3]),s[o+24>>0]=1}function $z(o,l,u){o=o|0,l=+l,u=+u,E[o>>3]=l,E[o+8>>3]=u}function kMe(){return 1472}function QMe(o,l){return o=+o,l=+l,RMe(o,l)|0}function RMe(o,l){o=+o,l=+l;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return A=I,I=I+16|0,B=A+4|0,k=A+8|0,R=A,g=_l(8)|0,u=g,m=Kt(16)|0,Ff(B,o),o=+Nf(B,o),Ff(k,l),$z(m,o,+Nf(k,l)),k=u+4|0,n[k>>2]=m,m=Kt(8)|0,k=n[k>>2]|0,n[R>>2]=0,n[B>>2]=n[R>>2],eX(m,k,B),n[g>>2]=m,I=A,u|0}function eX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,u=Kt(16)|0,n[u+4>>2]=0,n[u+8>>2]=0,n[u>>2]=1452,n[u+12>>2]=l,n[o+4>>2]=u}function TMe(o){o=o|0,rE(o),It(o)}function FMe(o){o=o|0,o=n[o+12>>2]|0,o|0&&It(o)}function NMe(o){o=o|0,It(o)}function OMe(){var o=0;return s[7928]|0||(tX(10488),dr(59,10488,U|0)|0,o=7928,n[o>>2]=1,n[o+4>>2]=0),_r(10488)|0||tX(10488),10488}function tX(o){o=o|0,UMe(o),lg(o,60)}function LMe(o){o=o|0,MMe(o+24|0)}function MMe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function UMe(o){o=o|0;var l=0;l=tn()|0,rn(o,5,6,l,GMe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function _Me(o){o=o|0,HMe(o)}function HMe(o){o=o|0,jMe(o)}function jMe(o){o=o|0,rX(o+8|0),s[o+24>>0]=1}function rX(o){o=o|0,n[o>>2]=0,n[o+4>>2]=0,n[o+8>>2]=0,n[o+12>>2]=0}function GMe(){return 1492}function qMe(){return WMe()|0}function WMe(){var o=0,l=0,u=0,A=0,g=0,m=0,B=0;return l=I,I=I+16|0,g=l+4|0,B=l,u=_l(8)|0,o=u,A=Kt(16)|0,rX(A),m=o+4|0,n[m>>2]=A,A=Kt(8)|0,m=n[m>>2]|0,n[B>>2]=0,n[g>>2]=n[B>>2],eX(A,m,g),n[u>>2]=A,I=l,o|0}function YMe(){var o=0;return s[7936]|0||($Me(10524),dr(25,10524,U|0)|0,o=7936,n[o>>2]=1,n[o+4>>2]=0),10524}function VMe(o,l){o=o|0,l=l|0,n[o>>2]=JMe()|0,n[o+4>>2]=KMe()|0,n[o+12>>2]=l,n[o+8>>2]=zMe()|0,n[o+32>>2]=7}function JMe(){return 11700}function KMe(){return 1484}function zMe(){return NP()|0}function XMe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(_h(A,896)|0)==512?u|0&&(ZMe(u),It(u)):l|0&&It(l)}function ZMe(o){o=o|0,o=n[o+4>>2]|0,o|0&&jh(o)}function $Me(o){o=o|0,Oh(o)}function eUe(o,l,u){o=o|0,l=l|0,u=u|0,o=Sn(l)|0,l=tUe(u)|0,u=rUe(u,0)|0,RUe(o,l,u,xM()|0,0)}function tUe(o){return o=o|0,o|0}function rUe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return k=I,I=I+16|0,g=k,m=k+4|0,n[g>>2]=o,R=xM()|0,B=R+24|0,l=yr(l,4)|0,n[m>>2]=l,u=R+28|0,A=n[u>>2]|0,A>>>0<(n[R+32>>2]|0)>>>0?(iX(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(cUe(B,g,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function xM(){var o=0,l=0;if(s[7944]|0||(nX(10568),dr(61,10568,U|0)|0,l=7944,n[l>>2]=1,n[l+4>>2]=0),!(_r(10568)|0)){o=10568,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));nX(10568)}return 10568}function nX(o){o=o|0,sUe(o)}function nUe(o){o=o|0,iUe(o+24|0)}function iUe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function sUe(o){o=o|0;var l=0;l=tn()|0,rn(o,1,17,l,rz()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function oUe(o){return o=o|0,lUe(n[(aUe(o)|0)>>2]|0)|0}function aUe(o){return o=o|0,(n[(xM()|0)+24>>2]|0)+(o<<3)|0}function lUe(o){return o=o|0,TP(VP[o&7]()|0)|0}function iX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function cUe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;if(k=I,I=I+32|0,g=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=uUe(o)|0,A>>>0>>0)an(o);else{R=n[o>>2]|0,L=(n[o+8>>2]|0)-R|0,M=L>>2,fUe(g,L>>3>>>0>>1>>>0?M>>>0>>0?B:M:A,(n[m>>2]|0)-R>>3,o+8|0),B=g+8|0,iX(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,AUe(o,g),pUe(g),I=k;return}}function uUe(o){return o=o|0,536870911}function fUe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{g=Kt(l<<3)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<3)}function AUe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>3)<<3)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function pUe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&It(o)}function hUe(){dUe()}function dUe(){gUe(10604)}function gUe(o){o=o|0,mUe(o,4955)}function mUe(o,l){o=o|0,l=l|0;var u=0;u=yUe()|0,n[o>>2]=u,EUe(u,l),Hh(n[o>>2]|0)}function yUe(){var o=0;return s[7952]|0||(PUe(10612),dr(25,10612,U|0)|0,o=7952,n[o>>2]=1,n[o+4>>2]=0),10612}function EUe(o,l){o=o|0,l=l|0,n[o>>2]=BUe()|0,n[o+4>>2]=vUe()|0,n[o+12>>2]=l,n[o+8>>2]=SUe()|0,n[o+32>>2]=8}function Hh(o){o=o|0;var l=0,u=0;l=I,I=I+16|0,u=l,Xy()|0,n[u>>2]=o,IUe(10608,u),I=l}function Xy(){return s[11714]|0||(n[2652]=0,dr(62,10608,U|0)|0,s[11714]=1),10608}function IUe(o,l){o=o|0,l=l|0;var u=0;u=Kt(8)|0,n[u+4>>2]=n[l>>2],n[u>>2]=n[o>>2],n[o>>2]=u}function CUe(o){o=o|0,wUe(o)}function wUe(o){o=o|0;var l=0,u=0;if(l=n[o>>2]|0,l|0)do u=l,l=n[l>>2]|0,It(u);while(l|0);n[o>>2]=0}function BUe(){return 11715}function vUe(){return 1496}function SUe(){return FP()|0}function DUe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(_h(A,896)|0)==512?u|0&&(bUe(u),It(u)):l|0&&It(l)}function bUe(o){o=o|0,o=n[o+4>>2]|0,o|0&&jh(o)}function PUe(o){o=o|0,Oh(o)}function xUe(o,l){o=o|0,l=l|0;var u=0,A=0;Xy()|0,u=n[2652]|0;e:do if(u|0){for(;A=n[u+4>>2]|0,!(A|0&&!(UX(kM(A)|0,o)|0));)if(u=n[u>>2]|0,!u)break e;kUe(A,l)}while(!1)}function kM(o){return o=o|0,n[o+12>>2]|0}function kUe(o,l){o=o|0,l=l|0;var u=0;o=o+36|0,u=n[o>>2]|0,u|0&&(Pf(u),It(u)),u=Kt(4)|0,DP(u,l),n[o>>2]=u}function QM(){return s[11716]|0||(n[2664]=0,dr(63,10656,U|0)|0,s[11716]=1),10656}function sX(){var o=0;return s[11717]|0?o=n[2665]|0:(QUe(),n[2665]=1504,s[11717]=1,o=1504),o|0}function QUe(){s[11740]|0||(s[11718]=yr(yr(8,0)|0,0)|0,s[11719]=yr(yr(0,0)|0,0)|0,s[11720]=yr(yr(0,16)|0,0)|0,s[11721]=yr(yr(8,0)|0,0)|0,s[11722]=yr(yr(0,0)|0,0)|0,s[11723]=yr(yr(8,0)|0,0)|0,s[11724]=yr(yr(0,0)|0,0)|0,s[11725]=yr(yr(8,0)|0,0)|0,s[11726]=yr(yr(0,0)|0,0)|0,s[11727]=yr(yr(8,0)|0,0)|0,s[11728]=yr(yr(0,0)|0,0)|0,s[11729]=yr(yr(0,0)|0,32)|0,s[11730]=yr(yr(0,0)|0,32)|0,s[11740]=1)}function oX(){return 1572}function RUe(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0;var m=0,B=0,k=0,R=0,M=0,L=0;m=I,I=I+32|0,L=m+16|0,M=m+12|0,R=m+8|0,k=m+4|0,B=m,n[L>>2]=o,n[M>>2]=l,n[R>>2]=u,n[k>>2]=A,n[B>>2]=g,QM()|0,TUe(10656,L,M,R,k,B),I=m}function TUe(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0;var B=0;B=Kt(24)|0,NK(B+4|0,n[l>>2]|0,n[u>>2]|0,n[A>>2]|0,n[g>>2]|0,n[m>>2]|0),n[B>>2]=n[o>>2],n[o>>2]=B}function aX(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0,Le=0,Te=0,nt=0,Ze=0,ft=0;if(ft=I,I=I+32|0,Le=ft+20|0,Te=ft+8|0,nt=ft+4|0,Ze=ft,l=n[l>>2]|0,l|0){Ve=Le+4|0,R=Le+8|0,M=Te+4|0,L=Te+8|0,q=Te+8|0,oe=Le+8|0;do{if(B=l+4|0,k=RM(B)|0,k|0){if(g=I2(k)|0,n[Le>>2]=0,n[Ve>>2]=0,n[R>>2]=0,A=(C2(k)|0)+1|0,FUe(Le,A),A|0)for(;A=A+-1|0,Su(Te,n[g>>2]|0),m=n[Ve>>2]|0,m>>>0<(n[oe>>2]|0)>>>0?(n[m>>2]=n[Te>>2],n[Ve>>2]=(n[Ve>>2]|0)+4):TM(Le,Te),A;)g=g+4|0;A=w2(k)|0,n[Te>>2]=0,n[M>>2]=0,n[L>>2]=0;e:do if(n[A>>2]|0)for(g=0,m=0;;){if((g|0)==(m|0)?NUe(Te,A):(n[g>>2]=n[A>>2],n[M>>2]=(n[M>>2]|0)+4),A=A+4|0,!(n[A>>2]|0))break e;g=n[M>>2]|0,m=n[q>>2]|0}while(!1);n[nt>>2]=OP(B)|0,n[Ze>>2]=_r(k)|0,OUe(u,o,nt,Ze,Le,Te),FM(Te),sp(Le)}l=n[l>>2]|0}while(l|0)}I=ft}function RM(o){return o=o|0,n[o+12>>2]|0}function I2(o){return o=o|0,n[o+12>>2]|0}function C2(o){return o=o|0,n[o+16>>2]|0}function FUe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0;g=I,I=I+32|0,u=g,A=n[o>>2]|0,(n[o+8>>2]|0)-A>>2>>>0>>0&&(dX(u,l,(n[o+4>>2]|0)-A>>2,o+8|0),gX(o,u),mX(u)),I=g}function TM(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0;if(B=I,I=I+32|0,u=B,A=o+4|0,g=((n[A>>2]|0)-(n[o>>2]|0)>>2)+1|0,m=hX(o)|0,m>>>0>>0)an(o);else{k=n[o>>2]|0,M=(n[o+8>>2]|0)-k|0,R=M>>1,dX(u,M>>2>>>0>>1>>>0?R>>>0>>0?g:R:m,(n[A>>2]|0)-k>>2,o+8|0),m=u+8|0,n[n[m>>2]>>2]=n[l>>2],n[m>>2]=(n[m>>2]|0)+4,gX(o,u),mX(u),I=B;return}}function w2(o){return o=o|0,n[o+8>>2]|0}function NUe(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0;if(B=I,I=I+32|0,u=B,A=o+4|0,g=((n[A>>2]|0)-(n[o>>2]|0)>>2)+1|0,m=pX(o)|0,m>>>0>>0)an(o);else{k=n[o>>2]|0,M=(n[o+8>>2]|0)-k|0,R=M>>1,t_e(u,M>>2>>>0>>1>>>0?R>>>0>>0?g:R:m,(n[A>>2]|0)-k>>2,o+8|0),m=u+8|0,n[n[m>>2]>>2]=n[l>>2],n[m>>2]=(n[m>>2]|0)+4,r_e(o,u),n_e(u),I=B;return}}function OP(o){return o=o|0,n[o>>2]|0}function OUe(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0,LUe(o,l,u,A,g,m)}function FM(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-4-A|0)>>>2)<<2)),It(u))}function sp(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-4-A|0)>>>2)<<2)),It(u))}function LUe(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0;var B=0,k=0,R=0,M=0,L=0,q=0;B=I,I=I+48|0,L=B+40|0,k=B+32|0,q=B+24|0,R=B+12|0,M=B,Hl(k),o=Us(o)|0,n[q>>2]=n[l>>2],u=n[u>>2]|0,A=n[A>>2]|0,NM(R,g),MUe(M,m),n[L>>2]=n[q>>2],UUe(o,L,u,A,R,M),FM(M),sp(R),jl(k),I=B}function NM(o,l){o=o|0,l=l|0;var u=0,A=0;n[o>>2]=0,n[o+4>>2]=0,n[o+8>>2]=0,u=l+4|0,A=(n[u>>2]|0)-(n[l>>2]|0)>>2,A|0&&($Ue(o,A),e_e(o,n[l>>2]|0,n[u>>2]|0,A))}function MUe(o,l){o=o|0,l=l|0;var u=0,A=0;n[o>>2]=0,n[o+4>>2]=0,n[o+8>>2]=0,u=l+4|0,A=(n[u>>2]|0)-(n[l>>2]|0)>>2,A|0&&(XUe(o,A),ZUe(o,n[l>>2]|0,n[u>>2]|0,A))}function UUe(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0;var B=0,k=0,R=0,M=0,L=0,q=0;B=I,I=I+32|0,L=B+28|0,q=B+24|0,k=B+12|0,R=B,M=Ca(_Ue()|0)|0,n[q>>2]=n[l>>2],n[L>>2]=n[q>>2],l=cg(L)|0,u=lX(u)|0,A=OM(A)|0,n[k>>2]=n[g>>2],L=g+4|0,n[k+4>>2]=n[L>>2],q=g+8|0,n[k+8>>2]=n[q>>2],n[q>>2]=0,n[L>>2]=0,n[g>>2]=0,g=LM(k)|0,n[R>>2]=n[m>>2],L=m+4|0,n[R+4>>2]=n[L>>2],q=m+8|0,n[R+8>>2]=n[q>>2],n[q>>2]=0,n[L>>2]=0,n[m>>2]=0,au(0,M|0,o|0,l|0,u|0,A|0,g|0,HUe(R)|0)|0,FM(R),sp(k),I=B}function _Ue(){var o=0;return s[7968]|0||(KUe(10708),o=7968,n[o>>2]=1,n[o+4>>2]=0),10708}function cg(o){return o=o|0,uX(o)|0}function lX(o){return o=o|0,cX(o)|0}function OM(o){return o=o|0,TP(o)|0}function LM(o){return o=o|0,GUe(o)|0}function HUe(o){return o=o|0,jUe(o)|0}function jUe(o){o=o|0;var l=0,u=0,A=0;if(A=(n[o+4>>2]|0)-(n[o>>2]|0)|0,u=A>>2,A=_l(A+4|0)|0,n[A>>2]=u,u|0){l=0;do n[A+4+(l<<2)>>2]=cX(n[(n[o>>2]|0)+(l<<2)>>2]|0)|0,l=l+1|0;while((l|0)!=(u|0))}return A|0}function cX(o){return o=o|0,o|0}function GUe(o){o=o|0;var l=0,u=0,A=0;if(A=(n[o+4>>2]|0)-(n[o>>2]|0)|0,u=A>>2,A=_l(A+4|0)|0,n[A>>2]=u,u|0){l=0;do n[A+4+(l<<2)>>2]=uX((n[o>>2]|0)+(l<<2)|0)|0,l=l+1|0;while((l|0)!=(u|0))}return A|0}function uX(o){o=o|0;var l=0,u=0,A=0,g=0;return g=I,I=I+32|0,l=g+12|0,u=g,A=KL(fX()|0)|0,A?(zL(l,A),XL(u,l),S8e(o,u),o=ZL(l)|0):o=qUe(o)|0,I=g,o|0}function fX(){var o=0;return s[7960]|0||(JUe(10664),dr(25,10664,U|0)|0,o=7960,n[o>>2]=1,n[o+4>>2]=0),10664}function qUe(o){o=o|0;var l=0,u=0,A=0,g=0,m=0,B=0,k=0;return u=I,I=I+16|0,g=u+4|0,B=u,A=_l(8)|0,l=A,k=Kt(4)|0,n[k>>2]=n[o>>2],m=l+4|0,n[m>>2]=k,o=Kt(8)|0,m=n[m>>2]|0,n[B>>2]=0,n[g>>2]=n[B>>2],AX(o,m,g),n[A>>2]=o,I=u,l|0}function AX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,u=Kt(16)|0,n[u+4>>2]=0,n[u+8>>2]=0,n[u>>2]=1656,n[u+12>>2]=l,n[o+4>>2]=u}function WUe(o){o=o|0,rE(o),It(o)}function YUe(o){o=o|0,o=n[o+12>>2]|0,o|0&&It(o)}function VUe(o){o=o|0,It(o)}function JUe(o){o=o|0,Oh(o)}function KUe(o){o=o|0,Lo(o,zUe()|0,5)}function zUe(){return 1676}function XUe(o,l){o=o|0,l=l|0;var u=0;if((pX(o)|0)>>>0>>0&&an(o),l>>>0>1073741823)Nt();else{u=Kt(l<<2)|0,n[o+4>>2]=u,n[o>>2]=u,n[o+8>>2]=u+(l<<2);return}}function ZUe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,A=o+4|0,o=u-l|0,(o|0)>0&&(Rr(n[A>>2]|0,l|0,o|0)|0,n[A>>2]=(n[A>>2]|0)+(o>>>2<<2))}function pX(o){return o=o|0,1073741823}function $Ue(o,l){o=o|0,l=l|0;var u=0;if((hX(o)|0)>>>0>>0&&an(o),l>>>0>1073741823)Nt();else{u=Kt(l<<2)|0,n[o+4>>2]=u,n[o>>2]=u,n[o+8>>2]=u+(l<<2);return}}function e_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,A=o+4|0,o=u-l|0,(o|0)>0&&(Rr(n[A>>2]|0,l|0,o|0)|0,n[A>>2]=(n[A>>2]|0)+(o>>>2<<2))}function hX(o){return o=o|0,1073741823}function t_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>1073741823)Nt();else{g=Kt(l<<2)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<2)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<2)}function r_e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>2)<<2)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function n_e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-4-l|0)>>>2)<<2)),o=n[o>>2]|0,o|0&&It(o)}function dX(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>1073741823)Nt();else{g=Kt(l<<2)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<2)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<2)}function gX(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>2)<<2)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function mX(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-4-l|0)>>>2)<<2)),o=n[o>>2]|0,o|0&&It(o)}function i_e(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0;var m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0,Le=0,Te=0;if(Te=I,I=I+32|0,L=Te+20|0,q=Te+12|0,M=Te+16|0,oe=Te+4|0,Ve=Te,Le=Te+8|0,k=sX()|0,m=n[k>>2]|0,B=n[m>>2]|0,B|0)for(R=n[k+8>>2]|0,k=n[k+4>>2]|0;Su(L,B),s_e(o,L,k,R),m=m+4|0,B=n[m>>2]|0,B;)R=R+1|0,k=k+1|0;if(m=oX()|0,B=n[m>>2]|0,B|0)do Su(L,B),n[q>>2]=n[m+4>>2],o_e(l,L,q),m=m+8|0,B=n[m>>2]|0;while(B|0);if(m=n[(Xy()|0)>>2]|0,m|0)do l=n[m+4>>2]|0,Su(L,n[(Zy(l)|0)>>2]|0),n[q>>2]=kM(l)|0,a_e(u,L,q),m=n[m>>2]|0;while(m|0);if(Su(M,0),m=QM()|0,n[L>>2]=n[M>>2],aX(L,m,g),m=n[(Xy()|0)>>2]|0,m|0){o=L+4|0,l=L+8|0,u=L+8|0;do{if(R=n[m+4>>2]|0,Su(q,n[(Zy(R)|0)>>2]|0),l_e(oe,yX(R)|0),B=n[oe>>2]|0,B|0){n[L>>2]=0,n[o>>2]=0,n[l>>2]=0;do Su(Ve,n[(Zy(n[B+4>>2]|0)|0)>>2]|0),k=n[o>>2]|0,k>>>0<(n[u>>2]|0)>>>0?(n[k>>2]=n[Ve>>2],n[o>>2]=(n[o>>2]|0)+4):TM(L,Ve),B=n[B>>2]|0;while(B|0);c_e(A,q,L),sp(L)}n[Le>>2]=n[q>>2],M=EX(R)|0,n[L>>2]=n[Le>>2],aX(L,M,g),HK(oe),m=n[m>>2]|0}while(m|0)}I=Te}function s_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,C_e(o,l,u,A)}function o_e(o,l,u){o=o|0,l=l|0,u=u|0,I_e(o,l,u)}function Zy(o){return o=o|0,o|0}function a_e(o,l,u){o=o|0,l=l|0,u=u|0,g_e(o,l,u)}function yX(o){return o=o|0,o+16|0}function l_e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;if(m=I,I=I+16|0,g=m+8|0,u=m,n[o>>2]=0,A=n[l>>2]|0,n[g>>2]=A,n[u>>2]=o,u=d_e(u)|0,A|0){if(A=Kt(12)|0,B=(IX(g)|0)+4|0,o=n[B+4>>2]|0,l=A+4|0,n[l>>2]=n[B>>2],n[l+4>>2]=o,l=n[n[g>>2]>>2]|0,n[g>>2]=l,!l)o=A;else for(l=A;o=Kt(12)|0,R=(IX(g)|0)+4|0,k=n[R+4>>2]|0,B=o+4|0,n[B>>2]=n[R>>2],n[B+4>>2]=k,n[l>>2]=o,B=n[n[g>>2]>>2]|0,n[g>>2]=B,B;)l=o;n[o>>2]=n[u>>2],n[u>>2]=A}I=m}function c_e(o,l,u){o=o|0,l=l|0,u=u|0,u_e(o,l,u)}function EX(o){return o=o|0,o+24|0}function u_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+32|0,B=A+24|0,g=A+16|0,k=A+12|0,m=A,Hl(g),o=Us(o)|0,n[k>>2]=n[l>>2],NM(m,u),n[B>>2]=n[k>>2],f_e(o,B,m),sp(m),jl(g),I=A}function f_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=I,I=I+32|0,B=A+16|0,k=A+12|0,g=A,m=Ca(A_e()|0)|0,n[k>>2]=n[l>>2],n[B>>2]=n[k>>2],l=cg(B)|0,n[g>>2]=n[u>>2],B=u+4|0,n[g+4>>2]=n[B>>2],k=u+8|0,n[g+8>>2]=n[k>>2],n[k>>2]=0,n[B>>2]=0,n[u>>2]=0,Ns(0,m|0,o|0,l|0,LM(g)|0)|0,sp(g),I=A}function A_e(){var o=0;return s[7976]|0||(p_e(10720),o=7976,n[o>>2]=1,n[o+4>>2]=0),10720}function p_e(o){o=o|0,Lo(o,h_e()|0,2)}function h_e(){return 1732}function d_e(o){return o=o|0,n[o>>2]|0}function IX(o){return o=o|0,n[o>>2]|0}function g_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;A=I,I=I+32|0,m=A+16|0,g=A+8|0,B=A,Hl(g),o=Us(o)|0,n[B>>2]=n[l>>2],u=n[u>>2]|0,n[m>>2]=n[B>>2],CX(o,m,u),jl(g),I=A}function CX(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;A=I,I=I+16|0,m=A+4|0,B=A,g=Ca(m_e()|0)|0,n[B>>2]=n[l>>2],n[m>>2]=n[B>>2],l=cg(m)|0,Ns(0,g|0,o|0,l|0,lX(u)|0)|0,I=A}function m_e(){var o=0;return s[7984]|0||(y_e(10732),o=7984,n[o>>2]=1,n[o+4>>2]=0),10732}function y_e(o){o=o|0,Lo(o,E_e()|0,2)}function E_e(){return 1744}function I_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;A=I,I=I+32|0,m=A+16|0,g=A+8|0,B=A,Hl(g),o=Us(o)|0,n[B>>2]=n[l>>2],u=n[u>>2]|0,n[m>>2]=n[B>>2],CX(o,m,u),jl(g),I=A}function C_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0;g=I,I=I+32|0,B=g+16|0,m=g+8|0,k=g,Hl(m),o=Us(o)|0,n[k>>2]=n[l>>2],u=s[u>>0]|0,A=s[A>>0]|0,n[B>>2]=n[k>>2],w_e(o,B,u,A),jl(m),I=g}function w_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0;g=I,I=I+16|0,B=g+4|0,k=g,m=Ca(B_e()|0)|0,n[k>>2]=n[l>>2],n[B>>2]=n[k>>2],l=cg(B)|0,u=$y(u)|0,Li(0,m|0,o|0,l|0,u|0,$y(A)|0)|0,I=g}function B_e(){var o=0;return s[7992]|0||(S_e(10744),o=7992,n[o>>2]=1,n[o+4>>2]=0),10744}function $y(o){return o=o|0,v_e(o)|0}function v_e(o){return o=o|0,o&255|0}function S_e(o){o=o|0,Lo(o,D_e()|0,3)}function D_e(){return 1756}function b_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;switch(oe=I,I=I+32|0,k=oe+8|0,R=oe+4|0,M=oe+20|0,L=oe,rM(o,0),A=v8e(l)|0,n[k>>2]=0,q=k+4|0,n[q>>2]=0,n[k+8>>2]=0,A<<24>>24){case 0:{s[M>>0]=0,P_e(R,u,M),LP(o,R)|0,xf(R);break}case 8:{q=GM(l)|0,s[M>>0]=8,Su(L,n[q+4>>2]|0),x_e(R,u,M,L,q+8|0),LP(o,R)|0,xf(R);break}case 9:{if(m=GM(l)|0,l=n[m+4>>2]|0,l|0)for(B=k+8|0,g=m+12|0;l=l+-1|0,Su(R,n[g>>2]|0),A=n[q>>2]|0,A>>>0<(n[B>>2]|0)>>>0?(n[A>>2]=n[R>>2],n[q>>2]=(n[q>>2]|0)+4):TM(k,R),l;)g=g+4|0;s[M>>0]=9,Su(L,n[m+8>>2]|0),k_e(R,u,M,L,k),LP(o,R)|0,xf(R);break}default:q=GM(l)|0,s[M>>0]=A,Su(L,n[q+4>>2]|0),Q_e(R,u,M,L),LP(o,R)|0,xf(R)}sp(k),I=oe}function P_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0;A=I,I=I+16|0,g=A,Hl(g),l=Us(l)|0,q_e(o,l,s[u>>0]|0),jl(g),I=A}function LP(o,l){o=o|0,l=l|0;var u=0;return u=n[o>>2]|0,u|0&&qa(u|0),n[o>>2]=n[l>>2],n[l>>2]=0,o|0}function x_e(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0;var m=0,B=0,k=0,R=0;m=I,I=I+32|0,k=m+16|0,B=m+8|0,R=m,Hl(B),l=Us(l)|0,u=s[u>>0]|0,n[R>>2]=n[A>>2],g=n[g>>2]|0,n[k>>2]=n[R>>2],__e(o,l,u,k,g),jl(B),I=m}function k_e(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0;var m=0,B=0,k=0,R=0,M=0;m=I,I=I+32|0,R=m+24|0,B=m+16|0,M=m+12|0,k=m,Hl(B),l=Us(l)|0,u=s[u>>0]|0,n[M>>2]=n[A>>2],NM(k,g),n[R>>2]=n[M>>2],O_e(o,l,u,R,k),sp(k),jl(B),I=m}function Q_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0;g=I,I=I+32|0,B=g+16|0,m=g+8|0,k=g,Hl(m),l=Us(l)|0,u=s[u>>0]|0,n[k>>2]=n[A>>2],n[B>>2]=n[k>>2],R_e(o,l,u,B),jl(m),I=g}function R_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0,B=0,k=0;g=I,I=I+16|0,m=g+4|0,k=g,B=Ca(T_e()|0)|0,u=$y(u)|0,n[k>>2]=n[A>>2],n[m>>2]=n[k>>2],MP(o,Ns(0,B|0,l|0,u|0,cg(m)|0)|0),I=g}function T_e(){var o=0;return s[8e3]|0||(F_e(10756),o=8e3,n[o>>2]=1,n[o+4>>2]=0),10756}function MP(o,l){o=o|0,l=l|0,rM(o,l)}function F_e(o){o=o|0,Lo(o,N_e()|0,2)}function N_e(){return 1772}function O_e(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0;var m=0,B=0,k=0,R=0,M=0;m=I,I=I+32|0,R=m+16|0,M=m+12|0,B=m,k=Ca(L_e()|0)|0,u=$y(u)|0,n[M>>2]=n[A>>2],n[R>>2]=n[M>>2],A=cg(R)|0,n[B>>2]=n[g>>2],R=g+4|0,n[B+4>>2]=n[R>>2],M=g+8|0,n[B+8>>2]=n[M>>2],n[M>>2]=0,n[R>>2]=0,n[g>>2]=0,MP(o,Li(0,k|0,l|0,u|0,A|0,LM(B)|0)|0),sp(B),I=m}function L_e(){var o=0;return s[8008]|0||(M_e(10768),o=8008,n[o>>2]=1,n[o+4>>2]=0),10768}function M_e(o){o=o|0,Lo(o,U_e()|0,3)}function U_e(){return 1784}function __e(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0;var m=0,B=0,k=0,R=0;m=I,I=I+16|0,k=m+4|0,R=m,B=Ca(H_e()|0)|0,u=$y(u)|0,n[R>>2]=n[A>>2],n[k>>2]=n[R>>2],A=cg(k)|0,MP(o,Li(0,B|0,l|0,u|0,A|0,OM(g)|0)|0),I=m}function H_e(){var o=0;return s[8016]|0||(j_e(10780),o=8016,n[o>>2]=1,n[o+4>>2]=0),10780}function j_e(o){o=o|0,Lo(o,G_e()|0,3)}function G_e(){return 1800}function q_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=Ca(W_e()|0)|0,MP(o,mn(0,A|0,l|0,$y(u)|0)|0)}function W_e(){var o=0;return s[8024]|0||(Y_e(10792),o=8024,n[o>>2]=1,n[o+4>>2]=0),10792}function Y_e(o){o=o|0,Lo(o,V_e()|0,1)}function V_e(){return 1816}function J_e(){K_e(),z_e(),X_e()}function K_e(){n[2702]=KX(65536)|0}function z_e(){m4e(10856)}function X_e(){Z_e(10816)}function Z_e(o){o=o|0,$_e(o,5044),e4e(o)|0}function $_e(o,l){o=o|0,l=l|0;var u=0;u=fX()|0,n[o>>2]=u,f4e(u,l),Hh(n[o>>2]|0)}function e4e(o){o=o|0;var l=0;return l=n[o>>2]|0,ag(l,t4e()|0),o|0}function t4e(){var o=0;return s[8032]|0||(wX(10820),dr(64,10820,U|0)|0,o=8032,n[o>>2]=1,n[o+4>>2]=0),_r(10820)|0||wX(10820),10820}function wX(o){o=o|0,i4e(o),lg(o,25)}function r4e(o){o=o|0,n4e(o+24|0)}function n4e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function i4e(o){o=o|0;var l=0;l=tn()|0,rn(o,5,18,l,l4e()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function s4e(o,l){o=o|0,l=l|0,o4e(o,l)}function o4e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0;u=I,I=I+16|0,A=u,g=u+4|0,sg(g,l),n[A>>2]=og(g,l)|0,a4e(o,A),I=u}function a4e(o,l){o=o|0,l=l|0,BX(o+4|0,n[l>>2]|0),s[o+8>>0]=1}function BX(o,l){o=o|0,l=l|0,n[o>>2]=l}function l4e(){return 1824}function c4e(o){return o=o|0,u4e(o)|0}function u4e(o){o=o|0;var l=0,u=0,A=0,g=0,m=0,B=0,k=0;return u=I,I=I+16|0,g=u+4|0,B=u,A=_l(8)|0,l=A,k=Kt(4)|0,sg(g,o),BX(k,og(g,o)|0),m=l+4|0,n[m>>2]=k,o=Kt(8)|0,m=n[m>>2]|0,n[B>>2]=0,n[g>>2]=n[B>>2],AX(o,m,g),n[A>>2]=o,I=u,l|0}function _l(o){o=o|0;var l=0,u=0;return o=o+7&-8,o>>>0<=32768&&(l=n[2701]|0,o>>>0<=(65536-l|0)>>>0)?(u=(n[2702]|0)+l|0,n[2701]=l+o,o=u):(o=KX(o+8|0)|0,n[o>>2]=n[2703],n[2703]=o,o=o+8|0),o|0}function f4e(o,l){o=o|0,l=l|0,n[o>>2]=A4e()|0,n[o+4>>2]=p4e()|0,n[o+12>>2]=l,n[o+8>>2]=h4e()|0,n[o+32>>2]=9}function A4e(){return 11744}function p4e(){return 1832}function h4e(){return NP()|0}function d4e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(_h(A,896)|0)==512?u|0&&(g4e(u),It(u)):l|0&&It(l)}function g4e(o){o=o|0,o=n[o+4>>2]|0,o|0&&jh(o)}function m4e(o){o=o|0,y4e(o,5052),E4e(o)|0,I4e(o,5058,26)|0,C4e(o,5069,1)|0,w4e(o,5077,10)|0,B4e(o,5087,19)|0,v4e(o,5094,27)|0}function y4e(o,l){o=o|0,l=l|0;var u=0;u=g8e()|0,n[o>>2]=u,m8e(u,l),Hh(n[o>>2]|0)}function E4e(o){o=o|0;var l=0;return l=n[o>>2]|0,ag(l,r8e()|0),o|0}function I4e(o,l,u){return o=o|0,l=l|0,u=u|0,M3e(o,Sn(l)|0,u,0),o|0}function C4e(o,l,u){return o=o|0,l=l|0,u=u|0,B3e(o,Sn(l)|0,u,0),o|0}function w4e(o,l,u){return o=o|0,l=l|0,u=u|0,t3e(o,Sn(l)|0,u,0),o|0}function B4e(o,l,u){return o=o|0,l=l|0,u=u|0,_4e(o,Sn(l)|0,u,0),o|0}function vX(o,l){o=o|0,l=l|0;var u=0,A=0;e:for(;;){for(u=n[2703]|0;;){if((u|0)==(l|0))break e;if(A=n[u>>2]|0,n[2703]=A,!u)u=A;else break}It(u)}n[2701]=o}function v4e(o,l,u){return o=o|0,l=l|0,u=u|0,S4e(o,Sn(l)|0,u,0),o|0}function S4e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;m=n[o>>2]|0,g=MM()|0,o=D4e(u)|0,Dn(m,l,g,o,b4e(u,A)|0,A)}function MM(){var o=0,l=0;if(s[8040]|0||(DX(10860),dr(65,10860,U|0)|0,l=8040,n[l>>2]=1,n[l+4>>2]=0),!(_r(10860)|0)){o=10860,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));DX(10860)}return 10860}function D4e(o){return o=o|0,o|0}function b4e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return k=I,I=I+16|0,g=k,m=k+4|0,n[g>>2]=o,R=MM()|0,B=R+24|0,l=yr(l,4)|0,n[m>>2]=l,u=R+28|0,A=n[u>>2]|0,A>>>0<(n[R+32>>2]|0)>>>0?(SX(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(P4e(B,g,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function SX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function P4e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;if(k=I,I=I+32|0,g=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=x4e(o)|0,A>>>0>>0)an(o);else{R=n[o>>2]|0,L=(n[o+8>>2]|0)-R|0,M=L>>2,k4e(g,L>>3>>>0>>1>>>0?M>>>0>>0?B:M:A,(n[m>>2]|0)-R>>3,o+8|0),B=g+8|0,SX(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,Q4e(o,g),R4e(g),I=k;return}}function x4e(o){return o=o|0,536870911}function k4e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{g=Kt(l<<3)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<3)}function Q4e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>3)<<3)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function R4e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&It(o)}function DX(o){o=o|0,N4e(o)}function T4e(o){o=o|0,F4e(o+24|0)}function F4e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function N4e(o){o=o|0;var l=0;l=tn()|0,rn(o,1,11,l,O4e()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function O4e(){return 1840}function L4e(o,l,u){o=o|0,l=l|0,u=u|0,U4e(n[(M4e(o)|0)>>2]|0,l,u)}function M4e(o){return o=o|0,(n[(MM()|0)+24>>2]|0)+(o<<3)|0}function U4e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0;A=I,I=I+16|0,m=A+1|0,g=A,sg(m,l),l=og(m,l)|0,sg(g,u),u=og(g,u)|0,ap[o&31](l,u),I=A}function _4e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;m=n[o>>2]|0,g=UM()|0,o=H4e(u)|0,Dn(m,l,g,o,j4e(u,A)|0,A)}function UM(){var o=0,l=0;if(s[8048]|0||(PX(10896),dr(66,10896,U|0)|0,l=8048,n[l>>2]=1,n[l+4>>2]=0),!(_r(10896)|0)){o=10896,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));PX(10896)}return 10896}function H4e(o){return o=o|0,o|0}function j4e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return k=I,I=I+16|0,g=k,m=k+4|0,n[g>>2]=o,R=UM()|0,B=R+24|0,l=yr(l,4)|0,n[m>>2]=l,u=R+28|0,A=n[u>>2]|0,A>>>0<(n[R+32>>2]|0)>>>0?(bX(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(G4e(B,g,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function bX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function G4e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;if(k=I,I=I+32|0,g=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=q4e(o)|0,A>>>0>>0)an(o);else{R=n[o>>2]|0,L=(n[o+8>>2]|0)-R|0,M=L>>2,W4e(g,L>>3>>>0>>1>>>0?M>>>0>>0?B:M:A,(n[m>>2]|0)-R>>3,o+8|0),B=g+8|0,bX(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,Y4e(o,g),V4e(g),I=k;return}}function q4e(o){return o=o|0,536870911}function W4e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{g=Kt(l<<3)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<3)}function Y4e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>3)<<3)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function V4e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&It(o)}function PX(o){o=o|0,z4e(o)}function J4e(o){o=o|0,K4e(o+24|0)}function K4e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function z4e(o){o=o|0;var l=0;l=tn()|0,rn(o,1,11,l,X4e()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function X4e(){return 1852}function Z4e(o,l){return o=o|0,l=l|0,e3e(n[($4e(o)|0)>>2]|0,l)|0}function $4e(o){return o=o|0,(n[(UM()|0)+24>>2]|0)+(o<<3)|0}function e3e(o,l){o=o|0,l=l|0;var u=0,A=0;return u=I,I=I+16|0,A=u,sg(A,l),l=og(A,l)|0,l=TP(pg[o&31](l)|0)|0,I=u,l|0}function t3e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;m=n[o>>2]|0,g=_M()|0,o=r3e(u)|0,Dn(m,l,g,o,n3e(u,A)|0,A)}function _M(){var o=0,l=0;if(s[8056]|0||(kX(10932),dr(67,10932,U|0)|0,l=8056,n[l>>2]=1,n[l+4>>2]=0),!(_r(10932)|0)){o=10932,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));kX(10932)}return 10932}function r3e(o){return o=o|0,o|0}function n3e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return k=I,I=I+16|0,g=k,m=k+4|0,n[g>>2]=o,R=_M()|0,B=R+24|0,l=yr(l,4)|0,n[m>>2]=l,u=R+28|0,A=n[u>>2]|0,A>>>0<(n[R+32>>2]|0)>>>0?(xX(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(i3e(B,g,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function xX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function i3e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;if(k=I,I=I+32|0,g=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=s3e(o)|0,A>>>0>>0)an(o);else{R=n[o>>2]|0,L=(n[o+8>>2]|0)-R|0,M=L>>2,o3e(g,L>>3>>>0>>1>>>0?M>>>0>>0?B:M:A,(n[m>>2]|0)-R>>3,o+8|0),B=g+8|0,xX(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,a3e(o,g),l3e(g),I=k;return}}function s3e(o){return o=o|0,536870911}function o3e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{g=Kt(l<<3)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<3)}function a3e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>3)<<3)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function l3e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&It(o)}function kX(o){o=o|0,f3e(o)}function c3e(o){o=o|0,u3e(o+24|0)}function u3e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function f3e(o){o=o|0;var l=0;l=tn()|0,rn(o,1,7,l,A3e()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function A3e(){return 1860}function p3e(o,l,u){return o=o|0,l=l|0,u=u|0,d3e(n[(h3e(o)|0)>>2]|0,l,u)|0}function h3e(o){return o=o|0,(n[(_M()|0)+24>>2]|0)+(o<<3)|0}function d3e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0;return A=I,I=I+32|0,B=A+12|0,m=A+8|0,k=A,R=A+16|0,g=A+4|0,g3e(R,l),m3e(k,R,l),Lh(g,u),u=Mh(g,u)|0,n[B>>2]=n[k>>2],D2[o&15](m,B,u),u=y3e(m)|0,xf(m),Uh(g),I=A,u|0}function g3e(o,l){o=o|0,l=l|0}function m3e(o,l,u){o=o|0,l=l|0,u=u|0,E3e(o,u)}function y3e(o){return o=o|0,Us(o)|0}function E3e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0;g=I,I=I+16|0,u=g,A=l,A&1?(I3e(u,0),Ga(A|0,u|0)|0,C3e(o,u),w3e(u)):n[o>>2]=n[l>>2],I=g}function I3e(o,l){o=o|0,l=l|0,Bu(o,l),n[o+4>>2]=0,s[o+8>>0]=0}function C3e(o,l){o=o|0,l=l|0,n[o>>2]=n[l+4>>2]}function w3e(o){o=o|0,s[o+8>>0]=0}function B3e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;m=n[o>>2]|0,g=HM()|0,o=v3e(u)|0,Dn(m,l,g,o,S3e(u,A)|0,A)}function HM(){var o=0,l=0;if(s[8064]|0||(RX(10968),dr(68,10968,U|0)|0,l=8064,n[l>>2]=1,n[l+4>>2]=0),!(_r(10968)|0)){o=10968,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));RX(10968)}return 10968}function v3e(o){return o=o|0,o|0}function S3e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return k=I,I=I+16|0,g=k,m=k+4|0,n[g>>2]=o,R=HM()|0,B=R+24|0,l=yr(l,4)|0,n[m>>2]=l,u=R+28|0,A=n[u>>2]|0,A>>>0<(n[R+32>>2]|0)>>>0?(QX(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(D3e(B,g,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function QX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function D3e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;if(k=I,I=I+32|0,g=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=b3e(o)|0,A>>>0>>0)an(o);else{R=n[o>>2]|0,L=(n[o+8>>2]|0)-R|0,M=L>>2,P3e(g,L>>3>>>0>>1>>>0?M>>>0>>0?B:M:A,(n[m>>2]|0)-R>>3,o+8|0),B=g+8|0,QX(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,x3e(o,g),k3e(g),I=k;return}}function b3e(o){return o=o|0,536870911}function P3e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{g=Kt(l<<3)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<3)}function x3e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>3)<<3)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function k3e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&It(o)}function RX(o){o=o|0,T3e(o)}function Q3e(o){o=o|0,R3e(o+24|0)}function R3e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function T3e(o){o=o|0;var l=0;l=tn()|0,rn(o,1,1,l,F3e()|0,5),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function F3e(){return 1872}function N3e(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0,L3e(n[(O3e(o)|0)>>2]|0,l,u,A,g,m)}function O3e(o){return o=o|0,(n[(HM()|0)+24>>2]|0)+(o<<3)|0}function L3e(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0;var B=0,k=0,R=0,M=0,L=0,q=0;B=I,I=I+32|0,k=B+16|0,R=B+12|0,M=B+8|0,L=B+4|0,q=B,Lh(k,l),l=Mh(k,l)|0,Lh(R,u),u=Mh(R,u)|0,Lh(M,A),A=Mh(M,A)|0,Lh(L,g),g=Mh(L,g)|0,Lh(q,m),m=Mh(q,m)|0,eZ[o&1](l,u,A,g,m),Uh(q),Uh(L),Uh(M),Uh(R),Uh(k),I=B}function M3e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;m=n[o>>2]|0,g=jM()|0,o=U3e(u)|0,Dn(m,l,g,o,_3e(u,A)|0,A)}function jM(){var o=0,l=0;if(s[8072]|0||(FX(11004),dr(69,11004,U|0)|0,l=8072,n[l>>2]=1,n[l+4>>2]=0),!(_r(11004)|0)){o=11004,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));FX(11004)}return 11004}function U3e(o){return o=o|0,o|0}function _3e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0,k=0,R=0;return k=I,I=I+16|0,g=k,m=k+4|0,n[g>>2]=o,R=jM()|0,B=R+24|0,l=yr(l,4)|0,n[m>>2]=l,u=R+28|0,A=n[u>>2]|0,A>>>0<(n[R+32>>2]|0)>>>0?(TX(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(H3e(B,g,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function TX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function H3e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0;if(k=I,I=I+32|0,g=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=j3e(o)|0,A>>>0>>0)an(o);else{R=n[o>>2]|0,L=(n[o+8>>2]|0)-R|0,M=L>>2,G3e(g,L>>3>>>0>>1>>>0?M>>>0>>0?B:M:A,(n[m>>2]|0)-R>>3,o+8|0),B=g+8|0,TX(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,q3e(o,g),W3e(g),I=k;return}}function j3e(o){return o=o|0,536870911}function G3e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{g=Kt(l<<3)|0;break}else g=0;while(!1);n[o>>2]=g,A=g+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=g+(l<<3)}function q3e(o,l){o=o|0,l=l|0;var u=0,A=0,g=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,g=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(g>>3)<<3)|0,n[m>>2]=u,(g|0)>0?(Rr(u|0,A|0,g|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,g=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=g,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function W3e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&It(o)}function FX(o){o=o|0,J3e(o)}function Y3e(o){o=o|0,V3e(o+24|0)}function V3e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function J3e(o){o=o|0;var l=0;l=tn()|0,rn(o,1,12,l,K3e()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function K3e(){return 1896}function z3e(o,l,u){o=o|0,l=l|0,u=u|0,Z3e(n[(X3e(o)|0)>>2]|0,l,u)}function X3e(o){return o=o|0,(n[(jM()|0)+24>>2]|0)+(o<<3)|0}function Z3e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0;A=I,I=I+16|0,m=A+4|0,g=A,$3e(m,l),l=e8e(m,l)|0,Lh(g,u),u=Mh(g,u)|0,ap[o&31](l,u),Uh(g),I=A}function $3e(o,l){o=o|0,l=l|0}function e8e(o,l){return o=o|0,l=l|0,t8e(l)|0}function t8e(o){return o=o|0,o|0}function r8e(){var o=0;return s[8080]|0||(NX(11040),dr(70,11040,U|0)|0,o=8080,n[o>>2]=1,n[o+4>>2]=0),_r(11040)|0||NX(11040),11040}function NX(o){o=o|0,s8e(o),lg(o,71)}function n8e(o){o=o|0,i8e(o+24|0)}function i8e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),It(u))}function s8e(o){o=o|0;var l=0;l=tn()|0,rn(o,5,7,l,c8e()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function o8e(o){o=o|0,a8e(o)}function a8e(o){o=o|0,l8e(o)}function l8e(o){o=o|0,s[o+8>>0]=1}function c8e(){return 1936}function u8e(){return f8e()|0}function f8e(){var o=0,l=0,u=0,A=0,g=0,m=0,B=0;return l=I,I=I+16|0,g=l+4|0,B=l,u=_l(8)|0,o=u,m=o+4|0,n[m>>2]=Kt(1)|0,A=Kt(8)|0,m=n[m>>2]|0,n[B>>2]=0,n[g>>2]=n[B>>2],A8e(A,m,g),n[u>>2]=A,I=l,o|0}function A8e(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,u=Kt(16)|0,n[u+4>>2]=0,n[u+8>>2]=0,n[u>>2]=1916,n[u+12>>2]=l,n[o+4>>2]=u}function p8e(o){o=o|0,rE(o),It(o)}function h8e(o){o=o|0,o=n[o+12>>2]|0,o|0&&It(o)}function d8e(o){o=o|0,It(o)}function g8e(){var o=0;return s[8088]|0||(B8e(11076),dr(25,11076,U|0)|0,o=8088,n[o>>2]=1,n[o+4>>2]=0),11076}function m8e(o,l){o=o|0,l=l|0,n[o>>2]=y8e()|0,n[o+4>>2]=E8e()|0,n[o+12>>2]=l,n[o+8>>2]=I8e()|0,n[o+32>>2]=10}function y8e(){return 11745}function E8e(){return 1940}function I8e(){return FP()|0}function C8e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(_h(A,896)|0)==512?u|0&&(w8e(u),It(u)):l|0&&It(l)}function w8e(o){o=o|0,o=n[o+4>>2]|0,o|0&&jh(o)}function B8e(o){o=o|0,Oh(o)}function Su(o,l){o=o|0,l=l|0,n[o>>2]=l}function GM(o){return o=o|0,n[o>>2]|0}function v8e(o){return o=o|0,s[n[o>>2]>>0]|0}function S8e(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,n[A>>2]=n[o>>2],D8e(l,A)|0,I=u}function D8e(o,l){o=o|0,l=l|0;var u=0;return u=b8e(n[o>>2]|0,l)|0,l=o+4|0,n[(n[l>>2]|0)+8>>2]=u,n[(n[l>>2]|0)+8>>2]|0}function b8e(o,l){o=o|0,l=l|0;var u=0,A=0;return u=I,I=I+16|0,A=u,Hl(A),o=Us(o)|0,l=P8e(o,n[l>>2]|0)|0,jl(A),I=u,l|0}function Hl(o){o=o|0,n[o>>2]=n[2701],n[o+4>>2]=n[2703]}function P8e(o,l){o=o|0,l=l|0;var u=0;return u=Ca(x8e()|0)|0,mn(0,u|0,o|0,OM(l)|0)|0}function jl(o){o=o|0,vX(n[o>>2]|0,n[o+4>>2]|0)}function x8e(){var o=0;return s[8096]|0||(k8e(11120),o=8096,n[o>>2]=1,n[o+4>>2]=0),11120}function k8e(o){o=o|0,Lo(o,Q8e()|0,1)}function Q8e(){return 1948}function R8e(){T8e()}function T8e(){var o=0,l=0,u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0,Le=0,Te=0;if(Le=I,I=I+16|0,L=Le+4|0,q=Le,fa(65536,10804,n[2702]|0,10812),u=sX()|0,l=n[u>>2]|0,o=n[l>>2]|0,o|0)for(A=n[u+8>>2]|0,u=n[u+4>>2]|0;gf(o|0,c[u>>0]|0|0,s[A>>0]|0),l=l+4|0,o=n[l>>2]|0,o;)A=A+1|0,u=u+1|0;if(o=oX()|0,l=n[o>>2]|0,l|0)do LA(l|0,n[o+4>>2]|0),o=o+8|0,l=n[o>>2]|0;while(l|0);LA(F8e()|0,5167),M=Xy()|0,o=n[M>>2]|0;e:do if(o|0){do N8e(n[o+4>>2]|0),o=n[o>>2]|0;while(o|0);if(o=n[M>>2]|0,o|0){R=M;do{for(;g=o,o=n[o>>2]|0,g=n[g+4>>2]|0,!!(O8e(g)|0);)if(n[q>>2]=R,n[L>>2]=n[q>>2],L8e(M,L)|0,!o)break e;if(M8e(g),R=n[R>>2]|0,l=OX(g)|0,m=Oi()|0,B=I,I=I+((1*(l<<2)|0)+15&-16)|0,k=I,I=I+((1*(l<<2)|0)+15&-16)|0,l=n[(yX(g)|0)>>2]|0,l|0)for(u=B,A=k;n[u>>2]=n[(Zy(n[l+4>>2]|0)|0)>>2],n[A>>2]=n[l+8>>2],l=n[l>>2]|0,l;)u=u+4|0,A=A+4|0;Te=Zy(g)|0,l=U8e(g)|0,u=OX(g)|0,A=_8e(g)|0,lc(Te|0,l|0,B|0,k|0,u|0,A|0,kM(g)|0),OA(m|0)}while(o|0)}}while(!1);if(o=n[(QM()|0)>>2]|0,o|0)do Te=o+4|0,M=RM(Te)|0,g=w2(M)|0,m=I2(M)|0,B=(C2(M)|0)+1|0,k=UP(M)|0,R=LX(Te)|0,M=_r(M)|0,L=OP(Te)|0,q=qM(Te)|0,lu(0,g|0,m|0,B|0,k|0,R|0,M|0,L|0,q|0,WM(Te)|0),o=n[o>>2]|0;while(o|0);o=n[(Xy()|0)>>2]|0;e:do if(o|0){t:for(;;){if(l=n[o+4>>2]|0,l|0&&(oe=n[(Zy(l)|0)>>2]|0,Ve=n[(EX(l)|0)>>2]|0,Ve|0)){u=Ve;do{l=u+4|0,A=RM(l)|0;r:do if(A|0)switch(_r(A)|0){case 0:break t;case 4:case 3:case 2:{k=w2(A)|0,R=I2(A)|0,M=(C2(A)|0)+1|0,L=UP(A)|0,q=_r(A)|0,Te=OP(l)|0,lu(oe|0,k|0,R|0,M|0,L|0,0,q|0,Te|0,qM(l)|0,WM(l)|0);break r}case 1:{B=w2(A)|0,k=I2(A)|0,R=(C2(A)|0)+1|0,M=UP(A)|0,L=LX(l)|0,q=_r(A)|0,Te=OP(l)|0,lu(oe|0,B|0,k|0,R|0,M|0,L|0,q|0,Te|0,qM(l)|0,WM(l)|0);break r}case 5:{M=w2(A)|0,L=I2(A)|0,q=(C2(A)|0)+1|0,Te=UP(A)|0,lu(oe|0,M|0,L|0,q|0,Te|0,H8e(A)|0,_r(A)|0,0,0,0);break r}default:break r}while(!1);u=n[u>>2]|0}while(u|0)}if(o=n[o>>2]|0,!o)break e}Nt()}while(!1);we(),I=Le}function F8e(){return 11703}function N8e(o){o=o|0,s[o+40>>0]=0}function O8e(o){return o=o|0,(s[o+40>>0]|0)!=0|0}function L8e(o,l){return o=o|0,l=l|0,l=j8e(l)|0,o=n[l>>2]|0,n[l>>2]=n[o>>2],It(o),n[l>>2]|0}function M8e(o){o=o|0,s[o+40>>0]=1}function OX(o){return o=o|0,n[o+20>>2]|0}function U8e(o){return o=o|0,n[o+8>>2]|0}function _8e(o){return o=o|0,n[o+32>>2]|0}function UP(o){return o=o|0,n[o+4>>2]|0}function LX(o){return o=o|0,n[o+4>>2]|0}function qM(o){return o=o|0,n[o+8>>2]|0}function WM(o){return o=o|0,n[o+16>>2]|0}function H8e(o){return o=o|0,n[o+20>>2]|0}function j8e(o){return o=o|0,n[o>>2]|0}function _P(o){o=o|0;var l=0,u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0,Le=0,Te=0,nt=0,Ze=0,ft=0,He=0,Ye=0,Mt=0;Mt=I,I=I+16|0,oe=Mt;do if(o>>>0<245){if(M=o>>>0<11?16:o+11&-8,o=M>>>3,q=n[2783]|0,u=q>>>o,u&3|0)return l=(u&1^1)+o|0,o=11172+(l<<1<<2)|0,u=o+8|0,A=n[u>>2]|0,g=A+8|0,m=n[g>>2]|0,(o|0)==(m|0)?n[2783]=q&~(1<>2]=o,n[u>>2]=m),Ye=l<<3,n[A+4>>2]=Ye|3,Ye=A+Ye+4|0,n[Ye>>2]=n[Ye>>2]|1,Ye=g,I=Mt,Ye|0;if(L=n[2785]|0,M>>>0>L>>>0){if(u|0)return l=2<>>12&16,l=l>>>B,u=l>>>5&8,l=l>>>u,g=l>>>2&4,l=l>>>g,o=l>>>1&2,l=l>>>o,A=l>>>1&1,A=(u|B|g|o|A)+(l>>>A)|0,l=11172+(A<<1<<2)|0,o=l+8|0,g=n[o>>2]|0,B=g+8|0,u=n[B>>2]|0,(l|0)==(u|0)?(o=q&~(1<>2]=l,n[o>>2]=u,o=q),m=(A<<3)-M|0,n[g+4>>2]=M|3,A=g+M|0,n[A+4>>2]=m|1,n[A+m>>2]=m,L|0&&(g=n[2788]|0,l=L>>>3,u=11172+(l<<1<<2)|0,l=1<>2]|0):(n[2783]=o|l,l=u,o=u+8|0),n[o>>2]=g,n[l+12>>2]=g,n[g+8>>2]=l,n[g+12>>2]=u),n[2785]=m,n[2788]=A,Ye=B,I=Mt,Ye|0;if(k=n[2784]|0,k){if(u=(k&0-k)+-1|0,B=u>>>12&16,u=u>>>B,m=u>>>5&8,u=u>>>m,R=u>>>2&4,u=u>>>R,A=u>>>1&2,u=u>>>A,o=u>>>1&1,o=n[11436+((m|B|R|A|o)+(u>>>o)<<2)>>2]|0,u=(n[o+4>>2]&-8)-M|0,A=n[o+16+(((n[o+16>>2]|0)==0&1)<<2)>>2]|0,!A)R=o,m=u;else{do B=(n[A+4>>2]&-8)-M|0,R=B>>>0>>0,u=R?B:u,o=R?A:o,A=n[A+16+(((n[A+16>>2]|0)==0&1)<<2)>>2]|0;while(A|0);R=o,m=u}if(B=R+M|0,R>>>0>>0){g=n[R+24>>2]|0,l=n[R+12>>2]|0;do if((l|0)==(R|0)){if(o=R+20|0,l=n[o>>2]|0,!l&&(o=R+16|0,l=n[o>>2]|0,!l)){u=0;break}for(;;){if(u=l+20|0,A=n[u>>2]|0,A|0){l=A,o=u;continue}if(u=l+16|0,A=n[u>>2]|0,A)l=A,o=u;else break}n[o>>2]=0,u=l}else u=n[R+8>>2]|0,n[u+12>>2]=l,n[l+8>>2]=u,u=l;while(!1);do if(g|0){if(l=n[R+28>>2]|0,o=11436+(l<<2)|0,(R|0)==(n[o>>2]|0)){if(n[o>>2]=u,!u){n[2784]=k&~(1<>2]|0)!=(R|0)&1)<<2)>>2]=u,!u)break;n[u+24>>2]=g,l=n[R+16>>2]|0,l|0&&(n[u+16>>2]=l,n[l+24>>2]=u),l=n[R+20>>2]|0,l|0&&(n[u+20>>2]=l,n[l+24>>2]=u)}while(!1);return m>>>0<16?(Ye=m+M|0,n[R+4>>2]=Ye|3,Ye=R+Ye+4|0,n[Ye>>2]=n[Ye>>2]|1):(n[R+4>>2]=M|3,n[B+4>>2]=m|1,n[B+m>>2]=m,L|0&&(A=n[2788]|0,l=L>>>3,u=11172+(l<<1<<2)|0,l=1<>2]|0):(n[2783]=q|l,l=u,o=u+8|0),n[o>>2]=A,n[l+12>>2]=A,n[A+8>>2]=l,n[A+12>>2]=u),n[2785]=m,n[2788]=B),Ye=R+8|0,I=Mt,Ye|0}else q=M}else q=M}else q=M}else if(o>>>0<=4294967231)if(o=o+11|0,M=o&-8,R=n[2784]|0,R){A=0-M|0,o=o>>>8,o?M>>>0>16777215?k=31:(q=(o+1048320|0)>>>16&8,He=o<>>16&4,He=He<>>16&2,k=14-(L|q|k)+(He<>>15)|0,k=M>>>(k+7|0)&1|k<<1):k=0,u=n[11436+(k<<2)>>2]|0;e:do if(!u)u=0,o=0,He=57;else for(o=0,B=M<<((k|0)==31?0:25-(k>>>1)|0),m=0;;){if(g=(n[u+4>>2]&-8)-M|0,g>>>0>>0)if(g)o=u,A=g;else{o=u,A=0,g=u,He=61;break e}if(g=n[u+20>>2]|0,u=n[u+16+(B>>>31<<2)>>2]|0,m=(g|0)==0|(g|0)==(u|0)?m:g,g=(u|0)==0,g){u=m,He=57;break}else B=B<<((g^1)&1)}while(!1);if((He|0)==57){if((u|0)==0&(o|0)==0){if(o=2<>>12&16,q=q>>>B,m=q>>>5&8,q=q>>>m,k=q>>>2&4,q=q>>>k,L=q>>>1&2,q=q>>>L,u=q>>>1&1,o=0,u=n[11436+((m|B|k|L|u)+(q>>>u)<<2)>>2]|0}u?(g=u,He=61):(k=o,B=A)}if((He|0)==61)for(;;)if(He=0,u=(n[g+4>>2]&-8)-M|0,q=u>>>0>>0,u=q?u:A,o=q?g:o,g=n[g+16+(((n[g+16>>2]|0)==0&1)<<2)>>2]|0,g)A=u,He=61;else{k=o,B=u;break}if(k|0&&B>>>0<((n[2785]|0)-M|0)>>>0){if(m=k+M|0,k>>>0>=m>>>0)return Ye=0,I=Mt,Ye|0;g=n[k+24>>2]|0,l=n[k+12>>2]|0;do if((l|0)==(k|0)){if(o=k+20|0,l=n[o>>2]|0,!l&&(o=k+16|0,l=n[o>>2]|0,!l)){l=0;break}for(;;){if(u=l+20|0,A=n[u>>2]|0,A|0){l=A,o=u;continue}if(u=l+16|0,A=n[u>>2]|0,A)l=A,o=u;else break}n[o>>2]=0}else Ye=n[k+8>>2]|0,n[Ye+12>>2]=l,n[l+8>>2]=Ye;while(!1);do if(g){if(o=n[k+28>>2]|0,u=11436+(o<<2)|0,(k|0)==(n[u>>2]|0)){if(n[u>>2]=l,!l){A=R&~(1<>2]|0)!=(k|0)&1)<<2)>>2]=l,!l){A=R;break}n[l+24>>2]=g,o=n[k+16>>2]|0,o|0&&(n[l+16>>2]=o,n[o+24>>2]=l),o=n[k+20>>2]|0,o&&(n[l+20>>2]=o,n[o+24>>2]=l),A=R}else A=R;while(!1);do if(B>>>0>=16){if(n[k+4>>2]=M|3,n[m+4>>2]=B|1,n[m+B>>2]=B,l=B>>>3,B>>>0<256){u=11172+(l<<1<<2)|0,o=n[2783]|0,l=1<>2]|0):(n[2783]=o|l,l=u,o=u+8|0),n[o>>2]=m,n[l+12>>2]=m,n[m+8>>2]=l,n[m+12>>2]=u;break}if(l=B>>>8,l?B>>>0>16777215?l=31:(He=(l+1048320|0)>>>16&8,Ye=l<>>16&4,Ye=Ye<>>16&2,l=14-(ft|He|l)+(Ye<>>15)|0,l=B>>>(l+7|0)&1|l<<1):l=0,u=11436+(l<<2)|0,n[m+28>>2]=l,o=m+16|0,n[o+4>>2]=0,n[o>>2]=0,o=1<>2]=m,n[m+24>>2]=u,n[m+12>>2]=m,n[m+8>>2]=m;break}for(o=B<<((l|0)==31?0:25-(l>>>1)|0),u=n[u>>2]|0;;){if((n[u+4>>2]&-8|0)==(B|0)){He=97;break}if(A=u+16+(o>>>31<<2)|0,l=n[A>>2]|0,l)o=o<<1,u=l;else{He=96;break}}if((He|0)==96){n[A>>2]=m,n[m+24>>2]=u,n[m+12>>2]=m,n[m+8>>2]=m;break}else if((He|0)==97){He=u+8|0,Ye=n[He>>2]|0,n[Ye+12>>2]=m,n[He>>2]=m,n[m+8>>2]=Ye,n[m+12>>2]=u,n[m+24>>2]=0;break}}else Ye=B+M|0,n[k+4>>2]=Ye|3,Ye=k+Ye+4|0,n[Ye>>2]=n[Ye>>2]|1;while(!1);return Ye=k+8|0,I=Mt,Ye|0}else q=M}else q=M;else q=-1;while(!1);if(u=n[2785]|0,u>>>0>=q>>>0)return l=u-q|0,o=n[2788]|0,l>>>0>15?(Ye=o+q|0,n[2788]=Ye,n[2785]=l,n[Ye+4>>2]=l|1,n[Ye+l>>2]=l,n[o+4>>2]=q|3):(n[2785]=0,n[2788]=0,n[o+4>>2]=u|3,Ye=o+u+4|0,n[Ye>>2]=n[Ye>>2]|1),Ye=o+8|0,I=Mt,Ye|0;if(B=n[2786]|0,B>>>0>q>>>0)return ft=B-q|0,n[2786]=ft,Ye=n[2789]|0,He=Ye+q|0,n[2789]=He,n[He+4>>2]=ft|1,n[Ye+4>>2]=q|3,Ye=Ye+8|0,I=Mt,Ye|0;if(n[2901]|0?o=n[2903]|0:(n[2903]=4096,n[2902]=4096,n[2904]=-1,n[2905]=-1,n[2906]=0,n[2894]=0,o=oe&-16^1431655768,n[oe>>2]=o,n[2901]=o,o=4096),k=q+48|0,R=q+47|0,m=o+R|0,g=0-o|0,M=m&g,M>>>0<=q>>>0||(o=n[2893]|0,o|0&&(L=n[2891]|0,oe=L+M|0,oe>>>0<=L>>>0|oe>>>0>o>>>0)))return Ye=0,I=Mt,Ye|0;e:do if(n[2894]&4)l=0,He=133;else{u=n[2789]|0;t:do if(u){for(A=11580;o=n[A>>2]|0,!(o>>>0<=u>>>0&&(Te=A+4|0,(o+(n[Te>>2]|0)|0)>>>0>u>>>0));)if(o=n[A+8>>2]|0,o)A=o;else{He=118;break t}if(l=m-B&g,l>>>0<2147483647)if(o=Gh(l|0)|0,(o|0)==((n[A>>2]|0)+(n[Te>>2]|0)|0)){if((o|0)!=-1){B=l,m=o,He=135;break e}}else A=o,He=126;else l=0}else He=118;while(!1);do if((He|0)==118)if(u=Gh(0)|0,(u|0)!=-1&&(l=u,Ve=n[2902]|0,Le=Ve+-1|0,l=(Le&l|0?(Le+l&0-Ve)-l|0:0)+M|0,Ve=n[2891]|0,Le=l+Ve|0,l>>>0>q>>>0&l>>>0<2147483647)){if(Te=n[2893]|0,Te|0&&Le>>>0<=Ve>>>0|Le>>>0>Te>>>0){l=0;break}if(o=Gh(l|0)|0,(o|0)==(u|0)){B=l,m=u,He=135;break e}else A=o,He=126}else l=0;while(!1);do if((He|0)==126){if(u=0-l|0,!(k>>>0>l>>>0&(l>>>0<2147483647&(A|0)!=-1)))if((A|0)==-1){l=0;break}else{B=l,m=A,He=135;break e}if(o=n[2903]|0,o=R-l+o&0-o,o>>>0>=2147483647){B=l,m=A,He=135;break e}if((Gh(o|0)|0)==-1){Gh(u|0)|0,l=0;break}else{B=o+l|0,m=A,He=135;break e}}while(!1);n[2894]=n[2894]|4,He=133}while(!1);if((He|0)==133&&M>>>0<2147483647&&(ft=Gh(M|0)|0,Te=Gh(0)|0,nt=Te-ft|0,Ze=nt>>>0>(q+40|0)>>>0,!((ft|0)==-1|Ze^1|ft>>>0>>0&((ft|0)!=-1&(Te|0)!=-1)^1))&&(B=Ze?nt:l,m=ft,He=135),(He|0)==135){l=(n[2891]|0)+B|0,n[2891]=l,l>>>0>(n[2892]|0)>>>0&&(n[2892]=l),R=n[2789]|0;do if(R){for(l=11580;;){if(o=n[l>>2]|0,u=l+4|0,A=n[u>>2]|0,(m|0)==(o+A|0)){He=145;break}if(g=n[l+8>>2]|0,g)l=g;else break}if((He|0)==145&&!(n[l+12>>2]&8|0)&&R>>>0>>0&R>>>0>=o>>>0){n[u>>2]=A+B,Ye=R+8|0,Ye=Ye&7|0?0-Ye&7:0,He=R+Ye|0,Ye=(n[2786]|0)+(B-Ye)|0,n[2789]=He,n[2786]=Ye,n[He+4>>2]=Ye|1,n[He+Ye+4>>2]=40,n[2790]=n[2905];break}for(m>>>0<(n[2787]|0)>>>0&&(n[2787]=m),u=m+B|0,l=11580;;){if((n[l>>2]|0)==(u|0)){He=153;break}if(o=n[l+8>>2]|0,o)l=o;else break}if((He|0)==153&&!(n[l+12>>2]&8|0)){n[l>>2]=m,L=l+4|0,n[L>>2]=(n[L>>2]|0)+B,L=m+8|0,L=m+(L&7|0?0-L&7:0)|0,l=u+8|0,l=u+(l&7|0?0-l&7:0)|0,M=L+q|0,k=l-L-q|0,n[L+4>>2]=q|3;do if((l|0)!=(R|0)){if((l|0)==(n[2788]|0)){Ye=(n[2785]|0)+k|0,n[2785]=Ye,n[2788]=M,n[M+4>>2]=Ye|1,n[M+Ye>>2]=Ye;break}if(o=n[l+4>>2]|0,(o&3|0)==1){B=o&-8,A=o>>>3;e:do if(o>>>0<256)if(o=n[l+8>>2]|0,u=n[l+12>>2]|0,(u|0)==(o|0)){n[2783]=n[2783]&~(1<>2]=u,n[u+8>>2]=o;break}else{m=n[l+24>>2]|0,o=n[l+12>>2]|0;do if((o|0)==(l|0)){if(A=l+16|0,u=A+4|0,o=n[u>>2]|0,!o)if(o=n[A>>2]|0,o)u=A;else{o=0;break}for(;;){if(A=o+20|0,g=n[A>>2]|0,g|0){o=g,u=A;continue}if(A=o+16|0,g=n[A>>2]|0,g)o=g,u=A;else break}n[u>>2]=0}else Ye=n[l+8>>2]|0,n[Ye+12>>2]=o,n[o+8>>2]=Ye;while(!1);if(!m)break;u=n[l+28>>2]|0,A=11436+(u<<2)|0;do if((l|0)!=(n[A>>2]|0)){if(n[m+16+(((n[m+16>>2]|0)!=(l|0)&1)<<2)>>2]=o,!o)break e}else{if(n[A>>2]=o,o|0)break;n[2784]=n[2784]&~(1<>2]=m,u=l+16|0,A=n[u>>2]|0,A|0&&(n[o+16>>2]=A,n[A+24>>2]=o),u=n[u+4>>2]|0,!u)break;n[o+20>>2]=u,n[u+24>>2]=o}while(!1);l=l+B|0,g=B+k|0}else g=k;if(l=l+4|0,n[l>>2]=n[l>>2]&-2,n[M+4>>2]=g|1,n[M+g>>2]=g,l=g>>>3,g>>>0<256){u=11172+(l<<1<<2)|0,o=n[2783]|0,l=1<>2]|0):(n[2783]=o|l,l=u,o=u+8|0),n[o>>2]=M,n[l+12>>2]=M,n[M+8>>2]=l,n[M+12>>2]=u;break}l=g>>>8;do if(!l)l=0;else{if(g>>>0>16777215){l=31;break}He=(l+1048320|0)>>>16&8,Ye=l<>>16&4,Ye=Ye<>>16&2,l=14-(ft|He|l)+(Ye<>>15)|0,l=g>>>(l+7|0)&1|l<<1}while(!1);if(A=11436+(l<<2)|0,n[M+28>>2]=l,o=M+16|0,n[o+4>>2]=0,n[o>>2]=0,o=n[2784]|0,u=1<>2]=M,n[M+24>>2]=A,n[M+12>>2]=M,n[M+8>>2]=M;break}for(o=g<<((l|0)==31?0:25-(l>>>1)|0),u=n[A>>2]|0;;){if((n[u+4>>2]&-8|0)==(g|0)){He=194;break}if(A=u+16+(o>>>31<<2)|0,l=n[A>>2]|0,l)o=o<<1,u=l;else{He=193;break}}if((He|0)==193){n[A>>2]=M,n[M+24>>2]=u,n[M+12>>2]=M,n[M+8>>2]=M;break}else if((He|0)==194){He=u+8|0,Ye=n[He>>2]|0,n[Ye+12>>2]=M,n[He>>2]=M,n[M+8>>2]=Ye,n[M+12>>2]=u,n[M+24>>2]=0;break}}else Ye=(n[2786]|0)+k|0,n[2786]=Ye,n[2789]=M,n[M+4>>2]=Ye|1;while(!1);return Ye=L+8|0,I=Mt,Ye|0}for(l=11580;o=n[l>>2]|0,!(o>>>0<=R>>>0&&(Ye=o+(n[l+4>>2]|0)|0,Ye>>>0>R>>>0));)l=n[l+8>>2]|0;g=Ye+-47|0,o=g+8|0,o=g+(o&7|0?0-o&7:0)|0,g=R+16|0,o=o>>>0>>0?R:o,l=o+8|0,u=m+8|0,u=u&7|0?0-u&7:0,He=m+u|0,u=B+-40-u|0,n[2789]=He,n[2786]=u,n[He+4>>2]=u|1,n[He+u+4>>2]=40,n[2790]=n[2905],u=o+4|0,n[u>>2]=27,n[l>>2]=n[2895],n[l+4>>2]=n[2896],n[l+8>>2]=n[2897],n[l+12>>2]=n[2898],n[2895]=m,n[2896]=B,n[2898]=0,n[2897]=l,l=o+24|0;do He=l,l=l+4|0,n[l>>2]=7;while((He+8|0)>>>0>>0);if((o|0)!=(R|0)){if(m=o-R|0,n[u>>2]=n[u>>2]&-2,n[R+4>>2]=m|1,n[o>>2]=m,l=m>>>3,m>>>0<256){u=11172+(l<<1<<2)|0,o=n[2783]|0,l=1<>2]|0):(n[2783]=o|l,l=u,o=u+8|0),n[o>>2]=R,n[l+12>>2]=R,n[R+8>>2]=l,n[R+12>>2]=u;break}if(l=m>>>8,l?m>>>0>16777215?u=31:(He=(l+1048320|0)>>>16&8,Ye=l<>>16&4,Ye=Ye<>>16&2,u=14-(ft|He|u)+(Ye<>>15)|0,u=m>>>(u+7|0)&1|u<<1):u=0,A=11436+(u<<2)|0,n[R+28>>2]=u,n[R+20>>2]=0,n[g>>2]=0,l=n[2784]|0,o=1<>2]=R,n[R+24>>2]=A,n[R+12>>2]=R,n[R+8>>2]=R;break}for(o=m<<((u|0)==31?0:25-(u>>>1)|0),u=n[A>>2]|0;;){if((n[u+4>>2]&-8|0)==(m|0)){He=216;break}if(A=u+16+(o>>>31<<2)|0,l=n[A>>2]|0,l)o=o<<1,u=l;else{He=215;break}}if((He|0)==215){n[A>>2]=R,n[R+24>>2]=u,n[R+12>>2]=R,n[R+8>>2]=R;break}else if((He|0)==216){He=u+8|0,Ye=n[He>>2]|0,n[Ye+12>>2]=R,n[He>>2]=R,n[R+8>>2]=Ye,n[R+12>>2]=u,n[R+24>>2]=0;break}}}else{Ye=n[2787]|0,(Ye|0)==0|m>>>0>>0&&(n[2787]=m),n[2895]=m,n[2896]=B,n[2898]=0,n[2792]=n[2901],n[2791]=-1,l=0;do Ye=11172+(l<<1<<2)|0,n[Ye+12>>2]=Ye,n[Ye+8>>2]=Ye,l=l+1|0;while((l|0)!=32);Ye=m+8|0,Ye=Ye&7|0?0-Ye&7:0,He=m+Ye|0,Ye=B+-40-Ye|0,n[2789]=He,n[2786]=Ye,n[He+4>>2]=Ye|1,n[He+Ye+4>>2]=40,n[2790]=n[2905]}while(!1);if(l=n[2786]|0,l>>>0>q>>>0)return ft=l-q|0,n[2786]=ft,Ye=n[2789]|0,He=Ye+q|0,n[2789]=He,n[He+4>>2]=ft|1,n[Ye+4>>2]=q|3,Ye=Ye+8|0,I=Mt,Ye|0}return n[(eE()|0)>>2]=12,Ye=0,I=Mt,Ye|0}function HP(o){o=o|0;var l=0,u=0,A=0,g=0,m=0,B=0,k=0,R=0;if(o){u=o+-8|0,g=n[2787]|0,o=n[o+-4>>2]|0,l=o&-8,R=u+l|0;do if(o&1)k=u,B=u;else{if(A=n[u>>2]|0,!(o&3)||(B=u+(0-A)|0,m=A+l|0,B>>>0>>0))return;if((B|0)==(n[2788]|0)){if(o=R+4|0,l=n[o>>2]|0,(l&3|0)!=3){k=B,l=m;break}n[2785]=m,n[o>>2]=l&-2,n[B+4>>2]=m|1,n[B+m>>2]=m;return}if(u=A>>>3,A>>>0<256)if(o=n[B+8>>2]|0,l=n[B+12>>2]|0,(l|0)==(o|0)){n[2783]=n[2783]&~(1<>2]=l,n[l+8>>2]=o,k=B,l=m;break}g=n[B+24>>2]|0,o=n[B+12>>2]|0;do if((o|0)==(B|0)){if(u=B+16|0,l=u+4|0,o=n[l>>2]|0,!o)if(o=n[u>>2]|0,o)l=u;else{o=0;break}for(;;){if(u=o+20|0,A=n[u>>2]|0,A|0){o=A,l=u;continue}if(u=o+16|0,A=n[u>>2]|0,A)o=A,l=u;else break}n[l>>2]=0}else k=n[B+8>>2]|0,n[k+12>>2]=o,n[o+8>>2]=k;while(!1);if(g){if(l=n[B+28>>2]|0,u=11436+(l<<2)|0,(B|0)==(n[u>>2]|0)){if(n[u>>2]=o,!o){n[2784]=n[2784]&~(1<>2]|0)!=(B|0)&1)<<2)>>2]=o,!o){k=B,l=m;break}n[o+24>>2]=g,l=B+16|0,u=n[l>>2]|0,u|0&&(n[o+16>>2]=u,n[u+24>>2]=o),l=n[l+4>>2]|0,l?(n[o+20>>2]=l,n[l+24>>2]=o,k=B,l=m):(k=B,l=m)}else k=B,l=m}while(!1);if(!(B>>>0>=R>>>0)&&(o=R+4|0,A=n[o>>2]|0,!!(A&1))){if(A&2)n[o>>2]=A&-2,n[k+4>>2]=l|1,n[B+l>>2]=l,g=l;else{if(o=n[2788]|0,(R|0)==(n[2789]|0)){if(R=(n[2786]|0)+l|0,n[2786]=R,n[2789]=k,n[k+4>>2]=R|1,(k|0)!=(o|0))return;n[2788]=0,n[2785]=0;return}if((R|0)==(o|0)){R=(n[2785]|0)+l|0,n[2785]=R,n[2788]=B,n[k+4>>2]=R|1,n[B+R>>2]=R;return}g=(A&-8)+l|0,u=A>>>3;do if(A>>>0<256)if(l=n[R+8>>2]|0,o=n[R+12>>2]|0,(o|0)==(l|0)){n[2783]=n[2783]&~(1<>2]=o,n[o+8>>2]=l;break}else{m=n[R+24>>2]|0,o=n[R+12>>2]|0;do if((o|0)==(R|0)){if(u=R+16|0,l=u+4|0,o=n[l>>2]|0,!o)if(o=n[u>>2]|0,o)l=u;else{u=0;break}for(;;){if(u=o+20|0,A=n[u>>2]|0,A|0){o=A,l=u;continue}if(u=o+16|0,A=n[u>>2]|0,A)o=A,l=u;else break}n[l>>2]=0,u=o}else u=n[R+8>>2]|0,n[u+12>>2]=o,n[o+8>>2]=u,u=o;while(!1);if(m|0){if(o=n[R+28>>2]|0,l=11436+(o<<2)|0,(R|0)==(n[l>>2]|0)){if(n[l>>2]=u,!u){n[2784]=n[2784]&~(1<>2]|0)!=(R|0)&1)<<2)>>2]=u,!u)break;n[u+24>>2]=m,o=R+16|0,l=n[o>>2]|0,l|0&&(n[u+16>>2]=l,n[l+24>>2]=u),o=n[o+4>>2]|0,o|0&&(n[u+20>>2]=o,n[o+24>>2]=u)}}while(!1);if(n[k+4>>2]=g|1,n[B+g>>2]=g,(k|0)==(n[2788]|0)){n[2785]=g;return}}if(o=g>>>3,g>>>0<256){u=11172+(o<<1<<2)|0,l=n[2783]|0,o=1<>2]|0):(n[2783]=l|o,o=u,l=u+8|0),n[l>>2]=k,n[o+12>>2]=k,n[k+8>>2]=o,n[k+12>>2]=u;return}o=g>>>8,o?g>>>0>16777215?o=31:(B=(o+1048320|0)>>>16&8,R=o<>>16&4,R=R<>>16&2,o=14-(m|B|o)+(R<>>15)|0,o=g>>>(o+7|0)&1|o<<1):o=0,A=11436+(o<<2)|0,n[k+28>>2]=o,n[k+20>>2]=0,n[k+16>>2]=0,l=n[2784]|0,u=1<>>1)|0),u=n[A>>2]|0;;){if((n[u+4>>2]&-8|0)==(g|0)){o=73;break}if(A=u+16+(l>>>31<<2)|0,o=n[A>>2]|0,o)l=l<<1,u=o;else{o=72;break}}if((o|0)==72){n[A>>2]=k,n[k+24>>2]=u,n[k+12>>2]=k,n[k+8>>2]=k;break}else if((o|0)==73){B=u+8|0,R=n[B>>2]|0,n[R+12>>2]=k,n[B>>2]=k,n[k+8>>2]=R,n[k+12>>2]=u,n[k+24>>2]=0;break}}else n[2784]=l|u,n[A>>2]=k,n[k+24>>2]=A,n[k+12>>2]=k,n[k+8>>2]=k;while(!1);if(R=(n[2791]|0)+-1|0,n[2791]=R,!R)o=11588;else return;for(;o=n[o>>2]|0,o;)o=o+8|0;n[2791]=-1}}}function G8e(){return 11628}function q8e(o){o=o|0;var l=0,u=0;return l=I,I=I+16|0,u=l,n[u>>2]=V8e(n[o+60>>2]|0)|0,o=jP(uu(6,u|0)|0)|0,I=l,o|0}function MX(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0;q=I,I=I+48|0,M=q+16|0,m=q,g=q+32|0,k=o+28|0,A=n[k>>2]|0,n[g>>2]=A,R=o+20|0,A=(n[R>>2]|0)-A|0,n[g+4>>2]=A,n[g+8>>2]=l,n[g+12>>2]=u,A=A+u|0,B=o+60|0,n[m>>2]=n[B>>2],n[m+4>>2]=g,n[m+8>>2]=2,m=jP(Ya(146,m|0)|0)|0;e:do if((A|0)!=(m|0)){for(l=2;!((m|0)<0);)if(A=A-m|0,Ve=n[g+4>>2]|0,oe=m>>>0>Ve>>>0,g=oe?g+8|0:g,l=(oe<<31>>31)+l|0,Ve=m-(oe?Ve:0)|0,n[g>>2]=(n[g>>2]|0)+Ve,oe=g+4|0,n[oe>>2]=(n[oe>>2]|0)-Ve,n[M>>2]=n[B>>2],n[M+4>>2]=g,n[M+8>>2]=l,m=jP(Ya(146,M|0)|0)|0,(A|0)==(m|0)){L=3;break e}n[o+16>>2]=0,n[k>>2]=0,n[R>>2]=0,n[o>>2]=n[o>>2]|32,(l|0)==2?u=0:u=u-(n[g+4>>2]|0)|0}else L=3;while(!1);return(L|0)==3&&(Ve=n[o+44>>2]|0,n[o+16>>2]=Ve+(n[o+48>>2]|0),n[k>>2]=Ve,n[R>>2]=Ve),I=q,u|0}function W8e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0;return g=I,I=I+32|0,m=g,A=g+20|0,n[m>>2]=n[o+60>>2],n[m+4>>2]=0,n[m+8>>2]=l,n[m+12>>2]=A,n[m+16>>2]=u,(jP(Wa(140,m|0)|0)|0)<0?(n[A>>2]=-1,o=-1):o=n[A>>2]|0,I=g,o|0}function jP(o){return o=o|0,o>>>0>4294963200&&(n[(eE()|0)>>2]=0-o,o=-1),o|0}function eE(){return(Y8e()|0)+64|0}function Y8e(){return YM()|0}function YM(){return 2084}function V8e(o){return o=o|0,o|0}function J8e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0;return g=I,I=I+32|0,A=g,n[o+36>>2]=1,!(n[o>>2]&64|0)&&(n[A>>2]=n[o+60>>2],n[A+4>>2]=21523,n[A+8>>2]=g+16,co(54,A|0)|0)&&(s[o+75>>0]=-1),A=MX(o,l,u)|0,I=g,A|0}function UX(o,l){o=o|0,l=l|0;var u=0,A=0;if(u=s[o>>0]|0,A=s[l>>0]|0,!(u<<24>>24)||u<<24>>24!=A<<24>>24)o=A;else{do o=o+1|0,l=l+1|0,u=s[o>>0]|0,A=s[l>>0]|0;while(!(!(u<<24>>24)||u<<24>>24!=A<<24>>24));o=A}return(u&255)-(o&255)|0}function K8e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0;e:do if(!u)o=0;else{for(;A=s[o>>0]|0,g=s[l>>0]|0,A<<24>>24==g<<24>>24;)if(u=u+-1|0,u)o=o+1|0,l=l+1|0;else{o=0;break e}o=(A&255)-(g&255)|0}while(!1);return o|0}function _X(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0,Le=0,Te=0;Te=I,I=I+224|0,L=Te+120|0,q=Te+80|0,Ve=Te,Le=Te+136|0,A=q,g=A+40|0;do n[A>>2]=0,A=A+4|0;while((A|0)<(g|0));return n[L>>2]=n[u>>2],(VM(0,l,L,Ve,q)|0)<0?u=-1:((n[o+76>>2]|0)>-1?oe=z8e(o)|0:oe=0,u=n[o>>2]|0,M=u&32,(s[o+74>>0]|0)<1&&(n[o>>2]=u&-33),A=o+48|0,n[A>>2]|0?u=VM(o,l,L,Ve,q)|0:(g=o+44|0,m=n[g>>2]|0,n[g>>2]=Le,B=o+28|0,n[B>>2]=Le,k=o+20|0,n[k>>2]=Le,n[A>>2]=80,R=o+16|0,n[R>>2]=Le+80,u=VM(o,l,L,Ve,q)|0,m&&(YP[n[o+36>>2]&7](o,0,0)|0,u=n[k>>2]|0?u:-1,n[g>>2]=m,n[A>>2]=0,n[R>>2]=0,n[B>>2]=0,n[k>>2]=0)),A=n[o>>2]|0,n[o>>2]=A|M,oe|0&&X8e(o),u=A&32|0?-1:u),I=Te,u|0}function VM(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0;var m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0,Le=0,Te=0,nt=0,Ze=0,ft=0,He=0,Ye=0,Mt=0,Gr=0,fr=0,$t=0,Tr=0,Hr=0,cr=0;cr=I,I=I+64|0,fr=cr+16|0,$t=cr,Mt=cr+24|0,Tr=cr+8|0,Hr=cr+20|0,n[fr>>2]=l,ft=(o|0)!=0,He=Mt+40|0,Ye=He,Mt=Mt+39|0,Gr=Tr+4|0,B=0,m=0,L=0;e:for(;;){do if((m|0)>-1)if((B|0)>(2147483647-m|0)){n[(eE()|0)>>2]=75,m=-1;break}else{m=B+m|0;break}while(!1);if(B=s[l>>0]|0,B<<24>>24)k=l;else{Ze=87;break}t:for(;;){switch(B<<24>>24){case 37:{B=k,Ze=9;break t}case 0:{B=k;break t}default:}nt=k+1|0,n[fr>>2]=nt,B=s[nt>>0]|0,k=nt}t:do if((Ze|0)==9)for(;;){if(Ze=0,(s[k+1>>0]|0)!=37)break t;if(B=B+1|0,k=k+2|0,n[fr>>2]=k,(s[k>>0]|0)==37)Ze=9;else break}while(!1);if(B=B-l|0,ft&&bs(o,l,B),B|0){l=k;continue}R=k+1|0,B=(s[R>>0]|0)+-48|0,B>>>0<10?(nt=(s[k+2>>0]|0)==36,Te=nt?B:-1,L=nt?1:L,R=nt?k+3|0:R):Te=-1,n[fr>>2]=R,B=s[R>>0]|0,k=(B<<24>>24)+-32|0;t:do if(k>>>0<32)for(M=0,q=B;;){if(B=1<>2]=R,B=s[R>>0]|0,k=(B<<24>>24)+-32|0,k>>>0>=32)break;q=B}else M=0;while(!1);if(B<<24>>24==42){if(k=R+1|0,B=(s[k>>0]|0)+-48|0,B>>>0<10&&(s[R+2>>0]|0)==36)n[g+(B<<2)>>2]=10,B=n[A+((s[k>>0]|0)+-48<<3)>>2]|0,L=1,R=R+3|0;else{if(L|0){m=-1;break}ft?(L=(n[u>>2]|0)+3&-4,B=n[L>>2]|0,n[u>>2]=L+4,L=0,R=k):(B=0,L=0,R=k)}n[fr>>2]=R,nt=(B|0)<0,B=nt?0-B|0:B,M=nt?M|8192:M}else{if(B=HX(fr)|0,(B|0)<0){m=-1;break}R=n[fr>>2]|0}do if((s[R>>0]|0)==46){if((s[R+1>>0]|0)!=42){n[fr>>2]=R+1,k=HX(fr)|0,R=n[fr>>2]|0;break}if(q=R+2|0,k=(s[q>>0]|0)+-48|0,k>>>0<10&&(s[R+3>>0]|0)==36){n[g+(k<<2)>>2]=10,k=n[A+((s[q>>0]|0)+-48<<3)>>2]|0,R=R+4|0,n[fr>>2]=R;break}if(L|0){m=-1;break e}ft?(nt=(n[u>>2]|0)+3&-4,k=n[nt>>2]|0,n[u>>2]=nt+4):k=0,n[fr>>2]=q,R=q}else k=-1;while(!1);for(Le=0;;){if(((s[R>>0]|0)+-65|0)>>>0>57){m=-1;break e}if(nt=R+1|0,n[fr>>2]=nt,q=s[(s[R>>0]|0)+-65+(5178+(Le*58|0))>>0]|0,oe=q&255,(oe+-1|0)>>>0<8)Le=oe,R=nt;else break}if(!(q<<24>>24)){m=-1;break}Ve=(Te|0)>-1;do if(q<<24>>24==19)if(Ve){m=-1;break e}else Ze=49;else{if(Ve){n[g+(Te<<2)>>2]=oe,Ve=A+(Te<<3)|0,Te=n[Ve+4>>2]|0,Ze=$t,n[Ze>>2]=n[Ve>>2],n[Ze+4>>2]=Te,Ze=49;break}if(!ft){m=0;break e}jX($t,oe,u)}while(!1);if((Ze|0)==49&&(Ze=0,!ft)){B=0,l=nt;continue}R=s[R>>0]|0,R=(Le|0)!=0&(R&15|0)==3?R&-33:R,Ve=M&-65537,Te=M&8192|0?Ve:M;t:do switch(R|0){case 110:switch((Le&255)<<24>>24){case 0:{n[n[$t>>2]>>2]=m,B=0,l=nt;continue e}case 1:{n[n[$t>>2]>>2]=m,B=0,l=nt;continue e}case 2:{B=n[$t>>2]|0,n[B>>2]=m,n[B+4>>2]=((m|0)<0)<<31>>31,B=0,l=nt;continue e}case 3:{a[n[$t>>2]>>1]=m,B=0,l=nt;continue e}case 4:{s[n[$t>>2]>>0]=m,B=0,l=nt;continue e}case 6:{n[n[$t>>2]>>2]=m,B=0,l=nt;continue e}case 7:{B=n[$t>>2]|0,n[B>>2]=m,n[B+4>>2]=((m|0)<0)<<31>>31,B=0,l=nt;continue e}default:{B=0,l=nt;continue e}}case 112:{R=120,k=k>>>0>8?k:8,l=Te|8,Ze=61;break}case 88:case 120:{l=Te,Ze=61;break}case 111:{R=$t,l=n[R>>2]|0,R=n[R+4>>2]|0,oe=$8e(l,R,He)|0,Ve=Ye-oe|0,M=0,q=5642,k=(Te&8|0)==0|(k|0)>(Ve|0)?k:Ve+1|0,Ve=Te,Ze=67;break}case 105:case 100:if(R=$t,l=n[R>>2]|0,R=n[R+4>>2]|0,(R|0)<0){l=GP(0,0,l|0,R|0)|0,R=Be,M=$t,n[M>>2]=l,n[M+4>>2]=R,M=1,q=5642,Ze=66;break t}else{M=(Te&2049|0)!=0&1,q=Te&2048|0?5643:Te&1|0?5644:5642,Ze=66;break t}case 117:{R=$t,M=0,q=5642,l=n[R>>2]|0,R=n[R+4>>2]|0,Ze=66;break}case 99:{s[Mt>>0]=n[$t>>2],l=Mt,M=0,q=5642,oe=He,R=1,k=Ve;break}case 109:{R=eHe(n[(eE()|0)>>2]|0)|0,Ze=71;break}case 115:{R=n[$t>>2]|0,R=R|0?R:5652,Ze=71;break}case 67:{n[Tr>>2]=n[$t>>2],n[Gr>>2]=0,n[$t>>2]=Tr,oe=-1,R=Tr,Ze=75;break}case 83:{l=n[$t>>2]|0,k?(oe=k,R=l,Ze=75):(_s(o,32,B,0,Te),l=0,Ze=84);break}case 65:case 71:case 70:case 69:case 97:case 103:case 102:case 101:{B=rHe(o,+E[$t>>3],B,k,Te,R)|0,l=nt;continue e}default:M=0,q=5642,oe=He,R=k,k=Te}while(!1);t:do if((Ze|0)==61)Te=$t,Le=n[Te>>2]|0,Te=n[Te+4>>2]|0,oe=Z8e(Le,Te,He,R&32)|0,q=(l&8|0)==0|(Le|0)==0&(Te|0)==0,M=q?0:2,q=q?5642:5642+(R>>4)|0,Ve=l,l=Le,R=Te,Ze=67;else if((Ze|0)==66)oe=tE(l,R,He)|0,Ve=Te,Ze=67;else if((Ze|0)==71)Ze=0,Te=tHe(R,0,k)|0,Le=(Te|0)==0,l=R,M=0,q=5642,oe=Le?R+k|0:Te,R=Le?k:Te-R|0,k=Ve;else if((Ze|0)==75){for(Ze=0,q=R,l=0,k=0;M=n[q>>2]|0,!(!M||(k=GX(Hr,M)|0,(k|0)<0|k>>>0>(oe-l|0)>>>0));)if(l=k+l|0,oe>>>0>l>>>0)q=q+4|0;else break;if((k|0)<0){m=-1;break e}if(_s(o,32,B,l,Te),!l)l=0,Ze=84;else for(M=0;;){if(k=n[R>>2]|0,!k){Ze=84;break t}if(k=GX(Hr,k)|0,M=k+M|0,(M|0)>(l|0)){Ze=84;break t}if(bs(o,Hr,k),M>>>0>=l>>>0){Ze=84;break}else R=R+4|0}}while(!1);if((Ze|0)==67)Ze=0,R=(l|0)!=0|(R|0)!=0,Te=(k|0)!=0|R,R=((R^1)&1)+(Ye-oe)|0,l=Te?oe:He,oe=He,R=Te?(k|0)>(R|0)?k:R:k,k=(k|0)>-1?Ve&-65537:Ve;else if((Ze|0)==84){Ze=0,_s(o,32,B,l,Te^8192),B=(B|0)>(l|0)?B:l,l=nt;continue}Le=oe-l|0,Ve=(R|0)<(Le|0)?Le:R,Te=Ve+M|0,B=(B|0)<(Te|0)?Te:B,_s(o,32,B,Te,k),bs(o,q,M),_s(o,48,B,Te,k^65536),_s(o,48,Ve,Le,0),bs(o,l,Le),_s(o,32,B,Te,k^8192),l=nt}e:do if((Ze|0)==87&&!o)if(!L)m=0;else{for(m=1;l=n[g+(m<<2)>>2]|0,!!l;)if(jX(A+(m<<3)|0,l,u),m=m+1|0,(m|0)>=10){m=1;break e}for(;;){if(n[g+(m<<2)>>2]|0){m=-1;break e}if(m=m+1|0,(m|0)>=10){m=1;break}}}while(!1);return I=cr,m|0}function z8e(o){return o=o|0,0}function X8e(o){o=o|0}function bs(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]&32||fHe(l,u,o)|0}function HX(o){o=o|0;var l=0,u=0,A=0;if(u=n[o>>2]|0,A=(s[u>>0]|0)+-48|0,A>>>0<10){l=0;do l=A+(l*10|0)|0,u=u+1|0,n[o>>2]=u,A=(s[u>>0]|0)+-48|0;while(A>>>0<10)}else l=0;return l|0}function jX(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0;e:do if(l>>>0<=20)do switch(l|0){case 9:{A=(n[u>>2]|0)+3&-4,l=n[A>>2]|0,n[u>>2]=A+4,n[o>>2]=l;break e}case 10:{A=(n[u>>2]|0)+3&-4,l=n[A>>2]|0,n[u>>2]=A+4,A=o,n[A>>2]=l,n[A+4>>2]=((l|0)<0)<<31>>31;break e}case 11:{A=(n[u>>2]|0)+3&-4,l=n[A>>2]|0,n[u>>2]=A+4,A=o,n[A>>2]=l,n[A+4>>2]=0;break e}case 12:{A=(n[u>>2]|0)+7&-8,l=A,g=n[l>>2]|0,l=n[l+4>>2]|0,n[u>>2]=A+8,A=o,n[A>>2]=g,n[A+4>>2]=l;break e}case 13:{g=(n[u>>2]|0)+3&-4,A=n[g>>2]|0,n[u>>2]=g+4,A=(A&65535)<<16>>16,g=o,n[g>>2]=A,n[g+4>>2]=((A|0)<0)<<31>>31;break e}case 14:{g=(n[u>>2]|0)+3&-4,A=n[g>>2]|0,n[u>>2]=g+4,g=o,n[g>>2]=A&65535,n[g+4>>2]=0;break e}case 15:{g=(n[u>>2]|0)+3&-4,A=n[g>>2]|0,n[u>>2]=g+4,A=(A&255)<<24>>24,g=o,n[g>>2]=A,n[g+4>>2]=((A|0)<0)<<31>>31;break e}case 16:{g=(n[u>>2]|0)+3&-4,A=n[g>>2]|0,n[u>>2]=g+4,g=o,n[g>>2]=A&255,n[g+4>>2]=0;break e}case 17:{g=(n[u>>2]|0)+7&-8,m=+E[g>>3],n[u>>2]=g+8,E[o>>3]=m;break e}case 18:{g=(n[u>>2]|0)+7&-8,m=+E[g>>3],n[u>>2]=g+8,E[o>>3]=m;break e}default:break e}while(!1);while(!1)}function Z8e(o,l,u,A){if(o=o|0,l=l|0,u=u|0,A=A|0,!((o|0)==0&(l|0)==0))do u=u+-1|0,s[u>>0]=c[5694+(o&15)>>0]|0|A,o=qP(o|0,l|0,4)|0,l=Be;while(!((o|0)==0&(l|0)==0));return u|0}function $8e(o,l,u){if(o=o|0,l=l|0,u=u|0,!((o|0)==0&(l|0)==0))do u=u+-1|0,s[u>>0]=o&7|48,o=qP(o|0,l|0,3)|0,l=Be;while(!((o|0)==0&(l|0)==0));return u|0}function tE(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;if(l>>>0>0|(l|0)==0&o>>>0>4294967295){for(;A=XM(o|0,l|0,10,0)|0,u=u+-1|0,s[u>>0]=A&255|48,A=o,o=zM(o|0,l|0,10,0)|0,l>>>0>9|(l|0)==9&A>>>0>4294967295;)l=Be;l=o}else l=o;if(l)for(;u=u+-1|0,s[u>>0]=(l>>>0)%10|0|48,!(l>>>0<10);)l=(l>>>0)/10|0;return u|0}function eHe(o){return o=o|0,aHe(o,n[(oHe()|0)+188>>2]|0)|0}function tHe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;m=l&255,A=(u|0)!=0;e:do if(A&(o&3|0)!=0)for(g=l&255;;){if((s[o>>0]|0)==g<<24>>24){B=6;break e}if(o=o+1|0,u=u+-1|0,A=(u|0)!=0,!(A&(o&3|0)!=0)){B=5;break}}else B=5;while(!1);(B|0)==5&&(A?B=6:u=0);e:do if((B|0)==6&&(g=l&255,(s[o>>0]|0)!=g<<24>>24)){A=Me(m,16843009)|0;t:do if(u>>>0>3){for(;m=n[o>>2]^A,!((m&-2139062144^-2139062144)&m+-16843009|0);)if(o=o+4|0,u=u+-4|0,u>>>0<=3){B=11;break t}}else B=11;while(!1);if((B|0)==11&&!u){u=0;break}for(;;){if((s[o>>0]|0)==g<<24>>24)break e;if(o=o+1|0,u=u+-1|0,!u){u=0;break}}}while(!1);return(u|0?o:0)|0}function _s(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0;var m=0,B=0;if(B=I,I=I+256|0,m=B,(u|0)>(A|0)&(g&73728|0)==0){if(g=u-A|0,nE(m|0,l|0,(g>>>0<256?g:256)|0)|0,g>>>0>255){l=u-A|0;do bs(o,m,256),g=g+-256|0;while(g>>>0>255);g=l&255}bs(o,m,g)}I=B}function GX(o,l){return o=o|0,l=l|0,o?o=iHe(o,l,0)|0:o=0,o|0}function rHe(o,l,u,A,g,m){o=o|0,l=+l,u=u|0,A=A|0,g=g|0,m=m|0;var B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0,Le=0,Te=0,nt=0,Ze=0,ft=0,He=0,Ye=0,Mt=0,Gr=0,fr=0,$t=0,Tr=0,Hr=0,cr=0,jn=0;jn=I,I=I+560|0,R=jn+8|0,nt=jn,cr=jn+524|0,Hr=cr,M=jn+512|0,n[nt>>2]=0,Tr=M+12|0,qX(l)|0,(Be|0)<0?(l=-l,fr=1,Gr=5659):(fr=(g&2049|0)!=0&1,Gr=g&2048|0?5662:g&1|0?5665:5660),qX(l)|0,$t=Be&2146435072;do if($t>>>0<2146435072|($t|0)==2146435072&!1){if(Ve=+nHe(l,nt)*2,B=Ve!=0,B&&(n[nt>>2]=(n[nt>>2]|0)+-1),ft=m|32,(ft|0)==97){Le=m&32,oe=Le|0?Gr+9|0:Gr,q=fr|2,B=12-A|0;do if(A>>>0>11|(B|0)==0)l=Ve;else{l=8;do B=B+-1|0,l=l*16;while(B|0);if((s[oe>>0]|0)==45){l=-(l+(-Ve-l));break}else{l=Ve+l-l;break}}while(!1);k=n[nt>>2]|0,B=(k|0)<0?0-k|0:k,B=tE(B,((B|0)<0)<<31>>31,Tr)|0,(B|0)==(Tr|0)&&(B=M+11|0,s[B>>0]=48),s[B+-1>>0]=(k>>31&2)+43,L=B+-2|0,s[L>>0]=m+15,M=(A|0)<1,R=(g&8|0)==0,B=cr;do $t=~~l,k=B+1|0,s[B>>0]=c[5694+$t>>0]|Le,l=(l-+($t|0))*16,(k-Hr|0)==1&&!(R&(M&l==0))?(s[k>>0]=46,B=B+2|0):B=k;while(l!=0);$t=B-Hr|0,Hr=Tr-L|0,Tr=(A|0)!=0&($t+-2|0)<(A|0)?A+2|0:$t,B=Hr+q+Tr|0,_s(o,32,u,B,g),bs(o,oe,q),_s(o,48,u,B,g^65536),bs(o,cr,$t),_s(o,48,Tr-$t|0,0,0),bs(o,L,Hr),_s(o,32,u,B,g^8192);break}k=(A|0)<0?6:A,B?(B=(n[nt>>2]|0)+-28|0,n[nt>>2]=B,l=Ve*268435456):(l=Ve,B=n[nt>>2]|0),$t=(B|0)<0?R:R+288|0,R=$t;do Ye=~~l>>>0,n[R>>2]=Ye,R=R+4|0,l=(l-+(Ye>>>0))*1e9;while(l!=0);if((B|0)>0)for(M=$t,q=R;;){if(L=(B|0)<29?B:29,B=q+-4|0,B>>>0>=M>>>0){R=0;do He=zX(n[B>>2]|0,0,L|0)|0,He=KM(He|0,Be|0,R|0,0)|0,Ye=Be,Ze=XM(He|0,Ye|0,1e9,0)|0,n[B>>2]=Ze,R=zM(He|0,Ye|0,1e9,0)|0,B=B+-4|0;while(B>>>0>=M>>>0);R&&(M=M+-4|0,n[M>>2]=R)}for(R=q;!(R>>>0<=M>>>0);)if(B=R+-4|0,!(n[B>>2]|0))R=B;else break;if(B=(n[nt>>2]|0)-L|0,n[nt>>2]=B,(B|0)>0)q=R;else break}else M=$t;if((B|0)<0){A=((k+25|0)/9|0)+1|0,Te=(ft|0)==102;do{if(Le=0-B|0,Le=(Le|0)<9?Le:9,M>>>0>>0){L=(1<>>Le,oe=0,B=M;do Ye=n[B>>2]|0,n[B>>2]=(Ye>>>Le)+oe,oe=Me(Ye&L,q)|0,B=B+4|0;while(B>>>0>>0);B=n[M>>2]|0?M:M+4|0,oe?(n[R>>2]=oe,M=B,B=R+4|0):(M=B,B=R)}else M=n[M>>2]|0?M:M+4|0,B=R;R=Te?$t:M,R=(B-R>>2|0)>(A|0)?R+(A<<2)|0:B,B=(n[nt>>2]|0)+Le|0,n[nt>>2]=B}while((B|0)<0);B=M,A=R}else B=M,A=R;if(Ye=$t,B>>>0>>0){if(R=(Ye-B>>2)*9|0,L=n[B>>2]|0,L>>>0>=10){M=10;do M=M*10|0,R=R+1|0;while(L>>>0>=M>>>0)}}else R=0;if(Te=(ft|0)==103,Ze=(k|0)!=0,M=k-((ft|0)!=102?R:0)+((Ze&Te)<<31>>31)|0,(M|0)<(((A-Ye>>2)*9|0)+-9|0)){if(M=M+9216|0,Le=$t+4+(((M|0)/9|0)+-1024<<2)|0,M=((M|0)%9|0)+1|0,(M|0)<9){L=10;do L=L*10|0,M=M+1|0;while((M|0)!=9)}else L=10;if(q=n[Le>>2]|0,oe=(q>>>0)%(L>>>0)|0,M=(Le+4|0)==(A|0),M&(oe|0)==0)M=Le;else if(Ve=((q>>>0)/(L>>>0)|0)&1|0?9007199254740994:9007199254740992,He=(L|0)/2|0,l=oe>>>0>>0?.5:M&(oe|0)==(He|0)?1:1.5,fr&&(He=(s[Gr>>0]|0)==45,l=He?-l:l,Ve=He?-Ve:Ve),M=q-oe|0,n[Le>>2]=M,Ve+l!=Ve){if(He=M+L|0,n[Le>>2]=He,He>>>0>999999999)for(R=Le;M=R+-4|0,n[R>>2]=0,M>>>0>>0&&(B=B+-4|0,n[B>>2]=0),He=(n[M>>2]|0)+1|0,n[M>>2]=He,He>>>0>999999999;)R=M;else M=Le;if(R=(Ye-B>>2)*9|0,q=n[B>>2]|0,q>>>0>=10){L=10;do L=L*10|0,R=R+1|0;while(q>>>0>=L>>>0)}}else M=Le;M=M+4|0,M=A>>>0>M>>>0?M:A,He=B}else M=A,He=B;for(ft=M;;){if(ft>>>0<=He>>>0){nt=0;break}if(B=ft+-4|0,!(n[B>>2]|0))ft=B;else{nt=1;break}}A=0-R|0;do if(Te)if(B=((Ze^1)&1)+k|0,(B|0)>(R|0)&(R|0)>-5?(L=m+-1|0,k=B+-1-R|0):(L=m+-2|0,k=B+-1|0),B=g&8,B)Le=B;else{if(nt&&(Mt=n[ft+-4>>2]|0,(Mt|0)!=0))if((Mt>>>0)%10|0)M=0;else{M=0,B=10;do B=B*10|0,M=M+1|0;while(!((Mt>>>0)%(B>>>0)|0|0))}else M=9;if(B=((ft-Ye>>2)*9|0)+-9|0,(L|32|0)==102){Le=B-M|0,Le=(Le|0)>0?Le:0,k=(k|0)<(Le|0)?k:Le,Le=0;break}else{Le=B+R-M|0,Le=(Le|0)>0?Le:0,k=(k|0)<(Le|0)?k:Le,Le=0;break}}else L=m,Le=g&8;while(!1);if(Te=k|Le,q=(Te|0)!=0&1,oe=(L|32|0)==102,oe)Ze=0,B=(R|0)>0?R:0;else{if(B=(R|0)<0?A:R,B=tE(B,((B|0)<0)<<31>>31,Tr)|0,M=Tr,(M-B|0)<2)do B=B+-1|0,s[B>>0]=48;while((M-B|0)<2);s[B+-1>>0]=(R>>31&2)+43,B=B+-2|0,s[B>>0]=L,Ze=B,B=M-B|0}if(B=fr+1+k+q+B|0,_s(o,32,u,B,g),bs(o,Gr,fr),_s(o,48,u,B,g^65536),oe){L=He>>>0>$t>>>0?$t:He,Le=cr+9|0,q=Le,oe=cr+8|0,M=L;do{if(R=tE(n[M>>2]|0,0,Le)|0,(M|0)==(L|0))(R|0)==(Le|0)&&(s[oe>>0]=48,R=oe);else if(R>>>0>cr>>>0){nE(cr|0,48,R-Hr|0)|0;do R=R+-1|0;while(R>>>0>cr>>>0)}bs(o,R,q-R|0),M=M+4|0}while(M>>>0<=$t>>>0);if(Te|0&&bs(o,5710,1),M>>>0>>0&(k|0)>0)for(;;){if(R=tE(n[M>>2]|0,0,Le)|0,R>>>0>cr>>>0){nE(cr|0,48,R-Hr|0)|0;do R=R+-1|0;while(R>>>0>cr>>>0)}if(bs(o,R,(k|0)<9?k:9),M=M+4|0,R=k+-9|0,M>>>0>>0&(k|0)>9)k=R;else{k=R;break}}_s(o,48,k+9|0,9,0)}else{if(Te=nt?ft:He+4|0,(k|0)>-1){nt=cr+9|0,Le=(Le|0)==0,A=nt,q=0-Hr|0,oe=cr+8|0,L=He;do{R=tE(n[L>>2]|0,0,nt)|0,(R|0)==(nt|0)&&(s[oe>>0]=48,R=oe);do if((L|0)==(He|0)){if(M=R+1|0,bs(o,R,1),Le&(k|0)<1){R=M;break}bs(o,5710,1),R=M}else{if(R>>>0<=cr>>>0)break;nE(cr|0,48,R+q|0)|0;do R=R+-1|0;while(R>>>0>cr>>>0)}while(!1);Hr=A-R|0,bs(o,R,(k|0)>(Hr|0)?Hr:k),k=k-Hr|0,L=L+4|0}while(L>>>0>>0&(k|0)>-1)}_s(o,48,k+18|0,18,0),bs(o,Ze,Tr-Ze|0)}_s(o,32,u,B,g^8192)}else cr=(m&32|0)!=0,B=fr+3|0,_s(o,32,u,B,g&-65537),bs(o,Gr,fr),bs(o,l!=l|!1?cr?5686:5690:cr?5678:5682,3),_s(o,32,u,B,g^8192);while(!1);return I=jn,((B|0)<(u|0)?u:B)|0}function qX(o){o=+o;var l=0;return E[S>>3]=o,l=n[S>>2]|0,Be=n[S+4>>2]|0,l|0}function nHe(o,l){return o=+o,l=l|0,+ +WX(o,l)}function WX(o,l){o=+o,l=l|0;var u=0,A=0,g=0;switch(E[S>>3]=o,u=n[S>>2]|0,A=n[S+4>>2]|0,g=qP(u|0,A|0,52)|0,g&2047){case 0:{o!=0?(o=+WX(o*18446744073709552e3,l),u=(n[l>>2]|0)+-64|0):u=0,n[l>>2]=u;break}case 2047:break;default:n[l>>2]=(g&2047)+-1022,n[S>>2]=u,n[S+4>>2]=A&-2146435073|1071644672,o=+E[S>>3]}return+o}function iHe(o,l,u){o=o|0,l=l|0,u=u|0;do if(o){if(l>>>0<128){s[o>>0]=l,o=1;break}if(!(n[n[(sHe()|0)+188>>2]>>2]|0))if((l&-128|0)==57216){s[o>>0]=l,o=1;break}else{n[(eE()|0)>>2]=84,o=-1;break}if(l>>>0<2048){s[o>>0]=l>>>6|192,s[o+1>>0]=l&63|128,o=2;break}if(l>>>0<55296|(l&-8192|0)==57344){s[o>>0]=l>>>12|224,s[o+1>>0]=l>>>6&63|128,s[o+2>>0]=l&63|128,o=3;break}if((l+-65536|0)>>>0<1048576){s[o>>0]=l>>>18|240,s[o+1>>0]=l>>>12&63|128,s[o+2>>0]=l>>>6&63|128,s[o+3>>0]=l&63|128,o=4;break}else{n[(eE()|0)>>2]=84,o=-1;break}}else o=1;while(!1);return o|0}function sHe(){return YM()|0}function oHe(){return YM()|0}function aHe(o,l){o=o|0,l=l|0;var u=0,A=0;for(A=0;;){if((c[5712+A>>0]|0)==(o|0)){o=2;break}if(u=A+1|0,(u|0)==87){u=5800,A=87,o=5;break}else A=u}if((o|0)==2&&(A?(u=5800,o=5):u=5800),(o|0)==5)for(;;){do o=u,u=u+1|0;while(s[o>>0]|0);if(A=A+-1|0,A)o=5;else break}return lHe(u,n[l+20>>2]|0)|0}function lHe(o,l){return o=o|0,l=l|0,cHe(o,l)|0}function cHe(o,l){return o=o|0,l=l|0,l?l=uHe(n[l>>2]|0,n[l+4>>2]|0,o)|0:l=0,(l|0?l:o)|0}function uHe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0;oe=(n[o>>2]|0)+1794895138|0,m=ug(n[o+8>>2]|0,oe)|0,A=ug(n[o+12>>2]|0,oe)|0,g=ug(n[o+16>>2]|0,oe)|0;e:do if(m>>>0>>2>>>0&&(q=l-(m<<2)|0,A>>>0>>0&g>>>0>>0)&&!((g|A)&3|0)){for(q=A>>>2,L=g>>>2,M=0;;){if(k=m>>>1,R=M+k|0,B=R<<1,g=B+q|0,A=ug(n[o+(g<<2)>>2]|0,oe)|0,g=ug(n[o+(g+1<<2)>>2]|0,oe)|0,!(g>>>0>>0&A>>>0<(l-g|0)>>>0)){A=0;break e}if(s[o+(g+A)>>0]|0){A=0;break e}if(A=UX(u,o+g|0)|0,!A)break;if(A=(A|0)<0,(m|0)==1){A=0;break e}else M=A?M:R,m=A?k:m-k|0}A=B+L|0,g=ug(n[o+(A<<2)>>2]|0,oe)|0,A=ug(n[o+(A+1<<2)>>2]|0,oe)|0,A>>>0>>0&g>>>0<(l-A|0)>>>0?A=s[o+(A+g)>>0]|0?0:o+A|0:A=0}else A=0;while(!1);return A|0}function ug(o,l){o=o|0,l=l|0;var u=0;return u=$X(o|0)|0,(l|0?u:o)|0}function fHe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0,k=0;A=u+16|0,g=n[A>>2]|0,g?m=5:AHe(u)|0?A=0:(g=n[A>>2]|0,m=5);e:do if((m|0)==5){if(k=u+20|0,B=n[k>>2]|0,A=B,(g-B|0)>>>0>>0){A=YP[n[u+36>>2]&7](u,o,l)|0;break}t:do if((s[u+75>>0]|0)>-1){for(B=l;;){if(!B){m=0,g=o;break t}if(g=B+-1|0,(s[o+g>>0]|0)==10)break;B=g}if(A=YP[n[u+36>>2]&7](u,o,B)|0,A>>>0>>0)break e;m=B,g=o+B|0,l=l-B|0,A=n[k>>2]|0}else m=0,g=o;while(!1);Rr(A|0,g|0,l|0)|0,n[k>>2]=(n[k>>2]|0)+l,A=m+l|0}while(!1);return A|0}function AHe(o){o=o|0;var l=0,u=0;return l=o+74|0,u=s[l>>0]|0,s[l>>0]=u+255|u,l=n[o>>2]|0,l&8?(n[o>>2]=l|32,o=-1):(n[o+8>>2]=0,n[o+4>>2]=0,u=n[o+44>>2]|0,n[o+28>>2]=u,n[o+20>>2]=u,n[o+16>>2]=u+(n[o+48>>2]|0),o=0),o|0}function ri(o,l){o=y(o),l=y(l);var u=0,A=0;u=YX(o)|0;do if((u&2147483647)>>>0<=2139095040){if(A=YX(l)|0,(A&2147483647)>>>0<=2139095040)if((A^u|0)<0){o=(u|0)<0?l:o;break}else{o=o>2]=o,n[S>>2]|0|0}function fg(o,l){o=y(o),l=y(l);var u=0,A=0;u=VX(o)|0;do if((u&2147483647)>>>0<=2139095040){if(A=VX(l)|0,(A&2147483647)>>>0<=2139095040)if((A^u|0)<0){o=(u|0)<0?o:l;break}else{o=o>2]=o,n[S>>2]|0|0}function JM(o,l){o=y(o),l=y(l);var u=0,A=0,g=0,m=0,B=0,k=0,R=0,M=0;m=(h[S>>2]=o,n[S>>2]|0),k=(h[S>>2]=l,n[S>>2]|0),u=m>>>23&255,B=k>>>23&255,R=m&-2147483648,g=k<<1;e:do if(g|0&&!((u|0)==255|((pHe(l)|0)&2147483647)>>>0>2139095040)){if(A=m<<1,A>>>0<=g>>>0)return l=y(o*y(0)),y((A|0)==(g|0)?l:o);if(u)A=m&8388607|8388608;else{if(u=m<<9,(u|0)>-1){A=u,u=0;do u=u+-1|0,A=A<<1;while((A|0)>-1)}else u=0;A=m<<1-u}if(B)k=k&8388607|8388608;else{if(m=k<<9,(m|0)>-1){g=0;do g=g+-1|0,m=m<<1;while((m|0)>-1)}else g=0;B=g,k=k<<1-g}g=A-k|0,m=(g|0)>-1;t:do if((u|0)>(B|0)){for(;;){if(m)if(g)A=g;else break;if(A=A<<1,u=u+-1|0,g=A-k|0,m=(g|0)>-1,(u|0)<=(B|0))break t}l=y(o*y(0));break e}while(!1);if(m)if(g)A=g;else{l=y(o*y(0));break}if(A>>>0<8388608)do A=A<<1,u=u+-1|0;while(A>>>0<8388608);(u|0)>0?u=A+-8388608|u<<23:u=A>>>(1-u|0),l=(n[S>>2]=u|R,y(h[S>>2]))}else M=3;while(!1);return(M|0)==3&&(l=y(o*l),l=y(l/l)),y(l)}function pHe(o){return o=y(o),h[S>>2]=o,n[S>>2]|0|0}function hHe(o,l){return o=o|0,l=l|0,_X(n[582]|0,o,l)|0}function an(o){o=o|0,Nt()}function rE(o){o=o|0}function dHe(o,l){return o=o|0,l=l|0,0}function gHe(o){return o=o|0,(JX(o+4|0)|0)==-1?(op[n[(n[o>>2]|0)+8>>2]&127](o),o=1):o=0,o|0}function JX(o){o=o|0;var l=0;return l=n[o>>2]|0,n[o>>2]=l+-1,l+-1|0}function jh(o){o=o|0,gHe(o)|0&&mHe(o)}function mHe(o){o=o|0;var l=0;l=o+8|0,n[l>>2]|0&&(JX(l)|0)!=-1||op[n[(n[o>>2]|0)+16>>2]&127](o)}function Kt(o){o=o|0;var l=0;for(l=o|0?o:1;o=_P(l)|0,!(o|0);){if(o=EHe()|0,!o){o=0;break}cZ[o&0]()}return o|0}function KX(o){return o=o|0,Kt(o)|0}function It(o){o=o|0,HP(o)}function yHe(o){o=o|0,(s[o+11>>0]|0)<0&&It(n[o>>2]|0)}function EHe(){var o=0;return o=n[2923]|0,n[2923]=o+0,o|0}function IHe(){}function GP(o,l,u,A){return o=o|0,l=l|0,u=u|0,A=A|0,A=l-A-(u>>>0>o>>>0|0)>>>0,Be=A,o-u>>>0|0|0}function KM(o,l,u,A){return o=o|0,l=l|0,u=u|0,A=A|0,u=o+u>>>0,Be=l+A+(u>>>0>>0|0)>>>0,u|0|0}function nE(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0,B=0;if(m=o+u|0,l=l&255,(u|0)>=67){for(;o&3;)s[o>>0]=l,o=o+1|0;for(A=m&-4|0,g=A-64|0,B=l|l<<8|l<<16|l<<24;(o|0)<=(g|0);)n[o>>2]=B,n[o+4>>2]=B,n[o+8>>2]=B,n[o+12>>2]=B,n[o+16>>2]=B,n[o+20>>2]=B,n[o+24>>2]=B,n[o+28>>2]=B,n[o+32>>2]=B,n[o+36>>2]=B,n[o+40>>2]=B,n[o+44>>2]=B,n[o+48>>2]=B,n[o+52>>2]=B,n[o+56>>2]=B,n[o+60>>2]=B,o=o+64|0;for(;(o|0)<(A|0);)n[o>>2]=B,o=o+4|0}for(;(o|0)<(m|0);)s[o>>0]=l,o=o+1|0;return m-u|0}function zX(o,l,u){return o=o|0,l=l|0,u=u|0,(u|0)<32?(Be=l<>>32-u,o<>>u,o>>>u|(l&(1<>>u-32|0)}function Rr(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,g=0,m=0;if((u|0)>=8192)return MA(o|0,l|0,u|0)|0;if(m=o|0,g=o+u|0,(o&3)==(l&3)){for(;o&3;){if(!u)return m|0;s[o>>0]=s[l>>0]|0,o=o+1|0,l=l+1|0,u=u-1|0}for(u=g&-4|0,A=u-64|0;(o|0)<=(A|0);)n[o>>2]=n[l>>2],n[o+4>>2]=n[l+4>>2],n[o+8>>2]=n[l+8>>2],n[o+12>>2]=n[l+12>>2],n[o+16>>2]=n[l+16>>2],n[o+20>>2]=n[l+20>>2],n[o+24>>2]=n[l+24>>2],n[o+28>>2]=n[l+28>>2],n[o+32>>2]=n[l+32>>2],n[o+36>>2]=n[l+36>>2],n[o+40>>2]=n[l+40>>2],n[o+44>>2]=n[l+44>>2],n[o+48>>2]=n[l+48>>2],n[o+52>>2]=n[l+52>>2],n[o+56>>2]=n[l+56>>2],n[o+60>>2]=n[l+60>>2],o=o+64|0,l=l+64|0;for(;(o|0)<(u|0);)n[o>>2]=n[l>>2],o=o+4|0,l=l+4|0}else for(u=g-4|0;(o|0)<(u|0);)s[o>>0]=s[l>>0]|0,s[o+1>>0]=s[l+1>>0]|0,s[o+2>>0]=s[l+2>>0]|0,s[o+3>>0]=s[l+3>>0]|0,o=o+4|0,l=l+4|0;for(;(o|0)<(g|0);)s[o>>0]=s[l>>0]|0,o=o+1|0,l=l+1|0;return m|0}function XX(o){o=o|0;var l=0;return l=s[O+(o&255)>>0]|0,(l|0)<8?l|0:(l=s[O+(o>>8&255)>>0]|0,(l|0)<8?l+8|0:(l=s[O+(o>>16&255)>>0]|0,(l|0)<8?l+16|0:(s[O+(o>>>24)>>0]|0)+24|0))}function ZX(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0;var m=0,B=0,k=0,R=0,M=0,L=0,q=0,oe=0,Ve=0,Le=0;if(L=o,R=l,M=R,B=u,oe=A,k=oe,!M)return m=(g|0)!=0,k?m?(n[g>>2]=o|0,n[g+4>>2]=l&0,oe=0,g=0,Be=oe,g|0):(oe=0,g=0,Be=oe,g|0):(m&&(n[g>>2]=(L>>>0)%(B>>>0),n[g+4>>2]=0),oe=0,g=(L>>>0)/(B>>>0)>>>0,Be=oe,g|0);m=(k|0)==0;do if(B){if(!m){if(m=(b(k|0)|0)-(b(M|0)|0)|0,m>>>0<=31){q=m+1|0,k=31-m|0,l=m-31>>31,B=q,o=L>>>(q>>>0)&l|M<>>(q>>>0)&l,m=0,k=L<>2]=o|0,n[g+4>>2]=R|l&0,oe=0,g=0,Be=oe,g|0):(oe=0,g=0,Be=oe,g|0)}if(m=B-1|0,m&B|0){k=(b(B|0)|0)+33-(b(M|0)|0)|0,Le=64-k|0,q=32-k|0,R=q>>31,Ve=k-32|0,l=Ve>>31,B=k,o=q-1>>31&M>>>(Ve>>>0)|(M<>>(k>>>0))&l,l=l&M>>>(k>>>0),m=L<>>(Ve>>>0))&R|L<>31;break}return g|0&&(n[g>>2]=m&L,n[g+4>>2]=0),(B|0)==1?(Ve=R|l&0,Le=o|0|0,Be=Ve,Le|0):(Le=XX(B|0)|0,Ve=M>>>(Le>>>0)|0,Le=M<<32-Le|L>>>(Le>>>0)|0,Be=Ve,Le|0)}else{if(m)return g|0&&(n[g>>2]=(M>>>0)%(B>>>0),n[g+4>>2]=0),Ve=0,Le=(M>>>0)/(B>>>0)>>>0,Be=Ve,Le|0;if(!L)return g|0&&(n[g>>2]=0,n[g+4>>2]=(M>>>0)%(k>>>0)),Ve=0,Le=(M>>>0)/(k>>>0)>>>0,Be=Ve,Le|0;if(m=k-1|0,!(m&k))return g|0&&(n[g>>2]=o|0,n[g+4>>2]=m&M|l&0),Ve=0,Le=M>>>((XX(k|0)|0)>>>0),Be=Ve,Le|0;if(m=(b(k|0)|0)-(b(M|0)|0)|0,m>>>0<=30){l=m+1|0,k=31-m|0,B=l,o=M<>>(l>>>0),l=M>>>(l>>>0),m=0,k=L<>2]=o|0,n[g+4>>2]=R|l&0,Ve=0,Le=0,Be=Ve,Le|0):(Ve=0,Le=0,Be=Ve,Le|0)}while(!1);if(!B)M=k,R=0,k=0;else{q=u|0|0,L=oe|A&0,M=KM(q|0,L|0,-1,-1)|0,u=Be,R=k,k=0;do A=R,R=m>>>31|R<<1,m=k|m<<1,A=o<<1|A>>>31|0,oe=o>>>31|l<<1|0,GP(M|0,u|0,A|0,oe|0)|0,Le=Be,Ve=Le>>31|((Le|0)<0?-1:0)<<1,k=Ve&1,o=GP(A|0,oe|0,Ve&q|0,(((Le|0)<0?-1:0)>>31|((Le|0)<0?-1:0)<<1)&L|0)|0,l=Be,B=B-1|0;while(B|0);M=R,R=0}return B=0,g|0&&(n[g>>2]=o,n[g+4>>2]=l),Ve=(m|0)>>>31|(M|B)<<1|(B<<1|m>>>31)&0|R,Le=(m<<1|0)&-2|k,Be=Ve,Le|0}function zM(o,l,u,A){return o=o|0,l=l|0,u=u|0,A=A|0,ZX(o,l,u,A,0)|0}function Gh(o){o=o|0;var l=0,u=0;return u=o+15&-16|0,l=n[C>>2]|0,o=l+u|0,(u|0)>0&(o|0)<(l|0)|(o|0)<0?(se()|0,cu(12),-1):(n[C>>2]=o,(o|0)>($()|0)&&!(X()|0)?(n[C>>2]=l,cu(12),-1):l|0)}function B2(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;if((l|0)<(o|0)&(o|0)<(l+u|0)){for(A=o,l=l+u|0,o=o+u|0;(u|0)>0;)o=o-1|0,l=l-1|0,u=u-1|0,s[o>>0]=s[l>>0]|0;o=A}else Rr(o,l,u)|0;return o|0}function XM(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var g=0,m=0;return m=I,I=I+16|0,g=m|0,ZX(o,l,u,A,g)|0,I=m,Be=n[g+4>>2]|0,n[g>>2]|0|0}function $X(o){return o=o|0,(o&255)<<24|(o>>8&255)<<16|(o>>16&255)<<8|o>>>24|0}function CHe(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0,eZ[o&1](l|0,u|0,A|0,g|0,m|0)}function wHe(o,l,u){o=o|0,l=l|0,u=y(u),tZ[o&1](l|0,y(u))}function BHe(o,l,u){o=o|0,l=l|0,u=+u,rZ[o&31](l|0,+u)}function vHe(o,l,u,A){return o=o|0,l=l|0,u=y(u),A=y(A),y(nZ[o&0](l|0,y(u),y(A)))}function SHe(o,l){o=o|0,l=l|0,op[o&127](l|0)}function DHe(o,l,u){o=o|0,l=l|0,u=u|0,ap[o&31](l|0,u|0)}function bHe(o,l){return o=o|0,l=l|0,pg[o&31](l|0)|0}function PHe(o,l,u,A,g){o=o|0,l=l|0,u=+u,A=+A,g=g|0,iZ[o&1](l|0,+u,+A,g|0)}function xHe(o,l,u,A){o=o|0,l=l|0,u=+u,A=+A,cje[o&1](l|0,+u,+A)}function kHe(o,l,u,A){return o=o|0,l=l|0,u=u|0,A=A|0,YP[o&7](l|0,u|0,A|0)|0}function QHe(o,l,u,A){return o=o|0,l=l|0,u=u|0,A=A|0,+uje[o&1](l|0,u|0,A|0)}function RHe(o,l){return o=o|0,l=l|0,+sZ[o&15](l|0)}function THe(o,l,u){return o=o|0,l=l|0,u=+u,fje[o&1](l|0,+u)|0}function FHe(o,l,u){return o=o|0,l=l|0,u=u|0,$M[o&15](l|0,u|0)|0}function NHe(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=+A,g=+g,m=m|0,Aje[o&1](l|0,u|0,+A,+g,m|0)}function OHe(o,l,u,A,g,m,B){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0,B=B|0,pje[o&1](l|0,u|0,A|0,g|0,m|0,B|0)}function LHe(o,l,u){return o=o|0,l=l|0,u=u|0,+oZ[o&7](l|0,u|0)}function MHe(o){return o=o|0,VP[o&7]()|0}function UHe(o,l,u,A,g,m){return o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0,aZ[o&1](l|0,u|0,A|0,g|0,m|0)|0}function _He(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=+g,hje[o&1](l|0,u|0,A|0,+g)}function HHe(o,l,u,A,g,m,B){o=o|0,l=l|0,u=u|0,A=y(A),g=g|0,m=y(m),B=B|0,lZ[o&1](l|0,u|0,y(A),g|0,y(m),B|0)}function jHe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,D2[o&15](l|0,u|0,A|0)}function GHe(o){o=o|0,cZ[o&0]()}function qHe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=+A,uZ[o&15](l|0,u|0,+A)}function WHe(o,l,u){return o=o|0,l=+l,u=+u,dje[o&1](+l,+u)|0}function YHe(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,eU[o&15](l|0,u|0,A|0,g|0)}function VHe(o,l,u,A,g){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,F(0)}function JHe(o,l){o=o|0,l=y(l),F(1)}function sl(o,l){o=o|0,l=+l,F(2)}function KHe(o,l,u){return o=o|0,l=y(l),u=y(u),F(3),$e}function Br(o){o=o|0,F(4)}function v2(o,l){o=o|0,l=l|0,F(5)}function Gl(o){return o=o|0,F(6),0}function zHe(o,l,u,A){o=o|0,l=+l,u=+u,A=A|0,F(7)}function XHe(o,l,u){o=o|0,l=+l,u=+u,F(8)}function ZHe(o,l,u){return o=o|0,l=l|0,u=u|0,F(9),0}function $He(o,l,u){return o=o|0,l=l|0,u=u|0,F(10),0}function Ag(o){return o=o|0,F(11),0}function eje(o,l){return o=o|0,l=+l,F(12),0}function S2(o,l){return o=o|0,l=l|0,F(13),0}function tje(o,l,u,A,g){o=o|0,l=l|0,u=+u,A=+A,g=g|0,F(14)}function rje(o,l,u,A,g,m){o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,m=m|0,F(15)}function ZM(o,l){return o=o|0,l=l|0,F(16),0}function nje(){return F(17),0}function ije(o,l,u,A,g){return o=o|0,l=l|0,u=u|0,A=A|0,g=g|0,F(18),0}function sje(o,l,u,A){o=o|0,l=l|0,u=u|0,A=+A,F(19)}function oje(o,l,u,A,g,m){o=o|0,l=l|0,u=y(u),A=A|0,g=y(g),m=m|0,F(20)}function WP(o,l,u){o=o|0,l=l|0,u=u|0,F(21)}function aje(){F(22)}function iE(o,l,u){o=o|0,l=l|0,u=+u,F(23)}function lje(o,l){return o=+o,l=+l,F(24),0}function sE(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,F(25)}var eZ=[VHe,i_e],tZ=[JHe,Ny],rZ=[sl,zd,Th,o2,a2,l2,c2,kf,Gy,u2,Qf,Xd,Zd,f2,A2,Iu,$d,p2,qy,sl,sl,sl,sl,sl,sl,sl,sl,sl,sl,sl,sl,sl],nZ=[KHe],op=[Br,rE,MPe,UPe,_Pe,dRe,gRe,mRe,TMe,FMe,NMe,WUe,YUe,VUe,p8e,h8e,d8e,kl,Kd,r2,sr,gc,xP,kP,PPe,JPe,oxe,vxe,_xe,nke,Ike,Nke,zke,AQe,PQe,qQe,oRe,FRe,zRe,ATe,PTe,qTe,oFe,SFe,_Fe,eNe,gNe,gP,VNe,cOe,POe,YOe,aLe,PLe,MLe,HLe,iMe,aMe,vMe,LMe,_Me,nUe,CUe,_K,r4e,T4e,J4e,c3e,Q3e,Y3e,n8e,o8e,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br,Br],ap=[v2,_y,QL,n2,i2,xr,fo,Xi,Ms,Ss,jy,Rh,d2,CP,rg,FL,NL,wP,BP,ML,Rf,ne,xFe,GFe,zOe,s4e,xUe,vX,v2,v2,v2,v2],pg=[Gl,q8e,My,tg,Yy,Ia,mP,Fh,h2,TL,EP,Vy,vP,UL,zy,ENe,fLe,oUe,c4e,_l,Gl,Gl,Gl,Gl,Gl,Gl,Gl,Gl,Gl,Gl,Gl,Gl],iZ=[zHe,WL],cje=[XHe,bMe],YP=[ZHe,MX,W8e,J8e,ake,MRe,XNe,p3e],uje=[$He,RQe],sZ=[Ag,Nh,IP,tp,YL,v,D,Q,H,Y,Ag,Ag,Ag,Ag,Ag,Ag],fje=[eje,NLe],$M=[S2,dHe,SP,RPe,Pxe,Bke,Mke,uRe,eTe,iNe,Oy,Z4e,S2,S2,S2,S2],Aje=[tje,uxe],pje=[rje,N3e],oZ=[ZM,OL,ve,_e,ht,JQe,ZM,ZM],VP=[nje,Wt,Ly,dP,WLe,fMe,qMe,u8e],aZ=[ije,Py],hje=[sje,RTe],lZ=[oje,_L],D2=[WP,Oo,yP,LL,wu,qxe,$ke,JTe,uFe,kL,b_e,L4e,z3e,WP,WP,WP],cZ=[aje],uZ=[iE,RL,Hy,ep,s2,Cu,Wy,eg,gTe,pOe,QLe,iE,iE,iE,iE,iE],dje=[lje,QMe],eU=[sE,gQe,bNe,ROe,ILe,XLe,mMe,XMe,DUe,d4e,C8e,sE,sE,sE,sE,sE];return{_llvm_bswap_i32:$X,dynCall_idd:WHe,dynCall_i:MHe,_i64Subtract:GP,___udivdi3:zM,dynCall_vif:wHe,setThrew:ha,dynCall_viii:jHe,_bitshift64Lshr:qP,_bitshift64Shl:zX,dynCall_vi:SHe,dynCall_viiddi:NHe,dynCall_diii:QHe,dynCall_iii:FHe,_memset:nE,_sbrk:Gh,_memcpy:Rr,__GLOBAL__sub_I_Yoga_cpp:$1,dynCall_vii:DHe,___uremdi3:XM,dynCall_vid:BHe,stackAlloc:Ja,_nbind_init:R8e,getTempRet0:_A,dynCall_di:RHe,dynCall_iid:THe,setTempRet0:UA,_i64Add:KM,dynCall_fiff:vHe,dynCall_iiii:kHe,_emscripten_get_global_libc:G8e,dynCall_viid:qHe,dynCall_viiid:_He,dynCall_viififi:HHe,dynCall_ii:bHe,__GLOBAL__sub_I_Binding_cc:J_e,dynCall_viiii:YHe,dynCall_iiiiii:UHe,stackSave:mf,dynCall_viiiii:CHe,__GLOBAL__sub_I_nbind_cc:Sr,dynCall_vidd:xHe,_free:HP,runPostSets:IHe,dynCall_viiiiii:OHe,establishStackSpace:vn,_memmove:B2,stackRestore:uc,_malloc:_P,__GLOBAL__sub_I_common_cc:hUe,dynCall_viddi:PHe,dynCall_dii:LHe,dynCall_v:GHe}}(Module.asmGlobalArg,Module.asmLibraryArg,buffer),_llvm_bswap_i32=Module._llvm_bswap_i32=asm._llvm_bswap_i32,getTempRet0=Module.getTempRet0=asm.getTempRet0,___udivdi3=Module.___udivdi3=asm.___udivdi3,setThrew=Module.setThrew=asm.setThrew,_bitshift64Lshr=Module._bitshift64Lshr=asm._bitshift64Lshr,_bitshift64Shl=Module._bitshift64Shl=asm._bitshift64Shl,_memset=Module._memset=asm._memset,_sbrk=Module._sbrk=asm._sbrk,_memcpy=Module._memcpy=asm._memcpy,stackAlloc=Module.stackAlloc=asm.stackAlloc,___uremdi3=Module.___uremdi3=asm.___uremdi3,_nbind_init=Module._nbind_init=asm._nbind_init,_i64Subtract=Module._i64Subtract=asm._i64Subtract,setTempRet0=Module.setTempRet0=asm.setTempRet0,_i64Add=Module._i64Add=asm._i64Add,_emscripten_get_global_libc=Module._emscripten_get_global_libc=asm._emscripten_get_global_libc,__GLOBAL__sub_I_Yoga_cpp=Module.__GLOBAL__sub_I_Yoga_cpp=asm.__GLOBAL__sub_I_Yoga_cpp,__GLOBAL__sub_I_Binding_cc=Module.__GLOBAL__sub_I_Binding_cc=asm.__GLOBAL__sub_I_Binding_cc,stackSave=Module.stackSave=asm.stackSave,__GLOBAL__sub_I_nbind_cc=Module.__GLOBAL__sub_I_nbind_cc=asm.__GLOBAL__sub_I_nbind_cc,_free=Module._free=asm._free,runPostSets=Module.runPostSets=asm.runPostSets,establishStackSpace=Module.establishStackSpace=asm.establishStackSpace,_memmove=Module._memmove=asm._memmove,stackRestore=Module.stackRestore=asm.stackRestore,_malloc=Module._malloc=asm._malloc,__GLOBAL__sub_I_common_cc=Module.__GLOBAL__sub_I_common_cc=asm.__GLOBAL__sub_I_common_cc,dynCall_viiiii=Module.dynCall_viiiii=asm.dynCall_viiiii,dynCall_vif=Module.dynCall_vif=asm.dynCall_vif,dynCall_vid=Module.dynCall_vid=asm.dynCall_vid,dynCall_fiff=Module.dynCall_fiff=asm.dynCall_fiff,dynCall_vi=Module.dynCall_vi=asm.dynCall_vi,dynCall_vii=Module.dynCall_vii=asm.dynCall_vii,dynCall_ii=Module.dynCall_ii=asm.dynCall_ii,dynCall_viddi=Module.dynCall_viddi=asm.dynCall_viddi,dynCall_vidd=Module.dynCall_vidd=asm.dynCall_vidd,dynCall_iiii=Module.dynCall_iiii=asm.dynCall_iiii,dynCall_diii=Module.dynCall_diii=asm.dynCall_diii,dynCall_di=Module.dynCall_di=asm.dynCall_di,dynCall_iid=Module.dynCall_iid=asm.dynCall_iid,dynCall_iii=Module.dynCall_iii=asm.dynCall_iii,dynCall_viiddi=Module.dynCall_viiddi=asm.dynCall_viiddi,dynCall_viiiiii=Module.dynCall_viiiiii=asm.dynCall_viiiiii,dynCall_dii=Module.dynCall_dii=asm.dynCall_dii,dynCall_i=Module.dynCall_i=asm.dynCall_i,dynCall_iiiiii=Module.dynCall_iiiiii=asm.dynCall_iiiiii,dynCall_viiid=Module.dynCall_viiid=asm.dynCall_viiid,dynCall_viififi=Module.dynCall_viififi=asm.dynCall_viififi,dynCall_viii=Module.dynCall_viii=asm.dynCall_viii,dynCall_v=Module.dynCall_v=asm.dynCall_v,dynCall_viid=Module.dynCall_viid=asm.dynCall_viid,dynCall_idd=Module.dynCall_idd=asm.dynCall_idd,dynCall_viiii=Module.dynCall_viiii=asm.dynCall_viiii;Runtime.stackAlloc=Module.stackAlloc,Runtime.stackSave=Module.stackSave,Runtime.stackRestore=Module.stackRestore,Runtime.establishStackSpace=Module.establishStackSpace,Runtime.setTempRet0=Module.setTempRet0,Runtime.getTempRet0=Module.getTempRet0,Module.asm=asm;function ExitStatus(e){this.name="ExitStatus",this.message="Program terminated with exit("+e+")",this.status=e}ExitStatus.prototype=new Error,ExitStatus.prototype.constructor=ExitStatus;var initialStackTop,preloadStartTime=null,calledMain=!1;dependenciesFulfilled=function e(){Module.calledRun||run(),Module.calledRun||(dependenciesFulfilled=e)},Module.callMain=Module.callMain=function e(t){t=t||[],ensureInitRuntime();var r=t.length+1;function s(){for(var p=0;p<3;p++)a.push(0)}var a=[allocate(intArrayFromString(Module.thisProgram),"i8",ALLOC_NORMAL)];s();for(var n=0;n0||(preRun(),runDependencies>0)||Module.calledRun)return;function t(){Module.calledRun||(Module.calledRun=!0,!ABORT&&(ensureInitRuntime(),preMain(),Module.onRuntimeInitialized&&Module.onRuntimeInitialized(),Module._main&&shouldRunNow&&Module.callMain(e),postRun()))}Module.setStatus?(Module.setStatus("Running..."),setTimeout(function(){setTimeout(function(){Module.setStatus("")},1),t()},1)):t()}Module.run=Module.run=run;function exit(e,t){t&&Module.noExitRuntime||(Module.noExitRuntime||(ABORT=!0,EXITSTATUS=e,STACKTOP=initialStackTop,exitRuntime(),Module.onExit&&Module.onExit(e)),ENVIRONMENT_IS_NODE&&process.exit(e),Module.quit(e,new ExitStatus(e)))}Module.exit=Module.exit=exit;var abortDecorators=[];function abort(e){Module.onAbort&&Module.onAbort(e),e!==void 0?(Module.print(e),Module.printErr(e),e=JSON.stringify(e)):e="",ABORT=!0,EXITSTATUS=1;var t=` If this abort() is unexpected, build with -s ASSERTIONS=1 which can give more information.`,r="abort("+e+") at "+stackTrace()+t;throw abortDecorators&&abortDecorators.forEach(function(s){r=s(r,e)}),r}if(Module.abort=Module.abort=abort,Module.preInit)for(typeof Module.preInit=="function"&&(Module.preInit=[Module.preInit]);Module.preInit.length>0;)Module.preInit.pop()();var shouldRunNow=!0;Module.noInitialRun&&(shouldRunNow=!1),run()})});var Lm=G((FVt,wIe)=>{"use strict";var Gut=IIe(),qut=CIe(),hq=!1,dq=null;qut({},function(e,t){if(!hq){if(hq=!0,e)throw e;dq=t}});if(!hq)throw new Error("Failed to load the yoga module - it needed to be loaded synchronously, but didn't");wIe.exports=Gut(dq.bind,dq.lib)});var mq=G((NVt,gq)=>{"use strict";var BIe=e=>Number.isNaN(e)?!1:e>=4352&&(e<=4447||e===9001||e===9002||11904<=e&&e<=12871&&e!==12351||12880<=e&&e<=19903||19968<=e&&e<=42182||43360<=e&&e<=43388||44032<=e&&e<=55203||63744<=e&&e<=64255||65040<=e&&e<=65049||65072<=e&&e<=65131||65281<=e&&e<=65376||65504<=e&&e<=65510||110592<=e&&e<=110593||127488<=e&&e<=127569||131072<=e&&e<=262141);gq.exports=BIe;gq.exports.default=BIe});var SIe=G((OVt,vIe)=>{"use strict";vIe.exports=function(){return/\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62(?:\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74|\uDB40\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F|\uD83D\uDC68(?:\uD83C\uDFFC\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68\uD83C\uDFFB|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFE])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFD])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFC])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83D\uDC68|(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D[\uDC66\uDC67])|[\u2695\u2696\u2708]\uFE0F|\uD83D[\uDC66\uDC67]|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|(?:\uD83C\uDFFB\u200D[\u2695\u2696\u2708]|\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708])\uFE0F|\uD83C\uDFFB\u200D(?:\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C[\uDFFB-\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFB\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDD1D\u200D\uD83D\uDC69)\uD83C\uDFFB|\uD83E\uDDD1(?:\uD83C\uDFFF\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1(?:\uD83C[\uDFFB-\uDFFF])|\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1)|(?:\uD83E\uDDD1\uD83C\uDFFE\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB-\uDFFE])|(?:\uD83E\uDDD1\uD83C\uDFFC\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDD1D\u200D\uD83D\uDC69)(?:\uD83C[\uDFFB\uDFFC])|\uD83D\uDC69(?:\uD83C\uDFFE\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFB\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFC-\uDFFF])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|(?:\uD83E\uDDD1\uD83C\uDFFD\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDD1D\u200D\uD83D\uDC69)(?:\uD83C[\uDFFB-\uDFFD])|\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D[\uDC66\uDC67])|(?:\uD83D\uDC41\uFE0F\u200D\uD83D\uDDE8|\uD83D\uDC69(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\uD83C\uDFFB\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])|(?:(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)\uFE0F|\uD83D\uDC6F|\uD83E[\uDD3C\uDDDE\uDDDF])\u200D[\u2640\u2642]|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642]|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD6-\uDDDD])(?:(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642]|\u200D[\u2640\u2642])|\uD83C\uDFF4\u200D\u2620)\uFE0F|\uD83D\uDC69\u200D\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08|\uD83D\uDC15\u200D\uD83E\uDDBA|\uD83D\uDC69\u200D\uD83D\uDC66|\uD83D\uDC69\u200D\uD83D\uDC67|\uD83C\uDDFD\uD83C\uDDF0|\uD83C\uDDF4\uD83C\uDDF2|\uD83C\uDDF6\uD83C\uDDE6|[#\*0-9]\uFE0F\u20E3|\uD83C\uDDE7(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF])|\uD83C\uDDF9(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF])|\uD83C\uDDEA(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA])|\uD83E\uDDD1(?:\uD83C[\uDFFB-\uDFFF])|\uD83C\uDDF7(?:\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC])|\uD83D\uDC69(?:\uD83C[\uDFFB-\uDFFF])|\uD83C\uDDF2(?:\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF])|\uD83C\uDDE6(?:\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF])|\uD83C\uDDF0(?:\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF])|\uD83C\uDDED(?:\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA])|\uD83C\uDDE9(?:\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF])|\uD83C\uDDFE(?:\uD83C[\uDDEA\uDDF9])|\uD83C\uDDEC(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE])|\uD83C\uDDF8(?:\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF])|\uD83C\uDDEB(?:\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7])|\uD83C\uDDF5(?:\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE])|\uD83C\uDDFB(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA])|\uD83C\uDDF3(?:\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF])|\uD83C\uDDE8(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF])|\uD83C\uDDF1(?:\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE])|\uD83C\uDDFF(?:\uD83C[\uDDE6\uDDF2\uDDFC])|\uD83C\uDDFC(?:\uD83C[\uDDEB\uDDF8])|\uD83C\uDDFA(?:\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF])|\uD83C\uDDEE(?:\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9])|\uD83C\uDDEF(?:\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5])|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uD83C[\uDFFB-\uDFFF])|(?:[\u261D\u270A-\u270D]|\uD83C[\uDF85\uDFC2\uDFC7]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC70\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDCAA\uDD74\uDD7A\uDD90\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC]|\uD83E[\uDD0F\uDD18-\uDD1C\uDD1E\uDD1F\uDD30-\uDD36\uDDB5\uDDB6\uDDBB\uDDD2-\uDDD5])(?:\uD83C[\uDFFB-\uDFFF])|(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDED5\uDEEB\uDEEC\uDEF4-\uDEFA\uDFE0-\uDFEB]|\uD83E[\uDD0D-\uDD3A\uDD3C-\uDD45\uDD47-\uDD71\uDD73-\uDD76\uDD7A-\uDDA2\uDDA5-\uDDAA\uDDAE-\uDDCA\uDDCD-\uDDFF\uDE70-\uDE73\uDE78-\uDE7A\uDE80-\uDE82\uDE90-\uDE95])|(?:[#\*0-9\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDCCF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE1A\uDE2F\uDE32-\uDE3A\uDE50\uDE51\uDF00-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFF]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDED5\uDEE0-\uDEE5\uDEE9\uDEEB\uDEEC\uDEF0\uDEF3-\uDEFA\uDFE0-\uDFEB]|\uD83E[\uDD0D-\uDD3A\uDD3C-\uDD45\uDD47-\uDD71\uDD73-\uDD76\uDD7A-\uDDA2\uDDA5-\uDDAA\uDDAE-\uDDCA\uDDCD-\uDDFF\uDE70-\uDE73\uDE78-\uDE7A\uDE80-\uDE82\uDE90-\uDE95])\uFE0F|(?:[\u261D\u26F9\u270A-\u270D]|\uD83C[\uDF85\uDFC2-\uDFC4\uDFC7\uDFCA-\uDFCC]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66-\uDC78\uDC7C\uDC81-\uDC83\uDC85-\uDC87\uDC8F\uDC91\uDCAA\uDD74\uDD75\uDD7A\uDD90\uDD95\uDD96\uDE45-\uDE47\uDE4B-\uDE4F\uDEA3\uDEB4-\uDEB6\uDEC0\uDECC]|\uD83E[\uDD0F\uDD18-\uDD1F\uDD26\uDD30-\uDD39\uDD3C-\uDD3E\uDDB5\uDDB6\uDDB8\uDDB9\uDDBB\uDDCD-\uDDCF\uDDD1-\uDDDD])/g}});var GS=G((LVt,yq)=>{"use strict";var Wut=dk(),Yut=mq(),Vut=SIe(),DIe=e=>{if(typeof e!="string"||e.length===0||(e=Wut(e),e.length===0))return 0;e=e.replace(Vut()," ");let t=0;for(let r=0;r=127&&s<=159||s>=768&&s<=879||(s>65535&&r++,t+=Yut(s)?2:1)}return t};yq.exports=DIe;yq.exports.default=DIe});var Iq=G((MVt,Eq)=>{"use strict";var Jut=GS(),bIe=e=>{let t=0;for(let r of e.split(` `))t=Math.max(t,Jut(r));return t};Eq.exports=bIe;Eq.exports.default=bIe});var PIe=G(qS=>{"use strict";var Kut=qS&&qS.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(qS,"__esModule",{value:!0});var zut=Kut(Iq()),Cq={};qS.default=e=>{if(e.length===0)return{width:0,height:0};if(Cq[e])return Cq[e];let t=zut.default(e),r=e.split(` `).length;return Cq[e]={width:t,height:r},{width:t,height:r}}});var xIe=G(WS=>{"use strict";var Xut=WS&&WS.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(WS,"__esModule",{value:!0});var xn=Xut(Lm()),Zut=(e,t)=>{"position"in t&&e.setPositionType(t.position==="absolute"?xn.default.POSITION_TYPE_ABSOLUTE:xn.default.POSITION_TYPE_RELATIVE)},$ut=(e,t)=>{"marginLeft"in t&&e.setMargin(xn.default.EDGE_START,t.marginLeft||0),"marginRight"in t&&e.setMargin(xn.default.EDGE_END,t.marginRight||0),"marginTop"in t&&e.setMargin(xn.default.EDGE_TOP,t.marginTop||0),"marginBottom"in t&&e.setMargin(xn.default.EDGE_BOTTOM,t.marginBottom||0)},eft=(e,t)=>{"paddingLeft"in t&&e.setPadding(xn.default.EDGE_LEFT,t.paddingLeft||0),"paddingRight"in t&&e.setPadding(xn.default.EDGE_RIGHT,t.paddingRight||0),"paddingTop"in t&&e.setPadding(xn.default.EDGE_TOP,t.paddingTop||0),"paddingBottom"in t&&e.setPadding(xn.default.EDGE_BOTTOM,t.paddingBottom||0)},tft=(e,t)=>{var r;"flexGrow"in t&&e.setFlexGrow((r=t.flexGrow)!==null&&r!==void 0?r:0),"flexShrink"in t&&e.setFlexShrink(typeof t.flexShrink=="number"?t.flexShrink:1),"flexDirection"in t&&(t.flexDirection==="row"&&e.setFlexDirection(xn.default.FLEX_DIRECTION_ROW),t.flexDirection==="row-reverse"&&e.setFlexDirection(xn.default.FLEX_DIRECTION_ROW_REVERSE),t.flexDirection==="column"&&e.setFlexDirection(xn.default.FLEX_DIRECTION_COLUMN),t.flexDirection==="column-reverse"&&e.setFlexDirection(xn.default.FLEX_DIRECTION_COLUMN_REVERSE)),"flexBasis"in t&&(typeof t.flexBasis=="number"?e.setFlexBasis(t.flexBasis):typeof t.flexBasis=="string"?e.setFlexBasisPercent(Number.parseInt(t.flexBasis,10)):e.setFlexBasis(NaN)),"alignItems"in t&&((t.alignItems==="stretch"||!t.alignItems)&&e.setAlignItems(xn.default.ALIGN_STRETCH),t.alignItems==="flex-start"&&e.setAlignItems(xn.default.ALIGN_FLEX_START),t.alignItems==="center"&&e.setAlignItems(xn.default.ALIGN_CENTER),t.alignItems==="flex-end"&&e.setAlignItems(xn.default.ALIGN_FLEX_END)),"alignSelf"in t&&((t.alignSelf==="auto"||!t.alignSelf)&&e.setAlignSelf(xn.default.ALIGN_AUTO),t.alignSelf==="flex-start"&&e.setAlignSelf(xn.default.ALIGN_FLEX_START),t.alignSelf==="center"&&e.setAlignSelf(xn.default.ALIGN_CENTER),t.alignSelf==="flex-end"&&e.setAlignSelf(xn.default.ALIGN_FLEX_END)),"justifyContent"in t&&((t.justifyContent==="flex-start"||!t.justifyContent)&&e.setJustifyContent(xn.default.JUSTIFY_FLEX_START),t.justifyContent==="center"&&e.setJustifyContent(xn.default.JUSTIFY_CENTER),t.justifyContent==="flex-end"&&e.setJustifyContent(xn.default.JUSTIFY_FLEX_END),t.justifyContent==="space-between"&&e.setJustifyContent(xn.default.JUSTIFY_SPACE_BETWEEN),t.justifyContent==="space-around"&&e.setJustifyContent(xn.default.JUSTIFY_SPACE_AROUND))},rft=(e,t)=>{var r,s;"width"in t&&(typeof t.width=="number"?e.setWidth(t.width):typeof t.width=="string"?e.setWidthPercent(Number.parseInt(t.width,10)):e.setWidthAuto()),"height"in t&&(typeof t.height=="number"?e.setHeight(t.height):typeof t.height=="string"?e.setHeightPercent(Number.parseInt(t.height,10)):e.setHeightAuto()),"minWidth"in t&&(typeof t.minWidth=="string"?e.setMinWidthPercent(Number.parseInt(t.minWidth,10)):e.setMinWidth((r=t.minWidth)!==null&&r!==void 0?r:0)),"minHeight"in t&&(typeof t.minHeight=="string"?e.setMinHeightPercent(Number.parseInt(t.minHeight,10)):e.setMinHeight((s=t.minHeight)!==null&&s!==void 0?s:0))},nft=(e,t)=>{"display"in t&&e.setDisplay(t.display==="flex"?xn.default.DISPLAY_FLEX:xn.default.DISPLAY_NONE)},ift=(e,t)=>{if("borderStyle"in t){let r=typeof t.borderStyle=="string"?1:0;e.setBorder(xn.default.EDGE_TOP,r),e.setBorder(xn.default.EDGE_BOTTOM,r),e.setBorder(xn.default.EDGE_LEFT,r),e.setBorder(xn.default.EDGE_RIGHT,r)}};WS.default=(e,t={})=>{Zut(e,t),$ut(e,t),eft(e,t),tft(e,t),rft(e,t),nft(e,t),ift(e,t)}});var RIe=G((HVt,QIe)=>{"use strict";var YS=GS(),sft=dk(),oft=ik(),Bq=new Set(["\x1B","\x9B"]),aft=39,kIe=e=>`${Bq.values().next().value}[${e}m`,lft=e=>e.split(" ").map(t=>YS(t)),wq=(e,t,r)=>{let s=[...t],a=!1,n=YS(sft(e[e.length-1]));for(let[c,f]of s.entries()){let p=YS(f);if(n+p<=r?e[e.length-1]+=f:(e.push(f),n=0),Bq.has(f))a=!0;else if(a&&f==="m"){a=!1;continue}a||(n+=p,n===r&&c0&&e.length>1&&(e[e.length-2]+=e.pop())},cft=e=>{let t=e.split(" "),r=t.length;for(;r>0&&!(YS(t[r-1])>0);)r--;return r===t.length?e:t.slice(0,r).join(" ")+t.slice(r).join("")},uft=(e,t,r={})=>{if(r.trim!==!1&&e.trim()==="")return"";let s="",a="",n,c=lft(e),f=[""];for(let[p,h]of e.split(" ").entries()){r.trim!==!1&&(f[f.length-1]=f[f.length-1].trimLeft());let E=YS(f[f.length-1]);if(p!==0&&(E>=t&&(r.wordWrap===!1||r.trim===!1)&&(f.push(""),E=0),(E>0||r.trim===!1)&&(f[f.length-1]+=" ",E++)),r.hard&&c[p]>t){let C=t-E,S=1+Math.floor((c[p]-C-1)/t);Math.floor((c[p]-1)/t)t&&E>0&&c[p]>0){if(r.wordWrap===!1&&Et&&r.wordWrap===!1){wq(f,h,t);continue}f[f.length-1]+=h}r.trim!==!1&&(f=f.map(cft)),s=f.join(` `);for(let[p,h]of[...s].entries()){if(a+=h,Bq.has(h)){let C=parseFloat(/\d[^m]*/.exec(s.slice(p,p+4)));n=C===aft?null:C}let E=oft.codes.get(Number(n));n&&E&&(s[p+1]===` `?a+=kIe(E):h===` `&&(a+=kIe(n)))}return a};QIe.exports=(e,t,r)=>String(e).normalize().replace(/\r\n/g,` `).split(` `).map(s=>uft(s,t,r)).join(` `)});var NIe=G((jVt,FIe)=>{"use strict";var TIe="[\uD800-\uDBFF][\uDC00-\uDFFF]",fft=e=>e&&e.exact?new RegExp(`^${TIe}$`):new RegExp(TIe,"g");FIe.exports=fft});var vq=G((GVt,UIe)=>{"use strict";var Aft=mq(),pft=NIe(),OIe=ik(),MIe=["\x1B","\x9B"],EF=e=>`${MIe[0]}[${e}m`,LIe=(e,t,r)=>{let s=[];e=[...e];for(let a of e){let n=a;a.match(";")&&(a=a.split(";")[0][0]+"0");let c=OIe.codes.get(parseInt(a,10));if(c){let f=e.indexOf(c.toString());f>=0?e.splice(f,1):s.push(EF(t?c:n))}else if(t){s.push(EF(0));break}else s.push(EF(n))}if(t&&(s=s.filter((a,n)=>s.indexOf(a)===n),r!==void 0)){let a=EF(OIe.codes.get(parseInt(r,10)));s=s.reduce((n,c)=>c===a?[c,...n]:[...n,c],[])}return s.join("")};UIe.exports=(e,t,r)=>{let s=[...e.normalize()],a=[];r=typeof r=="number"?r:s.length;let n=!1,c,f=0,p="";for(let[h,E]of s.entries()){let C=!1;if(MIe.includes(E)){let S=/\d[^m]*/.exec(e.slice(h,h+18));c=S&&S.length>0?S[0]:void 0,ft&&f<=r)p+=E;else if(f===t&&!n&&c!==void 0)p=LIe(a);else if(f>=r){p+=LIe(a,!0,c);break}}return p}});var HIe=G((qVt,_Ie)=>{"use strict";var X0=vq(),hft=GS();function IF(e,t,r){if(e.charAt(t)===" ")return t;for(let s=1;s<=3;s++)if(r){if(e.charAt(t+s)===" ")return t+s}else if(e.charAt(t-s)===" ")return t-s;return t}_Ie.exports=(e,t,r)=>{r={position:"end",preferTruncationOnSpace:!1,...r};let{position:s,space:a,preferTruncationOnSpace:n}=r,c="\u2026",f=1;if(typeof e!="string")throw new TypeError(`Expected \`input\` to be a string, got ${typeof e}`);if(typeof t!="number")throw new TypeError(`Expected \`columns\` to be a number, got ${typeof t}`);if(t<1)return"";if(t===1)return c;let p=hft(e);if(p<=t)return e;if(s==="start"){if(n){let h=IF(e,p-t+1,!0);return c+X0(e,h,p).trim()}return a===!0&&(c+=" ",f=2),c+X0(e,p-t+f,p)}if(s==="middle"){a===!0&&(c=" "+c+" ",f=3);let h=Math.floor(t/2);if(n){let E=IF(e,h),C=IF(e,p-(t-h)+1,!0);return X0(e,0,E)+c+X0(e,C,p).trim()}return X0(e,0,h)+c+X0(e,p-(t-h)+f,p)}if(s==="end"){if(n){let h=IF(e,t-1);return X0(e,0,h)+c}return a===!0&&(c=" "+c,f=2),X0(e,0,t-f)+c}throw new Error(`Expected \`options.position\` to be either \`start\`, \`middle\` or \`end\`, got ${s}`)}});var Dq=G(VS=>{"use strict";var jIe=VS&&VS.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(VS,"__esModule",{value:!0});var dft=jIe(RIe()),gft=jIe(HIe()),Sq={};VS.default=(e,t,r)=>{let s=e+String(t)+String(r);if(Sq[s])return Sq[s];let a=e;if(r==="wrap"&&(a=dft.default(e,t,{trim:!1,hard:!0})),r.startsWith("truncate")){let n="end";r==="truncate-middle"&&(n="middle"),r==="truncate-start"&&(n="start"),a=gft.default(e,t,{position:n})}return Sq[s]=a,a}});var Pq=G(bq=>{"use strict";Object.defineProperty(bq,"__esModule",{value:!0});var GIe=e=>{let t="";if(e.childNodes.length>0)for(let r of e.childNodes){let s="";r.nodeName==="#text"?s=r.nodeValue:((r.nodeName==="ink-text"||r.nodeName==="ink-virtual-text")&&(s=GIe(r)),s.length>0&&typeof r.internal_transform=="function"&&(s=r.internal_transform(s))),t+=s}return t};bq.default=GIe});var xq=G(bi=>{"use strict";var JS=bi&&bi.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(bi,"__esModule",{value:!0});bi.setTextNodeValue=bi.createTextNode=bi.setStyle=bi.setAttribute=bi.removeChildNode=bi.insertBeforeNode=bi.appendChildNode=bi.createNode=bi.TEXT_NAME=void 0;var mft=JS(Lm()),qIe=JS(PIe()),yft=JS(xIe()),Eft=JS(Dq()),Ift=JS(Pq());bi.TEXT_NAME="#text";bi.createNode=e=>{var t;let r={nodeName:e,style:{},attributes:{},childNodes:[],parentNode:null,yogaNode:e==="ink-virtual-text"?void 0:mft.default.Node.create()};return e==="ink-text"&&((t=r.yogaNode)===null||t===void 0||t.setMeasureFunc(Cft.bind(null,r))),r};bi.appendChildNode=(e,t)=>{var r;t.parentNode&&bi.removeChildNode(t.parentNode,t),t.parentNode=e,e.childNodes.push(t),t.yogaNode&&((r=e.yogaNode)===null||r===void 0||r.insertChild(t.yogaNode,e.yogaNode.getChildCount())),(e.nodeName==="ink-text"||e.nodeName==="ink-virtual-text")&&CF(e)};bi.insertBeforeNode=(e,t,r)=>{var s,a;t.parentNode&&bi.removeChildNode(t.parentNode,t),t.parentNode=e;let n=e.childNodes.indexOf(r);if(n>=0){e.childNodes.splice(n,0,t),t.yogaNode&&((s=e.yogaNode)===null||s===void 0||s.insertChild(t.yogaNode,n));return}e.childNodes.push(t),t.yogaNode&&((a=e.yogaNode)===null||a===void 0||a.insertChild(t.yogaNode,e.yogaNode.getChildCount())),(e.nodeName==="ink-text"||e.nodeName==="ink-virtual-text")&&CF(e)};bi.removeChildNode=(e,t)=>{var r,s;t.yogaNode&&((s=(r=t.parentNode)===null||r===void 0?void 0:r.yogaNode)===null||s===void 0||s.removeChild(t.yogaNode)),t.parentNode=null;let a=e.childNodes.indexOf(t);a>=0&&e.childNodes.splice(a,1),(e.nodeName==="ink-text"||e.nodeName==="ink-virtual-text")&&CF(e)};bi.setAttribute=(e,t,r)=>{e.attributes[t]=r};bi.setStyle=(e,t)=>{e.style=t,e.yogaNode&&yft.default(e.yogaNode,t)};bi.createTextNode=e=>{let t={nodeName:"#text",nodeValue:e,yogaNode:void 0,parentNode:null,style:{}};return bi.setTextNodeValue(t,e),t};var Cft=function(e,t){var r,s;let a=e.nodeName==="#text"?e.nodeValue:Ift.default(e),n=qIe.default(a);if(n.width<=t||n.width>=1&&t>0&&t<1)return n;let c=(s=(r=e.style)===null||r===void 0?void 0:r.textWrap)!==null&&s!==void 0?s:"wrap",f=Eft.default(a,t,c);return qIe.default(f)},WIe=e=>{var t;if(!(!e||!e.parentNode))return(t=e.yogaNode)!==null&&t!==void 0?t:WIe(e.parentNode)},CF=e=>{let t=WIe(e);t?.markDirty()};bi.setTextNodeValue=(e,t)=>{typeof t!="string"&&(t=String(t)),e.nodeValue=t,CF(e)}});var zIe=G(KS=>{"use strict";var KIe=KS&&KS.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(KS,"__esModule",{value:!0});var YIe=fq(),wft=KIe(hIe()),VIe=KIe(Lm()),sa=xq(),JIe=e=>{e?.unsetMeasureFunc(),e?.freeRecursive()};KS.default=wft.default({schedulePassiveEffects:YIe.unstable_scheduleCallback,cancelPassiveEffects:YIe.unstable_cancelCallback,now:Date.now,getRootHostContext:()=>({isInsideText:!1}),prepareForCommit:()=>null,preparePortalMount:()=>null,clearContainer:()=>!1,shouldDeprioritizeSubtree:()=>!1,resetAfterCommit:e=>{if(e.isStaticDirty){e.isStaticDirty=!1,typeof e.onImmediateRender=="function"&&e.onImmediateRender();return}typeof e.onRender=="function"&&e.onRender()},getChildHostContext:(e,t)=>{let r=e.isInsideText,s=t==="ink-text"||t==="ink-virtual-text";return r===s?e:{isInsideText:s}},shouldSetTextContent:()=>!1,createInstance:(e,t,r,s)=>{if(s.isInsideText&&e==="ink-box")throw new Error(" can\u2019t be nested inside component");let a=e==="ink-text"&&s.isInsideText?"ink-virtual-text":e,n=sa.createNode(a);for(let[c,f]of Object.entries(t))c!=="children"&&(c==="style"?sa.setStyle(n,f):c==="internal_transform"?n.internal_transform=f:c==="internal_static"?n.internal_static=!0:sa.setAttribute(n,c,f));return n},createTextInstance:(e,t,r)=>{if(!r.isInsideText)throw new Error(`Text string "${e}" must be rendered inside component`);return sa.createTextNode(e)},resetTextContent:()=>{},hideTextInstance:e=>{sa.setTextNodeValue(e,"")},unhideTextInstance:(e,t)=>{sa.setTextNodeValue(e,t)},getPublicInstance:e=>e,hideInstance:e=>{var t;(t=e.yogaNode)===null||t===void 0||t.setDisplay(VIe.default.DISPLAY_NONE)},unhideInstance:e=>{var t;(t=e.yogaNode)===null||t===void 0||t.setDisplay(VIe.default.DISPLAY_FLEX)},appendInitialChild:sa.appendChildNode,appendChild:sa.appendChildNode,insertBefore:sa.insertBeforeNode,finalizeInitialChildren:(e,t,r,s)=>(e.internal_static&&(s.isStaticDirty=!0,s.staticNode=e),!1),supportsMutation:!0,appendChildToContainer:sa.appendChildNode,insertInContainerBefore:sa.insertBeforeNode,removeChildFromContainer:(e,t)=>{sa.removeChildNode(e,t),JIe(t.yogaNode)},prepareUpdate:(e,t,r,s,a)=>{e.internal_static&&(a.isStaticDirty=!0);let n={},c=Object.keys(s);for(let f of c)if(s[f]!==r[f]){if(f==="style"&&typeof s.style=="object"&&typeof r.style=="object"){let h=s.style,E=r.style,C=Object.keys(h);for(let S of C){if(S==="borderStyle"||S==="borderColor"){if(typeof n.style!="object"){let x={};n.style=x}n.style.borderStyle=h.borderStyle,n.style.borderColor=h.borderColor}if(h[S]!==E[S]){if(typeof n.style!="object"){let x={};n.style=x}n.style[S]=h[S]}}continue}n[f]=s[f]}return n},commitUpdate:(e,t)=>{for(let[r,s]of Object.entries(t))r!=="children"&&(r==="style"?sa.setStyle(e,s):r==="internal_transform"?e.internal_transform=s:r==="internal_static"?e.internal_static=!0:sa.setAttribute(e,r,s))},commitTextUpdate:(e,t,r)=>{sa.setTextNodeValue(e,r)},removeChild:(e,t)=>{sa.removeChildNode(e,t),JIe(t.yogaNode)}})});var ZIe=G((KVt,XIe)=>{"use strict";XIe.exports=(e,t=1,r)=>{if(r={indent:" ",includeEmptyLines:!1,...r},typeof e!="string")throw new TypeError(`Expected \`input\` to be a \`string\`, got \`${typeof e}\``);if(typeof t!="number")throw new TypeError(`Expected \`count\` to be a \`number\`, got \`${typeof t}\``);if(typeof r.indent!="string")throw new TypeError(`Expected \`options.indent\` to be a \`string\`, got \`${typeof r.indent}\``);if(t===0)return e;let s=r.includeEmptyLines?/^/gm:/^(?!\s*$)/gm;return e.replace(s,r.indent.repeat(t))}});var $Ie=G(zS=>{"use strict";var Bft=zS&&zS.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(zS,"__esModule",{value:!0});var wF=Bft(Lm());zS.default=e=>e.getComputedWidth()-e.getComputedPadding(wF.default.EDGE_LEFT)-e.getComputedPadding(wF.default.EDGE_RIGHT)-e.getComputedBorder(wF.default.EDGE_LEFT)-e.getComputedBorder(wF.default.EDGE_RIGHT)});var eCe=G((XVt,vft)=>{vft.exports={single:{topLeft:"\u250C",topRight:"\u2510",bottomRight:"\u2518",bottomLeft:"\u2514",vertical:"\u2502",horizontal:"\u2500"},double:{topLeft:"\u2554",topRight:"\u2557",bottomRight:"\u255D",bottomLeft:"\u255A",vertical:"\u2551",horizontal:"\u2550"},round:{topLeft:"\u256D",topRight:"\u256E",bottomRight:"\u256F",bottomLeft:"\u2570",vertical:"\u2502",horizontal:"\u2500"},bold:{topLeft:"\u250F",topRight:"\u2513",bottomRight:"\u251B",bottomLeft:"\u2517",vertical:"\u2503",horizontal:"\u2501"},singleDouble:{topLeft:"\u2553",topRight:"\u2556",bottomRight:"\u255C",bottomLeft:"\u2559",vertical:"\u2551",horizontal:"\u2500"},doubleSingle:{topLeft:"\u2552",topRight:"\u2555",bottomRight:"\u255B",bottomLeft:"\u2558",vertical:"\u2502",horizontal:"\u2550"},classic:{topLeft:"+",topRight:"+",bottomRight:"+",bottomLeft:"+",vertical:"|",horizontal:"-"}}});var rCe=G((ZVt,kq)=>{"use strict";var tCe=eCe();kq.exports=tCe;kq.exports.default=tCe});var Qq=G(ZS=>{"use strict";var Sft=ZS&&ZS.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(ZS,"__esModule",{value:!0});var XS=Sft(NE()),Dft=/^(rgb|hsl|hsv|hwb)\(\s?(\d+),\s?(\d+),\s?(\d+)\s?\)$/,bft=/^(ansi|ansi256)\(\s?(\d+)\s?\)$/,BF=(e,t)=>t==="foreground"?e:"bg"+e[0].toUpperCase()+e.slice(1);ZS.default=(e,t,r)=>{if(!t)return e;if(t in XS.default){let a=BF(t,r);return XS.default[a](e)}if(t.startsWith("#")){let a=BF("hex",r);return XS.default[a](t)(e)}if(t.startsWith("ansi")){let a=bft.exec(t);if(!a)return e;let n=BF(a[1],r),c=Number(a[2]);return XS.default[n](c)(e)}if(t.startsWith("rgb")||t.startsWith("hsl")||t.startsWith("hsv")||t.startsWith("hwb")){let a=Dft.exec(t);if(!a)return e;let n=BF(a[1],r),c=Number(a[2]),f=Number(a[3]),p=Number(a[4]);return XS.default[n](c,f,p)(e)}return e}});var iCe=G($S=>{"use strict";var nCe=$S&&$S.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty($S,"__esModule",{value:!0});var Pft=nCe(rCe()),Rq=nCe(Qq());$S.default=(e,t,r,s)=>{if(typeof r.style.borderStyle=="string"){let a=r.yogaNode.getComputedWidth(),n=r.yogaNode.getComputedHeight(),c=r.style.borderColor,f=Pft.default[r.style.borderStyle],p=Rq.default(f.topLeft+f.horizontal.repeat(a-2)+f.topRight,c,"foreground"),h=(Rq.default(f.vertical,c,"foreground")+` `).repeat(n-2),E=Rq.default(f.bottomLeft+f.horizontal.repeat(a-2)+f.bottomRight,c,"foreground");s.write(e,t,p,{transformers:[]}),s.write(e,t+1,h,{transformers:[]}),s.write(e+a-1,t+1,h,{transformers:[]}),s.write(e,t+n-1,E,{transformers:[]})}}});var oCe=G(eD=>{"use strict";var Mm=eD&&eD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(eD,"__esModule",{value:!0});var xft=Mm(Lm()),kft=Mm(Iq()),Qft=Mm(ZIe()),Rft=Mm(Dq()),Tft=Mm($Ie()),Fft=Mm(Pq()),Nft=Mm(iCe()),Oft=(e,t)=>{var r;let s=(r=e.childNodes[0])===null||r===void 0?void 0:r.yogaNode;if(s){let a=s.getComputedLeft(),n=s.getComputedTop();t=` `.repeat(n)+Qft.default(t,a)}return t},sCe=(e,t,r)=>{var s;let{offsetX:a=0,offsetY:n=0,transformers:c=[],skipStaticElements:f}=r;if(f&&e.internal_static)return;let{yogaNode:p}=e;if(p){if(p.getDisplay()===xft.default.DISPLAY_NONE)return;let h=a+p.getComputedLeft(),E=n+p.getComputedTop(),C=c;if(typeof e.internal_transform=="function"&&(C=[e.internal_transform,...c]),e.nodeName==="ink-text"){let S=Fft.default(e);if(S.length>0){let x=kft.default(S),I=Tft.default(p);if(x>I){let T=(s=e.style.textWrap)!==null&&s!==void 0?s:"wrap";S=Rft.default(S,I,T)}S=Oft(e,S),t.write(h,E,S,{transformers:C})}return}if(e.nodeName==="ink-box"&&Nft.default(h,E,e,t),e.nodeName==="ink-root"||e.nodeName==="ink-box")for(let S of e.childNodes)sCe(S,t,{offsetX:h,offsetY:E,transformers:C,skipStaticElements:f})}};eD.default=sCe});var cCe=G(tD=>{"use strict";var lCe=tD&&tD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(tD,"__esModule",{value:!0});var aCe=lCe(vq()),Lft=lCe(GS()),Tq=class{constructor(t){this.writes=[];let{width:r,height:s}=t;this.width=r,this.height=s}write(t,r,s,a){let{transformers:n}=a;s&&this.writes.push({x:t,y:r,text:s,transformers:n})}get(){let t=[];for(let s=0;ss.trimRight()).join(` `),height:t.length}}};tD.default=Tq});var ACe=G(rD=>{"use strict";var Fq=rD&&rD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(rD,"__esModule",{value:!0});var Mft=Fq(Lm()),uCe=Fq(oCe()),fCe=Fq(cCe());rD.default=(e,t)=>{var r;if(e.yogaNode.setWidth(t),e.yogaNode){e.yogaNode.calculateLayout(void 0,void 0,Mft.default.DIRECTION_LTR);let s=new fCe.default({width:e.yogaNode.getComputedWidth(),height:e.yogaNode.getComputedHeight()});uCe.default(e,s,{skipStaticElements:!0});let a;!((r=e.staticNode)===null||r===void 0)&&r.yogaNode&&(a=new fCe.default({width:e.staticNode.yogaNode.getComputedWidth(),height:e.staticNode.yogaNode.getComputedHeight()}),uCe.default(e.staticNode,a,{skipStaticElements:!1}));let{output:n,height:c}=s.get();return{output:n,outputHeight:c,staticOutput:a?`${a.get().output} `:""}}return{output:"",outputHeight:0,staticOutput:""}}});var gCe=G((i7t,dCe)=>{"use strict";var pCe=Ie("stream"),hCe=["assert","count","countReset","debug","dir","dirxml","error","group","groupCollapsed","groupEnd","info","log","table","time","timeEnd","timeLog","trace","warn"],Nq={},Uft=e=>{let t=new pCe.PassThrough,r=new pCe.PassThrough;t.write=a=>e("stdout",a),r.write=a=>e("stderr",a);let s=new console.Console(t,r);for(let a of hCe)Nq[a]=console[a],console[a]=s[a];return()=>{for(let a of hCe)console[a]=Nq[a];Nq={}}};dCe.exports=Uft});var Lq=G(Oq=>{"use strict";Object.defineProperty(Oq,"__esModule",{value:!0});Oq.default=new WeakMap});var Uq=G(Mq=>{"use strict";Object.defineProperty(Mq,"__esModule",{value:!0});var _ft=dn(),mCe=_ft.createContext({exit:()=>{}});mCe.displayName="InternalAppContext";Mq.default=mCe});var Hq=G(_q=>{"use strict";Object.defineProperty(_q,"__esModule",{value:!0});var Hft=dn(),yCe=Hft.createContext({stdin:void 0,setRawMode:()=>{},isRawModeSupported:!1,internal_exitOnCtrlC:!0});yCe.displayName="InternalStdinContext";_q.default=yCe});var Gq=G(jq=>{"use strict";Object.defineProperty(jq,"__esModule",{value:!0});var jft=dn(),ECe=jft.createContext({stdout:void 0,write:()=>{}});ECe.displayName="InternalStdoutContext";jq.default=ECe});var Wq=G(qq=>{"use strict";Object.defineProperty(qq,"__esModule",{value:!0});var Gft=dn(),ICe=Gft.createContext({stderr:void 0,write:()=>{}});ICe.displayName="InternalStderrContext";qq.default=ICe});var vF=G(Yq=>{"use strict";Object.defineProperty(Yq,"__esModule",{value:!0});var qft=dn(),CCe=qft.createContext({activeId:void 0,add:()=>{},remove:()=>{},activate:()=>{},deactivate:()=>{},enableFocus:()=>{},disableFocus:()=>{},focusNext:()=>{},focusPrevious:()=>{},focus:()=>{}});CCe.displayName="InternalFocusContext";Yq.default=CCe});var BCe=G((f7t,wCe)=>{"use strict";var Wft=/[|\\{}()[\]^$+*?.-]/g;wCe.exports=e=>{if(typeof e!="string")throw new TypeError("Expected a string");return e.replace(Wft,"\\$&")}});var bCe=G((A7t,DCe)=>{"use strict";var Yft=BCe(),Vft=typeof process=="object"&&process&&typeof process.cwd=="function"?process.cwd():".",SCe=[].concat(Ie("module").builtinModules,"bootstrap_node","node").map(e=>new RegExp(`(?:\\((?:node:)?${e}(?:\\.js)?:\\d+:\\d+\\)$|^\\s*at (?:node:)?${e}(?:\\.js)?:\\d+:\\d+$)`));SCe.push(/\((?:node:)?internal\/[^:]+:\d+:\d+\)$/,/\s*at (?:node:)?internal\/[^:]+:\d+:\d+$/,/\/\.node-spawn-wrap-\w+-\w+\/node:\d+:\d+\)?$/);var Vq=class e{constructor(t){t={ignoredPackages:[],...t},"internals"in t||(t.internals=e.nodeInternals()),"cwd"in t||(t.cwd=Vft),this._cwd=t.cwd.replace(/\\/g,"/"),this._internals=[].concat(t.internals,Jft(t.ignoredPackages)),this._wrapCallSite=t.wrapCallSite||!1}static nodeInternals(){return[...SCe]}clean(t,r=0){r=" ".repeat(r),Array.isArray(t)||(t=t.split(` `)),!/^\s*at /.test(t[0])&&/^\s*at /.test(t[1])&&(t=t.slice(1));let s=!1,a=null,n=[];return t.forEach(c=>{if(c=c.replace(/\\/g,"/"),this._internals.some(p=>p.test(c)))return;let f=/^\s*at /.test(c);s?c=c.trimEnd().replace(/^(\s+)at /,"$1"):(c=c.trim(),f&&(c=c.slice(3))),c=c.replace(`${this._cwd}/`,""),c&&(f?(a&&(n.push(a),a=null),n.push(c)):(s=!0,a=c))}),n.map(c=>`${r}${c} `).join("")}captureString(t,r=this.captureString){typeof t=="function"&&(r=t,t=1/0);let{stackTraceLimit:s}=Error;t&&(Error.stackTraceLimit=t);let a={};Error.captureStackTrace(a,r);let{stack:n}=a;return Error.stackTraceLimit=s,this.clean(n)}capture(t,r=this.capture){typeof t=="function"&&(r=t,t=1/0);let{prepareStackTrace:s,stackTraceLimit:a}=Error;Error.prepareStackTrace=(f,p)=>this._wrapCallSite?p.map(this._wrapCallSite):p,t&&(Error.stackTraceLimit=t);let n={};Error.captureStackTrace(n,r);let{stack:c}=n;return Object.assign(Error,{prepareStackTrace:s,stackTraceLimit:a}),c}at(t=this.at){let[r]=this.capture(1,t);if(!r)return{};let s={line:r.getLineNumber(),column:r.getColumnNumber()};vCe(s,r.getFileName(),this._cwd),r.isConstructor()&&(s.constructor=!0),r.isEval()&&(s.evalOrigin=r.getEvalOrigin()),r.isNative()&&(s.native=!0);let a;try{a=r.getTypeName()}catch{}a&&a!=="Object"&&a!=="[object Object]"&&(s.type=a);let n=r.getFunctionName();n&&(s.function=n);let c=r.getMethodName();return c&&n!==c&&(s.method=c),s}parseLine(t){let r=t&&t.match(Kft);if(!r)return null;let s=r[1]==="new",a=r[2],n=r[3],c=r[4],f=Number(r[5]),p=Number(r[6]),h=r[7],E=r[8],C=r[9],S=r[10]==="native",x=r[11]===")",I,T={};if(E&&(T.line=Number(E)),C&&(T.column=Number(C)),x&&h){let O=0;for(let U=h.length-1;U>0;U--)if(h.charAt(U)===")")O++;else if(h.charAt(U)==="("&&h.charAt(U-1)===" "&&(O--,O===-1&&h.charAt(U-1)===" ")){let V=h.slice(0,U-1);h=h.slice(U+1),a+=` (${V}`;break}}if(a){let O=a.match(zft);O&&(a=O[1],I=O[2])}return vCe(T,h,this._cwd),s&&(T.constructor=!0),n&&(T.evalOrigin=n,T.evalLine=f,T.evalColumn=p,T.evalFile=c&&c.replace(/\\/g,"/")),S&&(T.native=!0),a&&(T.function=a),I&&a!==I&&(T.method=I),T}};function vCe(e,t,r){t&&(t=t.replace(/\\/g,"/"),t.startsWith(`${r}/`)&&(t=t.slice(r.length+1)),e.file=t)}function Jft(e){if(e.length===0)return[];let t=e.map(r=>Yft(r));return new RegExp(`[/\\\\]node_modules[/\\\\](?:${t.join("|")})[/\\\\][^:]+:\\d+:\\d+`)}var Kft=new RegExp("^(?:\\s*at )?(?:(new) )?(?:(.*?) \\()?(?:eval at ([^ ]+) \\((.+?):(\\d+):(\\d+)\\), )?(?:(.+?):(\\d+):(\\d+)|(native))(\\)?)$"),zft=/^(.*?) \[as (.*?)\]$/;DCe.exports=Vq});var xCe=G((p7t,PCe)=>{"use strict";PCe.exports=(e,t)=>e.replace(/^\t+/gm,r=>" ".repeat(r.length*(t||2)))});var QCe=G((h7t,kCe)=>{"use strict";var Xft=xCe(),Zft=(e,t)=>{let r=[],s=e-t,a=e+t;for(let n=s;n<=a;n++)r.push(n);return r};kCe.exports=(e,t,r)=>{if(typeof e!="string")throw new TypeError("Source code is missing.");if(!t||t<1)throw new TypeError("Line number must start from `1`.");if(e=Xft(e).split(/\r?\n/),!(t>e.length))return r={around:3,...r},Zft(t,r.around).filter(s=>e[s-1]!==void 0).map(s=>({line:s,value:e[s-1]}))}});var SF=G(sf=>{"use strict";var $ft=sf&&sf.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r),Object.defineProperty(e,s,{enumerable:!0,get:function(){return t[r]}})}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),eAt=sf&&sf.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),tAt=sf&&sf.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!=="default"&&Object.hasOwnProperty.call(e,r)&&$ft(t,e,r);return eAt(t,e),t},rAt=sf&&sf.__rest||function(e,t){var r={};for(var s in e)Object.prototype.hasOwnProperty.call(e,s)&&t.indexOf(s)<0&&(r[s]=e[s]);if(e!=null&&typeof Object.getOwnPropertySymbols=="function")for(var a=0,s=Object.getOwnPropertySymbols(e);a{var{children:r}=e,s=rAt(e,["children"]);let a=Object.assign(Object.assign({},s),{marginLeft:s.marginLeft||s.marginX||s.margin||0,marginRight:s.marginRight||s.marginX||s.margin||0,marginTop:s.marginTop||s.marginY||s.margin||0,marginBottom:s.marginBottom||s.marginY||s.margin||0,paddingLeft:s.paddingLeft||s.paddingX||s.padding||0,paddingRight:s.paddingRight||s.paddingX||s.padding||0,paddingTop:s.paddingTop||s.paddingY||s.padding||0,paddingBottom:s.paddingBottom||s.paddingY||s.padding||0});return RCe.default.createElement("ink-box",{ref:t,style:a},r)});Jq.displayName="Box";Jq.defaultProps={flexDirection:"row",flexGrow:0,flexShrink:1};sf.default=Jq});var Xq=G(nD=>{"use strict";var Kq=nD&&nD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(nD,"__esModule",{value:!0});var nAt=Kq(dn()),cw=Kq(NE()),TCe=Kq(Qq()),zq=({color:e,backgroundColor:t,dimColor:r,bold:s,italic:a,underline:n,strikethrough:c,inverse:f,wrap:p,children:h})=>{if(h==null)return null;let E=C=>(r&&(C=cw.default.dim(C)),e&&(C=TCe.default(C,e,"foreground")),t&&(C=TCe.default(C,t,"background")),s&&(C=cw.default.bold(C)),a&&(C=cw.default.italic(C)),n&&(C=cw.default.underline(C)),c&&(C=cw.default.strikethrough(C)),f&&(C=cw.default.inverse(C)),C);return nAt.default.createElement("ink-text",{style:{flexGrow:0,flexShrink:1,flexDirection:"row",textWrap:p},internal_transform:E},h)};zq.displayName="Text";zq.defaultProps={dimColor:!1,bold:!1,italic:!1,underline:!1,strikethrough:!1,wrap:"wrap"};nD.default=zq});var LCe=G(of=>{"use strict";var iAt=of&&of.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r),Object.defineProperty(e,s,{enumerable:!0,get:function(){return t[r]}})}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),sAt=of&&of.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),oAt=of&&of.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!=="default"&&Object.hasOwnProperty.call(e,r)&&iAt(t,e,r);return sAt(t,e),t},iD=of&&of.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(of,"__esModule",{value:!0});var FCe=oAt(Ie("fs")),Fs=iD(dn()),NCe=iD(bCe()),aAt=iD(QCe()),$p=iD(SF()),hA=iD(Xq()),OCe=new NCe.default({cwd:process.cwd(),internals:NCe.default.nodeInternals()}),lAt=({error:e})=>{let t=e.stack?e.stack.split(` `).slice(1):void 0,r=t?OCe.parseLine(t[0]):void 0,s,a=0;if(r?.file&&r?.line&&FCe.existsSync(r.file)){let n=FCe.readFileSync(r.file,"utf8");if(s=aAt.default(n,r.line),s)for(let{line:c}of s)a=Math.max(a,String(c).length)}return Fs.default.createElement($p.default,{flexDirection:"column",padding:1},Fs.default.createElement($p.default,null,Fs.default.createElement(hA.default,{backgroundColor:"red",color:"white"}," ","ERROR"," "),Fs.default.createElement(hA.default,null," ",e.message)),r&&Fs.default.createElement($p.default,{marginTop:1},Fs.default.createElement(hA.default,{dimColor:!0},r.file,":",r.line,":",r.column)),r&&s&&Fs.default.createElement($p.default,{marginTop:1,flexDirection:"column"},s.map(({line:n,value:c})=>Fs.default.createElement($p.default,{key:n},Fs.default.createElement($p.default,{width:a+1},Fs.default.createElement(hA.default,{dimColor:n!==r.line,backgroundColor:n===r.line?"red":void 0,color:n===r.line?"white":void 0},String(n).padStart(a," "),":")),Fs.default.createElement(hA.default,{key:n,backgroundColor:n===r.line?"red":void 0,color:n===r.line?"white":void 0}," "+c)))),e.stack&&Fs.default.createElement($p.default,{marginTop:1,flexDirection:"column"},e.stack.split(` `).slice(1).map(n=>{let c=OCe.parseLine(n);return c?Fs.default.createElement($p.default,{key:n},Fs.default.createElement(hA.default,{dimColor:!0},"- "),Fs.default.createElement(hA.default,{dimColor:!0,bold:!0},c.function),Fs.default.createElement(hA.default,{dimColor:!0,color:"gray"}," ","(",c.file,":",c.line,":",c.column,")")):Fs.default.createElement($p.default,{key:n},Fs.default.createElement(hA.default,{dimColor:!0},"- "),Fs.default.createElement(hA.default,{dimColor:!0,bold:!0},n))})))};of.default=lAt});var UCe=G(af=>{"use strict";var cAt=af&&af.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r),Object.defineProperty(e,s,{enumerable:!0,get:function(){return t[r]}})}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),uAt=af&&af.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),fAt=af&&af.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!=="default"&&Object.hasOwnProperty.call(e,r)&&cAt(t,e,r);return uAt(t,e),t},_m=af&&af.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(af,"__esModule",{value:!0});var Um=fAt(dn()),MCe=_m($9()),AAt=_m(Uq()),pAt=_m(Hq()),hAt=_m(Gq()),dAt=_m(Wq()),gAt=_m(vF()),mAt=_m(LCe()),yAt=" ",EAt="\x1B[Z",IAt="\x1B",DF=class extends Um.PureComponent{constructor(){super(...arguments),this.state={isFocusEnabled:!0,activeFocusId:void 0,focusables:[],error:void 0},this.rawModeEnabledCount=0,this.handleSetRawMode=t=>{let{stdin:r}=this.props;if(!this.isRawModeSupported())throw r===process.stdin?new Error(`Raw mode is not supported on the current process.stdin, which Ink uses as input stream by default. Read about how to prevent this error on https://github.com/vadimdemedes/ink/#israwmodesupported`):new Error(`Raw mode is not supported on the stdin provided to Ink. Read about how to prevent this error on https://github.com/vadimdemedes/ink/#israwmodesupported`);if(r.setEncoding("utf8"),t){this.rawModeEnabledCount===0&&(r.addListener("data",this.handleInput),r.resume(),r.setRawMode(!0)),this.rawModeEnabledCount++;return}--this.rawModeEnabledCount===0&&(r.setRawMode(!1),r.removeListener("data",this.handleInput),r.pause())},this.handleInput=t=>{t===""&&this.props.exitOnCtrlC&&this.handleExit(),t===IAt&&this.state.activeFocusId&&this.setState({activeFocusId:void 0}),this.state.isFocusEnabled&&this.state.focusables.length>0&&(t===yAt&&this.focusNext(),t===EAt&&this.focusPrevious())},this.handleExit=t=>{this.isRawModeSupported()&&this.handleSetRawMode(!1),this.props.onExit(t)},this.enableFocus=()=>{this.setState({isFocusEnabled:!0})},this.disableFocus=()=>{this.setState({isFocusEnabled:!1})},this.focus=t=>{this.setState(r=>r.focusables.some(a=>a?.id===t)?{activeFocusId:t}:r)},this.focusNext=()=>{this.setState(t=>{var r;let s=(r=t.focusables[0])===null||r===void 0?void 0:r.id;return{activeFocusId:this.findNextFocusable(t)||s}})},this.focusPrevious=()=>{this.setState(t=>{var r;let s=(r=t.focusables[t.focusables.length-1])===null||r===void 0?void 0:r.id;return{activeFocusId:this.findPreviousFocusable(t)||s}})},this.addFocusable=(t,{autoFocus:r})=>{this.setState(s=>{let a=s.activeFocusId;return!a&&r&&(a=t),{activeFocusId:a,focusables:[...s.focusables,{id:t,isActive:!0}]}})},this.removeFocusable=t=>{this.setState(r=>({activeFocusId:r.activeFocusId===t?void 0:r.activeFocusId,focusables:r.focusables.filter(s=>s.id!==t)}))},this.activateFocusable=t=>{this.setState(r=>({focusables:r.focusables.map(s=>s.id!==t?s:{id:t,isActive:!0})}))},this.deactivateFocusable=t=>{this.setState(r=>({activeFocusId:r.activeFocusId===t?void 0:r.activeFocusId,focusables:r.focusables.map(s=>s.id!==t?s:{id:t,isActive:!1})}))},this.findNextFocusable=t=>{var r;let s=t.focusables.findIndex(a=>a.id===t.activeFocusId);for(let a=s+1;a{var r;let s=t.focusables.findIndex(a=>a.id===t.activeFocusId);for(let a=s-1;a>=0;a--)if(!((r=t.focusables[a])===null||r===void 0)&&r.isActive)return t.focusables[a].id}}static getDerivedStateFromError(t){return{error:t}}isRawModeSupported(){return this.props.stdin.isTTY}render(){return Um.default.createElement(AAt.default.Provider,{value:{exit:this.handleExit}},Um.default.createElement(pAt.default.Provider,{value:{stdin:this.props.stdin,setRawMode:this.handleSetRawMode,isRawModeSupported:this.isRawModeSupported(),internal_exitOnCtrlC:this.props.exitOnCtrlC}},Um.default.createElement(hAt.default.Provider,{value:{stdout:this.props.stdout,write:this.props.writeToStdout}},Um.default.createElement(dAt.default.Provider,{value:{stderr:this.props.stderr,write:this.props.writeToStderr}},Um.default.createElement(gAt.default.Provider,{value:{activeId:this.state.activeFocusId,add:this.addFocusable,remove:this.removeFocusable,activate:this.activateFocusable,deactivate:this.deactivateFocusable,enableFocus:this.enableFocus,disableFocus:this.disableFocus,focusNext:this.focusNext,focusPrevious:this.focusPrevious,focus:this.focus}},this.state.error?Um.default.createElement(mAt.default,{error:this.state.error}):this.props.children)))))}componentDidMount(){MCe.default.hide(this.props.stdout)}componentWillUnmount(){MCe.default.show(this.props.stdout),this.isRawModeSupported()&&this.handleSetRawMode(!1)}componentDidCatch(t){this.handleExit(t)}};af.default=DF;DF.displayName="InternalApp"});var jCe=G(lf=>{"use strict";var CAt=lf&&lf.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r),Object.defineProperty(e,s,{enumerable:!0,get:function(){return t[r]}})}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),wAt=lf&&lf.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),BAt=lf&&lf.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!=="default"&&Object.hasOwnProperty.call(e,r)&&CAt(t,e,r);return wAt(t,e),t},cf=lf&&lf.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(lf,"__esModule",{value:!0});var vAt=cf(dn()),_Ce=FEe(),SAt=cf(zEe()),DAt=cf(J9()),bAt=cf(rIe()),PAt=cf(iIe()),Zq=cf(zIe()),xAt=cf(ACe()),kAt=cf(Z9()),QAt=cf(gCe()),RAt=BAt(xq()),TAt=cf(Lq()),FAt=cf(UCe()),uw=process.env.CI==="false"?!1:bAt.default,HCe=()=>{},$q=class{constructor(t){this.resolveExitPromise=()=>{},this.rejectExitPromise=()=>{},this.unsubscribeExit=()=>{},this.onRender=()=>{if(this.isUnmounted)return;let{output:r,outputHeight:s,staticOutput:a}=xAt.default(this.rootNode,this.options.stdout.columns||80),n=a&&a!==` `;if(this.options.debug){n&&(this.fullStaticOutput+=a),this.options.stdout.write(this.fullStaticOutput+r);return}if(uw){n&&this.options.stdout.write(a),this.lastOutput=r;return}if(n&&(this.fullStaticOutput+=a),s>=this.options.stdout.rows){this.options.stdout.write(DAt.default.clearTerminal+this.fullStaticOutput+r),this.lastOutput=r;return}n&&(this.log.clear(),this.options.stdout.write(a),this.log(r)),!n&&r!==this.lastOutput&&this.throttledLog(r),this.lastOutput=r},PAt.default(this),this.options=t,this.rootNode=RAt.createNode("ink-root"),this.rootNode.onRender=t.debug?this.onRender:_Ce(this.onRender,32,{leading:!0,trailing:!0}),this.rootNode.onImmediateRender=this.onRender,this.log=SAt.default.create(t.stdout),this.throttledLog=t.debug?this.log:_Ce(this.log,void 0,{leading:!0,trailing:!0}),this.isUnmounted=!1,this.lastOutput="",this.fullStaticOutput="",this.container=Zq.default.createContainer(this.rootNode,0,!1,null),this.unsubscribeExit=kAt.default(this.unmount,{alwaysLast:!1}),t.patchConsole&&this.patchConsole(),uw||(t.stdout.on("resize",this.onRender),this.unsubscribeResize=()=>{t.stdout.off("resize",this.onRender)})}render(t){let r=vAt.default.createElement(FAt.default,{stdin:this.options.stdin,stdout:this.options.stdout,stderr:this.options.stderr,writeToStdout:this.writeToStdout,writeToStderr:this.writeToStderr,exitOnCtrlC:this.options.exitOnCtrlC,onExit:this.unmount},t);Zq.default.updateContainer(r,this.container,null,HCe)}writeToStdout(t){if(!this.isUnmounted){if(this.options.debug){this.options.stdout.write(t+this.fullStaticOutput+this.lastOutput);return}if(uw){this.options.stdout.write(t);return}this.log.clear(),this.options.stdout.write(t),this.log(this.lastOutput)}}writeToStderr(t){if(!this.isUnmounted){if(this.options.debug){this.options.stderr.write(t),this.options.stdout.write(this.fullStaticOutput+this.lastOutput);return}if(uw){this.options.stderr.write(t);return}this.log.clear(),this.options.stderr.write(t),this.log(this.lastOutput)}}unmount(t){this.isUnmounted||(this.onRender(),this.unsubscribeExit(),typeof this.restoreConsole=="function"&&this.restoreConsole(),typeof this.unsubscribeResize=="function"&&this.unsubscribeResize(),uw?this.options.stdout.write(this.lastOutput+` `):this.options.debug||this.log.done(),this.isUnmounted=!0,Zq.default.updateContainer(null,this.container,null,HCe),TAt.default.delete(this.options.stdout),t instanceof Error?this.rejectExitPromise(t):this.resolveExitPromise())}waitUntilExit(){return this.exitPromise||(this.exitPromise=new Promise((t,r)=>{this.resolveExitPromise=t,this.rejectExitPromise=r})),this.exitPromise}clear(){!uw&&!this.options.debug&&this.log.clear()}patchConsole(){this.options.debug||(this.restoreConsole=QAt.default((t,r)=>{t==="stdout"&&this.writeToStdout(r),t==="stderr"&&(r.startsWith("The above error occurred")||this.writeToStderr(r))}))}};lf.default=$q});var qCe=G(sD=>{"use strict";var GCe=sD&&sD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(sD,"__esModule",{value:!0});var NAt=GCe(jCe()),bF=GCe(Lq()),OAt=Ie("stream"),LAt=(e,t)=>{let r=Object.assign({stdout:process.stdout,stdin:process.stdin,stderr:process.stderr,debug:!1,exitOnCtrlC:!0,patchConsole:!0},MAt(t)),s=UAt(r.stdout,()=>new NAt.default(r));return s.render(e),{rerender:s.render,unmount:()=>s.unmount(),waitUntilExit:s.waitUntilExit,cleanup:()=>bF.default.delete(r.stdout),clear:s.clear}};sD.default=LAt;var MAt=(e={})=>e instanceof OAt.Stream?{stdout:e,stdin:process.stdin}:e,UAt=(e,t)=>{let r;return bF.default.has(e)?r=bF.default.get(e):(r=t(),bF.default.set(e,r)),r}});var YCe=G(eh=>{"use strict";var _At=eh&&eh.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r),Object.defineProperty(e,s,{enumerable:!0,get:function(){return t[r]}})}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),HAt=eh&&eh.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),jAt=eh&&eh.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!=="default"&&Object.hasOwnProperty.call(e,r)&&_At(t,e,r);return HAt(t,e),t};Object.defineProperty(eh,"__esModule",{value:!0});var oD=jAt(dn()),WCe=e=>{let{items:t,children:r,style:s}=e,[a,n]=oD.useState(0),c=oD.useMemo(()=>t.slice(a),[t,a]);oD.useLayoutEffect(()=>{n(t.length)},[t.length]);let f=c.map((h,E)=>r(h,a+E)),p=oD.useMemo(()=>Object.assign({position:"absolute",flexDirection:"column"},s),[s]);return oD.default.createElement("ink-box",{internal_static:!0,style:p},f)};WCe.displayName="Static";eh.default=WCe});var JCe=G(aD=>{"use strict";var GAt=aD&&aD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(aD,"__esModule",{value:!0});var qAt=GAt(dn()),VCe=({children:e,transform:t})=>e==null?null:qAt.default.createElement("ink-text",{style:{flexGrow:0,flexShrink:1,flexDirection:"row"},internal_transform:t},e);VCe.displayName="Transform";aD.default=VCe});var zCe=G(lD=>{"use strict";var WAt=lD&&lD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(lD,"__esModule",{value:!0});var YAt=WAt(dn()),KCe=({count:e=1})=>YAt.default.createElement("ink-text",null,` `.repeat(e));KCe.displayName="Newline";lD.default=KCe});var $Ce=G(cD=>{"use strict";var XCe=cD&&cD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(cD,"__esModule",{value:!0});var VAt=XCe(dn()),JAt=XCe(SF()),ZCe=()=>VAt.default.createElement(JAt.default,{flexGrow:1});ZCe.displayName="Spacer";cD.default=ZCe});var PF=G(uD=>{"use strict";var KAt=uD&&uD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(uD,"__esModule",{value:!0});var zAt=dn(),XAt=KAt(Hq()),ZAt=()=>zAt.useContext(XAt.default);uD.default=ZAt});var twe=G(fD=>{"use strict";var $At=fD&&fD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(fD,"__esModule",{value:!0});var ewe=dn(),ept=$At(PF()),tpt=(e,t={})=>{let{stdin:r,setRawMode:s,internal_exitOnCtrlC:a}=ept.default();ewe.useEffect(()=>{if(t.isActive!==!1)return s(!0),()=>{s(!1)}},[t.isActive,s]),ewe.useEffect(()=>{if(t.isActive===!1)return;let n=c=>{let f=String(c),p={upArrow:f==="\x1B[A",downArrow:f==="\x1B[B",leftArrow:f==="\x1B[D",rightArrow:f==="\x1B[C",pageDown:f==="\x1B[6~",pageUp:f==="\x1B[5~",return:f==="\r",escape:f==="\x1B",ctrl:!1,shift:!1,tab:f===" "||f==="\x1B[Z",backspace:f==="\b",delete:f==="\x7F"||f==="\x1B[3~",meta:!1};f<=""&&!p.return&&(f=String.fromCharCode(f.charCodeAt(0)+97-1),p.ctrl=!0),f.startsWith("\x1B")&&(f=f.slice(1),p.meta=!0);let h=f>="A"&&f<="Z",E=f>="\u0410"&&f<="\u042F";f.length===1&&(h||E)&&(p.shift=!0),p.tab&&f==="[Z"&&(p.shift=!0),(p.tab||p.backspace||p.delete)&&(f=""),(!(f==="c"&&p.ctrl)||!a)&&e(f,p)};return r?.on("data",n),()=>{r?.off("data",n)}},[t.isActive,r,a,e])};fD.default=tpt});var rwe=G(AD=>{"use strict";var rpt=AD&&AD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(AD,"__esModule",{value:!0});var npt=dn(),ipt=rpt(Uq()),spt=()=>npt.useContext(ipt.default);AD.default=spt});var nwe=G(pD=>{"use strict";var opt=pD&&pD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(pD,"__esModule",{value:!0});var apt=dn(),lpt=opt(Gq()),cpt=()=>apt.useContext(lpt.default);pD.default=cpt});var iwe=G(hD=>{"use strict";var upt=hD&&hD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(hD,"__esModule",{value:!0});var fpt=dn(),Apt=upt(Wq()),ppt=()=>fpt.useContext(Apt.default);hD.default=ppt});var owe=G(gD=>{"use strict";var swe=gD&&gD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(gD,"__esModule",{value:!0});var dD=dn(),hpt=swe(vF()),dpt=swe(PF()),gpt=({isActive:e=!0,autoFocus:t=!1,id:r}={})=>{let{isRawModeSupported:s,setRawMode:a}=dpt.default(),{activeId:n,add:c,remove:f,activate:p,deactivate:h,focus:E}=dD.useContext(hpt.default),C=dD.useMemo(()=>r??Math.random().toString().slice(2,7),[r]);return dD.useEffect(()=>(c(C,{autoFocus:t}),()=>{f(C)}),[C,t]),dD.useEffect(()=>{e?p(C):h(C)},[e,C]),dD.useEffect(()=>{if(!(!s||!e))return a(!0),()=>{a(!1)}},[e]),{isFocused:!!C&&n===C,focus:E}};gD.default=gpt});var awe=G(mD=>{"use strict";var mpt=mD&&mD.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(mD,"__esModule",{value:!0});var ypt=dn(),Ept=mpt(vF()),Ipt=()=>{let e=ypt.useContext(Ept.default);return{enableFocus:e.enableFocus,disableFocus:e.disableFocus,focusNext:e.focusNext,focusPrevious:e.focusPrevious,focus:e.focus}};mD.default=Ipt});var lwe=G(eW=>{"use strict";Object.defineProperty(eW,"__esModule",{value:!0});eW.default=e=>{var t,r,s,a;return{width:(r=(t=e.yogaNode)===null||t===void 0?void 0:t.getComputedWidth())!==null&&r!==void 0?r:0,height:(a=(s=e.yogaNode)===null||s===void 0?void 0:s.getComputedHeight())!==null&&a!==void 0?a:0}}});var qc=G(wo=>{"use strict";Object.defineProperty(wo,"__esModule",{value:!0});var Cpt=qCe();Object.defineProperty(wo,"render",{enumerable:!0,get:function(){return Cpt.default}});var wpt=SF();Object.defineProperty(wo,"Box",{enumerable:!0,get:function(){return wpt.default}});var Bpt=Xq();Object.defineProperty(wo,"Text",{enumerable:!0,get:function(){return Bpt.default}});var vpt=YCe();Object.defineProperty(wo,"Static",{enumerable:!0,get:function(){return vpt.default}});var Spt=JCe();Object.defineProperty(wo,"Transform",{enumerable:!0,get:function(){return Spt.default}});var Dpt=zCe();Object.defineProperty(wo,"Newline",{enumerable:!0,get:function(){return Dpt.default}});var bpt=$Ce();Object.defineProperty(wo,"Spacer",{enumerable:!0,get:function(){return bpt.default}});var Ppt=twe();Object.defineProperty(wo,"useInput",{enumerable:!0,get:function(){return Ppt.default}});var xpt=rwe();Object.defineProperty(wo,"useApp",{enumerable:!0,get:function(){return xpt.default}});var kpt=PF();Object.defineProperty(wo,"useStdin",{enumerable:!0,get:function(){return kpt.default}});var Qpt=nwe();Object.defineProperty(wo,"useStdout",{enumerable:!0,get:function(){return Qpt.default}});var Rpt=iwe();Object.defineProperty(wo,"useStderr",{enumerable:!0,get:function(){return Rpt.default}});var Tpt=owe();Object.defineProperty(wo,"useFocus",{enumerable:!0,get:function(){return Tpt.default}});var Fpt=awe();Object.defineProperty(wo,"useFocusManager",{enumerable:!0,get:function(){return Fpt.default}});var Npt=lwe();Object.defineProperty(wo,"measureElement",{enumerable:!0,get:function(){return Npt.default}})});var rW={};Vt(rW,{Gem:()=>tW});var cwe,Hm,tW,xF=Xe(()=>{cwe=et(qc()),Hm=et(dn()),tW=(0,Hm.memo)(({active:e})=>{let t=(0,Hm.useMemo)(()=>e?"\u25C9":"\u25EF",[e]),r=(0,Hm.useMemo)(()=>e?"green":"yellow",[e]);return Hm.default.createElement(cwe.Text,{color:r},t)})});var nW={};Vt(nW,{useKeypress:()=>jm});function jm({active:e},t,r){let{stdin:s}=(0,uwe.useStdin)(),a=(0,kF.useCallback)((n,c)=>t(n,c),r);(0,kF.useEffect)(()=>{if(!(!e||!s))return s.on("keypress",a),()=>{s.off("keypress",a)}},[e,a,s])}var uwe,kF,fw=Xe(()=>{uwe=et(qc()),kF=et(dn())});var Awe={};Vt(Awe,{FocusRequest:()=>fwe,useFocusRequest:()=>iW});var fwe,iW,sW=Xe(()=>{fw();fwe=(r=>(r.BEFORE="before",r.AFTER="after",r))(fwe||{}),iW=function({active:e},t,r){jm({active:e},(s,a)=>{a.name==="tab"&&(a.shift?t("before"):t("after"))},r)}});var pwe={};Vt(pwe,{useListInput:()=>yD});var yD,QF=Xe(()=>{fw();yD=function(e,t,{active:r,minus:s,plus:a,set:n,loop:c=!0}){jm({active:r},(f,p)=>{let h=t.indexOf(e);switch(p.name){case s:{let E=h-1;if(c){n(t[(t.length+E)%t.length]);return}if(E<0)return;n(t[E])}break;case a:{let E=h+1;if(c){n(t[E%t.length]);return}if(E>=t.length)return;n(t[E])}break}},[t,e,a,n,c])}});var RF={};Vt(RF,{ScrollableItems:()=>Opt});var Z0,vl,Opt,TF=Xe(()=>{Z0=et(qc()),vl=et(dn());sW();QF();Opt=({active:e=!0,children:t=[],radius:r=10,size:s=1,loop:a=!0,onFocusRequest:n,willReachEnd:c})=>{let f=O=>{if(O.key===null)throw new Error("Expected all children to have a key");return O.key},p=vl.default.Children.map(t,O=>f(O)),h=p[0],[E,C]=(0,vl.useState)(h),S=p.indexOf(E);(0,vl.useEffect)(()=>{p.includes(E)||C(h)},[t]),(0,vl.useEffect)(()=>{c&&S>=p.length-2&&c()},[S]),iW({active:e&&!!n},O=>{n?.(O)},[n]),yD(E,p,{active:e,minus:"up",plus:"down",set:C,loop:a});let x=S-r,I=S+r;I>p.length&&(x-=I-p.length,I=p.length),x<0&&(I+=-x,x=0),I>=p.length&&(I=p.length-1);let T=[];for(let O=x;O<=I;++O){let U=p[O],V=e&&U===E;T.push(vl.default.createElement(Z0.Box,{key:U,height:s},vl.default.createElement(Z0.Box,{marginLeft:1,marginRight:1},vl.default.createElement(Z0.Text,null,V?vl.default.createElement(Z0.Text,{color:"cyan",bold:!0},">"):" ")),vl.default.createElement(Z0.Box,null,vl.default.cloneElement(t[O],{active:V}))))}return vl.default.createElement(Z0.Box,{flexDirection:"column",width:"100%"},T)}});var hwe,th,dwe,FF,gwe,oW=Xe(()=>{hwe=et(qc()),th=et(dn()),dwe=Ie("readline"),FF=th.default.createContext(null),gwe=({children:e})=>{let{stdin:t,setRawMode:r}=(0,hwe.useStdin)();(0,th.useEffect)(()=>{r&&r(!0),t&&(0,dwe.emitKeypressEvents)(t)},[t,r]);let[s,a]=(0,th.useState)(new Map),n=(0,th.useMemo)(()=>({getAll:()=>s,get:c=>s.get(c),set:(c,f)=>a(p=>new Map([...p,[c,f]])),setAll:c=>a(f=>new Map([...f,...c]))}),[s,a]);return th.default.createElement(FF.Provider,{value:n,children:e})}});var aW={};Vt(aW,{useMinistore:()=>Lpt,useMinistoreSetAll:()=>Mpt});function Lpt(e,t){let r=(0,ED.useContext)(FF);if(r===null)throw new Error("Expected this hook to run with a ministore context attached");if(typeof e>"u")return r.getAll();let s=(0,ED.useCallback)(n=>{r.set(e,n)},[e,r.set]),a=r.get(e);return typeof a>"u"&&(a=t),[a,s]}function Mpt(){let e=(0,ED.useContext)(FF);if(e===null)throw new Error("Expected this hook to run with a ministore context attached");return e.setAll}var ED,lW=Xe(()=>{ED=et(dn());oW()});var OF={};Vt(OF,{renderForm:()=>Upt});async function Upt(e,t,{stdin:r,stdout:s,stderr:a}){let n,c=p=>{let{exit:h}=(0,NF.useApp)();jm({active:!0},(E,C)=>{C.name==="return"&&(n=p,h())},[h,p])},{waitUntilExit:f}=(0,NF.render)(cW.default.createElement(gwe,null,cW.default.createElement(e,{...t,useSubmit:c})),{stdin:r,stdout:s,stderr:a});return await f(),n}var NF,cW,LF=Xe(()=>{NF=et(qc()),cW=et(dn());oW();fw()});var Iwe=G(ID=>{"use strict";Object.defineProperty(ID,"__esModule",{value:!0});ID.UncontrolledTextInput=void 0;var ywe=dn(),uW=dn(),mwe=qc(),Gm=NE(),Ewe=({value:e,placeholder:t="",focus:r=!0,mask:s,highlightPastedText:a=!1,showCursor:n=!0,onChange:c,onSubmit:f})=>{let[{cursorOffset:p,cursorWidth:h},E]=uW.useState({cursorOffset:(e||"").length,cursorWidth:0});uW.useEffect(()=>{E(T=>{if(!r||!n)return T;let O=e||"";return T.cursorOffset>O.length-1?{cursorOffset:O.length,cursorWidth:0}:T})},[e,r,n]);let C=a?h:0,S=s?s.repeat(e.length):e,x=S,I=t?Gm.grey(t):void 0;if(n&&r){I=t.length>0?Gm.inverse(t[0])+Gm.grey(t.slice(1)):Gm.inverse(" "),x=S.length>0?"":Gm.inverse(" ");let T=0;for(let O of S)T>=p-C&&T<=p?x+=Gm.inverse(O):x+=O,T++;S.length>0&&p===S.length&&(x+=Gm.inverse(" "))}return mwe.useInput((T,O)=>{if(O.upArrow||O.downArrow||O.ctrl&&T==="c"||O.tab||O.shift&&O.tab)return;if(O.return){f&&f(e);return}let U=p,V=e,te=0;O.leftArrow?n&&U--:O.rightArrow?n&&U++:O.backspace||O.delete?p>0&&(V=e.slice(0,p-1)+e.slice(p,e.length),U--):(V=e.slice(0,p)+T+e.slice(p,e.length),U+=T.length,T.length>1&&(te=T.length)),p<0&&(U=0),p>e.length&&(U=e.length),E({cursorOffset:U,cursorWidth:te}),V!==e&&c(V)},{isActive:r}),ywe.createElement(mwe.Text,null,t?S.length>0?x:I:x)};ID.default=Ewe;ID.UncontrolledTextInput=({initialValue:e="",...t})=>{let[r,s]=uW.useState(e);return ywe.createElement(Ewe,Object.assign({},t,{value:r,onChange:s}))}});var Bwe={};Vt(Bwe,{Pad:()=>fW});var Cwe,wwe,fW,AW=Xe(()=>{Cwe=et(qc()),wwe=et(dn()),fW=({length:e,active:t})=>{if(e===0)return null;let r=e>1?` ${"-".repeat(e-1)}`:" ";return wwe.default.createElement(Cwe.Text,{dimColor:!t},r)}});var vwe={};Vt(vwe,{ItemOptions:()=>_pt});var wD,$0,_pt,Swe=Xe(()=>{wD=et(qc()),$0=et(dn());QF();xF();AW();_pt=function({active:e,skewer:t,options:r,value:s,onChange:a,sizes:n=[]}){let c=r.filter(({label:p})=>!!p).map(({value:p})=>p),f=r.findIndex(p=>p.value===s&&p.label!="");return yD(s,c,{active:e,minus:"left",plus:"right",set:a}),$0.default.createElement($0.default.Fragment,null,r.map(({label:p},h)=>{let E=h===f,C=n[h]-1||0,S=p.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,""),x=Math.max(0,C-S.length-2);return p?$0.default.createElement(wD.Box,{key:p,width:C,marginLeft:1},$0.default.createElement(wD.Text,{wrap:"truncate"},$0.default.createElement(tW,{active:E})," ",p),t?$0.default.createElement(fW,{active:e,length:x}):null):$0.default.createElement(wD.Box,{key:`spacer-${h}`,width:C,marginLeft:1})}))}});var _we=G((yKt,Uwe)=>{var BW;Uwe.exports=()=>(typeof BW>"u"&&(BW=Ie("zlib").brotliDecompressSync(Buffer.from("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","base64")).toString()),BW)});var a1e=G((GKt,o1e)=>{var RW=Symbol("arg flag"),Wc=class e extends Error{constructor(t,r){super(t),this.name="ArgError",this.code=r,Object.setPrototypeOf(this,e.prototype)}};function UD(e,{argv:t=process.argv.slice(2),permissive:r=!1,stopAtPositional:s=!1}={}){if(!e)throw new Wc("argument specification object is required","ARG_CONFIG_NO_SPEC");let a={_:[]},n={},c={};for(let f of Object.keys(e)){if(!f)throw new Wc("argument key cannot be an empty string","ARG_CONFIG_EMPTY_KEY");if(f[0]!=="-")throw new Wc(`argument key must start with '-' but found: '${f}'`,"ARG_CONFIG_NONOPT_KEY");if(f.length===1)throw new Wc(`argument key must have a name; singular '-' keys are not allowed: ${f}`,"ARG_CONFIG_NONAME_KEY");if(typeof e[f]=="string"){n[f]=e[f];continue}let p=e[f],h=!1;if(Array.isArray(p)&&p.length===1&&typeof p[0]=="function"){let[E]=p;p=(C,S,x=[])=>(x.push(E(C,S,x[x.length-1])),x),h=E===Boolean||E[RW]===!0}else if(typeof p=="function")h=p===Boolean||p[RW]===!0;else throw new Wc(`type missing or not a function or valid array type: ${f}`,"ARG_CONFIG_VAD_TYPE");if(f[1]!=="-"&&f.length>2)throw new Wc(`short argument keys (with a single hyphen) must have only one character: ${f}`,"ARG_CONFIG_SHORTOPT_TOOLONG");c[f]=[p,h]}for(let f=0,p=t.length;f0){a._=a._.concat(t.slice(f));break}if(h==="--"){a._=a._.concat(t.slice(f+1));break}if(h.length>1&&h[0]==="-"){let E=h[1]==="-"||h.length===2?[h]:h.slice(1).split("").map(C=>`-${C}`);for(let C=0;C1&&t[f+1][0]==="-"&&!(t[f+1].match(/^-?\d*(\.(?=\d))?\d*$/)&&(O===Number||typeof BigInt<"u"&&O===BigInt))){let V=x===T?"":` (alias for ${T})`;throw new Wc(`option requires argument: ${x}${V}`,"ARG_MISSING_REQUIRED_LONGARG")}a[T]=O(t[f+1],T,a[T]),++f}else a[T]=O(I,T,a[T])}}else a._.push(h)}return a}UD.flag=e=>(e[RW]=!0,e);UD.COUNT=UD.flag((e,t,r)=>(r||0)+1);UD.ArgError=Wc;o1e.exports=UD});var h1e=G((Ezt,p1e)=>{var LW;p1e.exports=()=>(typeof LW>"u"&&(LW=Ie("zlib").brotliDecompressSync(Buffer.from("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","base64")).toString()),LW)});var E1e=G((GW,qW)=>{(function(e){GW&&typeof GW=="object"&&typeof qW<"u"?qW.exports=e():typeof define=="function"&&define.amd?define([],e):typeof window<"u"?window.isWindows=e():typeof global<"u"?global.isWindows=e():typeof self<"u"?self.isWindows=e():this.isWindows=e()})(function(){"use strict";return function(){return process&&(process.platform==="win32"||/^(msys|cygwin)$/.test(process.env.OSTYPE))}})});var B1e=G((mXt,w1e)=>{"use strict";WW.ifExists=Hht;var yw=Ie("util"),Yc=Ie("path"),I1e=E1e(),Mht=/^#!\s*(?:\/usr\/bin\/env)?\s*([^ \t]+)(.*)$/,Uht={createPwshFile:!0,createCmdFile:I1e(),fs:Ie("fs")},_ht=new Map([[".js","node"],[".cjs","node"],[".mjs","node"],[".cmd","cmd"],[".bat","cmd"],[".ps1","pwsh"],[".sh","sh"]]);function C1e(e){let t={...Uht,...e},r=t.fs;return t.fs_={chmod:r.chmod?yw.promisify(r.chmod):async()=>{},mkdir:yw.promisify(r.mkdir),readFile:yw.promisify(r.readFile),stat:yw.promisify(r.stat),unlink:yw.promisify(r.unlink),writeFile:yw.promisify(r.writeFile)},t}async function WW(e,t,r){let s=C1e(r);await s.fs_.stat(e),await Ght(e,t,s)}function Hht(e,t,r){return WW(e,t,r).catch(()=>{})}function jht(e,t){return t.fs_.unlink(e).catch(()=>{})}async function Ght(e,t,r){let s=await Jht(e,r);return await qht(t,r),Wht(e,t,s,r)}function qht(e,t){return t.fs_.mkdir(Yc.dirname(e),{recursive:!0})}function Wht(e,t,r,s){let a=C1e(s),n=[{generator:Xht,extension:""}];return a.createCmdFile&&n.push({generator:zht,extension:".cmd"}),a.createPwshFile&&n.push({generator:Zht,extension:".ps1"}),Promise.all(n.map(c=>Kht(e,t+c.extension,r,c.generator,a)))}function Yht(e,t){return jht(e,t)}function Vht(e,t){return $ht(e,t)}async function Jht(e,t){let a=(await t.fs_.readFile(e,"utf8")).trim().split(/\r*\n/)[0].match(Mht);if(!a){let n=Yc.extname(e).toLowerCase();return{program:_ht.get(n)||null,additionalArgs:""}}return{program:a[1],additionalArgs:a[2]}}async function Kht(e,t,r,s,a){let n=a.preserveSymlinks?"--preserve-symlinks":"",c=[r.additionalArgs,n].filter(f=>f).join(" ");return a=Object.assign({},a,{prog:r.program,args:c}),await Yht(t,a),await a.fs_.writeFile(t,s(e,t,a),"utf8"),Vht(t,a)}function zht(e,t,r){let a=Yc.relative(Yc.dirname(t),e).split("/").join("\\"),n=Yc.isAbsolute(a)?`"${a}"`:`"%~dp0\\${a}"`,c,f=r.prog,p=r.args||"",h=YW(r.nodePath).win32;f?(c=`"%~dp0\\${f}.exe"`,a=n):(f=n,p="",a="");let E=r.progArgs?`${r.progArgs.join(" ")} `:"",C=h?`@SET NODE_PATH=${h}\r `:"";return c?C+=`@IF EXIST ${c} (\r ${c} ${p} ${a} ${E}%*\r ) ELSE (\r @SETLOCAL\r @SET PATHEXT=%PATHEXT:;.JS;=;%\r ${f} ${p} ${a} ${E}%*\r )\r `:C+=`@${f} ${p} ${a} ${E}%*\r `,C}function Xht(e,t,r){let s=Yc.relative(Yc.dirname(t),e),a=r.prog&&r.prog.split("\\").join("/"),n;s=s.split("\\").join("/");let c=Yc.isAbsolute(s)?`"${s}"`:`"$basedir/${s}"`,f=r.args||"",p=YW(r.nodePath).posix;a?(n=`"$basedir/${r.prog}"`,s=c):(a=c,f="",s="");let h=r.progArgs?`${r.progArgs.join(" ")} `:"",E=`#!/bin/sh basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") case \`uname\` in *CYGWIN*) basedir=\`cygpath -w "$basedir"\`;; esac `,C=r.nodePath?`export NODE_PATH="${p}" `:"";return n?E+=`${C}if [ -x ${n} ]; then exec ${n} ${f} ${s} ${h}"$@" else exec ${a} ${f} ${s} ${h}"$@" fi `:E+=`${C}${a} ${f} ${s} ${h}"$@" exit $? `,E}function Zht(e,t,r){let s=Yc.relative(Yc.dirname(t),e),a=r.prog&&r.prog.split("\\").join("/"),n=a&&`"${a}$exe"`,c;s=s.split("\\").join("/");let f=Yc.isAbsolute(s)?`"${s}"`:`"$basedir/${s}"`,p=r.args||"",h=YW(r.nodePath),E=h.win32,C=h.posix;n?(c=`"$basedir/${r.prog}$exe"`,s=f):(n=f,p="",s="");let S=r.progArgs?`${r.progArgs.join(" ")} `:"",x=`#!/usr/bin/env pwsh $basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent $exe="" ${r.nodePath?`$env_node_path=$env:NODE_PATH $env:NODE_PATH="${E}" `:""}if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { # Fix case when both the Windows and Linux builds of Node # are installed in the same directory $exe=".exe" }`;return r.nodePath&&(x+=` else { $env:NODE_PATH="${C}" }`),c?x+=` $ret=0 if (Test-Path ${c}) { # Support pipeline input if ($MyInvocation.ExpectingInput) { $input | & ${c} ${p} ${s} ${S}$args } else { & ${c} ${p} ${s} ${S}$args } $ret=$LASTEXITCODE } else { # Support pipeline input if ($MyInvocation.ExpectingInput) { $input | & ${n} ${p} ${s} ${S}$args } else { & ${n} ${p} ${s} ${S}$args } $ret=$LASTEXITCODE } ${r.nodePath?`$env:NODE_PATH=$env_node_path `:""}exit $ret `:x+=` # Support pipeline input if ($MyInvocation.ExpectingInput) { $input | & ${n} ${p} ${s} ${S}$args } else { & ${n} ${p} ${s} ${S}$args } ${r.nodePath?`$env:NODE_PATH=$env_node_path `:""}exit $LASTEXITCODE `,x}function $ht(e,t){return t.fs_.chmod(e,493)}function YW(e){if(!e)return{win32:"",posix:""};let t=typeof e=="string"?e.split(Yc.delimiter):Array.from(e),r={};for(let s=0;s`/mnt/${f.toLowerCase()}`):t[s];r.win32=r.win32?`${r.win32};${a}`:a,r.posix=r.posix?`${r.posix}:${n}`:n,r[s]={win32:a,posix:n}}return r}w1e.exports=WW});var oY=G((WZt,j1e)=>{j1e.exports=Ie("stream")});var Y1e=G((YZt,W1e)=>{"use strict";function G1e(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);t&&(s=s.filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable})),r.push.apply(r,s)}return r}function T0t(e){for(var t=1;t0?this.tail.next=s:this.head=s,this.tail=s,++this.length}},{key:"unshift",value:function(r){var s={data:r,next:this.head};this.length===0&&(this.tail=s),this.head=s,++this.length}},{key:"shift",value:function(){if(this.length!==0){var r=this.head.data;return this.length===1?this.head=this.tail=null:this.head=this.head.next,--this.length,r}}},{key:"clear",value:function(){this.head=this.tail=null,this.length=0}},{key:"join",value:function(r){if(this.length===0)return"";for(var s=this.head,a=""+s.data;s=s.next;)a+=r+s.data;return a}},{key:"concat",value:function(r){if(this.length===0)return XF.alloc(0);for(var s=XF.allocUnsafe(r>>>0),a=this.head,n=0;a;)_0t(a.data,s,n),n+=a.data.length,a=a.next;return s}},{key:"consume",value:function(r,s){var a;return rc.length?c.length:r;if(f===c.length?n+=c:n+=c.slice(0,r),r-=f,r===0){f===c.length?(++a,s.next?this.head=s.next:this.head=this.tail=null):(this.head=s,s.data=c.slice(f));break}++a}return this.length-=a,n}},{key:"_getBuffer",value:function(r){var s=XF.allocUnsafe(r),a=this.head,n=1;for(a.data.copy(s),r-=a.data.length;a=a.next;){var c=a.data,f=r>c.length?c.length:r;if(c.copy(s,s.length-r,0,f),r-=f,r===0){f===c.length?(++n,a.next?this.head=a.next:this.head=this.tail=null):(this.head=a,a.data=c.slice(f));break}++n}return this.length-=n,s}},{key:U0t,value:function(r,s){return aY(this,T0t({},s,{depth:0,customInspect:!1}))}}]),e}()});var cY=G((VZt,J1e)=>{"use strict";function H0t(e,t){var r=this,s=this._readableState&&this._readableState.destroyed,a=this._writableState&&this._writableState.destroyed;return s||a?(t?t(e):e&&(this._writableState?this._writableState.errorEmitted||(this._writableState.errorEmitted=!0,process.nextTick(lY,this,e)):process.nextTick(lY,this,e)),this):(this._readableState&&(this._readableState.destroyed=!0),this._writableState&&(this._writableState.destroyed=!0),this._destroy(e||null,function(n){!t&&n?r._writableState?r._writableState.errorEmitted?process.nextTick(ZF,r):(r._writableState.errorEmitted=!0,process.nextTick(V1e,r,n)):process.nextTick(V1e,r,n):t?(process.nextTick(ZF,r),t(n)):process.nextTick(ZF,r)}),this)}function V1e(e,t){lY(e,t),ZF(e)}function ZF(e){e._writableState&&!e._writableState.emitClose||e._readableState&&!e._readableState.emitClose||e.emit("close")}function j0t(){this._readableState&&(this._readableState.destroyed=!1,this._readableState.reading=!1,this._readableState.ended=!1,this._readableState.endEmitted=!1),this._writableState&&(this._writableState.destroyed=!1,this._writableState.ended=!1,this._writableState.ending=!1,this._writableState.finalCalled=!1,this._writableState.prefinished=!1,this._writableState.finished=!1,this._writableState.errorEmitted=!1)}function lY(e,t){e.emit("error",t)}function G0t(e,t){var r=e._readableState,s=e._writableState;r&&r.autoDestroy||s&&s.autoDestroy?e.destroy(t):e.emit("error",t)}J1e.exports={destroy:H0t,undestroy:j0t,errorOrDestroy:G0t}});var od=G((JZt,X1e)=>{"use strict";var z1e={};function Jc(e,t,r){r||(r=Error);function s(n,c,f){return typeof t=="string"?t:t(n,c,f)}class a extends r{constructor(c,f,p){super(s(c,f,p))}}a.prototype.name=r.name,a.prototype.code=e,z1e[e]=a}function K1e(e,t){if(Array.isArray(e)){let r=e.length;return e=e.map(s=>String(s)),r>2?`one of ${t} ${e.slice(0,r-1).join(", ")}, or `+e[r-1]:r===2?`one of ${t} ${e[0]} or ${e[1]}`:`of ${t} ${e[0]}`}else return`of ${t} ${String(e)}`}function q0t(e,t,r){return e.substr(!r||r<0?0:+r,t.length)===t}function W0t(e,t,r){return(r===void 0||r>e.length)&&(r=e.length),e.substring(r-t.length,r)===t}function Y0t(e,t,r){return typeof r!="number"&&(r=0),r+t.length>e.length?!1:e.indexOf(t,r)!==-1}Jc("ERR_INVALID_OPT_VALUE",function(e,t){return'The value "'+t+'" is invalid for option "'+e+'"'},TypeError);Jc("ERR_INVALID_ARG_TYPE",function(e,t,r){let s;typeof t=="string"&&q0t(t,"not ")?(s="must not be",t=t.replace(/^not /,"")):s="must be";let a;if(W0t(e," argument"))a=`The ${e} ${s} ${K1e(t,"type")}`;else{let n=Y0t(e,".")?"property":"argument";a=`The "${e}" ${n} ${s} ${K1e(t,"type")}`}return a+=`. Received type ${typeof r}`,a},TypeError);Jc("ERR_STREAM_PUSH_AFTER_EOF","stream.push() after EOF");Jc("ERR_METHOD_NOT_IMPLEMENTED",function(e){return"The "+e+" method is not implemented"});Jc("ERR_STREAM_PREMATURE_CLOSE","Premature close");Jc("ERR_STREAM_DESTROYED",function(e){return"Cannot call "+e+" after a stream was destroyed"});Jc("ERR_MULTIPLE_CALLBACK","Callback called multiple times");Jc("ERR_STREAM_CANNOT_PIPE","Cannot pipe, not readable");Jc("ERR_STREAM_WRITE_AFTER_END","write after end");Jc("ERR_STREAM_NULL_VALUES","May not write null values to stream",TypeError);Jc("ERR_UNKNOWN_ENCODING",function(e){return"Unknown encoding: "+e},TypeError);Jc("ERR_STREAM_UNSHIFT_AFTER_END_EVENT","stream.unshift() after end event");X1e.exports.codes=z1e});var uY=G((KZt,Z1e)=>{"use strict";var V0t=od().codes.ERR_INVALID_OPT_VALUE;function J0t(e,t,r){return e.highWaterMark!=null?e.highWaterMark:t?e[r]:null}function K0t(e,t,r,s){var a=J0t(t,s,r);if(a!=null){if(!(isFinite(a)&&Math.floor(a)===a)||a<0){var n=s?r:"highWaterMark";throw new V0t(n,a)}return Math.floor(a)}return e.objectMode?16:16*1024}Z1e.exports={getHighWaterMark:K0t}});var $1e=G((zZt,fY)=>{typeof Object.create=="function"?fY.exports=function(t,r){r&&(t.super_=r,t.prototype=Object.create(r.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}))}:fY.exports=function(t,r){if(r){t.super_=r;var s=function(){};s.prototype=r.prototype,t.prototype=new s,t.prototype.constructor=t}}});var ad=G((XZt,pY)=>{try{if(AY=Ie("util"),typeof AY.inherits!="function")throw"";pY.exports=AY.inherits}catch{pY.exports=$1e()}var AY});var t2e=G((ZZt,e2e)=>{e2e.exports=Ie("util").deprecate});var gY=G(($Zt,a2e)=>{"use strict";a2e.exports=Vi;function n2e(e){var t=this;this.next=null,this.entry=null,this.finish=function(){wdt(t,e)}}var vw;Vi.WritableState=ZD;var z0t={deprecate:t2e()},i2e=oY(),eN=Ie("buffer").Buffer,X0t=global.Uint8Array||function(){};function Z0t(e){return eN.from(e)}function $0t(e){return eN.isBuffer(e)||e instanceof X0t}var dY=cY(),edt=uY(),tdt=edt.getHighWaterMark,ld=od().codes,rdt=ld.ERR_INVALID_ARG_TYPE,ndt=ld.ERR_METHOD_NOT_IMPLEMENTED,idt=ld.ERR_MULTIPLE_CALLBACK,sdt=ld.ERR_STREAM_CANNOT_PIPE,odt=ld.ERR_STREAM_DESTROYED,adt=ld.ERR_STREAM_NULL_VALUES,ldt=ld.ERR_STREAM_WRITE_AFTER_END,cdt=ld.ERR_UNKNOWN_ENCODING,Sw=dY.errorOrDestroy;ad()(Vi,i2e);function udt(){}function ZD(e,t,r){vw=vw||Km(),e=e||{},typeof r!="boolean"&&(r=t instanceof vw),this.objectMode=!!e.objectMode,r&&(this.objectMode=this.objectMode||!!e.writableObjectMode),this.highWaterMark=tdt(this,e,"writableHighWaterMark",r),this.finalCalled=!1,this.needDrain=!1,this.ending=!1,this.ended=!1,this.finished=!1,this.destroyed=!1;var s=e.decodeStrings===!1;this.decodeStrings=!s,this.defaultEncoding=e.defaultEncoding||"utf8",this.length=0,this.writing=!1,this.corked=0,this.sync=!0,this.bufferProcessing=!1,this.onwrite=function(a){mdt(t,a)},this.writecb=null,this.writelen=0,this.bufferedRequest=null,this.lastBufferedRequest=null,this.pendingcb=0,this.prefinished=!1,this.errorEmitted=!1,this.emitClose=e.emitClose!==!1,this.autoDestroy=!!e.autoDestroy,this.bufferedRequestCount=0,this.corkedRequestsFree=new n2e(this)}ZD.prototype.getBuffer=function(){for(var t=this.bufferedRequest,r=[];t;)r.push(t),t=t.next;return r};(function(){try{Object.defineProperty(ZD.prototype,"buffer",{get:z0t.deprecate(function(){return this.getBuffer()},"_writableState.buffer is deprecated. Use _writableState.getBuffer instead.","DEP0003")})}catch{}})();var $F;typeof Symbol=="function"&&Symbol.hasInstance&&typeof Function.prototype[Symbol.hasInstance]=="function"?($F=Function.prototype[Symbol.hasInstance],Object.defineProperty(Vi,Symbol.hasInstance,{value:function(t){return $F.call(this,t)?!0:this!==Vi?!1:t&&t._writableState instanceof ZD}})):$F=function(t){return t instanceof this};function Vi(e){vw=vw||Km();var t=this instanceof vw;if(!t&&!$F.call(Vi,this))return new Vi(e);this._writableState=new ZD(e,this,t),this.writable=!0,e&&(typeof e.write=="function"&&(this._write=e.write),typeof e.writev=="function"&&(this._writev=e.writev),typeof e.destroy=="function"&&(this._destroy=e.destroy),typeof e.final=="function"&&(this._final=e.final)),i2e.call(this)}Vi.prototype.pipe=function(){Sw(this,new sdt)};function fdt(e,t){var r=new ldt;Sw(e,r),process.nextTick(t,r)}function Adt(e,t,r,s){var a;return r===null?a=new adt:typeof r!="string"&&!t.objectMode&&(a=new rdt("chunk",["string","Buffer"],r)),a?(Sw(e,a),process.nextTick(s,a),!1):!0}Vi.prototype.write=function(e,t,r){var s=this._writableState,a=!1,n=!s.objectMode&&$0t(e);return n&&!eN.isBuffer(e)&&(e=Z0t(e)),typeof t=="function"&&(r=t,t=null),n?t="buffer":t||(t=s.defaultEncoding),typeof r!="function"&&(r=udt),s.ending?fdt(this,r):(n||Adt(this,s,e,r))&&(s.pendingcb++,a=hdt(this,s,n,e,t,r)),a};Vi.prototype.cork=function(){this._writableState.corked++};Vi.prototype.uncork=function(){var e=this._writableState;e.corked&&(e.corked--,!e.writing&&!e.corked&&!e.bufferProcessing&&e.bufferedRequest&&s2e(this,e))};Vi.prototype.setDefaultEncoding=function(t){if(typeof t=="string"&&(t=t.toLowerCase()),!(["hex","utf8","utf-8","ascii","binary","base64","ucs2","ucs-2","utf16le","utf-16le","raw"].indexOf((t+"").toLowerCase())>-1))throw new cdt(t);return this._writableState.defaultEncoding=t,this};Object.defineProperty(Vi.prototype,"writableBuffer",{enumerable:!1,get:function(){return this._writableState&&this._writableState.getBuffer()}});function pdt(e,t,r){return!e.objectMode&&e.decodeStrings!==!1&&typeof t=="string"&&(t=eN.from(t,r)),t}Object.defineProperty(Vi.prototype,"writableHighWaterMark",{enumerable:!1,get:function(){return this._writableState.highWaterMark}});function hdt(e,t,r,s,a,n){if(!r){var c=pdt(t,s,a);s!==c&&(r=!0,a="buffer",s=c)}var f=t.objectMode?1:s.length;t.length+=f;var p=t.length{"use strict";var Bdt=Object.keys||function(e){var t=[];for(var r in e)t.push(r);return t};c2e.exports=yA;var l2e=EY(),yY=gY();ad()(yA,l2e);for(mY=Bdt(yY.prototype),tN=0;tN{var nN=Ie("buffer"),oh=nN.Buffer;function u2e(e,t){for(var r in e)t[r]=e[r]}oh.from&&oh.alloc&&oh.allocUnsafe&&oh.allocUnsafeSlow?f2e.exports=nN:(u2e(nN,IY),IY.Buffer=Dw);function Dw(e,t,r){return oh(e,t,r)}u2e(oh,Dw);Dw.from=function(e,t,r){if(typeof e=="number")throw new TypeError("Argument must not be a number");return oh(e,t,r)};Dw.alloc=function(e,t,r){if(typeof e!="number")throw new TypeError("Argument must be a number");var s=oh(e);return t!==void 0?typeof r=="string"?s.fill(t,r):s.fill(t):s.fill(0),s};Dw.allocUnsafe=function(e){if(typeof e!="number")throw new TypeError("Argument must be a number");return oh(e)};Dw.allocUnsafeSlow=function(e){if(typeof e!="number")throw new TypeError("Argument must be a number");return nN.SlowBuffer(e)}});var BY=G(h2e=>{"use strict";var wY=A2e().Buffer,p2e=wY.isEncoding||function(e){switch(e=""+e,e&&e.toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"binary":case"base64":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":case"raw":return!0;default:return!1}};function Ddt(e){if(!e)return"utf8";for(var t;;)switch(e){case"utf8":case"utf-8":return"utf8";case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return"utf16le";case"latin1":case"binary":return"latin1";case"base64":case"ascii":case"hex":return e;default:if(t)return;e=(""+e).toLowerCase(),t=!0}}function bdt(e){var t=Ddt(e);if(typeof t!="string"&&(wY.isEncoding===p2e||!p2e(e)))throw new Error("Unknown encoding: "+e);return t||e}h2e.StringDecoder=$D;function $D(e){this.encoding=bdt(e);var t;switch(this.encoding){case"utf16le":this.text=Tdt,this.end=Fdt,t=4;break;case"utf8":this.fillLast=kdt,t=4;break;case"base64":this.text=Ndt,this.end=Odt,t=3;break;default:this.write=Ldt,this.end=Mdt;return}this.lastNeed=0,this.lastTotal=0,this.lastChar=wY.allocUnsafe(t)}$D.prototype.write=function(e){if(e.length===0)return"";var t,r;if(this.lastNeed){if(t=this.fillLast(e),t===void 0)return"";r=this.lastNeed,this.lastNeed=0}else r=0;return r>5===6?2:e>>4===14?3:e>>3===30?4:e>>6===2?-1:-2}function Pdt(e,t,r){var s=t.length-1;if(s=0?(a>0&&(e.lastNeed=a-1),a):--s=0?(a>0&&(e.lastNeed=a-2),a):--s=0?(a>0&&(a===2?a=0:e.lastNeed=a-3),a):0))}function xdt(e,t,r){if((t[0]&192)!==128)return e.lastNeed=0,"\uFFFD";if(e.lastNeed>1&&t.length>1){if((t[1]&192)!==128)return e.lastNeed=1,"\uFFFD";if(e.lastNeed>2&&t.length>2&&(t[2]&192)!==128)return e.lastNeed=2,"\uFFFD"}}function kdt(e){var t=this.lastTotal-this.lastNeed,r=xdt(this,e,t);if(r!==void 0)return r;if(this.lastNeed<=e.length)return e.copy(this.lastChar,t,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal);e.copy(this.lastChar,t,0,e.length),this.lastNeed-=e.length}function Qdt(e,t){var r=Pdt(this,e,t);if(!this.lastNeed)return e.toString("utf8",t);this.lastTotal=r;var s=e.length-(r-this.lastNeed);return e.copy(this.lastChar,0,s),e.toString("utf8",t,s)}function Rdt(e){var t=e&&e.length?this.write(e):"";return this.lastNeed?t+"\uFFFD":t}function Tdt(e,t){if((e.length-t)%2===0){var r=e.toString("utf16le",t);if(r){var s=r.charCodeAt(r.length-1);if(s>=55296&&s<=56319)return this.lastNeed=2,this.lastTotal=4,this.lastChar[0]=e[e.length-2],this.lastChar[1]=e[e.length-1],r.slice(0,-1)}return r}return this.lastNeed=1,this.lastTotal=2,this.lastChar[0]=e[e.length-1],e.toString("utf16le",t,e.length-1)}function Fdt(e){var t=e&&e.length?this.write(e):"";if(this.lastNeed){var r=this.lastTotal-this.lastNeed;return t+this.lastChar.toString("utf16le",0,r)}return t}function Ndt(e,t){var r=(e.length-t)%3;return r===0?e.toString("base64",t):(this.lastNeed=3-r,this.lastTotal=3,r===1?this.lastChar[0]=e[e.length-1]:(this.lastChar[0]=e[e.length-2],this.lastChar[1]=e[e.length-1]),e.toString("base64",t,e.length-r))}function Odt(e){var t=e&&e.length?this.write(e):"";return this.lastNeed?t+this.lastChar.toString("base64",0,3-this.lastNeed):t}function Ldt(e){return e.toString(this.encoding)}function Mdt(e){return e&&e.length?this.write(e):""}});var iN=G((r$t,m2e)=>{"use strict";var d2e=od().codes.ERR_STREAM_PREMATURE_CLOSE;function Udt(e){var t=!1;return function(){if(!t){t=!0;for(var r=arguments.length,s=new Array(r),a=0;a{"use strict";var sN;function cd(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}var jdt=iN(),ud=Symbol("lastResolve"),zm=Symbol("lastReject"),eb=Symbol("error"),oN=Symbol("ended"),Xm=Symbol("lastPromise"),vY=Symbol("handlePromise"),Zm=Symbol("stream");function fd(e,t){return{value:e,done:t}}function Gdt(e){var t=e[ud];if(t!==null){var r=e[Zm].read();r!==null&&(e[Xm]=null,e[ud]=null,e[zm]=null,t(fd(r,!1)))}}function qdt(e){process.nextTick(Gdt,e)}function Wdt(e,t){return function(r,s){e.then(function(){if(t[oN]){r(fd(void 0,!0));return}t[vY](r,s)},s)}}var Ydt=Object.getPrototypeOf(function(){}),Vdt=Object.setPrototypeOf((sN={get stream(){return this[Zm]},next:function(){var t=this,r=this[eb];if(r!==null)return Promise.reject(r);if(this[oN])return Promise.resolve(fd(void 0,!0));if(this[Zm].destroyed)return new Promise(function(c,f){process.nextTick(function(){t[eb]?f(t[eb]):c(fd(void 0,!0))})});var s=this[Xm],a;if(s)a=new Promise(Wdt(s,this));else{var n=this[Zm].read();if(n!==null)return Promise.resolve(fd(n,!1));a=new Promise(this[vY])}return this[Xm]=a,a}},cd(sN,Symbol.asyncIterator,function(){return this}),cd(sN,"return",function(){var t=this;return new Promise(function(r,s){t[Zm].destroy(null,function(a){if(a){s(a);return}r(fd(void 0,!0))})})}),sN),Ydt),Jdt=function(t){var r,s=Object.create(Vdt,(r={},cd(r,Zm,{value:t,writable:!0}),cd(r,ud,{value:null,writable:!0}),cd(r,zm,{value:null,writable:!0}),cd(r,eb,{value:null,writable:!0}),cd(r,oN,{value:t._readableState.endEmitted,writable:!0}),cd(r,vY,{value:function(n,c){var f=s[Zm].read();f?(s[Xm]=null,s[ud]=null,s[zm]=null,n(fd(f,!1))):(s[ud]=n,s[zm]=c)},writable:!0}),r));return s[Xm]=null,jdt(t,function(a){if(a&&a.code!=="ERR_STREAM_PREMATURE_CLOSE"){var n=s[zm];n!==null&&(s[Xm]=null,s[ud]=null,s[zm]=null,n(a)),s[eb]=a;return}var c=s[ud];c!==null&&(s[Xm]=null,s[ud]=null,s[zm]=null,c(fd(void 0,!0))),s[oN]=!0}),t.on("readable",qdt.bind(null,s)),s};y2e.exports=Jdt});var B2e=G((i$t,w2e)=>{"use strict";function I2e(e,t,r,s,a,n,c){try{var f=e[n](c),p=f.value}catch(h){r(h);return}f.done?t(p):Promise.resolve(p).then(s,a)}function Kdt(e){return function(){var t=this,r=arguments;return new Promise(function(s,a){var n=e.apply(t,r);function c(p){I2e(n,s,a,c,f,"next",p)}function f(p){I2e(n,s,a,c,f,"throw",p)}c(void 0)})}}function C2e(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);t&&(s=s.filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable})),r.push.apply(r,s)}return r}function zdt(e){for(var t=1;t{"use strict";T2e.exports=kn;var bw;kn.ReadableState=b2e;var s$t=Ie("events").EventEmitter,D2e=function(t,r){return t.listeners(r).length},rb=oY(),aN=Ie("buffer").Buffer,egt=global.Uint8Array||function(){};function tgt(e){return aN.from(e)}function rgt(e){return aN.isBuffer(e)||e instanceof egt}var SY=Ie("util"),un;SY&&SY.debuglog?un=SY.debuglog("stream"):un=function(){};var ngt=Y1e(),RY=cY(),igt=uY(),sgt=igt.getHighWaterMark,lN=od().codes,ogt=lN.ERR_INVALID_ARG_TYPE,agt=lN.ERR_STREAM_PUSH_AFTER_EOF,lgt=lN.ERR_METHOD_NOT_IMPLEMENTED,cgt=lN.ERR_STREAM_UNSHIFT_AFTER_END_EVENT,Pw,DY,bY;ad()(kn,rb);var tb=RY.errorOrDestroy,PY=["error","close","destroy","pause","resume"];function ugt(e,t,r){if(typeof e.prependListener=="function")return e.prependListener(t,r);!e._events||!e._events[t]?e.on(t,r):Array.isArray(e._events[t])?e._events[t].unshift(r):e._events[t]=[r,e._events[t]]}function b2e(e,t,r){bw=bw||Km(),e=e||{},typeof r!="boolean"&&(r=t instanceof bw),this.objectMode=!!e.objectMode,r&&(this.objectMode=this.objectMode||!!e.readableObjectMode),this.highWaterMark=sgt(this,e,"readableHighWaterMark",r),this.buffer=new ngt,this.length=0,this.pipes=null,this.pipesCount=0,this.flowing=null,this.ended=!1,this.endEmitted=!1,this.reading=!1,this.sync=!0,this.needReadable=!1,this.emittedReadable=!1,this.readableListening=!1,this.resumeScheduled=!1,this.paused=!0,this.emitClose=e.emitClose!==!1,this.autoDestroy=!!e.autoDestroy,this.destroyed=!1,this.defaultEncoding=e.defaultEncoding||"utf8",this.awaitDrain=0,this.readingMore=!1,this.decoder=null,this.encoding=null,e.encoding&&(Pw||(Pw=BY().StringDecoder),this.decoder=new Pw(e.encoding),this.encoding=e.encoding)}function kn(e){if(bw=bw||Km(),!(this instanceof kn))return new kn(e);var t=this instanceof bw;this._readableState=new b2e(e,this,t),this.readable=!0,e&&(typeof e.read=="function"&&(this._read=e.read),typeof e.destroy=="function"&&(this._destroy=e.destroy)),rb.call(this)}Object.defineProperty(kn.prototype,"destroyed",{enumerable:!1,get:function(){return this._readableState===void 0?!1:this._readableState.destroyed},set:function(t){this._readableState&&(this._readableState.destroyed=t)}});kn.prototype.destroy=RY.destroy;kn.prototype._undestroy=RY.undestroy;kn.prototype._destroy=function(e,t){t(e)};kn.prototype.push=function(e,t){var r=this._readableState,s;return r.objectMode?s=!0:typeof e=="string"&&(t=t||r.defaultEncoding,t!==r.encoding&&(e=aN.from(e,t),t=""),s=!0),P2e(this,e,t,!1,s)};kn.prototype.unshift=function(e){return P2e(this,e,null,!0,!1)};function P2e(e,t,r,s,a){un("readableAddChunk",t);var n=e._readableState;if(t===null)n.reading=!1,pgt(e,n);else{var c;if(a||(c=fgt(n,t)),c)tb(e,c);else if(n.objectMode||t&&t.length>0)if(typeof t!="string"&&!n.objectMode&&Object.getPrototypeOf(t)!==aN.prototype&&(t=tgt(t)),s)n.endEmitted?tb(e,new cgt):xY(e,n,t,!0);else if(n.ended)tb(e,new agt);else{if(n.destroyed)return!1;n.reading=!1,n.decoder&&!r?(t=n.decoder.write(t),n.objectMode||t.length!==0?xY(e,n,t,!1):QY(e,n)):xY(e,n,t,!1)}else s||(n.reading=!1,QY(e,n))}return!n.ended&&(n.length=v2e?e=v2e:(e--,e|=e>>>1,e|=e>>>2,e|=e>>>4,e|=e>>>8,e|=e>>>16,e++),e}function S2e(e,t){return e<=0||t.length===0&&t.ended?0:t.objectMode?1:e!==e?t.flowing&&t.length?t.buffer.head.data.length:t.length:(e>t.highWaterMark&&(t.highWaterMark=Agt(e)),e<=t.length?e:t.ended?t.length:(t.needReadable=!0,0))}kn.prototype.read=function(e){un("read",e),e=parseInt(e,10);var t=this._readableState,r=e;if(e!==0&&(t.emittedReadable=!1),e===0&&t.needReadable&&((t.highWaterMark!==0?t.length>=t.highWaterMark:t.length>0)||t.ended))return un("read: emitReadable",t.length,t.ended),t.length===0&&t.ended?kY(this):cN(this),null;if(e=S2e(e,t),e===0&&t.ended)return t.length===0&&kY(this),null;var s=t.needReadable;un("need readable",s),(t.length===0||t.length-e0?a=Q2e(e,t):a=null,a===null?(t.needReadable=t.length<=t.highWaterMark,e=0):(t.length-=e,t.awaitDrain=0),t.length===0&&(t.ended||(t.needReadable=!0),r!==e&&t.ended&&kY(this)),a!==null&&this.emit("data",a),a};function pgt(e,t){if(un("onEofChunk"),!t.ended){if(t.decoder){var r=t.decoder.end();r&&r.length&&(t.buffer.push(r),t.length+=t.objectMode?1:r.length)}t.ended=!0,t.sync?cN(e):(t.needReadable=!1,t.emittedReadable||(t.emittedReadable=!0,x2e(e)))}}function cN(e){var t=e._readableState;un("emitReadable",t.needReadable,t.emittedReadable),t.needReadable=!1,t.emittedReadable||(un("emitReadable",t.flowing),t.emittedReadable=!0,process.nextTick(x2e,e))}function x2e(e){var t=e._readableState;un("emitReadable_",t.destroyed,t.length,t.ended),!t.destroyed&&(t.length||t.ended)&&(e.emit("readable"),t.emittedReadable=!1),t.needReadable=!t.flowing&&!t.ended&&t.length<=t.highWaterMark,TY(e)}function QY(e,t){t.readingMore||(t.readingMore=!0,process.nextTick(hgt,e,t))}function hgt(e,t){for(;!t.reading&&!t.ended&&(t.length1&&R2e(s.pipes,e)!==-1)&&!h&&(un("false write response, pause",s.awaitDrain),s.awaitDrain++),r.pause())}function S(O){un("onerror",O),T(),e.removeListener("error",S),D2e(e,"error")===0&&tb(e,O)}ugt(e,"error",S);function x(){e.removeListener("finish",I),T()}e.once("close",x);function I(){un("onfinish"),e.removeListener("close",x),T()}e.once("finish",I);function T(){un("unpipe"),r.unpipe(e)}return e.emit("pipe",r),s.flowing||(un("pipe resume"),r.resume()),e};function dgt(e){return function(){var r=e._readableState;un("pipeOnDrain",r.awaitDrain),r.awaitDrain&&r.awaitDrain--,r.awaitDrain===0&&D2e(e,"data")&&(r.flowing=!0,TY(e))}}kn.prototype.unpipe=function(e){var t=this._readableState,r={hasUnpiped:!1};if(t.pipesCount===0)return this;if(t.pipesCount===1)return e&&e!==t.pipes?this:(e||(e=t.pipes),t.pipes=null,t.pipesCount=0,t.flowing=!1,e&&e.emit("unpipe",this,r),this);if(!e){var s=t.pipes,a=t.pipesCount;t.pipes=null,t.pipesCount=0,t.flowing=!1;for(var n=0;n0,s.flowing!==!1&&this.resume()):e==="readable"&&!s.endEmitted&&!s.readableListening&&(s.readableListening=s.needReadable=!0,s.flowing=!1,s.emittedReadable=!1,un("on readable",s.length,s.reading),s.length?cN(this):s.reading||process.nextTick(ggt,this)),r};kn.prototype.addListener=kn.prototype.on;kn.prototype.removeListener=function(e,t){var r=rb.prototype.removeListener.call(this,e,t);return e==="readable"&&process.nextTick(k2e,this),r};kn.prototype.removeAllListeners=function(e){var t=rb.prototype.removeAllListeners.apply(this,arguments);return(e==="readable"||e===void 0)&&process.nextTick(k2e,this),t};function k2e(e){var t=e._readableState;t.readableListening=e.listenerCount("readable")>0,t.resumeScheduled&&!t.paused?t.flowing=!0:e.listenerCount("data")>0&&e.resume()}function ggt(e){un("readable nexttick read 0"),e.read(0)}kn.prototype.resume=function(){var e=this._readableState;return e.flowing||(un("resume"),e.flowing=!e.readableListening,mgt(this,e)),e.paused=!1,this};function mgt(e,t){t.resumeScheduled||(t.resumeScheduled=!0,process.nextTick(ygt,e,t))}function ygt(e,t){un("resume",t.reading),t.reading||e.read(0),t.resumeScheduled=!1,e.emit("resume"),TY(e),t.flowing&&!t.reading&&e.read(0)}kn.prototype.pause=function(){return un("call pause flowing=%j",this._readableState.flowing),this._readableState.flowing!==!1&&(un("pause"),this._readableState.flowing=!1,this.emit("pause")),this._readableState.paused=!0,this};function TY(e){var t=e._readableState;for(un("flow",t.flowing);t.flowing&&e.read()!==null;);}kn.prototype.wrap=function(e){var t=this,r=this._readableState,s=!1;e.on("end",function(){if(un("wrapped end"),r.decoder&&!r.ended){var c=r.decoder.end();c&&c.length&&t.push(c)}t.push(null)}),e.on("data",function(c){if(un("wrapped data"),r.decoder&&(c=r.decoder.write(c)),!(r.objectMode&&c==null)&&!(!r.objectMode&&(!c||!c.length))){var f=t.push(c);f||(s=!0,e.pause())}});for(var a in e)this[a]===void 0&&typeof e[a]=="function"&&(this[a]=function(f){return function(){return e[f].apply(e,arguments)}}(a));for(var n=0;n=t.length?(t.decoder?r=t.buffer.join(""):t.buffer.length===1?r=t.buffer.first():r=t.buffer.concat(t.length),t.buffer.clear()):r=t.buffer.consume(e,t.decoder),r}function kY(e){var t=e._readableState;un("endReadable",t.endEmitted),t.endEmitted||(t.ended=!0,process.nextTick(Egt,t,e))}function Egt(e,t){if(un("endReadableNT",e.endEmitted,e.length),!e.endEmitted&&e.length===0&&(e.endEmitted=!0,t.readable=!1,t.emit("end"),e.autoDestroy)){var r=t._writableState;(!r||r.autoDestroy&&r.finished)&&t.destroy()}}typeof Symbol=="function"&&(kn.from=function(e,t){return bY===void 0&&(bY=B2e()),bY(kn,e,t)});function R2e(e,t){for(var r=0,s=e.length;r{"use strict";N2e.exports=ah;var uN=od().codes,Igt=uN.ERR_METHOD_NOT_IMPLEMENTED,Cgt=uN.ERR_MULTIPLE_CALLBACK,wgt=uN.ERR_TRANSFORM_ALREADY_TRANSFORMING,Bgt=uN.ERR_TRANSFORM_WITH_LENGTH_0,fN=Km();ad()(ah,fN);function vgt(e,t){var r=this._transformState;r.transforming=!1;var s=r.writecb;if(s===null)return this.emit("error",new Cgt);r.writechunk=null,r.writecb=null,t!=null&&this.push(t),s(e);var a=this._readableState;a.reading=!1,(a.needReadable||a.length{"use strict";L2e.exports=nb;var O2e=FY();ad()(nb,O2e);function nb(e){if(!(this instanceof nb))return new nb(e);O2e.call(this,e)}nb.prototype._transform=function(e,t,r){r(null,e)}});var G2e=G((c$t,j2e)=>{"use strict";var NY;function Dgt(e){var t=!1;return function(){t||(t=!0,e.apply(void 0,arguments))}}var H2e=od().codes,bgt=H2e.ERR_MISSING_ARGS,Pgt=H2e.ERR_STREAM_DESTROYED;function U2e(e){if(e)throw e}function xgt(e){return e.setHeader&&typeof e.abort=="function"}function kgt(e,t,r,s){s=Dgt(s);var a=!1;e.on("close",function(){a=!0}),NY===void 0&&(NY=iN()),NY(e,{readable:t,writable:r},function(c){if(c)return s(c);a=!0,s()});var n=!1;return function(c){if(!a&&!n){if(n=!0,xgt(e))return e.abort();if(typeof e.destroy=="function")return e.destroy();s(c||new Pgt("pipe"))}}}function _2e(e){e()}function Qgt(e,t){return e.pipe(t)}function Rgt(e){return!e.length||typeof e[e.length-1]!="function"?U2e:e.pop()}function Tgt(){for(var e=arguments.length,t=new Array(e),r=0;r0;return kgt(c,p,h,function(E){a||(a=E),E&&n.forEach(_2e),!p&&(n.forEach(_2e),s(a))})});return t.reduce(Qgt)}j2e.exports=Tgt});var xw=G((Kc,sb)=>{var ib=Ie("stream");process.env.READABLE_STREAM==="disable"&&ib?(sb.exports=ib.Readable,Object.assign(sb.exports,ib),sb.exports.Stream=ib):(Kc=sb.exports=EY(),Kc.Stream=ib||Kc,Kc.Readable=Kc,Kc.Writable=gY(),Kc.Duplex=Km(),Kc.Transform=FY(),Kc.PassThrough=M2e(),Kc.finished=iN(),Kc.pipeline=G2e())});var Y2e=G((u$t,W2e)=>{"use strict";var{Buffer:Af}=Ie("buffer"),q2e=Symbol.for("BufferList");function wi(e){if(!(this instanceof wi))return new wi(e);wi._init.call(this,e)}wi._init=function(t){Object.defineProperty(this,q2e,{value:!0}),this._bufs=[],this.length=0,t&&this.append(t)};wi.prototype._new=function(t){return new wi(t)};wi.prototype._offset=function(t){if(t===0)return[0,0];let r=0;for(let s=0;sthis.length||t<0)return;let r=this._offset(t);return this._bufs[r[0]][r[1]]};wi.prototype.slice=function(t,r){return typeof t=="number"&&t<0&&(t+=this.length),typeof r=="number"&&r<0&&(r+=this.length),this.copy(null,0,t,r)};wi.prototype.copy=function(t,r,s,a){if((typeof s!="number"||s<0)&&(s=0),(typeof a!="number"||a>this.length)&&(a=this.length),s>=this.length||a<=0)return t||Af.alloc(0);let n=!!t,c=this._offset(s),f=a-s,p=f,h=n&&r||0,E=c[1];if(s===0&&a===this.length){if(!n)return this._bufs.length===1?this._bufs[0]:Af.concat(this._bufs,this.length);for(let C=0;CS)this._bufs[C].copy(t,h,E),h+=S;else{this._bufs[C].copy(t,h,E,E+p),h+=S;break}p-=S,E&&(E=0)}return t.length>h?t.slice(0,h):t};wi.prototype.shallowSlice=function(t,r){if(t=t||0,r=typeof r!="number"?this.length:r,t<0&&(t+=this.length),r<0&&(r+=this.length),t===r)return this._new();let s=this._offset(t),a=this._offset(r),n=this._bufs.slice(s[0],a[0]+1);return a[1]===0?n.pop():n[n.length-1]=n[n.length-1].slice(0,a[1]),s[1]!==0&&(n[0]=n[0].slice(s[1])),this._new(n)};wi.prototype.toString=function(t,r,s){return this.slice(r,s).toString(t)};wi.prototype.consume=function(t){if(t=Math.trunc(t),Number.isNaN(t)||t<=0)return this;for(;this._bufs.length;)if(t>=this._bufs[0].length)t-=this._bufs[0].length,this.length-=this._bufs[0].length,this._bufs.shift();else{this._bufs[0]=this._bufs[0].slice(t),this.length-=t;break}return this};wi.prototype.duplicate=function(){let t=this._new();for(let r=0;rthis.length?this.length:t;let s=this._offset(t),a=s[0],n=s[1];for(;a=e.length){let p=c.indexOf(e,n);if(p!==-1)return this._reverseOffset([a,p]);n=c.length-e.length+1}else{let p=this._reverseOffset([a,n]);if(this._match(p,e))return p;n++}n=0}return-1};wi.prototype._match=function(e,t){if(this.length-e{"use strict";var OY=xw().Duplex,Fgt=ad(),ob=Y2e();function aa(e){if(!(this instanceof aa))return new aa(e);if(typeof e=="function"){this._callback=e;let t=function(s){this._callback&&(this._callback(s),this._callback=null)}.bind(this);this.on("pipe",function(s){s.on("error",t)}),this.on("unpipe",function(s){s.removeListener("error",t)}),e=null}ob._init.call(this,e),OY.call(this)}Fgt(aa,OY);Object.assign(aa.prototype,ob.prototype);aa.prototype._new=function(t){return new aa(t)};aa.prototype._write=function(t,r,s){this._appendBuffer(t),typeof s=="function"&&s()};aa.prototype._read=function(t){if(!this.length)return this.push(null);t=Math.min(t,this.length),this.push(this.slice(0,t)),this.consume(t)};aa.prototype.end=function(t){OY.prototype.end.call(this,t),this._callback&&(this._callback(null,this.slice()),this._callback=null)};aa.prototype._destroy=function(t,r){this._bufs.length=0,this.length=0,r(t)};aa.prototype._isBufferList=function(t){return t instanceof aa||t instanceof ob||aa.isBufferList(t)};aa.isBufferList=ob.isBufferList;AN.exports=aa;AN.exports.BufferListStream=aa;AN.exports.BufferList=ob});var UY=G(Qw=>{var Ngt=Buffer.alloc,Ogt="0000000000000000000",Lgt="7777777777777777777",J2e=48,K2e=Buffer.from("ustar\0","binary"),Mgt=Buffer.from("00","binary"),Ugt=Buffer.from("ustar ","binary"),_gt=Buffer.from(" \0","binary"),Hgt=parseInt("7777",8),ab=257,MY=263,jgt=function(e,t,r){return typeof e!="number"?r:(e=~~e,e>=t?t:e>=0||(e+=t,e>=0)?e:0)},Ggt=function(e){switch(e){case 0:return"file";case 1:return"link";case 2:return"symlink";case 3:return"character-device";case 4:return"block-device";case 5:return"directory";case 6:return"fifo";case 7:return"contiguous-file";case 72:return"pax-header";case 55:return"pax-global-header";case 27:return"gnu-long-link-path";case 28:case 30:return"gnu-long-path"}return null},qgt=function(e){switch(e){case"file":return 0;case"link":return 1;case"symlink":return 2;case"character-device":return 3;case"block-device":return 4;case"directory":return 5;case"fifo":return 6;case"contiguous-file":return 7;case"pax-header":return 72}return 0},z2e=function(e,t,r,s){for(;rt?Lgt.slice(0,t)+" ":Ogt.slice(0,t-e.length)+e+" "};function Wgt(e){var t;if(e[0]===128)t=!0;else if(e[0]===255)t=!1;else return null;for(var r=[],s=e.length-1;s>0;s--){var a=e[s];t?r.push(a):r.push(255-a)}var n=0,c=r.length;for(s=0;s=Math.pow(10,r)&&r++,t+r+e};Qw.decodeLongPath=function(e,t){return kw(e,0,e.length,t)};Qw.encodePax=function(e){var t="";e.name&&(t+=LY(" path="+e.name+` `)),e.linkname&&(t+=LY(" linkpath="+e.linkname+` `));var r=e.pax;if(r)for(var s in r)t+=LY(" "+s+"="+r[s]+` `);return Buffer.from(t)};Qw.decodePax=function(e){for(var t={};e.length;){for(var r=0;r100;){var a=r.indexOf("/");if(a===-1)return null;s+=s?"/"+r.slice(0,a):r.slice(0,a),r=r.slice(a+1)}return Buffer.byteLength(r)>100||Buffer.byteLength(s)>155||e.linkname&&Buffer.byteLength(e.linkname)>100?null:(t.write(r),t.write(Ad(e.mode&Hgt,6),100),t.write(Ad(e.uid,6),108),t.write(Ad(e.gid,6),116),t.write(Ad(e.size,11),124),t.write(Ad(e.mtime.getTime()/1e3|0,11),136),t[156]=J2e+qgt(e.type),e.linkname&&t.write(e.linkname,157),K2e.copy(t,ab),Mgt.copy(t,MY),e.uname&&t.write(e.uname,265),e.gname&&t.write(e.gname,297),t.write(Ad(e.devmajor||0,6),329),t.write(Ad(e.devminor||0,6),337),s&&t.write(s,345),t.write(Ad(X2e(t),6),148),t)};Qw.decode=function(e,t,r){var s=e[156]===0?0:e[156]-J2e,a=kw(e,0,100,t),n=pd(e,100,8),c=pd(e,108,8),f=pd(e,116,8),p=pd(e,124,12),h=pd(e,136,12),E=Ggt(s),C=e[157]===0?null:kw(e,157,100,t),S=kw(e,265,32),x=kw(e,297,32),I=pd(e,329,8),T=pd(e,337,8),O=X2e(e);if(O===8*32)return null;if(O!==pd(e,148,8))throw new Error("Invalid tar header. Maybe the tar is corrupted or it needs to be gunzipped?");if(K2e.compare(e,ab,ab+6)===0)e[345]&&(a=kw(e,345,155,t)+"/"+a);else if(!(Ugt.compare(e,ab,ab+6)===0&&_gt.compare(e,MY,MY+2)===0)){if(!r)throw new Error("Invalid tar header: unknown format.")}return s===0&&a&&a[a.length-1]==="/"&&(s=5),{name:a,mode:n,uid:c,gid:f,size:p,mtime:new Date(1e3*h),type:E,linkname:C,uname:S,gname:x,devmajor:I,devminor:T}}});var iBe=G((p$t,nBe)=>{var $2e=Ie("util"),Ygt=V2e(),lb=UY(),eBe=xw().Writable,tBe=xw().PassThrough,rBe=function(){},Z2e=function(e){return e&=511,e&&512-e},Vgt=function(e,t){var r=new pN(e,t);return r.end(),r},Jgt=function(e,t){return t.path&&(e.name=t.path),t.linkpath&&(e.linkname=t.linkpath),t.size&&(e.size=parseInt(t.size,10)),e.pax=t,e},pN=function(e,t){this._parent=e,this.offset=t,tBe.call(this,{autoDestroy:!1})};$2e.inherits(pN,tBe);pN.prototype.destroy=function(e){this._parent.destroy(e)};var lh=function(e){if(!(this instanceof lh))return new lh(e);eBe.call(this,e),e=e||{},this._offset=0,this._buffer=Ygt(),this._missing=0,this._partial=!1,this._onparse=rBe,this._header=null,this._stream=null,this._overflow=null,this._cb=null,this._locked=!1,this._destroyed=!1,this._pax=null,this._paxGlobal=null,this._gnuLongPath=null,this._gnuLongLinkPath=null;var t=this,r=t._buffer,s=function(){t._continue()},a=function(S){if(t._locked=!1,S)return t.destroy(S);t._stream||s()},n=function(){t._stream=null;var S=Z2e(t._header.size);S?t._parse(S,c):t._parse(512,C),t._locked||s()},c=function(){t._buffer.consume(Z2e(t._header.size)),t._parse(512,C),s()},f=function(){var S=t._header.size;t._paxGlobal=lb.decodePax(r.slice(0,S)),r.consume(S),n()},p=function(){var S=t._header.size;t._pax=lb.decodePax(r.slice(0,S)),t._paxGlobal&&(t._pax=Object.assign({},t._paxGlobal,t._pax)),r.consume(S),n()},h=function(){var S=t._header.size;this._gnuLongPath=lb.decodeLongPath(r.slice(0,S),e.filenameEncoding),r.consume(S),n()},E=function(){var S=t._header.size;this._gnuLongLinkPath=lb.decodeLongPath(r.slice(0,S),e.filenameEncoding),r.consume(S),n()},C=function(){var S=t._offset,x;try{x=t._header=lb.decode(r.slice(0,512),e.filenameEncoding,e.allowUnknownFormat)}catch(I){t.emit("error",I)}if(r.consume(512),!x){t._parse(512,C),s();return}if(x.type==="gnu-long-path"){t._parse(x.size,h),s();return}if(x.type==="gnu-long-link-path"){t._parse(x.size,E),s();return}if(x.type==="pax-global-header"){t._parse(x.size,f),s();return}if(x.type==="pax-header"){t._parse(x.size,p),s();return}if(t._gnuLongPath&&(x.name=t._gnuLongPath,t._gnuLongPath=null),t._gnuLongLinkPath&&(x.linkname=t._gnuLongLinkPath,t._gnuLongLinkPath=null),t._pax&&(t._header=x=Jgt(x,t._pax),t._pax=null),t._locked=!0,!x.size||x.type==="directory"){t._parse(512,C),t.emit("entry",x,Vgt(t,S),a);return}t._stream=new pN(t,S),t.emit("entry",x,t._stream,a),t._parse(x.size,n),s()};this._onheader=C,this._parse(512,C)};$2e.inherits(lh,eBe);lh.prototype.destroy=function(e){this._destroyed||(this._destroyed=!0,e&&this.emit("error",e),this.emit("close"),this._stream&&this._stream.emit("close"))};lh.prototype._parse=function(e,t){this._destroyed||(this._offset+=e,this._missing=e,t===this._onheader&&(this._partial=!1),this._onparse=t)};lh.prototype._continue=function(){if(!this._destroyed){var e=this._cb;this._cb=rBe,this._overflow?this._write(this._overflow,void 0,e):e()}};lh.prototype._write=function(e,t,r){if(!this._destroyed){var s=this._stream,a=this._buffer,n=this._missing;if(e.length&&(this._partial=!0),e.lengthn&&(c=e.slice(n),e=e.slice(0,n)),s?s.end(e):a.append(e),this._overflow=c,this._onparse()}};lh.prototype._final=function(e){if(this._partial)return this.destroy(new Error("Unexpected end of data"));e()};nBe.exports=lh});var oBe=G((h$t,sBe)=>{sBe.exports=Ie("fs").constants||Ie("constants")});var fBe=G((d$t,uBe)=>{var Rw=oBe(),aBe=K8(),dN=ad(),Kgt=Buffer.alloc,lBe=xw().Readable,Tw=xw().Writable,zgt=Ie("string_decoder").StringDecoder,hN=UY(),Xgt=parseInt("755",8),Zgt=parseInt("644",8),cBe=Kgt(1024),HY=function(){},_Y=function(e,t){t&=511,t&&e.push(cBe.slice(0,512-t))};function $gt(e){switch(e&Rw.S_IFMT){case Rw.S_IFBLK:return"block-device";case Rw.S_IFCHR:return"character-device";case Rw.S_IFDIR:return"directory";case Rw.S_IFIFO:return"fifo";case Rw.S_IFLNK:return"symlink"}return"file"}var gN=function(e){Tw.call(this),this.written=0,this._to=e,this._destroyed=!1};dN(gN,Tw);gN.prototype._write=function(e,t,r){if(this.written+=e.length,this._to.push(e))return r();this._to._drain=r};gN.prototype.destroy=function(){this._destroyed||(this._destroyed=!0,this.emit("close"))};var mN=function(){Tw.call(this),this.linkname="",this._decoder=new zgt("utf-8"),this._destroyed=!1};dN(mN,Tw);mN.prototype._write=function(e,t,r){this.linkname+=this._decoder.write(e),r()};mN.prototype.destroy=function(){this._destroyed||(this._destroyed=!0,this.emit("close"))};var ub=function(){Tw.call(this),this._destroyed=!1};dN(ub,Tw);ub.prototype._write=function(e,t,r){r(new Error("No body allowed for this entry"))};ub.prototype.destroy=function(){this._destroyed||(this._destroyed=!0,this.emit("close"))};var EA=function(e){if(!(this instanceof EA))return new EA(e);lBe.call(this,e),this._drain=HY,this._finalized=!1,this._finalizing=!1,this._destroyed=!1,this._stream=null};dN(EA,lBe);EA.prototype.entry=function(e,t,r){if(this._stream)throw new Error("already piping an entry");if(!(this._finalized||this._destroyed)){typeof t=="function"&&(r=t,t=null),r||(r=HY);var s=this;if((!e.size||e.type==="symlink")&&(e.size=0),e.type||(e.type=$gt(e.mode)),e.mode||(e.mode=e.type==="directory"?Xgt:Zgt),e.uid||(e.uid=0),e.gid||(e.gid=0),e.mtime||(e.mtime=new Date),typeof t=="string"&&(t=Buffer.from(t)),Buffer.isBuffer(t)){e.size=t.length,this._encode(e);var a=this.push(t);return _Y(s,e.size),a?process.nextTick(r):this._drain=r,new ub}if(e.type==="symlink"&&!e.linkname){var n=new mN;return aBe(n,function(f){if(f)return s.destroy(),r(f);e.linkname=n.linkname,s._encode(e),r()}),n}if(this._encode(e),e.type!=="file"&&e.type!=="contiguous-file")return process.nextTick(r),new ub;var c=new gN(this);return this._stream=c,aBe(c,function(f){if(s._stream=null,f)return s.destroy(),r(f);if(c.written!==e.size)return s.destroy(),r(new Error("size mismatch"));_Y(s,e.size),s._finalizing&&s.finalize(),r()}),c}};EA.prototype.finalize=function(){if(this._stream){this._finalizing=!0;return}this._finalized||(this._finalized=!0,this.push(cBe),this.push(null))};EA.prototype.destroy=function(e){this._destroyed||(this._destroyed=!0,e&&this.emit("error",e),this.emit("close"),this._stream&&this._stream.destroy&&this._stream.destroy())};EA.prototype._encode=function(e){if(!e.pax){var t=hN.encode(e);if(t){this.push(t);return}}this._encodePax(e)};EA.prototype._encodePax=function(e){var t=hN.encodePax({name:e.name,linkname:e.linkname,pax:e.pax}),r={name:"PaxHeader",mode:e.mode,uid:e.uid,gid:e.gid,size:t.length,mtime:e.mtime,type:"pax-header",linkname:e.linkname&&"PaxHeader",uname:e.uname,gname:e.gname,devmajor:e.devmajor,devminor:e.devminor};this.push(hN.encode(r)),this.push(t),_Y(this,t.length),r.size=e.size,r.type=e.type,this.push(hN.encode(r))};EA.prototype._read=function(e){var t=this._drain;this._drain=HY,t()};uBe.exports=EA});var ABe=G(jY=>{jY.extract=iBe();jY.pack=fBe()});var SBe=G(Ua=>{"use strict";var Amt=Ua&&Ua.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(Ua,"__esModule",{value:!0});Ua.Minipass=Ua.isWritable=Ua.isReadable=Ua.isStream=void 0;var IBe=typeof process=="object"&&process?process:{stdout:null,stderr:null},eV=Ie("node:events"),vBe=Amt(Ie("node:stream")),pmt=Ie("node:string_decoder"),hmt=e=>!!e&&typeof e=="object"&&(e instanceof DN||e instanceof vBe.default||(0,Ua.isReadable)(e)||(0,Ua.isWritable)(e));Ua.isStream=hmt;var dmt=e=>!!e&&typeof e=="object"&&e instanceof eV.EventEmitter&&typeof e.pipe=="function"&&e.pipe!==vBe.default.Writable.prototype.pipe;Ua.isReadable=dmt;var gmt=e=>!!e&&typeof e=="object"&&e instanceof eV.EventEmitter&&typeof e.write=="function"&&typeof e.end=="function";Ua.isWritable=gmt;var ch=Symbol("EOF"),uh=Symbol("maybeEmitEnd"),hd=Symbol("emittedEnd"),IN=Symbol("emittingEnd"),fb=Symbol("emittedError"),CN=Symbol("closed"),CBe=Symbol("read"),wN=Symbol("flush"),wBe=Symbol("flushChunk"),pf=Symbol("encoding"),Nw=Symbol("decoder"),to=Symbol("flowing"),Ab=Symbol("paused"),Ow=Symbol("resume"),ro=Symbol("buffer"),Ma=Symbol("pipes"),no=Symbol("bufferLength"),JY=Symbol("bufferPush"),BN=Symbol("bufferShift"),la=Symbol("objectMode"),rs=Symbol("destroyed"),KY=Symbol("error"),zY=Symbol("emitData"),BBe=Symbol("emitEnd"),XY=Symbol("emitEnd2"),CA=Symbol("async"),ZY=Symbol("abort"),vN=Symbol("aborted"),pb=Symbol("signal"),$m=Symbol("dataListeners"),ic=Symbol("discarded"),hb=e=>Promise.resolve().then(e),mmt=e=>e(),ymt=e=>e==="end"||e==="finish"||e==="prefinish",Emt=e=>e instanceof ArrayBuffer||!!e&&typeof e=="object"&&e.constructor&&e.constructor.name==="ArrayBuffer"&&e.byteLength>=0,Imt=e=>!Buffer.isBuffer(e)&&ArrayBuffer.isView(e),SN=class{src;dest;opts;ondrain;constructor(t,r,s){this.src=t,this.dest=r,this.opts=s,this.ondrain=()=>t[Ow](),this.dest.on("drain",this.ondrain)}unpipe(){this.dest.removeListener("drain",this.ondrain)}proxyErrors(t){}end(){this.unpipe(),this.opts.end&&this.dest.end()}},$Y=class extends SN{unpipe(){this.src.removeListener("error",this.proxyErrors),super.unpipe()}constructor(t,r,s){super(t,r,s),this.proxyErrors=a=>r.emit("error",a),t.on("error",this.proxyErrors)}},Cmt=e=>!!e.objectMode,wmt=e=>!e.objectMode&&!!e.encoding&&e.encoding!=="buffer",DN=class extends eV.EventEmitter{[to]=!1;[Ab]=!1;[Ma]=[];[ro]=[];[la];[pf];[CA];[Nw];[ch]=!1;[hd]=!1;[IN]=!1;[CN]=!1;[fb]=null;[no]=0;[rs]=!1;[pb];[vN]=!1;[$m]=0;[ic]=!1;writable=!0;readable=!0;constructor(...t){let r=t[0]||{};if(super(),r.objectMode&&typeof r.encoding=="string")throw new TypeError("Encoding and objectMode may not be used together");Cmt(r)?(this[la]=!0,this[pf]=null):wmt(r)?(this[pf]=r.encoding,this[la]=!1):(this[la]=!1,this[pf]=null),this[CA]=!!r.async,this[Nw]=this[pf]?new pmt.StringDecoder(this[pf]):null,r&&r.debugExposeBuffer===!0&&Object.defineProperty(this,"buffer",{get:()=>this[ro]}),r&&r.debugExposePipes===!0&&Object.defineProperty(this,"pipes",{get:()=>this[Ma]});let{signal:s}=r;s&&(this[pb]=s,s.aborted?this[ZY]():s.addEventListener("abort",()=>this[ZY]()))}get bufferLength(){return this[no]}get encoding(){return this[pf]}set encoding(t){throw new Error("Encoding must be set at instantiation time")}setEncoding(t){throw new Error("Encoding must be set at instantiation time")}get objectMode(){return this[la]}set objectMode(t){throw new Error("objectMode must be set at instantiation time")}get async(){return this[CA]}set async(t){this[CA]=this[CA]||!!t}[ZY](){this[vN]=!0,this.emit("abort",this[pb]?.reason),this.destroy(this[pb]?.reason)}get aborted(){return this[vN]}set aborted(t){}write(t,r,s){if(this[vN])return!1;if(this[ch])throw new Error("write after end");if(this[rs])return this.emit("error",Object.assign(new Error("Cannot call write after a stream was destroyed"),{code:"ERR_STREAM_DESTROYED"})),!0;typeof r=="function"&&(s=r,r="utf8"),r||(r="utf8");let a=this[CA]?hb:mmt;if(!this[la]&&!Buffer.isBuffer(t)){if(Imt(t))t=Buffer.from(t.buffer,t.byteOffset,t.byteLength);else if(Emt(t))t=Buffer.from(t);else if(typeof t!="string")throw new Error("Non-contiguous data written to non-objectMode stream")}return this[la]?(this[to]&&this[no]!==0&&this[wN](!0),this[to]?this.emit("data",t):this[JY](t),this[no]!==0&&this.emit("readable"),s&&a(s),this[to]):t.length?(typeof t=="string"&&!(r===this[pf]&&!this[Nw]?.lastNeed)&&(t=Buffer.from(t,r)),Buffer.isBuffer(t)&&this[pf]&&(t=this[Nw].write(t)),this[to]&&this[no]!==0&&this[wN](!0),this[to]?this.emit("data",t):this[JY](t),this[no]!==0&&this.emit("readable"),s&&a(s),this[to]):(this[no]!==0&&this.emit("readable"),s&&a(s),this[to])}read(t){if(this[rs])return null;if(this[ic]=!1,this[no]===0||t===0||t&&t>this[no])return this[uh](),null;this[la]&&(t=null),this[ro].length>1&&!this[la]&&(this[ro]=[this[pf]?this[ro].join(""):Buffer.concat(this[ro],this[no])]);let r=this[CBe](t||null,this[ro][0]);return this[uh](),r}[CBe](t,r){if(this[la])this[BN]();else{let s=r;t===s.length||t===null?this[BN]():typeof s=="string"?(this[ro][0]=s.slice(t),r=s.slice(0,t),this[no]-=t):(this[ro][0]=s.subarray(t),r=s.subarray(0,t),this[no]-=t)}return this.emit("data",r),!this[ro].length&&!this[ch]&&this.emit("drain"),r}end(t,r,s){return typeof t=="function"&&(s=t,t=void 0),typeof r=="function"&&(s=r,r="utf8"),t!==void 0&&this.write(t,r),s&&this.once("end",s),this[ch]=!0,this.writable=!1,(this[to]||!this[Ab])&&this[uh](),this}[Ow](){this[rs]||(!this[$m]&&!this[Ma].length&&(this[ic]=!0),this[Ab]=!1,this[to]=!0,this.emit("resume"),this[ro].length?this[wN]():this[ch]?this[uh]():this.emit("drain"))}resume(){return this[Ow]()}pause(){this[to]=!1,this[Ab]=!0,this[ic]=!1}get destroyed(){return this[rs]}get flowing(){return this[to]}get paused(){return this[Ab]}[JY](t){this[la]?this[no]+=1:this[no]+=t.length,this[ro].push(t)}[BN](){return this[la]?this[no]-=1:this[no]-=this[ro][0].length,this[ro].shift()}[wN](t=!1){do;while(this[wBe](this[BN]())&&this[ro].length);!t&&!this[ro].length&&!this[ch]&&this.emit("drain")}[wBe](t){return this.emit("data",t),this[to]}pipe(t,r){if(this[rs])return t;this[ic]=!1;let s=this[hd];return r=r||{},t===IBe.stdout||t===IBe.stderr?r.end=!1:r.end=r.end!==!1,r.proxyErrors=!!r.proxyErrors,s?r.end&&t.end():(this[Ma].push(r.proxyErrors?new $Y(this,t,r):new SN(this,t,r)),this[CA]?hb(()=>this[Ow]()):this[Ow]()),t}unpipe(t){let r=this[Ma].find(s=>s.dest===t);r&&(this[Ma].length===1?(this[to]&&this[$m]===0&&(this[to]=!1),this[Ma]=[]):this[Ma].splice(this[Ma].indexOf(r),1),r.unpipe())}addListener(t,r){return this.on(t,r)}on(t,r){let s=super.on(t,r);if(t==="data")this[ic]=!1,this[$m]++,!this[Ma].length&&!this[to]&&this[Ow]();else if(t==="readable"&&this[no]!==0)super.emit("readable");else if(ymt(t)&&this[hd])super.emit(t),this.removeAllListeners(t);else if(t==="error"&&this[fb]){let a=r;this[CA]?hb(()=>a.call(this,this[fb])):a.call(this,this[fb])}return s}removeListener(t,r){return this.off(t,r)}off(t,r){let s=super.off(t,r);return t==="data"&&(this[$m]=this.listeners("data").length,this[$m]===0&&!this[ic]&&!this[Ma].length&&(this[to]=!1)),s}removeAllListeners(t){let r=super.removeAllListeners(t);return(t==="data"||t===void 0)&&(this[$m]=0,!this[ic]&&!this[Ma].length&&(this[to]=!1)),r}get emittedEnd(){return this[hd]}[uh](){!this[IN]&&!this[hd]&&!this[rs]&&this[ro].length===0&&this[ch]&&(this[IN]=!0,this.emit("end"),this.emit("prefinish"),this.emit("finish"),this[CN]&&this.emit("close"),this[IN]=!1)}emit(t,...r){let s=r[0];if(t!=="error"&&t!=="close"&&t!==rs&&this[rs])return!1;if(t==="data")return!this[la]&&!s?!1:this[CA]?(hb(()=>this[zY](s)),!0):this[zY](s);if(t==="end")return this[BBe]();if(t==="close"){if(this[CN]=!0,!this[hd]&&!this[rs])return!1;let n=super.emit("close");return this.removeAllListeners("close"),n}else if(t==="error"){this[fb]=s,super.emit(KY,s);let n=!this[pb]||this.listeners("error").length?super.emit("error",s):!1;return this[uh](),n}else if(t==="resume"){let n=super.emit("resume");return this[uh](),n}else if(t==="finish"||t==="prefinish"){let n=super.emit(t);return this.removeAllListeners(t),n}let a=super.emit(t,...r);return this[uh](),a}[zY](t){for(let s of this[Ma])s.dest.write(t)===!1&&this.pause();let r=this[ic]?!1:super.emit("data",t);return this[uh](),r}[BBe](){return this[hd]?!1:(this[hd]=!0,this.readable=!1,this[CA]?(hb(()=>this[XY]()),!0):this[XY]())}[XY](){if(this[Nw]){let r=this[Nw].end();if(r){for(let s of this[Ma])s.dest.write(r);this[ic]||super.emit("data",r)}}for(let r of this[Ma])r.end();let t=super.emit("end");return this.removeAllListeners("end"),t}async collect(){let t=Object.assign([],{dataLength:0});this[la]||(t.dataLength=0);let r=this.promise();return this.on("data",s=>{t.push(s),this[la]||(t.dataLength+=s.length)}),await r,t}async concat(){if(this[la])throw new Error("cannot concat in objectMode");let t=await this.collect();return this[pf]?t.join(""):Buffer.concat(t,t.dataLength)}async promise(){return new Promise((t,r)=>{this.on(rs,()=>r(new Error("stream destroyed"))),this.on("error",s=>r(s)),this.on("end",()=>t())})}[Symbol.asyncIterator](){this[ic]=!1;let t=!1,r=async()=>(this.pause(),t=!0,{value:void 0,done:!0});return{next:()=>{if(t)return r();let a=this.read();if(a!==null)return Promise.resolve({done:!1,value:a});if(this[ch])return r();let n,c,f=C=>{this.off("data",p),this.off("end",h),this.off(rs,E),r(),c(C)},p=C=>{this.off("error",f),this.off("end",h),this.off(rs,E),this.pause(),n({value:C,done:!!this[ch]})},h=()=>{this.off("error",f),this.off("data",p),this.off(rs,E),r(),n({done:!0,value:void 0})},E=()=>f(new Error("stream destroyed"));return new Promise((C,S)=>{c=S,n=C,this.once(rs,E),this.once("error",f),this.once("end",h),this.once("data",p)})},throw:r,return:r,[Symbol.asyncIterator](){return this}}}[Symbol.iterator](){this[ic]=!1;let t=!1,r=()=>(this.pause(),this.off(KY,r),this.off(rs,r),this.off("end",r),t=!0,{done:!0,value:void 0}),s=()=>{if(t)return r();let a=this.read();return a===null?r():{done:!1,value:a}};return this.once("end",r),this.once(KY,r),this.once(rs,r),{next:s,throw:r,return:r,[Symbol.iterator](){return this}}}destroy(t){if(this[rs])return t?this.emit("error",t):this.emit(rs),this;this[rs]=!0,this[ic]=!0,this[ro].length=0,this[no]=0;let r=this;return typeof r.close=="function"&&!this[CN]&&r.close(),t?this.emit("error",t):this.emit(rs),this}static get isStream(){return Ua.isStream}};Ua.Minipass=DN});var PBe=G((O$t,wA)=>{"use strict";var gb=Ie("crypto"),{Minipass:Bmt}=SBe(),rV=["sha512","sha384","sha256"],iV=["sha512"],vmt=/^[a-z0-9+/]+(?:=?=?)$/i,Smt=/^([a-z0-9]+)-([^?]+)([?\S*]*)$/,Dmt=/^([a-z0-9]+)-([A-Za-z0-9+/=]{44,88})(\?[\x21-\x7E]*)?$/,bmt=/^[\x21-\x7E]+$/,mb=e=>e?.length?`?${e.join("?")}`:"",nV=class extends Bmt{#e;#t;#s;constructor(t){super(),this.size=0,this.opts=t,this.#r(),t?.algorithms?this.algorithms=[...t.algorithms]:this.algorithms=[...iV],this.algorithm!==null&&!this.algorithms.includes(this.algorithm)&&this.algorithms.push(this.algorithm),this.hashes=this.algorithms.map(gb.createHash)}#r(){this.sri=this.opts?.integrity?sc(this.opts?.integrity,this.opts):null,this.expectedSize=this.opts?.size,this.sri?this.sri.isHash?(this.goodSri=!0,this.algorithm=this.sri.algorithm):(this.goodSri=!this.sri.isEmpty(),this.algorithm=this.sri.pickAlgorithm(this.opts)):this.algorithm=null,this.digests=this.goodSri?this.sri[this.algorithm]:null,this.optString=mb(this.opts?.options)}on(t,r){return t==="size"&&this.#t?r(this.#t):t==="integrity"&&this.#e?r(this.#e):t==="verified"&&this.#s?r(this.#s):super.on(t,r)}emit(t,r){return t==="end"&&this.#i(),super.emit(t,r)}write(t){return this.size+=t.length,this.hashes.forEach(r=>r.update(t)),super.write(t)}#i(){this.goodSri||this.#r();let t=sc(this.hashes.map((s,a)=>`${this.algorithms[a]}-${s.digest("base64")}${this.optString}`).join(" "),this.opts),r=this.goodSri&&t.match(this.sri,this.opts);if(typeof this.expectedSize=="number"&&this.size!==this.expectedSize){let s=new Error(`stream size mismatch when checking ${this.sri}. Wanted: ${this.expectedSize} Found: ${this.size}`);s.code="EBADSIZE",s.found=this.size,s.expected=this.expectedSize,s.sri=this.sri,this.emit("error",s)}else if(this.sri&&!r){let s=new Error(`${this.sri} integrity checksum failed when using ${this.algorithm}: wanted ${this.digests} but got ${t}. (${this.size} bytes)`);s.code="EINTEGRITY",s.found=t,s.expected=this.digests,s.algorithm=this.algorithm,s.sri=this.sri,this.emit("error",s)}else this.#t=this.size,this.emit("size",this.size),this.#e=t,this.emit("integrity",t),r&&(this.#s=r,this.emit("verified",r))}},fh=class{get isHash(){return!0}constructor(t,r){let s=r?.strict;this.source=t.trim(),this.digest="",this.algorithm="",this.options=[];let a=this.source.match(s?Dmt:Smt);if(!a||s&&!rV.includes(a[1]))return;this.algorithm=a[1],this.digest=a[2];let n=a[3];n&&(this.options=n.slice(1).split("?"))}hexDigest(){return this.digest&&Buffer.from(this.digest,"base64").toString("hex")}toJSON(){return this.toString()}match(t,r){let s=sc(t,r);if(!s)return!1;if(s.isIntegrity){let a=s.pickAlgorithm(r,[this.algorithm]);if(!a)return!1;let n=s[a].find(c=>c.digest===this.digest);return n||!1}return s.digest===this.digest?s:!1}toString(t){return t?.strict&&!(rV.includes(this.algorithm)&&this.digest.match(vmt)&&this.options.every(r=>r.match(bmt)))?"":`${this.algorithm}-${this.digest}${mb(this.options)}`}};function DBe(e,t,r,s){let a=e!=="",n=!1,c="",f=s.length-1;for(let h=0;hs[a].find(c=>n.digest===c.digest)))throw new Error("hashes do not match, cannot update integrity")}else this[a]=s[a]}match(t,r){let s=sc(t,r);if(!s)return!1;let a=s.pickAlgorithm(r,Object.keys(this));return!!a&&this[a]&&s[a]&&this[a].find(n=>s[a].find(c=>n.digest===c.digest))||!1}pickAlgorithm(t,r){let s=t?.pickAlgorithm||Nmt,a=Object.keys(this).filter(n=>r?.length?r.includes(n):!0);return a.length?a.reduce((n,c)=>s(n,c)||n):null}};wA.exports.parse=sc;function sc(e,t){if(!e)return null;if(typeof e=="string")return tV(e,t);if(e.algorithm&&e.digest){let r=new ey;return r[e.algorithm]=[e],tV(db(r,t),t)}else return tV(db(e,t),t)}function tV(e,t){if(t?.single)return new fh(e,t);let r=e.trim().split(/\s+/).reduce((s,a)=>{let n=new fh(a,t);if(n.algorithm&&n.digest){let c=n.algorithm;s[c]||(s[c]=[]),s[c].push(n)}return s},new ey);return r.isEmpty()?null:r}wA.exports.stringify=db;function db(e,t){return e.algorithm&&e.digest?fh.prototype.toString.call(e,t):typeof e=="string"?db(sc(e,t),t):ey.prototype.toString.call(e,t)}wA.exports.fromHex=Pmt;function Pmt(e,t,r){let s=mb(r?.options);return sc(`${t}-${Buffer.from(e,"hex").toString("base64")}${s}`,r)}wA.exports.fromData=xmt;function xmt(e,t){let r=t?.algorithms||[...iV],s=mb(t?.options);return r.reduce((a,n)=>{let c=gb.createHash(n).update(e).digest("base64"),f=new fh(`${n}-${c}${s}`,t);if(f.algorithm&&f.digest){let p=f.algorithm;a[p]||(a[p]=[]),a[p].push(f)}return a},new ey)}wA.exports.fromStream=kmt;function kmt(e,t){let r=sV(t);return new Promise((s,a)=>{e.pipe(r),e.on("error",a),r.on("error",a);let n;r.on("integrity",c=>{n=c}),r.on("end",()=>s(n)),r.resume()})}wA.exports.checkData=Qmt;function Qmt(e,t,r){if(t=sc(t,r),!t||!Object.keys(t).length){if(r?.error)throw Object.assign(new Error("No valid integrity hashes to check against"),{code:"EINTEGRITY"});return!1}let s=t.pickAlgorithm(r),a=gb.createHash(s).update(e).digest("base64"),n=sc({algorithm:s,digest:a}),c=n.match(t,r);if(r=r||{},c||!r.error)return c;if(typeof r.size=="number"&&e.length!==r.size){let f=new Error(`data size mismatch when checking ${t}. Wanted: ${r.size} Found: ${e.length}`);throw f.code="EBADSIZE",f.found=e.length,f.expected=r.size,f.sri=t,f}else{let f=new Error(`Integrity checksum failed when using ${s}: Wanted ${t}, but got ${n}. (${e.length} bytes)`);throw f.code="EINTEGRITY",f.found=n,f.expected=t,f.algorithm=s,f.sri=t,f}}wA.exports.checkStream=Rmt;function Rmt(e,t,r){if(r=r||Object.create(null),r.integrity=t,t=sc(t,r),!t||!Object.keys(t).length)return Promise.reject(Object.assign(new Error("No valid integrity hashes to check against"),{code:"EINTEGRITY"}));let s=sV(r);return new Promise((a,n)=>{e.pipe(s),e.on("error",n),s.on("error",n);let c;s.on("verified",f=>{c=f}),s.on("end",()=>a(c)),s.resume()})}wA.exports.integrityStream=sV;function sV(e=Object.create(null)){return new nV(e)}wA.exports.create=Tmt;function Tmt(e){let t=e?.algorithms||[...iV],r=mb(e?.options),s=t.map(gb.createHash);return{update:function(a,n){return s.forEach(c=>c.update(a,n)),this},digest:function(){return t.reduce((n,c)=>{let f=s.shift().digest("base64"),p=new fh(`${c}-${f}${r}`,e);if(p.algorithm&&p.digest){let h=p.algorithm;n[h]||(n[h]=[]),n[h].push(p)}return n},new ey)}}}var Fmt=gb.getHashes(),bBe=["md5","whirlpool","sha1","sha224","sha256","sha384","sha512","sha3","sha3-256","sha3-384","sha3-512","sha3_256","sha3_384","sha3_512"].filter(e=>Fmt.includes(e));function Nmt(e,t){return bBe.indexOf(e.toLowerCase())>=bBe.indexOf(t.toLowerCase())?e:t}});var oV=G(dd=>{"use strict";Object.defineProperty(dd,"__esModule",{value:!0});dd.Signature=dd.Envelope=void 0;dd.Envelope={fromJSON(e){return{payload:bN(e.payload)?Buffer.from(xBe(e.payload)):Buffer.alloc(0),payloadType:bN(e.payloadType)?globalThis.String(e.payloadType):"",signatures:globalThis.Array.isArray(e?.signatures)?e.signatures.map(t=>dd.Signature.fromJSON(t)):[]}},toJSON(e){let t={};return e.payload.length!==0&&(t.payload=kBe(e.payload)),e.payloadType!==""&&(t.payloadType=e.payloadType),e.signatures?.length&&(t.signatures=e.signatures.map(r=>dd.Signature.toJSON(r))),t}};dd.Signature={fromJSON(e){return{sig:bN(e.sig)?Buffer.from(xBe(e.sig)):Buffer.alloc(0),keyid:bN(e.keyid)?globalThis.String(e.keyid):""}},toJSON(e){let t={};return e.sig.length!==0&&(t.sig=kBe(e.sig)),e.keyid!==""&&(t.keyid=e.keyid),t}};function xBe(e){return Uint8Array.from(globalThis.Buffer.from(e,"base64"))}function kBe(e){return globalThis.Buffer.from(e).toString("base64")}function bN(e){return e!=null}});var RBe=G(PN=>{"use strict";Object.defineProperty(PN,"__esModule",{value:!0});PN.Timestamp=void 0;PN.Timestamp={fromJSON(e){return{seconds:QBe(e.seconds)?globalThis.String(e.seconds):"0",nanos:QBe(e.nanos)?globalThis.Number(e.nanos):0}},toJSON(e){let t={};return e.seconds!=="0"&&(t.seconds=e.seconds),e.nanos!==0&&(t.nanos=Math.round(e.nanos)),t}};function QBe(e){return e!=null}});var Lw=G(Ur=>{"use strict";Object.defineProperty(Ur,"__esModule",{value:!0});Ur.TimeRange=Ur.X509CertificateChain=Ur.SubjectAlternativeName=Ur.X509Certificate=Ur.DistinguishedName=Ur.ObjectIdentifierValuePair=Ur.ObjectIdentifier=Ur.PublicKeyIdentifier=Ur.PublicKey=Ur.RFC3161SignedTimestamp=Ur.LogId=Ur.MessageSignature=Ur.HashOutput=Ur.SubjectAlternativeNameType=Ur.PublicKeyDetails=Ur.HashAlgorithm=void 0;Ur.hashAlgorithmFromJSON=FBe;Ur.hashAlgorithmToJSON=NBe;Ur.publicKeyDetailsFromJSON=OBe;Ur.publicKeyDetailsToJSON=LBe;Ur.subjectAlternativeNameTypeFromJSON=MBe;Ur.subjectAlternativeNameTypeToJSON=UBe;var Omt=RBe(),Sl;(function(e){e[e.HASH_ALGORITHM_UNSPECIFIED=0]="HASH_ALGORITHM_UNSPECIFIED",e[e.SHA2_256=1]="SHA2_256",e[e.SHA2_384=2]="SHA2_384",e[e.SHA2_512=3]="SHA2_512",e[e.SHA3_256=4]="SHA3_256",e[e.SHA3_384=5]="SHA3_384"})(Sl||(Ur.HashAlgorithm=Sl={}));function FBe(e){switch(e){case 0:case"HASH_ALGORITHM_UNSPECIFIED":return Sl.HASH_ALGORITHM_UNSPECIFIED;case 1:case"SHA2_256":return Sl.SHA2_256;case 2:case"SHA2_384":return Sl.SHA2_384;case 3:case"SHA2_512":return Sl.SHA2_512;case 4:case"SHA3_256":return Sl.SHA3_256;case 5:case"SHA3_384":return Sl.SHA3_384;default:throw new globalThis.Error("Unrecognized enum value "+e+" for enum HashAlgorithm")}}function NBe(e){switch(e){case Sl.HASH_ALGORITHM_UNSPECIFIED:return"HASH_ALGORITHM_UNSPECIFIED";case Sl.SHA2_256:return"SHA2_256";case Sl.SHA2_384:return"SHA2_384";case Sl.SHA2_512:return"SHA2_512";case Sl.SHA3_256:return"SHA3_256";case Sl.SHA3_384:return"SHA3_384";default:throw new globalThis.Error("Unrecognized enum value "+e+" for enum HashAlgorithm")}}var sn;(function(e){e[e.PUBLIC_KEY_DETAILS_UNSPECIFIED=0]="PUBLIC_KEY_DETAILS_UNSPECIFIED",e[e.PKCS1_RSA_PKCS1V5=1]="PKCS1_RSA_PKCS1V5",e[e.PKCS1_RSA_PSS=2]="PKCS1_RSA_PSS",e[e.PKIX_RSA_PKCS1V5=3]="PKIX_RSA_PKCS1V5",e[e.PKIX_RSA_PSS=4]="PKIX_RSA_PSS",e[e.PKIX_RSA_PKCS1V15_2048_SHA256=9]="PKIX_RSA_PKCS1V15_2048_SHA256",e[e.PKIX_RSA_PKCS1V15_3072_SHA256=10]="PKIX_RSA_PKCS1V15_3072_SHA256",e[e.PKIX_RSA_PKCS1V15_4096_SHA256=11]="PKIX_RSA_PKCS1V15_4096_SHA256",e[e.PKIX_RSA_PSS_2048_SHA256=16]="PKIX_RSA_PSS_2048_SHA256",e[e.PKIX_RSA_PSS_3072_SHA256=17]="PKIX_RSA_PSS_3072_SHA256",e[e.PKIX_RSA_PSS_4096_SHA256=18]="PKIX_RSA_PSS_4096_SHA256",e[e.PKIX_ECDSA_P256_HMAC_SHA_256=6]="PKIX_ECDSA_P256_HMAC_SHA_256",e[e.PKIX_ECDSA_P256_SHA_256=5]="PKIX_ECDSA_P256_SHA_256",e[e.PKIX_ECDSA_P384_SHA_384=12]="PKIX_ECDSA_P384_SHA_384",e[e.PKIX_ECDSA_P521_SHA_512=13]="PKIX_ECDSA_P521_SHA_512",e[e.PKIX_ED25519=7]="PKIX_ED25519",e[e.PKIX_ED25519_PH=8]="PKIX_ED25519_PH",e[e.LMS_SHA256=14]="LMS_SHA256",e[e.LMOTS_SHA256=15]="LMOTS_SHA256"})(sn||(Ur.PublicKeyDetails=sn={}));function OBe(e){switch(e){case 0:case"PUBLIC_KEY_DETAILS_UNSPECIFIED":return sn.PUBLIC_KEY_DETAILS_UNSPECIFIED;case 1:case"PKCS1_RSA_PKCS1V5":return sn.PKCS1_RSA_PKCS1V5;case 2:case"PKCS1_RSA_PSS":return sn.PKCS1_RSA_PSS;case 3:case"PKIX_RSA_PKCS1V5":return sn.PKIX_RSA_PKCS1V5;case 4:case"PKIX_RSA_PSS":return sn.PKIX_RSA_PSS;case 9:case"PKIX_RSA_PKCS1V15_2048_SHA256":return sn.PKIX_RSA_PKCS1V15_2048_SHA256;case 10:case"PKIX_RSA_PKCS1V15_3072_SHA256":return sn.PKIX_RSA_PKCS1V15_3072_SHA256;case 11:case"PKIX_RSA_PKCS1V15_4096_SHA256":return sn.PKIX_RSA_PKCS1V15_4096_SHA256;case 16:case"PKIX_RSA_PSS_2048_SHA256":return sn.PKIX_RSA_PSS_2048_SHA256;case 17:case"PKIX_RSA_PSS_3072_SHA256":return sn.PKIX_RSA_PSS_3072_SHA256;case 18:case"PKIX_RSA_PSS_4096_SHA256":return sn.PKIX_RSA_PSS_4096_SHA256;case 6:case"PKIX_ECDSA_P256_HMAC_SHA_256":return sn.PKIX_ECDSA_P256_HMAC_SHA_256;case 5:case"PKIX_ECDSA_P256_SHA_256":return sn.PKIX_ECDSA_P256_SHA_256;case 12:case"PKIX_ECDSA_P384_SHA_384":return sn.PKIX_ECDSA_P384_SHA_384;case 13:case"PKIX_ECDSA_P521_SHA_512":return sn.PKIX_ECDSA_P521_SHA_512;case 7:case"PKIX_ED25519":return sn.PKIX_ED25519;case 8:case"PKIX_ED25519_PH":return sn.PKIX_ED25519_PH;case 14:case"LMS_SHA256":return sn.LMS_SHA256;case 15:case"LMOTS_SHA256":return sn.LMOTS_SHA256;default:throw new globalThis.Error("Unrecognized enum value "+e+" for enum PublicKeyDetails")}}function LBe(e){switch(e){case sn.PUBLIC_KEY_DETAILS_UNSPECIFIED:return"PUBLIC_KEY_DETAILS_UNSPECIFIED";case sn.PKCS1_RSA_PKCS1V5:return"PKCS1_RSA_PKCS1V5";case sn.PKCS1_RSA_PSS:return"PKCS1_RSA_PSS";case sn.PKIX_RSA_PKCS1V5:return"PKIX_RSA_PKCS1V5";case sn.PKIX_RSA_PSS:return"PKIX_RSA_PSS";case sn.PKIX_RSA_PKCS1V15_2048_SHA256:return"PKIX_RSA_PKCS1V15_2048_SHA256";case sn.PKIX_RSA_PKCS1V15_3072_SHA256:return"PKIX_RSA_PKCS1V15_3072_SHA256";case sn.PKIX_RSA_PKCS1V15_4096_SHA256:return"PKIX_RSA_PKCS1V15_4096_SHA256";case sn.PKIX_RSA_PSS_2048_SHA256:return"PKIX_RSA_PSS_2048_SHA256";case sn.PKIX_RSA_PSS_3072_SHA256:return"PKIX_RSA_PSS_3072_SHA256";case sn.PKIX_RSA_PSS_4096_SHA256:return"PKIX_RSA_PSS_4096_SHA256";case sn.PKIX_ECDSA_P256_HMAC_SHA_256:return"PKIX_ECDSA_P256_HMAC_SHA_256";case sn.PKIX_ECDSA_P256_SHA_256:return"PKIX_ECDSA_P256_SHA_256";case sn.PKIX_ECDSA_P384_SHA_384:return"PKIX_ECDSA_P384_SHA_384";case sn.PKIX_ECDSA_P521_SHA_512:return"PKIX_ECDSA_P521_SHA_512";case sn.PKIX_ED25519:return"PKIX_ED25519";case sn.PKIX_ED25519_PH:return"PKIX_ED25519_PH";case sn.LMS_SHA256:return"LMS_SHA256";case sn.LMOTS_SHA256:return"LMOTS_SHA256";default:throw new globalThis.Error("Unrecognized enum value "+e+" for enum PublicKeyDetails")}}var BA;(function(e){e[e.SUBJECT_ALTERNATIVE_NAME_TYPE_UNSPECIFIED=0]="SUBJECT_ALTERNATIVE_NAME_TYPE_UNSPECIFIED",e[e.EMAIL=1]="EMAIL",e[e.URI=2]="URI",e[e.OTHER_NAME=3]="OTHER_NAME"})(BA||(Ur.SubjectAlternativeNameType=BA={}));function MBe(e){switch(e){case 0:case"SUBJECT_ALTERNATIVE_NAME_TYPE_UNSPECIFIED":return BA.SUBJECT_ALTERNATIVE_NAME_TYPE_UNSPECIFIED;case 1:case"EMAIL":return BA.EMAIL;case 2:case"URI":return BA.URI;case 3:case"OTHER_NAME":return BA.OTHER_NAME;default:throw new globalThis.Error("Unrecognized enum value "+e+" for enum SubjectAlternativeNameType")}}function UBe(e){switch(e){case BA.SUBJECT_ALTERNATIVE_NAME_TYPE_UNSPECIFIED:return"SUBJECT_ALTERNATIVE_NAME_TYPE_UNSPECIFIED";case BA.EMAIL:return"EMAIL";case BA.URI:return"URI";case BA.OTHER_NAME:return"OTHER_NAME";default:throw new globalThis.Error("Unrecognized enum value "+e+" for enum SubjectAlternativeNameType")}}Ur.HashOutput={fromJSON(e){return{algorithm:Es(e.algorithm)?FBe(e.algorithm):0,digest:Es(e.digest)?Buffer.from(ty(e.digest)):Buffer.alloc(0)}},toJSON(e){let t={};return e.algorithm!==0&&(t.algorithm=NBe(e.algorithm)),e.digest.length!==0&&(t.digest=ry(e.digest)),t}};Ur.MessageSignature={fromJSON(e){return{messageDigest:Es(e.messageDigest)?Ur.HashOutput.fromJSON(e.messageDigest):void 0,signature:Es(e.signature)?Buffer.from(ty(e.signature)):Buffer.alloc(0)}},toJSON(e){let t={};return e.messageDigest!==void 0&&(t.messageDigest=Ur.HashOutput.toJSON(e.messageDigest)),e.signature.length!==0&&(t.signature=ry(e.signature)),t}};Ur.LogId={fromJSON(e){return{keyId:Es(e.keyId)?Buffer.from(ty(e.keyId)):Buffer.alloc(0)}},toJSON(e){let t={};return e.keyId.length!==0&&(t.keyId=ry(e.keyId)),t}};Ur.RFC3161SignedTimestamp={fromJSON(e){return{signedTimestamp:Es(e.signedTimestamp)?Buffer.from(ty(e.signedTimestamp)):Buffer.alloc(0)}},toJSON(e){let t={};return e.signedTimestamp.length!==0&&(t.signedTimestamp=ry(e.signedTimestamp)),t}};Ur.PublicKey={fromJSON(e){return{rawBytes:Es(e.rawBytes)?Buffer.from(ty(e.rawBytes)):void 0,keyDetails:Es(e.keyDetails)?OBe(e.keyDetails):0,validFor:Es(e.validFor)?Ur.TimeRange.fromJSON(e.validFor):void 0}},toJSON(e){let t={};return e.rawBytes!==void 0&&(t.rawBytes=ry(e.rawBytes)),e.keyDetails!==0&&(t.keyDetails=LBe(e.keyDetails)),e.validFor!==void 0&&(t.validFor=Ur.TimeRange.toJSON(e.validFor)),t}};Ur.PublicKeyIdentifier={fromJSON(e){return{hint:Es(e.hint)?globalThis.String(e.hint):""}},toJSON(e){let t={};return e.hint!==""&&(t.hint=e.hint),t}};Ur.ObjectIdentifier={fromJSON(e){return{id:globalThis.Array.isArray(e?.id)?e.id.map(t=>globalThis.Number(t)):[]}},toJSON(e){let t={};return e.id?.length&&(t.id=e.id.map(r=>Math.round(r))),t}};Ur.ObjectIdentifierValuePair={fromJSON(e){return{oid:Es(e.oid)?Ur.ObjectIdentifier.fromJSON(e.oid):void 0,value:Es(e.value)?Buffer.from(ty(e.value)):Buffer.alloc(0)}},toJSON(e){let t={};return e.oid!==void 0&&(t.oid=Ur.ObjectIdentifier.toJSON(e.oid)),e.value.length!==0&&(t.value=ry(e.value)),t}};Ur.DistinguishedName={fromJSON(e){return{organization:Es(e.organization)?globalThis.String(e.organization):"",commonName:Es(e.commonName)?globalThis.String(e.commonName):""}},toJSON(e){let t={};return e.organization!==""&&(t.organization=e.organization),e.commonName!==""&&(t.commonName=e.commonName),t}};Ur.X509Certificate={fromJSON(e){return{rawBytes:Es(e.rawBytes)?Buffer.from(ty(e.rawBytes)):Buffer.alloc(0)}},toJSON(e){let t={};return e.rawBytes.length!==0&&(t.rawBytes=ry(e.rawBytes)),t}};Ur.SubjectAlternativeName={fromJSON(e){return{type:Es(e.type)?MBe(e.type):0,identity:Es(e.regexp)?{$case:"regexp",regexp:globalThis.String(e.regexp)}:Es(e.value)?{$case:"value",value:globalThis.String(e.value)}:void 0}},toJSON(e){let t={};return e.type!==0&&(t.type=UBe(e.type)),e.identity?.$case==="regexp"?t.regexp=e.identity.regexp:e.identity?.$case==="value"&&(t.value=e.identity.value),t}};Ur.X509CertificateChain={fromJSON(e){return{certificates:globalThis.Array.isArray(e?.certificates)?e.certificates.map(t=>Ur.X509Certificate.fromJSON(t)):[]}},toJSON(e){let t={};return e.certificates?.length&&(t.certificates=e.certificates.map(r=>Ur.X509Certificate.toJSON(r))),t}};Ur.TimeRange={fromJSON(e){return{start:Es(e.start)?TBe(e.start):void 0,end:Es(e.end)?TBe(e.end):void 0}},toJSON(e){let t={};return e.start!==void 0&&(t.start=e.start.toISOString()),e.end!==void 0&&(t.end=e.end.toISOString()),t}};function ty(e){return Uint8Array.from(globalThis.Buffer.from(e,"base64"))}function ry(e){return globalThis.Buffer.from(e).toString("base64")}function Lmt(e){let t=(globalThis.Number(e.seconds)||0)*1e3;return t+=(e.nanos||0)/1e6,new globalThis.Date(t)}function TBe(e){return e instanceof globalThis.Date?e:typeof e=="string"?new globalThis.Date(e):Lmt(Omt.Timestamp.fromJSON(e))}function Es(e){return e!=null}});var aV=G(Is=>{"use strict";Object.defineProperty(Is,"__esModule",{value:!0});Is.TransparencyLogEntry=Is.InclusionPromise=Is.InclusionProof=Is.Checkpoint=Is.KindVersion=void 0;var _Be=Lw();Is.KindVersion={fromJSON(e){return{kind:Ha(e.kind)?globalThis.String(e.kind):"",version:Ha(e.version)?globalThis.String(e.version):""}},toJSON(e){let t={};return e.kind!==""&&(t.kind=e.kind),e.version!==""&&(t.version=e.version),t}};Is.Checkpoint={fromJSON(e){return{envelope:Ha(e.envelope)?globalThis.String(e.envelope):""}},toJSON(e){let t={};return e.envelope!==""&&(t.envelope=e.envelope),t}};Is.InclusionProof={fromJSON(e){return{logIndex:Ha(e.logIndex)?globalThis.String(e.logIndex):"0",rootHash:Ha(e.rootHash)?Buffer.from(xN(e.rootHash)):Buffer.alloc(0),treeSize:Ha(e.treeSize)?globalThis.String(e.treeSize):"0",hashes:globalThis.Array.isArray(e?.hashes)?e.hashes.map(t=>Buffer.from(xN(t))):[],checkpoint:Ha(e.checkpoint)?Is.Checkpoint.fromJSON(e.checkpoint):void 0}},toJSON(e){let t={};return e.logIndex!=="0"&&(t.logIndex=e.logIndex),e.rootHash.length!==0&&(t.rootHash=kN(e.rootHash)),e.treeSize!=="0"&&(t.treeSize=e.treeSize),e.hashes?.length&&(t.hashes=e.hashes.map(r=>kN(r))),e.checkpoint!==void 0&&(t.checkpoint=Is.Checkpoint.toJSON(e.checkpoint)),t}};Is.InclusionPromise={fromJSON(e){return{signedEntryTimestamp:Ha(e.signedEntryTimestamp)?Buffer.from(xN(e.signedEntryTimestamp)):Buffer.alloc(0)}},toJSON(e){let t={};return e.signedEntryTimestamp.length!==0&&(t.signedEntryTimestamp=kN(e.signedEntryTimestamp)),t}};Is.TransparencyLogEntry={fromJSON(e){return{logIndex:Ha(e.logIndex)?globalThis.String(e.logIndex):"0",logId:Ha(e.logId)?_Be.LogId.fromJSON(e.logId):void 0,kindVersion:Ha(e.kindVersion)?Is.KindVersion.fromJSON(e.kindVersion):void 0,integratedTime:Ha(e.integratedTime)?globalThis.String(e.integratedTime):"0",inclusionPromise:Ha(e.inclusionPromise)?Is.InclusionPromise.fromJSON(e.inclusionPromise):void 0,inclusionProof:Ha(e.inclusionProof)?Is.InclusionProof.fromJSON(e.inclusionProof):void 0,canonicalizedBody:Ha(e.canonicalizedBody)?Buffer.from(xN(e.canonicalizedBody)):Buffer.alloc(0)}},toJSON(e){let t={};return e.logIndex!=="0"&&(t.logIndex=e.logIndex),e.logId!==void 0&&(t.logId=_Be.LogId.toJSON(e.logId)),e.kindVersion!==void 0&&(t.kindVersion=Is.KindVersion.toJSON(e.kindVersion)),e.integratedTime!=="0"&&(t.integratedTime=e.integratedTime),e.inclusionPromise!==void 0&&(t.inclusionPromise=Is.InclusionPromise.toJSON(e.inclusionPromise)),e.inclusionProof!==void 0&&(t.inclusionProof=Is.InclusionProof.toJSON(e.inclusionProof)),e.canonicalizedBody.length!==0&&(t.canonicalizedBody=kN(e.canonicalizedBody)),t}};function xN(e){return Uint8Array.from(globalThis.Buffer.from(e,"base64"))}function kN(e){return globalThis.Buffer.from(e).toString("base64")}function Ha(e){return e!=null}});var lV=G(zc=>{"use strict";Object.defineProperty(zc,"__esModule",{value:!0});zc.Bundle=zc.VerificationMaterial=zc.TimestampVerificationData=void 0;var HBe=oV(),vA=Lw(),jBe=aV();zc.TimestampVerificationData={fromJSON(e){return{rfc3161Timestamps:globalThis.Array.isArray(e?.rfc3161Timestamps)?e.rfc3161Timestamps.map(t=>vA.RFC3161SignedTimestamp.fromJSON(t)):[]}},toJSON(e){let t={};return e.rfc3161Timestamps?.length&&(t.rfc3161Timestamps=e.rfc3161Timestamps.map(r=>vA.RFC3161SignedTimestamp.toJSON(r))),t}};zc.VerificationMaterial={fromJSON(e){return{content:gd(e.publicKey)?{$case:"publicKey",publicKey:vA.PublicKeyIdentifier.fromJSON(e.publicKey)}:gd(e.x509CertificateChain)?{$case:"x509CertificateChain",x509CertificateChain:vA.X509CertificateChain.fromJSON(e.x509CertificateChain)}:gd(e.certificate)?{$case:"certificate",certificate:vA.X509Certificate.fromJSON(e.certificate)}:void 0,tlogEntries:globalThis.Array.isArray(e?.tlogEntries)?e.tlogEntries.map(t=>jBe.TransparencyLogEntry.fromJSON(t)):[],timestampVerificationData:gd(e.timestampVerificationData)?zc.TimestampVerificationData.fromJSON(e.timestampVerificationData):void 0}},toJSON(e){let t={};return e.content?.$case==="publicKey"?t.publicKey=vA.PublicKeyIdentifier.toJSON(e.content.publicKey):e.content?.$case==="x509CertificateChain"?t.x509CertificateChain=vA.X509CertificateChain.toJSON(e.content.x509CertificateChain):e.content?.$case==="certificate"&&(t.certificate=vA.X509Certificate.toJSON(e.content.certificate)),e.tlogEntries?.length&&(t.tlogEntries=e.tlogEntries.map(r=>jBe.TransparencyLogEntry.toJSON(r))),e.timestampVerificationData!==void 0&&(t.timestampVerificationData=zc.TimestampVerificationData.toJSON(e.timestampVerificationData)),t}};zc.Bundle={fromJSON(e){return{mediaType:gd(e.mediaType)?globalThis.String(e.mediaType):"",verificationMaterial:gd(e.verificationMaterial)?zc.VerificationMaterial.fromJSON(e.verificationMaterial):void 0,content:gd(e.messageSignature)?{$case:"messageSignature",messageSignature:vA.MessageSignature.fromJSON(e.messageSignature)}:gd(e.dsseEnvelope)?{$case:"dsseEnvelope",dsseEnvelope:HBe.Envelope.fromJSON(e.dsseEnvelope)}:void 0}},toJSON(e){let t={};return e.mediaType!==""&&(t.mediaType=e.mediaType),e.verificationMaterial!==void 0&&(t.verificationMaterial=zc.VerificationMaterial.toJSON(e.verificationMaterial)),e.content?.$case==="messageSignature"?t.messageSignature=vA.MessageSignature.toJSON(e.content.messageSignature):e.content?.$case==="dsseEnvelope"&&(t.dsseEnvelope=HBe.Envelope.toJSON(e.content.dsseEnvelope)),t}};function gd(e){return e!=null}});var cV=G(Ti=>{"use strict";Object.defineProperty(Ti,"__esModule",{value:!0});Ti.ClientTrustConfig=Ti.SigningConfig=Ti.TrustedRoot=Ti.CertificateAuthority=Ti.TransparencyLogInstance=void 0;var Dl=Lw();Ti.TransparencyLogInstance={fromJSON(e){return{baseUrl:ca(e.baseUrl)?globalThis.String(e.baseUrl):"",hashAlgorithm:ca(e.hashAlgorithm)?(0,Dl.hashAlgorithmFromJSON)(e.hashAlgorithm):0,publicKey:ca(e.publicKey)?Dl.PublicKey.fromJSON(e.publicKey):void 0,logId:ca(e.logId)?Dl.LogId.fromJSON(e.logId):void 0,checkpointKeyId:ca(e.checkpointKeyId)?Dl.LogId.fromJSON(e.checkpointKeyId):void 0}},toJSON(e){let t={};return e.baseUrl!==""&&(t.baseUrl=e.baseUrl),e.hashAlgorithm!==0&&(t.hashAlgorithm=(0,Dl.hashAlgorithmToJSON)(e.hashAlgorithm)),e.publicKey!==void 0&&(t.publicKey=Dl.PublicKey.toJSON(e.publicKey)),e.logId!==void 0&&(t.logId=Dl.LogId.toJSON(e.logId)),e.checkpointKeyId!==void 0&&(t.checkpointKeyId=Dl.LogId.toJSON(e.checkpointKeyId)),t}};Ti.CertificateAuthority={fromJSON(e){return{subject:ca(e.subject)?Dl.DistinguishedName.fromJSON(e.subject):void 0,uri:ca(e.uri)?globalThis.String(e.uri):"",certChain:ca(e.certChain)?Dl.X509CertificateChain.fromJSON(e.certChain):void 0,validFor:ca(e.validFor)?Dl.TimeRange.fromJSON(e.validFor):void 0}},toJSON(e){let t={};return e.subject!==void 0&&(t.subject=Dl.DistinguishedName.toJSON(e.subject)),e.uri!==""&&(t.uri=e.uri),e.certChain!==void 0&&(t.certChain=Dl.X509CertificateChain.toJSON(e.certChain)),e.validFor!==void 0&&(t.validFor=Dl.TimeRange.toJSON(e.validFor)),t}};Ti.TrustedRoot={fromJSON(e){return{mediaType:ca(e.mediaType)?globalThis.String(e.mediaType):"",tlogs:globalThis.Array.isArray(e?.tlogs)?e.tlogs.map(t=>Ti.TransparencyLogInstance.fromJSON(t)):[],certificateAuthorities:globalThis.Array.isArray(e?.certificateAuthorities)?e.certificateAuthorities.map(t=>Ti.CertificateAuthority.fromJSON(t)):[],ctlogs:globalThis.Array.isArray(e?.ctlogs)?e.ctlogs.map(t=>Ti.TransparencyLogInstance.fromJSON(t)):[],timestampAuthorities:globalThis.Array.isArray(e?.timestampAuthorities)?e.timestampAuthorities.map(t=>Ti.CertificateAuthority.fromJSON(t)):[]}},toJSON(e){let t={};return e.mediaType!==""&&(t.mediaType=e.mediaType),e.tlogs?.length&&(t.tlogs=e.tlogs.map(r=>Ti.TransparencyLogInstance.toJSON(r))),e.certificateAuthorities?.length&&(t.certificateAuthorities=e.certificateAuthorities.map(r=>Ti.CertificateAuthority.toJSON(r))),e.ctlogs?.length&&(t.ctlogs=e.ctlogs.map(r=>Ti.TransparencyLogInstance.toJSON(r))),e.timestampAuthorities?.length&&(t.timestampAuthorities=e.timestampAuthorities.map(r=>Ti.CertificateAuthority.toJSON(r))),t}};Ti.SigningConfig={fromJSON(e){return{mediaType:ca(e.mediaType)?globalThis.String(e.mediaType):"",caUrl:ca(e.caUrl)?globalThis.String(e.caUrl):"",oidcUrl:ca(e.oidcUrl)?globalThis.String(e.oidcUrl):"",tlogUrls:globalThis.Array.isArray(e?.tlogUrls)?e.tlogUrls.map(t=>globalThis.String(t)):[],tsaUrls:globalThis.Array.isArray(e?.tsaUrls)?e.tsaUrls.map(t=>globalThis.String(t)):[]}},toJSON(e){let t={};return e.mediaType!==""&&(t.mediaType=e.mediaType),e.caUrl!==""&&(t.caUrl=e.caUrl),e.oidcUrl!==""&&(t.oidcUrl=e.oidcUrl),e.tlogUrls?.length&&(t.tlogUrls=e.tlogUrls),e.tsaUrls?.length&&(t.tsaUrls=e.tsaUrls),t}};Ti.ClientTrustConfig={fromJSON(e){return{mediaType:ca(e.mediaType)?globalThis.String(e.mediaType):"",trustedRoot:ca(e.trustedRoot)?Ti.TrustedRoot.fromJSON(e.trustedRoot):void 0,signingConfig:ca(e.signingConfig)?Ti.SigningConfig.fromJSON(e.signingConfig):void 0}},toJSON(e){let t={};return e.mediaType!==""&&(t.mediaType=e.mediaType),e.trustedRoot!==void 0&&(t.trustedRoot=Ti.TrustedRoot.toJSON(e.trustedRoot)),e.signingConfig!==void 0&&(t.signingConfig=Ti.SigningConfig.toJSON(e.signingConfig)),t}};function ca(e){return e!=null}});var WBe=G(Vr=>{"use strict";Object.defineProperty(Vr,"__esModule",{value:!0});Vr.Input=Vr.Artifact=Vr.ArtifactVerificationOptions_ObserverTimestampOptions=Vr.ArtifactVerificationOptions_TlogIntegratedTimestampOptions=Vr.ArtifactVerificationOptions_TimestampAuthorityOptions=Vr.ArtifactVerificationOptions_CtlogOptions=Vr.ArtifactVerificationOptions_TlogOptions=Vr.ArtifactVerificationOptions=Vr.PublicKeyIdentities=Vr.CertificateIdentities=Vr.CertificateIdentity=void 0;var GBe=lV(),md=Lw(),qBe=cV();Vr.CertificateIdentity={fromJSON(e){return{issuer:gi(e.issuer)?globalThis.String(e.issuer):"",san:gi(e.san)?md.SubjectAlternativeName.fromJSON(e.san):void 0,oids:globalThis.Array.isArray(e?.oids)?e.oids.map(t=>md.ObjectIdentifierValuePair.fromJSON(t)):[]}},toJSON(e){let t={};return e.issuer!==""&&(t.issuer=e.issuer),e.san!==void 0&&(t.san=md.SubjectAlternativeName.toJSON(e.san)),e.oids?.length&&(t.oids=e.oids.map(r=>md.ObjectIdentifierValuePair.toJSON(r))),t}};Vr.CertificateIdentities={fromJSON(e){return{identities:globalThis.Array.isArray(e?.identities)?e.identities.map(t=>Vr.CertificateIdentity.fromJSON(t)):[]}},toJSON(e){let t={};return e.identities?.length&&(t.identities=e.identities.map(r=>Vr.CertificateIdentity.toJSON(r))),t}};Vr.PublicKeyIdentities={fromJSON(e){return{publicKeys:globalThis.Array.isArray(e?.publicKeys)?e.publicKeys.map(t=>md.PublicKey.fromJSON(t)):[]}},toJSON(e){let t={};return e.publicKeys?.length&&(t.publicKeys=e.publicKeys.map(r=>md.PublicKey.toJSON(r))),t}};Vr.ArtifactVerificationOptions={fromJSON(e){return{signers:gi(e.certificateIdentities)?{$case:"certificateIdentities",certificateIdentities:Vr.CertificateIdentities.fromJSON(e.certificateIdentities)}:gi(e.publicKeys)?{$case:"publicKeys",publicKeys:Vr.PublicKeyIdentities.fromJSON(e.publicKeys)}:void 0,tlogOptions:gi(e.tlogOptions)?Vr.ArtifactVerificationOptions_TlogOptions.fromJSON(e.tlogOptions):void 0,ctlogOptions:gi(e.ctlogOptions)?Vr.ArtifactVerificationOptions_CtlogOptions.fromJSON(e.ctlogOptions):void 0,tsaOptions:gi(e.tsaOptions)?Vr.ArtifactVerificationOptions_TimestampAuthorityOptions.fromJSON(e.tsaOptions):void 0,integratedTsOptions:gi(e.integratedTsOptions)?Vr.ArtifactVerificationOptions_TlogIntegratedTimestampOptions.fromJSON(e.integratedTsOptions):void 0,observerOptions:gi(e.observerOptions)?Vr.ArtifactVerificationOptions_ObserverTimestampOptions.fromJSON(e.observerOptions):void 0}},toJSON(e){let t={};return e.signers?.$case==="certificateIdentities"?t.certificateIdentities=Vr.CertificateIdentities.toJSON(e.signers.certificateIdentities):e.signers?.$case==="publicKeys"&&(t.publicKeys=Vr.PublicKeyIdentities.toJSON(e.signers.publicKeys)),e.tlogOptions!==void 0&&(t.tlogOptions=Vr.ArtifactVerificationOptions_TlogOptions.toJSON(e.tlogOptions)),e.ctlogOptions!==void 0&&(t.ctlogOptions=Vr.ArtifactVerificationOptions_CtlogOptions.toJSON(e.ctlogOptions)),e.tsaOptions!==void 0&&(t.tsaOptions=Vr.ArtifactVerificationOptions_TimestampAuthorityOptions.toJSON(e.tsaOptions)),e.integratedTsOptions!==void 0&&(t.integratedTsOptions=Vr.ArtifactVerificationOptions_TlogIntegratedTimestampOptions.toJSON(e.integratedTsOptions)),e.observerOptions!==void 0&&(t.observerOptions=Vr.ArtifactVerificationOptions_ObserverTimestampOptions.toJSON(e.observerOptions)),t}};Vr.ArtifactVerificationOptions_TlogOptions={fromJSON(e){return{threshold:gi(e.threshold)?globalThis.Number(e.threshold):0,performOnlineVerification:gi(e.performOnlineVerification)?globalThis.Boolean(e.performOnlineVerification):!1,disable:gi(e.disable)?globalThis.Boolean(e.disable):!1}},toJSON(e){let t={};return e.threshold!==0&&(t.threshold=Math.round(e.threshold)),e.performOnlineVerification!==!1&&(t.performOnlineVerification=e.performOnlineVerification),e.disable!==!1&&(t.disable=e.disable),t}};Vr.ArtifactVerificationOptions_CtlogOptions={fromJSON(e){return{threshold:gi(e.threshold)?globalThis.Number(e.threshold):0,disable:gi(e.disable)?globalThis.Boolean(e.disable):!1}},toJSON(e){let t={};return e.threshold!==0&&(t.threshold=Math.round(e.threshold)),e.disable!==!1&&(t.disable=e.disable),t}};Vr.ArtifactVerificationOptions_TimestampAuthorityOptions={fromJSON(e){return{threshold:gi(e.threshold)?globalThis.Number(e.threshold):0,disable:gi(e.disable)?globalThis.Boolean(e.disable):!1}},toJSON(e){let t={};return e.threshold!==0&&(t.threshold=Math.round(e.threshold)),e.disable!==!1&&(t.disable=e.disable),t}};Vr.ArtifactVerificationOptions_TlogIntegratedTimestampOptions={fromJSON(e){return{threshold:gi(e.threshold)?globalThis.Number(e.threshold):0,disable:gi(e.disable)?globalThis.Boolean(e.disable):!1}},toJSON(e){let t={};return e.threshold!==0&&(t.threshold=Math.round(e.threshold)),e.disable!==!1&&(t.disable=e.disable),t}};Vr.ArtifactVerificationOptions_ObserverTimestampOptions={fromJSON(e){return{threshold:gi(e.threshold)?globalThis.Number(e.threshold):0,disable:gi(e.disable)?globalThis.Boolean(e.disable):!1}},toJSON(e){let t={};return e.threshold!==0&&(t.threshold=Math.round(e.threshold)),e.disable!==!1&&(t.disable=e.disable),t}};Vr.Artifact={fromJSON(e){return{data:gi(e.artifactUri)?{$case:"artifactUri",artifactUri:globalThis.String(e.artifactUri)}:gi(e.artifact)?{$case:"artifact",artifact:Buffer.from(Mmt(e.artifact))}:gi(e.artifactDigest)?{$case:"artifactDigest",artifactDigest:md.HashOutput.fromJSON(e.artifactDigest)}:void 0}},toJSON(e){let t={};return e.data?.$case==="artifactUri"?t.artifactUri=e.data.artifactUri:e.data?.$case==="artifact"?t.artifact=Umt(e.data.artifact):e.data?.$case==="artifactDigest"&&(t.artifactDigest=md.HashOutput.toJSON(e.data.artifactDigest)),t}};Vr.Input={fromJSON(e){return{artifactTrustRoot:gi(e.artifactTrustRoot)?qBe.TrustedRoot.fromJSON(e.artifactTrustRoot):void 0,artifactVerificationOptions:gi(e.artifactVerificationOptions)?Vr.ArtifactVerificationOptions.fromJSON(e.artifactVerificationOptions):void 0,bundle:gi(e.bundle)?GBe.Bundle.fromJSON(e.bundle):void 0,artifact:gi(e.artifact)?Vr.Artifact.fromJSON(e.artifact):void 0}},toJSON(e){let t={};return e.artifactTrustRoot!==void 0&&(t.artifactTrustRoot=qBe.TrustedRoot.toJSON(e.artifactTrustRoot)),e.artifactVerificationOptions!==void 0&&(t.artifactVerificationOptions=Vr.ArtifactVerificationOptions.toJSON(e.artifactVerificationOptions)),e.bundle!==void 0&&(t.bundle=GBe.Bundle.toJSON(e.bundle)),e.artifact!==void 0&&(t.artifact=Vr.Artifact.toJSON(e.artifact)),t}};function Mmt(e){return Uint8Array.from(globalThis.Buffer.from(e,"base64"))}function Umt(e){return globalThis.Buffer.from(e).toString("base64")}function gi(e){return e!=null}});var yb=G(Xc=>{"use strict";var _mt=Xc&&Xc.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(t,r);(!a||("get"in a?!t.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,s,a)}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),Mw=Xc&&Xc.__exportStar||function(e,t){for(var r in e)r!=="default"&&!Object.prototype.hasOwnProperty.call(t,r)&&_mt(t,e,r)};Object.defineProperty(Xc,"__esModule",{value:!0});Mw(oV(),Xc);Mw(lV(),Xc);Mw(Lw(),Xc);Mw(aV(),Xc);Mw(cV(),Xc);Mw(WBe(),Xc)});var QN=G(bl=>{"use strict";Object.defineProperty(bl,"__esModule",{value:!0});bl.BUNDLE_V03_MEDIA_TYPE=bl.BUNDLE_V03_LEGACY_MEDIA_TYPE=bl.BUNDLE_V02_MEDIA_TYPE=bl.BUNDLE_V01_MEDIA_TYPE=void 0;bl.isBundleWithCertificateChain=Hmt;bl.isBundleWithPublicKey=jmt;bl.isBundleWithMessageSignature=Gmt;bl.isBundleWithDsseEnvelope=qmt;bl.BUNDLE_V01_MEDIA_TYPE="application/vnd.dev.sigstore.bundle+json;version=0.1";bl.BUNDLE_V02_MEDIA_TYPE="application/vnd.dev.sigstore.bundle+json;version=0.2";bl.BUNDLE_V03_LEGACY_MEDIA_TYPE="application/vnd.dev.sigstore.bundle+json;version=0.3";bl.BUNDLE_V03_MEDIA_TYPE="application/vnd.dev.sigstore.bundle.v0.3+json";function Hmt(e){return e.verificationMaterial.content.$case==="x509CertificateChain"}function jmt(e){return e.verificationMaterial.content.$case==="publicKey"}function Gmt(e){return e.content.$case==="messageSignature"}function qmt(e){return e.content.$case==="dsseEnvelope"}});var VBe=G(TN=>{"use strict";Object.defineProperty(TN,"__esModule",{value:!0});TN.toMessageSignatureBundle=Ymt;TN.toDSSEBundle=Vmt;var Wmt=yb(),RN=QN();function Ymt(e){return{mediaType:e.certificateChain?RN.BUNDLE_V02_MEDIA_TYPE:RN.BUNDLE_V03_MEDIA_TYPE,content:{$case:"messageSignature",messageSignature:{messageDigest:{algorithm:Wmt.HashAlgorithm.SHA2_256,digest:e.digest},signature:e.signature}},verificationMaterial:YBe(e)}}function Vmt(e){return{mediaType:e.certificateChain?RN.BUNDLE_V02_MEDIA_TYPE:RN.BUNDLE_V03_MEDIA_TYPE,content:{$case:"dsseEnvelope",dsseEnvelope:Jmt(e)},verificationMaterial:YBe(e)}}function Jmt(e){return{payloadType:e.artifactType,payload:e.artifact,signatures:[Kmt(e)]}}function Kmt(e){return{keyid:e.keyHint||"",sig:e.signature}}function YBe(e){return{content:zmt(e),tlogEntries:[],timestampVerificationData:{rfc3161Timestamps:[]}}}function zmt(e){return e.certificate?e.certificateChain?{$case:"x509CertificateChain",x509CertificateChain:{certificates:[{rawBytes:e.certificate}]}}:{$case:"certificate",certificate:{rawBytes:e.certificate}}:{$case:"publicKey",publicKey:{hint:e.keyHint||""}}}});var fV=G(FN=>{"use strict";Object.defineProperty(FN,"__esModule",{value:!0});FN.ValidationError=void 0;var uV=class extends Error{constructor(t,r){super(t),this.fields=r}};FN.ValidationError=uV});var AV=G(ny=>{"use strict";Object.defineProperty(ny,"__esModule",{value:!0});ny.assertBundle=Xmt;ny.assertBundleV01=JBe;ny.isBundleV01=Zmt;ny.assertBundleV02=$mt;ny.assertBundleLatest=eyt;var NN=fV();function Xmt(e){let t=ON(e);if(t.length>0)throw new NN.ValidationError("invalid bundle",t)}function JBe(e){let t=[];if(t.push(...ON(e)),t.push(...tyt(e)),t.length>0)throw new NN.ValidationError("invalid v0.1 bundle",t)}function Zmt(e){try{return JBe(e),!0}catch{return!1}}function $mt(e){let t=[];if(t.push(...ON(e)),t.push(...KBe(e)),t.length>0)throw new NN.ValidationError("invalid v0.2 bundle",t)}function eyt(e){let t=[];if(t.push(...ON(e)),t.push(...KBe(e)),t.push(...ryt(e)),t.length>0)throw new NN.ValidationError("invalid bundle",t)}function ON(e){let t=[];if((e.mediaType===void 0||!e.mediaType.match(/^application\/vnd\.dev\.sigstore\.bundle\+json;version=\d\.\d/)&&!e.mediaType.match(/^application\/vnd\.dev\.sigstore\.bundle\.v\d\.\d\+json/))&&t.push("mediaType"),e.content===void 0)t.push("content");else switch(e.content.$case){case"messageSignature":e.content.messageSignature.messageDigest===void 0?t.push("content.messageSignature.messageDigest"):e.content.messageSignature.messageDigest.digest.length===0&&t.push("content.messageSignature.messageDigest.digest"),e.content.messageSignature.signature.length===0&&t.push("content.messageSignature.signature");break;case"dsseEnvelope":e.content.dsseEnvelope.payload.length===0&&t.push("content.dsseEnvelope.payload"),e.content.dsseEnvelope.signatures.length!==1?t.push("content.dsseEnvelope.signatures"):e.content.dsseEnvelope.signatures[0].sig.length===0&&t.push("content.dsseEnvelope.signatures[0].sig");break}if(e.verificationMaterial===void 0)t.push("verificationMaterial");else{if(e.verificationMaterial.content===void 0)t.push("verificationMaterial.content");else switch(e.verificationMaterial.content.$case){case"x509CertificateChain":e.verificationMaterial.content.x509CertificateChain.certificates.length===0&&t.push("verificationMaterial.content.x509CertificateChain.certificates"),e.verificationMaterial.content.x509CertificateChain.certificates.forEach((r,s)=>{r.rawBytes.length===0&&t.push(`verificationMaterial.content.x509CertificateChain.certificates[${s}].rawBytes`)});break;case"certificate":e.verificationMaterial.content.certificate.rawBytes.length===0&&t.push("verificationMaterial.content.certificate.rawBytes");break}e.verificationMaterial.tlogEntries===void 0?t.push("verificationMaterial.tlogEntries"):e.verificationMaterial.tlogEntries.length>0&&e.verificationMaterial.tlogEntries.forEach((r,s)=>{r.logId===void 0&&t.push(`verificationMaterial.tlogEntries[${s}].logId`),r.kindVersion===void 0&&t.push(`verificationMaterial.tlogEntries[${s}].kindVersion`)})}return t}function tyt(e){let t=[];return e.verificationMaterial&&e.verificationMaterial.tlogEntries?.length>0&&e.verificationMaterial.tlogEntries.forEach((r,s)=>{r.inclusionPromise===void 0&&t.push(`verificationMaterial.tlogEntries[${s}].inclusionPromise`)}),t}function KBe(e){let t=[];return e.verificationMaterial&&e.verificationMaterial.tlogEntries?.length>0&&e.verificationMaterial.tlogEntries.forEach((r,s)=>{r.inclusionProof===void 0?t.push(`verificationMaterial.tlogEntries[${s}].inclusionProof`):r.inclusionProof.checkpoint===void 0&&t.push(`verificationMaterial.tlogEntries[${s}].inclusionProof.checkpoint`)}),t}function ryt(e){let t=[];return e.verificationMaterial?.content?.$case==="x509CertificateChain"&&t.push("verificationMaterial.content.$case"),t}});var XBe=G(SA=>{"use strict";Object.defineProperty(SA,"__esModule",{value:!0});SA.envelopeToJSON=SA.envelopeFromJSON=SA.bundleToJSON=SA.bundleFromJSON=void 0;var LN=yb(),zBe=QN(),pV=AV(),nyt=e=>{let t=LN.Bundle.fromJSON(e);switch(t.mediaType){case zBe.BUNDLE_V01_MEDIA_TYPE:(0,pV.assertBundleV01)(t);break;case zBe.BUNDLE_V02_MEDIA_TYPE:(0,pV.assertBundleV02)(t);break;default:(0,pV.assertBundleLatest)(t);break}return t};SA.bundleFromJSON=nyt;var iyt=e=>LN.Bundle.toJSON(e);SA.bundleToJSON=iyt;var syt=e=>LN.Envelope.fromJSON(e);SA.envelopeFromJSON=syt;var oyt=e=>LN.Envelope.toJSON(e);SA.envelopeToJSON=oyt});var Ib=G(Xr=>{"use strict";Object.defineProperty(Xr,"__esModule",{value:!0});Xr.isBundleV01=Xr.assertBundleV02=Xr.assertBundleV01=Xr.assertBundleLatest=Xr.assertBundle=Xr.envelopeToJSON=Xr.envelopeFromJSON=Xr.bundleToJSON=Xr.bundleFromJSON=Xr.ValidationError=Xr.isBundleWithPublicKey=Xr.isBundleWithMessageSignature=Xr.isBundleWithDsseEnvelope=Xr.isBundleWithCertificateChain=Xr.BUNDLE_V03_MEDIA_TYPE=Xr.BUNDLE_V03_LEGACY_MEDIA_TYPE=Xr.BUNDLE_V02_MEDIA_TYPE=Xr.BUNDLE_V01_MEDIA_TYPE=Xr.toMessageSignatureBundle=Xr.toDSSEBundle=void 0;var ZBe=VBe();Object.defineProperty(Xr,"toDSSEBundle",{enumerable:!0,get:function(){return ZBe.toDSSEBundle}});Object.defineProperty(Xr,"toMessageSignatureBundle",{enumerable:!0,get:function(){return ZBe.toMessageSignatureBundle}});var yd=QN();Object.defineProperty(Xr,"BUNDLE_V01_MEDIA_TYPE",{enumerable:!0,get:function(){return yd.BUNDLE_V01_MEDIA_TYPE}});Object.defineProperty(Xr,"BUNDLE_V02_MEDIA_TYPE",{enumerable:!0,get:function(){return yd.BUNDLE_V02_MEDIA_TYPE}});Object.defineProperty(Xr,"BUNDLE_V03_LEGACY_MEDIA_TYPE",{enumerable:!0,get:function(){return yd.BUNDLE_V03_LEGACY_MEDIA_TYPE}});Object.defineProperty(Xr,"BUNDLE_V03_MEDIA_TYPE",{enumerable:!0,get:function(){return yd.BUNDLE_V03_MEDIA_TYPE}});Object.defineProperty(Xr,"isBundleWithCertificateChain",{enumerable:!0,get:function(){return yd.isBundleWithCertificateChain}});Object.defineProperty(Xr,"isBundleWithDsseEnvelope",{enumerable:!0,get:function(){return yd.isBundleWithDsseEnvelope}});Object.defineProperty(Xr,"isBundleWithMessageSignature",{enumerable:!0,get:function(){return yd.isBundleWithMessageSignature}});Object.defineProperty(Xr,"isBundleWithPublicKey",{enumerable:!0,get:function(){return yd.isBundleWithPublicKey}});var ayt=fV();Object.defineProperty(Xr,"ValidationError",{enumerable:!0,get:function(){return ayt.ValidationError}});var MN=XBe();Object.defineProperty(Xr,"bundleFromJSON",{enumerable:!0,get:function(){return MN.bundleFromJSON}});Object.defineProperty(Xr,"bundleToJSON",{enumerable:!0,get:function(){return MN.bundleToJSON}});Object.defineProperty(Xr,"envelopeFromJSON",{enumerable:!0,get:function(){return MN.envelopeFromJSON}});Object.defineProperty(Xr,"envelopeToJSON",{enumerable:!0,get:function(){return MN.envelopeToJSON}});var Eb=AV();Object.defineProperty(Xr,"assertBundle",{enumerable:!0,get:function(){return Eb.assertBundle}});Object.defineProperty(Xr,"assertBundleLatest",{enumerable:!0,get:function(){return Eb.assertBundleLatest}});Object.defineProperty(Xr,"assertBundleV01",{enumerable:!0,get:function(){return Eb.assertBundleV01}});Object.defineProperty(Xr,"assertBundleV02",{enumerable:!0,get:function(){return Eb.assertBundleV02}});Object.defineProperty(Xr,"isBundleV01",{enumerable:!0,get:function(){return Eb.isBundleV01}})});var Cb=G(_N=>{"use strict";Object.defineProperty(_N,"__esModule",{value:!0});_N.ByteStream=void 0;var hV=class extends Error{},UN=class e{constructor(t){this.start=0,t?(this.buf=t,this.view=Buffer.from(t)):(this.buf=new ArrayBuffer(0),this.view=Buffer.from(this.buf))}get buffer(){return this.view.subarray(0,this.start)}get length(){return this.view.byteLength}get position(){return this.start}seek(t){this.start=t}slice(t,r){let s=t+r;if(s>this.length)throw new hV("request past end of buffer");return this.view.subarray(t,s)}appendChar(t){this.ensureCapacity(1),this.view[this.start]=t,this.start+=1}appendUint16(t){this.ensureCapacity(2);let r=new Uint16Array([t]),s=new Uint8Array(r.buffer);this.view[this.start]=s[1],this.view[this.start+1]=s[0],this.start+=2}appendUint24(t){this.ensureCapacity(3);let r=new Uint32Array([t]),s=new Uint8Array(r.buffer);this.view[this.start]=s[2],this.view[this.start+1]=s[1],this.view[this.start+2]=s[0],this.start+=3}appendView(t){this.ensureCapacity(t.length),this.view.set(t,this.start),this.start+=t.length}getBlock(t){if(t<=0)return Buffer.alloc(0);if(this.start+t>this.view.length)throw new Error("request past end of buffer");let r=this.view.subarray(this.start,this.start+t);return this.start+=t,r}getUint8(){return this.getBlock(1)[0]}getUint16(){let t=this.getBlock(2);return t[0]<<8|t[1]}ensureCapacity(t){if(this.start+t>this.view.byteLength){let r=e.BLOCK_SIZE+(t>e.BLOCK_SIZE?t:0);this.realloc(this.view.byteLength+r)}}realloc(t){let r=new ArrayBuffer(t),s=Buffer.from(r);s.set(this.view),this.buf=r,this.view=s}};_N.ByteStream=UN;UN.BLOCK_SIZE=1024});var HN=G(Uw=>{"use strict";Object.defineProperty(Uw,"__esModule",{value:!0});Uw.ASN1TypeError=Uw.ASN1ParseError=void 0;var dV=class extends Error{};Uw.ASN1ParseError=dV;var gV=class extends Error{};Uw.ASN1TypeError=gV});var eve=G(jN=>{"use strict";Object.defineProperty(jN,"__esModule",{value:!0});jN.decodeLength=lyt;jN.encodeLength=cyt;var $Be=HN();function lyt(e){let t=e.getUint8();if(!(t&128))return t;let r=t&127;if(r>6)throw new $Be.ASN1ParseError("length exceeds 6 byte limit");let s=0;for(let a=0;a0n;)r.unshift(Number(t&255n)),t=t>>8n;return Buffer.from([128|r.length,...r])}});var rve=G(Ed=>{"use strict";Object.defineProperty(Ed,"__esModule",{value:!0});Ed.parseInteger=Ayt;Ed.parseStringASCII=tve;Ed.parseTime=pyt;Ed.parseOID=hyt;Ed.parseBoolean=dyt;Ed.parseBitString=gyt;var uyt=/^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\.\d{3})?Z$/,fyt=/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\.\d{3})?Z$/;function Ayt(e){let t=0,r=e.length,s=e[t],a=s>127,n=a?255:0;for(;s==n&&++t=50?1900:2e3,s[1]=a.toString()}return new Date(`${s[1]}-${s[2]}-${s[3]}T${s[4]}:${s[5]}:${s[6]}Z`)}function hyt(e){let t=0,r=e.length,s=e[t++],a=Math.floor(s/40),n=s%40,c=`${a}.${n}`,f=0;for(;t=f;--p)a.push(c>>p&1)}return a}});var ive=G(GN=>{"use strict";Object.defineProperty(GN,"__esModule",{value:!0});GN.ASN1Tag=void 0;var nve=HN(),iy={BOOLEAN:1,INTEGER:2,BIT_STRING:3,OCTET_STRING:4,OBJECT_IDENTIFIER:6,SEQUENCE:16,SET:17,PRINTABLE_STRING:19,UTC_TIME:23,GENERALIZED_TIME:24},mV={UNIVERSAL:0,APPLICATION:1,CONTEXT_SPECIFIC:2,PRIVATE:3},yV=class{constructor(t){if(this.number=t&31,this.constructed=(t&32)===32,this.class=t>>6,this.number===31)throw new nve.ASN1ParseError("long form tags not supported");if(this.class===mV.UNIVERSAL&&this.number===0)throw new nve.ASN1ParseError("unsupported tag 0x00")}isUniversal(){return this.class===mV.UNIVERSAL}isContextSpecific(t){let r=this.class===mV.CONTEXT_SPECIFIC;return t!==void 0?r&&this.number===t:r}isBoolean(){return this.isUniversal()&&this.number===iy.BOOLEAN}isInteger(){return this.isUniversal()&&this.number===iy.INTEGER}isBitString(){return this.isUniversal()&&this.number===iy.BIT_STRING}isOctetString(){return this.isUniversal()&&this.number===iy.OCTET_STRING}isOID(){return this.isUniversal()&&this.number===iy.OBJECT_IDENTIFIER}isUTCTime(){return this.isUniversal()&&this.number===iy.UTC_TIME}isGeneralizedTime(){return this.isUniversal()&&this.number===iy.GENERALIZED_TIME}toDER(){return this.number|(this.constructed?32:0)|this.class<<6}};GN.ASN1Tag=yV});var lve=G(WN=>{"use strict";Object.defineProperty(WN,"__esModule",{value:!0});WN.ASN1Obj=void 0;var EV=Cb(),sy=HN(),ove=eve(),_w=rve(),myt=ive(),qN=class{constructor(t,r,s){this.tag=t,this.value=r,this.subs=s}static parseBuffer(t){return ave(new EV.ByteStream(t))}toDER(){let t=new EV.ByteStream;if(this.subs.length>0)for(let a of this.subs)t.appendView(a.toDER());else t.appendView(this.value);let r=t.buffer,s=new EV.ByteStream;return s.appendChar(this.tag.toDER()),s.appendView((0,ove.encodeLength)(r.length)),s.appendView(r),s.buffer}toBoolean(){if(!this.tag.isBoolean())throw new sy.ASN1TypeError("not a boolean");return(0,_w.parseBoolean)(this.value)}toInteger(){if(!this.tag.isInteger())throw new sy.ASN1TypeError("not an integer");return(0,_w.parseInteger)(this.value)}toOID(){if(!this.tag.isOID())throw new sy.ASN1TypeError("not an OID");return(0,_w.parseOID)(this.value)}toDate(){switch(!0){case this.tag.isUTCTime():return(0,_w.parseTime)(this.value,!0);case this.tag.isGeneralizedTime():return(0,_w.parseTime)(this.value,!1);default:throw new sy.ASN1TypeError("not a date")}}toBitString(){if(!this.tag.isBitString())throw new sy.ASN1TypeError("not a bit string");return(0,_w.parseBitString)(this.value)}};WN.ASN1Obj=qN;function ave(e){let t=new myt.ASN1Tag(e.getUint8()),r=(0,ove.decodeLength)(e),s=e.slice(e.position,r),a=e.position,n=[];if(t.constructed)n=sve(e,r);else if(t.isOctetString())try{n=sve(e,r)}catch{}return n.length===0&&e.seek(a+r),new qN(t,s,n)}function sve(e,t){let r=e.position+t;if(r>e.length)throw new sy.ASN1ParseError("invalid length");let s=[];for(;e.position{"use strict";Object.defineProperty(YN,"__esModule",{value:!0});YN.ASN1Obj=void 0;var yyt=lve();Object.defineProperty(YN,"ASN1Obj",{enumerable:!0,get:function(){return yyt.ASN1Obj}})});var Hw=G(Id=>{"use strict";var Eyt=Id&&Id.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(Id,"__esModule",{value:!0});Id.createPublicKey=Iyt;Id.digest=Cyt;Id.verify=wyt;Id.bufferEqual=Byt;var wb=Eyt(Ie("crypto"));function Iyt(e,t="spki"){return typeof e=="string"?wb.default.createPublicKey(e):wb.default.createPublicKey({key:e,format:"der",type:t})}function Cyt(e,...t){let r=wb.default.createHash(e);for(let s of t)r.update(s);return r.digest()}function wyt(e,t,r,s){try{return wb.default.verify(s,e,t,r)}catch{return!1}}function Byt(e,t){try{return wb.default.timingSafeEqual(e,t)}catch{return!1}}});var cve=G(IV=>{"use strict";Object.defineProperty(IV,"__esModule",{value:!0});IV.preAuthEncoding=Syt;var vyt="DSSEv1";function Syt(e,t){let r=[vyt,e.length,e,t.length,""].join(" ");return Buffer.concat([Buffer.from(r,"ascii"),t])}});var Ave=G(JN=>{"use strict";Object.defineProperty(JN,"__esModule",{value:!0});JN.base64Encode=Dyt;JN.base64Decode=byt;var uve="base64",fve="utf-8";function Dyt(e){return Buffer.from(e,fve).toString(uve)}function byt(e){return Buffer.from(e,uve).toString(fve)}});var pve=G(wV=>{"use strict";Object.defineProperty(wV,"__esModule",{value:!0});wV.canonicalize=CV;function CV(e){let t="";if(e===null||typeof e!="object"||e.toJSON!=null)t+=JSON.stringify(e);else if(Array.isArray(e)){t+="[";let r=!0;e.forEach(s=>{r||(t+=","),r=!1,t+=CV(s)}),t+="]"}else{t+="{";let r=!0;Object.keys(e).sort().forEach(s=>{r||(t+=","),r=!1,t+=JSON.stringify(s),t+=":",t+=CV(e[s])}),t+="}"}return t}});var BV=G(KN=>{"use strict";Object.defineProperty(KN,"__esModule",{value:!0});KN.toDER=kyt;KN.fromDER=Qyt;var Pyt=/-----BEGIN (.*)-----/,xyt=/-----END (.*)-----/;function kyt(e){let t="";return e.split(` `).forEach(r=>{r.match(Pyt)||r.match(xyt)||(t+=r)}),Buffer.from(t,"base64")}function Qyt(e,t="CERTIFICATE"){let s=e.toString("base64").match(/.{1,64}/g)||"";return[`-----BEGIN ${t}-----`,...s,`-----END ${t}-----`].join(` `).concat(` `)}});var zN=G(jw=>{"use strict";Object.defineProperty(jw,"__esModule",{value:!0});jw.SHA2_HASH_ALGOS=jw.ECDSA_SIGNATURE_ALGOS=void 0;jw.ECDSA_SIGNATURE_ALGOS={"1.2.840.10045.4.3.1":"sha224","1.2.840.10045.4.3.2":"sha256","1.2.840.10045.4.3.3":"sha384","1.2.840.10045.4.3.4":"sha512"};jw.SHA2_HASH_ALGOS={"2.16.840.1.101.3.4.2.1":"sha256","2.16.840.1.101.3.4.2.2":"sha384","2.16.840.1.101.3.4.2.3":"sha512"}});var SV=G(XN=>{"use strict";Object.defineProperty(XN,"__esModule",{value:!0});XN.RFC3161TimestampVerificationError=void 0;var vV=class extends Error{};XN.RFC3161TimestampVerificationError=vV});var dve=G(DA=>{"use strict";var Ryt=DA&&DA.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(t,r);(!a||("get"in a?!t.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,s,a)}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),Tyt=DA&&DA.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),Fyt=DA&&DA.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!=="default"&&Object.prototype.hasOwnProperty.call(e,r)&&Ryt(t,e,r);return Tyt(t,e),t};Object.defineProperty(DA,"__esModule",{value:!0});DA.TSTInfo=void 0;var hve=Fyt(Hw()),Nyt=zN(),Oyt=SV(),DV=class{constructor(t){this.root=t}get version(){return this.root.subs[0].toInteger()}get genTime(){return this.root.subs[4].toDate()}get messageImprintHashAlgorithm(){let t=this.messageImprintObj.subs[0].subs[0].toOID();return Nyt.SHA2_HASH_ALGOS[t]}get messageImprintHashedMessage(){return this.messageImprintObj.subs[1].value}get raw(){return this.root.toDER()}verify(t){let r=hve.digest(this.messageImprintHashAlgorithm,t);if(!hve.bufferEqual(r,this.messageImprintHashedMessage))throw new Oyt.RFC3161TimestampVerificationError("message imprint does not match artifact")}get messageImprintObj(){return this.root.subs[2]}};DA.TSTInfo=DV});var mve=G(bA=>{"use strict";var Lyt=bA&&bA.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(t,r);(!a||("get"in a?!t.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,s,a)}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),Myt=bA&&bA.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),Uyt=bA&&bA.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!=="default"&&Object.prototype.hasOwnProperty.call(e,r)&&Lyt(t,e,r);return Myt(t,e),t};Object.defineProperty(bA,"__esModule",{value:!0});bA.RFC3161Timestamp=void 0;var _yt=VN(),bV=Uyt(Hw()),gve=zN(),Bb=SV(),Hyt=dve(),jyt="1.2.840.113549.1.7.2",Gyt="1.2.840.113549.1.9.16.1.4",qyt="1.2.840.113549.1.9.4",PV=class e{constructor(t){this.root=t}static parse(t){let r=_yt.ASN1Obj.parseBuffer(t);return new e(r)}get status(){return this.pkiStatusInfoObj.subs[0].toInteger()}get contentType(){return this.contentTypeObj.toOID()}get eContentType(){return this.eContentTypeObj.toOID()}get signingTime(){return this.tstInfo.genTime}get signerIssuer(){return this.signerSidObj.subs[0].value}get signerSerialNumber(){return this.signerSidObj.subs[1].value}get signerDigestAlgorithm(){let t=this.signerDigestAlgorithmObj.subs[0].toOID();return gve.SHA2_HASH_ALGOS[t]}get signatureAlgorithm(){let t=this.signatureAlgorithmObj.subs[0].toOID();return gve.ECDSA_SIGNATURE_ALGOS[t]}get signatureValue(){return this.signatureValueObj.value}get tstInfo(){return new Hyt.TSTInfo(this.eContentObj.subs[0].subs[0])}verify(t,r){if(!this.timeStampTokenObj)throw new Bb.RFC3161TimestampVerificationError("timeStampToken is missing");if(this.contentType!==jyt)throw new Bb.RFC3161TimestampVerificationError(`incorrect content type: ${this.contentType}`);if(this.eContentType!==Gyt)throw new Bb.RFC3161TimestampVerificationError(`incorrect encapsulated content type: ${this.eContentType}`);this.tstInfo.verify(t),this.verifyMessageDigest(),this.verifySignature(r)}verifyMessageDigest(){let t=bV.digest(this.signerDigestAlgorithm,this.tstInfo.raw),r=this.messageDigestAttributeObj.subs[1].subs[0].value;if(!bV.bufferEqual(t,r))throw new Bb.RFC3161TimestampVerificationError("signed data does not match tstInfo")}verifySignature(t){let r=this.signedAttrsObj.toDER();if(r[0]=49,!bV.verify(r,t,this.signatureValue,this.signatureAlgorithm))throw new Bb.RFC3161TimestampVerificationError("signature verification failed")}get pkiStatusInfoObj(){return this.root.subs[0]}get timeStampTokenObj(){return this.root.subs[1]}get contentTypeObj(){return this.timeStampTokenObj.subs[0]}get signedDataObj(){return this.timeStampTokenObj.subs.find(r=>r.tag.isContextSpecific(0)).subs[0]}get encapContentInfoObj(){return this.signedDataObj.subs[2]}get signerInfosObj(){let t=this.signedDataObj;return t.subs[t.subs.length-1]}get signerInfoObj(){return this.signerInfosObj.subs[0]}get eContentTypeObj(){return this.encapContentInfoObj.subs[0]}get eContentObj(){return this.encapContentInfoObj.subs[1]}get signedAttrsObj(){return this.signerInfoObj.subs.find(r=>r.tag.isContextSpecific(0))}get messageDigestAttributeObj(){return this.signedAttrsObj.subs.find(r=>r.subs[0].tag.isOID()&&r.subs[0].toOID()===qyt)}get signerSidObj(){return this.signerInfoObj.subs[1]}get signerDigestAlgorithmObj(){return this.signerInfoObj.subs[2]}get signatureAlgorithmObj(){return this.signerInfoObj.subs[4]}get signatureValueObj(){return this.signerInfoObj.subs[5]}};bA.RFC3161Timestamp=PV});var yve=G(ZN=>{"use strict";Object.defineProperty(ZN,"__esModule",{value:!0});ZN.RFC3161Timestamp=void 0;var Wyt=mve();Object.defineProperty(ZN,"RFC3161Timestamp",{enumerable:!0,get:function(){return Wyt.RFC3161Timestamp}})});var Ive=G(PA=>{"use strict";var Yyt=PA&&PA.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(t,r);(!a||("get"in a?!t.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,s,a)}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),Vyt=PA&&PA.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),Jyt=PA&&PA.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!=="default"&&Object.prototype.hasOwnProperty.call(e,r)&&Yyt(t,e,r);return Vyt(t,e),t};Object.defineProperty(PA,"__esModule",{value:!0});PA.SignedCertificateTimestamp=void 0;var Kyt=Jyt(Hw()),Eve=Cb(),xV=class e{constructor(t){this.version=t.version,this.logID=t.logID,this.timestamp=t.timestamp,this.extensions=t.extensions,this.hashAlgorithm=t.hashAlgorithm,this.signatureAlgorithm=t.signatureAlgorithm,this.signature=t.signature}get datetime(){return new Date(Number(this.timestamp.readBigInt64BE()))}get algorithm(){switch(this.hashAlgorithm){case 0:return"none";case 1:return"md5";case 2:return"sha1";case 3:return"sha224";case 4:return"sha256";case 5:return"sha384";case 6:return"sha512";default:return"unknown"}}verify(t,r){let s=new Eve.ByteStream;return s.appendChar(this.version),s.appendChar(0),s.appendView(this.timestamp),s.appendUint16(1),s.appendView(t),s.appendUint16(this.extensions.byteLength),this.extensions.byteLength>0&&s.appendView(this.extensions),Kyt.verify(s.buffer,r,this.signature,this.algorithm)}static parse(t){let r=new Eve.ByteStream(t),s=r.getUint8(),a=r.getBlock(32),n=r.getBlock(8),c=r.getUint16(),f=r.getBlock(c),p=r.getUint8(),h=r.getUint8(),E=r.getUint16(),C=r.getBlock(E);if(r.position!==t.length)throw new Error("SCT buffer length mismatch");return new e({version:s,logID:a,timestamp:n,extensions:f,hashAlgorithm:p,signatureAlgorithm:h,signature:C})}};PA.SignedCertificateTimestamp=xV});var OV=G(ua=>{"use strict";Object.defineProperty(ua,"__esModule",{value:!0});ua.X509SCTExtension=ua.X509SubjectKeyIDExtension=ua.X509AuthorityKeyIDExtension=ua.X509SubjectAlternativeNameExtension=ua.X509KeyUsageExtension=ua.X509BasicConstraintsExtension=ua.X509Extension=void 0;var zyt=Cb(),Xyt=Ive(),Ah=class{constructor(t){this.root=t}get oid(){return this.root.subs[0].toOID()}get critical(){return this.root.subs.length===3?this.root.subs[1].toBoolean():!1}get value(){return this.extnValueObj.value}get valueObj(){return this.extnValueObj}get extnValueObj(){return this.root.subs[this.root.subs.length-1]}};ua.X509Extension=Ah;var kV=class extends Ah{get isCA(){return this.sequence.subs[0]?.toBoolean()??!1}get pathLenConstraint(){return this.sequence.subs.length>1?this.sequence.subs[1].toInteger():void 0}get sequence(){return this.extnValueObj.subs[0]}};ua.X509BasicConstraintsExtension=kV;var QV=class extends Ah{get digitalSignature(){return this.bitString[0]===1}get keyCertSign(){return this.bitString[5]===1}get crlSign(){return this.bitString[6]===1}get bitString(){return this.extnValueObj.subs[0].toBitString()}};ua.X509KeyUsageExtension=QV;var RV=class extends Ah{get rfc822Name(){return this.findGeneralName(1)?.value.toString("ascii")}get uri(){return this.findGeneralName(6)?.value.toString("ascii")}otherName(t){let r=this.findGeneralName(0);return r===void 0||r.subs[0].toOID()!==t?void 0:r.subs[1].subs[0].value.toString("ascii")}findGeneralName(t){return this.generalNames.find(r=>r.tag.isContextSpecific(t))}get generalNames(){return this.extnValueObj.subs[0].subs}};ua.X509SubjectAlternativeNameExtension=RV;var TV=class extends Ah{get keyIdentifier(){return this.findSequenceMember(0)?.value}findSequenceMember(t){return this.sequence.subs.find(r=>r.tag.isContextSpecific(t))}get sequence(){return this.extnValueObj.subs[0]}};ua.X509AuthorityKeyIDExtension=TV;var FV=class extends Ah{get keyIdentifier(){return this.extnValueObj.subs[0].value}};ua.X509SubjectKeyIDExtension=FV;var NV=class extends Ah{constructor(t){super(t)}get signedCertificateTimestamps(){let t=this.extnValueObj.subs[0].value,r=new zyt.ByteStream(t),s=r.getUint16()+2,a=[];for(;r.position{"use strict";var Zyt=oc&&oc.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(t,r);(!a||("get"in a?!t.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,s,a)}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),$yt=oc&&oc.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),wve=oc&&oc.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!=="default"&&Object.prototype.hasOwnProperty.call(e,r)&&Zyt(t,e,r);return $yt(t,e),t};Object.defineProperty(oc,"__esModule",{value:!0});oc.X509Certificate=oc.EXTENSION_OID_SCT=void 0;var eEt=VN(),Cve=wve(Hw()),tEt=zN(),rEt=wve(BV()),oy=OV(),nEt="2.5.29.14",iEt="2.5.29.15",sEt="2.5.29.17",oEt="2.5.29.19",aEt="2.5.29.35";oc.EXTENSION_OID_SCT="1.3.6.1.4.1.11129.2.4.2";var LV=class e{constructor(t){this.root=t}static parse(t){let r=typeof t=="string"?rEt.toDER(t):t,s=eEt.ASN1Obj.parseBuffer(r);return new e(s)}get tbsCertificate(){return this.tbsCertificateObj}get version(){return`v${(this.versionObj.subs[0].toInteger()+BigInt(1)).toString()}`}get serialNumber(){return this.serialNumberObj.value}get notBefore(){return this.validityObj.subs[0].toDate()}get notAfter(){return this.validityObj.subs[1].toDate()}get issuer(){return this.issuerObj.value}get subject(){return this.subjectObj.value}get publicKey(){return this.subjectPublicKeyInfoObj.toDER()}get signatureAlgorithm(){let t=this.signatureAlgorithmObj.subs[0].toOID();return tEt.ECDSA_SIGNATURE_ALGOS[t]}get signatureValue(){return this.signatureValueObj.value.subarray(1)}get subjectAltName(){let t=this.extSubjectAltName;return t?.uri||t?.rfc822Name}get extensions(){return this.extensionsObj?.subs[0]?.subs||[]}get extKeyUsage(){let t=this.findExtension(iEt);return t?new oy.X509KeyUsageExtension(t):void 0}get extBasicConstraints(){let t=this.findExtension(oEt);return t?new oy.X509BasicConstraintsExtension(t):void 0}get extSubjectAltName(){let t=this.findExtension(sEt);return t?new oy.X509SubjectAlternativeNameExtension(t):void 0}get extAuthorityKeyID(){let t=this.findExtension(aEt);return t?new oy.X509AuthorityKeyIDExtension(t):void 0}get extSubjectKeyID(){let t=this.findExtension(nEt);return t?new oy.X509SubjectKeyIDExtension(t):void 0}get extSCT(){let t=this.findExtension(oc.EXTENSION_OID_SCT);return t?new oy.X509SCTExtension(t):void 0}get isCA(){let t=this.extBasicConstraints?.isCA||!1;return this.extKeyUsage?t&&this.extKeyUsage.keyCertSign:t}extension(t){let r=this.findExtension(t);return r?new oy.X509Extension(r):void 0}verify(t){let r=t?.publicKey||this.publicKey,s=Cve.createPublicKey(r);return Cve.verify(this.tbsCertificate.toDER(),s,this.signatureValue,this.signatureAlgorithm)}validForDate(t){return this.notBefore<=t&&t<=this.notAfter}equals(t){return this.root.toDER().equals(t.root.toDER())}clone(){let t=this.root.toDER(),r=Buffer.alloc(t.length);return t.copy(r),e.parse(r)}findExtension(t){return this.extensions.find(r=>r.subs[0].toOID()===t)}get tbsCertificateObj(){return this.root.subs[0]}get signatureAlgorithmObj(){return this.root.subs[1]}get signatureValueObj(){return this.root.subs[2]}get versionObj(){return this.tbsCertificateObj.subs[0]}get serialNumberObj(){return this.tbsCertificateObj.subs[1]}get issuerObj(){return this.tbsCertificateObj.subs[3]}get validityObj(){return this.tbsCertificateObj.subs[4]}get subjectObj(){return this.tbsCertificateObj.subs[5]}get subjectPublicKeyInfoObj(){return this.tbsCertificateObj.subs[6]}get extensionsObj(){return this.tbsCertificateObj.subs.find(t=>t.tag.isContextSpecific(3))}};oc.X509Certificate=LV});var Sve=G(Cd=>{"use strict";Object.defineProperty(Cd,"__esModule",{value:!0});Cd.X509SCTExtension=Cd.X509Certificate=Cd.EXTENSION_OID_SCT=void 0;var vve=Bve();Object.defineProperty(Cd,"EXTENSION_OID_SCT",{enumerable:!0,get:function(){return vve.EXTENSION_OID_SCT}});Object.defineProperty(Cd,"X509Certificate",{enumerable:!0,get:function(){return vve.X509Certificate}});var lEt=OV();Object.defineProperty(Cd,"X509SCTExtension",{enumerable:!0,get:function(){return lEt.X509SCTExtension}})});var Pl=G(zn=>{"use strict";var cEt=zn&&zn.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(t,r);(!a||("get"in a?!t.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,s,a)}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),uEt=zn&&zn.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),vb=zn&&zn.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!=="default"&&Object.prototype.hasOwnProperty.call(e,r)&&cEt(t,e,r);return uEt(t,e),t};Object.defineProperty(zn,"__esModule",{value:!0});zn.X509SCTExtension=zn.X509Certificate=zn.EXTENSION_OID_SCT=zn.ByteStream=zn.RFC3161Timestamp=zn.pem=zn.json=zn.encoding=zn.dsse=zn.crypto=zn.ASN1Obj=void 0;var fEt=VN();Object.defineProperty(zn,"ASN1Obj",{enumerable:!0,get:function(){return fEt.ASN1Obj}});zn.crypto=vb(Hw());zn.dsse=vb(cve());zn.encoding=vb(Ave());zn.json=vb(pve());zn.pem=vb(BV());var AEt=yve();Object.defineProperty(zn,"RFC3161Timestamp",{enumerable:!0,get:function(){return AEt.RFC3161Timestamp}});var pEt=Cb();Object.defineProperty(zn,"ByteStream",{enumerable:!0,get:function(){return pEt.ByteStream}});var MV=Sve();Object.defineProperty(zn,"EXTENSION_OID_SCT",{enumerable:!0,get:function(){return MV.EXTENSION_OID_SCT}});Object.defineProperty(zn,"X509Certificate",{enumerable:!0,get:function(){return MV.X509Certificate}});Object.defineProperty(zn,"X509SCTExtension",{enumerable:!0,get:function(){return MV.X509SCTExtension}})});var Dve=G(UV=>{"use strict";Object.defineProperty(UV,"__esModule",{value:!0});UV.extractJWTSubject=dEt;var hEt=Pl();function dEt(e){let t=e.split(".",3),r=JSON.parse(hEt.encoding.base64Decode(t[1]));switch(r.iss){case"https://accounts.google.com":case"https://oauth2.sigstore.dev/auth":return r.email;default:return r.sub}}});var bve=G((Ier,gEt)=>{gEt.exports={name:"@sigstore/sign",version:"3.1.0",description:"Sigstore signing library",main:"dist/index.js",types:"dist/index.d.ts",scripts:{clean:"shx rm -rf dist *.tsbuildinfo",build:"tsc --build",test:"jest"},files:["dist"],author:"bdehamer@github.com",license:"Apache-2.0",repository:{type:"git",url:"git+https://github.com/sigstore/sigstore-js.git"},bugs:{url:"https://github.com/sigstore/sigstore-js/issues"},homepage:"https://github.com/sigstore/sigstore-js/tree/main/packages/sign#readme",publishConfig:{provenance:!0},devDependencies:{"@sigstore/jest":"^0.0.0","@sigstore/mock":"^0.10.0","@sigstore/rekor-types":"^3.0.0","@types/make-fetch-happen":"^10.0.4","@types/promise-retry":"^1.1.6"},dependencies:{"@sigstore/bundle":"^3.1.0","@sigstore/core":"^2.0.0","@sigstore/protobuf-specs":"^0.4.0","make-fetch-happen":"^14.0.2","proc-log":"^5.0.0","promise-retry":"^2.0.1"},engines:{node:"^18.17.0 || >=20.5.0"}}});var xve=G(Gw=>{"use strict";var mEt=Gw&&Gw.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(Gw,"__esModule",{value:!0});Gw.getUserAgent=void 0;var Pve=mEt(Ie("os")),yEt=()=>{let e=bve().version,t=process.version,r=Pve.default.platform(),s=Pve.default.arch();return`sigstore-js/${e} (Node ${t}) (${r}/${s})`};Gw.getUserAgent=yEt});var wd=G(Ji=>{"use strict";var EEt=Ji&&Ji.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(t,r);(!a||("get"in a?!t.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,s,a)}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),IEt=Ji&&Ji.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),kve=Ji&&Ji.__importStar||function(){var e=function(t){return e=Object.getOwnPropertyNames||function(r){var s=[];for(var a in r)Object.prototype.hasOwnProperty.call(r,a)&&(s[s.length]=a);return s},e(t)};return function(t){if(t&&t.__esModule)return t;var r={};if(t!=null)for(var s=e(t),a=0;a{"use strict";Object.defineProperty($N,"__esModule",{value:!0});$N.BaseBundleBuilder=void 0;var _V=class{constructor(t){this.signer=t.signer,this.witnesses=t.witnesses}async create(t){let r=await this.prepare(t).then(f=>this.signer.sign(f)),s=await this.package(t,r),a=await Promise.all(this.witnesses.map(f=>f.testify(s.content,CEt(r.key)))),n=[],c=[];return a.forEach(({tlogEntries:f,rfc3161Timestamps:p})=>{n.push(...f??[]),c.push(...p??[])}),s.verificationMaterial.tlogEntries=n,s.verificationMaterial.timestampVerificationData={rfc3161Timestamps:c},s}async prepare(t){return t.data}};$N.BaseBundleBuilder=_V;function CEt(e){switch(e.$case){case"publicKey":return e.publicKey;case"x509Certificate":return e.certificate}}});var GV=G(xA=>{"use strict";var wEt=xA&&xA.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(t,r);(!a||("get"in a?!t.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,s,a)}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),BEt=xA&&xA.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),vEt=xA&&xA.__importStar||function(){var e=function(t){return e=Object.getOwnPropertyNames||function(r){var s=[];for(var a in r)Object.prototype.hasOwnProperty.call(r,a)&&(s[s.length]=a);return s},e(t)};return function(t){if(t&&t.__esModule)return t;var r={};if(t!=null)for(var s=e(t),a=0;a{"use strict";Object.defineProperty(eO,"__esModule",{value:!0});eO.DSSEBundleBuilder=void 0;var bEt=wd(),PEt=HV(),xEt=GV(),qV=class extends PEt.BaseBundleBuilder{constructor(t){super(t),this.certificateChain=t.certificateChain??!1}async prepare(t){let r=Rve(t);return bEt.dsse.preAuthEncoding(r.type,r.data)}async package(t,r){return(0,xEt.toDSSEBundle)(Rve(t),r,this.certificateChain)}};eO.DSSEBundleBuilder=qV;function Rve(e){return{...e,type:e.type??""}}});var Fve=G(tO=>{"use strict";Object.defineProperty(tO,"__esModule",{value:!0});tO.MessageSignatureBundleBuilder=void 0;var kEt=HV(),QEt=GV(),WV=class extends kEt.BaseBundleBuilder{constructor(t){super(t)}async package(t,r){return(0,QEt.toMessageSignatureBundle)(t,r)}};tO.MessageSignatureBundleBuilder=WV});var Nve=G(qw=>{"use strict";Object.defineProperty(qw,"__esModule",{value:!0});qw.MessageSignatureBundleBuilder=qw.DSSEBundleBuilder=void 0;var REt=Tve();Object.defineProperty(qw,"DSSEBundleBuilder",{enumerable:!0,get:function(){return REt.DSSEBundleBuilder}});var TEt=Fve();Object.defineProperty(qw,"MessageSignatureBundleBuilder",{enumerable:!0,get:function(){return TEt.MessageSignatureBundleBuilder}})});var nO=G(rO=>{"use strict";Object.defineProperty(rO,"__esModule",{value:!0});rO.HTTPError=void 0;var YV=class extends Error{constructor({status:t,message:r,location:s}){super(`(${t}) ${r}`),this.statusCode=t,this.location=s}};rO.HTTPError=YV});var Ww=G(Db=>{"use strict";Object.defineProperty(Db,"__esModule",{value:!0});Db.InternalError=void 0;Db.internalError=NEt;var FEt=nO(),iO=class extends Error{constructor({code:t,message:r,cause:s}){super(r),this.name=this.constructor.name,this.cause=s,this.code=t}};Db.InternalError=iO;function NEt(e,t,r){throw e instanceof FEt.HTTPError&&(r+=` - ${e.message}`),new iO({code:t,message:r,cause:e})}});var sO=G((ker,Ove)=>{Ove.exports=fetch});var Lve=G(Yw=>{"use strict";var OEt=Yw&&Yw.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(Yw,"__esModule",{value:!0});Yw.CIContextProvider=void 0;var LEt=OEt(sO()),MEt=[UEt,_Et],VV=class{constructor(t="sigstore"){this.audience=t}async getToken(){return Promise.any(MEt.map(t=>t(this.audience))).catch(()=>Promise.reject("CI: no tokens available"))}};Yw.CIContextProvider=VV;async function UEt(e){if(!process.env.ACTIONS_ID_TOKEN_REQUEST_URL||!process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN)return Promise.reject("no token available");let t=new URL(process.env.ACTIONS_ID_TOKEN_REQUEST_URL);return t.searchParams.append("audience",e),(await(0,LEt.default)(t.href,{retry:2,headers:{Accept:"application/json",Authorization:`Bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}`}})).json().then(s=>s.value)}async function _Et(){return process.env.SIGSTORE_ID_TOKEN?process.env.SIGSTORE_ID_TOKEN:Promise.reject("no token available")}});var Mve=G(oO=>{"use strict";Object.defineProperty(oO,"__esModule",{value:!0});oO.CIContextProvider=void 0;var HEt=Lve();Object.defineProperty(oO,"CIContextProvider",{enumerable:!0,get:function(){return HEt.CIContextProvider}})});var _ve=G((Ter,Uve)=>{var jEt=Symbol("proc-log.meta");Uve.exports={META:jEt,output:{LEVELS:["standard","error","buffer","flush"],KEYS:{standard:"standard",error:"error",buffer:"buffer",flush:"flush"},standard:function(...e){return process.emit("output","standard",...e)},error:function(...e){return process.emit("output","error",...e)},buffer:function(...e){return process.emit("output","buffer",...e)},flush:function(...e){return process.emit("output","flush",...e)}},log:{LEVELS:["notice","error","warn","info","verbose","http","silly","timing","pause","resume"],KEYS:{notice:"notice",error:"error",warn:"warn",info:"info",verbose:"verbose",http:"http",silly:"silly",timing:"timing",pause:"pause",resume:"resume"},error:function(...e){return process.emit("log","error",...e)},notice:function(...e){return process.emit("log","notice",...e)},warn:function(...e){return process.emit("log","warn",...e)},info:function(...e){return process.emit("log","info",...e)},verbose:function(...e){return process.emit("log","verbose",...e)},http:function(...e){return process.emit("log","http",...e)},silly:function(...e){return process.emit("log","silly",...e)},timing:function(...e){return process.emit("log","timing",...e)},pause:function(){return process.emit("log","pause")},resume:function(){return process.emit("log","resume")}},time:{LEVELS:["start","end"],KEYS:{start:"start",end:"end"},start:function(e,t){process.emit("time","start",e);function r(){return process.emit("time","end",e)}if(typeof t=="function"){let s=t();return s&&s.finally?s.finally(r):(r(),s)}return r},end:function(e){return process.emit("time","end",e)}},input:{LEVELS:["start","end","read"],KEYS:{start:"start",end:"end",read:"read"},start:function(e){process.emit("input","start");function t(){return process.emit("input","end")}if(typeof e=="function"){let r=e();return r&&r.finally?r.finally(t):(t(),r)}return t},end:function(){return process.emit("input","end")},read:function(...e){let t,r,s=new Promise((a,n)=>{t=a,r=n});return process.emit("input","read",t,r,...e),s}}}});var Gve=G((Fer,jve)=>{"use strict";function Hve(e,t){for(let r in t)Object.defineProperty(e,r,{value:t[r],enumerable:!0,configurable:!0});return e}function GEt(e,t,r){if(!e||typeof e=="string")throw new TypeError("Please pass an Error to err-code");r||(r={}),typeof t=="object"&&(r=t,t=void 0),t!=null&&(r.code=t);try{return Hve(e,r)}catch{r.message=e.message,r.stack=e.stack;let a=function(){};return a.prototype=Object.create(Object.getPrototypeOf(e)),Hve(new a,r)}}jve.exports=GEt});var Wve=G((Ner,qve)=>{function Zc(e,t){typeof t=="boolean"&&(t={forever:t}),this._originalTimeouts=JSON.parse(JSON.stringify(e)),this._timeouts=e,this._options=t||{},this._maxRetryTime=t&&t.maxRetryTime||1/0,this._fn=null,this._errors=[],this._attempts=1,this._operationTimeout=null,this._operationTimeoutCb=null,this._timeout=null,this._operationStart=null,this._options.forever&&(this._cachedTimeouts=this._timeouts.slice(0))}qve.exports=Zc;Zc.prototype.reset=function(){this._attempts=1,this._timeouts=this._originalTimeouts};Zc.prototype.stop=function(){this._timeout&&clearTimeout(this._timeout),this._timeouts=[],this._cachedTimeouts=null};Zc.prototype.retry=function(e){if(this._timeout&&clearTimeout(this._timeout),!e)return!1;var t=new Date().getTime();if(e&&t-this._operationStart>=this._maxRetryTime)return this._errors.unshift(new Error("RetryOperation timeout occurred")),!1;this._errors.push(e);var r=this._timeouts.shift();if(r===void 0)if(this._cachedTimeouts)this._errors.splice(this._errors.length-1,this._errors.length),this._timeouts=this._cachedTimeouts.slice(0),r=this._timeouts.shift();else return!1;var s=this,a=setTimeout(function(){s._attempts++,s._operationTimeoutCb&&(s._timeout=setTimeout(function(){s._operationTimeoutCb(s._attempts)},s._operationTimeout),s._options.unref&&s._timeout.unref()),s._fn(s._attempts)},r);return this._options.unref&&a.unref(),!0};Zc.prototype.attempt=function(e,t){this._fn=e,t&&(t.timeout&&(this._operationTimeout=t.timeout),t.cb&&(this._operationTimeoutCb=t.cb));var r=this;this._operationTimeoutCb&&(this._timeout=setTimeout(function(){r._operationTimeoutCb()},r._operationTimeout)),this._operationStart=new Date().getTime(),this._fn(this._attempts)};Zc.prototype.try=function(e){console.log("Using RetryOperation.try() is deprecated"),this.attempt(e)};Zc.prototype.start=function(e){console.log("Using RetryOperation.start() is deprecated"),this.attempt(e)};Zc.prototype.start=Zc.prototype.try;Zc.prototype.errors=function(){return this._errors};Zc.prototype.attempts=function(){return this._attempts};Zc.prototype.mainError=function(){if(this._errors.length===0)return null;for(var e={},t=null,r=0,s=0;s=r&&(t=a,r=c)}return t}});var Yve=G(ay=>{var qEt=Wve();ay.operation=function(e){var t=ay.timeouts(e);return new qEt(t,{forever:e&&e.forever,unref:e&&e.unref,maxRetryTime:e&&e.maxRetryTime})};ay.timeouts=function(e){if(e instanceof Array)return[].concat(e);var t={retries:10,factor:2,minTimeout:1*1e3,maxTimeout:1/0,randomize:!1};for(var r in e)t[r]=e[r];if(t.minTimeout>t.maxTimeout)throw new Error("minTimeout is greater than maxTimeout");for(var s=[],a=0;a{Vve.exports=Yve()});var Xve=G((Mer,zve)=>{"use strict";var WEt=Gve(),YEt=Jve(),VEt=Object.prototype.hasOwnProperty;function Kve(e){return e&&e.code==="EPROMISERETRY"&&VEt.call(e,"retried")}function JEt(e,t){var r,s;return typeof e=="object"&&typeof t=="function"&&(r=t,t=e,e=r),s=YEt.operation(t),new Promise(function(a,n){s.attempt(function(c){Promise.resolve().then(function(){return e(function(f){throw Kve(f)&&(f=f.retried),WEt(new Error("Retrying"),"EPROMISERETRY",{retried:f})},c)}).then(a,function(f){Kve(f)&&(f=f.retried,s.retry(f||new Error))||n(f)})})})}zve.exports=JEt});var aO=G(bb=>{"use strict";var $ve=bb&&bb.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(bb,"__esModule",{value:!0});bb.fetchWithRetry=oIt;var KEt=Ie("http2"),zEt=$ve(sO()),Zve=_ve(),XEt=$ve(Xve()),ZEt=wd(),$Et=nO(),{HTTP2_HEADER_LOCATION:eIt,HTTP2_HEADER_CONTENT_TYPE:tIt,HTTP2_HEADER_USER_AGENT:rIt,HTTP_STATUS_INTERNAL_SERVER_ERROR:nIt,HTTP_STATUS_TOO_MANY_REQUESTS:iIt,HTTP_STATUS_REQUEST_TIMEOUT:sIt}=KEt.constants;async function oIt(e,t){return(0,XEt.default)(async(r,s)=>{let a=t.method||"POST",n={[rIt]:ZEt.ua.getUserAgent(),...t.headers},c=await(0,zEt.default)(e,{method:a,headers:n,body:t.body,timeout:t.timeout,retry:!1}).catch(f=>(Zve.log.http("fetch",`${a} ${e} attempt ${s} failed with ${f}`),r(f)));if(c.ok)return c;{let f=await aIt(c);if(Zve.log.http("fetch",`${a} ${e} attempt ${s} failed with ${c.status}`),lIt(c.status))return r(f);throw f}},cIt(t.retry))}var aIt=async e=>{let t=e.statusText,r=e.headers.get(eIt)||void 0;if(e.headers.get(tIt)?.includes("application/json"))try{t=(await e.json()).message||t}catch{}return new $Et.HTTPError({status:e.status,message:t,location:r})},lIt=e=>[sIt,iIt].includes(e)||e>=nIt,cIt=e=>typeof e=="boolean"?{retries:e?1:0}:typeof e=="number"?{retries:e}:{retries:0,...e}});var eSe=G(lO=>{"use strict";Object.defineProperty(lO,"__esModule",{value:!0});lO.Fulcio=void 0;var uIt=aO(),JV=class{constructor(t){this.options=t}async createSigningCertificate(t){let{baseURL:r,retry:s,timeout:a}=this.options,n=`${r}/api/v2/signingCert`;return(await(0,uIt.fetchWithRetry)(n,{headers:{"Content-Type":"application/json"},body:JSON.stringify(t),timeout:a,retry:s})).json()}};lO.Fulcio=JV});var tSe=G(cO=>{"use strict";Object.defineProperty(cO,"__esModule",{value:!0});cO.CAClient=void 0;var fIt=Ww(),AIt=eSe(),KV=class{constructor(t){this.fulcio=new AIt.Fulcio({baseURL:t.fulcioBaseURL,retry:t.retry,timeout:t.timeout})}async createSigningCertificate(t,r,s){let a=pIt(t,r,s);try{let n=await this.fulcio.createSigningCertificate(a);return(n.signedCertificateEmbeddedSct?n.signedCertificateEmbeddedSct:n.signedCertificateDetachedSct).chain.certificates}catch(n){(0,fIt.internalError)(n,"CA_CREATE_SIGNING_CERTIFICATE_ERROR","error creating signing certificate")}}};cO.CAClient=KV;function pIt(e,t,r){return{credentials:{oidcIdentityToken:e},publicKeyRequest:{publicKey:{algorithm:"ECDSA",content:t},proofOfPossession:r.toString("base64")}}}});var nSe=G(Vw=>{"use strict";var hIt=Vw&&Vw.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(Vw,"__esModule",{value:!0});Vw.EphemeralSigner=void 0;var rSe=hIt(Ie("crypto")),dIt="ec",gIt="P-256",zV=class{constructor(){this.keypair=rSe.default.generateKeyPairSync(dIt,{namedCurve:gIt})}async sign(t){let r=rSe.default.sign(null,t,this.keypair.privateKey),s=this.keypair.publicKey.export({format:"pem",type:"spki"}).toString("ascii");return{signature:r,key:{$case:"publicKey",publicKey:s}}}};Vw.EphemeralSigner=zV});var iSe=G(ly=>{"use strict";Object.defineProperty(ly,"__esModule",{value:!0});ly.FulcioSigner=ly.DEFAULT_FULCIO_URL=void 0;var XV=Ww(),mIt=wd(),yIt=tSe(),EIt=nSe();ly.DEFAULT_FULCIO_URL="https://fulcio.sigstore.dev";var ZV=class{constructor(t){this.ca=new yIt.CAClient({...t,fulcioBaseURL:t.fulcioBaseURL||ly.DEFAULT_FULCIO_URL}),this.identityProvider=t.identityProvider,this.keyHolder=t.keyHolder||new EIt.EphemeralSigner}async sign(t){let r=await this.getIdentityToken(),s;try{s=mIt.oidc.extractJWTSubject(r)}catch(f){throw new XV.InternalError({code:"IDENTITY_TOKEN_PARSE_ERROR",message:`invalid identity token: ${r}`,cause:f})}let a=await this.keyHolder.sign(Buffer.from(s));if(a.key.$case!=="publicKey")throw new XV.InternalError({code:"CA_CREATE_SIGNING_CERTIFICATE_ERROR",message:"unexpected format for signing key"});let n=await this.ca.createSigningCertificate(r,a.key.publicKey,a.signature);return{signature:(await this.keyHolder.sign(t)).signature,key:{$case:"x509Certificate",certificate:n[0]}}}async getIdentityToken(){try{return await this.identityProvider.getToken()}catch(t){throw new XV.InternalError({code:"IDENTITY_TOKEN_READ_ERROR",message:"error retrieving identity token",cause:t})}}};ly.FulcioSigner=ZV});var oSe=G(Jw=>{"use strict";Object.defineProperty(Jw,"__esModule",{value:!0});Jw.FulcioSigner=Jw.DEFAULT_FULCIO_URL=void 0;var sSe=iSe();Object.defineProperty(Jw,"DEFAULT_FULCIO_URL",{enumerable:!0,get:function(){return sSe.DEFAULT_FULCIO_URL}});Object.defineProperty(Jw,"FulcioSigner",{enumerable:!0,get:function(){return sSe.FulcioSigner}})});var cSe=G(uO=>{"use strict";Object.defineProperty(uO,"__esModule",{value:!0});uO.Rekor=void 0;var aSe=aO(),$V=class{constructor(t){this.options=t}async createEntry(t){let{baseURL:r,timeout:s,retry:a}=this.options,n=`${r}/api/v1/log/entries`,f=await(await(0,aSe.fetchWithRetry)(n,{headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(t),timeout:s,retry:a})).json();return lSe(f)}async getEntry(t){let{baseURL:r,timeout:s,retry:a}=this.options,n=`${r}/api/v1/log/entries/${t}`,f=await(await(0,aSe.fetchWithRetry)(n,{method:"GET",headers:{Accept:"application/json"},timeout:s,retry:a})).json();return lSe(f)}};uO.Rekor=$V;function lSe(e){let t=Object.entries(e);if(t.length!=1)throw new Error("Received multiple entries in Rekor response");let[r,s]=t[0];return{...s,uuid:r}}});var fSe=G(fO=>{"use strict";Object.defineProperty(fO,"__esModule",{value:!0});fO.TLogClient=void 0;var uSe=Ww(),IIt=nO(),CIt=cSe(),e7=class{constructor(t){this.fetchOnConflict=t.fetchOnConflict??!1,this.rekor=new CIt.Rekor({baseURL:t.rekorBaseURL,retry:t.retry,timeout:t.timeout})}async createEntry(t){let r;try{r=await this.rekor.createEntry(t)}catch(s){if(wIt(s)&&this.fetchOnConflict){let a=s.location.split("/").pop()||"";try{r=await this.rekor.getEntry(a)}catch(n){(0,uSe.internalError)(n,"TLOG_FETCH_ENTRY_ERROR","error fetching tlog entry")}}else(0,uSe.internalError)(s,"TLOG_CREATE_ENTRY_ERROR","error creating tlog entry")}return r}};fO.TLogClient=e7;function wIt(e){return e instanceof IIt.HTTPError&&e.statusCode===409&&e.location!==void 0}});var ASe=G(t7=>{"use strict";Object.defineProperty(t7,"__esModule",{value:!0});t7.toProposedEntry=vIt;var BIt=Ib(),Bd=wd(),Pb="sha256";function vIt(e,t,r="dsse"){switch(e.$case){case"dsseEnvelope":return r==="intoto"?bIt(e.dsseEnvelope,t):DIt(e.dsseEnvelope,t);case"messageSignature":return SIt(e.messageSignature,t)}}function SIt(e,t){let r=e.messageDigest.digest.toString("hex"),s=e.signature.toString("base64"),a=Bd.encoding.base64Encode(t);return{apiVersion:"0.0.1",kind:"hashedrekord",spec:{data:{hash:{algorithm:Pb,value:r}},signature:{content:s,publicKey:{content:a}}}}}function DIt(e,t){let r=JSON.stringify((0,BIt.envelopeToJSON)(e)),s=Bd.encoding.base64Encode(t);return{apiVersion:"0.0.1",kind:"dsse",spec:{proposedContent:{envelope:r,verifiers:[s]}}}}function bIt(e,t){let r=Bd.crypto.digest(Pb,e.payload).toString("hex"),s=PIt(e,t),a=Bd.encoding.base64Encode(e.payload.toString("base64")),n=Bd.encoding.base64Encode(e.signatures[0].sig.toString("base64")),c=e.signatures[0].keyid,f=Bd.encoding.base64Encode(t),p={payloadType:e.payloadType,payload:a,signatures:[{sig:n,publicKey:f}]};return c.length>0&&(p.signatures[0].keyid=c),{apiVersion:"0.0.2",kind:"intoto",spec:{content:{envelope:p,hash:{algorithm:Pb,value:s},payloadHash:{algorithm:Pb,value:r}}}}}function PIt(e,t){let r={payloadType:e.payloadType,payload:e.payload.toString("base64"),signatures:[{sig:e.signatures[0].sig.toString("base64"),publicKey:t}]};return e.signatures[0].keyid.length>0&&(r.signatures[0].keyid=e.signatures[0].keyid),Bd.crypto.digest(Pb,Bd.json.canonicalize(r)).toString("hex")}});var pSe=G(cy=>{"use strict";Object.defineProperty(cy,"__esModule",{value:!0});cy.RekorWitness=cy.DEFAULT_REKOR_URL=void 0;var xIt=wd(),kIt=fSe(),QIt=ASe();cy.DEFAULT_REKOR_URL="https://rekor.sigstore.dev";var r7=class{constructor(t){this.entryType=t.entryType,this.tlog=new kIt.TLogClient({...t,rekorBaseURL:t.rekorBaseURL||cy.DEFAULT_REKOR_URL})}async testify(t,r){let s=(0,QIt.toProposedEntry)(t,r,this.entryType),a=await this.tlog.createEntry(s);return RIt(a)}};cy.RekorWitness=r7;function RIt(e){let t=Buffer.from(e.logID,"hex"),r=xIt.encoding.base64Decode(e.body),s=JSON.parse(r),a=e?.verification?.signedEntryTimestamp?TIt(e.verification.signedEntryTimestamp):void 0,n=e?.verification?.inclusionProof?FIt(e.verification.inclusionProof):void 0;return{tlogEntries:[{logIndex:e.logIndex.toString(),logId:{keyId:t},integratedTime:e.integratedTime.toString(),kindVersion:{kind:s.kind,version:s.apiVersion},inclusionPromise:a,inclusionProof:n,canonicalizedBody:Buffer.from(e.body,"base64")}]}}function TIt(e){return{signedEntryTimestamp:Buffer.from(e,"base64")}}function FIt(e){return{logIndex:e.logIndex.toString(),treeSize:e.treeSize.toString(),rootHash:Buffer.from(e.rootHash,"hex"),hashes:e.hashes.map(t=>Buffer.from(t,"hex")),checkpoint:{envelope:e.checkpoint}}}});var hSe=G(AO=>{"use strict";Object.defineProperty(AO,"__esModule",{value:!0});AO.TimestampAuthority=void 0;var NIt=aO(),n7=class{constructor(t){this.options=t}async createTimestamp(t){let{baseURL:r,timeout:s,retry:a}=this.options,n=`${r}/api/v1/timestamp`;return(await(0,NIt.fetchWithRetry)(n,{headers:{"Content-Type":"application/json"},body:JSON.stringify(t),timeout:s,retry:a})).buffer()}};AO.TimestampAuthority=n7});var gSe=G(pO=>{"use strict";Object.defineProperty(pO,"__esModule",{value:!0});pO.TSAClient=void 0;var OIt=Ww(),LIt=hSe(),MIt=wd(),dSe="sha256",i7=class{constructor(t){this.tsa=new LIt.TimestampAuthority({baseURL:t.tsaBaseURL,retry:t.retry,timeout:t.timeout})}async createTimestamp(t){let r={artifactHash:MIt.crypto.digest(dSe,t).toString("base64"),hashAlgorithm:dSe};try{return await this.tsa.createTimestamp(r)}catch(s){(0,OIt.internalError)(s,"TSA_CREATE_TIMESTAMP_ERROR","error creating timestamp")}}};pO.TSAClient=i7});var mSe=G(hO=>{"use strict";Object.defineProperty(hO,"__esModule",{value:!0});hO.TSAWitness=void 0;var UIt=gSe(),s7=class{constructor(t){this.tsa=new UIt.TSAClient({tsaBaseURL:t.tsaBaseURL,retry:t.retry,timeout:t.timeout})}async testify(t){let r=_It(t);return{rfc3161Timestamps:[{signedTimestamp:await this.tsa.createTimestamp(r)}]}}};hO.TSAWitness=s7;function _It(e){switch(e.$case){case"dsseEnvelope":return e.dsseEnvelope.signatures[0].sig;case"messageSignature":return e.messageSignature.signature}}});var ESe=G(vd=>{"use strict";Object.defineProperty(vd,"__esModule",{value:!0});vd.TSAWitness=vd.RekorWitness=vd.DEFAULT_REKOR_URL=void 0;var ySe=pSe();Object.defineProperty(vd,"DEFAULT_REKOR_URL",{enumerable:!0,get:function(){return ySe.DEFAULT_REKOR_URL}});Object.defineProperty(vd,"RekorWitness",{enumerable:!0,get:function(){return ySe.RekorWitness}});var HIt=mSe();Object.defineProperty(vd,"TSAWitness",{enumerable:!0,get:function(){return HIt.TSAWitness}})});var a7=G(Cs=>{"use strict";Object.defineProperty(Cs,"__esModule",{value:!0});Cs.TSAWitness=Cs.RekorWitness=Cs.DEFAULT_REKOR_URL=Cs.FulcioSigner=Cs.DEFAULT_FULCIO_URL=Cs.CIContextProvider=Cs.InternalError=Cs.MessageSignatureBundleBuilder=Cs.DSSEBundleBuilder=void 0;var ISe=Nve();Object.defineProperty(Cs,"DSSEBundleBuilder",{enumerable:!0,get:function(){return ISe.DSSEBundleBuilder}});Object.defineProperty(Cs,"MessageSignatureBundleBuilder",{enumerable:!0,get:function(){return ISe.MessageSignatureBundleBuilder}});var jIt=Ww();Object.defineProperty(Cs,"InternalError",{enumerable:!0,get:function(){return jIt.InternalError}});var GIt=Mve();Object.defineProperty(Cs,"CIContextProvider",{enumerable:!0,get:function(){return GIt.CIContextProvider}});var CSe=oSe();Object.defineProperty(Cs,"DEFAULT_FULCIO_URL",{enumerable:!0,get:function(){return CSe.DEFAULT_FULCIO_URL}});Object.defineProperty(Cs,"FulcioSigner",{enumerable:!0,get:function(){return CSe.FulcioSigner}});var o7=ESe();Object.defineProperty(Cs,"DEFAULT_REKOR_URL",{enumerable:!0,get:function(){return o7.DEFAULT_REKOR_URL}});Object.defineProperty(Cs,"RekorWitness",{enumerable:!0,get:function(){return o7.RekorWitness}});Object.defineProperty(Cs,"TSAWitness",{enumerable:!0,get:function(){return o7.TSAWitness}})});var BSe=G(xb=>{"use strict";var wSe=xb&&xb.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(xb,"__esModule",{value:!0});xb.appDataPath=WIt;var qIt=wSe(Ie("os")),Kw=wSe(Ie("path"));function WIt(e){let t=qIt.default.homedir();switch(process.platform){case"darwin":{let r=Kw.default.join(t,"Library","Application Support");return Kw.default.join(r,e)}case"win32":{let r=process.env.LOCALAPPDATA||Kw.default.join(t,"AppData","Local");return Kw.default.join(r,e,"Data")}default:{let r=process.env.XDG_DATA_HOME||Kw.default.join(t,".local","share");return Kw.default.join(r,e)}}}});var kA=G(xl=>{"use strict";Object.defineProperty(xl,"__esModule",{value:!0});xl.UnsupportedAlgorithmError=xl.CryptoError=xl.LengthOrHashMismatchError=xl.UnsignedMetadataError=xl.RepositoryError=xl.ValueError=void 0;var l7=class extends Error{};xl.ValueError=l7;var kb=class extends Error{};xl.RepositoryError=kb;var c7=class extends kb{};xl.UnsignedMetadataError=c7;var u7=class extends kb{};xl.LengthOrHashMismatchError=u7;var dO=class extends Error{};xl.CryptoError=dO;var f7=class extends dO{};xl.UnsupportedAlgorithmError=f7});var SSe=G(Sd=>{"use strict";Object.defineProperty(Sd,"__esModule",{value:!0});Sd.isDefined=YIt;Sd.isObject=vSe;Sd.isStringArray=VIt;Sd.isObjectArray=JIt;Sd.isStringRecord=KIt;Sd.isObjectRecord=zIt;function YIt(e){return e!==void 0}function vSe(e){return typeof e=="object"&&e!==null}function VIt(e){return Array.isArray(e)&&e.every(t=>typeof t=="string")}function JIt(e){return Array.isArray(e)&&e.every(vSe)}function KIt(e){return typeof e=="object"&&e!==null&&Object.keys(e).every(t=>typeof t=="string")&&Object.values(e).every(t=>typeof t=="string")}function zIt(e){return typeof e=="object"&&e!==null&&Object.keys(e).every(t=>typeof t=="string")&&Object.values(e).every(t=>typeof t=="object"&&t!==null)}});var p7=G((ntr,PSe)=>{var DSe=",",XIt=":",ZIt="[",$It="]",eCt="{",tCt="}";function A7(e){let t=[];if(typeof e=="string")t.push(bSe(e));else if(typeof e=="boolean")t.push(JSON.stringify(e));else if(Number.isInteger(e))t.push(JSON.stringify(e));else if(e===null)t.push(JSON.stringify(e));else if(Array.isArray(e)){t.push(ZIt);let r=!0;e.forEach(s=>{r||t.push(DSe),r=!1,t.push(A7(s))}),t.push($It)}else if(typeof e=="object"){t.push(eCt);let r=!0;Object.keys(e).sort().forEach(s=>{r||t.push(DSe),r=!1,t.push(bSe(s)),t.push(XIt),t.push(A7(e[s]))}),t.push(tCt)}else throw new TypeError("cannot encode "+e.toString());return t.join("")}function bSe(e){return'"'+e.replace(/\\/g,"\\\\").replace(/"/g,'\\"')+'"'}PSe.exports={canonicalize:A7}});var xSe=G(zw=>{"use strict";var rCt=zw&&zw.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(zw,"__esModule",{value:!0});zw.verifySignature=void 0;var nCt=p7(),iCt=rCt(Ie("crypto")),sCt=(e,t,r)=>{let s=Buffer.from((0,nCt.canonicalize)(e));return iCt.default.verify(void 0,s,t,Buffer.from(r,"hex"))};zw.verifySignature=sCt});var hf=G($c=>{"use strict";var oCt=$c&&$c.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(t,r);(!a||("get"in a?!t.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,s,a)}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),aCt=$c&&$c.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),kSe=$c&&$c.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!=="default"&&Object.prototype.hasOwnProperty.call(e,r)&&oCt(t,e,r);return aCt(t,e),t};Object.defineProperty($c,"__esModule",{value:!0});$c.crypto=$c.guard=void 0;$c.guard=kSe(SSe());$c.crypto=kSe(xSe())});var uy=G(ph=>{"use strict";var lCt=ph&&ph.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(ph,"__esModule",{value:!0});ph.Signed=ph.MetadataKind=void 0;ph.isMetadataKind=uCt;var cCt=lCt(Ie("util")),Qb=kA(),h7=hf(),QSe=["1","0","31"],d7;(function(e){e.Root="root",e.Timestamp="timestamp",e.Snapshot="snapshot",e.Targets="targets"})(d7||(ph.MetadataKind=d7={}));function uCt(e){return typeof e=="string"&&Object.values(d7).includes(e)}var g7=class e{constructor(t){this.specVersion=t.specVersion||QSe.join(".");let r=this.specVersion.split(".");if(!(r.length===2||r.length===3)||!r.every(s=>fCt(s)))throw new Qb.ValueError("Failed to parse specVersion");if(r[0]!=QSe[0])throw new Qb.ValueError("Unsupported specVersion");this.expires=t.expires,this.version=t.version,this.unrecognizedFields=t.unrecognizedFields||{}}equals(t){return t instanceof e?this.specVersion===t.specVersion&&this.expires===t.expires&&this.version===t.version&&cCt.default.isDeepStrictEqual(this.unrecognizedFields,t.unrecognizedFields):!1}isExpired(t){return t||(t=new Date),t>=new Date(this.expires)}static commonFieldsFromJSON(t){let{spec_version:r,expires:s,version:a,...n}=t;if(h7.guard.isDefined(r)){if(typeof r!="string")throw new TypeError("spec_version must be a string")}else throw new Qb.ValueError("spec_version is not defined");if(h7.guard.isDefined(s)){if(typeof s!="string")throw new TypeError("expires must be a string")}else throw new Qb.ValueError("expires is not defined");if(h7.guard.isDefined(a)){if(typeof a!="number")throw new TypeError("version must be a number")}else throw new Qb.ValueError("version is not defined");return{specVersion:r,expires:s,version:a,unrecognizedFields:n}}};ph.Signed=g7;function fCt(e){return!isNaN(Number(e))}});var Rb=G(bd=>{"use strict";var RSe=bd&&bd.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(bd,"__esModule",{value:!0});bd.TargetFile=bd.MetaFile=void 0;var TSe=RSe(Ie("crypto")),mO=RSe(Ie("util")),Dd=kA(),gO=hf(),m7=class e{constructor(t){if(t.version<=0)throw new Dd.ValueError("Metafile version must be at least 1");t.length!==void 0&&FSe(t.length),this.version=t.version,this.length=t.length,this.hashes=t.hashes,this.unrecognizedFields=t.unrecognizedFields||{}}equals(t){return t instanceof e?this.version===t.version&&this.length===t.length&&mO.default.isDeepStrictEqual(this.hashes,t.hashes)&&mO.default.isDeepStrictEqual(this.unrecognizedFields,t.unrecognizedFields):!1}verify(t){if(this.length!==void 0&&t.length!==this.length)throw new Dd.LengthOrHashMismatchError(`Expected length ${this.length} but got ${t.length}`);this.hashes&&Object.entries(this.hashes).forEach(([r,s])=>{let a;try{a=TSe.default.createHash(r)}catch{throw new Dd.LengthOrHashMismatchError(`Hash algorithm ${r} not supported`)}let n=a.update(t).digest("hex");if(n!==s)throw new Dd.LengthOrHashMismatchError(`Expected hash ${s} but got ${n}`)})}toJSON(){let t={version:this.version,...this.unrecognizedFields};return this.length!==void 0&&(t.length=this.length),this.hashes&&(t.hashes=this.hashes),t}static fromJSON(t){let{version:r,length:s,hashes:a,...n}=t;if(typeof r!="number")throw new TypeError("version must be a number");if(gO.guard.isDefined(s)&&typeof s!="number")throw new TypeError("length must be a number");if(gO.guard.isDefined(a)&&!gO.guard.isStringRecord(a))throw new TypeError("hashes must be string keys and values");return new e({version:r,length:s,hashes:a,unrecognizedFields:n})}};bd.MetaFile=m7;var y7=class e{constructor(t){FSe(t.length),this.length=t.length,this.path=t.path,this.hashes=t.hashes,this.unrecognizedFields=t.unrecognizedFields||{}}get custom(){let t=this.unrecognizedFields.custom;return!t||Array.isArray(t)||typeof t!="object"?{}:t}equals(t){return t instanceof e?this.length===t.length&&this.path===t.path&&mO.default.isDeepStrictEqual(this.hashes,t.hashes)&&mO.default.isDeepStrictEqual(this.unrecognizedFields,t.unrecognizedFields):!1}async verify(t){let r=0,s=Object.keys(this.hashes).reduce((a,n)=>{try{a[n]=TSe.default.createHash(n)}catch{throw new Dd.LengthOrHashMismatchError(`Hash algorithm ${n} not supported`)}return a},{});for await(let a of t)r+=a.length,Object.values(s).forEach(n=>{n.update(a)});if(r!==this.length)throw new Dd.LengthOrHashMismatchError(`Expected length ${this.length} but got ${r}`);Object.entries(s).forEach(([a,n])=>{let c=this.hashes[a],f=n.digest("hex");if(f!==c)throw new Dd.LengthOrHashMismatchError(`Expected hash ${c} but got ${f}`)})}toJSON(){return{length:this.length,hashes:this.hashes,...this.unrecognizedFields}}static fromJSON(t,r){let{length:s,hashes:a,...n}=r;if(typeof s!="number")throw new TypeError("length must be a number");if(!gO.guard.isStringRecord(a))throw new TypeError("hashes must have string keys and values");return new e({length:s,path:t,hashes:a,unrecognizedFields:n})}};bd.TargetFile=y7;function FSe(e){if(e<0)throw new Dd.ValueError("Length must be at least 0")}});var NSe=G(E7=>{"use strict";Object.defineProperty(E7,"__esModule",{value:!0});E7.encodeOIDString=pCt;var ACt=6;function pCt(e){let t=e.split("."),r=parseInt(t[0],10)*40+parseInt(t[1],10),s=[];t.slice(2).forEach(n=>{let c=hCt(parseInt(n,10));s.push(...c)});let a=Buffer.from([r,...s]);return Buffer.from([ACt,a.length,...a])}function hCt(e){let t=[],r=0;for(;e>0;)t.unshift(e&127|r),e>>=7,r=128;return t}});var USe=G(Fb=>{"use strict";var dCt=Fb&&Fb.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(Fb,"__esModule",{value:!0});Fb.getPublicKey=ECt;var Xw=dCt(Ie("crypto")),Tb=kA(),I7=NSe(),yO=48,OSe=3,LSe=0,gCt="1.3.101.112",mCt="1.2.840.10045.2.1",yCt="1.2.840.10045.3.1.7",C7="-----BEGIN PUBLIC KEY-----";function ECt(e){switch(e.keyType){case"rsa":return ICt(e);case"ed25519":return CCt(e);case"ecdsa":case"ecdsa-sha2-nistp256":case"ecdsa-sha2-nistp384":return wCt(e);default:throw new Tb.UnsupportedAlgorithmError(`Unsupported key type: ${e.keyType}`)}}function ICt(e){if(!e.keyVal.startsWith(C7))throw new Tb.CryptoError("Invalid key format");let t=Xw.default.createPublicKey(e.keyVal);switch(e.scheme){case"rsassa-pss-sha256":return{key:t,padding:Xw.default.constants.RSA_PKCS1_PSS_PADDING};default:throw new Tb.UnsupportedAlgorithmError(`Unsupported RSA scheme: ${e.scheme}`)}}function CCt(e){let t;if(e.keyVal.startsWith(C7))t=Xw.default.createPublicKey(e.keyVal);else{if(!MSe(e.keyVal))throw new Tb.CryptoError("Invalid key format");t=Xw.default.createPublicKey({key:BCt.hexToDER(e.keyVal),format:"der",type:"spki"})}return{key:t}}function wCt(e){let t;if(e.keyVal.startsWith(C7))t=Xw.default.createPublicKey(e.keyVal);else{if(!MSe(e.keyVal))throw new Tb.CryptoError("Invalid key format");t=Xw.default.createPublicKey({key:vCt.hexToDER(e.keyVal),format:"der",type:"spki"})}return{key:t}}var BCt={hexToDER:e=>{let t=Buffer.from(e,"hex"),r=(0,I7.encodeOIDString)(gCt),s=Buffer.concat([Buffer.concat([Buffer.from([yO]),Buffer.from([r.length]),r]),Buffer.concat([Buffer.from([OSe]),Buffer.from([t.length+1]),Buffer.from([LSe]),t])]);return Buffer.concat([Buffer.from([yO]),Buffer.from([s.length]),s])}},vCt={hexToDER:e=>{let t=Buffer.from(e,"hex"),r=Buffer.concat([Buffer.from([OSe]),Buffer.from([t.length+1]),Buffer.from([LSe]),t]),s=Buffer.concat([(0,I7.encodeOIDString)(mCt),(0,I7.encodeOIDString)(yCt)]),a=Buffer.concat([Buffer.from([yO]),Buffer.from([s.length]),s]);return Buffer.concat([Buffer.from([yO]),Buffer.from([a.length+r.length]),a,r])}},MSe=e=>/^[0-9a-fA-F]+$/.test(e)});var EO=G(Zw=>{"use strict";var SCt=Zw&&Zw.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(Zw,"__esModule",{value:!0});Zw.Key=void 0;var _Se=SCt(Ie("util")),Nb=kA(),HSe=hf(),DCt=USe(),w7=class e{constructor(t){let{keyID:r,keyType:s,scheme:a,keyVal:n,unrecognizedFields:c}=t;this.keyID=r,this.keyType=s,this.scheme=a,this.keyVal=n,this.unrecognizedFields=c||{}}verifySignature(t){let r=t.signatures[this.keyID];if(!r)throw new Nb.UnsignedMetadataError("no signature for key found in metadata");if(!this.keyVal.public)throw new Nb.UnsignedMetadataError("no public key found");let s=(0,DCt.getPublicKey)({keyType:this.keyType,scheme:this.scheme,keyVal:this.keyVal.public}),a=t.signed.toJSON();try{if(!HSe.crypto.verifySignature(a,s,r.sig))throw new Nb.UnsignedMetadataError(`failed to verify ${this.keyID} signature`)}catch(n){throw n instanceof Nb.UnsignedMetadataError?n:new Nb.UnsignedMetadataError(`failed to verify ${this.keyID} signature`)}}equals(t){return t instanceof e?this.keyID===t.keyID&&this.keyType===t.keyType&&this.scheme===t.scheme&&_Se.default.isDeepStrictEqual(this.keyVal,t.keyVal)&&_Se.default.isDeepStrictEqual(this.unrecognizedFields,t.unrecognizedFields):!1}toJSON(){return{keytype:this.keyType,scheme:this.scheme,keyval:this.keyVal,...this.unrecognizedFields}}static fromJSON(t,r){let{keytype:s,scheme:a,keyval:n,...c}=r;if(typeof s!="string")throw new TypeError("keytype must be a string");if(typeof a!="string")throw new TypeError("scheme must be a string");if(!HSe.guard.isStringRecord(n))throw new TypeError("keyval must be a string record");return new e({keyID:t,keyType:s,scheme:a,keyVal:n,unrecognizedFields:c})}};Zw.Key=w7});var YSe=G((ftr,WSe)=>{"use strict";WSe.exports=GSe;function GSe(e,t,r){e instanceof RegExp&&(e=jSe(e,r)),t instanceof RegExp&&(t=jSe(t,r));var s=qSe(e,t,r);return s&&{start:s[0],end:s[1],pre:r.slice(0,s[0]),body:r.slice(s[0]+e.length,s[1]),post:r.slice(s[1]+t.length)}}function jSe(e,t){var r=t.match(e);return r?r[0]:null}GSe.range=qSe;function qSe(e,t,r){var s,a,n,c,f,p=r.indexOf(e),h=r.indexOf(t,p+1),E=p;if(p>=0&&h>0){for(s=[],n=r.length;E>=0&&!f;)E==p?(s.push(E),p=r.indexOf(e,E+1)):s.length==1?f=[s.pop(),h]:(a=s.pop(),a=0?p:h;s.length&&(f=[n,c])}return f}});var eDe=G((Atr,$Se)=>{var VSe=YSe();$Se.exports=xCt;var JSe="\0SLASH"+Math.random()+"\0",KSe="\0OPEN"+Math.random()+"\0",v7="\0CLOSE"+Math.random()+"\0",zSe="\0COMMA"+Math.random()+"\0",XSe="\0PERIOD"+Math.random()+"\0";function B7(e){return parseInt(e,10)==e?parseInt(e,10):e.charCodeAt(0)}function bCt(e){return e.split("\\\\").join(JSe).split("\\{").join(KSe).split("\\}").join(v7).split("\\,").join(zSe).split("\\.").join(XSe)}function PCt(e){return e.split(JSe).join("\\").split(KSe).join("{").split(v7).join("}").split(zSe).join(",").split(XSe).join(".")}function ZSe(e){if(!e)return[""];var t=[],r=VSe("{","}",e);if(!r)return e.split(",");var s=r.pre,a=r.body,n=r.post,c=s.split(",");c[c.length-1]+="{"+a+"}";var f=ZSe(n);return n.length&&(c[c.length-1]+=f.shift(),c.push.apply(c,f)),t.push.apply(t,c),t}function xCt(e){return e?(e.substr(0,2)==="{}"&&(e="\\{\\}"+e.substr(2)),Ob(bCt(e),!0).map(PCt)):[]}function kCt(e){return"{"+e+"}"}function QCt(e){return/^-?0\d/.test(e)}function RCt(e,t){return e<=t}function TCt(e,t){return e>=t}function Ob(e,t){var r=[],s=VSe("{","}",e);if(!s)return[e];var a=s.pre,n=s.post.length?Ob(s.post,!1):[""];if(/\$$/.test(s.pre))for(var c=0;c=0;if(!E&&!C)return s.post.match(/,.*\}/)?(e=s.pre+"{"+s.body+v7+s.post,Ob(e)):[e];var S;if(E)S=s.body.split(/\.\./);else if(S=ZSe(s.body),S.length===1&&(S=Ob(S[0],!1).map(kCt),S.length===1))return n.map(function(Ee){return s.pre+S[0]+Ee});var x;if(E){var I=B7(S[0]),T=B7(S[1]),O=Math.max(S[0].length,S[1].length),U=S.length==3?Math.abs(B7(S[2])):1,V=RCt,te=T0){var Ae=new Array(ge+1).join("0");ue<0?ae="-"+Ae+ae.slice(1):ae=Ae+ae}}x.push(ae)}}else{x=[];for(var Ce=0;Ce{"use strict";Object.defineProperty(IO,"__esModule",{value:!0});IO.assertValidPattern=void 0;var FCt=1024*64,NCt=e=>{if(typeof e!="string")throw new TypeError("invalid pattern");if(e.length>FCt)throw new TypeError("pattern is too long")};IO.assertValidPattern=NCt});var nDe=G(CO=>{"use strict";Object.defineProperty(CO,"__esModule",{value:!0});CO.parseClass=void 0;var OCt={"[:alnum:]":["\\p{L}\\p{Nl}\\p{Nd}",!0],"[:alpha:]":["\\p{L}\\p{Nl}",!0],"[:ascii:]":["\\x00-\\x7f",!1],"[:blank:]":["\\p{Zs}\\t",!0],"[:cntrl:]":["\\p{Cc}",!0],"[:digit:]":["\\p{Nd}",!0],"[:graph:]":["\\p{Z}\\p{C}",!0,!0],"[:lower:]":["\\p{Ll}",!0],"[:print:]":["\\p{C}",!0],"[:punct:]":["\\p{P}",!0],"[:space:]":["\\p{Z}\\t\\r\\n\\v\\f",!0],"[:upper:]":["\\p{Lu}",!0],"[:word:]":["\\p{L}\\p{Nl}\\p{Nd}\\p{Pc}",!0],"[:xdigit:]":["A-Fa-f0-9",!1]},Lb=e=>e.replace(/[[\]\\-]/g,"\\$&"),LCt=e=>e.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&"),rDe=e=>e.join(""),MCt=(e,t)=>{let r=t;if(e.charAt(r)!=="[")throw new Error("not in a brace expression");let s=[],a=[],n=r+1,c=!1,f=!1,p=!1,h=!1,E=r,C="";e:for(;nC?s.push(Lb(C)+"-"+Lb(T)):T===C&&s.push(Lb(T)),C="",n++;continue}if(e.startsWith("-]",n+1)){s.push(Lb(T+"-")),n+=2;continue}if(e.startsWith("-",n+1)){C=T,n+=2;continue}s.push(Lb(T)),n++}if(E{"use strict";Object.defineProperty(wO,"__esModule",{value:!0});wO.unescape=void 0;var UCt=(e,{windowsPathsNoEscape:t=!1}={})=>t?e.replace(/\[([^\/\\])\]/g,"$1"):e.replace(/((?!\\).|^)\[([^\/\\])\]/g,"$1$2").replace(/\\([^\/])/g,"$1");wO.unescape=UCt});var b7=G(DO=>{"use strict";Object.defineProperty(DO,"__esModule",{value:!0});DO.AST=void 0;var _Ct=nDe(),vO=BO(),HCt=new Set(["!","?","+","*","@"]),iDe=e=>HCt.has(e),jCt="(?!(?:^|/)\\.\\.?(?:$|/))",SO="(?!\\.)",GCt=new Set(["[","."]),qCt=new Set(["..","."]),WCt=new Set("().*{}+?[]^$\\!"),YCt=e=>e.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&"),D7="[^/]",sDe=D7+"*?",oDe=D7+"+?",S7=class e{type;#e;#t;#s=!1;#r=[];#i;#n;#o;#l=!1;#a;#c;#f=!1;constructor(t,r,s={}){this.type=t,t&&(this.#t=!0),this.#i=r,this.#e=this.#i?this.#i.#e:this,this.#a=this.#e===this?s:this.#e.#a,this.#o=this.#e===this?[]:this.#e.#o,t==="!"&&!this.#e.#l&&this.#o.push(this),this.#n=this.#i?this.#i.#r.length:0}get hasMagic(){if(this.#t!==void 0)return this.#t;for(let t of this.#r)if(typeof t!="string"&&(t.type||t.hasMagic))return this.#t=!0;return this.#t}toString(){return this.#c!==void 0?this.#c:this.type?this.#c=this.type+"("+this.#r.map(t=>String(t)).join("|")+")":this.#c=this.#r.map(t=>String(t)).join("")}#p(){if(this!==this.#e)throw new Error("should only call on root");if(this.#l)return this;this.toString(),this.#l=!0;let t;for(;t=this.#o.pop();){if(t.type!=="!")continue;let r=t,s=r.#i;for(;s;){for(let a=r.#n+1;!s.type&&atypeof r=="string"?r:r.toJSON()):[this.type,...this.#r.map(r=>r.toJSON())];return this.isStart()&&!this.type&&t.unshift([]),this.isEnd()&&(this===this.#e||this.#e.#l&&this.#i?.type==="!")&&t.push({}),t}isStart(){if(this.#e===this)return!0;if(!this.#i?.isStart())return!1;if(this.#n===0)return!0;let t=this.#i;for(let r=0;r{let[I,T,O,U]=typeof x=="string"?e.#h(x,this.#t,p):x.toRegExpSource(t);return this.#t=this.#t||O,this.#s=this.#s||U,I}).join(""),E="";if(this.isStart()&&typeof this.#r[0]=="string"&&!(this.#r.length===1&&qCt.has(this.#r[0]))){let I=GCt,T=r&&I.has(h.charAt(0))||h.startsWith("\\.")&&I.has(h.charAt(2))||h.startsWith("\\.\\.")&&I.has(h.charAt(4)),O=!r&&!t&&I.has(h.charAt(0));E=T?jCt:O?SO:""}let C="";return this.isEnd()&&this.#e.#l&&this.#i?.type==="!"&&(C="(?:$|\\/)"),[E+h+C,(0,vO.unescape)(h),this.#t=!!this.#t,this.#s]}let s=this.type==="*"||this.type==="+",a=this.type==="!"?"(?:(?!(?:":"(?:",n=this.#A(r);if(this.isStart()&&this.isEnd()&&!n&&this.type!=="!"){let p=this.toString();return this.#r=[p],this.type=null,this.#t=void 0,[p,(0,vO.unescape)(this.toString()),!1,!1]}let c=!s||t||r||!SO?"":this.#A(!0);c===n&&(c=""),c&&(n=`(?:${n})(?:${c})*?`);let f="";if(this.type==="!"&&this.#f)f=(this.isStart()&&!r?SO:"")+oDe;else{let p=this.type==="!"?"))"+(this.isStart()&&!r&&!t?SO:"")+sDe+")":this.type==="@"?")":this.type==="?"?")?":this.type==="+"&&c?")":this.type==="*"&&c?")?":`)${this.type}`;f=a+n+p}return[f,(0,vO.unescape)(n),this.#t=!!this.#t,this.#s]}#A(t){return this.#r.map(r=>{if(typeof r=="string")throw new Error("string type in extglob ast??");let[s,a,n,c]=r.toRegExpSource(t);return this.#s=this.#s||c,s}).filter(r=>!(this.isStart()&&this.isEnd())||!!r).join("|")}static#h(t,r,s=!1){let a=!1,n="",c=!1;for(let f=0;f{"use strict";Object.defineProperty(bO,"__esModule",{value:!0});bO.escape=void 0;var VCt=(e,{windowsPathsNoEscape:t=!1}={})=>t?e.replace(/[?*()[\]]/g,"[$&]"):e.replace(/[?*()[\]\\]/g,"\\$&");bO.escape=VCt});var pDe=G(pr=>{"use strict";var JCt=pr&&pr.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(pr,"__esModule",{value:!0});pr.unescape=pr.escape=pr.AST=pr.Minimatch=pr.match=pr.makeRe=pr.braceExpand=pr.defaults=pr.filter=pr.GLOBSTAR=pr.sep=pr.minimatch=void 0;var KCt=JCt(eDe()),PO=tDe(),cDe=b7(),zCt=P7(),XCt=BO(),ZCt=(e,t,r={})=>((0,PO.assertValidPattern)(t),!r.nocomment&&t.charAt(0)==="#"?!1:new fy(t,r).match(e));pr.minimatch=ZCt;var $Ct=/^\*+([^+@!?\*\[\(]*)$/,ewt=e=>t=>!t.startsWith(".")&&t.endsWith(e),twt=e=>t=>t.endsWith(e),rwt=e=>(e=e.toLowerCase(),t=>!t.startsWith(".")&&t.toLowerCase().endsWith(e)),nwt=e=>(e=e.toLowerCase(),t=>t.toLowerCase().endsWith(e)),iwt=/^\*+\.\*+$/,swt=e=>!e.startsWith(".")&&e.includes("."),owt=e=>e!=="."&&e!==".."&&e.includes("."),awt=/^\.\*+$/,lwt=e=>e!=="."&&e!==".."&&e.startsWith("."),cwt=/^\*+$/,uwt=e=>e.length!==0&&!e.startsWith("."),fwt=e=>e.length!==0&&e!=="."&&e!=="..",Awt=/^\?+([^+@!?\*\[\(]*)?$/,pwt=([e,t=""])=>{let r=uDe([e]);return t?(t=t.toLowerCase(),s=>r(s)&&s.toLowerCase().endsWith(t)):r},hwt=([e,t=""])=>{let r=fDe([e]);return t?(t=t.toLowerCase(),s=>r(s)&&s.toLowerCase().endsWith(t)):r},dwt=([e,t=""])=>{let r=fDe([e]);return t?s=>r(s)&&s.endsWith(t):r},gwt=([e,t=""])=>{let r=uDe([e]);return t?s=>r(s)&&s.endsWith(t):r},uDe=([e])=>{let t=e.length;return r=>r.length===t&&!r.startsWith(".")},fDe=([e])=>{let t=e.length;return r=>r.length===t&&r!=="."&&r!==".."},ADe=typeof process=="object"&&process?typeof process.env=="object"&&process.env&&process.env.__MINIMATCH_TESTING_PLATFORM__||process.platform:"posix",aDe={win32:{sep:"\\"},posix:{sep:"/"}};pr.sep=ADe==="win32"?aDe.win32.sep:aDe.posix.sep;pr.minimatch.sep=pr.sep;pr.GLOBSTAR=Symbol("globstar **");pr.minimatch.GLOBSTAR=pr.GLOBSTAR;var mwt="[^/]",ywt=mwt+"*?",Ewt="(?:(?!(?:\\/|^)(?:\\.{1,2})($|\\/)).)*?",Iwt="(?:(?!(?:\\/|^)\\.).)*?",Cwt=(e,t={})=>r=>(0,pr.minimatch)(r,e,t);pr.filter=Cwt;pr.minimatch.filter=pr.filter;var eu=(e,t={})=>Object.assign({},e,t),wwt=e=>{if(!e||typeof e!="object"||!Object.keys(e).length)return pr.minimatch;let t=pr.minimatch;return Object.assign((s,a,n={})=>t(s,a,eu(e,n)),{Minimatch:class extends t.Minimatch{constructor(a,n={}){super(a,eu(e,n))}static defaults(a){return t.defaults(eu(e,a)).Minimatch}},AST:class extends t.AST{constructor(a,n,c={}){super(a,n,eu(e,c))}static fromGlob(a,n={}){return t.AST.fromGlob(a,eu(e,n))}},unescape:(s,a={})=>t.unescape(s,eu(e,a)),escape:(s,a={})=>t.escape(s,eu(e,a)),filter:(s,a={})=>t.filter(s,eu(e,a)),defaults:s=>t.defaults(eu(e,s)),makeRe:(s,a={})=>t.makeRe(s,eu(e,a)),braceExpand:(s,a={})=>t.braceExpand(s,eu(e,a)),match:(s,a,n={})=>t.match(s,a,eu(e,n)),sep:t.sep,GLOBSTAR:pr.GLOBSTAR})};pr.defaults=wwt;pr.minimatch.defaults=pr.defaults;var Bwt=(e,t={})=>((0,PO.assertValidPattern)(e),t.nobrace||!/\{(?:(?!\{).)*\}/.test(e)?[e]:(0,KCt.default)(e));pr.braceExpand=Bwt;pr.minimatch.braceExpand=pr.braceExpand;var vwt=(e,t={})=>new fy(e,t).makeRe();pr.makeRe=vwt;pr.minimatch.makeRe=pr.makeRe;var Swt=(e,t,r={})=>{let s=new fy(t,r);return e=e.filter(a=>s.match(a)),s.options.nonull&&!e.length&&e.push(t),e};pr.match=Swt;pr.minimatch.match=pr.match;var lDe=/[?*]|[+@!]\(.*?\)|\[|\]/,Dwt=e=>e.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&"),fy=class{options;set;pattern;windowsPathsNoEscape;nonegate;negate;comment;empty;preserveMultipleSlashes;partial;globSet;globParts;nocase;isWindows;platform;windowsNoMagicRoot;regexp;constructor(t,r={}){(0,PO.assertValidPattern)(t),r=r||{},this.options=r,this.pattern=t,this.platform=r.platform||ADe,this.isWindows=this.platform==="win32",this.windowsPathsNoEscape=!!r.windowsPathsNoEscape||r.allowWindowsEscape===!1,this.windowsPathsNoEscape&&(this.pattern=this.pattern.replace(/\\/g,"/")),this.preserveMultipleSlashes=!!r.preserveMultipleSlashes,this.regexp=null,this.negate=!1,this.nonegate=!!r.nonegate,this.comment=!1,this.empty=!1,this.partial=!!r.partial,this.nocase=!!this.options.nocase,this.windowsNoMagicRoot=r.windowsNoMagicRoot!==void 0?r.windowsNoMagicRoot:!!(this.isWindows&&this.nocase),this.globSet=[],this.globParts=[],this.set=[],this.make()}hasMagic(){if(this.options.magicalBraces&&this.set.length>1)return!0;for(let t of this.set)for(let r of t)if(typeof r!="string")return!0;return!1}debug(...t){}make(){let t=this.pattern,r=this.options;if(!r.nocomment&&t.charAt(0)==="#"){this.comment=!0;return}if(!t){this.empty=!0;return}this.parseNegate(),this.globSet=[...new Set(this.braceExpand())],r.debug&&(this.debug=(...n)=>console.error(...n)),this.debug(this.pattern,this.globSet);let s=this.globSet.map(n=>this.slashSplit(n));this.globParts=this.preprocess(s),this.debug(this.pattern,this.globParts);let a=this.globParts.map((n,c,f)=>{if(this.isWindows&&this.windowsNoMagicRoot){let p=n[0]===""&&n[1]===""&&(n[2]==="?"||!lDe.test(n[2]))&&!lDe.test(n[3]),h=/^[a-z]:/i.test(n[0]);if(p)return[...n.slice(0,4),...n.slice(4).map(E=>this.parse(E))];if(h)return[n[0],...n.slice(1).map(E=>this.parse(E))]}return n.map(p=>this.parse(p))});if(this.debug(this.pattern,a),this.set=a.filter(n=>n.indexOf(!1)===-1),this.isWindows)for(let n=0;n=2?(t=this.firstPhasePreProcess(t),t=this.secondPhasePreProcess(t)):r>=1?t=this.levelOneOptimize(t):t=this.adjascentGlobstarOptimize(t),t}adjascentGlobstarOptimize(t){return t.map(r=>{let s=-1;for(;(s=r.indexOf("**",s+1))!==-1;){let a=s;for(;r[a+1]==="**";)a++;a!==s&&r.splice(s,a-s)}return r})}levelOneOptimize(t){return t.map(r=>(r=r.reduce((s,a)=>{let n=s[s.length-1];return a==="**"&&n==="**"?s:a===".."&&n&&n!==".."&&n!=="."&&n!=="**"?(s.pop(),s):(s.push(a),s)},[]),r.length===0?[""]:r))}levelTwoFileOptimize(t){Array.isArray(t)||(t=this.slashSplit(t));let r=!1;do{if(r=!1,!this.preserveMultipleSlashes){for(let a=1;aa&&s.splice(a+1,c-a);let f=s[a+1],p=s[a+2],h=s[a+3];if(f!==".."||!p||p==="."||p===".."||!h||h==="."||h==="..")continue;r=!0,s.splice(a,1);let E=s.slice(0);E[a]="**",t.push(E),a--}if(!this.preserveMultipleSlashes){for(let c=1;cr.length)}partsMatch(t,r,s=!1){let a=0,n=0,c=[],f="";for(;ate?r=r.slice(ie):te>ie&&(t=t.slice(te)))}}let{optimizationLevel:n=1}=this.options;n>=2&&(t=this.levelTwoFileOptimize(t)),this.debug("matchOne",this,{file:t,pattern:r}),this.debug("matchOne",t.length,r.length);for(var c=0,f=0,p=t.length,h=r.length;c>> no match, partial?`,t,S,r,x),S===p))}let T;if(typeof E=="string"?(T=C===E,this.debug("string match",E,C,T)):(T=E.test(C),this.debug("pattern match",E,C,T)),!T)return!1}if(c===p&&f===h)return!0;if(c===p)return s;if(f===h)return c===p-1&&t[c]==="";throw new Error("wtf?")}braceExpand(){return(0,pr.braceExpand)(this.pattern,this.options)}parse(t){(0,PO.assertValidPattern)(t);let r=this.options;if(t==="**")return pr.GLOBSTAR;if(t==="")return"";let s,a=null;(s=t.match(cwt))?a=r.dot?fwt:uwt:(s=t.match($Ct))?a=(r.nocase?r.dot?nwt:rwt:r.dot?twt:ewt)(s[1]):(s=t.match(Awt))?a=(r.nocase?r.dot?hwt:pwt:r.dot?dwt:gwt)(s):(s=t.match(iwt))?a=r.dot?owt:swt:(s=t.match(awt))&&(a=lwt);let n=cDe.AST.fromGlob(t,this.options).toMMPattern();return a&&typeof n=="object"&&Reflect.defineProperty(n,"test",{value:a}),n}makeRe(){if(this.regexp||this.regexp===!1)return this.regexp;let t=this.set;if(!t.length)return this.regexp=!1,this.regexp;let r=this.options,s=r.noglobstar?ywt:r.dot?Ewt:Iwt,a=new Set(r.nocase?["i"]:[]),n=t.map(p=>{let h=p.map(E=>{if(E instanceof RegExp)for(let C of E.flags.split(""))a.add(C);return typeof E=="string"?Dwt(E):E===pr.GLOBSTAR?pr.GLOBSTAR:E._src});return h.forEach((E,C)=>{let S=h[C+1],x=h[C-1];E!==pr.GLOBSTAR||x===pr.GLOBSTAR||(x===void 0?S!==void 0&&S!==pr.GLOBSTAR?h[C+1]="(?:\\/|"+s+"\\/)?"+S:h[C]=s:S===void 0?h[C-1]=x+"(?:\\/|"+s+")?":S!==pr.GLOBSTAR&&(h[C-1]=x+"(?:\\/|\\/"+s+"\\/)"+S,h[C+1]=pr.GLOBSTAR))}),h.filter(E=>E!==pr.GLOBSTAR).join("/")}).join("|"),[c,f]=t.length>1?["(?:",")"]:["",""];n="^"+c+n+f+"$",this.negate&&(n="^(?!"+n+").+$");try{this.regexp=new RegExp(n,[...a].join(""))}catch{this.regexp=!1}return this.regexp}slashSplit(t){return this.preserveMultipleSlashes?t.split("/"):this.isWindows&&/^\/\/[^\/]+/.test(t)?["",...t.split(/\/+/)]:t.split(/\/+/)}match(t,r=this.partial){if(this.debug("match",t,this.pattern),this.comment)return!1;if(this.empty)return t==="";if(t==="/"&&r)return!0;let s=this.options;this.isWindows&&(t=t.split("\\").join("/"));let a=this.slashSplit(t);this.debug(this.pattern,"split",a);let n=this.set;this.debug(this.pattern,"set",n);let c=a[a.length-1];if(!c)for(let f=a.length-2;!c&&f>=0;f--)c=a[f];for(let f=0;f{"use strict";var hDe=tu&&tu.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(tu,"__esModule",{value:!0});tu.SuccinctRoles=tu.DelegatedRole=tu.Role=tu.TOP_LEVEL_ROLE_NAMES=void 0;var dDe=hDe(Ie("crypto")),kwt=pDe(),xO=hDe(Ie("util")),kO=kA(),Ay=hf();tu.TOP_LEVEL_ROLE_NAMES=["root","targets","snapshot","timestamp"];var Mb=class e{constructor(t){let{keyIDs:r,threshold:s,unrecognizedFields:a}=t;if(Qwt(r))throw new kO.ValueError("duplicate key IDs found");if(s<1)throw new kO.ValueError("threshold must be at least 1");this.keyIDs=r,this.threshold=s,this.unrecognizedFields=a||{}}equals(t){return t instanceof e?this.threshold===t.threshold&&xO.default.isDeepStrictEqual(this.keyIDs,t.keyIDs)&&xO.default.isDeepStrictEqual(this.unrecognizedFields,t.unrecognizedFields):!1}toJSON(){return{keyids:this.keyIDs,threshold:this.threshold,...this.unrecognizedFields}}static fromJSON(t){let{keyids:r,threshold:s,...a}=t;if(!Ay.guard.isStringArray(r))throw new TypeError("keyids must be an array");if(typeof s!="number")throw new TypeError("threshold must be a number");return new e({keyIDs:r,threshold:s,unrecognizedFields:a})}};tu.Role=Mb;function Qwt(e){return new Set(e).size!==e.length}var x7=class e extends Mb{constructor(t){super(t);let{name:r,terminating:s,paths:a,pathHashPrefixes:n}=t;if(this.name=r,this.terminating=s,t.paths&&t.pathHashPrefixes)throw new kO.ValueError("paths and pathHashPrefixes are mutually exclusive");this.paths=a,this.pathHashPrefixes=n}equals(t){return t instanceof e?super.equals(t)&&this.name===t.name&&this.terminating===t.terminating&&xO.default.isDeepStrictEqual(this.paths,t.paths)&&xO.default.isDeepStrictEqual(this.pathHashPrefixes,t.pathHashPrefixes):!1}isDelegatedPath(t){if(this.paths)return this.paths.some(r=>Twt(t,r));if(this.pathHashPrefixes){let s=dDe.default.createHash("sha256").update(t).digest("hex");return this.pathHashPrefixes.some(a=>s.startsWith(a))}return!1}toJSON(){let t={...super.toJSON(),name:this.name,terminating:this.terminating};return this.paths&&(t.paths=this.paths),this.pathHashPrefixes&&(t.path_hash_prefixes=this.pathHashPrefixes),t}static fromJSON(t){let{keyids:r,threshold:s,name:a,terminating:n,paths:c,path_hash_prefixes:f,...p}=t;if(!Ay.guard.isStringArray(r))throw new TypeError("keyids must be an array of strings");if(typeof s!="number")throw new TypeError("threshold must be a number");if(typeof a!="string")throw new TypeError("name must be a string");if(typeof n!="boolean")throw new TypeError("terminating must be a boolean");if(Ay.guard.isDefined(c)&&!Ay.guard.isStringArray(c))throw new TypeError("paths must be an array of strings");if(Ay.guard.isDefined(f)&&!Ay.guard.isStringArray(f))throw new TypeError("path_hash_prefixes must be an array of strings");return new e({keyIDs:r,threshold:s,name:a,terminating:n,paths:c,pathHashPrefixes:f,unrecognizedFields:p})}};tu.DelegatedRole=x7;var Rwt=(e,t)=>e.map((r,s)=>[r,t[s]]);function Twt(e,t){let r=e.split("/"),s=t.split("/");return s.length!=r.length?!1:Rwt(r,s).every(([a,n])=>(0,kwt.minimatch)(a,n))}var k7=class e extends Mb{constructor(t){super(t);let{bitLength:r,namePrefix:s}=t;if(r<=0||r>32)throw new kO.ValueError("bitLength must be between 1 and 32");this.bitLength=r,this.namePrefix=s,this.numberOfBins=Math.pow(2,r),this.suffixLen=(this.numberOfBins-1).toString(16).length}equals(t){return t instanceof e?super.equals(t)&&this.bitLength===t.bitLength&&this.namePrefix===t.namePrefix:!1}getRoleForTarget(t){let a=dDe.default.createHash("sha256").update(t).digest().subarray(0,4),n=32-this.bitLength,f=(a.readUInt32BE()>>>n).toString(16).padStart(this.suffixLen,"0");return`${this.namePrefix}-${f}`}*getRoles(){for(let t=0;t{"use strict";var Fwt=$w&&$w.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty($w,"__esModule",{value:!0});$w.Root=void 0;var gDe=Fwt(Ie("util")),R7=uy(),mDe=kA(),Nwt=EO(),QO=Q7(),RO=hf(),T7=class e extends R7.Signed{constructor(t){if(super(t),this.type=R7.MetadataKind.Root,this.keys=t.keys||{},this.consistentSnapshot=t.consistentSnapshot??!0,!t.roles)this.roles=QO.TOP_LEVEL_ROLE_NAMES.reduce((r,s)=>({...r,[s]:new QO.Role({keyIDs:[],threshold:1})}),{});else{let r=new Set(Object.keys(t.roles));if(!QO.TOP_LEVEL_ROLE_NAMES.every(s=>r.has(s)))throw new mDe.ValueError("missing top-level role");this.roles=t.roles}}addKey(t,r){if(!this.roles[r])throw new mDe.ValueError(`role ${r} does not exist`);this.roles[r].keyIDs.includes(t.keyID)||this.roles[r].keyIDs.push(t.keyID),this.keys[t.keyID]=t}equals(t){return t instanceof e?super.equals(t)&&this.consistentSnapshot===t.consistentSnapshot&&gDe.default.isDeepStrictEqual(this.keys,t.keys)&&gDe.default.isDeepStrictEqual(this.roles,t.roles):!1}toJSON(){return{_type:this.type,spec_version:this.specVersion,version:this.version,expires:this.expires,keys:Owt(this.keys),roles:Lwt(this.roles),consistent_snapshot:this.consistentSnapshot,...this.unrecognizedFields}}static fromJSON(t){let{unrecognizedFields:r,...s}=R7.Signed.commonFieldsFromJSON(t),{keys:a,roles:n,consistent_snapshot:c,...f}=r;if(typeof c!="boolean")throw new TypeError("consistent_snapshot must be a boolean");return new e({...s,keys:Mwt(a),roles:Uwt(n),consistentSnapshot:c,unrecognizedFields:f})}};$w.Root=T7;function Owt(e){return Object.entries(e).reduce((t,[r,s])=>({...t,[r]:s.toJSON()}),{})}function Lwt(e){return Object.entries(e).reduce((t,[r,s])=>({...t,[r]:s.toJSON()}),{})}function Mwt(e){let t;if(RO.guard.isDefined(e)){if(!RO.guard.isObjectRecord(e))throw new TypeError("keys must be an object");t=Object.entries(e).reduce((r,[s,a])=>({...r,[s]:Nwt.Key.fromJSON(s,a)}),{})}return t}function Uwt(e){let t;if(RO.guard.isDefined(e)){if(!RO.guard.isObjectRecord(e))throw new TypeError("roles must be an object");t=Object.entries(e).reduce((r,[s,a])=>({...r,[s]:QO.Role.fromJSON(a)}),{})}return t}});var O7=G(TO=>{"use strict";Object.defineProperty(TO,"__esModule",{value:!0});TO.Signature=void 0;var N7=class e{constructor(t){let{keyID:r,sig:s}=t;this.keyID=r,this.sig=s}toJSON(){return{keyid:this.keyID,sig:this.sig}}static fromJSON(t){let{keyid:r,sig:s}=t;if(typeof r!="string")throw new TypeError("keyid must be a string");if(typeof s!="string")throw new TypeError("sig must be a string");return new e({keyID:r,sig:s})}};TO.Signature=N7});var U7=G(e1=>{"use strict";var _wt=e1&&e1.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(e1,"__esModule",{value:!0});e1.Snapshot=void 0;var Hwt=_wt(Ie("util")),L7=uy(),EDe=Rb(),yDe=hf(),M7=class e extends L7.Signed{constructor(t){super(t),this.type=L7.MetadataKind.Snapshot,this.meta=t.meta||{"targets.json":new EDe.MetaFile({version:1})}}equals(t){return t instanceof e?super.equals(t)&&Hwt.default.isDeepStrictEqual(this.meta,t.meta):!1}toJSON(){return{_type:this.type,meta:jwt(this.meta),spec_version:this.specVersion,version:this.version,expires:this.expires,...this.unrecognizedFields}}static fromJSON(t){let{unrecognizedFields:r,...s}=L7.Signed.commonFieldsFromJSON(t),{meta:a,...n}=r;return new e({...s,meta:Gwt(a),unrecognizedFields:n})}};e1.Snapshot=M7;function jwt(e){return Object.entries(e).reduce((t,[r,s])=>({...t,[r]:s.toJSON()}),{})}function Gwt(e){let t;if(yDe.guard.isDefined(e))if(yDe.guard.isObjectRecord(e))t=Object.entries(e).reduce((r,[s,a])=>({...r,[s]:EDe.MetaFile.fromJSON(a)}),{});else throw new TypeError("meta field is malformed");return t}});var IDe=G(t1=>{"use strict";var qwt=t1&&t1.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t1,"__esModule",{value:!0});t1.Delegations=void 0;var FO=qwt(Ie("util")),Wwt=kA(),Ywt=EO(),_7=Q7(),NO=hf(),H7=class e{constructor(t){if(this.keys=t.keys,this.unrecognizedFields=t.unrecognizedFields||{},t.roles&&Object.keys(t.roles).some(r=>_7.TOP_LEVEL_ROLE_NAMES.includes(r)))throw new Wwt.ValueError("Delegated role name conflicts with top-level role name");this.succinctRoles=t.succinctRoles,this.roles=t.roles}equals(t){return t instanceof e?FO.default.isDeepStrictEqual(this.keys,t.keys)&&FO.default.isDeepStrictEqual(this.roles,t.roles)&&FO.default.isDeepStrictEqual(this.unrecognizedFields,t.unrecognizedFields)&&FO.default.isDeepStrictEqual(this.succinctRoles,t.succinctRoles):!1}*rolesForTarget(t){if(this.roles)for(let r of Object.values(this.roles))r.isDelegatedPath(t)&&(yield{role:r.name,terminating:r.terminating});else this.succinctRoles&&(yield{role:this.succinctRoles.getRoleForTarget(t),terminating:!0})}toJSON(){let t={keys:Vwt(this.keys),...this.unrecognizedFields};return this.roles?t.roles=Jwt(this.roles):this.succinctRoles&&(t.succinct_roles=this.succinctRoles.toJSON()),t}static fromJSON(t){let{keys:r,roles:s,succinct_roles:a,...n}=t,c;return NO.guard.isObject(a)&&(c=_7.SuccinctRoles.fromJSON(a)),new e({keys:Kwt(r),roles:zwt(s),unrecognizedFields:n,succinctRoles:c})}};t1.Delegations=H7;function Vwt(e){return Object.entries(e).reduce((t,[r,s])=>({...t,[r]:s.toJSON()}),{})}function Jwt(e){return Object.values(e).map(t=>t.toJSON())}function Kwt(e){if(!NO.guard.isObjectRecord(e))throw new TypeError("keys is malformed");return Object.entries(e).reduce((t,[r,s])=>({...t,[r]:Ywt.Key.fromJSON(r,s)}),{})}function zwt(e){let t;if(NO.guard.isDefined(e)){if(!NO.guard.isObjectArray(e))throw new TypeError("roles is malformed");t=e.reduce((r,s)=>{let a=_7.DelegatedRole.fromJSON(s);return{...r,[a.name]:a}},{})}return t}});var q7=G(r1=>{"use strict";var Xwt=r1&&r1.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(r1,"__esModule",{value:!0});r1.Targets=void 0;var CDe=Xwt(Ie("util")),j7=uy(),Zwt=IDe(),$wt=Rb(),OO=hf(),G7=class e extends j7.Signed{constructor(t){super(t),this.type=j7.MetadataKind.Targets,this.targets=t.targets||{},this.delegations=t.delegations}addTarget(t){this.targets[t.path]=t}equals(t){return t instanceof e?super.equals(t)&&CDe.default.isDeepStrictEqual(this.targets,t.targets)&&CDe.default.isDeepStrictEqual(this.delegations,t.delegations):!1}toJSON(){let t={_type:this.type,spec_version:this.specVersion,version:this.version,expires:this.expires,targets:e1t(this.targets),...this.unrecognizedFields};return this.delegations&&(t.delegations=this.delegations.toJSON()),t}static fromJSON(t){let{unrecognizedFields:r,...s}=j7.Signed.commonFieldsFromJSON(t),{targets:a,delegations:n,...c}=r;return new e({...s,targets:t1t(a),delegations:r1t(n),unrecognizedFields:c})}};r1.Targets=G7;function e1t(e){return Object.entries(e).reduce((t,[r,s])=>({...t,[r]:s.toJSON()}),{})}function t1t(e){let t;if(OO.guard.isDefined(e))if(OO.guard.isObjectRecord(e))t=Object.entries(e).reduce((r,[s,a])=>({...r,[s]:$wt.TargetFile.fromJSON(s,a)}),{});else throw new TypeError("targets must be an object");return t}function r1t(e){let t;if(OO.guard.isDefined(e))if(OO.guard.isObject(e))t=Zwt.Delegations.fromJSON(e);else throw new TypeError("delegations must be an object");return t}});var J7=G(LO=>{"use strict";Object.defineProperty(LO,"__esModule",{value:!0});LO.Timestamp=void 0;var W7=uy(),wDe=Rb(),Y7=hf(),V7=class e extends W7.Signed{constructor(t){super(t),this.type=W7.MetadataKind.Timestamp,this.snapshotMeta=t.snapshotMeta||new wDe.MetaFile({version:1})}equals(t){return t instanceof e?super.equals(t)&&this.snapshotMeta.equals(t.snapshotMeta):!1}toJSON(){return{_type:this.type,spec_version:this.specVersion,version:this.version,expires:this.expires,meta:{"snapshot.json":this.snapshotMeta.toJSON()},...this.unrecognizedFields}}static fromJSON(t){let{unrecognizedFields:r,...s}=W7.Signed.commonFieldsFromJSON(t),{meta:a,...n}=r;return new e({...s,snapshotMeta:n1t(a),unrecognizedFields:n})}};LO.Timestamp=V7;function n1t(e){let t;if(Y7.guard.isDefined(e)){let r=e["snapshot.json"];if(!Y7.guard.isDefined(r)||!Y7.guard.isObject(r))throw new TypeError("missing snapshot.json in meta");t=wDe.MetaFile.fromJSON(r)}return t}});var vDe=G(i1=>{"use strict";var i1t=i1&&i1.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(i1,"__esModule",{value:!0});i1.Metadata=void 0;var s1t=p7(),BDe=i1t(Ie("util")),n1=uy(),Ub=kA(),o1t=F7(),a1t=O7(),l1t=U7(),c1t=q7(),u1t=J7(),K7=hf(),z7=class e{constructor(t,r,s){this.signed=t,this.signatures=r||{},this.unrecognizedFields=s||{}}sign(t,r=!0){let s=Buffer.from((0,s1t.canonicalize)(this.signed.toJSON())),a=t(s);r||(this.signatures={}),this.signatures[a.keyID]=a}verifyDelegate(t,r){let s,a={};switch(this.signed.type){case n1.MetadataKind.Root:a=this.signed.keys,s=this.signed.roles[t];break;case n1.MetadataKind.Targets:if(!this.signed.delegations)throw new Ub.ValueError(`No delegations found for ${t}`);a=this.signed.delegations.keys,this.signed.delegations.roles?s=this.signed.delegations.roles[t]:this.signed.delegations.succinctRoles&&this.signed.delegations.succinctRoles.isDelegatedRole(t)&&(s=this.signed.delegations.succinctRoles);break;default:throw new TypeError("invalid metadata type")}if(!s)throw new Ub.ValueError(`no delegation found for ${t}`);let n=new Set;if(s.keyIDs.forEach(c=>{let f=a[c];if(f)try{f.verifySignature(r),n.add(f.keyID)}catch{}}),n.sizer.toJSON()),signed:this.signed.toJSON(),...this.unrecognizedFields}}static fromJSON(t,r){let{signed:s,signatures:a,...n}=r;if(!K7.guard.isDefined(s)||!K7.guard.isObject(s))throw new TypeError("signed is not defined");if(t!==s._type)throw new Ub.ValueError(`expected '${t}', got ${s._type}`);if(!K7.guard.isObjectArray(a))throw new TypeError("signatures is not an array");let c;switch(t){case n1.MetadataKind.Root:c=o1t.Root.fromJSON(s);break;case n1.MetadataKind.Timestamp:c=u1t.Timestamp.fromJSON(s);break;case n1.MetadataKind.Snapshot:c=l1t.Snapshot.fromJSON(s);break;case n1.MetadataKind.Targets:c=c1t.Targets.fromJSON(s);break;default:throw new TypeError("invalid metadata type")}let f={};return a.forEach(p=>{let h=a1t.Signature.fromJSON(p);if(f[h.keyID])throw new Ub.ValueError(`multiple signatures found for keyid: ${h.keyID}`);f[h.keyID]=h}),new e(c,f,n)}};i1.Metadata=z7});var MO=G(Fi=>{"use strict";Object.defineProperty(Fi,"__esModule",{value:!0});Fi.Timestamp=Fi.Targets=Fi.Snapshot=Fi.Signature=Fi.Root=Fi.Metadata=Fi.Key=Fi.TargetFile=Fi.MetaFile=Fi.ValueError=Fi.MetadataKind=void 0;var f1t=uy();Object.defineProperty(Fi,"MetadataKind",{enumerable:!0,get:function(){return f1t.MetadataKind}});var A1t=kA();Object.defineProperty(Fi,"ValueError",{enumerable:!0,get:function(){return A1t.ValueError}});var SDe=Rb();Object.defineProperty(Fi,"MetaFile",{enumerable:!0,get:function(){return SDe.MetaFile}});Object.defineProperty(Fi,"TargetFile",{enumerable:!0,get:function(){return SDe.TargetFile}});var p1t=EO();Object.defineProperty(Fi,"Key",{enumerable:!0,get:function(){return p1t.Key}});var h1t=vDe();Object.defineProperty(Fi,"Metadata",{enumerable:!0,get:function(){return h1t.Metadata}});var d1t=F7();Object.defineProperty(Fi,"Root",{enumerable:!0,get:function(){return d1t.Root}});var g1t=O7();Object.defineProperty(Fi,"Signature",{enumerable:!0,get:function(){return g1t.Signature}});var m1t=U7();Object.defineProperty(Fi,"Snapshot",{enumerable:!0,get:function(){return m1t.Snapshot}});var y1t=q7();Object.defineProperty(Fi,"Targets",{enumerable:!0,get:function(){return y1t.Targets}});var E1t=J7();Object.defineProperty(Fi,"Timestamp",{enumerable:!0,get:function(){return E1t.Timestamp}})});var bDe=G((Qtr,DDe)=>{var s1=1e3,o1=s1*60,a1=o1*60,py=a1*24,I1t=py*7,C1t=py*365.25;DDe.exports=function(e,t){t=t||{};var r=typeof e;if(r==="string"&&e.length>0)return w1t(e);if(r==="number"&&isFinite(e))return t.long?v1t(e):B1t(e);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(e))};function w1t(e){if(e=String(e),!(e.length>100)){var t=/^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(e);if(t){var r=parseFloat(t[1]),s=(t[2]||"ms").toLowerCase();switch(s){case"years":case"year":case"yrs":case"yr":case"y":return r*C1t;case"weeks":case"week":case"w":return r*I1t;case"days":case"day":case"d":return r*py;case"hours":case"hour":case"hrs":case"hr":case"h":return r*a1;case"minutes":case"minute":case"mins":case"min":case"m":return r*o1;case"seconds":case"second":case"secs":case"sec":case"s":return r*s1;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return r;default:return}}}}function B1t(e){var t=Math.abs(e);return t>=py?Math.round(e/py)+"d":t>=a1?Math.round(e/a1)+"h":t>=o1?Math.round(e/o1)+"m":t>=s1?Math.round(e/s1)+"s":e+"ms"}function v1t(e){var t=Math.abs(e);return t>=py?UO(e,t,py,"day"):t>=a1?UO(e,t,a1,"hour"):t>=o1?UO(e,t,o1,"minute"):t>=s1?UO(e,t,s1,"second"):e+" ms"}function UO(e,t,r,s){var a=t>=r*1.5;return Math.round(e/r)+" "+s+(a?"s":"")}});var X7=G((Rtr,PDe)=>{function S1t(e){r.debug=r,r.default=r,r.coerce=p,r.disable=c,r.enable=a,r.enabled=f,r.humanize=bDe(),r.destroy=h,Object.keys(e).forEach(E=>{r[E]=e[E]}),r.names=[],r.skips=[],r.formatters={};function t(E){let C=0;for(let S=0;S{if(ae==="%%")return"%";ie++;let Ae=r.formatters[ge];if(typeof Ae=="function"){let Ce=O[ie];ae=Ae.call(U,Ce),O.splice(ie,1),ie--}return ae}),r.formatArgs.call(U,O),(U.log||r.log).apply(U,O)}return T.namespace=E,T.useColors=r.useColors(),T.color=r.selectColor(E),T.extend=s,T.destroy=r.destroy,Object.defineProperty(T,"enabled",{enumerable:!0,configurable:!1,get:()=>S!==null?S:(x!==r.namespaces&&(x=r.namespaces,I=r.enabled(E)),I),set:O=>{S=O}}),typeof r.init=="function"&&r.init(T),T}function s(E,C){let S=r(this.namespace+(typeof C>"u"?":":C)+E);return S.log=this.log,S}function a(E){r.save(E),r.namespaces=E,r.names=[],r.skips=[];let C=(typeof E=="string"?E:"").trim().replace(" ",",").split(",").filter(Boolean);for(let S of C)S[0]==="-"?r.skips.push(S.slice(1)):r.names.push(S)}function n(E,C){let S=0,x=0,I=-1,T=0;for(;S"-"+C)].join(",");return r.enable(""),E}function f(E){for(let C of r.skips)if(n(E,C))return!1;for(let C of r.names)if(n(E,C))return!0;return!1}function p(E){return E instanceof Error?E.stack||E.message:E}function h(){console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.")}return r.enable(r.load()),r}PDe.exports=S1t});var xDe=G((ac,_O)=>{ac.formatArgs=b1t;ac.save=P1t;ac.load=x1t;ac.useColors=D1t;ac.storage=k1t();ac.destroy=(()=>{let e=!1;return()=>{e||(e=!0,console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."))}})();ac.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"];function D1t(){if(typeof window<"u"&&window.process&&(window.process.type==="renderer"||window.process.__nwjs))return!0;if(typeof navigator<"u"&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))return!1;let e;return typeof document<"u"&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||typeof window<"u"&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||typeof navigator<"u"&&navigator.userAgent&&(e=navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/))&&parseInt(e[1],10)>=31||typeof navigator<"u"&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)}function b1t(e){if(e[0]=(this.useColors?"%c":"")+this.namespace+(this.useColors?" %c":" ")+e[0]+(this.useColors?"%c ":" ")+"+"+_O.exports.humanize(this.diff),!this.useColors)return;let t="color: "+this.color;e.splice(1,0,t,"color: inherit");let r=0,s=0;e[0].replace(/%[a-zA-Z%]/g,a=>{a!=="%%"&&(r++,a==="%c"&&(s=r))}),e.splice(s,0,t)}ac.log=console.debug||console.log||(()=>{});function P1t(e){try{e?ac.storage.setItem("debug",e):ac.storage.removeItem("debug")}catch{}}function x1t(){let e;try{e=ac.storage.getItem("debug")}catch{}return!e&&typeof process<"u"&&"env"in process&&(e=process.env.DEBUG),e}function k1t(){try{return localStorage}catch{}}_O.exports=X7()(ac);var{formatters:Q1t}=_O.exports;Q1t.j=function(e){try{return JSON.stringify(e)}catch(t){return"[UnexpectedJSONParseError]: "+t.message}}});var QDe=G((io,jO)=>{var R1t=Ie("tty"),HO=Ie("util");io.init=U1t;io.log=O1t;io.formatArgs=F1t;io.save=L1t;io.load=M1t;io.useColors=T1t;io.destroy=HO.deprecate(()=>{},"Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.");io.colors=[6,2,3,4,5,1];try{let e=Ie("supports-color");e&&(e.stderr||e).level>=2&&(io.colors=[20,21,26,27,32,33,38,39,40,41,42,43,44,45,56,57,62,63,68,69,74,75,76,77,78,79,80,81,92,93,98,99,112,113,128,129,134,135,148,149,160,161,162,163,164,165,166,167,168,169,170,171,172,173,178,179,184,185,196,197,198,199,200,201,202,203,204,205,206,207,208,209,214,215,220,221])}catch{}io.inspectOpts=Object.keys(process.env).filter(e=>/^debug_/i.test(e)).reduce((e,t)=>{let r=t.substring(6).toLowerCase().replace(/_([a-z])/g,(a,n)=>n.toUpperCase()),s=process.env[t];return/^(yes|on|true|enabled)$/i.test(s)?s=!0:/^(no|off|false|disabled)$/i.test(s)?s=!1:s==="null"?s=null:s=Number(s),e[r]=s,e},{});function T1t(){return"colors"in io.inspectOpts?!!io.inspectOpts.colors:R1t.isatty(process.stderr.fd)}function F1t(e){let{namespace:t,useColors:r}=this;if(r){let s=this.color,a="\x1B[3"+(s<8?s:"8;5;"+s),n=` ${a};1m${t} \x1B[0m`;e[0]=n+e[0].split(` `).join(` `+n),e.push(a+"m+"+jO.exports.humanize(this.diff)+"\x1B[0m")}else e[0]=N1t()+t+" "+e[0]}function N1t(){return io.inspectOpts.hideDate?"":new Date().toISOString()+" "}function O1t(...e){return process.stderr.write(HO.formatWithOptions(io.inspectOpts,...e)+` `)}function L1t(e){e?process.env.DEBUG=e:delete process.env.DEBUG}function M1t(){return process.env.DEBUG}function U1t(e){e.inspectOpts={};let t=Object.keys(io.inspectOpts);for(let r=0;rt.trim()).join(" ")};kDe.O=function(e){return this.inspectOpts.colors=this.useColors,HO.inspect(e,this.inspectOpts)}});var $7=G((Ttr,Z7)=>{typeof process>"u"||process.type==="renderer"||process.browser===!0||process.__nwjs?Z7.exports=xDe():Z7.exports=QDe()});var qO=G(Ki=>{"use strict";Object.defineProperty(Ki,"__esModule",{value:!0});Ki.DownloadHTTPError=Ki.DownloadLengthMismatchError=Ki.DownloadError=Ki.ExpiredMetadataError=Ki.EqualVersionError=Ki.BadVersionError=Ki.RepositoryError=Ki.PersistError=Ki.RuntimeError=Ki.ValueError=void 0;var eJ=class extends Error{};Ki.ValueError=eJ;var tJ=class extends Error{};Ki.RuntimeError=tJ;var rJ=class extends Error{};Ki.PersistError=rJ;var _b=class extends Error{};Ki.RepositoryError=_b;var GO=class extends _b{};Ki.BadVersionError=GO;var nJ=class extends GO{};Ki.EqualVersionError=nJ;var iJ=class extends _b{};Ki.ExpiredMetadataError=iJ;var Hb=class extends Error{};Ki.DownloadError=Hb;var sJ=class extends Hb{};Ki.DownloadLengthMismatchError=sJ;var oJ=class extends Hb{constructor(t,r){super(t),this.statusCode=r}};Ki.DownloadHTTPError=oJ});var TDe=G(l1=>{"use strict";var lJ=l1&&l1.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(l1,"__esModule",{value:!0});l1.withTempFile=void 0;var aJ=lJ(Ie("fs/promises")),_1t=lJ(Ie("os")),RDe=lJ(Ie("path")),H1t=async e=>j1t(async t=>e(RDe.default.join(t,"tempfile")));l1.withTempFile=H1t;var j1t=async e=>{let t=await aJ.default.realpath(_1t.default.tmpdir()),r=await aJ.default.mkdtemp(t+RDe.default.sep);try{return await e(r)}finally{await aJ.default.rm(r,{force:!0,recursive:!0,maxRetries:3})}}});var uJ=G(Pd=>{"use strict";var YO=Pd&&Pd.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(Pd,"__esModule",{value:!0});Pd.DefaultFetcher=Pd.BaseFetcher=void 0;var G1t=YO($7()),FDe=YO(Ie("fs")),q1t=YO(sO()),W1t=YO(Ie("util")),NDe=qO(),Y1t=TDe(),V1t=(0,G1t.default)("tuf:fetch"),WO=class{async downloadFile(t,r,s){return(0,Y1t.withTempFile)(async a=>{let n=await this.fetch(t),c=0,f=FDe.default.createWriteStream(a);try{for await(let p of n){let h=Buffer.from(p);if(c+=h.length,c>r)throw new NDe.DownloadLengthMismatchError("Max length reached");await J1t(f,h)}}finally{await W1t.default.promisify(f.close).bind(f)()}return s(a)})}async downloadBytes(t,r){return this.downloadFile(t,r,async s=>{let a=FDe.default.createReadStream(s),n=[];for await(let c of a)n.push(c);return Buffer.concat(n)})}};Pd.BaseFetcher=WO;var cJ=class extends WO{constructor(t={}){super(),this.timeout=t.timeout,this.retry=t.retry}async fetch(t){V1t("GET %s",t);let r=await(0,q1t.default)(t,{timeout:this.timeout,retry:this.retry});if(!r.ok||!r?.body)throw new NDe.DownloadHTTPError("Failed to download",r.status);return r.body}};Pd.DefaultFetcher=cJ;var J1t=async(e,t)=>new Promise((r,s)=>{e.write(t,a=>{a&&s(a),r(!0)})})});var ODe=G(VO=>{"use strict";Object.defineProperty(VO,"__esModule",{value:!0});VO.defaultConfig=void 0;VO.defaultConfig={maxRootRotations:256,maxDelegations:32,rootMaxLength:512e3,timestampMaxLength:16384,snapshotMaxLength:2e6,targetsMaxLength:5e6,prefixTargetsWithHash:!0,fetchTimeout:1e5,fetchRetries:void 0,fetchRetry:2}});var LDe=G(JO=>{"use strict";Object.defineProperty(JO,"__esModule",{value:!0});JO.TrustedMetadataStore=void 0;var ws=MO(),Hi=qO(),fJ=class{constructor(t){this.trustedSet={},this.referenceTime=new Date,this.loadTrustedRoot(t)}get root(){if(!this.trustedSet.root)throw new ReferenceError("No trusted root metadata");return this.trustedSet.root}get timestamp(){return this.trustedSet.timestamp}get snapshot(){return this.trustedSet.snapshot}get targets(){return this.trustedSet.targets}getRole(t){return this.trustedSet[t]}updateRoot(t){let r=JSON.parse(t.toString("utf8")),s=ws.Metadata.fromJSON(ws.MetadataKind.Root,r);if(s.signed.type!=ws.MetadataKind.Root)throw new Hi.RepositoryError(`Expected 'root', got ${s.signed.type}`);if(this.root.verifyDelegate(ws.MetadataKind.Root,s),s.signed.version!=this.root.signed.version+1)throw new Hi.BadVersionError(`Expected version ${this.root.signed.version+1}, got ${s.signed.version}`);return s.verifyDelegate(ws.MetadataKind.Root,s),this.trustedSet.root=s,s}updateTimestamp(t){if(this.snapshot)throw new Hi.RuntimeError("Cannot update timestamp after snapshot");if(this.root.signed.isExpired(this.referenceTime))throw new Hi.ExpiredMetadataError("Final root.json is expired");let r=JSON.parse(t.toString("utf8")),s=ws.Metadata.fromJSON(ws.MetadataKind.Timestamp,r);if(s.signed.type!=ws.MetadataKind.Timestamp)throw new Hi.RepositoryError(`Expected 'timestamp', got ${s.signed.type}`);if(this.root.verifyDelegate(ws.MetadataKind.Timestamp,s),this.timestamp){if(s.signed.version{let p=n.signed.meta[c];if(!p)throw new Hi.RepositoryError(`Missing file ${c} in new snapshot`);if(p.version{"use strict";Object.defineProperty(AJ,"__esModule",{value:!0});AJ.join=z1t;var K1t=Ie("url");function z1t(e,t){return new K1t.URL(X1t(e)+Z1t(t)).toString()}function X1t(e){return e.endsWith("/")?e:e+"/"}function Z1t(e){return e.startsWith("/")?e.slice(1):e}});var UDe=G(ru=>{"use strict";var $1t=ru&&ru.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(t,r);(!a||("get"in a?!t.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,s,a)}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),e2t=ru&&ru.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),dJ=ru&&ru.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var r in e)r!=="default"&&Object.prototype.hasOwnProperty.call(e,r)&&$1t(t,e,r);return e2t(t,e),t},t2t=ru&&ru.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(ru,"__esModule",{value:!0});ru.Updater=void 0;var QA=MO(),r2t=t2t($7()),c1=dJ(Ie("fs")),KO=dJ(Ie("path")),n2t=ODe(),hy=qO(),i2t=uJ(),s2t=LDe(),jb=dJ(MDe()),pJ=(0,r2t.default)("tuf:cache"),hJ=class{constructor(t){let{metadataDir:r,metadataBaseUrl:s,targetDir:a,targetBaseUrl:n,fetcher:c,config:f}=t;this.dir=r,this.metadataBaseUrl=s,this.targetDir=a,this.targetBaseUrl=n,this.forceCache=t.forceCache??!1;let p=this.loadLocalMetadata(QA.MetadataKind.Root);this.trustedSet=new s2t.TrustedMetadataStore(p),this.config={...n2t.defaultConfig,...f},this.fetcher=c||new i2t.DefaultFetcher({timeout:this.config.fetchTimeout,retry:this.config.fetchRetries??this.config.fetchRetry})}async refresh(){if(this.forceCache)try{await this.loadTimestamp({checkRemote:!1})}catch{await this.loadRoot(),await this.loadTimestamp()}else await this.loadRoot(),await this.loadTimestamp();await this.loadSnapshot(),await this.loadTargets(QA.MetadataKind.Targets,QA.MetadataKind.Root)}async getTargetInfo(t){return this.trustedSet.targets||await this.refresh(),this.preorderDepthFirstWalk(t)}async downloadTarget(t,r,s){let a=r||this.generateTargetPath(t);if(!s){if(!this.targetBaseUrl)throw new hy.ValueError("Target base URL not set");s=this.targetBaseUrl}let n=t.path;if(this.trustedSet.root.signed.consistentSnapshot&&this.config.prefixTargetsWithHash){let p=Object.values(t.hashes),{dir:h,base:E}=KO.parse(n),C=`${p[0]}.${E}`;n=h?`${h}/${C}`:C}let f=jb.join(s,n);return await this.fetcher.downloadFile(f,t.length,async p=>{await t.verify(c1.createReadStream(p)),pJ("WRITE %s",a),c1.copyFileSync(p,a)}),a}async findCachedTarget(t,r){r||(r=this.generateTargetPath(t));try{if(c1.existsSync(r))return await t.verify(c1.createReadStream(r)),r}catch{return}}loadLocalMetadata(t){let r=KO.join(this.dir,`${t}.json`);return pJ("READ %s",r),c1.readFileSync(r)}async loadRoot(){let r=this.trustedSet.root.signed.version+1,s=r+this.config.maxRootRotations;for(let a=r;a0;){let{roleName:a,parentRoleName:n}=r.pop();if(s.has(a))continue;let c=(await this.loadTargets(a,n))?.signed;if(!c)continue;let f=c.targets?.[t];if(f)return f;if(s.add(a),c.delegations){let p=[],h=c.delegations.rolesForTarget(t);for(let{role:E,terminating:C}of h)if(p.push({roleName:E,parentRoleName:a}),C){r.splice(0);break}p.reverse(),r.push(...p)}}}generateTargetPath(t){if(!this.targetDir)throw new hy.ValueError("Target directory not set");let r=encodeURIComponent(t.path);return KO.join(this.targetDir,r)}persistMetadata(t,r){let s=encodeURIComponent(t);try{let a=KO.join(this.dir,`${s}.json`);pJ("WRITE %s",a),c1.writeFileSync(a,r.toString("utf8"))}catch(a){throw new hy.PersistError(`Failed to persist metadata ${s} error: ${a}`)}}};ru.Updater=hJ});var _De=G(xd=>{"use strict";Object.defineProperty(xd,"__esModule",{value:!0});xd.Updater=xd.BaseFetcher=xd.TargetFile=void 0;var o2t=MO();Object.defineProperty(xd,"TargetFile",{enumerable:!0,get:function(){return o2t.TargetFile}});var a2t=uJ();Object.defineProperty(xd,"BaseFetcher",{enumerable:!0,get:function(){return a2t.BaseFetcher}});var l2t=UDe();Object.defineProperty(xd,"Updater",{enumerable:!0,get:function(){return l2t.Updater}})});var mJ=G(zO=>{"use strict";Object.defineProperty(zO,"__esModule",{value:!0});zO.TUFError=void 0;var gJ=class extends Error{constructor({code:t,message:r,cause:s}){super(r),this.code=t,this.cause=s,this.name=this.constructor.name}};zO.TUFError=gJ});var HDe=G(Gb=>{"use strict";var c2t=Gb&&Gb.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(Gb,"__esModule",{value:!0});Gb.readTarget=f2t;var u2t=c2t(Ie("fs")),XO=mJ();async function f2t(e,t){let r=await A2t(e,t);return new Promise((s,a)=>{u2t.default.readFile(r,"utf-8",(n,c)=>{n?a(new XO.TUFError({code:"TUF_READ_TARGET_ERROR",message:`error reading target ${r}`,cause:n})):s(c)})})}async function A2t(e,t){let r;try{r=await e.getTargetInfo(t)}catch(a){throw new XO.TUFError({code:"TUF_REFRESH_METADATA_ERROR",message:"error refreshing TUF metadata",cause:a})}if(!r)throw new XO.TUFError({code:"TUF_FIND_TARGET_ERROR",message:`target ${t} not found`});let s=await e.findCachedTarget(r);if(!s)try{s=await e.downloadTarget(r)}catch(a){throw new XO.TUFError({code:"TUF_DOWNLOAD_TARGET_ERROR",message:`error downloading target ${s}`,cause:a})}return s}});var jDe=G((qtr,p2t)=>{p2t.exports={"https://tuf-repo-cdn.sigstore.dev":{"root.json":"ewogInNpZ25hdHVyZXMiOiBbCiAgewogICAia2V5aWQiOiAiNmYyNjAwODlkNTkyM2RhZjIwMTY2Y2E2NTdjNTQzYWY2MTgzNDZhYjk3MTg4NGE5OTk2MmIwMTk4OGJiZTBjMyIsCiAgICJzaWciOiAiMzA0NjAyMjEwMDhhYjFmNmYxN2Q0ZjllNmQ3ZGNmMWM4ODkxMmI2YjUzY2MxMDM4ODY0NGFlMWYwOWJjMzdhMDgyY2QwNjAwM2UwMjIxMDBlMTQ1ZWY0YzdiNzgyZDRlODEwN2I1MzQzN2U2NjlkMDQ3Njg5MmNlOTk5OTAzYWUzM2QxNDQ0ODM2Njk5NmU3IgogIH0sCiAgewogICAia2V5aWQiOiAiZTcxYTU0ZDU0MzgzNWJhODZhZGFkOTQ2MDM3OWM3NjQxZmI4NzI2ZDE2NGVhNzY2ODAxYTFjNTIyYWJhN2VhMiIsCiAgICJzaWciOiAiMzA0NTAyMjEwMGM3NjhiMmY4NmRhOTk1NjkwMTljMTYwYTA4MWRhNTRhZTM2YzM0YzBhMzEyMGQzY2I2OWI1M2I3ZDExMzc1OGUwMjIwNGY2NzE1MThmNjE3YjIwZDQ2NTM3ZmFlNmMzYjYzYmFlODkxM2Y0ZjE5NjIxNTYxMDVjYzRmMDE5YWMzNWM2YSIKICB9LAogIHsKICAgImtleWlkIjogIjIyZjRjYWVjNmQ4ZTZmOTU1NWFmNjZiM2Q0YzNjYjA2YTNiYjIzZmRjN2UzOWM5MTZjNjFmNDYyZTZmNTJiMDYiLAogICAic2lnIjogIjMwNDUwMjIxMDBiNDQzNGU2OTk1ZDM2OGQyM2U3NDc1OWFjZDBjYjkwMTNjODNhNWQzNTExZjBmOTk3ZWM1NGM0NTZhZTQzNTBhMDIyMDE1YjBlMjY1ZDE4MmQyYjYxZGM3NGUxNTVkOThiM2MzZmJlNTY0YmEwNTI4NmFhMTRjOGRmMDJjOWI3NTY1MTYiCiAgfSwKICB7CiAgICJrZXlpZCI6ICI2MTY0MzgzODEyNWI0NDBiNDBkYjY5NDJmNWNiNWEzMWMwZGMwNDM2ODMxNmViMmFhYTU4Yjk1OTA0YTU4MjIyIiwKICAgInNpZyI6ICIzMDQ1MDIyMTAwODJjNTg0MTFkOTg5ZWI5Zjg2MTQxMDg1N2Q0MjM4MTU5MGVjOTQyNGRiZGFhNTFlNzhlZDEzNTE1NDMxOTA0ZTAyMjAxMTgxODVkYTZhNmMyOTQ3MTMxYzE3Nzk3ZTJiYjc2MjBjZTI2ZTVmMzAxZDFjZWFjNWYyYTdlNThmOWRjZjJlIgogIH0sCiAgewogICAia2V5aWQiOiAiYTY4N2U1YmY0ZmFiODJiMGVlNThkNDZlMDVjOTUzNTE0NWEyYzlhZmI0NThmNDNkNDJiNDVjYTBmZGNlMmE3MCIsCiAgICJzaWciOiAiMzA0NjAyMjEwMGM3ODUxMzg1NGNhZTljMzJlYWE2Yjg4ZTE4OTEyZjQ4MDA2YzI3NTdhMjU4ZjkxNzMxMmNhYmE3NTk0OGViOWUwMjIxMDBkOWUxYjRjZTBhZGZlOWZkMmUyMTQ4ZDdmYTI3YTJmNDBiYTExMjJiZDY5ZGE3NjEyZDhkMTc3NmIwMTNjOTFkIgogIH0sCiAgewogICAia2V5aWQiOiAiZmRmYTgzYTA3YjVhODM1ODliODdkZWQ0MWY3N2YzOWQyMzJhZDkxZjdjY2U1Mjg2OGRhY2QwNmJhMDg5ODQ5ZiIsCiAgICJzaWciOiAiMzA0NTAyMjA1NjQ4M2EyZDVkOWVhOWNlYzZlMTFlYWRmYjMzYzQ4NGI2MTQyOThmYWNhMTVhY2YxYzQzMWIxMWVkN2Y3MzRjMDIyMTAwZDBjMWQ3MjZhZjkyYTg3ZTRlNjY0NTljYTVhZGYzOGEwNWI0NGUxZjk0MzE4NDIzZjk1NGJhZThiY2E1YmIyZSIKICB9LAogIHsKICAgImtleWlkIjogImUyZjU5YWNiOTQ4ODUxOTQwN2UxOGNiZmM5MzI5NTEwYmUwM2MwNGFjYTk5MjlkMmYwMzAxMzQzZmVjODU1MjMiLAogICAic2lnIjogIjMwNDYwMjIxMDBkMDA0ZGU4ODAyNGMzMmRjNTY1M2E5ZjQ4NDNjZmM1MjE1NDI3MDQ4YWQ5NjAwZDJjZjljOTY5ZTZlZGZmM2QyMDIyMTAwZDllYmI3OThmNWZjNjZhZjEwODk5ZGVjZTAxNGE4NjI4Y2NmM2M1NDAyY2Q0YTQyNzAyMDc0NzJmOGY2ZTcxMiIKICB9LAogIHsKICAgImtleWlkIjogIjNjMzQ0YWEwNjhmZDRjYzRlODdkYzUwYjYxMmMwMjQzMWZiYzc3MWU5NTAwMzk5MzY4M2EyYjBiZjI2MGNmMGUiLAogICAic2lnIjogIjMwNDYwMjIxMDBiN2IwOTk5NmM0NWNhMmQ0YjA1NjAzZTU2YmFlZmEyOTcxOGEwYjcxMTQ3Y2Y4YzZlNjYzNDliYWE2MTQ3N2RmMDIyMTAwYzRkYTgwYzcxN2I0ZmE3YmJhMGZkNWM3MmRhOGEwNDk5MzU4YjAxMzU4YjIzMDlmNDFkMTQ1NmVhMWU3ZTFkOSIKICB9LAogIHsKICAgImtleWlkIjogImVjODE2Njk3MzRlMDE3OTk2YzViODVmM2QwMmMzZGUxZGQ0NjM3YTE1MjAxOWZlMWFmMTI1ZDJmOTM2OGI5NWUiLAogICAic2lnIjogIjMwNDYwMjIxMDBiZTk3ODJjMzA3NDRlNDExYTgyZmE4NWI1MTM4ZDYwMWNlMTQ4YmMxOTI1OGFlYzY0ZTdlYzI0NDc4ZjM4ODEyMDIyMTAwY2FlZjYzZGNhZjFhNGI5YTUwMGQzYmQwZTNmMTY0ZWMxOGYxYjYzZDdhOTQ2MGQ5YWNhYjEwNjZkYjBmMDE2ZCIKICB9LAogIHsKICAgImtleWlkIjogIjFlMWQ2NWNlOThiMTBhZGRhZDQ3NjRmZWJmN2RkYTJkMDQzNmIzZDNhMzg5MzU3OWMwZGRkYWVhMjBlNTQ4NDkiLAogICAic2lnIjogIjMwNDUwMjIwNzQ2ZWMzZjg1MzRjZTU1NTMxZDBkMDFmZjY0OTY0ZWY0NDBkMWU3ZDJjNGMxNDI0MDliOGU5NzY5ZjFhZGE2ZjAyMjEwMGUzYjkyOWZjZDkzZWExOGZlYWEwODI1ODg3YTcyMTA0ODk4NzlhNjY3ODBjMDdhODNmNGJkNDZlMmYwOWFiM2IiCiAgfQogXSwKICJzaWduZWQiOiB7CiAgIl90eXBlIjogInJvb3QiLAogICJjb25zaXN0ZW50X3NuYXBzaG90IjogdHJ1ZSwKICAiZXhwaXJlcyI6ICIyMDI1LTAyLTE5VDA4OjA0OjMyWiIsCiAgImtleXMiOiB7CiAgICIyMmY0Y2FlYzZkOGU2Zjk1NTVhZjY2YjNkNGMzY2IwNmEzYmIyM2ZkYzdlMzljOTE2YzYxZjQ2MmU2ZjUyYjA2IjogewogICAgImtleWlkX2hhc2hfYWxnb3JpdGhtcyI6IFsKICAgICAic2hhMjU2IiwKICAgICAic2hhNTEyIgogICAgXSwKICAgICJrZXl0eXBlIjogImVjZHNhIiwKICAgICJrZXl2YWwiOiB7CiAgICAgInB1YmxpYyI6ICItLS0tLUJFR0lOIFBVQkxJQyBLRVktLS0tLVxuTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFekJ6Vk9tSENQb2pNVkxTSTM2NFdpaVY4TlByRFxuNklnUnhWbGlza3ovdit5M0pFUjVtY1ZHY09ObGlEY1dNQzVKMmxmSG1qUE5QaGI0SDd4bThMemZTQT09XG4tLS0tLUVORCBQVUJMSUMgS0VZLS0tLS1cbiIKICAgIH0sCiAgICAic2NoZW1lIjogImVjZHNhLXNoYTItbmlzdHAyNTYiLAogICAgIngtdHVmLW9uLWNpLWtleW93bmVyIjogIkBzYW50aWFnb3RvcnJlcyIKICAgfSwKICAgIjYxNjQzODM4MTI1YjQ0MGI0MGRiNjk0MmY1Y2I1YTMxYzBkYzA0MzY4MzE2ZWIyYWFhNThiOTU5MDRhNTgyMjIiOiB7CiAgICAia2V5aWRfaGFzaF9hbGdvcml0aG1zIjogWwogICAgICJzaGEyNTYiLAogICAgICJzaGE1MTIiCiAgICBdLAogICAgImtleXR5cGUiOiAiZWNkc2EiLAogICAgImtleXZhbCI6IHsKICAgICAicHVibGljIjogIi0tLS0tQkVHSU4gUFVCTElDIEtFWS0tLS0tXG5NRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVpbmlrU3NBUW1Za05lSDVlWXEvQ25JekxhYWNPXG54bFNhYXdRRE93cUt5L3RDcXhxNXh4UFNKYzIxSzRXSWhzOUd5T2tLZnp1ZVkzR0lMemNNSlo0Y1d3PT1cbi0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLVxuIgogICAgfSwKICAgICJzY2hlbWUiOiAiZWNkc2Etc2hhMi1uaXN0cDI1NiIsCiAgICAieC10dWYtb24tY2kta2V5b3duZXIiOiAiQGJvYmNhbGxhd2F5IgogICB9LAogICAiNmYyNjAwODlkNTkyM2RhZjIwMTY2Y2E2NTdjNTQzYWY2MTgzNDZhYjk3MTg4NGE5OTk2MmIwMTk4OGJiZTBjMyI6IHsKICAgICJrZXlpZF9oYXNoX2FsZ29yaXRobXMiOiBbCiAgICAgInNoYTI1NiIsCiAgICAgInNoYTUxMiIKICAgIF0sCiAgICAia2V5dHlwZSI6ICJlY2RzYSIsCiAgICAia2V5dmFsIjogewogICAgICJwdWJsaWMiOiAiLS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS1cbk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRXk4WEtzbWhCWURJOEpjMEd3ekJ4ZUtheDBjbTVcblNUS0VVNjVIUEZ1blVuNDFzVDhwaTBGak00SWtIei9ZVW13bUxVTzBXdDdseGhqNkJrTElLNHFZQXc9PVxuLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tXG4iCiAgICB9LAogICAgInNjaGVtZSI6ICJlY2RzYS1zaGEyLW5pc3RwMjU2IiwKICAgICJ4LXR1Zi1vbi1jaS1rZXlvd25lciI6ICJAZGxvcmVuYyIKICAgfSwKICAgIjcyNDdmMGRiYWQ4NWIxNDdlMTg2M2JhZGU3NjEyNDNjYzc4NWRjYjdhYTQxMGU3MTA1ZGQzZDJiNjFhMzZkMmMiOiB7CiAgICAia2V5aWRfaGFzaF9hbGdvcml0aG1zIjogWwogICAgICJzaGEyNTYiLAogICAgICJzaGE1MTIiCiAgICBdLAogICAgImtleXR5cGUiOiAiZWNkc2EiLAogICAgImtleXZhbCI6IHsKICAgICAicHVibGljIjogIi0tLS0tQkVHSU4gUFVCTElDIEtFWS0tLS0tXG5NRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVXUmlHcjUraiszSjVTc0grWnRyNW5FMkgyd083XG5CVituTzNzOTNnTGNhMThxVE96SFkxb1d5QUdEeWtNU3NHVFVCU3Q5RCtBbjBLZktzRDJtZlNNNDJRPT1cbi0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLVxuIgogICAgfSwKICAgICJzY2hlbWUiOiAiZWNkc2Etc2hhMi1uaXN0cDI1NiIsCiAgICAieC10dWYtb24tY2ktb25saW5lLXVyaSI6ICJnY3BrbXM6Ly9wcm9qZWN0cy9zaWdzdG9yZS1yb290LXNpZ25pbmcvbG9jYXRpb25zL2dsb2JhbC9rZXlSaW5ncy9yb290L2NyeXB0b0tleXMvdGltZXN0YW1wIgogICB9LAogICAiYTY4N2U1YmY0ZmFiODJiMGVlNThkNDZlMDVjOTUzNTE0NWEyYzlhZmI0NThmNDNkNDJiNDVjYTBmZGNlMmE3MCI6IHsKICAgICJrZXlpZF9oYXNoX2FsZ29yaXRobXMiOiBbCiAgICAgInNoYTI1NiIsCiAgICAgInNoYTUxMiIKICAgIF0sCiAgICAia2V5dHlwZSI6ICJlY2RzYSIsCiAgICAia2V5dmFsIjogewogICAgICJwdWJsaWMiOiAiLS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS1cbk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRTBnaHJoOTJMdzFZcjNpZEdWNVdxQ3RNREI4Q3hcbitEOGhkQzR3MlpMTklwbFZSb1ZHTHNrWWEzZ2hlTXlPamlKOGtQaTE1YVEyLy83UCtvajdVdkpQR3c9PVxuLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tXG4iCiAgICB9LAogICAgInNjaGVtZSI6ICJlY2RzYS1zaGEyLW5pc3RwMjU2IiwKICAgICJ4LXR1Zi1vbi1jaS1rZXlvd25lciI6ICJAam9zaHVhZ2wiCiAgIH0sCiAgICJlNzFhNTRkNTQzODM1YmE4NmFkYWQ5NDYwMzc5Yzc2NDFmYjg3MjZkMTY0ZWE3NjY4MDFhMWM1MjJhYmE3ZWEyIjogewogICAgImtleWlkX2hhc2hfYWxnb3JpdGhtcyI6IFsKICAgICAic2hhMjU2IiwKICAgICAic2hhNTEyIgogICAgXSwKICAgICJrZXl0eXBlIjogImVjZHNhIiwKICAgICJrZXl2YWwiOiB7CiAgICAgInB1YmxpYyI6ICItLS0tLUJFR0lOIFBVQkxJQyBLRVktLS0tLVxuTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFRVhzejNTWlhGYjhqTVY0Mmo2cEpseWpialI4S1xuTjNCd29jZXhxNkxNSWI1cXNXS09RdkxOMTZOVWVmTGM0SHN3T291bVJzVlZhYWpTcFFTNmZvYmtSdz09XG4tLS0tLUVORCBQVUJMSUMgS0VZLS0tLS1cbiIKICAgIH0sCiAgICAic2NoZW1lIjogImVjZHNhLXNoYTItbmlzdHAyNTYiLAogICAgIngtdHVmLW9uLWNpLWtleW93bmVyIjogIkBtbm02NzgiCiAgIH0KICB9LAogICJyb2xlcyI6IHsKICAgInJvb3QiOiB7CiAgICAia2V5aWRzIjogWwogICAgICI2ZjI2MDA4OWQ1OTIzZGFmMjAxNjZjYTY1N2M1NDNhZjYxODM0NmFiOTcxODg0YTk5OTYyYjAxOTg4YmJlMGMzIiwKICAgICAiZTcxYTU0ZDU0MzgzNWJhODZhZGFkOTQ2MDM3OWM3NjQxZmI4NzI2ZDE2NGVhNzY2ODAxYTFjNTIyYWJhN2VhMiIsCiAgICAgIjIyZjRjYWVjNmQ4ZTZmOTU1NWFmNjZiM2Q0YzNjYjA2YTNiYjIzZmRjN2UzOWM5MTZjNjFmNDYyZTZmNTJiMDYiLAogICAgICI2MTY0MzgzODEyNWI0NDBiNDBkYjY5NDJmNWNiNWEzMWMwZGMwNDM2ODMxNmViMmFhYTU4Yjk1OTA0YTU4MjIyIiwKICAgICAiYTY4N2U1YmY0ZmFiODJiMGVlNThkNDZlMDVjOTUzNTE0NWEyYzlhZmI0NThmNDNkNDJiNDVjYTBmZGNlMmE3MCIKICAgIF0sCiAgICAidGhyZXNob2xkIjogMwogICB9LAogICAic25hcHNob3QiOiB7CiAgICAia2V5aWRzIjogWwogICAgICI3MjQ3ZjBkYmFkODViMTQ3ZTE4NjNiYWRlNzYxMjQzY2M3ODVkY2I3YWE0MTBlNzEwNWRkM2QyYjYxYTM2ZDJjIgogICAgXSwKICAgICJ0aHJlc2hvbGQiOiAxLAogICAgIngtdHVmLW9uLWNpLWV4cGlyeS1wZXJpb2QiOiAzNjUwLAogICAgIngtdHVmLW9uLWNpLXNpZ25pbmctcGVyaW9kIjogMzY1CiAgIH0sCiAgICJ0YXJnZXRzIjogewogICAgImtleWlkcyI6IFsKICAgICAiNmYyNjAwODlkNTkyM2RhZjIwMTY2Y2E2NTdjNTQzYWY2MTgzNDZhYjk3MTg4NGE5OTk2MmIwMTk4OGJiZTBjMyIsCiAgICAgImU3MWE1NGQ1NDM4MzViYTg2YWRhZDk0NjAzNzljNzY0MWZiODcyNmQxNjRlYTc2NjgwMWExYzUyMmFiYTdlYTIiLAogICAgICIyMmY0Y2FlYzZkOGU2Zjk1NTVhZjY2YjNkNGMzY2IwNmEzYmIyM2ZkYzdlMzljOTE2YzYxZjQ2MmU2ZjUyYjA2IiwKICAgICAiNjE2NDM4MzgxMjViNDQwYjQwZGI2OTQyZjVjYjVhMzFjMGRjMDQzNjgzMTZlYjJhYWE1OGI5NTkwNGE1ODIyMiIsCiAgICAgImE2ODdlNWJmNGZhYjgyYjBlZTU4ZDQ2ZTA1Yzk1MzUxNDVhMmM5YWZiNDU4ZjQzZDQyYjQ1Y2EwZmRjZTJhNzAiCiAgICBdLAogICAgInRocmVzaG9sZCI6IDMKICAgfSwKICAgInRpbWVzdGFtcCI6IHsKICAgICJrZXlpZHMiOiBbCiAgICAgIjcyNDdmMGRiYWQ4NWIxNDdlMTg2M2JhZGU3NjEyNDNjYzc4NWRjYjdhYTQxMGU3MTA1ZGQzZDJiNjFhMzZkMmMiCiAgICBdLAogICAgInRocmVzaG9sZCI6IDEsCiAgICAieC10dWYtb24tY2ktZXhwaXJ5LXBlcmlvZCI6IDcsCiAgICAieC10dWYtb24tY2ktc2lnbmluZy1wZXJpb2QiOiA0CiAgIH0KICB9LAogICJzcGVjX3ZlcnNpb24iOiAiMS4wIiwKICAidmVyc2lvbiI6IDEwLAogICJ4LXR1Zi1vbi1jaS1leHBpcnktcGVyaW9kIjogMTgyLAogICJ4LXR1Zi1vbi1jaS1zaWduaW5nLXBlcmlvZCI6IDMxCiB9Cn0=",targets:{"trusted_root.json":"ewogICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRldi5zaWdzdG9yZS50cnVzdGVkcm9vdCtqc29uO3ZlcnNpb249MC4xIiwKICAidGxvZ3MiOiBbCiAgICB7CiAgICAgICJiYXNlVXJsIjogImh0dHBzOi8vcmVrb3Iuc2lnc3RvcmUuZGV2IiwKICAgICAgImhhc2hBbGdvcml0aG0iOiAiU0hBMl8yNTYiLAogICAgICAicHVibGljS2V5IjogewogICAgICAgICJyYXdCeXRlcyI6ICJNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUUyRzJZKzJ0YWJkVFY1QmNHaUJJeDBhOWZBRndya0JibUxTR3RrczRMM3FYNnlZWTB6dWZCbmhDOFVyL2l5NTVHaFdQLzlBL2JZMkxoQzMwTTkrUll0dz09IiwKICAgICAgICAia2V5RGV0YWlscyI6ICJQS0lYX0VDRFNBX1AyNTZfU0hBXzI1NiIsCiAgICAgICAgInZhbGlkRm9yIjogewogICAgICAgICAgInN0YXJ0IjogIjIwMjEtMDEtMTJUMTE6NTM6MjcuMDAwWiIKICAgICAgICB9CiAgICAgIH0sCiAgICAgICJsb2dJZCI6IHsKICAgICAgICAia2V5SWQiOiAid05JOWF0UUdseitWV2ZPNkxSeWdINFFVZlkvOFc0UkZ3aVQ1aTVXUmdCMD0iCiAgICAgIH0KICAgIH0KICBdLAogICJjZXJ0aWZpY2F0ZUF1dGhvcml0aWVzIjogWwogICAgewogICAgICAic3ViamVjdCI6IHsKICAgICAgICAib3JnYW5pemF0aW9uIjogInNpZ3N0b3JlLmRldiIsCiAgICAgICAgImNvbW1vbk5hbWUiOiAic2lnc3RvcmUiCiAgICAgIH0sCiAgICAgICJ1cmkiOiAiaHR0cHM6Ly9mdWxjaW8uc2lnc3RvcmUuZGV2IiwKICAgICAgImNlcnRDaGFpbiI6IHsKICAgICAgICAiY2VydGlmaWNhdGVzIjogWwogICAgICAgICAgewogICAgICAgICAgICAicmF3Qnl0ZXMiOiAiTUlJQitEQ0NBWDZnQXdJQkFnSVROVmtEWm9DaW9mUERzeTdkZm02Z2VMYnVoekFLQmdncWhrak9QUVFEQXpBcU1SVXdFd1lEVlFRS0V3eHphV2R6ZEc5eVpTNWtaWFl4RVRBUEJnTlZCQU1UQ0hOcFozTjBiM0psTUI0WERUSXhNRE13TnpBek1qQXlPVm9YRFRNeE1ESXlNekF6TWpBeU9Wb3dLakVWTUJNR0ExVUVDaE1NYzJsbmMzUnZjbVV1WkdWMk1SRXdEd1lEVlFRREV3aHphV2R6ZEc5eVpUQjJNQkFHQnlxR1NNNDlBZ0VHQlN1QkJBQWlBMklBQkxTeUE3SWk1aytwTk84WkVXWTB5bGVtV0Rvd09rTmEza0wrR1pFNVo1R1dlaEw5L0E5YlJOQTNSYnJzWjVpMEpjYXN0YVJMN1NwNWZwL2pENWR4cWMvVWRUVm5sdlMxNmFuKzJZZnN3ZS9RdUxvbFJVQ3JjT0UyKzJpQTUrdHpkNk5tTUdRd0RnWURWUjBQQVFIL0JBUURBZ0VHTUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFFd0hRWURWUjBPQkJZRUZNakZIUUJCbWlRcE1sRWs2dzJ1U3UxS0J0UHNNQjhHQTFVZEl3UVlNQmFBRk1qRkhRQkJtaVFwTWxFazZ3MnVTdTFLQnRQc01Bb0dDQ3FHU000OUJBTURBMmdBTUdVQ01IOGxpV0pmTXVpNnZYWEJoakRnWTRNd3NsbU4vVEp4VmUvODNXckZvbXdtTmYwNTZ5MVg0OEY5YzRtM2Ezb3pYQUl4QUtqUmF5NS9hai9qc0tLR0lrbVFhdGpJOHV1cEhyLytDeEZ2YUpXbXBZcU5rTERHUlUrOW9yemg1aEkyUnJjdWFRPT0iCiAgICAgICAgICB9CiAgICAgICAgXQogICAgICB9LAogICAgICAidmFsaWRGb3IiOiB7CiAgICAgICAgInN0YXJ0IjogIjIwMjEtMDMtMDdUMDM6MjA6MjkuMDAwWiIsCiAgICAgICAgImVuZCI6ICIyMDIyLTEyLTMxVDIzOjU5OjU5Ljk5OVoiCiAgICAgIH0KICAgIH0sCiAgICB7CiAgICAgICJzdWJqZWN0IjogewogICAgICAgICJvcmdhbml6YXRpb24iOiAic2lnc3RvcmUuZGV2IiwKICAgICAgICAiY29tbW9uTmFtZSI6ICJzaWdzdG9yZSIKICAgICAgfSwKICAgICAgInVyaSI6ICJodHRwczovL2Z1bGNpby5zaWdzdG9yZS5kZXYiLAogICAgICAiY2VydENoYWluIjogewogICAgICAgICJjZXJ0aWZpY2F0ZXMiOiBbCiAgICAgICAgICB7CiAgICAgICAgICAgICJyYXdCeXRlcyI6ICJNSUlDR2pDQ0FhR2dBd0lCQWdJVUFMblZpVmZuVTBickphc21Sa0hybi9VbmZhUXdDZ1lJS29aSXpqMEVBd013S2pFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUkV3RHdZRFZRUURFd2h6YVdkemRHOXlaVEFlRncweU1qQTBNVE15TURBMk1UVmFGdzB6TVRFd01EVXhNelUyTlRoYU1EY3hGVEFUQmdOVkJBb1RESE5wWjNOMGIzSmxMbVJsZGpFZU1Cd0dBMVVFQXhNVmMybG5jM1J2Y21VdGFXNTBaWEp0WldScFlYUmxNSFl3RUFZSEtvWkl6ajBDQVFZRks0RUVBQ0lEWWdBRThSVlMveXNIK05PdnVEWnlQSVp0aWxnVUY5TmxhcllwQWQ5SFAxdkJCSDFVNUNWNzdMU1M3czBaaUg0bkU3SHY3cHRTNkx2dlIvU1RrNzk4TFZnTXpMbEo0SGVJZkYzdEhTYWV4TGNZcFNBU3Ixa1MwTi9SZ0JKei85aldDaVhubzNzd2VUQU9CZ05WSFE4QkFmOEVCQU1DQVFZd0V3WURWUjBsQkF3d0NnWUlLd1lCQlFVSEF3TXdFZ1lEVlIwVEFRSC9CQWd3QmdFQi93SUJBREFkQmdOVkhRNEVGZ1FVMzlQcHoxWWtFWmI1cU5qcEtGV2l4aTRZWkQ4d0h3WURWUjBqQkJnd0ZvQVVXTUFlWDVGRnBXYXBlc3lRb1pNaTBDckZ4Zm93Q2dZSUtvWkl6ajBFQXdNRFp3QXdaQUl3UENzUUs0RFlpWllEUElhRGk1SEZLbmZ4WHg2QVNTVm1FUmZzeW5ZQmlYMlg2U0pSblpVODQvOURaZG5GdnZ4bUFqQk90NlFwQmxjNEovMER4dmtUQ3FwY2x2emlMNkJDQ1BuamRsSUIzUHUzQnhzUG15Z1VZN0lpMnpiZENkbGlpb3c9IgogICAgICAgICAgfSwKICAgICAgICAgIHsKICAgICAgICAgICAgInJhd0J5dGVzIjogIk1JSUI5ekNDQVh5Z0F3SUJBZ0lVQUxaTkFQRmR4SFB3amVEbG9Ed3lZQ2hBTy80d0NnWUlLb1pJemowRUF3TXdLakVWTUJNR0ExVUVDaE1NYzJsbmMzUnZjbVV1WkdWMk1SRXdEd1lEVlFRREV3aHphV2R6ZEc5eVpUQWVGdzB5TVRFd01EY3hNelUyTlRsYUZ3MHpNVEV3TURVeE16VTJOVGhhTUNveEZUQVRCZ05WQkFvVERITnBaM04wYjNKbExtUmxkakVSTUE4R0ExVUVBeE1JYzJsbmMzUnZjbVV3ZGpBUUJnY3Foa2pPUFFJQkJnVXJnUVFBSWdOaUFBVDdYZUZUNHJiM1BRR3dTNElhanRMazMvT2xucGdhbmdhQmNsWXBzWUJyNWkrNHluQjA3Y2ViM0xQME9JT1pkeGV4WDY5YzVpVnV5SlJRK0h6MDV5aStVRjN1QldBbEhwaVM1c2gwK0gyR0hFN1NYcmsxRUM1bTFUcjE5TDlnZzkyall6QmhNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01CMEdBMVVkRGdRV0JCUll3QjVma1VXbFpxbDZ6SkNoa3lMUUtzWEYrakFmQmdOVkhTTUVHREFXZ0JSWXdCNWZrVVdsWnFsNnpKQ2hreUxRS3NYRitqQUtCZ2dxaGtqT1BRUURBd05wQURCbUFqRUFqMW5IZVhacCsxM05XQk5hK0VEc0RQOEcxV1dnMXRDTVdQL1dIUHFwYVZvMGpoc3dlTkZaZ1NzMGVFN3dZSTRxQWpFQTJXQjlvdDk4c0lrb0YzdlpZZGQzL1Z0V0I1YjlUTk1lYTdJeC9zdEo1VGZjTExlQUJMRTRCTkpPc1E0dm5CSEoiCiAgICAgICAgICB9CiAgICAgICAgXQogICAgICB9LAogICAgICAidmFsaWRGb3IiOiB7CiAgICAgICAgInN0YXJ0IjogIjIwMjItMDQtMTNUMjA6MDY6MTUuMDAwWiIKICAgICAgfQogICAgfQogIF0sCiAgImN0bG9ncyI6IFsKICAgIHsKICAgICAgImJhc2VVcmwiOiAiaHR0cHM6Ly9jdGZlLnNpZ3N0b3JlLmRldi90ZXN0IiwKICAgICAgImhhc2hBbGdvcml0aG0iOiAiU0hBMl8yNTYiLAogICAgICAicHVibGljS2V5IjogewogICAgICAgICJyYXdCeXRlcyI6ICJNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUViZndSK1JKdWRYc2NnUkJScEtYMVhGRHkzUHl1ZER4ei9TZm5SaTFmVDhla3BmQmQyTzF1b3o3anIzWjhuS3p4QTY5RVVRK2VGQ0ZJM3pldWJQV1U3dz09IiwKICAgICAgICAia2V5RGV0YWlscyI6ICJQS0lYX0VDRFNBX1AyNTZfU0hBXzI1NiIsCiAgICAgICAgInZhbGlkRm9yIjogewogICAgICAgICAgInN0YXJ0IjogIjIwMjEtMDMtMTRUMDA6MDA6MDAuMDAwWiIsCiAgICAgICAgICAiZW5kIjogIjIwMjItMTAtMzFUMjM6NTk6NTkuOTk5WiIKICAgICAgICB9CiAgICAgIH0sCiAgICAgICJsb2dJZCI6IHsKICAgICAgICAia2V5SWQiOiAiQ0dDUzhDaFMvMmhGMGRGcko0U2NSV2NZckJZOXd6alNiZWE4SWdZMmIzST0iCiAgICAgIH0KICAgIH0sCiAgICB7CiAgICAgICJiYXNlVXJsIjogImh0dHBzOi8vY3RmZS5zaWdzdG9yZS5kZXYvMjAyMiIsCiAgICAgICJoYXNoQWxnb3JpdGhtIjogIlNIQTJfMjU2IiwKICAgICAgInB1YmxpY0tleSI6IHsKICAgICAgICAicmF3Qnl0ZXMiOiAiTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFaVBTbEZpMENtRlRmRWpDVXFGOUh1Q0VjWVhOS0FhWWFsSUptQlo4eXllelBqVHFoeHJLQnBNbmFvY1Z0TEpCSTFlTTN1WG5RelFHQUpkSjRnczlGeXc9PSIsCiAgICAgICAgImtleURldGFpbHMiOiAiUEtJWF9FQ0RTQV9QMjU2X1NIQV8yNTYiLAogICAgICAgICJ2YWxpZEZvciI6IHsKICAgICAgICAgICJzdGFydCI6ICIyMDIyLTEwLTIwVDAwOjAwOjAwLjAwMFoiCiAgICAgICAgfQogICAgICB9LAogICAgICAibG9nSWQiOiB7CiAgICAgICAgImtleUlkIjogIjNUMHdhc2JIRVRKakdSNGNtV2MzQXFKS1hyamVQSzMvaDRweWdDOHA3bzQ9IgogICAgICB9CiAgICB9CiAgXSwKICAidGltZXN0YW1wQXV0aG9yaXRpZXMiOiBbCiAgICB7CiAgICAgICJzdWJqZWN0IjogewogICAgICAgICJvcmdhbml6YXRpb24iOiAiR2l0SHViLCBJbmMuIiwKICAgICAgICAiY29tbW9uTmFtZSI6ICJJbnRlcm5hbCBTZXJ2aWNlcyBSb290IgogICAgICB9LAogICAgICAiY2VydENoYWluIjogewogICAgICAgICJjZXJ0aWZpY2F0ZXMiOiBbCiAgICAgICAgICB7CiAgICAgICAgICAgICJyYXdCeXRlcyI6ICJNSUlCM0RDQ0FXS2dBd0lCQWdJVWNoa05zSDM2WGEwNGIxTHFJYytxcjlEVmVjTXdDZ1lJS29aSXpqMEVBd013TWpFVk1CTUdBMVVFQ2hNTVIybDBTSFZpTENCSmJtTXVNUmt3RndZRFZRUURFeEJVVTBFZ2FXNTBaWEp0WldScFlYUmxNQjRYRFRJek1EUXhOREF3TURBd01Gb1hEVEkwTURReE16QXdNREF3TUZvd01qRVZNQk1HQTFVRUNoTU1SMmwwU0hWaUxDQkpibU11TVJrd0Z3WURWUVFERXhCVVUwRWdWR2x0WlhOMFlXMXdhVzVuTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFVUQ1Wk5iU3FZTWQ2cjhxcE9PRVg5aWJHblpUOUdzdVhPaHIvZjhVOUZKdWdCR0V4S1lwNDBPVUxTMGVyalpXN3hWOXhWNTJObkpmNU9lRHE0ZTVaS3FOV01GUXdEZ1lEVlIwUEFRSC9CQVFEQWdlQU1CTUdBMVVkSlFRTU1Bb0dDQ3NHQVFVRkJ3TUlNQXdHQTFVZEV3RUIvd1FDTUFBd0h3WURWUjBqQkJnd0ZvQVVhVzFSdWRPZ1Z0MGxlcVkwV0tZYnVQcjQ3d0F3Q2dZSUtvWkl6ajBFQXdNRGFBQXdaUUl3YlVIOUh2RDRlakNaSk9XUW5xQWxrcVVSbGx2dTlNOCtWcUxiaVJLK3pTZlpDWndzaWxqUm44TVFRUlNrWEVFNUFqRUFnK1Z4cXRvamZWZnU4RGh6emhDeDlHS0VUYkpIYjE5aVY3Mm1NS1ViREFGbXpaNmJROGI1NFpiOHRpZHk1YVdlIgogICAgICAgICAgfSwKICAgICAgICAgIHsKICAgICAgICAgICAgInJhd0J5dGVzIjogIk1JSUNFRENDQVpXZ0F3SUJBZ0lVWDhaTzVRWFA3dk40ZE1RNWU5c1UzbnViOE9nd0NnWUlLb1pJemowRUF3TXdPREVWTUJNR0ExVUVDaE1NUjJsMFNIVmlMQ0JKYm1NdU1SOHdIUVlEVlFRREV4WkpiblJsY201aGJDQlRaWEoyYVdObGN5QlNiMjkwTUI0WERUSXpNRFF4TkRBd01EQXdNRm9YRFRJNE1EUXhNakF3TURBd01Gb3dNakVWTUJNR0ExVUVDaE1NUjJsMFNIVmlMQ0JKYm1NdU1Sa3dGd1lEVlFRREV4QlVVMEVnYVc1MFpYSnRaV1JwWVhSbE1IWXdFQVlIS29aSXpqMENBUVlGSzRFRUFDSURZZ0FFdk1MWS9kVFZidklKWUFOQXVzekV3Sm5RRTFsbGZ0eW55TUtJTWhoNDhIbXFiVnI1eWd5YnpzTFJMVktiQldPZFoyMWFlSnorZ1ppeXRaZXRxY3lGOVdsRVI1TkVNZjZKVjdaTm9qUXB4SHE0UkhHb0dTY2VRdi9xdlRpWnhFREtvMll3WkRBT0JnTlZIUThCQWY4RUJBTUNBUVl3RWdZRFZSMFRBUUgvQkFnd0JnRUIvd0lCQURBZEJnTlZIUTRFRmdRVWFXMVJ1ZE9nVnQwbGVxWTBXS1lidVByNDd3QXdId1lEVlIwakJCZ3dGb0FVOU5ZWWxvYm5BRzRjMC9xanh5SC9scS93eitRd0NnWUlLb1pJemowRUF3TURhUUF3WmdJeEFLMUIxODV5Z0NySVlGbElzM0dqc3dqbndTTUc2TFk4d29MVmRha0tEWnhWYThmOGNxTXMxRGhjeEowKzA5dzk1UUl4QU8rdEJ6Wms3dmpVSjlpSmdENFI2WldUeFFXS3FObTc0ak85OW8rbzlzdjRGSS9TWlRaVEZ5TW4wSUpFSGRObXlBPT0iCiAgICAgICAgICB9LAogICAgICAgICAgewogICAgICAgICAgICAicmF3Qnl0ZXMiOiAiTUlJQjlEQ0NBWHFnQXdJQkFnSVVhL0pBa2RVaks0SlV3c3F0YWlSSkdXaHFMU293Q2dZSUtvWkl6ajBFQXdNd09ERVZNQk1HQTFVRUNoTU1SMmwwU0hWaUxDQkpibU11TVI4d0hRWURWUVFERXhaSmJuUmxjbTVoYkNCVFpYSjJhV05sY3lCU2IyOTBNQjRYRFRJek1EUXhOREF3TURBd01Gb1hEVE16TURReE1UQXdNREF3TUZvd09ERVZNQk1HQTFVRUNoTU1SMmwwU0hWaUxDQkpibU11TVI4d0hRWURWUVFERXhaSmJuUmxjbTVoYkNCVFpYSjJhV05sY3lCU2IyOTBNSFl3RUFZSEtvWkl6ajBDQVFZRks0RUVBQ0lEWWdBRWY5akZBWHh6NGt4NjhBSFJNT2tGQmhmbERjTVR2emFYejR4L0ZDY1hqSi8xcUVLb24vcVBJR25hVVJza0R0eU5iTkRPcGVKVERERnF0NDhpTVBybnpweDZJWndxZW1mVUpONHhCRVpmemErcFl0L2l5b2QrOXRacjIwUlJXU3YvbzBVd1F6QU9CZ05WSFE4QkFmOEVCQU1DQVFZd0VnWURWUjBUQVFIL0JBZ3dCZ0VCL3dJQkFqQWRCZ05WSFE0RUZnUVU5TllZbG9ibkFHNGMwL3FqeHlIL2xxL3d6K1F3Q2dZSUtvWkl6ajBFQXdNRGFBQXdaUUl4QUxaTFo4QmdSWHpLeExNTU45VklsTytlNGhyQm5OQmdGN3R6N0hucm93djJOZXRaRXJJQUNLRnltQmx2V0R2dE1BSXdaTytraTZzc1ExYnNabzk4TzhtRUFmMk5aN2lpQ2dERFUwVndqZWNvNnp5ZWgwekJUczkvN2dWNkFITlE1M3hEIgogICAgICAgICAgfQogICAgICAgIF0KICAgICAgfSwKICAgICAgInZhbGlkRm9yIjogewogICAgICAgICJzdGFydCI6ICIyMDIzLTA0LTE0VDAwOjAwOjAwLjAwMFoiCiAgICAgIH0KICAgIH0KICBdCn0K","registry.npmjs.org%2Fkeys.json":"ewogICAgImtleXMiOiBbCiAgICAgICAgewogICAgICAgICAgICAia2V5SWQiOiAiU0hBMjU2OmpsM2J3c3d1ODBQampva0NnaDBvMnc1YzJVNExoUUFFNTdnajljejFrekEiLAogICAgICAgICAgICAia2V5VXNhZ2UiOiAibnBtOnNpZ25hdHVyZXMiLAogICAgICAgICAgICAicHVibGljS2V5IjogewogICAgICAgICAgICAgICAgInJhd0J5dGVzIjogIk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRTFPbGIzek1BRkZ4WEtIaUlrUU81Y0ozWWhsNWk2VVBwK0lodXRlQkpidUhjQTVVb2dLbzBFV3RsV3dXNktTYUtvVE5FWUw3SmxDUWlWbmtoQmt0VWdnPT0iLAogICAgICAgICAgICAgICAgImtleURldGFpbHMiOiAiUEtJWF9FQ0RTQV9QMjU2X1NIQV8yNTYiLAogICAgICAgICAgICAgICAgInZhbGlkRm9yIjogewogICAgICAgICAgICAgICAgICAgICJzdGFydCI6ICIxOTk5LTAxLTAxVDAwOjAwOjAwLjAwMFoiLAogICAgICAgICAgICAgICAgICAgICJlbmQiOiAiMjAyNS0wMS0yOVQwMDowMDowMC4wMDBaIgogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJrZXlJZCI6ICJTSEEyNTY6amwzYndzd3U4MFBqam9rQ2doMG8ydzVjMlU0TGhRQUU1N2dqOWN6MWt6QSIsCiAgICAgICAgICAgICJrZXlVc2FnZSI6ICJucG06YXR0ZXN0YXRpb25zIiwKICAgICAgICAgICAgInB1YmxpY0tleSI6IHsKICAgICAgICAgICAgICAgICJyYXdCeXRlcyI6ICJNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUUxT2xiM3pNQUZGeFhLSGlJa1FPNWNKM1lobDVpNlVQcCtJaHV0ZUJKYnVIY0E1VW9nS28wRVd0bFd3VzZLU2FLb1RORVlMN0psQ1FpVm5raEJrdFVnZz09IiwKICAgICAgICAgICAgICAgICJrZXlEZXRhaWxzIjogIlBLSVhfRUNEU0FfUDI1Nl9TSEFfMjU2IiwKICAgICAgICAgICAgICAgICJ2YWxpZEZvciI6IHsKICAgICAgICAgICAgICAgICAgICAic3RhcnQiOiAiMjAyMi0xMi0wMVQwMDowMDowMC4wMDBaIiwKICAgICAgICAgICAgICAgICAgICAiZW5kIjogIjIwMjUtMDEtMjlUMDA6MDA6MDAuMDAwWiIKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAia2V5SWQiOiAiU0hBMjU2OkRoUTh3UjVBUEJ2RkhMRi8rVGMrQVl2UE9kVHBjSURxT2h4c0JIUndDN1UiLAogICAgICAgICAgICAia2V5VXNhZ2UiOiAibnBtOnNpZ25hdHVyZXMiLAogICAgICAgICAgICAicHVibGljS2V5IjogewogICAgICAgICAgICAgICAgInJhd0J5dGVzIjogIk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRVk2WWE3VysrN2FVUHp2TVRyZXpINlljeDNjK0hPS1lDY05HeWJKWlNDSnEvZmQ3UWE4dXVBS3RkSWtVUXRRaUVLRVJoQW1FNWxNTUpoUDhPa0RPYTJnPT0iLAogICAgICAgICAgICAgICAgImtleURldGFpbHMiOiAiUEtJWF9FQ0RTQV9QMjU2X1NIQV8yNTYiLAogICAgICAgICAgICAgICAgInZhbGlkRm9yIjogewogICAgICAgICAgICAgICAgICAgICJzdGFydCI6ICIyMDI1LTAxLTEzVDAwOjAwOjAwLjAwMFoiCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICAgImtleUlkIjogIlNIQTI1NjpEaFE4d1I1QVBCdkZITEYvK1RjK0FZdlBPZFRwY0lEcU9oeHNCSFJ3QzdVIiwKICAgICAgICAgICAgImtleVVzYWdlIjogIm5wbTphdHRlc3RhdGlvbnMiLAogICAgICAgICAgICAicHVibGljS2V5IjogewogICAgICAgICAgICAgICAgInJhd0J5dGVzIjogIk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRVk2WWE3VysrN2FVUHp2TVRyZXpINlljeDNjK0hPS1lDY05HeWJKWlNDSnEvZmQ3UWE4dXVBS3RkSWtVUXRRaUVLRVJoQW1FNWxNTUpoUDhPa0RPYTJnPT0iLAogICAgICAgICAgICAgICAgImtleURldGFpbHMiOiAiUEtJWF9FQ0RTQV9QMjU2X1NIQV8yNTYiLAogICAgICAgICAgICAgICAgInZhbGlkRm9yIjogewogICAgICAgICAgICAgICAgICAgICJzdGFydCI6ICIyMDI1LTAxLTEzVDAwOjAwOjAwLjAwMFoiCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICBdCn0K"}}}});var qDe=G(u1=>{"use strict";var GDe=u1&&u1.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(u1,"__esModule",{value:!0});u1.TUFClient=void 0;var kd=GDe(Ie("fs")),qb=GDe(Ie("path")),h2t=_De(),d2t=ZO(),g2t=HDe(),EJ="targets",yJ=class{constructor(t){let r=new URL(t.mirrorURL),s=encodeURIComponent(r.host+r.pathname.replace(/\/$/,"")),a=qb.default.join(t.cachePath,s);m2t(a),y2t({cachePath:a,mirrorURL:t.mirrorURL,tufRootPath:t.rootPath,forceInit:t.forceInit}),this.updater=E2t({mirrorURL:t.mirrorURL,cachePath:a,forceCache:t.forceCache,retry:t.retry,timeout:t.timeout})}async refresh(){return this.updater.refresh()}getTarget(t){return(0,g2t.readTarget)(this.updater,t)}};u1.TUFClient=yJ;function m2t(e){let t=qb.default.join(e,EJ);kd.default.existsSync(e)||kd.default.mkdirSync(e,{recursive:!0}),kd.default.existsSync(t)||kd.default.mkdirSync(t)}function y2t({cachePath:e,mirrorURL:t,tufRootPath:r,forceInit:s}){let a=qb.default.join(e,"root.json");if(!kd.default.existsSync(a)||s)if(r)kd.default.copyFileSync(r,a);else{let c=jDe()[t];if(!c)throw new d2t.TUFError({code:"TUF_INIT_CACHE_ERROR",message:`No root.json found for mirror: ${t}`});kd.default.writeFileSync(a,Buffer.from(c["root.json"],"base64")),Object.entries(c.targets).forEach(([f,p])=>{kd.default.writeFileSync(qb.default.join(e,EJ,f),Buffer.from(p,"base64"))})}}function E2t(e){let t={fetchTimeout:e.timeout,fetchRetry:e.retry};return new h2t.Updater({metadataBaseUrl:e.mirrorURL,targetBaseUrl:`${e.mirrorURL}/targets`,metadataDir:e.cachePath,targetDir:qb.default.join(e.cachePath,EJ),forceCache:e.forceCache,config:t})}});var ZO=G(hh=>{"use strict";Object.defineProperty(hh,"__esModule",{value:!0});hh.TUFError=hh.DEFAULT_MIRROR_URL=void 0;hh.getTrustedRoot=b2t;hh.initTUF=P2t;var I2t=yb(),C2t=BSe(),w2t=qDe();hh.DEFAULT_MIRROR_URL="https://tuf-repo-cdn.sigstore.dev";var B2t="sigstore-js",v2t={retries:2},S2t=5e3,D2t="trusted_root.json";async function b2t(e={}){let r=await WDe(e).getTarget(D2t);return I2t.TrustedRoot.fromJSON(JSON.parse(r))}async function P2t(e={}){let t=WDe(e);return t.refresh().then(()=>t)}function WDe(e){return new w2t.TUFClient({cachePath:e.cachePath||(0,C2t.appDataPath)(B2t),rootPath:e.rootPath,mirrorURL:e.mirrorURL||hh.DEFAULT_MIRROR_URL,retry:e.retry??v2t,timeout:e.timeout??S2t,forceCache:e.forceCache??!1,forceInit:e.forceInit??e.force??!1})}var x2t=mJ();Object.defineProperty(hh,"TUFError",{enumerable:!0,get:function(){return x2t.TUFError}})});var YDe=G($O=>{"use strict";Object.defineProperty($O,"__esModule",{value:!0});$O.DSSESignatureContent=void 0;var Wb=Pl(),IJ=class{constructor(t){this.env=t}compareDigest(t){return Wb.crypto.bufferEqual(t,Wb.crypto.digest("sha256",this.env.payload))}compareSignature(t){return Wb.crypto.bufferEqual(t,this.signature)}verifySignature(t){return Wb.crypto.verify(this.preAuthEncoding,t,this.signature)}get signature(){return this.env.signatures.length>0?this.env.signatures[0].sig:Buffer.from("")}get preAuthEncoding(){return Wb.dsse.preAuthEncoding(this.env.payloadType,this.env.payload)}};$O.DSSESignatureContent=IJ});var VDe=G(eL=>{"use strict";Object.defineProperty(eL,"__esModule",{value:!0});eL.MessageSignatureContent=void 0;var CJ=Pl(),wJ=class{constructor(t,r){this.signature=t.signature,this.messageDigest=t.messageDigest.digest,this.artifact=r}compareSignature(t){return CJ.crypto.bufferEqual(t,this.signature)}compareDigest(t){return CJ.crypto.bufferEqual(t,this.messageDigest)}verifySignature(t){return CJ.crypto.verify(this.artifact,t,this.signature)}};eL.MessageSignatureContent=wJ});var KDe=G(tL=>{"use strict";Object.defineProperty(tL,"__esModule",{value:!0});tL.toSignedEntity=R2t;tL.signatureContent=JDe;var BJ=Pl(),k2t=YDe(),Q2t=VDe();function R2t(e,t){let{tlogEntries:r,timestampVerificationData:s}=e.verificationMaterial,a=[];for(let n of r)a.push({$case:"transparency-log",tlogEntry:n});for(let n of s?.rfc3161Timestamps??[])a.push({$case:"timestamp-authority",timestamp:BJ.RFC3161Timestamp.parse(n.signedTimestamp)});return{signature:JDe(e,t),key:T2t(e),tlogEntries:r,timestamps:a}}function JDe(e,t){switch(e.content.$case){case"dsseEnvelope":return new k2t.DSSESignatureContent(e.content.dsseEnvelope);case"messageSignature":return new Q2t.MessageSignatureContent(e.content.messageSignature,t)}}function T2t(e){switch(e.verificationMaterial.content.$case){case"publicKey":return{$case:"public-key",hint:e.verificationMaterial.content.publicKey.hint};case"x509CertificateChain":return{$case:"certificate",certificate:BJ.X509Certificate.parse(e.verificationMaterial.content.x509CertificateChain.certificates[0].rawBytes)};case"certificate":return{$case:"certificate",certificate:BJ.X509Certificate.parse(e.verificationMaterial.content.certificate.rawBytes)}}}});var So=G(f1=>{"use strict";Object.defineProperty(f1,"__esModule",{value:!0});f1.PolicyError=f1.VerificationError=void 0;var rL=class extends Error{constructor({code:t,message:r,cause:s}){super(r),this.code=t,this.cause=s,this.name=this.constructor.name}},vJ=class extends rL{};f1.VerificationError=vJ;var SJ=class extends rL{};f1.PolicyError=SJ});var zDe=G(nL=>{"use strict";Object.defineProperty(nL,"__esModule",{value:!0});nL.filterCertAuthorities=F2t;nL.filterTLogAuthorities=N2t;function F2t(e,t){return e.filter(r=>r.validFor.start<=t.start&&r.validFor.end>=t.end)}function N2t(e,t){return e.filter(r=>t.logID&&!r.logID.equals(t.logID)?!1:r.validFor.start<=t.targetDate&&t.targetDate<=r.validFor.end)}});var gy=G(dy=>{"use strict";Object.defineProperty(dy,"__esModule",{value:!0});dy.filterTLogAuthorities=dy.filterCertAuthorities=void 0;dy.toTrustMaterial=L2t;var DJ=Pl(),Yb=yb(),O2t=So(),bJ=new Date(0),PJ=new Date(864e13),$De=zDe();Object.defineProperty(dy,"filterCertAuthorities",{enumerable:!0,get:function(){return $De.filterCertAuthorities}});Object.defineProperty(dy,"filterTLogAuthorities",{enumerable:!0,get:function(){return $De.filterTLogAuthorities}});function L2t(e,t){let r=typeof t=="function"?t:M2t(t);return{certificateAuthorities:e.certificateAuthorities.map(ZDe),timestampAuthorities:e.timestampAuthorities.map(ZDe),tlogs:e.tlogs.map(XDe),ctlogs:e.ctlogs.map(XDe),publicKey:r}}function XDe(e){let t=e.publicKey.keyDetails,r=t===Yb.PublicKeyDetails.PKCS1_RSA_PKCS1V5||t===Yb.PublicKeyDetails.PKIX_RSA_PKCS1V5||t===Yb.PublicKeyDetails.PKIX_RSA_PKCS1V15_2048_SHA256||t===Yb.PublicKeyDetails.PKIX_RSA_PKCS1V15_3072_SHA256||t===Yb.PublicKeyDetails.PKIX_RSA_PKCS1V15_4096_SHA256?"pkcs1":"spki";return{logID:e.logId.keyId,publicKey:DJ.crypto.createPublicKey(e.publicKey.rawBytes,r),validFor:{start:e.publicKey.validFor?.start||bJ,end:e.publicKey.validFor?.end||PJ}}}function ZDe(e){return{certChain:e.certChain.certificates.map(t=>DJ.X509Certificate.parse(t.rawBytes)),validFor:{start:e.validFor?.start||bJ,end:e.validFor?.end||PJ}}}function M2t(e){return t=>{let r=(e||{})[t];if(!r)throw new O2t.VerificationError({code:"PUBLIC_KEY_ERROR",message:`key not found: ${t}`});return{publicKey:DJ.crypto.createPublicKey(r.rawBytes),validFor:s=>(r.validFor?.start||bJ)<=s&&(r.validFor?.end||PJ)>=s}}}});var xJ=G(Vb=>{"use strict";Object.defineProperty(Vb,"__esModule",{value:!0});Vb.CertificateChainVerifier=void 0;Vb.verifyCertificateChain=_2t;var my=So(),U2t=gy();function _2t(e,t){let r=(0,U2t.filterCertAuthorities)(t,{start:e.notBefore,end:e.notAfter}),s;for(let a of r)try{return new iL({trustedCerts:a.certChain,untrustedCert:e}).verify()}catch(n){s=n}throw new my.VerificationError({code:"CERTIFICATE_ERROR",message:"Failed to verify certificate chain",cause:s})}var iL=class{constructor(t){this.untrustedCert=t.untrustedCert,this.trustedCerts=t.trustedCerts,this.localCerts=H2t([...t.trustedCerts,t.untrustedCert])}verify(){let t=this.sort();return this.checkPath(t),t}sort(){let t=this.untrustedCert,r=this.buildPaths(t);if(r=r.filter(a=>a.some(n=>this.trustedCerts.includes(n))),r.length===0)throw new my.VerificationError({code:"CERTIFICATE_ERROR",message:"no trusted certificate path found"});let s=r.reduce((a,n)=>a.length{if(s&&a.extSubjectKeyID){a.extSubjectKeyID.keyIdentifier.equals(s)&&r.push(a);return}a.subject.equals(t.issuer)&&r.push(a)}),r=r.filter(a=>{try{return t.verify(a)}catch{return!1}}),r)}checkPath(t){if(t.length<1)throw new my.VerificationError({code:"CERTIFICATE_ERROR",message:"certificate chain must contain at least one certificate"});if(!t.slice(1).every(s=>s.isCA))throw new my.VerificationError({code:"CERTIFICATE_ERROR",message:"intermediate certificate is not a CA"});for(let s=t.length-2;s>=0;s--)if(!t[s].issuer.equals(t[s+1].subject))throw new my.VerificationError({code:"CERTIFICATE_ERROR",message:"incorrect certificate name chaining"});for(let s=0;s{"use strict";Object.defineProperty(kJ,"__esModule",{value:!0});kJ.verifySCTs=q2t;var sL=Pl(),j2t=So(),G2t=gy();function q2t(e,t,r){let s,a=e.clone();for(let p=0;p{if(!(0,G2t.filterTLogAuthorities)(r,{logID:p.logID,targetDate:p.datetime}).some(C=>p.verify(n.buffer,C.publicKey)))throw new j2t.VerificationError({code:"CERTIFICATE_ERROR",message:"SCT verification failed"});return p.logID})}});var rbe=G(oL=>{"use strict";Object.defineProperty(oL,"__esModule",{value:!0});oL.verifyPublicKey=z2t;oL.verifyCertificate=X2t;var W2t=Pl(),tbe=So(),Y2t=xJ(),V2t=ebe(),J2t="1.3.6.1.4.1.57264.1.1",K2t="1.3.6.1.4.1.57264.1.8";function z2t(e,t,r){let s=r.publicKey(e);return t.forEach(a=>{if(!s.validFor(a))throw new tbe.VerificationError({code:"PUBLIC_KEY_ERROR",message:`Public key is not valid for timestamp: ${a.toISOString()}`})}),{key:s.publicKey}}function X2t(e,t,r){let s=(0,Y2t.verifyCertificateChain)(e,r.certificateAuthorities);if(!t.every(n=>s.every(c=>c.validForDate(n))))throw new tbe.VerificationError({code:"CERTIFICATE_ERROR",message:"certificate is not valid or expired at the specified date"});return{scts:(0,V2t.verifySCTs)(s[0],s[1],r.ctlogs),signer:Z2t(s[0])}}function Z2t(e){let t,r=e.extension(K2t);r?t=r.valueObj.subs?.[0]?.value.toString("ascii"):t=e.extension(J2t)?.value.toString("ascii");let s={extensions:{issuer:t},subjectAlternativeName:e.subjectAltName};return{key:W2t.crypto.createPublicKey(e.publicKey),identity:s}}});var ibe=G(aL=>{"use strict";Object.defineProperty(aL,"__esModule",{value:!0});aL.verifySubjectAlternativeName=$2t;aL.verifyExtensions=eBt;var nbe=So();function $2t(e,t){if(t===void 0||!t.match(e))throw new nbe.PolicyError({code:"UNTRUSTED_SIGNER_ERROR",message:`certificate identity error - expected ${e}, got ${t}`})}function eBt(e,t={}){let r;for(r in e)if(t[r]!==e[r])throw new nbe.PolicyError({code:"UNTRUSTED_SIGNER_ERROR",message:`invalid certificate extension - expected ${r}=${e[r]}, got ${r}=${t[r]}`})}});var sbe=G(NJ=>{"use strict";Object.defineProperty(NJ,"__esModule",{value:!0});NJ.verifyCheckpoint=nBt;var RJ=Pl(),A1=So(),tBt=gy(),QJ=` `,rBt=/\u2014 (\S+) (\S+)\n/g;function nBt(e,t){let r=(0,tBt.filterTLogAuthorities)(t,{targetDate:new Date(Number(e.integratedTime)*1e3)}),s=e.inclusionProof,a=TJ.fromString(s.checkpoint.envelope),n=FJ.fromString(a.note);if(!iBt(a,r))throw new A1.VerificationError({code:"TLOG_INCLUSION_PROOF_ERROR",message:"invalid checkpoint signature"});if(!RJ.crypto.bufferEqual(n.logHash,s.rootHash))throw new A1.VerificationError({code:"TLOG_INCLUSION_PROOF_ERROR",message:"root hash mismatch"})}function iBt(e,t){let r=Buffer.from(e.note,"utf-8");return e.signatures.every(s=>{let a=t.find(n=>RJ.crypto.bufferEqual(n.logID.subarray(0,4),s.keyHint));return a?RJ.crypto.verify(r,a.publicKey,s.signature):!1})}var TJ=class e{constructor(t,r){this.note=t,this.signatures=r}static fromString(t){if(!t.includes(QJ))throw new A1.VerificationError({code:"TLOG_INCLUSION_PROOF_ERROR",message:"missing checkpoint separator"});let r=t.indexOf(QJ),s=t.slice(0,r+1),n=t.slice(r+QJ.length).matchAll(rBt),c=Array.from(n,f=>{let[,p,h]=f,E=Buffer.from(h,"base64");if(E.length<5)throw new A1.VerificationError({code:"TLOG_INCLUSION_PROOF_ERROR",message:"malformed checkpoint signature"});return{name:p,keyHint:E.subarray(0,4),signature:E.subarray(4)}});if(c.length===0)throw new A1.VerificationError({code:"TLOG_INCLUSION_PROOF_ERROR",message:"no signatures found in checkpoint"});return new e(s,c)}},FJ=class e{constructor(t,r,s,a){this.origin=t,this.logSize=r,this.logHash=s,this.rest=a}static fromString(t){let r=t.trimEnd().split(` `);if(r.length<3)throw new A1.VerificationError({code:"TLOG_INCLUSION_PROOF_ERROR",message:"too few lines in checkpoint header"});let s=r[0],a=BigInt(r[1]),n=Buffer.from(r[2],"base64"),c=r.slice(3);return new e(s,a,n,c)}}});var obe=G(UJ=>{"use strict";Object.defineProperty(UJ,"__esModule",{value:!0});UJ.verifyMerkleInclusion=aBt;var MJ=Pl(),OJ=So(),sBt=Buffer.from([0]),oBt=Buffer.from([1]);function aBt(e){let t=e.inclusionProof,r=BigInt(t.logIndex),s=BigInt(t.treeSize);if(r<0n||r>=s)throw new OJ.VerificationError({code:"TLOG_INCLUSION_PROOF_ERROR",message:`invalid index: ${r}`});let{inner:a,border:n}=lBt(r,s);if(t.hashes.length!==a+n)throw new OJ.VerificationError({code:"TLOG_INCLUSION_PROOF_ERROR",message:"invalid hash count"});let c=t.hashes.slice(0,a),f=t.hashes.slice(a),p=hBt(e.canonicalizedBody),h=uBt(cBt(p,c,r),f);if(!MJ.crypto.bufferEqual(h,t.rootHash))throw new OJ.VerificationError({code:"TLOG_INCLUSION_PROOF_ERROR",message:"calculated root hash does not match inclusion proof"})}function lBt(e,t){let r=fBt(e,t),s=ABt(e>>BigInt(r));return{inner:r,border:s}}function cBt(e,t,r){return t.reduce((s,a,n)=>r>>BigInt(n)&BigInt(1)?LJ(a,s):LJ(s,a),e)}function uBt(e,t){return t.reduce((r,s)=>LJ(s,r),e)}function fBt(e,t){return pBt(e^t-BigInt(1))}function ABt(e){return e.toString(2).split("1").length-1}function pBt(e){return e===0n?0:e.toString(2).length}function LJ(e,t){return MJ.crypto.digest("sha256",oBt,e,t)}function hBt(e){return MJ.crypto.digest("sha256",sBt,e)}});var lbe=G(_J=>{"use strict";Object.defineProperty(_J,"__esModule",{value:!0});_J.verifyTLogSET=mBt;var abe=Pl(),dBt=So(),gBt=gy();function mBt(e,t){if(!(0,gBt.filterTLogAuthorities)(t,{logID:e.logId.keyId,targetDate:new Date(Number(e.integratedTime)*1e3)}).some(a=>{let n=yBt(e),c=Buffer.from(abe.json.canonicalize(n),"utf8"),f=e.inclusionPromise.signedEntryTimestamp;return abe.crypto.verify(c,a.publicKey,f)}))throw new dBt.VerificationError({code:"TLOG_INCLUSION_PROMISE_ERROR",message:"inclusion promise could not be verified"})}function yBt(e){let{integratedTime:t,logIndex:r,logId:s,canonicalizedBody:a}=e;return{body:a.toString("base64"),integratedTime:Number(t),logIndex:Number(r),logID:s.keyId.toString("hex")}}});var cbe=G(GJ=>{"use strict";Object.defineProperty(GJ,"__esModule",{value:!0});GJ.verifyRFC3161Timestamp=CBt;var HJ=Pl(),jJ=So(),EBt=xJ(),IBt=gy();function CBt(e,t,r){let s=e.signingTime;if(r=(0,IBt.filterCertAuthorities)(r,{start:s,end:s}),r=BBt(r,{serialNumber:e.signerSerialNumber,issuer:e.signerIssuer}),!r.some(n=>{try{return wBt(e,t,n),!0}catch{return!1}}))throw new jJ.VerificationError({code:"TIMESTAMP_ERROR",message:"timestamp could not be verified"})}function wBt(e,t,r){let[s,...a]=r.certChain,n=HJ.crypto.createPublicKey(s.publicKey),c=e.signingTime;try{new EBt.CertificateChainVerifier({untrustedCert:s,trustedCerts:a}).verify()}catch{throw new jJ.VerificationError({code:"TIMESTAMP_ERROR",message:"invalid certificate chain"})}if(!r.certChain.every(p=>p.validForDate(c)))throw new jJ.VerificationError({code:"TIMESTAMP_ERROR",message:"timestamp was signed with an expired certificate"});e.verify(t,n)}function BBt(e,t){return e.filter(r=>r.certChain.length>0&&HJ.crypto.bufferEqual(r.certChain[0].serialNumber,t.serialNumber)&&HJ.crypto.bufferEqual(r.certChain[0].issuer,t.issuer))}});var ube=G(lL=>{"use strict";Object.defineProperty(lL,"__esModule",{value:!0});lL.verifyTSATimestamp=xBt;lL.verifyTLogTimestamp=kBt;var vBt=So(),SBt=sbe(),DBt=obe(),bBt=lbe(),PBt=cbe();function xBt(e,t,r){return(0,PBt.verifyRFC3161Timestamp)(e,t,r),{type:"timestamp-authority",logID:e.signerSerialNumber,timestamp:e.signingTime}}function kBt(e,t){let r=!1;if(QBt(e)&&((0,bBt.verifyTLogSET)(e,t),r=!0),RBt(e)&&((0,DBt.verifyMerkleInclusion)(e),(0,SBt.verifyCheckpoint)(e,t),r=!0),!r)throw new vBt.VerificationError({code:"TLOG_MISSING_INCLUSION_ERROR",message:"inclusion could not be verified"});return{type:"transparency-log",logID:e.logId.keyId,timestamp:new Date(Number(e.integratedTime)*1e3)}}function QBt(e){return e.inclusionPromise!==void 0}function RBt(e){return e.inclusionProof!==void 0}});var fbe=G(qJ=>{"use strict";Object.defineProperty(qJ,"__esModule",{value:!0});qJ.verifyDSSETLogBody=TBt;var cL=So();function TBt(e,t){switch(e.apiVersion){case"0.0.1":return FBt(e,t);default:throw new cL.VerificationError({code:"TLOG_BODY_ERROR",message:`unsupported dsse version: ${e.apiVersion}`})}}function FBt(e,t){if(e.spec.signatures?.length!==1)throw new cL.VerificationError({code:"TLOG_BODY_ERROR",message:"signature count mismatch"});let r=e.spec.signatures[0].signature;if(!t.compareSignature(Buffer.from(r,"base64")))throw new cL.VerificationError({code:"TLOG_BODY_ERROR",message:"tlog entry signature mismatch"});let s=e.spec.payloadHash?.value||"";if(!t.compareDigest(Buffer.from(s,"hex")))throw new cL.VerificationError({code:"TLOG_BODY_ERROR",message:"DSSE payload hash mismatch"})}});var Abe=G(YJ=>{"use strict";Object.defineProperty(YJ,"__esModule",{value:!0});YJ.verifyHashedRekordTLogBody=NBt;var WJ=So();function NBt(e,t){switch(e.apiVersion){case"0.0.1":return OBt(e,t);default:throw new WJ.VerificationError({code:"TLOG_BODY_ERROR",message:`unsupported hashedrekord version: ${e.apiVersion}`})}}function OBt(e,t){let r=e.spec.signature.content||"";if(!t.compareSignature(Buffer.from(r,"base64")))throw new WJ.VerificationError({code:"TLOG_BODY_ERROR",message:"signature mismatch"});let s=e.spec.data.hash?.value||"";if(!t.compareDigest(Buffer.from(s,"hex")))throw new WJ.VerificationError({code:"TLOG_BODY_ERROR",message:"digest mismatch"})}});var pbe=G(VJ=>{"use strict";Object.defineProperty(VJ,"__esModule",{value:!0});VJ.verifyIntotoTLogBody=LBt;var uL=So();function LBt(e,t){switch(e.apiVersion){case"0.0.2":return MBt(e,t);default:throw new uL.VerificationError({code:"TLOG_BODY_ERROR",message:`unsupported intoto version: ${e.apiVersion}`})}}function MBt(e,t){if(e.spec.content.envelope.signatures?.length!==1)throw new uL.VerificationError({code:"TLOG_BODY_ERROR",message:"signature count mismatch"});let r=UBt(e.spec.content.envelope.signatures[0].sig);if(!t.compareSignature(Buffer.from(r,"base64")))throw new uL.VerificationError({code:"TLOG_BODY_ERROR",message:"tlog entry signature mismatch"});let s=e.spec.content.payloadHash?.value||"";if(!t.compareDigest(Buffer.from(s,"hex")))throw new uL.VerificationError({code:"TLOG_BODY_ERROR",message:"DSSE payload hash mismatch"})}function UBt(e){return Buffer.from(e,"base64").toString("utf-8")}});var dbe=G(JJ=>{"use strict";Object.defineProperty(JJ,"__esModule",{value:!0});JJ.verifyTLogBody=GBt;var hbe=So(),_Bt=fbe(),HBt=Abe(),jBt=pbe();function GBt(e,t){let{kind:r,version:s}=e.kindVersion,a=JSON.parse(e.canonicalizedBody.toString("utf8"));if(r!==a.kind||s!==a.apiVersion)throw new hbe.VerificationError({code:"TLOG_BODY_ERROR",message:`kind/version mismatch - expected: ${r}/${s}, received: ${a.kind}/${a.apiVersion}`});switch(a.kind){case"dsse":return(0,_Bt.verifyDSSETLogBody)(a,t);case"intoto":return(0,jBt.verifyIntotoTLogBody)(a,t);case"hashedrekord":return(0,HBt.verifyHashedRekordTLogBody)(a,t);default:throw new hbe.VerificationError({code:"TLOG_BODY_ERROR",message:`unsupported kind: ${r}`})}}});var Ibe=G(fL=>{"use strict";Object.defineProperty(fL,"__esModule",{value:!0});fL.Verifier=void 0;var qBt=Ie("util"),p1=So(),gbe=rbe(),mbe=ibe(),ybe=ube(),WBt=dbe(),KJ=class{constructor(t,r={}){this.trustMaterial=t,this.options={ctlogThreshold:r.ctlogThreshold??1,tlogThreshold:r.tlogThreshold??1,tsaThreshold:r.tsaThreshold??0}}verify(t,r){let s=this.verifyTimestamps(t),a=this.verifySigningKey(t,s);return this.verifyTLogs(t),this.verifySignature(t,a),r&&this.verifyPolicy(r,a.identity||{}),a}verifyTimestamps(t){let r=0,s=0,a=t.timestamps.map(n=>{switch(n.$case){case"timestamp-authority":return s++,(0,ybe.verifyTSATimestamp)(n.timestamp,t.signature.signature,this.trustMaterial.timestampAuthorities);case"transparency-log":return r++,(0,ybe.verifyTLogTimestamp)(n.tlogEntry,this.trustMaterial.tlogs)}});if(Ebe(a))throw new p1.VerificationError({code:"TIMESTAMP_ERROR",message:"duplicate timestamp"});if(rn.timestamp)}verifySigningKey({key:t},r){switch(t.$case){case"public-key":return(0,gbe.verifyPublicKey)(t.hint,r,this.trustMaterial);case"certificate":{let s=(0,gbe.verifyCertificate)(t.certificate,r,this.trustMaterial);if(Ebe(s.scts))throw new p1.VerificationError({code:"CERTIFICATE_ERROR",message:"duplicate SCT"});if(s.scts.length(0,WBt.verifyTLogBody)(s,t))}verifySignature(t,r){if(!t.signature.verifySignature(r.key))throw new p1.VerificationError({code:"SIGNATURE_ERROR",message:"signature verification failed"})}verifyPolicy(t,r){t.subjectAlternativeName&&(0,mbe.verifySubjectAlternativeName)(t.subjectAlternativeName,r.subjectAlternativeName),t.extensions&&(0,mbe.verifyExtensions)(t.extensions,r.extensions)}};fL.Verifier=KJ;function Ebe(e){for(let t=0;t{"use strict";Object.defineProperty(nu,"__esModule",{value:!0});nu.Verifier=nu.toTrustMaterial=nu.VerificationError=nu.PolicyError=nu.toSignedEntity=void 0;var YBt=KDe();Object.defineProperty(nu,"toSignedEntity",{enumerable:!0,get:function(){return YBt.toSignedEntity}});var Cbe=So();Object.defineProperty(nu,"PolicyError",{enumerable:!0,get:function(){return Cbe.PolicyError}});Object.defineProperty(nu,"VerificationError",{enumerable:!0,get:function(){return Cbe.VerificationError}});var VBt=gy();Object.defineProperty(nu,"toTrustMaterial",{enumerable:!0,get:function(){return VBt.toTrustMaterial}});var JBt=Ibe();Object.defineProperty(nu,"Verifier",{enumerable:!0,get:function(){return JBt.Verifier}})});var wbe=G(ja=>{"use strict";Object.defineProperty(ja,"__esModule",{value:!0});ja.DEFAULT_TIMEOUT=ja.DEFAULT_RETRY=void 0;ja.createBundleBuilder=XBt;ja.createKeyFinder=ZBt;ja.createVerificationPolicy=$Bt;var KBt=Pl(),h1=a7(),zBt=AL();ja.DEFAULT_RETRY={retries:2};ja.DEFAULT_TIMEOUT=5e3;function XBt(e,t){let r={signer:evt(t),witnesses:rvt(t)};switch(e){case"messageSignature":return new h1.MessageSignatureBundleBuilder(r);case"dsseEnvelope":return new h1.DSSEBundleBuilder({...r,certificateChain:t.legacyCompatibility})}}function ZBt(e){return t=>{let r=e(t);if(!r)throw new zBt.VerificationError({code:"PUBLIC_KEY_ERROR",message:`key not found: ${t}`});return{publicKey:KBt.crypto.createPublicKey(r),validFor:()=>!0}}}function $Bt(e){let t={},r=e.certificateIdentityEmail||e.certificateIdentityURI;return r&&(t.subjectAlternativeName=r),e.certificateIssuer&&(t.extensions={issuer:e.certificateIssuer}),t}function evt(e){return new h1.FulcioSigner({fulcioBaseURL:e.fulcioURL,identityProvider:e.identityProvider||tvt(e),retry:e.retry??ja.DEFAULT_RETRY,timeout:e.timeout??ja.DEFAULT_TIMEOUT})}function tvt(e){let t=e.identityToken;return t?{getToken:()=>Promise.resolve(t)}:new h1.CIContextProvider("sigstore")}function rvt(e){let t=[];return nvt(e)&&t.push(new h1.RekorWitness({rekorBaseURL:e.rekorURL,entryType:e.legacyCompatibility?"intoto":"dsse",fetchOnConflict:!1,retry:e.retry??ja.DEFAULT_RETRY,timeout:e.timeout??ja.DEFAULT_TIMEOUT})),ivt(e)&&t.push(new h1.TSAWitness({tsaBaseURL:e.tsaServerURL,retry:e.retry??ja.DEFAULT_RETRY,timeout:e.timeout??ja.DEFAULT_TIMEOUT})),t}function nvt(e){return e.tlogUpload!==!1}function ivt(e){return e.tsaServerURL!==void 0}});var Sbe=G(iu=>{"use strict";var svt=iu&&iu.__createBinding||(Object.create?function(e,t,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(t,r);(!a||("get"in a?!t.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,s,a)}:function(e,t,r,s){s===void 0&&(s=r),e[s]=t[r]}),ovt=iu&&iu.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),Bbe=iu&&iu.__importStar||function(){var e=function(t){return e=Object.getOwnPropertyNames||function(r){var s=[];for(var a in r)Object.prototype.hasOwnProperty.call(r,a)&&(s[s.length]=a);return s},e(t)};return function(t){if(t&&t.__esModule)return t;var r={};if(t!=null)for(var s=e(t),a=0;aa.verify(e,s))}async function vbe(e={}){let t=await avt.getTrustedRoot({mirrorURL:e.tufMirrorURL,rootPath:e.tufRootPath,cachePath:e.tufCachePath,forceCache:e.tufForceCache,retry:e.retry??d1.DEFAULT_RETRY,timeout:e.timeout??d1.DEFAULT_TIMEOUT}),r=e.keySelector?d1.createKeyFinder(e.keySelector):void 0,s=(0,zJ.toTrustMaterial)(t,r),a={ctlogThreshold:e.ctLogThreshold,tlogThreshold:e.tlogThreshold},n=new zJ.Verifier(s,a),c=d1.createVerificationPolicy(e);return{verify:(f,p)=>{let h=(0,XJ.bundleFromJSON)(f),E=(0,zJ.toSignedEntity)(h,p);n.verify(E,c)}}}});var bbe=G(Ni=>{"use strict";Object.defineProperty(Ni,"__esModule",{value:!0});Ni.verify=Ni.sign=Ni.createVerifier=Ni.attest=Ni.VerificationError=Ni.PolicyError=Ni.TUFError=Ni.InternalError=Ni.DEFAULT_REKOR_URL=Ni.DEFAULT_FULCIO_URL=Ni.ValidationError=void 0;var fvt=Ib();Object.defineProperty(Ni,"ValidationError",{enumerable:!0,get:function(){return fvt.ValidationError}});var ZJ=a7();Object.defineProperty(Ni,"DEFAULT_FULCIO_URL",{enumerable:!0,get:function(){return ZJ.DEFAULT_FULCIO_URL}});Object.defineProperty(Ni,"DEFAULT_REKOR_URL",{enumerable:!0,get:function(){return ZJ.DEFAULT_REKOR_URL}});Object.defineProperty(Ni,"InternalError",{enumerable:!0,get:function(){return ZJ.InternalError}});var Avt=ZO();Object.defineProperty(Ni,"TUFError",{enumerable:!0,get:function(){return Avt.TUFError}});var Dbe=AL();Object.defineProperty(Ni,"PolicyError",{enumerable:!0,get:function(){return Dbe.PolicyError}});Object.defineProperty(Ni,"VerificationError",{enumerable:!0,get:function(){return Dbe.VerificationError}});var pL=Sbe();Object.defineProperty(Ni,"attest",{enumerable:!0,get:function(){return pL.attest}});Object.defineProperty(Ni,"createVerifier",{enumerable:!0,get:function(){return pL.createVerifier}});Object.defineProperty(Ni,"sign",{enumerable:!0,get:function(){return pL.sign}});Object.defineProperty(Ni,"verify",{enumerable:!0,get:function(){return pL.verify}})});Dt();qe();Dt();var nPe=Ie("child_process"),iPe=et(Rg());Yt();var qI=new Map([]);var Gv={};Vt(Gv,{BaseCommand:()=>At,WorkspaceRequiredError:()=>ar,getCli:()=>g0e,getDynamicLibs:()=>d0e,getPluginConfiguration:()=>YI,openWorkspace:()=>WI,pluginCommands:()=>qI,runExit:()=>QT});Yt();var At=class extends at{constructor(){super(...arguments);this.cwd=he.String("--cwd",{hidden:!0})}validateAndExecute(){if(typeof this.cwd<"u")throw new it("The --cwd option is ambiguous when used anywhere else than the very first parameter provided in the command line, before even the command path");return super.validateAndExecute()}};qe();Dt();Yt();var ar=class extends it{constructor(t,r){let s=J.relative(t,r),a=J.join(t,_t.fileName);super(`This command can only be run from within a workspace of your project (${s} isn't a workspace of ${a}).`)}};qe();Dt();nA();vc();sv();Yt();var Tit=et(pi());Al();var d0e=()=>new Map([["@yarnpkg/cli",Gv],["@yarnpkg/core",jv],["@yarnpkg/fslib",R2],["@yarnpkg/libzip",nv],["@yarnpkg/parsers",_2],["@yarnpkg/shell",cv],["clipanion",Z2],["semver",Tit],["typanion",Jo]]);qe();async function WI(e,t){let{project:r,workspace:s}=await Rt.find(e,t);if(!s)throw new ar(r.cwd,t);return s}qe();Dt();nA();vc();sv();Yt();var OSt=et(pi());Al();var T5={};Vt(T5,{AddCommand:()=>zI,BinCommand:()=>XI,CacheCleanCommand:()=>ZI,ClipanionCommand:()=>iC,ConfigCommand:()=>rC,ConfigGetCommand:()=>$I,ConfigSetCommand:()=>eC,ConfigUnsetCommand:()=>tC,DedupeCommand:()=>nC,EntryCommand:()=>oC,ExecCommand:()=>lC,ExplainCommand:()=>fC,ExplainPeerRequirementsCommand:()=>cC,HelpCommand:()=>sC,InfoCommand:()=>AC,LinkCommand:()=>hC,NodeCommand:()=>dC,PluginCheckCommand:()=>gC,PluginImportCommand:()=>EC,PluginImportSourcesCommand:()=>IC,PluginListCommand:()=>mC,PluginRemoveCommand:()=>CC,PluginRuntimeCommand:()=>wC,RebuildCommand:()=>BC,RemoveCommand:()=>vC,RunCommand:()=>DC,RunIndexCommand:()=>SC,SetResolutionCommand:()=>bC,SetVersionCommand:()=>uC,SetVersionSourcesCommand:()=>yC,UnlinkCommand:()=>PC,UpCommand:()=>xC,VersionCommand:()=>aC,WhyCommand:()=>kC,WorkspaceCommand:()=>NC,WorkspacesListCommand:()=>FC,YarnCommand:()=>pC,dedupeUtils:()=>_T,default:()=>Vot,suggestUtils:()=>$u});var Uge=et(Rg());qe();qe();qe();Yt();var nge=et(Vv());Al();var $u={};Vt($u,{Modifier:()=>u5,Strategy:()=>MT,Target:()=>Jv,WorkspaceModifier:()=>Zde,applyModifier:()=>$st,extractDescriptorFromPath:()=>f5,extractRangeModifier:()=>$de,fetchDescriptorFrom:()=>A5,findProjectDescriptors:()=>rge,getModifier:()=>Kv,getSuggestedDescriptors:()=>zv,makeWorkspaceDescriptor:()=>tge,toWorkspaceModifier:()=>ege});qe();qe();Dt();var c5=et(pi()),Xst="workspace:",Jv=(s=>(s.REGULAR="dependencies",s.DEVELOPMENT="devDependencies",s.PEER="peerDependencies",s))(Jv||{}),u5=(s=>(s.CARET="^",s.TILDE="~",s.EXACT="",s))(u5||{}),Zde=(s=>(s.CARET="^",s.TILDE="~",s.EXACT="*",s))(Zde||{}),MT=(n=>(n.KEEP="keep",n.REUSE="reuse",n.PROJECT="project",n.LATEST="latest",n.CACHE="cache",n))(MT||{});function Kv(e,t){return e.exact?"":e.caret?"^":e.tilde?"~":t.configuration.get("defaultSemverRangePrefix")}var Zst=/^([\^~]?)[0-9]+(?:\.[0-9]+){0,2}(?:-\S+)?$/;function $de(e,{project:t}){let r=e.match(Zst);return r?r[1]:t.configuration.get("defaultSemverRangePrefix")}function $st(e,t){let{protocol:r,source:s,params:a,selector:n}=j.parseRange(e.range);return c5.default.valid(n)&&(n=`${t}${e.range}`),j.makeDescriptor(e,j.makeRange({protocol:r,source:s,params:a,selector:n}))}function ege(e){switch(e){case"^":return"^";case"~":return"~";case"":return"*";default:throw new Error(`Assertion failed: Unknown modifier: "${e}"`)}}function tge(e,t){return j.makeDescriptor(e.anchoredDescriptor,`${Xst}${ege(t)}`)}async function rge(e,{project:t,target:r}){let s=new Map,a=n=>{let c=s.get(n.descriptorHash);return c||s.set(n.descriptorHash,c={descriptor:n,locators:[]}),c};for(let n of t.workspaces)if(r==="peerDependencies"){let c=n.manifest.peerDependencies.get(e.identHash);c!==void 0&&a(c).locators.push(n.anchoredLocator)}else{let c=n.manifest.dependencies.get(e.identHash),f=n.manifest.devDependencies.get(e.identHash);r==="devDependencies"?f!==void 0?a(f).locators.push(n.anchoredLocator):c!==void 0&&a(c).locators.push(n.anchoredLocator):c!==void 0?a(c).locators.push(n.anchoredLocator):f!==void 0&&a(f).locators.push(n.anchoredLocator)}return s}async function f5(e,{cwd:t,workspace:r}){return await tot(async s=>{J.isAbsolute(e)||(e=J.relative(r.cwd,J.resolve(t,e)),e.match(/^\.{0,2}\//)||(e=`./${e}`));let{project:a}=r,n=await A5(j.makeIdent(null,"archive"),e,{project:r.project,cache:s,workspace:r});if(!n)throw new Error("Assertion failed: The descriptor should have been found");let c=new ki,f=a.configuration.makeResolver(),p=a.configuration.makeFetcher(),h={checksums:a.storedChecksums,project:a,cache:s,fetcher:p,report:c,resolver:f},E=f.bindDescriptor(n,r.anchoredLocator,h),C=j.convertDescriptorToLocator(E),S=await p.fetch(C,h),x=await _t.find(S.prefixPath,{baseFs:S.packageFs});if(!x.name)throw new Error("Target path doesn't have a name");return j.makeDescriptor(x.name,e)})}function eot(e){if(e.range==="unknown")return{type:"resolve",range:"latest"};if(kr.validRange(e.range))return{type:"fixed",range:e.range};if(_p.test(e.range))return{type:"resolve",range:e.range};let t=e.range.match(/^(?:jsr:|npm:)(.*)/);if(!t)return{type:"fixed",range:e.range};let[,r]=t,s=`${j.stringifyIdent(e)}@`;return r.startsWith(s)&&(r=r.slice(s.length)),kr.validRange(r)?{type:"fixed",range:e.range}:_p.test(r)?{type:"resolve",range:e.range}:{type:"fixed",range:e.range}}async function zv(e,{project:t,workspace:r,cache:s,target:a,fixed:n,modifier:c,strategies:f,maxResults:p=1/0}){if(!(p>=0))throw new Error(`Invalid maxResults (${p})`);let h=!n||e.range==="unknown"?eot(e):{type:"fixed",range:e.range};if(h.type==="fixed")return{suggestions:[{descriptor:e,name:`Use ${j.prettyDescriptor(t.configuration,e)}`,reason:"(unambiguous explicit request)"}],rejections:[]};let E=typeof r<"u"&&r!==null&&r.manifest[a].get(e.identHash)||null,C=[],S=[],x=async I=>{try{await I()}catch(T){S.push(T)}};for(let I of f){if(C.length>=p)break;switch(I){case"keep":await x(async()=>{E&&C.push({descriptor:E,name:`Keep ${j.prettyDescriptor(t.configuration,E)}`,reason:"(no changes)"})});break;case"reuse":await x(async()=>{for(let{descriptor:T,locators:O}of(await rge(e,{project:t,target:a})).values()){if(O.length===1&&O[0].locatorHash===r.anchoredLocator.locatorHash&&f.includes("keep"))continue;let U=`(originally used by ${j.prettyLocator(t.configuration,O[0])}`;U+=O.length>1?` and ${O.length-1} other${O.length>2?"s":""})`:")",C.push({descriptor:T,name:`Reuse ${j.prettyDescriptor(t.configuration,T)}`,reason:U})}});break;case"cache":await x(async()=>{for(let T of t.storedDescriptors.values())T.identHash===e.identHash&&C.push({descriptor:T,name:`Reuse ${j.prettyDescriptor(t.configuration,T)}`,reason:"(already used somewhere in the lockfile)"})});break;case"project":await x(async()=>{if(r.manifest.name!==null&&e.identHash===r.manifest.name.identHash)return;let T=t.tryWorkspaceByIdent(e);if(T===null)return;let O=tge(T,c);C.push({descriptor:O,name:`Attach ${j.prettyDescriptor(t.configuration,O)}`,reason:`(local workspace at ${pe.pretty(t.configuration,T.relativeCwd,pe.Type.PATH)})`})});break;case"latest":{let T=t.configuration.get("enableNetwork"),O=t.configuration.get("enableOfflineMode");await x(async()=>{if(a==="peerDependencies")C.push({descriptor:j.makeDescriptor(e,"*"),name:"Use *",reason:"(catch-all peer dependency pattern)"});else if(!T&&!O)C.push({descriptor:null,name:"Resolve from latest",reason:pe.pretty(t.configuration,"(unavailable because enableNetwork is toggled off)","grey")});else{let U=await A5(e,h.range,{project:t,cache:s,workspace:r,modifier:c});U&&C.push({descriptor:U,name:`Use ${j.prettyDescriptor(t.configuration,U)}`,reason:`(resolved from ${O?"the cache":"latest"})`})}})}break}}return{suggestions:C.slice(0,p),rejections:S.slice(0,p)}}async function A5(e,t,{project:r,cache:s,workspace:a,preserveModifier:n=!0,modifier:c}){let f=r.configuration.normalizeDependency(j.makeDescriptor(e,t)),p=new ki,h=r.configuration.makeFetcher(),E=r.configuration.makeResolver(),C={project:r,fetcher:h,cache:s,checksums:r.storedChecksums,report:p,cacheOptions:{skipIntegrityCheck:!0}},S={...C,resolver:E,fetchOptions:C},x=E.bindDescriptor(f,a.anchoredLocator,S),I=await E.getCandidates(x,{},S);if(I.length===0)return null;let T=I[0],{protocol:O,source:U,params:V,selector:te}=j.parseRange(j.convertToManifestRange(T.reference));if(O===r.configuration.get("defaultProtocol")&&(O=null),c5.default.valid(te)){let ie=te;if(typeof c<"u")te=c+te;else if(n!==!1){let ge=typeof n=="string"?n:f.range;te=$de(ge,{project:r})+te}let ue=j.makeDescriptor(T,j.makeRange({protocol:O,source:U,params:V,selector:te}));(await E.getCandidates(r.configuration.normalizeDependency(ue),{},S)).length!==1&&(te=ie)}return j.makeDescriptor(T,j.makeRange({protocol:O,source:U,params:V,selector:te}))}async function tot(e){return await le.mktempPromise(async t=>{let r=ze.create(t);return r.useWithSource(t,{enableMirror:!1,compressionLevel:0},t,{overwrite:!0}),await e(new Kr(t,{configuration:r,check:!1,immutable:!1}))})}var zI=class extends At{constructor(){super(...arguments);this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.fixed=he.Boolean("-F,--fixed",!1,{description:"Store dependency tags as-is instead of resolving them"});this.exact=he.Boolean("-E,--exact",!1,{description:"Don't use any semver modifier on the resolved range"});this.tilde=he.Boolean("-T,--tilde",!1,{description:"Use the `~` semver modifier on the resolved range"});this.caret=he.Boolean("-C,--caret",!1,{description:"Use the `^` semver modifier on the resolved range"});this.dev=he.Boolean("-D,--dev",!1,{description:"Add a package as a dev dependency"});this.peer=he.Boolean("-P,--peer",!1,{description:"Add a package as a peer dependency"});this.optional=he.Boolean("-O,--optional",!1,{description:"Add / upgrade a package to an optional regular / peer dependency"});this.preferDev=he.Boolean("--prefer-dev",!1,{description:"Add / upgrade a package to a dev dependency"});this.interactive=he.Boolean("-i,--interactive",{description:"Reuse the specified package from other workspaces in the project"});this.cached=he.Boolean("--cached",!1,{description:"Reuse the highest version already used somewhere within the project"});this.mode=he.String("--mode",{description:"Change what artifacts installs generate",validator:ks(Oa)});this.silent=he.Boolean("--silent",{hidden:!0});this.packages=he.Rest()}static{this.paths=[["add"]]}static{this.usage=at.Usage({description:"add dependencies to the project",details:"\n This command adds a package to the package.json for the nearest workspace.\n\n - If it didn't exist before, the package will by default be added to the regular `dependencies` field, but this behavior can be overriden thanks to the `-D,--dev` flag (which will cause the dependency to be added to the `devDependencies` field instead) and the `-P,--peer` flag (which will do the same but for `peerDependencies`).\n\n - If the package was already listed in your dependencies, it will by default be upgraded whether it's part of your `dependencies` or `devDependencies` (it won't ever update `peerDependencies`, though).\n\n - If set, the `--prefer-dev` flag will operate as a more flexible `-D,--dev` in that it will add the package to your `devDependencies` if it isn't already listed in either `dependencies` or `devDependencies`, but it will also happily upgrade your `dependencies` if that's what you already use (whereas `-D,--dev` would throw an exception).\n\n - If set, the `-O,--optional` flag will add the package to the `optionalDependencies` field and, in combination with the `-P,--peer` flag, it will add the package as an optional peer dependency. If the package was already listed in your `dependencies`, it will be upgraded to `optionalDependencies`. If the package was already listed in your `peerDependencies`, in combination with the `-P,--peer` flag, it will be upgraded to an optional peer dependency: `\"peerDependenciesMeta\": { \"\": { \"optional\": true } }`\n\n - If the added package doesn't specify a range at all its `latest` tag will be resolved and the returned version will be used to generate a new semver range (using the `^` modifier by default unless otherwise configured via the `defaultSemverRangePrefix` configuration, or the `~` modifier if `-T,--tilde` is specified, or no modifier at all if `-E,--exact` is specified). Two exceptions to this rule: the first one is that if the package is a workspace then its local version will be used, and the second one is that if you use `-P,--peer` the default range will be `*` and won't be resolved at all.\n\n - If the added package specifies a range (such as `^1.0.0`, `latest`, or `rc`), Yarn will add this range as-is in the resulting package.json entry (in particular, tags such as `rc` will be encoded as-is rather than being converted into a semver range).\n\n If the `--cached` option is used, Yarn will preferably reuse the highest version already used somewhere within the project, even if through a transitive dependency.\n\n If the `-i,--interactive` option is used (or if the `preferInteractive` settings is toggled on) the command will first try to check whether other workspaces in the project use the specified package and, if so, will offer to reuse them.\n\n If the `--mode=` option is set, Yarn will change which artifacts are generated. The modes currently supported are:\n\n - `skip-build` will not run the build scripts at all. Note that this is different from setting `enableScripts` to false because the latter will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.\n\n - `update-lockfile` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.\n\n For a compilation of all the supported protocols, please consult the dedicated page from our website: https://yarnpkg.com/protocols.\n ",examples:[["Add a regular package to the current workspace","$0 add lodash"],["Add a specific version for a package to the current workspace","$0 add lodash@1.2.3"],["Add a package from a GitHub repository (the master branch) to the current workspace using a URL","$0 add lodash@https://github.com/lodash/lodash"],["Add a package from a GitHub repository (the master branch) to the current workspace using the GitHub protocol","$0 add lodash@github:lodash/lodash"],["Add a package from a GitHub repository (the master branch) to the current workspace using the GitHub protocol (shorthand)","$0 add lodash@lodash/lodash"],["Add a package from a specific branch of a GitHub repository to the current workspace using the GitHub protocol (shorthand)","$0 add lodash-es@lodash/lodash#es"],["Add a local package (gzipped tarball format) to the current workspace","$0 add local-package-name@file:../path/to/local-package-name-v0.1.2.tgz"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState({restoreResolutions:!1});let c=this.fixed,f=r.isInteractive({interactive:this.interactive,stdout:this.context.stdout}),p=f||r.get("preferReuse"),h=Kv(this,s),E=[p?"reuse":void 0,"project",this.cached?"cache":void 0,"latest"].filter(V=>typeof V<"u"),C=f?1/0:1,S=V=>{let te=j.tryParseDescriptor(V.slice(4));return te?te.range==="unknown"?j.makeDescriptor(te,`jsr:${j.stringifyIdent(te)}@latest`):j.makeDescriptor(te,`jsr:${te.range}`):null},x=await Promise.all(this.packages.map(async V=>{let te=V.match(/^\.{0,2}\//)?await f5(V,{cwd:this.context.cwd,workspace:a}):V.startsWith("jsr:")?S(V):j.tryParseDescriptor(V),ie=V.match(/^(https?:|git@github)/);if(ie)throw new it(`It seems you are trying to add a package using a ${pe.pretty(r,`${ie[0]}...`,pe.Type.RANGE)} url; we now require package names to be explicitly specified. Try running the command again with the package name prefixed: ${pe.pretty(r,"yarn add",pe.Type.CODE)} ${pe.pretty(r,j.makeDescriptor(j.makeIdent(null,"my-package"),`${ie[0]}...`),pe.Type.DESCRIPTOR)}`);if(!te)throw new it(`The ${pe.pretty(r,V,pe.Type.CODE)} string didn't match the required format (package-name@range). Did you perhaps forget to explicitly reference the package name?`);let ue=rot(a,te,{dev:this.dev,peer:this.peer,preferDev:this.preferDev,optional:this.optional});return await Promise.all(ue.map(async ge=>{let Ae=await zv(te,{project:s,workspace:a,cache:n,fixed:c,target:ge,modifier:h,strategies:E,maxResults:C});return{request:te,suggestedDescriptors:Ae,target:ge}}))})).then(V=>V.flat()),I=await uA.start({configuration:r,stdout:this.context.stdout,suggestInstall:!1},async V=>{for(let{request:te,suggestedDescriptors:{suggestions:ie,rejections:ue}}of x)if(ie.filter(ge=>ge.descriptor!==null).length===0){let[ge]=ue;if(typeof ge>"u")throw new Error("Assertion failed: Expected an error to have been set");s.configuration.get("enableNetwork")?V.reportError(27,`${j.prettyDescriptor(r,te)} can't be resolved to a satisfying range`):V.reportError(27,`${j.prettyDescriptor(r,te)} can't be resolved to a satisfying range (note: network resolution has been disabled)`),V.reportSeparator(),V.reportExceptionOnce(ge)}});if(I.hasErrors())return I.exitCode();let T=!1,O=[],U=[];for(let{suggestedDescriptors:{suggestions:V},target:te}of x){let ie,ue=V.filter(Ce=>Ce.descriptor!==null),ae=ue[0].descriptor,ge=ue.every(Ce=>j.areDescriptorsEqual(Ce.descriptor,ae));ue.length===1||ge?ie=ae:(T=!0,{answer:ie}=await(0,nge.prompt)({type:"select",name:"answer",message:"Which range do you want to use?",choices:V.map(({descriptor:Ce,name:Ee,reason:d})=>Ce?{name:Ee,hint:d,descriptor:Ce}:{name:Ee,hint:d,disabled:!0}),onCancel:()=>process.exit(130),result(Ce){return this.find(Ce,"descriptor")},stdin:this.context.stdin,stdout:this.context.stdout}));let Ae=a.manifest[te].get(ie.identHash);(typeof Ae>"u"||Ae.descriptorHash!==ie.descriptorHash)&&(a.manifest[te].set(ie.identHash,ie),this.optional&&(te==="dependencies"?a.manifest.ensureDependencyMeta({...ie,range:"unknown"}).optional=!0:te==="peerDependencies"&&(a.manifest.ensurePeerDependencyMeta({...ie,range:"unknown"}).optional=!0)),typeof Ae>"u"?O.push([a,te,ie,E]):U.push([a,te,Ae,ie]))}return await r.triggerMultipleHooks(V=>V.afterWorkspaceDependencyAddition,O),await r.triggerMultipleHooks(V=>V.afterWorkspaceDependencyReplacement,U),T&&this.context.stdout.write(` `),await s.installWithNewReport({json:this.json,stdout:this.context.stdout,quiet:this.context.quiet},{cache:n,mode:this.mode})}};function rot(e,t,{dev:r,peer:s,preferDev:a,optional:n}){let c=e.manifest.dependencies.has(t.identHash),f=e.manifest.devDependencies.has(t.identHash),p=e.manifest.peerDependencies.has(t.identHash);if((r||s)&&c)throw new it(`Package "${j.prettyIdent(e.project.configuration,t)}" is already listed as a regular dependency - remove the -D,-P flags or remove it from your dependencies first`);if(!r&&!s&&p)throw new it(`Package "${j.prettyIdent(e.project.configuration,t)}" is already listed as a peer dependency - use either of -D or -P, or remove it from your peer dependencies first`);if(n&&f)throw new it(`Package "${j.prettyIdent(e.project.configuration,t)}" is already listed as a dev dependency - remove the -O flag or remove it from your dev dependencies first`);if(n&&!s&&p)throw new it(`Package "${j.prettyIdent(e.project.configuration,t)}" is already listed as a peer dependency - remove the -O flag or add the -P flag or remove it from your peer dependencies first`);if((r||a)&&n)throw new it(`Package "${j.prettyIdent(e.project.configuration,t)}" cannot simultaneously be a dev dependency and an optional dependency`);let h=[];return s&&h.push("peerDependencies"),(r||a)&&h.push("devDependencies"),n&&h.push("dependencies"),h.length>0?h:f?["devDependencies"]:p?["peerDependencies"]:["dependencies"]}qe();qe();Yt();var XI=class extends At{constructor(){super(...arguments);this.verbose=he.Boolean("-v,--verbose",!1,{description:"Print both the binary name and the locator of the package that provides the binary"});this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.name=he.String({required:!1})}static{this.paths=[["bin"]]}static{this.usage=at.Usage({description:"get the path to a binary script",details:` When used without arguments, this command will print the list of all the binaries available in the current workspace. Adding the \`-v,--verbose\` flag will cause the output to contain both the binary name and the locator of the package that provides the binary. When an argument is specified, this command will just print the path to the binary on the standard output and exit. Note that the reported path may be stored within a zip archive. `,examples:[["List all the available binaries","$0 bin"],["Print the path to a specific binary","$0 bin eslint"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,locator:a}=await Rt.find(r,this.context.cwd);if(await s.restoreInstallState(),this.name){let f=(await Cn.getPackageAccessibleBinaries(a,{project:s})).get(this.name);if(!f)throw new it(`Couldn't find a binary named "${this.name}" for package "${j.prettyLocator(r,a)}"`);let[,p]=f;return this.context.stdout.write(`${p} `),0}return(await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout},async c=>{let f=await Cn.getPackageAccessibleBinaries(a,{project:s}),h=Array.from(f.keys()).reduce((E,C)=>Math.max(E,C.length),0);for(let[E,[C,S]]of f)c.reportJson({name:E,source:j.stringifyIdent(C),path:S});if(this.verbose)for(let[E,[C]]of f)c.reportInfo(null,`${E.padEnd(h," ")} ${j.prettyLocator(r,C)}`);else for(let E of f.keys())c.reportInfo(null,E)})).exitCode()}};qe();Dt();Yt();var ZI=class extends At{constructor(){super(...arguments);this.mirror=he.Boolean("--mirror",!1,{description:"Remove the global cache files instead of the local cache files"});this.all=he.Boolean("--all",!1,{description:"Remove both the global cache files and the local cache files of the current project"})}static{this.paths=[["cache","clean"],["cache","clear"]]}static{this.usage=at.Usage({description:"remove the shared cache files",details:` This command will remove all the files from the cache. `,examples:[["Remove all the local archives","$0 cache clean"],["Remove all the archives stored in the ~/.yarn directory","$0 cache clean --mirror"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins);if(!r.get("enableCacheClean"))throw new it("Cache cleaning is currently disabled. To enable it, set `enableCacheClean: true` in your configuration file. Note: Cache cleaning is typically not required and should be avoided when using Zero-Installs.");let s=await Kr.find(r);return(await Ot.start({configuration:r,stdout:this.context.stdout},async()=>{let n=(this.all||this.mirror)&&s.mirrorCwd!==null,c=!this.mirror;n&&(await le.removePromise(s.mirrorCwd),await r.triggerHook(f=>f.cleanGlobalArtifacts,r)),c&&await le.removePromise(s.cwd)})).exitCode()}};qe();Yt();zl();var p5=Ie("util"),$I=class extends At{constructor(){super(...arguments);this.why=he.Boolean("--why",!1,{description:"Print the explanation for why a setting has its value"});this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.unsafe=he.Boolean("--no-redacted",!1,{description:"Don't redact secrets (such as tokens) from the output"});this.name=he.String()}static{this.paths=[["config","get"]]}static{this.usage=at.Usage({description:"read a configuration settings",details:` This command will print a configuration setting. Secrets (such as tokens) will be redacted from the output by default. If this behavior isn't desired, set the \`--no-redacted\` to get the untransformed value. `,examples:[["Print a simple configuration setting","yarn config get yarnPath"],["Print a complex configuration setting","yarn config get packageExtensions"],["Print a nested field from the configuration",`yarn config get 'npmScopes["my-company"].npmRegistryServer'`],["Print a token from the configuration","yarn config get npmAuthToken --no-redacted"],["Print a configuration setting as JSON","yarn config get packageExtensions --json"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=this.name.replace(/[.[].*$/,""),a=this.name.replace(/^[^.[]*/,"");if(typeof r.settings.get(s)>"u")throw new it(`Couldn't find a configuration settings named "${s}"`);let c=r.getSpecial(s,{hideSecrets:!this.unsafe,getNativePaths:!0}),f=Ge.convertMapsToIndexableObjects(c),p=a?Pa(f,a):f,h=await Ot.start({configuration:r,includeFooter:!1,json:this.json,stdout:this.context.stdout},async E=>{E.reportJson(p)});if(!this.json){if(typeof p=="string")return this.context.stdout.write(`${p} `),h.exitCode();p5.inspect.styles.name="cyan",this.context.stdout.write(`${(0,p5.inspect)(p,{depth:1/0,colors:r.get("enableColors"),compact:!1})} `)}return h.exitCode()}};qe();Yt();zl();var h5=Ie("util"),eC=class extends At{constructor(){super(...arguments);this.json=he.Boolean("--json",!1,{description:"Set complex configuration settings to JSON values"});this.home=he.Boolean("-H,--home",!1,{description:"Update the home configuration instead of the project configuration"});this.name=he.String();this.value=he.String()}static{this.paths=[["config","set"]]}static{this.usage=at.Usage({description:"change a configuration settings",details:` This command will set a configuration setting. When used without the \`--json\` flag, it can only set a simple configuration setting (a string, a number, or a boolean). When used with the \`--json\` flag, it can set both simple and complex configuration settings, including Arrays and Objects. `,examples:[["Set a simple configuration setting (a string, a number, or a boolean)","yarn config set initScope myScope"],["Set a simple configuration setting (a string, a number, or a boolean) using the `--json` flag",'yarn config set initScope --json \\"myScope\\"'],["Set a complex configuration setting (an Array) using the `--json` flag",`yarn config set unsafeHttpWhitelist --json '["*.example.com", "example.com"]'`],["Set a complex configuration setting (an Object) using the `--json` flag",`yarn config set packageExtensions --json '{ "@babel/parser@*": { "dependencies": { "@babel/types": "*" } } }'`],["Set a nested configuration setting",'yarn config set npmScopes.company.npmRegistryServer "https://npm.example.com"'],["Set a nested configuration setting using indexed access for non-simple keys",`yarn config set 'npmRegistries["//npm.example.com"].npmAuthToken' "ffffffff-ffff-ffff-ffff-ffffffffffff"`]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=()=>{if(!r.projectCwd)throw new it("This command must be run from within a project folder");return r.projectCwd},a=this.name.replace(/[.[].*$/,""),n=this.name.replace(/^[^.[]*\.?/,"");if(typeof r.settings.get(a)>"u")throw new it(`Couldn't find a configuration settings named "${a}"`);if(a==="enableStrictSettings")throw new it("This setting only affects the file it's in, and thus cannot be set from the CLI");let f=this.json?JSON.parse(this.value):this.value;await(this.home?I=>ze.updateHomeConfiguration(I):I=>ze.updateConfiguration(s(),I))(I=>{if(n){let T=u0(I);return Yg(T,this.name,f),T}else return{...I,[a]:f}});let E=(await ze.find(this.context.cwd,this.context.plugins)).getSpecial(a,{hideSecrets:!0,getNativePaths:!0}),C=Ge.convertMapsToIndexableObjects(E),S=n?Pa(C,n):C;return(await Ot.start({configuration:r,includeFooter:!1,stdout:this.context.stdout},async I=>{h5.inspect.styles.name="cyan",I.reportInfo(0,`Successfully set ${this.name} to ${(0,h5.inspect)(S,{depth:1/0,colors:r.get("enableColors"),compact:!1})}`)})).exitCode()}};qe();Yt();zl();var tC=class extends At{constructor(){super(...arguments);this.home=he.Boolean("-H,--home",!1,{description:"Update the home configuration instead of the project configuration"});this.name=he.String()}static{this.paths=[["config","unset"]]}static{this.usage=at.Usage({description:"unset a configuration setting",details:` This command will unset a configuration setting. `,examples:[["Unset a simple configuration setting","yarn config unset initScope"],["Unset a complex configuration setting","yarn config unset packageExtensions"],["Unset a nested configuration setting","yarn config unset npmScopes.company.npmRegistryServer"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=()=>{if(!r.projectCwd)throw new it("This command must be run from within a project folder");return r.projectCwd},a=this.name.replace(/[.[].*$/,""),n=this.name.replace(/^[^.[]*\.?/,"");if(typeof r.settings.get(a)>"u")throw new it(`Couldn't find a configuration settings named "${a}"`);let f=this.home?h=>ze.updateHomeConfiguration(h):h=>ze.updateConfiguration(s(),h);return(await Ot.start({configuration:r,includeFooter:!1,stdout:this.context.stdout},async h=>{let E=!1;await f(C=>{if(!gB(C,this.name))return h.reportWarning(0,`Configuration doesn't contain setting ${this.name}; there is nothing to unset`),E=!0,C;let S=n?u0(C):{...C};return f0(S,this.name),S}),E||h.reportInfo(0,`Successfully unset ${this.name}`)})).exitCode()}};qe();Dt();Yt();var UT=Ie("util"),rC=class extends At{constructor(){super(...arguments);this.noDefaults=he.Boolean("--no-defaults",!1,{description:"Omit the default values from the display"});this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.verbose=he.Boolean("-v,--verbose",{hidden:!0});this.why=he.Boolean("--why",{hidden:!0});this.names=he.Rest()}static{this.paths=[["config"]]}static{this.usage=at.Usage({description:"display the current configuration",details:` This command prints the current active configuration settings. `,examples:[["Print the active configuration settings","$0 config"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins,{strict:!1}),s=await PI({configuration:r,stdout:this.context.stdout,forceError:this.json},[{option:this.verbose,message:"The --verbose option is deprecated, the settings' descriptions are now always displayed"},{option:this.why,message:"The --why option is deprecated, the settings' sources are now always displayed"}]);if(s!==null)return s;let a=this.names.length>0?[...new Set(this.names)].sort():[...r.settings.keys()].sort(),n,c=await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout,includeFooter:!1},async f=>{if(r.invalid.size>0&&!this.json){for(let[p,h]of r.invalid)f.reportError(34,`Invalid configuration key "${p}" in ${h}`);f.reportSeparator()}if(this.json)for(let p of a){if(this.noDefaults&&!r.sources.has(p))continue;let h=r.settings.get(p);typeof h>"u"&&f.reportError(34,`No configuration key named "${p}"`);let E=r.getSpecial(p,{hideSecrets:!0,getNativePaths:!0}),C=r.sources.get(p)??"",S=C&&C[0]!=="<"?fe.fromPortablePath(C):C;f.reportJson({key:p,effective:E,source:S,...h})}else{let p={breakLength:1/0,colors:r.get("enableColors"),maxArrayLength:2},h={},E={children:h};for(let C of a){if(this.noDefaults&&!r.sources.has(C))continue;let S=r.settings.get(C),x=r.sources.get(C)??"",I=r.getSpecial(C,{hideSecrets:!0,getNativePaths:!0}),T={Description:{label:"Description",value:pe.tuple(pe.Type.MARKDOWN,{text:S.description,format:this.cli.format(),paragraphs:!1})},Source:{label:"Source",value:pe.tuple(x[0]==="<"?pe.Type.CODE:pe.Type.PATH,x)}};h[C]={value:pe.tuple(pe.Type.CODE,C),children:T};let O=(U,V)=>{for(let[te,ie]of V)if(ie instanceof Map){let ue={};U[te]={children:ue},O(ue,ie)}else U[te]={label:te,value:pe.tuple(pe.Type.NO_HINT,(0,UT.inspect)(ie,p))}};I instanceof Map?O(T,I):T.Value={label:"Value",value:pe.tuple(pe.Type.NO_HINT,(0,UT.inspect)(I,p))}}a.length!==1&&(n=void 0),Rs.emitTree(E,{configuration:r,json:this.json,stdout:this.context.stdout,separators:2})}});if(!this.json&&typeof n<"u"){let f=a[0],p=(0,UT.inspect)(r.getSpecial(f,{hideSecrets:!0,getNativePaths:!0}),{colors:r.get("enableColors")});this.context.stdout.write(` `),this.context.stdout.write(`${p} `)}return c.exitCode()}};qe();Yt();Al();var _T={};Vt(_T,{Strategy:()=>Xv,acceptedStrategies:()=>not,dedupe:()=>d5});qe();qe();var ige=et(zo()),Xv=(t=>(t.HIGHEST="highest",t))(Xv||{}),not=new Set(Object.values(Xv)),iot={highest:async(e,t,{resolver:r,fetcher:s,resolveOptions:a,fetchOptions:n})=>{let c=new Map;for(let[p,h]of e.storedResolutions){let E=e.storedDescriptors.get(p);if(typeof E>"u")throw new Error(`Assertion failed: The descriptor (${p}) should have been registered`);Ge.getSetWithDefault(c,E.identHash).add(h)}let f=new Map(Ge.mapAndFilter(e.storedDescriptors.values(),p=>j.isVirtualDescriptor(p)?Ge.mapAndFilter.skip:[p.descriptorHash,Ge.makeDeferred()]));for(let p of e.storedDescriptors.values()){let h=f.get(p.descriptorHash);if(typeof h>"u")throw new Error(`Assertion failed: The descriptor (${p.descriptorHash}) should have been registered`);let E=e.storedResolutions.get(p.descriptorHash);if(typeof E>"u")throw new Error(`Assertion failed: The resolution (${p.descriptorHash}) should have been registered`);let C=e.originalPackages.get(E);if(typeof C>"u")throw new Error(`Assertion failed: The package (${E}) should have been registered`);Promise.resolve().then(async()=>{let S=r.getResolutionDependencies(p,a),x=Object.fromEntries(await Ge.allSettledSafe(Object.entries(S).map(async([te,ie])=>{let ue=f.get(ie.descriptorHash);if(typeof ue>"u")throw new Error(`Assertion failed: The descriptor (${ie.descriptorHash}) should have been registered`);let ae=await ue.promise;if(!ae)throw new Error("Assertion failed: Expected the dependency to have been through the dedupe process itself");return[te,ae.updatedPackage]})));if(t.length&&!ige.default.isMatch(j.stringifyIdent(p),t)||!r.shouldPersistResolution(C,a))return C;let I=c.get(p.identHash);if(typeof I>"u")throw new Error(`Assertion failed: The resolutions (${p.identHash}) should have been registered`);if(I.size===1)return C;let T=[...I].map(te=>{let ie=e.originalPackages.get(te);if(typeof ie>"u")throw new Error(`Assertion failed: The package (${te}) should have been registered`);return ie}),O=await r.getSatisfying(p,x,T,a),U=O.locators?.[0];if(typeof U>"u"||!O.sorted)return C;let V=e.originalPackages.get(U.locatorHash);if(typeof V>"u")throw new Error(`Assertion failed: The package (${U.locatorHash}) should have been registered`);return V}).then(async S=>{let x=await e.preparePackage(S,{resolver:r,resolveOptions:a});h.resolve({descriptor:p,currentPackage:C,updatedPackage:S,resolvedPackage:x})}).catch(S=>{h.reject(S)})}return[...f.values()].map(p=>p.promise)}};async function d5(e,{strategy:t,patterns:r,cache:s,report:a}){let{configuration:n}=e,c=new ki,f=n.makeResolver(),p=n.makeFetcher(),h={cache:s,checksums:e.storedChecksums,fetcher:p,project:e,report:c,cacheOptions:{skipIntegrityCheck:!0}},E={project:e,resolver:f,report:c,fetchOptions:h};return await a.startTimerPromise("Deduplication step",async()=>{let C=iot[t],S=await C(e,r,{resolver:f,resolveOptions:E,fetcher:p,fetchOptions:h}),x=yo.progressViaCounter(S.length);await a.reportProgress(x);let I=0;await Promise.all(S.map(U=>U.then(V=>{if(V===null||V.currentPackage.locatorHash===V.updatedPackage.locatorHash)return;I++;let{descriptor:te,currentPackage:ie,updatedPackage:ue}=V;a.reportInfo(0,`${j.prettyDescriptor(n,te)} can be deduped from ${j.prettyLocator(n,ie)} to ${j.prettyLocator(n,ue)}`),a.reportJson({descriptor:j.stringifyDescriptor(te),currentResolution:j.stringifyLocator(ie),updatedResolution:j.stringifyLocator(ue)}),e.storedResolutions.set(te.descriptorHash,ue.locatorHash)}).finally(()=>x.tick())));let T;switch(I){case 0:T="No packages";break;case 1:T="One package";break;default:T=`${I} packages`}let O=pe.pretty(n,t,pe.Type.CODE);return a.reportInfo(0,`${T} can be deduped using the ${O} strategy`),I})}var nC=class extends At{constructor(){super(...arguments);this.strategy=he.String("-s,--strategy","highest",{description:"The strategy to use when deduping dependencies",validator:ks(Xv)});this.check=he.Boolean("-c,--check",!1,{description:"Exit with exit code 1 when duplicates are found, without persisting the dependency tree"});this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.mode=he.String("--mode",{description:"Change what artifacts installs generate",validator:ks(Oa)});this.patterns=he.Rest()}static{this.paths=[["dedupe"]]}static{this.usage=at.Usage({description:"deduplicate dependencies with overlapping ranges",details:"\n Duplicates are defined as descriptors with overlapping ranges being resolved and locked to different locators. They are a natural consequence of Yarn's deterministic installs, but they can sometimes pile up and unnecessarily increase the size of your project.\n\n This command dedupes dependencies in the current project using different strategies (only one is implemented at the moment):\n\n - `highest`: Reuses (where possible) the locators with the highest versions. This means that dependencies can only be upgraded, never downgraded. It's also guaranteed that it never takes more than a single pass to dedupe the entire dependency tree.\n\n **Note:** Even though it never produces a wrong dependency tree, this command should be used with caution, as it modifies the dependency tree, which can sometimes cause problems when packages don't strictly follow semver recommendations. Because of this, it is recommended to also review the changes manually.\n\n If set, the `-c,--check` flag will only report the found duplicates, without persisting the modified dependency tree. If changes are found, the command will exit with a non-zero exit code, making it suitable for CI purposes.\n\n If the `--mode=` option is set, Yarn will change which artifacts are generated. The modes currently supported are:\n\n - `skip-build` will not run the build scripts at all. Note that this is different from setting `enableScripts` to false because the latter will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.\n\n - `update-lockfile` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.\n\n This command accepts glob patterns as arguments (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them.\n\n ### In-depth explanation:\n\n Yarn doesn't deduplicate dependencies by default, otherwise installs wouldn't be deterministic and the lockfile would be useless. What it actually does is that it tries to not duplicate dependencies in the first place.\n\n **Example:** If `foo@^2.3.4` (a dependency of a dependency) has already been resolved to `foo@2.3.4`, running `yarn add foo@*`will cause Yarn to reuse `foo@2.3.4`, even if the latest `foo` is actually `foo@2.10.14`, thus preventing unnecessary duplication.\n\n Duplication happens when Yarn can't unlock dependencies that have already been locked inside the lockfile.\n\n **Example:** If `foo@^2.3.4` (a dependency of a dependency) has already been resolved to `foo@2.3.4`, running `yarn add foo@2.10.14` will cause Yarn to install `foo@2.10.14` because the existing resolution doesn't satisfy the range `2.10.14`. This behavior can lead to (sometimes) unwanted duplication, since now the lockfile contains 2 separate resolutions for the 2 `foo` descriptors, even though they have overlapping ranges, which means that the lockfile can be simplified so that both descriptors resolve to `foo@2.10.14`.\n ",examples:[["Dedupe all packages","$0 dedupe"],["Dedupe all packages using a specific strategy","$0 dedupe --strategy highest"],["Dedupe a specific package","$0 dedupe lodash"],["Dedupe all packages with the `@babel/*` scope","$0 dedupe '@babel/*'"],["Check for duplicates (can be used as a CI step)","$0 dedupe --check"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Rt.find(r,this.context.cwd),a=await Kr.find(r);await s.restoreInstallState({restoreResolutions:!1});let n=0,c=await Ot.start({configuration:r,includeFooter:!1,stdout:this.context.stdout,json:this.json},async f=>{n=await d5(s,{strategy:this.strategy,patterns:this.patterns,cache:a,report:f})});return c.hasErrors()?c.exitCode():this.check?n?1:0:await s.installWithNewReport({json:this.json,stdout:this.context.stdout},{cache:a,mode:this.mode})}};qe();Yt();var iC=class extends At{static{this.paths=[["--clipanion=definitions"]]}async execute(){let{plugins:t}=await ze.find(this.context.cwd,this.context.plugins),r=[];for(let c of t){let{commands:f}=c[1];if(f){let h=Sa.from(f).definitions();r.push([c[0],h])}}let s=this.cli.definitions(),a=(c,f)=>c.split(" ").slice(1).join()===f.split(" ").slice(1).join(),n=sge()["@yarnpkg/builder"].bundles.standard;for(let c of r){let f=c[1];for(let p of f)s.find(h=>a(h.path,p.path)).plugin={name:c[0],isDefault:n.includes(c[0])}}this.context.stdout.write(`${JSON.stringify(s,null,2)} `)}};var sC=class extends At{static{this.paths=[["help"],["--help"],["-h"]]}async execute(){this.context.stdout.write(this.cli.usage(null))}};qe();Dt();Yt();var oC=class extends At{constructor(){super(...arguments);this.leadingArgument=he.String();this.args=he.Proxy()}async execute(){if(this.leadingArgument.match(/[\\/]/)&&!j.tryParseIdent(this.leadingArgument)){let r=J.resolve(this.context.cwd,fe.toPortablePath(this.leadingArgument));return await this.cli.run(this.args,{cwd:r})}else return await this.cli.run(["run",this.leadingArgument,...this.args])}};qe();var aC=class extends At{static{this.paths=[["-v"],["--version"]]}async execute(){this.context.stdout.write(`${An||""} `)}};qe();qe();Yt();var lC=class extends At{constructor(){super(...arguments);this.commandName=he.String();this.args=he.Proxy()}static{this.paths=[["exec"]]}static{this.usage=at.Usage({description:"execute a shell script",details:` This command simply executes a shell script within the context of the root directory of the active workspace using the portable shell. It also makes sure to call it in a way that's compatible with the current project (for example, on PnP projects the environment will be setup in such a way that PnP will be correctly injected into the environment). `,examples:[["Execute a single shell command","$0 exec echo Hello World"],["Execute a shell script",'$0 exec "tsc & babel src --out-dir lib"']]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,locator:a}=await Rt.find(r,this.context.cwd);return await s.restoreInstallState(),await Cn.executePackageShellcode(a,this.commandName,this.args,{cwd:this.context.cwd,stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr,project:s})}};qe();Yt();Al();var cC=class extends At{constructor(){super(...arguments);this.hash=he.String({required:!1,validator:Nx(SE(),[q2(/^p[0-9a-f]{6}$/)])})}static{this.paths=[["explain","peer-requirements"]]}static{this.usage=at.Usage({description:"explain a set of peer requirements",details:` A peer requirement represents all peer requests that a subject must satisfy when providing a requested package to requesters. When the hash argument is specified, this command prints a detailed explanation of the peer requirement corresponding to the hash and whether it is satisfied or not. When used without arguments, this command lists all peer requirements and the corresponding hash that can be used to get detailed information about a given requirement. **Note:** A hash is a seven-letter code consisting of the letter 'p' followed by six characters that can be obtained from peer dependency warnings or from the list of all peer requirements(\`yarn explain peer-requirements\`). `,examples:[["Explain the corresponding peer requirement for a hash","$0 explain peer-requirements p1a4ed"],["List all peer requirements","$0 explain peer-requirements"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Rt.find(r,this.context.cwd);return await s.restoreInstallState({restoreResolutions:!1}),await s.applyLightResolution(),typeof this.hash<"u"?await oot(this.hash,s,{stdout:this.context.stdout}):await aot(s,{stdout:this.context.stdout})}};async function oot(e,t,r){let s=t.peerRequirementNodes.get(e);if(typeof s>"u")throw new Error(`No peerDependency requirements found for hash: "${e}"`);let a=new Set,n=p=>a.has(p.requester.locatorHash)?{value:pe.tuple(pe.Type.DEPENDENT,{locator:p.requester,descriptor:p.descriptor}),children:p.children.size>0?[{value:pe.tuple(pe.Type.NO_HINT,"...")}]:[]}:(a.add(p.requester.locatorHash),{value:pe.tuple(pe.Type.DEPENDENT,{locator:p.requester,descriptor:p.descriptor}),children:Object.fromEntries(Array.from(p.children.values(),h=>[j.stringifyLocator(h.requester),n(h)]))}),c=t.peerWarnings.find(p=>p.hash===e);return(await Ot.start({configuration:t.configuration,stdout:r.stdout,includeFooter:!1,includePrefix:!1},async p=>{let h=pe.mark(t.configuration),E=c?h.Cross:h.Check;if(p.reportInfo(0,`Package ${pe.pretty(t.configuration,s.subject,pe.Type.LOCATOR)} is requested to provide ${pe.pretty(t.configuration,s.ident,pe.Type.IDENT)} by its descendants`),p.reportSeparator(),p.reportInfo(0,pe.pretty(t.configuration,s.subject,pe.Type.LOCATOR)),Rs.emitTree({children:Object.fromEntries(Array.from(s.requests.values(),C=>[j.stringifyLocator(C.requester),n(C)]))},{configuration:t.configuration,stdout:r.stdout,json:!1}),p.reportSeparator(),s.provided.range==="missing:"){let C=c?"":" , but all peer requests are optional";p.reportInfo(0,`${E} Package ${pe.pretty(t.configuration,s.subject,pe.Type.LOCATOR)} does not provide ${pe.pretty(t.configuration,s.ident,pe.Type.IDENT)}${C}.`)}else{let C=t.storedResolutions.get(s.provided.descriptorHash);if(!C)throw new Error("Assertion failed: Expected the descriptor to be registered");let S=t.storedPackages.get(C);if(!S)throw new Error("Assertion failed: Expected the package to be registered");p.reportInfo(0,`${E} Package ${pe.pretty(t.configuration,s.subject,pe.Type.LOCATOR)} provides ${pe.pretty(t.configuration,s.ident,pe.Type.IDENT)} with version ${j.prettyReference(t.configuration,S.version??"0.0.0")}, ${c?"which does not satisfy all requests.":"which satisfies all requests"}`),c?.type===3&&(c.range?p.reportInfo(0,` The combined requested range is ${pe.pretty(t.configuration,c.range,pe.Type.RANGE)}`):p.reportInfo(0," Unfortunately, the requested ranges have no overlap"))}})).exitCode()}async function aot(e,t){return(await Ot.start({configuration:e.configuration,stdout:t.stdout,includeFooter:!1,includePrefix:!1},async s=>{let a=pe.mark(e.configuration),n=Ge.sortMap(e.peerRequirementNodes,[([,c])=>j.stringifyLocator(c.subject),([,c])=>j.stringifyIdent(c.ident)]);for(let[,c]of n.values()){if(!c.root)continue;let f=e.peerWarnings.find(E=>E.hash===c.hash),p=[...j.allPeerRequests(c)],h;if(p.length>2?h=` and ${p.length-1} other dependencies`:p.length===2?h=" and 1 other dependency":h="",c.provided.range!=="missing:"){let E=e.storedResolutions.get(c.provided.descriptorHash);if(!E)throw new Error("Assertion failed: Expected the resolution to have been registered");let C=e.storedPackages.get(E);if(!C)throw new Error("Assertion failed: Expected the provided package to have been registered");let S=`${pe.pretty(e.configuration,c.hash,pe.Type.CODE)} \u2192 ${f?a.Cross:a.Check} ${j.prettyLocator(e.configuration,c.subject)} provides ${j.prettyLocator(e.configuration,C)} to ${j.prettyLocator(e.configuration,p[0].requester)}${h}`;f?s.reportWarning(0,S):s.reportInfo(0,S)}else{let E=`${pe.pretty(e.configuration,c.hash,pe.Type.CODE)} \u2192 ${f?a.Cross:a.Check} ${j.prettyLocator(e.configuration,c.subject)} doesn't provide ${j.prettyIdent(e.configuration,c.ident)} to ${j.prettyLocator(e.configuration,p[0].requester)}${h}`;f?s.reportWarning(0,E):s.reportInfo(0,E)}}})).exitCode()}qe();Yt();Al();qe();qe();Dt();Yt();var oge=et(pi()),uC=class extends At{constructor(){super(...arguments);this.useYarnPath=he.Boolean("--yarn-path",{description:"Set the yarnPath setting even if the version can be accessed by Corepack"});this.onlyIfNeeded=he.Boolean("--only-if-needed",!1,{description:"Only lock the Yarn version if it isn't already locked"});this.version=he.String()}static{this.paths=[["set","version"]]}static{this.usage=at.Usage({description:"lock the Yarn version used by the project",details:"\n This command will set a specific release of Yarn to be used by Corepack: https://nodejs.org/api/corepack.html.\n\n By default it only will set the `packageManager` field at the root of your project, but if the referenced release cannot be represented this way, if you already have `yarnPath` configured, or if you set the `--yarn-path` command line flag, then the release will also be downloaded from the Yarn GitHub repository, stored inside your project, and referenced via the `yarnPath` settings from your project `.yarnrc.yml` file.\n\n A very good use case for this command is to enforce the version of Yarn used by any single member of your team inside the same project - by doing this you ensure that you have control over Yarn upgrades and downgrades (including on your deployment servers), and get rid of most of the headaches related to someone using a slightly different version and getting different behavior.\n\n The version specifier can be:\n\n - a tag:\n - `latest` / `berry` / `stable` -> the most recent stable berry (`>=2.0.0`) release\n - `canary` -> the most recent canary (release candidate) berry (`>=2.0.0`) release\n - `classic` -> the most recent classic (`^0.x || ^1.x`) release\n\n - a semver range (e.g. `2.x`) -> the most recent version satisfying the range (limited to berry releases)\n\n - a semver version (e.g. `2.4.1`, `1.22.1`)\n\n - a local file referenced through either a relative or absolute path\n\n - `self` -> the version used to invoke the command\n ",examples:[["Download the latest release from the Yarn repository","$0 set version latest"],["Download the latest canary release from the Yarn repository","$0 set version canary"],["Download the latest classic release from the Yarn repository","$0 set version classic"],["Download the most recent Yarn 3 build","$0 set version 3.x"],["Download a specific Yarn 2 build","$0 set version 2.0.0-rc.30"],["Switch back to a specific Yarn 1 release","$0 set version 1.22.1"],["Use a release from the local filesystem","$0 set version ./yarn.cjs"],["Use a release from a URL","$0 set version https://repo.yarnpkg.com/3.1.0/packages/yarnpkg-cli/bin/yarn.js"],["Download the version used to invoke the command","$0 set version self"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins);if(this.onlyIfNeeded&&r.get("yarnPath")){let f=r.sources.get("yarnPath");if(!f)throw new Error("Assertion failed: Expected 'yarnPath' to have a source");let p=r.projectCwd??r.startingCwd;if(J.contains(p,f))return 0}let s=()=>{if(typeof An>"u")throw new it("The --install flag can only be used without explicit version specifier from the Yarn CLI");return`file://${process.argv[1]}`},a,n=(f,p)=>({version:p,url:f.replace(/\{\}/g,p)});if(this.version==="self")a={url:s(),version:An??"self"};else if(this.version==="latest"||this.version==="berry"||this.version==="stable")a=n("https://repo.yarnpkg.com/{}/packages/yarnpkg-cli/bin/yarn.js",await Zv(r,"stable"));else if(this.version==="canary")a=n("https://repo.yarnpkg.com/{}/packages/yarnpkg-cli/bin/yarn.js",await Zv(r,"canary"));else if(this.version==="classic")a={url:"https://classic.yarnpkg.com/latest.js",version:"classic"};else if(this.version.match(/^https?:/))a={url:this.version,version:"remote"};else if(this.version.match(/^\.{0,2}[\\/]/)||fe.isAbsolute(this.version))a={url:`file://${J.resolve(fe.toPortablePath(this.version))}`,version:"file"};else if(kr.satisfiesWithPrereleases(this.version,">=2.0.0"))a=n("https://repo.yarnpkg.com/{}/packages/yarnpkg-cli/bin/yarn.js",this.version);else if(kr.satisfiesWithPrereleases(this.version,"^0.x || ^1.x"))a=n("https://github.com/yarnpkg/yarn/releases/download/v{}/yarn-{}.js",this.version);else if(kr.validRange(this.version))a=n("https://repo.yarnpkg.com/{}/packages/yarnpkg-cli/bin/yarn.js",await lot(r,this.version));else throw new it(`Invalid version descriptor "${this.version}"`);return(await Ot.start({configuration:r,stdout:this.context.stdout,includeLogs:!this.context.quiet},async f=>{let p=async()=>{let h="file://";return a.url.startsWith(h)?(f.reportInfo(0,`Retrieving ${pe.pretty(r,a.url,pe.Type.PATH)}`),await le.readFilePromise(a.url.slice(h.length))):(f.reportInfo(0,`Downloading ${pe.pretty(r,a.url,pe.Type.URL)}`),await nn.get(a.url,{configuration:r}))};await g5(r,a.version,p,{report:f,useYarnPath:this.useYarnPath})})).exitCode()}};async function lot(e,t){let s=(await nn.get("https://repo.yarnpkg.com/tags",{configuration:e,jsonResponse:!0})).tags.filter(a=>kr.satisfiesWithPrereleases(a,t));if(s.length===0)throw new it(`No matching release found for range ${pe.pretty(e,t,pe.Type.RANGE)}.`);return s[0]}async function Zv(e,t){let r=await nn.get("https://repo.yarnpkg.com/tags",{configuration:e,jsonResponse:!0});if(!r.latest[t])throw new it(`Tag ${pe.pretty(e,t,pe.Type.RANGE)} not found`);return r.latest[t]}async function g5(e,t,r,{report:s,useYarnPath:a}){let n,c=async()=>(typeof n>"u"&&(n=await r()),n);if(t===null){let te=await c();await le.mktempPromise(async ie=>{let ue=J.join(ie,"yarn.cjs");await le.writeFilePromise(ue,te);let{stdout:ae}=await qr.execvp(process.execPath,[fe.fromPortablePath(ue),"--version"],{cwd:ie,env:{...e.env,YARN_IGNORE_PATH:"1"}});if(t=ae.trim(),!oge.default.valid(t))throw new Error(`Invalid semver version. ${pe.pretty(e,"yarn --version",pe.Type.CODE)} returned: ${t}`)})}let f=e.projectCwd??e.startingCwd,p=J.resolve(f,".yarn/releases"),h=J.resolve(p,`yarn-${t}.cjs`),E=J.relative(e.startingCwd,h),C=Ge.isTaggedYarnVersion(t),S=e.get("yarnPath"),x=!C,I=x||!!S||!!a;if(a===!1){if(x)throw new Lt(0,"You explicitly opted out of yarnPath usage in your command line, but the version you specified cannot be represented by Corepack");I=!1}else!I&&!process.env.COREPACK_ROOT&&(s.reportWarning(0,`You don't seem to have ${pe.applyHyperlink(e,"Corepack","https://nodejs.org/api/corepack.html")} enabled; we'll have to rely on ${pe.applyHyperlink(e,"yarnPath","https://yarnpkg.com/configuration/yarnrc#yarnPath")} instead`),I=!0);if(I){let te=await c();s.reportInfo(0,`Saving the new release in ${pe.pretty(e,E,"magenta")}`),await le.removePromise(J.dirname(h)),await le.mkdirPromise(J.dirname(h),{recursive:!0}),await le.writeFilePromise(h,te,{mode:493}),await ze.updateConfiguration(f,{yarnPath:J.relative(f,h)})}else await le.removePromise(J.dirname(h)),await ze.updateConfiguration(f,{yarnPath:ze.deleteProperty});let T=await _t.tryFind(f)||new _t;T.packageManager=`yarn@${C?t:await Zv(e,"stable")}`;let O={};T.exportTo(O);let U=J.join(f,_t.fileName),V=`${JSON.stringify(O,null,T.indent)} `;return await le.changeFilePromise(U,V,{automaticNewlines:!0}),{bundleVersion:t}}function age(e){return Ir[jx(e)]}var cot=/## (?YN[0-9]{4}) - `(?[A-Z_]+)`\n\n(?
    (?:.(?!##))+)/gs;async function uot(e){let r=`https://repo.yarnpkg.com/${Ge.isTaggedYarnVersion(An)?An:await Zv(e,"canary")}/packages/docusaurus/docs/advanced/01-general-reference/error-codes.mdx`,s=await nn.get(r,{configuration:e});return new Map(Array.from(s.toString().matchAll(cot),({groups:a})=>{if(!a)throw new Error("Assertion failed: Expected the match to have been successful");let n=age(a.code);if(a.name!==n)throw new Error(`Assertion failed: Invalid error code data: Expected "${a.name}" to be named "${n}"`);return[a.code,a.details]}))}var fC=class extends At{constructor(){super(...arguments);this.code=he.String({required:!1,validator:W2(SE(),[q2(/^YN[0-9]{4}$/)])});this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}static{this.paths=[["explain"]]}static{this.usage=at.Usage({description:"explain an error code",details:` When the code argument is specified, this command prints its name and its details. When used without arguments, this command lists all error codes and their names. `,examples:[["Explain an error code","$0 explain YN0006"],["List all error codes","$0 explain"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins);if(typeof this.code<"u"){let s=age(this.code),a=pe.pretty(r,s,pe.Type.CODE),n=this.cli.format().header(`${this.code} - ${a}`),f=(await uot(r)).get(this.code),p=typeof f<"u"?pe.jsonOrPretty(this.json,r,pe.tuple(pe.Type.MARKDOWN,{text:f,format:this.cli.format(),paragraphs:!0})):`This error code does not have a description. You can help us by editing this page on GitHub \u{1F642}: ${pe.jsonOrPretty(this.json,r,pe.tuple(pe.Type.URL,"https://github.com/yarnpkg/berry/blob/master/packages/docusaurus/docs/advanced/01-general-reference/error-codes.mdx"))} `;this.json?this.context.stdout.write(`${JSON.stringify({code:this.code,name:s,details:p})} `):this.context.stdout.write(`${n} ${p} `)}else{let s={children:Ge.mapAndFilter(Object.entries(Ir),([a,n])=>Number.isNaN(Number(a))?Ge.mapAndFilter.skip:{label:Kf(Number(a)),value:pe.tuple(pe.Type.CODE,n)})};Rs.emitTree(s,{configuration:r,stdout:this.context.stdout,json:this.json})}}};qe();Dt();Yt();var lge=et(zo()),AC=class extends At{constructor(){super(...arguments);this.all=he.Boolean("-A,--all",!1,{description:"Print versions of a package from the whole project"});this.recursive=he.Boolean("-R,--recursive",!1,{description:"Print information for all packages, including transitive dependencies"});this.extra=he.Array("-X,--extra",[],{description:"An array of requests of extra data provided by plugins"});this.cache=he.Boolean("--cache",!1,{description:"Print information about the cache entry of a package (path, size, checksum)"});this.dependents=he.Boolean("--dependents",!1,{description:"Print all dependents for each matching package"});this.manifest=he.Boolean("--manifest",!1,{description:"Print data obtained by looking at the package archive (license, homepage, ...)"});this.nameOnly=he.Boolean("--name-only",!1,{description:"Only print the name for the matching packages"});this.virtuals=he.Boolean("--virtuals",!1,{description:"Print each instance of the virtual packages"});this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.patterns=he.Rest()}static{this.paths=[["info"]]}static{this.usage=at.Usage({description:"see information related to packages",details:"\n This command prints various information related to the specified packages, accepting glob patterns.\n\n By default, if the locator reference is missing, Yarn will default to print the information about all the matching direct dependencies of the package for the active workspace. To instead print all versions of the package that are direct dependencies of any of your workspaces, use the `-A,--all` flag. Adding the `-R,--recursive` flag will also report transitive dependencies.\n\n Some fields will be hidden by default in order to keep the output readable, but can be selectively displayed by using additional options (`--dependents`, `--manifest`, `--virtuals`, ...) described in the option descriptions.\n\n Note that this command will only print the information directly related to the selected packages - if you wish to know why the package is there in the first place, use `yarn why` which will do just that (it also provides a `-R,--recursive` flag that may be of some help).\n ",examples:[["Show information about Lodash","$0 info lodash"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(!a&&!this.all)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState();let c=new Set(this.extra);this.cache&&c.add("cache"),this.dependents&&c.add("dependents"),this.manifest&&c.add("manifest");let f=(ie,{recursive:ue})=>{let ae=ie.anchoredLocator.locatorHash,ge=new Map,Ae=[ae];for(;Ae.length>0;){let Ce=Ae.shift();if(ge.has(Ce))continue;let Ee=s.storedPackages.get(Ce);if(typeof Ee>"u")throw new Error("Assertion failed: Expected the package to be registered");if(ge.set(Ce,Ee),j.isVirtualLocator(Ee)&&Ae.push(j.devirtualizeLocator(Ee).locatorHash),!(!ue&&Ce!==ae))for(let d of Ee.dependencies.values()){let Se=s.storedResolutions.get(d.descriptorHash);if(typeof Se>"u")throw new Error("Assertion failed: Expected the resolution to be registered");Ae.push(Se)}}return ge.values()},p=({recursive:ie})=>{let ue=new Map;for(let ae of s.workspaces)for(let ge of f(ae,{recursive:ie}))ue.set(ge.locatorHash,ge);return ue.values()},h=({all:ie,recursive:ue})=>ie&&ue?s.storedPackages.values():ie?p({recursive:ue}):f(a,{recursive:ue}),E=({all:ie,recursive:ue})=>{let ae=h({all:ie,recursive:ue}),ge=this.patterns.map(Ee=>{let d=j.parseLocator(Ee),Se=lge.default.makeRe(j.stringifyIdent(d)),Be=j.isVirtualLocator(d),me=Be?j.devirtualizeLocator(d):d;return ce=>{let Z=j.stringifyIdent(ce);if(!Se.test(Z))return!1;if(d.reference==="unknown")return!0;let De=j.isVirtualLocator(ce),Qe=De?j.devirtualizeLocator(ce):ce;return!(Be&&De&&d.reference!==ce.reference||me.reference!==Qe.reference)}}),Ae=Ge.sortMap([...ae],Ee=>j.stringifyLocator(Ee));return{selection:Ae.filter(Ee=>ge.length===0||ge.some(d=>d(Ee))),sortedLookup:Ae}},{selection:C,sortedLookup:S}=E({all:this.all,recursive:this.recursive});if(C.length===0)throw new it("No package matched your request");let x=new Map;if(this.dependents)for(let ie of S)for(let ue of ie.dependencies.values()){let ae=s.storedResolutions.get(ue.descriptorHash);if(typeof ae>"u")throw new Error("Assertion failed: Expected the resolution to be registered");Ge.getArrayWithDefault(x,ae).push(ie)}let I=new Map;for(let ie of S){if(!j.isVirtualLocator(ie))continue;let ue=j.devirtualizeLocator(ie);Ge.getArrayWithDefault(I,ue.locatorHash).push(ie)}let T={},O={children:T},U=r.makeFetcher(),V={project:s,fetcher:U,cache:n,checksums:s.storedChecksums,report:new ki,cacheOptions:{skipIntegrityCheck:!0}},te=[async(ie,ue,ae)=>{if(!ue.has("manifest"))return;let ge=await U.fetch(ie,V),Ae;try{Ae=await _t.find(ge.prefixPath,{baseFs:ge.packageFs})}finally{ge.releaseFs?.()}ae("Manifest",{License:pe.tuple(pe.Type.NO_HINT,Ae.license),Homepage:pe.tuple(pe.Type.URL,Ae.raw.homepage??null)})},async(ie,ue,ae)=>{if(!ue.has("cache"))return;let ge=s.storedChecksums.get(ie.locatorHash)??null,Ae=n.getLocatorPath(ie,ge),Ce;if(Ae!==null)try{Ce=await le.statPromise(Ae)}catch{}let Ee=typeof Ce<"u"?[Ce.size,pe.Type.SIZE]:void 0;ae("Cache",{Checksum:pe.tuple(pe.Type.NO_HINT,ge),Path:pe.tuple(pe.Type.PATH,Ae),Size:Ee})}];for(let ie of C){let ue=j.isVirtualLocator(ie);if(!this.virtuals&&ue)continue;let ae={},ge={value:[ie,pe.Type.LOCATOR],children:ae};if(T[j.stringifyLocator(ie)]=ge,this.nameOnly){delete ge.children;continue}let Ae=I.get(ie.locatorHash);typeof Ae<"u"&&(ae.Instances={label:"Instances",value:pe.tuple(pe.Type.NUMBER,Ae.length)}),ae.Version={label:"Version",value:pe.tuple(pe.Type.NO_HINT,ie.version)};let Ce=(d,Se)=>{let Be={};if(ae[d]=Be,Array.isArray(Se))Be.children=Se.map(me=>({value:me}));else{let me={};Be.children=me;for(let[ce,Z]of Object.entries(Se))typeof Z>"u"||(me[ce]={label:ce,value:Z})}};if(!ue){for(let d of te)await d(ie,c,Ce);await r.triggerHook(d=>d.fetchPackageInfo,ie,c,Ce)}ie.bin.size>0&&!ue&&Ce("Exported Binaries",[...ie.bin.keys()].map(d=>pe.tuple(pe.Type.PATH,d)));let Ee=x.get(ie.locatorHash);typeof Ee<"u"&&Ee.length>0&&Ce("Dependents",Ee.map(d=>pe.tuple(pe.Type.LOCATOR,d))),ie.dependencies.size>0&&!ue&&Ce("Dependencies",[...ie.dependencies.values()].map(d=>{let Se=s.storedResolutions.get(d.descriptorHash),Be=typeof Se<"u"?s.storedPackages.get(Se)??null:null;return pe.tuple(pe.Type.RESOLUTION,{descriptor:d,locator:Be})})),ie.peerDependencies.size>0&&ue&&Ce("Peer dependencies",[...ie.peerDependencies.values()].map(d=>{let Se=ie.dependencies.get(d.identHash),Be=typeof Se<"u"?s.storedResolutions.get(Se.descriptorHash)??null:null,me=Be!==null?s.storedPackages.get(Be)??null:null;return pe.tuple(pe.Type.RESOLUTION,{descriptor:d,locator:me})}))}Rs.emitTree(O,{configuration:r,json:this.json,stdout:this.context.stdout,separators:this.nameOnly?0:2})}};qe();Dt();vc();var HT=et(Rg());Yt();var m5=et(pi());Al();var fot=[{selector:e=>e===-1,name:"nodeLinker",value:"node-modules"},{selector:e=>e!==-1&&e<8,name:"enableGlobalCache",value:!1},{selector:e=>e!==-1&&e<8,name:"compressionLevel",value:"mixed"},{selector:e=>e<9,name:"approvedGitRepositories",value:["**"]},{selector:e=>e<9,name:"enableScripts",value:!0}],pC=class extends At{constructor(){super(...arguments);this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.immutable=he.Boolean("--immutable",{description:"Abort with an error exit code if the lockfile was to be modified"});this.immutableCache=he.Boolean("--immutable-cache",{description:"Abort with an error exit code if the cache folder was to be modified"});this.refreshLockfile=he.Boolean("--refresh-lockfile",{description:"Refresh the package metadata stored in the lockfile"});this.checkCache=he.Boolean("--check-cache",{description:"Always refetch the packages and ensure that their checksums are consistent"});this.checkResolutions=he.Boolean("--check-resolutions",{description:"Validates that the package resolutions are coherent"});this.inlineBuilds=he.Boolean("--inline-builds",{description:"Verbosely print the output of the build steps of dependencies"});this.mode=he.String("--mode",{description:"Change what artifacts installs generate",validator:ks(Oa)});this.cacheFolder=he.String("--cache-folder",{hidden:!0});this.frozenLockfile=he.Boolean("--frozen-lockfile",{hidden:!0});this.ignoreEngines=he.Boolean("--ignore-engines",{hidden:!0});this.nonInteractive=he.Boolean("--non-interactive",{hidden:!0});this.preferOffline=he.Boolean("--prefer-offline",{hidden:!0});this.production=he.Boolean("--production",{hidden:!0});this.registry=he.String("--registry",{hidden:!0});this.silent=he.Boolean("--silent",{hidden:!0});this.networkTimeout=he.String("--network-timeout",{hidden:!0})}static{this.paths=[["install"],at.Default]}static{this.usage=at.Usage({description:"install the project dependencies",details:"\n This command sets up your project if needed. The installation is split into four different steps that each have their own characteristics:\n\n - **Resolution:** First the package manager will resolve your dependencies. The exact way a dependency version is privileged over another isn't standardized outside of the regular semver guarantees. If a package doesn't resolve to what you would expect, check that all dependencies are correctly declared (also check our website for more information: ).\n\n - **Fetch:** Then we download all the dependencies if needed, and make sure that they're all stored within our cache (check the value of `cacheFolder` in `yarn config` to see where the cache files are stored).\n\n - **Link:** Then we send the dependency tree information to internal plugins tasked with writing them on the disk in some form (for example by generating the `.pnp.cjs` file you might know).\n\n - **Build:** Once the dependency tree has been written on the disk, the package manager will now be free to run the build scripts for all packages that might need it, in a topological order compatible with the way they depend on one another. See https://yarnpkg.com/advanced/lifecycle-scripts for detail.\n\n Note that running this command is not part of the recommended workflow. Yarn supports zero-installs, which means that as long as you store your cache and your `.pnp.cjs` file inside your repository, everything will work without requiring any install right after cloning your repository or switching branches.\n\n If the `--immutable` option is set (defaults to true on CI), Yarn will abort with an error exit code if the lockfile was to be modified (other paths can be added using the `immutablePatterns` configuration setting). For backward compatibility we offer an alias under the name of `--frozen-lockfile`, but it will be removed in a later release.\n\n If the `--immutable-cache` option is set, Yarn will abort with an error exit code if the cache folder was to be modified (either because files would be added, or because they'd be removed).\n\n If the `--refresh-lockfile` option is set, Yarn will keep the same resolution for the packages currently in the lockfile but will refresh their metadata. If used together with `--immutable`, it can validate that the lockfile information are consistent. This flag is enabled by default when Yarn detects it runs within a pull request context.\n\n If the `--check-cache` option is set, Yarn will always refetch the packages and will ensure that their checksum matches what's 1/ described in the lockfile 2/ inside the existing cache files (if present). This is recommended as part of your CI workflow if you're both following the Zero-Installs model and accepting PRs from third-parties, as they'd otherwise have the ability to alter the checked-in packages before submitting them.\n\n If the `--inline-builds` option is set, Yarn will verbosely print the output of the build steps of your dependencies (instead of writing them into individual files). This is likely useful mostly for debug purposes only when using Docker-like environments.\n\n If the `--mode=` option is set, Yarn will change which artifacts are generated. The modes currently supported are:\n\n - `skip-build` will not run the build scripts at all. Note that this is different from setting `enableScripts` to false because the latter will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.\n\n - `update-lockfile` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.\n ",examples:[["Install the project","$0 install"],["Validate a project when using Zero-Installs","$0 install --immutable --immutable-cache"],["Validate a project when using Zero-Installs (slightly safer if you accept external PRs)","$0 install --immutable --immutable-cache --check-cache"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins);typeof this.inlineBuilds<"u"&&r.useWithSource("",{enableInlineBuilds:this.inlineBuilds},r.startingCwd,{overwrite:!0});let s=!!process.env.FUNCTION_TARGET||!!process.env.GOOGLE_RUNTIME,a=await PI({configuration:r,stdout:this.context.stdout},[{option:this.ignoreEngines,message:"The --ignore-engines option is deprecated; engine checking isn't a core feature anymore",error:!HT.default.VERCEL},{option:this.registry,message:"The --registry option is deprecated; prefer setting npmRegistryServer in your .yarnrc.yml file"},{option:this.preferOffline,message:"The --prefer-offline flag is deprecated; use the --cached flag with 'yarn add' instead",error:!HT.default.VERCEL},{option:this.production,message:"The --production option is deprecated on 'install'; use 'yarn workspaces focus' instead",error:!0},{option:this.nonInteractive,message:"The --non-interactive option is deprecated",error:!s},{option:this.frozenLockfile,message:"The --frozen-lockfile option is deprecated; use --immutable and/or --immutable-cache instead",callback:()=>this.immutable=this.frozenLockfile},{option:this.cacheFolder,message:"The cache-folder option has been deprecated; use rc settings instead",error:!HT.default.NETLIFY}]);if(a!==null)return a;let n=this.mode==="update-lockfile";if(n&&(this.immutable||this.immutableCache))throw new it(`${pe.pretty(r,"--immutable",pe.Type.CODE)} and ${pe.pretty(r,"--immutable-cache",pe.Type.CODE)} cannot be used with ${pe.pretty(r,"--mode=update-lockfile",pe.Type.CODE)}`);let c=(this.immutable??r.get("enableImmutableInstalls"))&&!n,f=this.immutableCache&&!n;if(r.projectCwd!==null){let T=await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout,includeFooter:!1},async O=>{let U=!1;await hot(r,c)&&(O.reportInfo(48,"Automatically removed core plugins that are now builtins \u{1F44D}"),U=!0),await pot(r,c)&&(O.reportInfo(48,"Automatically fixed merge conflicts \u{1F44D}"),U=!0),U&&O.reportSeparator()});if(T.hasErrors())return T.exitCode()}if(r.projectCwd!==null){let T=await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout,includeFooter:!1},async O=>{if(ze.telemetry?.isNew)ze.telemetry.commitTips(),O.reportInfo(65,"Yarn will periodically gather anonymous telemetry: https://yarnpkg.com/advanced/telemetry"),O.reportInfo(65,`Run ${pe.pretty(r,"yarn config set --home enableTelemetry 0",pe.Type.CODE)} to disable`),O.reportSeparator();else if(ze.telemetry?.shouldShowTips){let U=await nn.get("https://repo.yarnpkg.com/tags",{configuration:r,jsonResponse:!0}).catch(()=>null);if(U!==null){let V=null;if(An!==null){let ie=m5.default.prerelease(An)?"canary":"stable",ue=U.latest[ie];ue!==null&&m5.default.gt(ue,An)&&(V=[ie,ue])}if(V)ze.telemetry.commitTips(),O.reportInfo(88,`${pe.applyStyle(r,`A new ${V[0]} version of Yarn is available:`,pe.Style.BOLD)} ${j.prettyReference(r,V[1])}!`),O.reportInfo(88,`Upgrade now by running ${pe.pretty(r,`yarn set version ${V[1]}`,pe.Type.CODE)}`),O.reportSeparator();else{let te=ze.telemetry.selectTip(U.tips);te&&(O.reportInfo(89,pe.pretty(r,te.message,pe.Type.MARKDOWN_INLINE)),te.url&&O.reportInfo(89,`Learn more at ${te.url}`),O.reportSeparator())}}}});if(T.hasErrors())return T.exitCode()}let{project:p,workspace:h}=await Rt.find(r,this.context.cwd),E=p.lockfileLastVersion;if(E!==null){let T=await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout,includeFooter:!1},async O=>{let U={};for(let V of fot)V.selector(E)&&typeof r.sources.get(V.name)>"u"&&(r.use("",{[V.name]:V.value},p.cwd,{overwrite:!0}),U[V.name]=V.value);Object.keys(U).length>0&&(await ze.updateConfiguration(p.cwd,U),O.reportInfo(87,"Migrated your project to the latest Yarn version \u{1F680}"),O.reportSeparator())});if(T.hasErrors())return T.exitCode()}let C=await Kr.find(r,{immutable:f,check:this.checkCache});if(!h)throw new ar(p.cwd,this.context.cwd);await p.restoreInstallState({restoreResolutions:!1});let S=r.get("enableHardenedMode");S&&typeof r.sources.get("enableHardenedMode")>"u"&&await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout,includeFooter:!1},async T=>{T.reportWarning(0,"Yarn detected that the current workflow is executed from a public pull request. For safety the hardened mode has been enabled."),T.reportWarning(0,`It will prevent malicious lockfile manipulations, in exchange for a slower install time. You can opt-out if necessary; check our ${pe.applyHyperlink(r,"documentation","https://yarnpkg.com/features/security#hardened-mode")} for more details.`),T.reportSeparator()}),(this.refreshLockfile??S)&&(p.lockfileNeedsRefresh=!0);let x=this.checkResolutions??S;return(await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout,forceSectionAlignment:!0,includeLogs:!0,includeVersion:!0},async T=>{await p.install({cache:C,report:T,immutable:c,checkResolutions:x,mode:this.mode})})).exitCode()}},Aot="<<<<<<<";async function pot(e,t){if(!e.projectCwd)return!1;let r=J.join(e.projectCwd,Er.lockfile);if(!await le.existsPromise(r)||!(await le.readFilePromise(r,"utf8")).includes(Aot))return!1;if(t)throw new Lt(47,"Cannot autofix a lockfile when running an immutable install");let a=await qr.execvp("git",["rev-parse","MERGE_HEAD","HEAD"],{cwd:e.projectCwd});if(a.code!==0&&(a=await qr.execvp("git",["rev-parse","REBASE_HEAD","HEAD"],{cwd:e.projectCwd})),a.code!==0&&(a=await qr.execvp("git",["rev-parse","CHERRY_PICK_HEAD","HEAD"],{cwd:e.projectCwd})),a.code!==0)throw new Lt(83,"Git returned an error when trying to find the commits pertaining to the conflict");let n=await Promise.all(a.stdout.trim().split(/\n/).map(async f=>{let p=await qr.execvp("git",["show",`${f}:./${Er.lockfile}`],{cwd:e.projectCwd});if(p.code!==0)throw new Lt(83,`Git returned an error when trying to access the lockfile content in ${f}`);try{return cs(p.stdout)}catch{throw new Lt(46,"A variant of the conflicting lockfile failed to parse")}}));n=n.filter(f=>!!f.__metadata);for(let f of n){if(f.__metadata.version<7)for(let p of Object.keys(f)){if(p==="__metadata")continue;let h=j.parseDescriptor(p,!0),E=e.normalizeDependency(h),C=j.stringifyDescriptor(E);C!==p&&(f[C]=f[p],delete f[p])}for(let p of Object.keys(f)){if(p==="__metadata")continue;let h=f[p].checksum;typeof h>"u"||h.includes("/")||(f[p].checksum=`${f.__metadata.cacheKey}/${h}`)}}let c=Object.assign({},...n);c.__metadata.version=`${Math.min(...n.map(f=>parseInt(f.__metadata.version??0)))}`,c.__metadata.cacheKey="merged";for(let[f,p]of Object.entries(c))typeof p=="string"&&delete c[f];return await le.changeFilePromise(r,fl(c),{automaticNewlines:!0}),!0}async function hot(e,t){if(!e.projectCwd)return!1;let r=[],s=J.join(e.projectCwd,".yarn/plugins/@yarnpkg");return await ze.updateConfiguration(e.projectCwd,{plugins:n=>{if(!Array.isArray(n))return n;let c=n.filter(f=>{if(!f.path)return!0;let p=J.resolve(e.projectCwd,f.path),h=ZB.has(f.spec)&&J.contains(s,p);return h&&r.push(p),!h});return c.length===0?ze.deleteProperty:c.length===n.length?n:c}},{immutable:t})?(await Promise.all(r.map(async n=>{await le.removePromise(n)})),!0):!1}qe();Dt();Yt();var hC=class extends At{constructor(){super(...arguments);this.all=he.Boolean("-A,--all",!1,{description:"Link all workspaces belonging to the target projects to the current one"});this.private=he.Boolean("-p,--private",!1,{description:"Also link private workspaces belonging to the target projects to the current one"});this.relative=he.Boolean("-r,--relative",!1,{description:"Link workspaces using relative paths instead of absolute paths"});this.destinations=he.Rest()}static{this.paths=[["link"]]}static{this.usage=at.Usage({description:"connect the local project to another one",details:"\n This command will set a new `resolutions` field in the project-level manifest and point it to the workspace at the specified location (even if part of another project).\n ",examples:[["Register one or more remote workspaces for use in the current project","$0 link ~/ts-loader ~/jest"],["Register all workspaces from a remote project for use in the current project","$0 link ~/jest --all"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState({restoreResolutions:!1});let c=s.topLevelWorkspace,f=[];for(let p of this.destinations){let h=J.resolve(this.context.cwd,fe.toPortablePath(p)),E=await ze.find(h,this.context.plugins,{useRc:!1,strict:!1}),{project:C,workspace:S}=await Rt.find(E,h);if(s.cwd===C.cwd)throw new it(`Invalid destination '${p}'; Can't link the project to itself`);if(!S)throw new ar(C.cwd,h);if(this.all){let x=!1;for(let I of C.workspaces)I.manifest.name&&(!I.manifest.private||this.private)&&(f.push(I),x=!0);if(!x)throw new it(`No workspace found to be linked in the target project: ${p}`)}else{if(!S.manifest.name)throw new it(`The target workspace at '${p}' doesn't have a name and thus cannot be linked`);if(S.manifest.private&&!this.private)throw new it(`The target workspace at '${p}' is marked private - use the --private flag to link it anyway`);f.push(S)}}for(let p of f){let h=j.stringifyIdent(p.anchoredLocator),E=this.relative?J.relative(s.cwd,p.cwd):p.cwd;c.manifest.resolutions.push({pattern:{descriptor:{fullName:h}},reference:`portal:${E}`})}return await s.installWithNewReport({stdout:this.context.stdout},{cache:n})}};Yt();var dC=class extends At{constructor(){super(...arguments);this.args=he.Proxy()}static{this.paths=[["node"]]}static{this.usage=at.Usage({description:"run node with the hook already setup",details:` This command simply runs Node. It also makes sure to call it in a way that's compatible with the current project (for example, on PnP projects the environment will be setup in such a way that PnP will be correctly injected into the environment). The Node process will use the exact same version of Node as the one used to run Yarn itself, which might be a good way to ensure that your commands always use a consistent Node version. `,examples:[["Run a Node script","$0 node ./my-script.js"]]})}async execute(){return this.cli.run(["exec","node",...this.args])}};qe();Yt();var gC=class extends At{constructor(){super(...arguments);this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}static{this.paths=[["plugin","check"]]}static{this.usage=at.Usage({category:"Plugin-related commands",description:"find all third-party plugins that differ from their own spec",details:` Check only the plugins from https. If this command detects any plugin differences in the CI environment, it will throw an error. `,examples:[["find all third-party plugins that differ from their own spec","$0 plugin check"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=await ze.findRcFiles(this.context.cwd);return(await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout},async n=>{for(let c of s)if(c.data?.plugins)for(let f of c.data.plugins){if(!f.checksum||!f.spec.match(/^https?:/))continue;let p=await nn.get(f.spec,{configuration:r}),h=Ln.makeHash(p);if(f.checksum===h)continue;let E=pe.pretty(r,f.path,pe.Type.PATH),C=pe.pretty(r,f.spec,pe.Type.URL),S=`${E} is different from the file provided by ${C}`;n.reportJson({...f,newChecksum:h}),n.reportError(0,S)}})).exitCode()}};qe();qe();Dt();Yt();var pge=Ie("os");qe();Dt();Yt();var cge=Ie("os");qe();vc();Yt();var dot="https://raw.githubusercontent.com/yarnpkg/berry/master/plugins.yml";async function Pm(e,t){let r=await nn.get(dot,{configuration:e}),s=cs(r.toString());return Object.fromEntries(Object.entries(s).filter(([a,n])=>!t||kr.satisfiesWithPrereleases(t,n.range??"<4.0.0-rc.1")))}var mC=class extends At{constructor(){super(...arguments);this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}static{this.paths=[["plugin","list"]]}static{this.usage=at.Usage({category:"Plugin-related commands",description:"list the available official plugins",details:"\n This command prints the plugins available directly from the Yarn repository. Only those plugins can be referenced by name in `yarn plugin import`.\n ",examples:[["List the official plugins","$0 plugin list"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins);return(await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout},async a=>{let n=await Pm(r,An);for(let[c,{experimental:f,...p}]of Object.entries(n)){let h=c;f&&(h+=" [experimental]"),a.reportJson({name:c,experimental:f,...p}),a.reportInfo(null,h)}})).exitCode()}};var got=/^[0-9]+$/,mot=process.platform==="win32";function uge(e){return got.test(e)?`pull/${e}/head`:e}var yot=({repository:e,branch:t},r)=>[["git","init",fe.fromPortablePath(r)],["git","remote","add","origin",e],["git","fetch","origin","--depth=1",uge(t)],["git","reset","--hard","FETCH_HEAD"]],Eot=({branch:e})=>[["git","fetch","origin","--depth=1",uge(e),"--force"],["git","reset","--hard","FETCH_HEAD"],["git","clean","-dfx","-e","packages/yarnpkg-cli/bundles"]],Iot=({plugins:e,noMinify:t},r,s)=>[["yarn","build:cli",...new Array().concat(...e.map(a=>["--plugin",J.resolve(s,a)])),...t?["--no-minify"]:[],"|"],[mot?"move":"mv","packages/yarnpkg-cli/bundles/yarn.js",fe.fromPortablePath(r),"|"]],yC=class extends At{constructor(){super(...arguments);this.installPath=he.String("--path",{description:"The path where the repository should be cloned to"});this.repository=he.String("--repository","https://github.com/yarnpkg/berry.git",{description:"The repository that should be cloned"});this.branch=he.String("--branch","master",{description:"The branch of the repository that should be cloned"});this.plugins=he.Array("--plugin",[],{description:"An array of additional plugins that should be included in the bundle"});this.dryRun=he.Boolean("-n,--dry-run",!1,{description:"If set, the bundle will be built but not added to the project"});this.noMinify=he.Boolean("--no-minify",!1,{description:"Build a bundle for development (debugging) - non-minified and non-mangled"});this.force=he.Boolean("-f,--force",!1,{description:"Always clone the repository instead of trying to fetch the latest commits"});this.skipPlugins=he.Boolean("--skip-plugins",!1,{description:"Skip updating the contrib plugins"})}static{this.paths=[["set","version","from","sources"]]}static{this.usage=at.Usage({description:"build Yarn from master",details:` This command will clone the Yarn repository into a temporary folder, then build it. The resulting bundle will then be copied into the local project. By default, it also updates all contrib plugins to the same commit the bundle is built from. This behavior can be disabled by using the \`--skip-plugins\` flag. `,examples:[["Build Yarn from master","$0 set version from sources"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Rt.find(r,this.context.cwd),a=typeof this.installPath<"u"?J.resolve(this.context.cwd,fe.toPortablePath(this.installPath)):J.resolve(fe.toPortablePath((0,cge.tmpdir)()),"yarnpkg-sources",Ln.makeHash(this.repository).slice(0,6));return(await Ot.start({configuration:r,stdout:this.context.stdout},async c=>{await y5(this,{configuration:r,report:c,target:a}),c.reportSeparator(),c.reportInfo(0,"Building a fresh bundle"),c.reportSeparator();let f=await qr.execvp("git",["rev-parse","--short","HEAD"],{cwd:a,strict:!0}),p=J.join(a,`packages/yarnpkg-cli/bundles/yarn-${f.stdout.trim()}.js`);le.existsSync(p)||(await $v(Iot(this,p,a),{configuration:r,context:this.context,target:a}),c.reportSeparator());let h=await le.readFilePromise(p);if(!this.dryRun){let{bundleVersion:E}=await g5(r,null,async()=>h,{report:c});this.skipPlugins||await Cot(this,E,{project:s,report:c,target:a})}})).exitCode()}};async function $v(e,{configuration:t,context:r,target:s}){for(let[a,...n]of e){let c=n[n.length-1]==="|";if(c&&n.pop(),c)await qr.pipevp(a,n,{cwd:s,stdin:r.stdin,stdout:r.stdout,stderr:r.stderr,strict:!0});else{r.stdout.write(`${pe.pretty(t,` $ ${[a,...n].join(" ")}`,"grey")} `);try{await qr.execvp(a,n,{cwd:s,strict:!0})}catch(f){throw r.stdout.write(f.stdout||f.stack),f}}}}async function y5(e,{configuration:t,report:r,target:s}){let a=!1;if(!e.force&&le.existsSync(J.join(s,".git"))){r.reportInfo(0,"Fetching the latest commits"),r.reportSeparator();try{await $v(Eot(e),{configuration:t,context:e.context,target:s}),a=!0}catch{r.reportSeparator(),r.reportWarning(0,"Repository update failed; we'll try to regenerate it")}}a||(r.reportInfo(0,"Cloning the remote repository"),r.reportSeparator(),await le.removePromise(s),await le.mkdirPromise(s,{recursive:!0}),await $v(yot(e,s),{configuration:t,context:e.context,target:s}))}async function Cot(e,t,{project:r,report:s,target:a}){let n=await Pm(r.configuration,t),c=new Set(Object.keys(n));for(let f of r.configuration.plugins.keys())c.has(f)&&await E5(f,e,{project:r,report:s,target:a})}qe();qe();Dt();Yt();var fge=et(pi()),Age=Ie("vm");var EC=class extends At{constructor(){super(...arguments);this.name=he.String();this.checksum=he.Boolean("--checksum",!0,{description:"Whether to care if this plugin is modified"})}static{this.paths=[["plugin","import"]]}static{this.usage=at.Usage({category:"Plugin-related commands",description:"download a plugin",details:` This command downloads the specified plugin from its remote location and updates the configuration to reference it in further CLI invocations. Three types of plugin references are accepted: - If the plugin is stored within the Yarn repository, it can be referenced by name. - Third-party plugins can be referenced directly through their public urls. - Local plugins can be referenced by their path on the disk. If the \`--no-checksum\` option is set, Yarn will no longer care if the plugin is modified. Plugins cannot be downloaded from the npm registry, and aren't allowed to have dependencies (they need to be bundled into a single file, possibly thanks to the \`@yarnpkg/builder\` package). `,examples:[['Download and activate the "@yarnpkg/plugin-exec" plugin',"$0 plugin import @yarnpkg/plugin-exec"],['Download and activate the "@yarnpkg/plugin-exec" plugin (shorthand)',"$0 plugin import exec"],["Download and activate a community plugin","$0 plugin import https://example.org/path/to/plugin.js"],["Activate a local plugin","$0 plugin import ./path/to/plugin.js"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins);return(await Ot.start({configuration:r,stdout:this.context.stdout},async a=>{let{project:n}=await Rt.find(r,this.context.cwd),c,f;if(this.name.match(/^\.{0,2}[\\/]/)||fe.isAbsolute(this.name)){let p=J.resolve(this.context.cwd,fe.toPortablePath(this.name));a.reportInfo(0,`Reading ${pe.pretty(r,p,pe.Type.PATH)}`),c=J.relative(n.cwd,p),f=await le.readFilePromise(p)}else{let p;if(this.name.match(/^https?:/)){try{new URL(this.name)}catch{throw new Lt(52,`Plugin specifier "${this.name}" is neither a plugin name nor a valid url`)}c=this.name,p=this.name}else{let h=j.parseLocator(this.name.replace(/^((@yarnpkg\/)?plugin-)?/,"@yarnpkg/plugin-"));if(h.reference!=="unknown"&&!fge.default.valid(h.reference))throw new Lt(0,"Official plugins only accept strict version references. Use an explicit URL if you wish to download them from another location.");let E=j.stringifyIdent(h),C=await Pm(r,An);if(!Object.hasOwn(C,E)){let S=`Couldn't find a plugin named ${j.prettyIdent(r,h)} on the remote registry. `;throw r.plugins.has(E)?S+=`A plugin named ${j.prettyIdent(r,h)} is already installed; possibly attempting to import a built-in plugin.`:S+=`Note that only the plugins referenced on our website (${pe.pretty(r,"https://github.com/yarnpkg/berry/blob/master/plugins.yml",pe.Type.URL)}) can be referenced by their name; any other plugin will have to be referenced through its public url (for example ${pe.pretty(r,"https://github.com/yarnpkg/berry/raw/master/packages/plugin-typescript/bin/%40yarnpkg/plugin-typescript.js",pe.Type.URL)}).`,new Lt(51,S)}c=E,p=C[E].url,h.reference!=="unknown"?p=p.replace(/\/master\//,`/${E}/${h.reference}/`):An!==null&&(p=p.replace(/\/master\//,`/@yarnpkg/cli/${An}/`))}a.reportInfo(0,`Downloading ${pe.pretty(r,p,"green")}`),f=await nn.get(p,{configuration:r})}await I5(c,f,{checksum:this.checksum,project:n,report:a})})).exitCode()}};async function I5(e,t,{checksum:r=!0,project:s,report:a}){let{configuration:n}=s,c={},f={exports:c};(0,Age.runInNewContext)(t.toString(),{module:f,exports:c});let h=`.yarn/plugins/${f.exports.name}.cjs`,E=J.resolve(s.cwd,h);a.reportInfo(0,`Saving the new plugin in ${pe.pretty(n,h,"magenta")}`),await le.mkdirPromise(J.dirname(E),{recursive:!0}),await le.writeFilePromise(E,t);let C={path:h,spec:e};r&&(C.checksum=Ln.makeHash(t)),await ze.addPlugin(s.cwd,[C])}var wot=({pluginName:e,noMinify:t},r)=>[["yarn",`build:${e}`,...t?["--no-minify"]:[],"|"]],IC=class extends At{constructor(){super(...arguments);this.installPath=he.String("--path",{description:"The path where the repository should be cloned to"});this.repository=he.String("--repository","https://github.com/yarnpkg/berry.git",{description:"The repository that should be cloned"});this.branch=he.String("--branch","master",{description:"The branch of the repository that should be cloned"});this.noMinify=he.Boolean("--no-minify",!1,{description:"Build a plugin for development (debugging) - non-minified and non-mangled"});this.force=he.Boolean("-f,--force",!1,{description:"Always clone the repository instead of trying to fetch the latest commits"});this.name=he.String()}static{this.paths=[["plugin","import","from","sources"]]}static{this.usage=at.Usage({category:"Plugin-related commands",description:"build a plugin from sources",details:` This command clones the Yarn repository into a temporary folder, builds the specified contrib plugin and updates the configuration to reference it in further CLI invocations. The plugins can be referenced by their short name if sourced from the official Yarn repository. `,examples:[['Build and activate the "@yarnpkg/plugin-exec" plugin',"$0 plugin import from sources @yarnpkg/plugin-exec"],['Build and activate the "@yarnpkg/plugin-exec" plugin (shorthand)',"$0 plugin import from sources exec"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=typeof this.installPath<"u"?J.resolve(this.context.cwd,fe.toPortablePath(this.installPath)):J.resolve(fe.toPortablePath((0,pge.tmpdir)()),"yarnpkg-sources",Ln.makeHash(this.repository).slice(0,6));return(await Ot.start({configuration:r,stdout:this.context.stdout},async n=>{let{project:c}=await Rt.find(r,this.context.cwd),f=j.parseIdent(this.name.replace(/^((@yarnpkg\/)?plugin-)?/,"@yarnpkg/plugin-")),p=j.stringifyIdent(f),h=await Pm(r,An);if(!Object.hasOwn(h,p))throw new Lt(51,`Couldn't find a plugin named "${p}" on the remote registry. Note that only the plugins referenced on our website (https://github.com/yarnpkg/berry/blob/master/plugins.yml) can be built and imported from sources.`);let E=p;await y5(this,{configuration:r,report:n,target:s}),await E5(E,this,{project:c,report:n,target:s})})).exitCode()}};async function E5(e,{context:t,noMinify:r},{project:s,report:a,target:n}){let c=e.replace(/@yarnpkg\//,""),{configuration:f}=s;a.reportSeparator(),a.reportInfo(0,`Building a fresh ${c}`),a.reportSeparator(),await $v(wot({pluginName:c,noMinify:r},n),{configuration:f,context:t,target:n}),a.reportSeparator();let p=J.resolve(n,`packages/${c}/bundles/${e}.js`),h=await le.readFilePromise(p);await I5(e,h,{project:s,report:a})}qe();Dt();Yt();var CC=class extends At{constructor(){super(...arguments);this.name=he.String()}static{this.paths=[["plugin","remove"]]}static{this.usage=at.Usage({category:"Plugin-related commands",description:"remove a plugin",details:` This command deletes the specified plugin from the .yarn/plugins folder and removes it from the configuration. **Note:** The plugins have to be referenced by their name property, which can be obtained using the \`yarn plugin runtime\` command. Shorthands are not allowed. `,examples:[["Remove a plugin imported from the Yarn repository","$0 plugin remove @yarnpkg/plugin-typescript"],["Remove a plugin imported from a local file","$0 plugin remove my-local-plugin"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Rt.find(r,this.context.cwd);return(await Ot.start({configuration:r,stdout:this.context.stdout},async n=>{let c=this.name,f=j.parseIdent(c);if(!r.plugins.has(c))throw new it(`${j.prettyIdent(r,f)} isn't referenced by the current configuration`);let p=`.yarn/plugins/${c}.cjs`,h=J.resolve(s.cwd,p);le.existsSync(h)&&(n.reportInfo(0,`Removing ${pe.pretty(r,p,pe.Type.PATH)}...`),await le.removePromise(h)),n.reportInfo(0,"Updating the configuration..."),await ze.updateConfiguration(s.cwd,{plugins:E=>{if(!Array.isArray(E))return E;let C=E.filter(S=>S.path!==p);return C.length===0?ze.deleteProperty:C.length===E.length?E:C}})})).exitCode()}};qe();Yt();var wC=class extends At{constructor(){super(...arguments);this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}static{this.paths=[["plugin","runtime"]]}static{this.usage=at.Usage({category:"Plugin-related commands",description:"list the active plugins",details:` This command prints the currently active plugins. Will be displayed both builtin plugins and external plugins. `,examples:[["List the currently active plugins","$0 plugin runtime"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins);return(await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout},async a=>{for(let n of r.plugins.keys()){let c=this.context.plugins.plugins.has(n),f=n;c&&(f+=" [builtin]"),a.reportJson({name:n,builtin:c}),a.reportInfo(null,`${f}`)}})).exitCode()}};qe();qe();Yt();var BC=class extends At{constructor(){super(...arguments);this.idents=he.Rest()}static{this.paths=[["rebuild"]]}static{this.usage=at.Usage({description:"rebuild the project's native packages",details:` This command will automatically cause Yarn to forget about previous compilations of the given packages and to run them again. Note that while Yarn forgets the compilation, the previous artifacts aren't erased from the filesystem and may affect the next builds (in good or bad). To avoid this, you may remove the .yarn/unplugged folder, or any other relevant location where packages might have been stored (Yarn may offer a way to do that automatically in the future). By default all packages will be rebuilt, but you can filter the list by specifying the names of the packages you want to clear from memory. `,examples:[["Rebuild all packages","$0 rebuild"],["Rebuild fsevents only","$0 rebuild fsevents"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);let c=new Set;for(let f of this.idents)c.add(j.parseIdent(f).identHash);if(await s.restoreInstallState({restoreResolutions:!1}),await s.resolveEverything({cache:n,report:new ki}),c.size>0)for(let f of s.storedPackages.values())c.has(f.identHash)&&(s.storedBuildState.delete(f.locatorHash),s.skippedBuilds.delete(f.locatorHash));else s.storedBuildState.clear(),s.skippedBuilds.clear();return await s.installWithNewReport({stdout:this.context.stdout,quiet:this.context.quiet},{cache:n})}};qe();qe();qe();Yt();var C5=et(zo());Al();var vC=class extends At{constructor(){super(...arguments);this.all=he.Boolean("-A,--all",!1,{description:"Apply the operation to all workspaces from the current project"});this.mode=he.String("--mode",{description:"Change what artifacts installs generate",validator:ks(Oa)});this.patterns=he.Rest()}static{this.paths=[["remove"]]}static{this.usage=at.Usage({description:"remove dependencies from the project",details:` This command will remove the packages matching the specified patterns from the current workspace. If the \`--mode=\` option is set, Yarn will change which artifacts are generated. The modes currently supported are: - \`skip-build\` will not run the build scripts at all. Note that this is different from setting \`enableScripts\` to false because the latter will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run. - \`update-lockfile\` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost. This command accepts glob patterns as arguments (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them. `,examples:[["Remove a dependency from the current project","$0 remove lodash"],["Remove a dependency from all workspaces at once","$0 remove lodash --all"],["Remove all dependencies starting with `eslint-`","$0 remove 'eslint-*'"],["Remove all dependencies with the `@babel` scope","$0 remove '@babel/*'"],["Remove all dependencies matching `react-dom` or `react-helmet`","$0 remove 'react-{dom,helmet}'"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState({restoreResolutions:!1});let c=this.all?s.workspaces:[a],f=["dependencies","devDependencies","peerDependencies"],p=[],h=!1,E=[];for(let I of this.patterns){let T=!1,O=j.parseIdent(I);for(let U of c){let V=[...U.manifest.peerDependenciesMeta.keys()];for(let te of(0,C5.default)(V,I))U.manifest.peerDependenciesMeta.delete(te),h=!0,T=!0;for(let te of f){let ie=U.manifest.getForScope(te),ue=[...ie.values()].map(ae=>j.stringifyIdent(ae));for(let ae of(0,C5.default)(ue,j.stringifyIdent(O))){let{identHash:ge}=j.parseIdent(ae),Ae=ie.get(ge);if(typeof Ae>"u")throw new Error("Assertion failed: Expected the descriptor to be registered");U.manifest[te].delete(ge),E.push([U,te,Ae]),h=!0,T=!0}}}T||p.push(I)}let C=p.length>1?"Patterns":"Pattern",S=p.length>1?"don't":"doesn't",x=this.all?"any":"this";if(p.length>0)throw new it(`${C} ${pe.prettyList(r,p,pe.Type.CODE)} ${S} match any packages referenced by ${x} workspace`);return h?(await r.triggerMultipleHooks(I=>I.afterWorkspaceDependencyRemoval,E),await s.installWithNewReport({stdout:this.context.stdout},{cache:n,mode:this.mode})):0}};qe();qe();Yt();var hge=Ie("util"),SC=class extends At{constructor(){super(...arguments);this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}static{this.paths=[["run"]]}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);return(await Ot.start({configuration:r,stdout:this.context.stdout,json:this.json},async c=>{let f=a.manifest.scripts,p=Ge.sortMap(f.keys(),C=>C),h={breakLength:1/0,colors:r.get("enableColors"),maxArrayLength:2},E=p.reduce((C,S)=>Math.max(C,S.length),0);for(let[C,S]of f.entries())c.reportInfo(null,`${C.padEnd(E," ")} ${(0,hge.inspect)(S,h)}`),c.reportJson({name:C,script:S})})).exitCode()}};qe();qe();Yt();var DC=class extends At{constructor(){super(...arguments);this.inspect=he.String("--inspect",!1,{tolerateBoolean:!0,description:"Forwarded to the underlying Node process when executing a binary"});this.inspectBrk=he.String("--inspect-brk",!1,{tolerateBoolean:!0,description:"Forwarded to the underlying Node process when executing a binary"});this.topLevel=he.Boolean("-T,--top-level",!1,{description:"Check the root workspace for scripts and/or binaries instead of the current one"});this.binariesOnly=he.Boolean("-B,--binaries-only",!1,{description:"Ignore any user defined scripts and only check for binaries"});this.require=he.String("--require",{description:"Forwarded to the underlying Node process when executing a binary"});this.silent=he.Boolean("--silent",{hidden:!0});this.scriptName=he.String();this.args=he.Proxy()}static{this.paths=[["run"]]}static{this.usage=at.Usage({description:"run a script defined in the package.json",details:` This command will run a tool. The exact tool that will be executed will depend on the current state of your workspace: - If the \`scripts\` field from your local package.json contains a matching script name, its definition will get executed. - Otherwise, if one of the local workspace's dependencies exposes a binary with a matching name, this binary will get executed. - Otherwise, if the specified name contains a colon character and if one of the workspaces in the project contains exactly one script with a matching name, then this script will get executed. Whatever happens, the cwd of the spawned process will be the workspace that declares the script (which makes it possible to call commands cross-workspaces using the third syntax). `,examples:[["Run the tests from the local workspace","$0 run test"],['Same thing, but without the "run" keyword',"$0 test"],["Inspect Webpack while running","$0 run --inspect-brk webpack"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a,locator:n}=await Rt.find(r,this.context.cwd);await s.restoreInstallState();let c=this.topLevel?s.topLevelWorkspace.anchoredLocator:n;if(!this.binariesOnly&&await Cn.hasPackageScript(c,this.scriptName,{project:s}))return await Cn.executePackageScript(c,this.scriptName,this.args,{project:s,stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr});let f=await Cn.getPackageAccessibleBinaries(c,{project:s});if(f.get(this.scriptName)){let h=[];return this.inspect&&(typeof this.inspect=="string"?h.push(`--inspect=${this.inspect}`):h.push("--inspect")),this.inspectBrk&&(typeof this.inspectBrk=="string"?h.push(`--inspect-brk=${this.inspectBrk}`):h.push("--inspect-brk")),this.require&&h.push(`--require=${this.require}`),await Cn.executePackageAccessibleBinary(c,this.scriptName,this.args,{cwd:this.context.cwd,project:s,stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr,nodeArgs:h,packageAccessibleBinaries:f})}if(!this.topLevel&&!this.binariesOnly&&a&&this.scriptName.includes(":")){let E=(await Promise.all(s.workspaces.map(async C=>C.manifest.scripts.has(this.scriptName)?C:null))).filter(C=>C!==null);if(E.length===1)return await Cn.executeWorkspaceScript(E[0],this.scriptName,this.args,{stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr})}if(this.topLevel)throw this.scriptName==="node-gyp"?new it(`Couldn't find a script name "${this.scriptName}" in the top-level (used by ${j.prettyLocator(r,n)}). This typically happens because some package depends on "node-gyp" to build itself, but didn't list it in their dependencies. To fix that, please run "yarn add node-gyp" into your top-level workspace. You also can open an issue on the repository of the specified package to suggest them to use an optional peer dependency.`):new it(`Couldn't find a script name "${this.scriptName}" in the top-level (used by ${j.prettyLocator(r,n)}).`);{if(this.scriptName==="global")throw new it("The 'yarn global' commands have been removed in 2.x - consider using 'yarn dlx' or a third-party plugin instead");let h=[this.scriptName].concat(this.args);for(let[E,C]of qI)for(let S of C)if(h.length>=S.length&&JSON.stringify(h.slice(0,S.length))===JSON.stringify(S))throw new it(`Couldn't find a script named "${this.scriptName}", but a matching command can be found in the ${E} plugin. You can install it with "yarn plugin import ${E}".`);throw new it(`Couldn't find a script named "${this.scriptName}".`)}}};qe();qe();Yt();var bC=class extends At{constructor(){super(...arguments);this.descriptor=he.String();this.resolution=he.String()}static{this.paths=[["set","resolution"]]}static{this.usage=at.Usage({description:"enforce a package resolution",details:'\n This command updates the resolution table so that `descriptor` is resolved by `resolution`.\n\n Note that by default this command only affect the current resolution table - meaning that this "manual override" will disappear if you remove the lockfile, or if the package disappear from the table. If you wish to make the enforced resolution persist whatever happens, edit the `resolutions` field in your top-level manifest.\n\n Note that no attempt is made at validating that `resolution` is a valid resolution entry for `descriptor`.\n ',examples:[["Force all instances of lodash@npm:^1.2.3 to resolve to 1.5.0","$0 set resolution lodash@npm:^1.2.3 npm:1.5.0"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(await s.restoreInstallState({restoreResolutions:!1}),!a)throw new ar(s.cwd,this.context.cwd);let c=j.parseDescriptor(this.descriptor,!0),f=j.makeDescriptor(c,this.resolution);return s.storedDescriptors.set(c.descriptorHash,c),s.storedDescriptors.set(f.descriptorHash,f),s.resolutionAliases.set(c.descriptorHash,f.descriptorHash),await s.installWithNewReport({stdout:this.context.stdout},{cache:n})}};qe();Dt();Yt();var dge=et(zo()),PC=class extends At{constructor(){super(...arguments);this.all=he.Boolean("-A,--all",!1,{description:"Unlink all workspaces belonging to the target project from the current one"});this.leadingArguments=he.Rest()}static{this.paths=[["unlink"]]}static{this.usage=at.Usage({description:"disconnect the local project from another one",details:` This command will remove any resolutions in the project-level manifest that would have been added via a yarn link with similar arguments. `,examples:[["Unregister a remote workspace in the current project","$0 unlink ~/ts-loader"],["Unregister all workspaces from a remote project in the current project","$0 unlink ~/jest --all"],["Unregister all previously linked workspaces","$0 unlink --all"],["Unregister all workspaces matching a glob","$0 unlink '@babel/*' 'pkg-{a,b}'"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);let c=s.topLevelWorkspace,f=new Set;if(this.leadingArguments.length===0&&this.all)for(let{pattern:p,reference:h}of c.manifest.resolutions)h.startsWith("portal:")&&f.add(p.descriptor.fullName);if(this.leadingArguments.length>0)for(let p of this.leadingArguments){let h=J.resolve(this.context.cwd,fe.toPortablePath(p));if(Ge.isPathLike(p)){let E=await ze.find(h,this.context.plugins,{useRc:!1,strict:!1}),{project:C,workspace:S}=await Rt.find(E,h);if(!S)throw new ar(C.cwd,h);if(this.all){for(let x of C.workspaces)x.manifest.name&&f.add(j.stringifyIdent(x.anchoredLocator));if(f.size===0)throw new it("No workspace found to be unlinked in the target project")}else{if(!S.manifest.name)throw new it("The target workspace doesn't have a name and thus cannot be unlinked");f.add(j.stringifyIdent(S.anchoredLocator))}}else{let E=[...c.manifest.resolutions.map(({pattern:C})=>C.descriptor.fullName)];for(let C of(0,dge.default)(E,p))f.add(C)}}return c.manifest.resolutions=c.manifest.resolutions.filter(({pattern:p})=>!f.has(p.descriptor.fullName)),await s.installWithNewReport({stdout:this.context.stdout,quiet:this.context.quiet},{cache:n})}};qe();qe();qe();Yt();var gge=et(Vv()),w5=et(zo());Al();var xC=class extends At{constructor(){super(...arguments);this.interactive=he.Boolean("-i,--interactive",{description:"Offer various choices, depending on the detected upgrade paths"});this.fixed=he.Boolean("-F,--fixed",!1,{description:"Store dependency tags as-is instead of resolving them"});this.exact=he.Boolean("-E,--exact",!1,{description:"Don't use any semver modifier on the resolved range"});this.tilde=he.Boolean("-T,--tilde",!1,{description:"Use the `~` semver modifier on the resolved range"});this.caret=he.Boolean("-C,--caret",!1,{description:"Use the `^` semver modifier on the resolved range"});this.recursive=he.Boolean("-R,--recursive",!1,{description:"Resolve again ALL resolutions for those packages"});this.mode=he.String("--mode",{description:"Change what artifacts installs generate",validator:ks(Oa)});this.patterns=he.Rest()}static{this.paths=[["up"]]}static{this.usage=at.Usage({description:"upgrade dependencies across the project",details:"\n This command upgrades the packages matching the list of specified patterns to their latest available version across the whole project (regardless of whether they're part of `dependencies` or `devDependencies` - `peerDependencies` won't be affected). This is a project-wide command: all workspaces will be upgraded in the process.\n\n If `-R,--recursive` is set the command will change behavior and no other switch will be allowed. When operating under this mode `yarn up` will force all ranges matching the selected packages to be resolved again (often to the highest available versions) before being stored in the lockfile. It however won't touch your manifests anymore, so depending on your needs you might want to run both `yarn up` and `yarn up -R` to cover all bases.\n\n If `-i,--interactive` is set (or if the `preferInteractive` settings is toggled on) the command will offer various choices, depending on the detected upgrade paths. Some upgrades require this flag in order to resolve ambiguities.\n\n The, `-C,--caret`, `-E,--exact` and `-T,--tilde` options have the same meaning as in the `add` command (they change the modifier used when the range is missing or a tag, and are ignored when the range is explicitly set).\n\n If the `--mode=` option is set, Yarn will change which artifacts are generated. The modes currently supported are:\n\n - `skip-build` will not run the build scripts at all. Note that this is different from setting `enableScripts` to false because the latter will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.\n\n - `update-lockfile` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.\n\n Generally you can see `yarn up` as a counterpart to what was `yarn upgrade --latest` in Yarn 1 (ie it ignores the ranges previously listed in your manifests), but unlike `yarn upgrade` which only upgraded dependencies in the current workspace, `yarn up` will upgrade all workspaces at the same time.\n\n This command accepts glob patterns as arguments (if valid Descriptors and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them.\n\n **Note:** The ranges have to be static, only the package scopes and names can contain glob patterns.\n ",examples:[["Upgrade all instances of lodash to the latest release","$0 up lodash"],["Upgrade all instances of lodash to the latest release, but ask confirmation for each","$0 up lodash -i"],["Upgrade all instances of lodash to 1.2.3","$0 up lodash@1.2.3"],["Upgrade all instances of packages with the `@babel` scope to the latest release","$0 up '@babel/*'"],["Upgrade all instances of packages containing the word `jest` to the latest release","$0 up '*jest*'"],["Upgrade all instances of packages with the `@babel` scope to 7.0.0","$0 up '@babel/*@7.0.0'"]]})}static{this.schema=[V2("recursive",Vf.Forbids,["interactive","exact","tilde","caret"],{ignore:[void 0,!1]})]}async execute(){return this.recursive?await this.executeUpRecursive():await this.executeUpClassic()}async executeUpRecursive(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState({restoreResolutions:!1});let c=[...s.storedDescriptors.values()],f=c.map(E=>j.stringifyIdent(E)),p=new Set;for(let E of this.patterns){if(j.parseDescriptor(E).range!=="unknown")throw new it("Ranges aren't allowed when using --recursive");for(let C of(0,w5.default)(f,E)){let S=j.parseIdent(C);p.add(S.identHash)}}let h=c.filter(E=>p.has(E.identHash));for(let E of h)s.storedDescriptors.delete(E.descriptorHash),s.storedResolutions.delete(E.descriptorHash);return await s.installWithNewReport({stdout:this.context.stdout},{cache:n,mode:this.mode})}async executeUpClassic(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState({restoreResolutions:!1});let c=this.fixed,f=r.isInteractive({interactive:this.interactive,stdout:this.context.stdout}),p=Kv(this,s),h=f?["keep","reuse","project","latest"]:["project","latest"],E=[],C=[];for(let O of this.patterns){let U=!1,V=j.parseDescriptor(O),te=j.stringifyIdent(V);for(let ie of s.workspaces)for(let ue of["dependencies","devDependencies"]){let ge=[...ie.manifest.getForScope(ue).values()].map(Ce=>j.stringifyIdent(Ce)),Ae=te==="*"?ge:(0,w5.default)(ge,te);for(let Ce of Ae){let Ee=j.parseIdent(Ce),d=ie.manifest[ue].get(Ee.identHash);if(typeof d>"u")throw new Error("Assertion failed: Expected the descriptor to be registered");let Se=j.makeDescriptor(Ee,V.range);E.push(Promise.resolve().then(async()=>[ie,ue,d,await zv(Se,{project:s,workspace:ie,cache:n,target:ue,fixed:c,modifier:p,strategies:h})])),U=!0}}U||C.push(O)}if(C.length>1)throw new it(`Patterns ${pe.prettyList(r,C,pe.Type.CODE)} don't match any packages referenced by any workspace`);if(C.length>0)throw new it(`Pattern ${pe.prettyList(r,C,pe.Type.CODE)} doesn't match any packages referenced by any workspace`);let S=await Promise.all(E),x=await uA.start({configuration:r,stdout:this.context.stdout,suggestInstall:!1},async O=>{for(let[,,U,{suggestions:V,rejections:te}]of S){let ie=V.filter(ue=>ue.descriptor!==null);if(ie.length===0){let[ue]=te;if(typeof ue>"u")throw new Error("Assertion failed: Expected an error to have been set");let ae=this.cli.error(ue);s.configuration.get("enableNetwork")?O.reportError(27,`${j.prettyDescriptor(r,U)} can't be resolved to a satisfying range ${ae}`):O.reportError(27,`${j.prettyDescriptor(r,U)} can't be resolved to a satisfying range (note: network resolution has been disabled) ${ae}`)}else ie.length>1&&!f&&O.reportError(27,`${j.prettyDescriptor(r,U)} has multiple possible upgrade strategies; use -i to disambiguate manually`)}});if(x.hasErrors())return x.exitCode();let I=!1,T=[];for(let[O,U,,{suggestions:V}]of S){let te,ie=V.filter(Ae=>Ae.descriptor!==null),ue=ie[0].descriptor,ae=ie.every(Ae=>j.areDescriptorsEqual(Ae.descriptor,ue));ie.length===1||ae?te=ue:(I=!0,{answer:te}=await(0,gge.prompt)({type:"select",name:"answer",message:`Which range do you want to use in ${j.prettyWorkspace(r,O)} \u276F ${U}?`,choices:V.map(({descriptor:Ae,name:Ce,reason:Ee})=>Ae?{name:Ce,hint:Ee,descriptor:Ae}:{name:Ce,hint:Ee,disabled:!0}),onCancel:()=>process.exit(130),result(Ae){return this.find(Ae,"descriptor")},stdin:this.context.stdin,stdout:this.context.stdout}));let ge=O.manifest[U].get(te.identHash);if(typeof ge>"u")throw new Error("Assertion failed: This descriptor should have a matching entry");if(ge.descriptorHash!==te.descriptorHash)O.manifest[U].set(te.identHash,te),T.push([O,U,ge,te]);else{let Ae=r.makeResolver(),Ce={project:s,resolver:Ae},Ee=r.normalizeDependency(ge),d=Ae.bindDescriptor(Ee,O.anchoredLocator,Ce);s.forgetResolution(d)}}return await r.triggerMultipleHooks(O=>O.afterWorkspaceDependencyReplacement,T),I&&this.context.stdout.write(` `),await s.installWithNewReport({stdout:this.context.stdout},{cache:n,mode:this.mode})}};qe();qe();Yt();var kC=class extends At{constructor(){super(...arguments);this.recursive=he.Boolean("-R,--recursive",!1,{description:"List, for each workspace, what are all the paths that lead to the dependency"});this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.peers=he.Boolean("--peers",!1,{description:"Also print the peer dependencies that match the specified name"});this.package=he.String()}static{this.paths=[["why"]]}static{this.usage=at.Usage({description:"display the reason why a package is needed",details:` This command prints the exact reasons why a package appears in the dependency tree. Specify a version or range to determine why the dependency tree contains a specific version of a package. This is particularly useful when trying to find out why your project depends on lower versions. If \`-R,--recursive\` is set, the listing will go in depth and will list, for each workspaces, what are all the paths that lead to the dependency. Note that the display is somewhat optimized in that it will not print the package listing twice for a single package, so if you see a leaf named "Foo" when looking for "Bar", it means that "Foo" already got printed higher in the tree. `,examples:[["Explain why lodash is used in your project","$0 why lodash"],["Explain why version 3.3.1 of lodash is in your project","$0 why lodash@3.3.1"],["Explain why version 3.X of lodash is in your project","$0 why lodash@^3"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState();let n=j.parseDescriptor(this.package,!1);if(n.range!=="unknown"&&kr.validRange(n.range)===null)throw new it(`Expected a valid semver range, got ${n.range}`);let c=this.recursive?vot(s,n,{configuration:r,peers:this.peers}):Bot(s,n,{configuration:r,peers:this.peers});Rs.emitTree(c,{configuration:r,stdout:this.context.stdout,json:this.json,separators:1})}};function Bot(e,t,{configuration:r,peers:s}){let a=Ge.sortMap(e.storedPackages.values(),f=>j.stringifyLocator(f)),n={},c={children:n};for(let f of a){let p={};for(let E of f.dependencies.values()){if(!s&&f.peerDependencies.has(E.identHash))continue;let C=e.storedResolutions.get(E.descriptorHash);if(!C)throw new Error("Assertion failed: The resolution should have been registered");let S=e.storedPackages.get(C);if(!S)throw new Error("Assertion failed: The package should have been registered");if(!j.areIdentsEqual(S,t)||!j.isPackageInRange(S,t.range))continue;{let I=j.stringifyLocator(f);n[I]={value:[f,pe.Type.LOCATOR],children:p}}let x=j.stringifyLocator(S);p[x]={value:[{descriptor:E,locator:S},pe.Type.DEPENDENT]}}}return c}function vot(e,t,{configuration:r,peers:s}){let a=Ge.sortMap(e.workspaces,S=>j.stringifyLocator(S.anchoredLocator)),n=new Set,c=new Set,f=S=>{if(n.has(S.locatorHash))return c.has(S.locatorHash);if(n.add(S.locatorHash),j.areIdentsEqual(S,t)&&j.isPackageInRange(S,t.range))return c.add(S.locatorHash),!0;let x=!1;for(let I of S.dependencies.values()){if(!s&&S.peerDependencies.has(I.identHash))continue;let T=e.storedResolutions.get(I.descriptorHash);if(!T)throw new Error("Assertion failed: The resolution should have been registered");let O=e.storedPackages.get(T);if(!O)throw new Error("Assertion failed: The package should have been registered");f(O)&&(x=!0)}return x&&c.add(S.locatorHash),x};for(let S of a)f(S.anchoredPackage);let p=new Set,h={},E={children:h},C=(S,x,I)=>{if(!c.has(S.locatorHash))return;let T=I!==null?pe.tuple(pe.Type.DEPENDENT,{locator:S,descriptor:I}):pe.tuple(pe.Type.LOCATOR,S),O={},U={value:T,children:O},V=j.stringifyLocator(S);if(x[V]=U,!(I!==null&&e.tryWorkspaceByLocator(S))&&!p.has(S.locatorHash)){p.add(S.locatorHash);for(let te of S.dependencies.values()){if(!s&&S.peerDependencies.has(te.identHash))continue;let ie=e.storedResolutions.get(te.descriptorHash);if(!ie)throw new Error("Assertion failed: The resolution should have been registered");let ue=e.storedPackages.get(ie);if(!ue)throw new Error("Assertion failed: The package should have been registered");C(ue,O,te)}}};for(let S of a)C(S.anchoredPackage,h,null);return E}qe();var R5={};Vt(R5,{GitFetcher:()=>tS,GitResolver:()=>rS,default:()=>Wot,gitUtils:()=>La});qe();Dt();var La={};Vt(La,{TreeishProtocols:()=>eS,clone:()=>Q5,fetchBase:()=>Lge,fetchChangedFiles:()=>Mge,fetchChangedWorkspaces:()=>Got,fetchRoot:()=>Oge,isGitUrl:()=>TC,lsRemote:()=>Nge,normalizeLocator:()=>jot,normalizeRepoUrl:()=>QC,resolveUrl:()=>k5,splitRepoUrl:()=>zp,validateRepoUrl:()=>x5});qe();Dt();Yt();zl();var Tge=et(kge()),RC=et(Ie("querystring")),b5=et(pi());function D5(e,t,r){let s=e.indexOf(r);return e.lastIndexOf(t,s>-1?s:1/0)}function Qge(e){try{return new URL(e)}catch{return}}function _ot(e){let t=D5(e,"@","#"),r=D5(e,":","#");return r>t&&(e=`${e.slice(0,r)}/${e.slice(r+1)}`),D5(e,":","#")===-1&&e.indexOf("//")===-1&&(e=`ssh://${e}`),e}function Rge(e){return Qge(e)||Qge(_ot(e))}function QC(e,{git:t=!1}={}){if(e=e.replace(/^git\+https:/,"https:"),e=e.replace(/^(?:github:|https:\/\/github\.com\/|git:\/\/github\.com\/)?(?!\.{1,2}\/)([a-zA-Z0-9._-]+)\/(?!\.{1,2}(?:#|$))([a-zA-Z0-9._-]+?)(?:\.git)?(#.*)?$/,"https://github.com/$1/$2.git$3"),e=e.replace(/^https:\/\/github\.com\/(?!\.{1,2}\/)([a-zA-Z0-9._-]+)\/(?!\.{1,2}(?:#|$))([a-zA-Z0-9._-]+?)\/tarball\/(.+)?$/,"https://github.com/$1/$2.git#$3"),t){let r=Rge(e);r&&(e=r.href),e=e.replace(/^git\+([^:]+):/,"$1:")}return e}function Fge(){return{...process.env,GIT_SSH_COMMAND:process.env.GIT_SSH_COMMAND||`${process.env.GIT_SSH||"ssh"} -o BatchMode=yes`}}var Hot=[/^ssh:/,/^git(?:\+[^:]+)?:/,/^(?:git\+)?https?:[^#]+\/[^#]+(?:\.git)(?:#.*)?$/,/^git@[^#]+\/[^#]+\.git(?:#.*)?$/,/^(?:github:|https:\/\/github\.com\/)?(?!\.{1,2}\/)([a-zA-Z._0-9-]+)\/(?!\.{1,2}(?:#|$))([a-zA-Z._0-9-]+?)(?:\.git)?(?:#.*)?$/,/^https:\/\/github\.com\/(?!\.{1,2}\/)([a-zA-Z0-9._-]+)\/(?!\.{1,2}(?:#|$))([a-zA-Z0-9._-]+?)\/tarball\/(.+)?$/],eS=(a=>(a.Commit="commit",a.Head="head",a.Tag="tag",a.Semver="semver",a))(eS||{});function TC(e){return e?Hot.some(t=>!!e.match(t)):!1}function zp(e){e=QC(e);let t=e.indexOf("#");if(t===-1)return{repo:e,treeish:{protocol:"head",request:"HEAD"},extra:{}};let r=e.slice(0,t),s=e.slice(t+1);if(s.match(/^[a-z]+=/)){let a=RC.default.parse(s);for(let[p,h]of Object.entries(a))if(typeof h!="string")throw new Error(`Assertion failed: The ${p} parameter must be a literal string`);let n=Object.values(eS).find(p=>Object.hasOwn(a,p)),[c,f]=typeof n<"u"?[n,a[n]]:["head","HEAD"];for(let p of Object.values(eS))delete a[p];return{repo:r,treeish:{protocol:c,request:f},extra:a}}else{let a=s.indexOf(":"),[n,c]=a===-1?[null,s]:[s.slice(0,a),s.slice(a+1)];return{repo:r,treeish:{protocol:n,request:c},extra:{}}}}function jot(e){return j.makeLocator(e,QC(e.reference))}function x5(e,{configuration:t}){let{repo:r}=zp(e),s=QC(r,{git:!0});if(!nn.getNetworkSettings(`https://${(0,Tge.default)(s).resource}`,{configuration:t}).enableNetwork)throw new Lt(80,`Request to '${s}' has been blocked because of your configuration settings`);let n=Ge.buildIgnorePattern(t.get("approvedGitRepositories"));if(n===null||!s.match(n))throw new Lt(80,`Request to '${s}' has been blocked because it doesn't match any of the patterns in 'approvedGitRepositories'`);return s}async function Nge(e,t){let r=x5(e,{configuration:t}),s=await P5("listing refs",["ls-remote",r],{cwd:t.startingCwd,env:Fge()},{configuration:t,normalizedRepoUrl:r}),a=new Map,n=/^([a-f0-9]{40})\t([^\n]+)/gm,c;for(;(c=n.exec(s.stdout))!==null;)a.set(c[2],c[1]);return a}async function k5(e,t){let{repo:r,treeish:{protocol:s,request:a},extra:n}=zp(e),c=await Nge(r,t),f=(h,E)=>{switch(h){case"commit":{if(!E.match(/^[a-f0-9]{40}$/))throw new Error("Invalid commit hash");return RC.default.stringify({...n,commit:E})}case"head":{let C=c.get(E==="HEAD"?E:`refs/heads/${E}`);if(typeof C>"u")throw new Error(`Unknown head ("${E}")`);return RC.default.stringify({...n,commit:C})}case"tag":{let C=c.get(`refs/tags/${E}`);if(typeof C>"u")throw new Error(`Unknown tag ("${E}")`);return RC.default.stringify({...n,commit:C})}case"semver":{let C=kr.validRange(E);if(!C)throw new Error(`Invalid range ("${E}")`);let S=new Map([...c.entries()].filter(([I])=>I.startsWith("refs/tags/")).map(([I,T])=>[b5.default.parse(I.slice(10)),T]).filter(I=>I[0]!==null)),x=b5.default.maxSatisfying([...S.keys()],C);if(x===null)throw new Error(`No matching range ("${E}")`);return RC.default.stringify({...n,commit:S.get(x)})}case null:{let C;if((C=p("commit",E))!==null||(C=p("tag",E))!==null||(C=p("head",E))!==null)return C;throw E.match(/^[a-f0-9]+$/)?new Error(`Couldn't resolve "${E}" as either a commit, a tag, or a head - if a commit, use the 40-characters commit hash`):new Error(`Couldn't resolve "${E}" as either a commit, a tag, or a head`)}default:throw new Error(`Invalid Git resolution protocol ("${h}")`)}},p=(h,E)=>{try{return f(h,E)}catch{return null}};return QC(`${r}#${f(s,a)}`)}async function Q5(e,t){return await t.getLimit("cloneConcurrency")(async()=>{let{repo:r,treeish:{protocol:s,request:a}}=zp(e);if(s!=="commit")throw new Error("Invalid treeish protocol when cloning");let n=x5(r,{configuration:t}),c=await le.mktempPromise(),f={cwd:c,env:Fge()};return await P5("cloning the repository",["clone","-c","core.autocrlf=false",n,fe.fromPortablePath(c)],f,{configuration:t,normalizedRepoUrl:n}),await P5("switching branch",["checkout",`${a}`],f,{configuration:t,normalizedRepoUrl:n}),c})}async function Oge(e){let t,r=e;do{if(t=r,await le.existsPromise(J.join(t,".git")))return t;r=J.dirname(t)}while(r!==t);return null}async function Lge(e,{baseRefs:t}){if(t.length===0)throw new it("Can't run this command with zero base refs specified.");let r=[];for(let f of t){let{code:p}=await qr.execvp("git",["merge-base",f,"HEAD"],{cwd:e});p===0&&r.push(f)}if(r.length===0)throw new it(`No ancestor could be found between any of HEAD and ${t.join(", ")}`);let{stdout:s}=await qr.execvp("git",["merge-base","HEAD",...r],{cwd:e,strict:!0}),a=s.trim(),{stdout:n}=await qr.execvp("git",["show","--quiet","--pretty=format:%s",a],{cwd:e,strict:!0}),c=n.trim();return{hash:a,title:c}}async function Mge(e,{base:t,project:r}){let s=Ge.buildIgnorePattern(r.configuration.get("changesetIgnorePatterns")),{stdout:a}=await qr.execvp("git",["diff","--name-only",`${t}`],{cwd:e,strict:!0}),n=a.split(/\r\n|\r|\n/).filter(h=>h.length>0).map(h=>J.resolve(e,fe.toPortablePath(h))),{stdout:c}=await qr.execvp("git",["ls-files","--others","--exclude-standard"],{cwd:e,strict:!0}),f=c.split(/\r\n|\r|\n/).filter(h=>h.length>0).map(h=>J.resolve(e,fe.toPortablePath(h))),p=[...new Set([...n,...f].sort())];return s?p.filter(h=>!J.relative(r.cwd,h).match(s)):p}async function Got({ref:e,project:t}){if(t.configuration.projectCwd===null)throw new it("This command can only be run from within a Yarn project");let r=[J.resolve(t.cwd,Er.lockfile),J.resolve(t.cwd,t.configuration.get("cacheFolder")),J.resolve(t.cwd,t.configuration.get("installStatePath")),J.resolve(t.cwd,t.configuration.get("virtualFolder"))];await t.configuration.triggerHook(c=>c.populateYarnPaths,t,c=>{c!=null&&r.push(c)});let s=await Oge(t.configuration.projectCwd);if(s==null)throw new it("This command can only be run on Git repositories");let a=await Lge(s,{baseRefs:typeof e=="string"?[e]:t.configuration.get("changesetBaseRefs")}),n=await Mge(s,{base:a.hash,project:t});return new Set(Ge.mapAndFilter(n,c=>{let f=t.tryWorkspaceByFilePath(c);return f===null?Ge.mapAndFilter.skip:r.some(p=>c.startsWith(p))?Ge.mapAndFilter.skip:f}))}async function P5(e,t,r,{configuration:s,normalizedRepoUrl:a}){try{return await qr.execvp("git",t,{...r,strict:!0})}catch(n){if(!(n instanceof qr.ExecError))throw n;let c=n.reportExtra,f=n.stderr.toString();throw new Lt(1,`Failed ${e}`,p=>{p.reportError(1,` ${pe.prettyField(s,{label:"Repository URL",value:pe.tuple(pe.Type.URL,a)})}`);for(let h of f.matchAll(/^(.+?): (.*)$/gm)){let[,E,C]=h;E=E.toLowerCase();let S=E==="error"?"Error":`${EB(E)} Error`;p.reportError(1,` ${pe.prettyField(s,{label:S,value:pe.tuple(pe.Type.NO_HINT,C)})}`)}c?.(p)})}}var tS=class{supports(t,r){return TC(t.reference)}getLocalPath(t,r){return null}async fetch(t,r){let s=r.checksums.get(t.locatorHash)||null,a=new Map(r.checksums);a.set(t.locatorHash,s);let n={...r,checksums:a},c=await this.downloadHosted(t,n);if(c!==null)return c;let[f,p,h]=await r.cache.fetchPackageFromCache(t,s,{onHit:()=>r.report.reportCacheHit(t),onMiss:()=>r.report.reportCacheMiss(t,`${j.prettyLocator(r.project.configuration,t)} can't be found in the cache and will be fetched from the remote repository`),loader:()=>this.cloneFromRemote(t,n),...r.cacheOptions});return{packageFs:f,releaseFs:p,prefixPath:j.getIdentVendorPath(t),checksum:h}}async downloadHosted(t,r){return r.project.configuration.reduceHook(s=>s.fetchHostedRepository,null,t,r)}async cloneFromRemote(t,r){let s=zp(t.reference),a=await Q5(t.reference,r.project.configuration),n=J.resolve(a,s.extra.cwd??vt.dot),c=J.join(n,"package.tgz");await Cn.prepareExternalProject(n,c,{configuration:r.project.configuration,report:r.report,workspace:s.extra.workspace,locator:t});let f=await le.readFilePromise(c);return await Ge.releaseAfterUseAsync(async()=>await gs.convertToZip(f,{configuration:r.project.configuration,prefixPath:j.getIdentVendorPath(t),stripComponents:1}))}};qe();qe();var rS=class{supportsDescriptor(t,r){return TC(t.range)}supportsLocator(t,r){return TC(t.reference)}shouldPersistResolution(t,r){return!0}bindDescriptor(t,r,s){return t}getResolutionDependencies(t,r){return{}}async getCandidates(t,r,s){let a=await k5(t.range,s.project.configuration);return[j.makeLocator(t,a)]}async getSatisfying(t,r,s,a){let n=zp(t.range);return{locators:s.filter(f=>{if(f.identHash!==t.identHash)return!1;let p=zp(f.reference);return!(n.repo!==p.repo||n.treeish.protocol==="commit"&&n.treeish.request!==p.treeish.request)}),sorted:!1}}async resolve(t,r){if(!r.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let s=await r.fetchOptions.fetcher.fetch(t,r.fetchOptions),a=await Ge.releaseAfterUseAsync(async()=>await _t.find(s.prefixPath,{baseFs:s.packageFs}),s.releaseFs);return{...t,version:a.version||"0.0.0",languageName:a.languageName||r.project.configuration.get("defaultLanguageName"),linkType:"HARD",conditions:a.getConditions(),dependencies:r.project.configuration.normalizeDependencyMap(a.dependencies),peerDependencies:a.peerDependencies,dependenciesMeta:a.dependenciesMeta,peerDependenciesMeta:a.peerDependenciesMeta,bin:a.bin}}};var qot={configuration:{approvedGitRepositories:{description:"Array of git repository URL glob patterns that are allowed to be fetched",type:"STRING",default:[],isArray:!0},changesetBaseRefs:{description:"The base git refs that the current HEAD is compared against when detecting changes. Supports git branches, tags, and commits.",type:"STRING",isArray:!0,isNullable:!1,default:["master","origin/master","upstream/master","main","origin/main","upstream/main"]},changesetIgnorePatterns:{description:"Array of glob patterns; files matching them will be ignored when fetching the changed files",type:"STRING",default:[],isArray:!0},cloneConcurrency:{description:"Maximal number of concurrent clones",type:"NUMBER",default:2}},fetchers:[tS],resolvers:[rS]};var Wot=qot;Yt();var FC=class extends At{constructor(){super(...arguments);this.since=he.String("--since",{description:"Only include workspaces that have been changed since the specified ref.",tolerateBoolean:!0});this.recursive=he.Boolean("-R,--recursive",!1,{description:"Find packages via dependencies/devDependencies instead of using the workspaces field"});this.noPrivate=he.Boolean("--no-private",{description:"Exclude workspaces that have the private field set to true"});this.verbose=he.Boolean("-v,--verbose",!1,{description:"Also return the cross-dependencies between workspaces"});this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}static{this.paths=[["workspaces","list"]]}static{this.usage=at.Usage({category:"Workspace-related commands",description:"list all available workspaces",details:"\n This command will print the list of all workspaces in the project.\n\n - If `--since` is set, Yarn will only list workspaces that have been modified since the specified ref. By default Yarn will use the refs specified by the `changesetBaseRefs` configuration option.\n\n - If `-R,--recursive` is set, Yarn will find workspaces to run the command on by recursively evaluating `dependencies` and `devDependencies` fields, instead of looking at the `workspaces` fields.\n\n - If `--no-private` is set, Yarn will not list any workspaces that have the `private` field set to `true`.\n\n - If both the `-v,--verbose` and `--json` options are set, Yarn will also return the cross-dependencies between each workspaces (useful when you wish to automatically generate Buck / Bazel rules).\n "})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Rt.find(r,this.context.cwd);return(await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout},async n=>{let c=this.since?await La.fetchChangedWorkspaces({ref:this.since,project:s}):s.workspaces,f=new Set(c);if(this.recursive)for(let p of[...c].map(h=>h.getRecursiveWorkspaceDependents()))for(let h of p)f.add(h);for(let p of f){let{manifest:h}=p;if(h.private&&this.noPrivate)continue;let E;if(this.verbose){let C=new Set,S=new Set;for(let x of _t.hardDependencies)for(let[I,T]of h.getForScope(x)){let O=s.tryWorkspaceByDescriptor(T);O===null?s.workspacesByIdent.has(I)&&S.add(T):C.add(O)}E={workspaceDependencies:Array.from(C).map(x=>x.relativeCwd),mismatchedWorkspaceDependencies:Array.from(S).map(x=>j.stringifyDescriptor(x))}}n.reportInfo(null,`${p.relativeCwd}`),n.reportJson({location:p.relativeCwd,name:h.name?j.stringifyIdent(h.name):null,...E})}})).exitCode()}};qe();qe();Yt();var NC=class extends At{constructor(){super(...arguments);this.workspaceName=he.String();this.commandName=he.String();this.args=he.Proxy()}static{this.paths=[["workspace"]]}static{this.usage=at.Usage({category:"Workspace-related commands",description:"run a command within the specified workspace",details:` This command will run a given sub-command on a single workspace. `,examples:[["Add a package to a single workspace","yarn workspace components add -D react"],["Run build script on a single workspace","yarn workspace components run build"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);let n=s.workspaces,c=new Map(n.map(p=>[j.stringifyIdent(p.anchoredLocator),p])),f=c.get(this.workspaceName);if(f===void 0){let p=Array.from(c.keys()).sort();throw new it(`Workspace '${this.workspaceName}' not found. Did you mean any of the following: - ${p.join(` - `)}?`)}return this.cli.run([this.commandName,...this.args],{cwd:f.cwd})}};var Yot={configuration:{enableImmutableInstalls:{description:"If true (the default on CI), prevents the install command from modifying the lockfile",type:"BOOLEAN",default:Uge.isCI},defaultSemverRangePrefix:{description:"The default save prefix: '^', '~' or ''",type:"STRING",values:["^","~",""],default:"^"},preferReuse:{description:"If true, `yarn add` will attempt to reuse the most common dependency range in other workspaces.",type:"BOOLEAN",default:!1}},commands:[ZI,$I,eC,tC,bC,yC,uC,FC,iC,sC,oC,aC,zI,XI,rC,nC,lC,cC,fC,AC,pC,hC,PC,dC,gC,IC,EC,CC,mC,wC,BC,vC,SC,DC,xC,kC,NC]},Vot=Yot;var L5={};Vt(L5,{default:()=>Xot});qe();qe();var F5="catalog:";var N5=e=>e.startsWith(F5),Jot=e=>e.range.slice(F5.length)||null,_ge=e=>e===null?"default catalog":`catalog "${e}"`,Kot=e=>e.scope?`@${e.scope}/${e.name}`:e.name,O5=(e,t,r,s)=>{let a=Jot(t),n;if(a===null)n=e.configuration.get("catalog");else try{let E=e.configuration.get("catalogs");E&&(n=E.get(a))}catch{n=void 0}if(!n||n.size===0)throw new Lt(82,`${j.prettyDescriptor(e.configuration,t)}: ${_ge(a)} not found or empty`);let c=Kot(t),f=n.get(c);if(!f)throw new Lt(82,`${j.prettyDescriptor(e.configuration,t)}: entry not found in ${_ge(a)}`);let p=e.configuration.normalizeDependency(j.makeDescriptor(t,f));return r.supportsDescriptor(p,s)?r.bindDescriptor(p,e.topLevelWorkspace.anchoredLocator,s):p};var zot={configuration:{catalog:{description:"The default catalog of packages",type:"MAP",valueDefinition:{description:"The catalog of packages",type:"STRING"}},catalogs:{description:"Named catalogs of packages",type:"MAP",valueDefinition:{description:"A named catalog",type:"MAP",valueDefinition:{description:"Package version in the catalog",type:"STRING"}}}},hooks:{beforeWorkspacePacking:(e,t)=>{let r=e.project,s=r.configuration.makeResolver(),a={project:r,resolver:s,report:new ki};for(let n of _t.allDependencies){let c=t[n];if(c)for(let[f,p]of Object.entries(c)){if(typeof p!="string"||!N5(p))continue;let h=j.parseIdent(f),E=j.makeDescriptor(h,p),C=O5(r,E,s,a),{protocol:S,source:x,params:I,selector:T}=j.parseRange(j.convertToManifestRange(C.range));S===e.project.configuration.get("defaultProtocol")&&(S=null),c[f]=j.makeRange({protocol:S,source:x,params:I,selector:T})}}},reduceDependency:async(e,t,r,s,{resolver:a,resolveOptions:n})=>N5(e.range)?O5(t,e,a,n):e}},Xot=zot;var j5={};Vt(j5,{default:()=>$ot});qe();var Qt={optional:!0},M5=[["@tailwindcss/aspect-ratio@<0.2.1",{peerDependencies:{tailwindcss:"^2.0.2"}}],["@tailwindcss/line-clamp@<0.2.1",{peerDependencies:{tailwindcss:"^2.0.2"}}],["@fullhuman/postcss-purgecss@3.1.3 || 3.1.3-alpha.0",{peerDependencies:{postcss:"^8.0.0"}}],["@samverschueren/stream-to-observable@<0.3.1",{peerDependenciesMeta:{rxjs:Qt,zenObservable:Qt}}],["any-observable@<0.5.1",{peerDependenciesMeta:{rxjs:Qt,zenObservable:Qt}}],["@pm2/agent@<1.0.4",{dependencies:{debug:"*"}}],["debug@<4.2.0",{peerDependenciesMeta:{"supports-color":Qt}}],["got@<11",{dependencies:{"@types/responselike":"^1.0.0","@types/keyv":"^3.1.1"}}],["cacheable-lookup@<4.1.2",{dependencies:{"@types/keyv":"^3.1.1"}}],["http-link-dataloader@*",{peerDependencies:{graphql:"^0.13.1 || ^14.0.0"}}],["typescript-language-server@*",{dependencies:{"vscode-jsonrpc":"^5.0.1","vscode-languageserver-protocol":"^3.15.0"}}],["postcss-syntax@*",{peerDependenciesMeta:{"postcss-html":Qt,"postcss-jsx":Qt,"postcss-less":Qt,"postcss-markdown":Qt,"postcss-scss":Qt}}],["jss-plugin-rule-value-function@<=10.1.1",{dependencies:{"tiny-warning":"^1.0.2"}}],["ink-select-input@<4.1.0",{peerDependencies:{react:"^16.8.2"}}],["license-webpack-plugin@<2.3.18",{peerDependenciesMeta:{webpack:Qt}}],["snowpack@>=3.3.0",{dependencies:{"node-gyp":"^7.1.0"}}],["promise-inflight@*",{peerDependenciesMeta:{bluebird:Qt}}],["reactcss@*",{peerDependencies:{react:"*"}}],["react-color@<=2.19.0",{peerDependencies:{react:"*"}}],["gatsby-plugin-i18n@*",{dependencies:{ramda:"^0.24.1"}}],["useragent@^2.0.0",{dependencies:{request:"^2.88.0",yamlparser:"0.0.x",semver:"5.5.x"}}],["@apollographql/apollo-tools@<=0.5.2",{peerDependencies:{graphql:"^14.2.1 || ^15.0.0"}}],["material-table@^2.0.0",{dependencies:{"@babel/runtime":"^7.11.2"}}],["@babel/parser@*",{dependencies:{"@babel/types":"^7.8.3"}}],["fork-ts-checker-webpack-plugin@<=6.3.4",{peerDependencies:{eslint:">= 6",typescript:">= 2.7",webpack:">= 4","vue-template-compiler":"*"},peerDependenciesMeta:{eslint:Qt,"vue-template-compiler":Qt}}],["rc-animate@<=3.1.1",{peerDependencies:{react:">=16.9.0","react-dom":">=16.9.0"}}],["react-bootstrap-table2-paginator@*",{dependencies:{classnames:"^2.2.6"}}],["react-draggable@<=4.4.3",{peerDependencies:{react:">= 16.3.0","react-dom":">= 16.3.0"}}],["apollo-upload-client@<14",{peerDependencies:{graphql:"14 - 15"}}],["react-instantsearch-core@<=6.7.0",{peerDependencies:{algoliasearch:">= 3.1 < 5"}}],["react-instantsearch-dom@<=6.7.0",{dependencies:{"react-fast-compare":"^3.0.0"}}],["ws@<7.2.1",{peerDependencies:{bufferutil:"^4.0.1","utf-8-validate":"^5.0.2"},peerDependenciesMeta:{bufferutil:Qt,"utf-8-validate":Qt}}],["react-portal@<4.2.2",{peerDependencies:{"react-dom":"^15.0.0-0 || ^16.0.0-0 || ^17.0.0-0"}}],["react-scripts@<=4.0.1",{peerDependencies:{react:"*"}}],["testcafe@<=1.10.1",{dependencies:{"@babel/plugin-transform-for-of":"^7.12.1","@babel/runtime":"^7.12.5"}}],["testcafe-legacy-api@<=4.2.0",{dependencies:{"testcafe-hammerhead":"^17.0.1","read-file-relative":"^1.2.0"}}],["@google-cloud/firestore@<=4.9.3",{dependencies:{protobufjs:"^6.8.6"}}],["gatsby-source-apiserver@*",{dependencies:{"babel-polyfill":"^6.26.0"}}],["@webpack-cli/package-utils@<=1.0.1-alpha.4",{dependencies:{"cross-spawn":"^7.0.3"}}],["gatsby-remark-prismjs@<3.3.28",{dependencies:{lodash:"^4"}}],["gatsby-plugin-favicon@*",{peerDependencies:{webpack:"*"}}],["gatsby-plugin-sharp@<=4.6.0-next.3",{dependencies:{debug:"^4.3.1"}}],["gatsby-react-router-scroll@<=5.6.0-next.0",{dependencies:{"prop-types":"^15.7.2"}}],["@rebass/forms@*",{dependencies:{"@styled-system/should-forward-prop":"^5.0.0"},peerDependencies:{react:"^16.8.6"}}],["rebass@*",{peerDependencies:{react:"^16.8.6"}}],["@ant-design/react-slick@<=0.28.3",{peerDependencies:{react:">=16.0.0"}}],["mqtt@<4.2.7",{dependencies:{duplexify:"^4.1.1"}}],["vue-cli-plugin-vuetify@<=2.0.3",{dependencies:{semver:"^6.3.0"},peerDependenciesMeta:{"sass-loader":Qt,"vuetify-loader":Qt}}],["vue-cli-plugin-vuetify@<=2.0.4",{dependencies:{"null-loader":"^3.0.0"}}],["vue-cli-plugin-vuetify@>=2.4.3",{peerDependencies:{vue:"*"}}],["@vuetify/cli-plugin-utils@<=0.0.4",{dependencies:{semver:"^6.3.0"},peerDependenciesMeta:{"sass-loader":Qt}}],["@vue/cli-plugin-typescript@<=5.0.0-alpha.0",{dependencies:{"babel-loader":"^8.1.0"}}],["@vue/cli-plugin-typescript@<=5.0.0-beta.0",{dependencies:{"@babel/core":"^7.12.16"},peerDependencies:{"vue-template-compiler":"^2.0.0"},peerDependenciesMeta:{"vue-template-compiler":Qt}}],["cordova-ios@<=6.3.0",{dependencies:{underscore:"^1.9.2"}}],["cordova-lib@<=10.0.1",{dependencies:{underscore:"^1.9.2"}}],["git-node-fs@*",{peerDependencies:{"js-git":"^0.7.8"},peerDependenciesMeta:{"js-git":Qt}}],["consolidate@<0.16.0",{peerDependencies:{mustache:"^3.0.0"},peerDependenciesMeta:{mustache:Qt}}],["consolidate@<=0.16.0",{peerDependencies:{velocityjs:"^2.0.1",tinyliquid:"^0.2.34","liquid-node":"^3.0.1",jade:"^1.11.0","then-jade":"*",dust:"^0.3.0","dustjs-helpers":"^1.7.4","dustjs-linkedin":"^2.7.5",swig:"^1.4.2","swig-templates":"^2.0.3","razor-tmpl":"^1.3.1",atpl:">=0.7.6",liquor:"^0.0.5",twig:"^1.15.2",ejs:"^3.1.5",eco:"^1.1.0-rc-3",jazz:"^0.0.18",jqtpl:"~1.1.0",hamljs:"^0.6.2",hamlet:"^0.3.3",whiskers:"^0.4.0","haml-coffee":"^1.14.1","hogan.js":"^3.0.2",templayed:">=0.2.3",handlebars:"^4.7.6",underscore:"^1.11.0",lodash:"^4.17.20",pug:"^3.0.0","then-pug":"*",qejs:"^3.0.5",walrus:"^0.10.1",mustache:"^4.0.1",just:"^0.1.8",ect:"^0.5.9",mote:"^0.2.0",toffee:"^0.3.6",dot:"^1.1.3","bracket-template":"^1.1.5",ractive:"^1.3.12",nunjucks:"^3.2.2",htmling:"^0.0.8","babel-core":"^6.26.3",plates:"~0.4.11","react-dom":"^16.13.1",react:"^16.13.1","arc-templates":"^0.5.3",vash:"^0.13.0",slm:"^2.0.0",marko:"^3.14.4",teacup:"^2.0.0","coffee-script":"^1.12.7",squirrelly:"^5.1.0",twing:"^5.0.2"},peerDependenciesMeta:{velocityjs:Qt,tinyliquid:Qt,"liquid-node":Qt,jade:Qt,"then-jade":Qt,dust:Qt,"dustjs-helpers":Qt,"dustjs-linkedin":Qt,swig:Qt,"swig-templates":Qt,"razor-tmpl":Qt,atpl:Qt,liquor:Qt,twig:Qt,ejs:Qt,eco:Qt,jazz:Qt,jqtpl:Qt,hamljs:Qt,hamlet:Qt,whiskers:Qt,"haml-coffee":Qt,"hogan.js":Qt,templayed:Qt,handlebars:Qt,underscore:Qt,lodash:Qt,pug:Qt,"then-pug":Qt,qejs:Qt,walrus:Qt,mustache:Qt,just:Qt,ect:Qt,mote:Qt,toffee:Qt,dot:Qt,"bracket-template":Qt,ractive:Qt,nunjucks:Qt,htmling:Qt,"babel-core":Qt,plates:Qt,"react-dom":Qt,react:Qt,"arc-templates":Qt,vash:Qt,slm:Qt,marko:Qt,teacup:Qt,"coffee-script":Qt,squirrelly:Qt,twing:Qt}}],["vue-loader@<=16.3.3",{peerDependencies:{"@vue/compiler-sfc":"^3.0.8",webpack:"^4.1.0 || ^5.0.0-0"},peerDependenciesMeta:{"@vue/compiler-sfc":Qt}}],["vue-loader@^16.7.0",{peerDependencies:{"@vue/compiler-sfc":"^3.0.8",vue:"^3.2.13"},peerDependenciesMeta:{"@vue/compiler-sfc":Qt,vue:Qt}}],["scss-parser@<=1.0.5",{dependencies:{lodash:"^4.17.21"}}],["query-ast@<1.0.5",{dependencies:{lodash:"^4.17.21"}}],["redux-thunk@<=2.3.0",{peerDependencies:{redux:"^4.0.0"}}],["skypack@<=0.3.2",{dependencies:{tar:"^6.1.0"}}],["@npmcli/metavuln-calculator@<2.0.0",{dependencies:{"json-parse-even-better-errors":"^2.3.1"}}],["bin-links@<2.3.0",{dependencies:{"mkdirp-infer-owner":"^1.0.2"}}],["rollup-plugin-polyfill-node@<=0.8.0",{peerDependencies:{rollup:"^1.20.0 || ^2.0.0"}}],["snowpack@<3.8.6",{dependencies:{"magic-string":"^0.25.7"}}],["elm-webpack-loader@*",{dependencies:{temp:"^0.9.4"}}],["winston-transport@<=4.4.0",{dependencies:{logform:"^2.2.0"}}],["jest-vue-preprocessor@*",{dependencies:{"@babel/core":"7.8.7","@babel/template":"7.8.6"},peerDependencies:{pug:"^2.0.4"},peerDependenciesMeta:{pug:Qt}}],["redux-persist@*",{peerDependencies:{react:">=16"},peerDependenciesMeta:{react:Qt}}],["sodium@>=3",{dependencies:{"node-gyp":"^3.8.0"}}],["babel-plugin-graphql-tag@<=3.1.0",{peerDependencies:{graphql:"^14.0.0 || ^15.0.0"}}],["@playwright/test@<=1.14.1",{dependencies:{"jest-matcher-utils":"^26.4.2"}}],...["babel-plugin-remove-graphql-queries@<3.14.0-next.1","babel-preset-gatsby-package@<1.14.0-next.1","create-gatsby@<1.14.0-next.1","gatsby-admin@<0.24.0-next.1","gatsby-cli@<3.14.0-next.1","gatsby-core-utils@<2.14.0-next.1","gatsby-design-tokens@<3.14.0-next.1","gatsby-legacy-polyfills@<1.14.0-next.1","gatsby-plugin-benchmark-reporting@<1.14.0-next.1","gatsby-plugin-graphql-config@<0.23.0-next.1","gatsby-plugin-image@<1.14.0-next.1","gatsby-plugin-mdx@<2.14.0-next.1","gatsby-plugin-netlify-cms@<5.14.0-next.1","gatsby-plugin-no-sourcemaps@<3.14.0-next.1","gatsby-plugin-page-creator@<3.14.0-next.1","gatsby-plugin-preact@<5.14.0-next.1","gatsby-plugin-preload-fonts@<2.14.0-next.1","gatsby-plugin-schema-snapshot@<2.14.0-next.1","gatsby-plugin-styletron@<6.14.0-next.1","gatsby-plugin-subfont@<3.14.0-next.1","gatsby-plugin-utils@<1.14.0-next.1","gatsby-recipes@<0.25.0-next.1","gatsby-source-shopify@<5.6.0-next.1","gatsby-source-wikipedia@<3.14.0-next.1","gatsby-transformer-screenshot@<3.14.0-next.1","gatsby-worker@<0.5.0-next.1"].map(e=>[e,{dependencies:{"@babel/runtime":"^7.14.8"}}]),["gatsby-core-utils@<2.14.0-next.1",{dependencies:{got:"8.3.2"}}],["gatsby-plugin-gatsby-cloud@<=3.1.0-next.0",{dependencies:{"gatsby-core-utils":"^2.13.0-next.0"}}],["gatsby-plugin-gatsby-cloud@<=3.2.0-next.1",{peerDependencies:{webpack:"*"}}],["babel-plugin-remove-graphql-queries@<=3.14.0-next.1",{dependencies:{"gatsby-core-utils":"^2.8.0-next.1"}}],["gatsby-plugin-netlify@3.13.0-next.1",{dependencies:{"gatsby-core-utils":"^2.13.0-next.0"}}],["clipanion-v3-codemod@<=0.2.0",{peerDependencies:{jscodeshift:"^0.11.0"}}],["react-live@*",{peerDependencies:{"react-dom":"*",react:"*"}}],["webpack@<4.44.1",{peerDependenciesMeta:{"webpack-cli":Qt,"webpack-command":Qt}}],["webpack@<5.0.0-beta.23",{peerDependenciesMeta:{"webpack-cli":Qt}}],["webpack-dev-server@<3.10.2",{peerDependenciesMeta:{"webpack-cli":Qt}}],["@docusaurus/responsive-loader@<1.5.0",{peerDependenciesMeta:{sharp:Qt,jimp:Qt}}],["eslint-module-utils@*",{peerDependenciesMeta:{"eslint-import-resolver-node":Qt,"eslint-import-resolver-typescript":Qt,"eslint-import-resolver-webpack":Qt,"@typescript-eslint/parser":Qt}}],["eslint-plugin-import@*",{peerDependenciesMeta:{"@typescript-eslint/parser":Qt}}],["critters-webpack-plugin@<3.0.2",{peerDependenciesMeta:{"html-webpack-plugin":Qt}}],["terser@<=5.10.0",{dependencies:{acorn:"^8.5.0"}}],["babel-preset-react-app@10.0.x <10.0.2",{dependencies:{"@babel/plugin-proposal-private-property-in-object":"^7.16.7"}}],["eslint-config-react-app@*",{peerDependenciesMeta:{typescript:Qt}}],["@vue/eslint-config-typescript@<11.0.0",{peerDependenciesMeta:{typescript:Qt}}],["unplugin-vue2-script-setup@<0.9.1",{peerDependencies:{"@vue/composition-api":"^1.4.3","@vue/runtime-dom":"^3.2.26"}}],["@cypress/snapshot@*",{dependencies:{debug:"^3.2.7"}}],["auto-relay@<=0.14.0",{peerDependencies:{"reflect-metadata":"^0.1.13"}}],["vue-template-babel-compiler@<1.2.0",{peerDependencies:{"vue-template-compiler":"^2.6.0"}}],["@parcel/transformer-image@<2.5.0",{peerDependencies:{"@parcel/core":"*"}}],["@parcel/transformer-js@<2.5.0",{peerDependencies:{"@parcel/core":"*"}}],["parcel@*",{peerDependenciesMeta:{"@parcel/core":Qt}}],["react-scripts@*",{peerDependencies:{eslint:"*"}}],["focus-trap-react@^8.0.0",{dependencies:{tabbable:"^5.3.2"}}],["react-rnd@<10.3.7",{peerDependencies:{react:">=16.3.0","react-dom":">=16.3.0"}}],["connect-mongo@<5.0.0",{peerDependencies:{"express-session":"^1.17.1"}}],["vue-i18n@<9",{peerDependencies:{vue:"^2"}}],["vue-router@<4",{peerDependencies:{vue:"^2"}}],["unified@<10",{dependencies:{"@types/unist":"^2.0.0"}}],["react-github-btn@<=1.3.0",{peerDependencies:{react:">=16.3.0"}}],["react-dev-utils@*",{peerDependencies:{typescript:">=2.7",webpack:">=4"},peerDependenciesMeta:{typescript:Qt}}],["@asyncapi/react-component@<=1.0.0-next.39",{peerDependencies:{react:">=16.8.0","react-dom":">=16.8.0"}}],["xo@*",{peerDependencies:{webpack:">=1.11.0"},peerDependenciesMeta:{webpack:Qt}}],["babel-plugin-remove-graphql-queries@<=4.20.0-next.0",{dependencies:{"@babel/types":"^7.15.4"}}],["gatsby-plugin-page-creator@<=4.20.0-next.1",{dependencies:{"fs-extra":"^10.1.0"}}],["gatsby-plugin-utils@<=3.14.0-next.1",{dependencies:{fastq:"^1.13.0"},peerDependencies:{graphql:"^15.0.0"}}],["gatsby-plugin-mdx@<3.1.0-next.1",{dependencies:{mkdirp:"^1.0.4"}}],["gatsby-plugin-mdx@^2",{peerDependencies:{gatsby:"^3.0.0-next"}}],["fdir@<=5.2.0",{peerDependencies:{picomatch:"2.x"},peerDependenciesMeta:{picomatch:Qt}}],["babel-plugin-transform-typescript-metadata@<=0.3.2",{peerDependencies:{"@babel/core":"^7","@babel/traverse":"^7"},peerDependenciesMeta:{"@babel/traverse":Qt}}],["graphql-compose@>=9.0.10",{peerDependencies:{graphql:"^14.2.0 || ^15.0.0 || ^16.0.0"}}],["vite-plugin-vuetify@<=1.0.2",{peerDependencies:{vue:"^3.0.0"}}],["webpack-plugin-vuetify@<=2.0.1",{peerDependencies:{vue:"^3.2.6"}}],["eslint-import-resolver-vite@<2.0.1",{dependencies:{debug:"^4.3.4",resolve:"^1.22.8"}}],["notistack@^3.0.0",{dependencies:{csstype:"^3.0.10"}}],["@fastify/type-provider-typebox@^5.0.0",{peerDependencies:{fastify:"^5.0.0"}}],["@fastify/type-provider-typebox@^4.0.0",{peerDependencies:{fastify:"^4.0.0"}}]];var U5;function Hge(){return typeof U5>"u"&&(U5=Ie("zlib").brotliDecompressSync(Buffer.from("G7weAByFTVk3Vs7UfHhq4yykgEM7pbW7TI43SG2S5tvGrwHBAzdz+s/npQ6tgEvobvxisrPIadkXeUAJotBn5bDZ5kAhcRqsIHe3F75Walet5hNalwgFDtxb0BiDUjiUQkjG0yW2hto9HPgiCkm316d6bC0kST72YN7D7rfkhCE9x4J0XwB0yavalxpUu2t9xszHrmtwalOxT7VslsxWcB1qpqZwERUra4psWhTV8BgwWeizurec82Caf1ABL11YMfbf8FJ9JBceZOkgmvrQPbC9DUldX/yMbmX06UQluCEjSwUoyO+EZPIjofr+/oAZUck2enraRD+oWLlnlYnj8xB+gwSo9lmmks4fXv574qSqcWA6z21uYkzMu3EWj+K23RxeQlLqiE35/rC8GcS4CGkKHKKq+zAIQwD9iRDNfiAqueLLpicFFrNsAI4zeTD/eO9MHcnRa5m8UT+M2+V+AkFST4BlKneiAQRSdST8KEAIyFlULt6wa9EBd0Ds28VmpaxquJdVt+nwdEs5xUskI13OVtFyY0UrQIRAlCuvvWivvlSKQfTO+2Q8OyUR1W5RvetaPz4jD27hdtwHFFA1Ptx6Ee/t2cY2rg2G46M1pNDRf2pWhvpy8pqMnuI3++4OF3+7OFIWXGjh+o7Nr2jNvbiYcQdQS1h903/jVFgOpA0yJ78z+x759bFA0rq+6aY5qPB4FzS3oYoLupDUhD9nDz6F6H7hpnlMf18KNKDu4IKjTWwrAnY6MFQw1W6ymOALHlFyCZmQhldg1MQHaMVVQTVgDC60TfaBqG++Y8PEoFhN/PBTZT175KNP/BlHDYGOOBmnBdzqJKplZ/ljiVG0ZBzfqeBRrrUkn6rA54462SgiliKoYVnbeptMdXNfAuaupIEi0bApF10TlgHfmEJAPUVidRVFyDupSem5po5vErPqWKhKbUIp0LozpYsIKK57dM/HKr+nguF+7924IIWMICkQ8JUigs9D+W+c4LnNoRtPPKNRUiCYmP+Jfo2lfKCKw8qpraEeWU3uiNRO6zcyKQoXPR5htmzzLznke7b4YbXW3I1lIRzmgG02Udb58U+7TpwyN7XymCgH+wuPDthZVQvRZuEP+SnLtMicz9m5zASWOBiAcLmkuFlTKuHspSIhCBD0yUPKcxu81A+4YD78rA2vtwsUEday9WNyrShyrl60rWmA+SmbYZkQOwFJWArxRYYc5jGhA5ikxYw1rx3ei4NmeX/lKiwpZ9Ln1tV2Ae7sArvxuVLbJjqJRjW1vFXAyHpvLG+8MJ6T2Ubx5M2KDa2SN6vuIGxJ9WQM9Mk3Q7aCNiZONXllhqq24DmoLbQfW2rYWsOgHWjtOmIQMyMKdiHZDjoyIq5+U700nZ6odJAoYXPQBvFNiQ78d5jaXliBqLTJEqUCwi+LiH2mx92EmNKDsJL74Z613+3lf20pxkV1+erOrjj8pW00vsPaahKUM+05ssd5uwM7K482KWEf3TCwlg/o3e5ngto7qSMz7YteIgCsF1UOcsLk7F7MxWbvrPMY473ew0G+noVL8EPbkmEMftMSeL6HFub/zy+2JQ==","base64")).toString()),U5}var _5;function jge(){return typeof _5>"u"&&(_5=Ie("zlib").brotliDecompressSync(Buffer.from("G8MSIIzURnVBnObTcvb3XE6v2S9Qgc2K801Oa5otNKEtK8BINZNcaQHy+9/vf/WXBimwutXC33P2DPc64pps5rz7NGGWaOKNSPL4Y2KRE8twut2lFOIN+OXPtRmPMRhMTILib2bEQx43az2I5d3YS8Roa5UZpF/ujHb3Djd3GDvYUfvFYSUQ39vb2cmifp/rgB4J/65JK3wRBTvMBoNBmn3mbXC63/gbBkW/2IRPri0O8bcsRBsmarF328pAln04nyJFkwUAvNu934supAqLtyerZZpJ8I8suJHhf/ocMV+scKwa8NOiDKIPXw6Ex/EEZD6TEGaW8N5zvNHYF10l6Lfooj7D5W2k3dgvQSbp2Wv8TGOayS978gxlOLVjTGXs66ozewbrjwElLtyrYNnWTfzzdEutgROUFPVMhnMoy8EjJLLlWwIEoySxliim9kYW30JUHiPVyjt0iAw/ZpPmCbUCltYPnq6ZNblIKhTNhqS/oqC9iya5sGKZTOVsTEg34n92uZTf2iPpcZih8rPW8CzA+adIGmyCPcKdLMsBLShd+zuEbTrqpwuh+DLmracZcjPC5Sdf5odDAhKpFuOsQS67RT+1VgWWygSv3YwxDnylc04/PYuaMeIzhBkLrvs7e/OUzRTF56MmfY6rI63QtEjEQzq637zQqJ39nNhu3NmoRRhW/086bHGBUtx0PE0j3aEGvkdh9WJC8y8j8mqqke9/dQ5la+Q3ba4RlhvTbnfQhPDDab3tUifkjKuOsp13mXEmO00Mu88F/M67R7LXfoFDFLNtgCSWjWX+3Jn1371pJTK9xPBiMJafvDjtFyAzu8rxeQ0TKMQXNPs5xxiBOd+BRJP8KP88XPtJIbZKh/cdW8KvBUkpqKpGoiIaA32c3/JnQr4efXt85mXvidOvn/eU3Pase1typLYBalJ14mCso9h79nuMOuCa/kZAOkJHmTjP5RM2WNoPasZUAnT1TAE/NH25hUxcQv6hQWR/m1PKk4ooXMcM4SR1iYU3fUohvqk4RY2hbmTVVIXv6TvqO+0doOjgeVFAcom+RlwJQmOVH7pr1Q9LoJT6n1DeQEB+NHygsATbIwTcOKZlJsY8G4+suX1uQLjUWwLjjs0mvSvZcLTpIGAekeR7GCgl8eo3ndAqEe2XCav4huliHjdbIPBsGJuPX7lrO9HX1UbXRH5opOe1x6JsOSgHZR+EaxuXVhpLLxm6jk1LJtZfHSc6BKPun3CpYYVMJGwEUyk8MTGG0XL5MfEwaXpnc9TKnBmlGn6nHiGREc3ysn47XIBDzA+YvFdjZzVIEDcKGpS6PbUJehFRjEne8D0lVU1XuRtlgszq6pTNlQ/3MzNOEgCWPyTct22V2mEi2krizn5VDo9B19/X2DB3hCGRMM7ONbtnAcIx/OWB1u5uPbW1gsH8irXxT/IzG0PoXWYjhbMsH3KTuoOl5o17PulcgvsfTSnKFM354GWI8luqZnrswWjiXy3G+Vbyo1KMopFmmvBwNELgaS8z8dNZchx/Cl/xjddxhMcyqtzFyONb2Zdu90NkI8pAeufe7YlXrp53v8Dj/l8vWeVspRKBGXScBBPI/HinSTGmLDOGGOCIyH0JFdOZx0gWsacNlQLJMIrBhqRxXxHF/5pseWwejlAAvZ3klZSDSYY8mkToaWejXhgNomeGtx1DTLEUFMRkgF5yFB22WYdJnaWN14r1YJj81hGi45+jrADS5nYRhCiSlCJJ1nL8pYX+HDSMhdTEWyRcgHVp/IsUIZYMfT+YYncUQPgcxNGCHfZ88vDdrcUuaGIl6zhAsiaq7R5dfqrqXH/JcBhfjT8D0azayIyEz75Nxp6YkcyDxlJq3EXnJUpqDohJJOysL1t1uNiHESlvsxPb5cpbW0+ICZqJmUZus1BMW0F5IVBODLIo2zHHjA0=","base64")).toString()),_5}var H5;function Gge(){return typeof H5>"u"&&(H5=Ie("zlib").brotliDecompressSync(Buffer.from("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","base64")).toString()),H5}var qge=new Map([[j.makeIdent(null,"fsevents").identHash,Hge],[j.makeIdent(null,"resolve").identHash,jge],[j.makeIdent(null,"typescript").identHash,Gge]]),Zot={hooks:{registerPackageExtensions:async(e,t)=>{for(let[r,s]of M5)t(j.parseDescriptor(r,!0),s)},getBuiltinPatch:async(e,t)=>{let r="compat/";if(!t.startsWith(r))return;let s=j.parseIdent(t.slice(r.length)),a=qge.get(s.identHash)?.();return typeof a<"u"?a:null},reduceDependency:async(e,t,r,s)=>typeof qge.get(e.identHash)>"u"?e:j.makeDescriptor(e,j.makeRange({protocol:"patch:",source:j.stringifyDescriptor(e),selector:`optional!builtin`,params:null}))}},$ot=Zot;var s9={};Vt(s9,{ConstraintsCheckCommand:()=>GC,ConstraintsQueryCommand:()=>HC,ConstraintsSourceCommand:()=>jC,default:()=>yat});qe();qe();iS();var LC=class{constructor(t){this.project=t}createEnvironment(){let t=new OC(["cwd","ident"]),r=new OC(["workspace","type","ident"]),s=new OC(["ident"]),a={manifestUpdates:new Map,reportedErrors:new Map},n=new Map,c=new Map;for(let f of this.project.storedPackages.values()){let p=Array.from(f.peerDependencies.values(),h=>[j.stringifyIdent(h),h.range]);n.set(f.locatorHash,{workspace:null,ident:j.stringifyIdent(f),version:f.version,dependencies:new Map,peerDependencies:new Map(p.filter(([h])=>f.peerDependenciesMeta.get(h)?.optional!==!0)),optionalPeerDependencies:new Map(p.filter(([h])=>f.peerDependenciesMeta.get(h)?.optional===!0))})}for(let f of this.project.storedPackages.values()){let p=n.get(f.locatorHash);p.dependencies=new Map(Array.from(f.dependencies.values(),h=>{let E=this.project.storedResolutions.get(h.descriptorHash);if(typeof E>"u")throw new Error("Assertion failed: The resolution should have been registered");let C=n.get(E);if(typeof C>"u")throw new Error("Assertion failed: The package should have been registered");return[j.stringifyIdent(h),C]})),p.dependencies.delete(p.ident)}for(let f of this.project.workspaces){let p=j.stringifyIdent(f.anchoredLocator),h=f.manifest.exportTo({}),E=n.get(f.anchoredLocator.locatorHash);if(typeof E>"u")throw new Error("Assertion failed: The package should have been registered");let C=(T,O,{caller:U=Ui.getCaller()}={})=>{let V=nS(T),te=Ge.getMapWithDefault(a.manifestUpdates,f.cwd),ie=Ge.getMapWithDefault(te,V),ue=Ge.getSetWithDefault(ie,O);U!==null&&ue.add(U)},S=T=>C(T,void 0,{caller:Ui.getCaller()}),x=T=>{Ge.getArrayWithDefault(a.reportedErrors,f.cwd).push(T)},I=t.insert({cwd:f.relativeCwd,ident:p,manifest:h,pkg:E,set:C,unset:S,error:x});c.set(f,I);for(let T of _t.allDependencies)for(let O of f.manifest[T].values()){let U=j.stringifyIdent(O),V=()=>{C([T,U],void 0,{caller:Ui.getCaller()})},te=ue=>{C([T,U],ue,{caller:Ui.getCaller()})},ie=null;if(T!=="peerDependencies"&&(T!=="dependencies"||!f.manifest.devDependencies.has(O.identHash))){let ue=f.anchoredPackage.dependencies.get(O.identHash);if(ue){if(typeof ue>"u")throw new Error("Assertion failed: The dependency should have been registered");let ae=this.project.storedResolutions.get(ue.descriptorHash);if(typeof ae>"u")throw new Error("Assertion failed: The resolution should have been registered");let ge=n.get(ae);if(typeof ge>"u")throw new Error("Assertion failed: The package should have been registered");ie=ge}}r.insert({workspace:I,ident:U,range:O.range,type:T,resolution:ie,update:te,delete:V,error:x})}}for(let f of this.project.storedPackages.values()){let p=this.project.tryWorkspaceByLocator(f);if(!p)continue;let h=c.get(p);if(typeof h>"u")throw new Error("Assertion failed: The workspace should have been registered");let E=n.get(f.locatorHash);if(typeof E>"u")throw new Error("Assertion failed: The package should have been registered");E.workspace=h}return{workspaces:t,dependencies:r,packages:s,result:a}}async process(){let t=this.createEnvironment(),r={Yarn:{workspace:a=>t.workspaces.find(a)[0]??null,workspaces:a=>t.workspaces.find(a),dependency:a=>t.dependencies.find(a)[0]??null,dependencies:a=>t.dependencies.find(a),package:a=>t.packages.find(a)[0]??null,packages:a=>t.packages.find(a)}},s=await this.project.loadUserConfig();return s?.constraints?(await s.constraints(r),t.result):null}};qe();qe();Yt();var HC=class extends At{constructor(){super(...arguments);this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.query=he.String()}static{this.paths=[["constraints","query"]]}static{this.usage=at.Usage({category:"Constraints-related commands",description:"query the constraints fact database",details:` This command will output all matches to the given prolog query. `,examples:[["List all dependencies throughout the workspace","yarn constraints query 'workspace_has_dependency(_, DependencyName, _, _).'"]]})}async execute(){let{Constraints:r}=await Promise.resolve().then(()=>(lS(),aS)),s=await ze.find(this.context.cwd,this.context.plugins),{project:a}=await Rt.find(s,this.context.cwd),n=await r.find(a),c=this.query;return c.endsWith(".")||(c=`${c}.`),(await Ot.start({configuration:s,json:this.json,stdout:this.context.stdout},async p=>{for await(let h of n.query(c)){let E=Array.from(Object.entries(h)),C=E.length,S=E.reduce((x,[I])=>Math.max(x,I.length),0);for(let x=0;x(lS(),aS)),s=await ze.find(this.context.cwd,this.context.plugins),{project:a}=await Rt.find(s,this.context.cwd),n=await r.find(a);this.context.stdout.write(this.verbose?n.fullSource:n.source)}};qe();qe();Yt();iS();var GC=class extends At{constructor(){super(...arguments);this.fix=he.Boolean("--fix",!1,{description:"Attempt to automatically fix unambiguous issues, following a multi-pass process"});this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}static{this.paths=[["constraints"]]}static{this.usage=at.Usage({category:"Constraints-related commands",description:"check that the project constraints are met",details:` This command will run constraints on your project and emit errors for each one that is found but isn't met. If any error is emitted the process will exit with a non-zero exit code. If the \`--fix\` flag is used, Yarn will attempt to automatically fix the issues the best it can, following a multi-pass process (with a maximum of 10 iterations). Some ambiguous patterns cannot be autofixed, in which case you'll have to manually specify the right resolution. For more information as to how to write constraints, please consult our dedicated page on our website: https://yarnpkg.com/features/constraints. `,examples:[["Check that all constraints are satisfied","yarn constraints"],["Autofix all unmet constraints","yarn constraints --fix"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Rt.find(r,this.context.cwd);await s.restoreInstallState();let a=await s.loadUserConfig(),n;if(a?.constraints)n=new LC(s);else{let{Constraints:h}=await Promise.resolve().then(()=>(lS(),aS));n=await h.find(s)}let c,f=!1,p=!1;for(let h=this.fix?10:1;h>0;--h){let E=await n.process();if(!E)break;let{changedWorkspaces:C,remainingErrors:S}=jT(s,E,{fix:this.fix}),x=[];for(let[I,T]of C){let O=I.manifest.indent;I.manifest=new _t,I.manifest.indent=O,I.manifest.load(T),x.push(I.persistManifest())}if(await Promise.all(x),!(C.size>0&&h>1)){c=Wge(S,{configuration:r}),f=!1,p=!0;for(let[,I]of S)for(let T of I)T.fixable?f=!0:p=!1}}if(c.children.length===0)return 0;if(f){let h=p?`Those errors can all be fixed by running ${pe.pretty(r,"yarn constraints --fix",pe.Type.CODE)}`:`Errors prefixed by '\u2699' can be fixed by running ${pe.pretty(r,"yarn constraints --fix",pe.Type.CODE)}`;await Ot.start({configuration:r,stdout:this.context.stdout,includeNames:!1,includeFooter:!1},async E=>{E.reportInfo(0,h),E.reportSeparator()})}return c.children=Ge.sortMap(c.children,h=>h.value[1]),Rs.emitTree(c,{configuration:r,stdout:this.context.stdout,json:this.json,separators:1}),1}};iS();var mat={configuration:{enableConstraintsChecks:{description:"If true, constraints will run during installs",type:"BOOLEAN",default:!1},constraintsPath:{description:"The path of the constraints file.",type:"ABSOLUTE_PATH",default:"./constraints.pro"}},commands:[HC,jC,GC],hooks:{async validateProjectAfterInstall(e,{reportError:t}){if(!e.configuration.get("enableConstraintsChecks"))return;let r=await e.loadUserConfig(),s;if(r?.constraints)s=new LC(e);else{let{Constraints:c}=await Promise.resolve().then(()=>(lS(),aS));s=await c.find(e)}let a=await s.process();if(!a)return;let{remainingErrors:n}=jT(e,a);if(n.size!==0)if(e.configuration.isCI)for(let[c,f]of n)for(let p of f)t(84,`${pe.pretty(e.configuration,c.anchoredLocator,pe.Type.IDENT)}: ${p.text}`);else t(84,`Constraint check failed; run ${pe.pretty(e.configuration,"yarn constraints",pe.Type.CODE)} for more details`)}}},yat=mat;var o9={};Vt(o9,{CreateCommand:()=>qC,DlxCommand:()=>WC,default:()=>Iat});qe();Yt();var qC=class extends At{constructor(){super(...arguments);this.pkg=he.String("-p,--package",{description:"The package to run the provided command from"});this.quiet=he.Boolean("-q,--quiet",!1,{description:"Only report critical errors instead of printing the full install logs"});this.command=he.String();this.args=he.Proxy()}static{this.paths=[["create"]]}async execute(){let r=[];this.pkg&&r.push("--package",this.pkg),this.quiet&&r.push("--quiet");let s=this.command.replace(/^(@[^@/]+)(@|$)/,"$1/create$2"),a=j.parseDescriptor(s),n=a.name.match(/^create(-|$)/)?a:a.scope?j.makeIdent(a.scope,`create-${a.name}`):j.makeIdent(null,`create-${a.name}`),c=j.stringifyIdent(n);return a.range!=="unknown"&&(c+=`@${a.range}`),this.cli.run(["dlx",...r,c,...this.args])}};qe();qe();Dt();Yt();var WC=class extends At{constructor(){super(...arguments);this.packages=he.Array("-p,--package",{description:"The package(s) to install before running the command"});this.quiet=he.Boolean("-q,--quiet",!1,{description:"Only report critical errors instead of printing the full install logs"});this.command=he.String();this.args=he.Proxy()}static{this.paths=[["dlx"]]}static{this.usage=at.Usage({description:"run a package in a temporary environment",details:"\n This command will install a package within a temporary environment, and run its binary script if it contains any. The binary will run within the current cwd.\n\n By default Yarn will download the package named `command`, but this can be changed through the use of the `-p,--package` flag which will instruct Yarn to still run the same command but from a different package.\n\n Using `yarn dlx` as a replacement of `yarn add` isn't recommended, as it makes your project non-deterministic (Yarn doesn't keep track of the packages installed through `dlx` - neither their name, nor their version).\n ",examples:[["Use create-vite to scaffold a new Vite project","yarn dlx create-vite"],["Install multiple packages for a single command",`yarn dlx -p typescript -p ts-node ts-node --transpile-only -e "console.log('hello!')"`]]})}async execute(){return ze.telemetry=null,await le.mktempPromise(async r=>{let s=J.join(r,`dlx-${process.pid}`);await le.mkdirPromise(s),await le.writeFilePromise(J.join(s,"package.json"),`{} `),await le.writeFilePromise(J.join(s,"yarn.lock"),"");let a=J.join(s,".yarnrc.yml"),n=await ze.findProjectCwd(this.context.cwd),f={enableGlobalCache:!(await ze.find(this.context.cwd,null,{strict:!1})).get("enableGlobalCache"),enableTelemetry:!1,logFilters:[{code:Kf(68),level:pe.LogLevel.Discard}]},p=n!==null?J.join(n,".yarnrc.yml"):null;p!==null&&le.existsSync(p)?(await le.copyFilePromise(p,a),await ze.updateConfiguration(s,O=>{let U=Ge.toMerged(O,f);return Array.isArray(O.plugins)&&(U.plugins=O.plugins.map(V=>{let te=typeof V=="string"?V:V.path,ie=fe.isAbsolute(te)?te:fe.resolve(fe.fromPortablePath(n),te);return typeof V=="string"?ie:{path:ie,spec:V.spec}})),U})):await le.writeJsonPromise(a,f);let h=this.packages??[this.command],E=j.parseDescriptor(this.command).name,C=await this.cli.run(["add","--fixed","--",...h],{cwd:s,quiet:this.quiet});if(C!==0)return C;this.quiet||this.context.stdout.write(` `);let S=await ze.find(s,this.context.plugins),{project:x,workspace:I}=await Rt.find(S,s);if(I===null)throw new ar(x.cwd,s);await x.restoreInstallState();let T=await Cn.getWorkspaceAccessibleBinaries(I);return T.has(E)===!1&&T.size===1&&typeof this.packages>"u"&&(E=Array.from(T)[0][0]),await Cn.executeWorkspaceAccessibleBinary(I,E,this.args,{packageAccessibleBinaries:T,cwd:this.context.cwd,stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr})})}};var Eat={commands:[qC,WC]},Iat=Eat;var c9={};Vt(c9,{ExecFetcher:()=>uS,ExecResolver:()=>fS,default:()=>Bat,execUtils:()=>YT});qe();qe();qe();Dt();var fA="exec:";var YT={};Vt(YT,{loadGeneratorFile:()=>cS,makeLocator:()=>l9,makeSpec:()=>mme,parseSpec:()=>a9});qe();Dt();function a9(e){let{params:t,selector:r}=j.parseRange(e),s=fe.toPortablePath(r);return{parentLocator:t&&typeof t.locator=="string"?j.parseLocator(t.locator):null,path:s}}function mme({parentLocator:e,path:t,generatorHash:r,protocol:s}){let a=e!==null?{locator:j.stringifyLocator(e)}:{},n=typeof r<"u"?{hash:r}:{};return j.makeRange({protocol:s,source:t,selector:t,params:{...n,...a}})}function l9(e,{parentLocator:t,path:r,generatorHash:s,protocol:a}){return j.makeLocator(e,mme({parentLocator:t,path:r,generatorHash:s,protocol:a}))}async function cS(e,t,r){let{parentLocator:s,path:a}=j.parseFileStyleRange(e,{protocol:t}),n=J.isAbsolute(a)?{packageFs:new bn(vt.root),prefixPath:vt.dot,localPath:vt.root}:await r.fetcher.fetch(s,r),c=n.localPath?{packageFs:new bn(vt.root),prefixPath:J.relative(vt.root,n.localPath)}:n;n!==c&&n.releaseFs&&n.releaseFs();let f=c.packageFs,p=J.join(c.prefixPath,a);return await f.readFilePromise(p,"utf8")}var uS=class{supports(t,r){return!!t.reference.startsWith(fA)}getLocalPath(t,r){let{parentLocator:s,path:a}=j.parseFileStyleRange(t.reference,{protocol:fA});if(J.isAbsolute(a))return a;let n=r.fetcher.getLocalPath(s,r);return n===null?null:J.resolve(n,a)}async fetch(t,r){let s=r.checksums.get(t.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(t,s,{onHit:()=>r.report.reportCacheHit(t),onMiss:()=>r.report.reportCacheMiss(t),loader:()=>this.fetchFromDisk(t,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:j.getIdentVendorPath(t),localPath:this.getLocalPath(t,r),checksum:c}}async fetchFromDisk(t,r){let s=r.project.getDependencyMeta(t,null);if(!r.project.configuration.get("enableScripts")&&!s.built)throw new Lt(4,`${j.prettyLocator(r.project.configuration,t)} can't be built with the exec: protocol because all scripts have been disabled.`);let a=await cS(t.reference,fA,r);return le.mktempPromise(async n=>{let c=J.join(n,"generator.js");return await le.writeFilePromise(c,a),le.mktempPromise(async f=>{if(await this.generatePackage(f,t,c,r),!le.existsSync(J.join(f,"build")))throw new Error("The script should have generated a build directory");return await gs.makeArchiveFromDirectory(J.join(f,"build"),{prefixPath:j.getIdentVendorPath(t),compressionLevel:r.project.configuration.get("compressionLevel")})})})}async generatePackage(t,r,s,a){return await le.mktempPromise(async n=>{let c=await Cn.makeScriptEnv({project:a.project,binFolder:n}),f=J.join(t,"runtime.js");return await le.mktempPromise(async p=>{let h=J.join(p,"buildfile.log"),E=J.join(t,"generator"),C=J.join(t,"build");await le.mkdirPromise(E),await le.mkdirPromise(C);let S={tempDir:fe.fromPortablePath(E),buildDir:fe.fromPortablePath(C),locator:j.stringifyLocator(r)};await le.writeFilePromise(f,` // Expose 'Module' as a global variable Object.defineProperty(global, 'Module', { get: () => require('module'), configurable: true, enumerable: false, }); // Expose non-hidden built-in modules as global variables for (const name of Module.builtinModules.filter((name) => name !== 'module' && !name.startsWith('_'))) { Object.defineProperty(global, name, { get: () => require(name), configurable: true, enumerable: false, }); } // Expose the 'execEnv' global variable Object.defineProperty(global, 'execEnv', { value: { ...${JSON.stringify(S)}, }, enumerable: true, }); `);let x=c.NODE_OPTIONS||"",I=/\s*--require\s+\S*\.pnp\.c?js\s*/g;x=x.replace(I," ").trim(),c.NODE_OPTIONS=x;let{stdout:T,stderr:O}=a.project.configuration.getSubprocessStreams(h,{header:`# This file contains the result of Yarn generating a package (${j.stringifyLocator(r)}) `,prefix:j.prettyLocator(a.project.configuration,r),report:a.report}),{code:U}=await qr.pipevp(process.execPath,["--require",fe.fromPortablePath(f),fe.fromPortablePath(s),j.stringifyIdent(r)],{cwd:t,env:c,stdin:null,stdout:T,stderr:O});if(U!==0)throw le.detachTemp(p),new Error(`Package generation failed (exit code ${U}, logs can be found here: ${pe.pretty(a.project.configuration,h,pe.Type.PATH)})`)})})}};qe();qe();qe();var Cat=2,fS=class{supportsDescriptor(t,r){return!!t.range.startsWith(fA)}supportsLocator(t,r){return!!t.reference.startsWith(fA)}shouldPersistResolution(t,r){return!1}bindDescriptor(t,r,s){if(s.project.tryWorkspaceByLocator(r)===null)throw new Lt(57,`${j.prettyLocator(s.project.configuration,r)} lists ${j.prettyDescriptor(s.project.configuration,t)} as dependency, but only workspaces can depend on exec: packages.`);return j.bindDescriptor(t,{locator:j.stringifyLocator(r)})}getResolutionDependencies(t,r){return{}}async getCandidates(t,r,s){if(!s.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let{path:a,parentLocator:n}=a9(t.range);if(n===null)throw new Error("Assertion failed: The descriptor should have been bound");let c=await cS(j.makeRange({protocol:fA,source:a,selector:a,params:{locator:j.stringifyLocator(n)}}),fA,s.fetchOptions),f=Ln.makeHash(`${Cat}`,c).slice(0,6);return[l9(t,{parentLocator:n,path:a,generatorHash:f,protocol:fA})]}async getSatisfying(t,r,s,a){let[n]=await this.getCandidates(t,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(t,r){if(!r.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let s=await r.fetchOptions.fetcher.fetch(t,r.fetchOptions),a=await Ge.releaseAfterUseAsync(async()=>await _t.find(s.prefixPath,{baseFs:s.packageFs}),s.releaseFs);return{...t,version:a.version||"0.0.0",languageName:a.languageName||r.project.configuration.get("defaultLanguageName"),linkType:"HARD",conditions:a.getConditions(),dependencies:r.project.configuration.normalizeDependencyMap(a.dependencies),peerDependencies:a.peerDependencies,dependenciesMeta:a.dependenciesMeta,peerDependenciesMeta:a.peerDependenciesMeta,bin:a.bin}}};var wat={fetchers:[uS],resolvers:[fS]},Bat=wat;var f9={};Vt(f9,{FileFetcher:()=>dS,FileResolver:()=>gS,TarballFileFetcher:()=>mS,TarballFileResolver:()=>yS,default:()=>Dat,fileUtils:()=>Rm});qe();Dt();var YC=/^(?:[a-zA-Z]:[\\/]|\.{0,2}\/)/,AS=/^[^?]*\.(?:tar\.gz|tgz)(?:::.*)?$/,ts="file:";var Rm={};Vt(Rm,{fetchArchiveFromLocator:()=>hS,makeArchiveFromLocator:()=>VT,makeBufferFromLocator:()=>u9,makeLocator:()=>VC,makeSpec:()=>yme,parseSpec:()=>pS});qe();Dt();function pS(e){let{params:t,selector:r}=j.parseRange(e),s=fe.toPortablePath(r);return{parentLocator:t&&typeof t.locator=="string"?j.parseLocator(t.locator):null,path:s}}function yme({parentLocator:e,path:t,hash:r,protocol:s}){let a=e!==null?{locator:j.stringifyLocator(e)}:{},n=typeof r<"u"?{hash:r}:{};return j.makeRange({protocol:s,source:t,selector:t,params:{...n,...a}})}function VC(e,{parentLocator:t,path:r,hash:s,protocol:a}){return j.makeLocator(e,yme({parentLocator:t,path:r,hash:s,protocol:a}))}async function hS(e,t){let{parentLocator:r,path:s}=j.parseFileStyleRange(e.reference,{protocol:ts}),a=J.isAbsolute(s)?{packageFs:new bn(vt.root),prefixPath:vt.dot,localPath:vt.root}:await t.fetcher.fetch(r,t),n=a.localPath?{packageFs:new bn(vt.root),prefixPath:J.relative(vt.root,a.localPath)}:a;a!==n&&a.releaseFs&&a.releaseFs();let c=n.packageFs,f=J.join(n.prefixPath,s);return await Ge.releaseAfterUseAsync(async()=>await c.readFilePromise(f),n.releaseFs)}async function VT(e,{protocol:t,fetchOptions:r,inMemory:s=!1}){let{parentLocator:a,path:n}=j.parseFileStyleRange(e.reference,{protocol:t}),c=J.isAbsolute(n)?{packageFs:new bn(vt.root),prefixPath:vt.dot,localPath:vt.root}:await r.fetcher.fetch(a,r),f=c.localPath?{packageFs:new bn(vt.root),prefixPath:J.relative(vt.root,c.localPath)}:c;c!==f&&c.releaseFs&&c.releaseFs();let p=f.packageFs,h=J.join(f.prefixPath,n);return await Ge.releaseAfterUseAsync(async()=>await gs.makeArchiveFromDirectory(h,{baseFs:p,prefixPath:j.getIdentVendorPath(e),compressionLevel:r.project.configuration.get("compressionLevel"),inMemory:s}),f.releaseFs)}async function u9(e,{protocol:t,fetchOptions:r}){return(await VT(e,{protocol:t,fetchOptions:r,inMemory:!0})).getBufferAndClose()}var dS=class{supports(t,r){return!!t.reference.startsWith(ts)}getLocalPath(t,r){let{parentLocator:s,path:a}=j.parseFileStyleRange(t.reference,{protocol:ts});if(J.isAbsolute(a))return a;let n=r.fetcher.getLocalPath(s,r);return n===null?null:J.resolve(n,a)}async fetch(t,r){let s=r.checksums.get(t.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(t,s,{onHit:()=>r.report.reportCacheHit(t),onMiss:()=>r.report.reportCacheMiss(t,`${j.prettyLocator(r.project.configuration,t)} can't be found in the cache and will be fetched from the disk`),loader:()=>this.fetchFromDisk(t,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:j.getIdentVendorPath(t),localPath:this.getLocalPath(t,r),checksum:c}}async fetchFromDisk(t,r){return VT(t,{protocol:ts,fetchOptions:r})}};qe();qe();var vat=2,gS=class{supportsDescriptor(t,r){return t.range.match(YC)?!0:!!t.range.startsWith(ts)}supportsLocator(t,r){return!!t.reference.startsWith(ts)}shouldPersistResolution(t,r){return!1}bindDescriptor(t,r,s){return YC.test(t.range)&&(t=j.makeDescriptor(t,`${ts}${t.range}`)),j.bindDescriptor(t,{locator:j.stringifyLocator(r)})}getResolutionDependencies(t,r){return{}}async getCandidates(t,r,s){if(!s.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let{path:a,parentLocator:n}=pS(t.range);if(n===null)throw new Error("Assertion failed: The descriptor should have been bound");let c=await u9(j.makeLocator(t,j.makeRange({protocol:ts,source:a,selector:a,params:{locator:j.stringifyLocator(n)}})),{protocol:ts,fetchOptions:s.fetchOptions}),f=Ln.makeHash(`${vat}`,c).slice(0,6);return[VC(t,{parentLocator:n,path:a,hash:f,protocol:ts})]}async getSatisfying(t,r,s,a){let[n]=await this.getCandidates(t,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(t,r){if(!r.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let s=await r.fetchOptions.fetcher.fetch(t,r.fetchOptions),a=await Ge.releaseAfterUseAsync(async()=>await _t.find(s.prefixPath,{baseFs:s.packageFs}),s.releaseFs);return{...t,version:a.version||"0.0.0",languageName:a.languageName||r.project.configuration.get("defaultLanguageName"),linkType:"HARD",conditions:a.getConditions(),dependencies:r.project.configuration.normalizeDependencyMap(a.dependencies),peerDependencies:a.peerDependencies,dependenciesMeta:a.dependenciesMeta,peerDependenciesMeta:a.peerDependenciesMeta,bin:a.bin}}};qe();var mS=class{supports(t,r){return AS.test(t.reference)?!!t.reference.startsWith(ts):!1}getLocalPath(t,r){return null}async fetch(t,r){let s=r.checksums.get(t.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(t,s,{onHit:()=>r.report.reportCacheHit(t),onMiss:()=>r.report.reportCacheMiss(t,`${j.prettyLocator(r.project.configuration,t)} can't be found in the cache and will be fetched from the disk`),loader:()=>this.fetchFromDisk(t,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:j.getIdentVendorPath(t),checksum:c}}async fetchFromDisk(t,r){let s=await hS(t,r);return await gs.convertToZip(s,{configuration:r.project.configuration,prefixPath:j.getIdentVendorPath(t),stripComponents:1})}};qe();qe();qe();var yS=class{supportsDescriptor(t,r){return AS.test(t.range)?!!(t.range.startsWith(ts)||YC.test(t.range)):!1}supportsLocator(t,r){return AS.test(t.reference)?!!t.reference.startsWith(ts):!1}shouldPersistResolution(t,r){return!1}bindDescriptor(t,r,s){return YC.test(t.range)&&(t=j.makeDescriptor(t,`${ts}${t.range}`)),j.bindDescriptor(t,{locator:j.stringifyLocator(r)})}getResolutionDependencies(t,r){return{}}async getCandidates(t,r,s){if(!s.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let{path:a,parentLocator:n}=pS(t.range);if(n===null)throw new Error("Assertion failed: The descriptor should have been bound");let c=VC(t,{parentLocator:n,path:a,hash:"",protocol:ts}),f=await hS(c,s.fetchOptions),p=Ln.makeHash(f).slice(0,6);return[VC(t,{parentLocator:n,path:a,hash:p,protocol:ts})]}async getSatisfying(t,r,s,a){let[n]=await this.getCandidates(t,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(t,r){if(!r.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let s=await r.fetchOptions.fetcher.fetch(t,r.fetchOptions),a=await Ge.releaseAfterUseAsync(async()=>await _t.find(s.prefixPath,{baseFs:s.packageFs}),s.releaseFs);return{...t,version:a.version||"0.0.0",languageName:a.languageName||r.project.configuration.get("defaultLanguageName"),linkType:"HARD",conditions:a.getConditions(),dependencies:r.project.configuration.normalizeDependencyMap(a.dependencies),peerDependencies:a.peerDependencies,dependenciesMeta:a.dependenciesMeta,peerDependenciesMeta:a.peerDependenciesMeta,bin:a.bin}}};var Sat={fetchers:[mS,dS],resolvers:[yS,gS]},Dat=Sat;var h9={};Vt(h9,{GithubFetcher:()=>ES,default:()=>Pat,githubUtils:()=>JT});qe();Dt();var JT={};Vt(JT,{invalidGithubUrlMessage:()=>Cme,isGithubUrl:()=>A9,parseGithubUrl:()=>p9});var Eme=et(Ie("querystring")),Ime=[/^https?:\/\/(?:([^/]+?)@)?github.com\/([^/#]+)\/([^/#]+)\/tarball\/([^/#]+)(?:#(.*))?$/,/^https?:\/\/(?:([^/]+?)@)?github.com\/([^/#]+)\/([^/#]+?)(?:\.git)?(?:#(.*))?$/];function A9(e){return e?Ime.some(t=>!!e.match(t)):!1}function p9(e){let t;for(let f of Ime)if(t=e.match(f),t)break;if(!t)throw new Error(Cme(e));let[,r,s,a,n="master"]=t,{commit:c}=Eme.default.parse(n);return n=c||n.replace(/[^:]*:/,""),{auth:r,username:s,reponame:a,treeish:n}}function Cme(e){return`Input cannot be parsed as a valid GitHub URL ('${e}').`}var ES=class{supports(t,r){return!!A9(t.reference)}getLocalPath(t,r){return null}async fetch(t,r){let s=r.checksums.get(t.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(t,s,{onHit:()=>r.report.reportCacheHit(t),onMiss:()=>r.report.reportCacheMiss(t,`${j.prettyLocator(r.project.configuration,t)} can't be found in the cache and will be fetched from GitHub`),loader:()=>this.fetchFromNetwork(t,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:j.getIdentVendorPath(t),checksum:c}}async fetchFromNetwork(t,r){let s=await nn.get(this.getLocatorUrl(t,r),{configuration:r.project.configuration});return await le.mktempPromise(async a=>{let n=new bn(a);await gs.extractArchiveTo(s,n,{stripComponents:1});let c=La.splitRepoUrl(t.reference),f=J.join(a,"package.tgz");await Cn.prepareExternalProject(a,f,{configuration:r.project.configuration,report:r.report,workspace:c.extra.workspace,locator:t});let p=await le.readFilePromise(f);return await gs.convertToZip(p,{configuration:r.project.configuration,prefixPath:j.getIdentVendorPath(t),stripComponents:1})})}getLocatorUrl(t,r){let{auth:s,username:a,reponame:n,treeish:c}=p9(t.reference);return`https://${s?`${s}@`:""}github.com/${a}/${n}/archive/${c}.tar.gz`}};var bat={hooks:{async fetchHostedRepository(e,t,r){if(e!==null)return e;let s=new ES;if(!s.supports(t,r))return null;try{return await s.fetch(t,r)}catch{return null}}}},Pat=bat;var d9={};Vt(d9,{TarballHttpFetcher:()=>CS,TarballHttpResolver:()=>wS,default:()=>kat});qe();function IS(e){let t;try{t=new URL(e)}catch{return!1}return!(t.protocol!=="http:"&&t.protocol!=="https:"||!t.pathname.match(/(\.tar\.gz|\.tgz|\/[^.]+)$/))}var CS=class{supports(t,r){return IS(t.reference)}getLocalPath(t,r){return null}async fetch(t,r){let s=r.checksums.get(t.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(t,s,{onHit:()=>r.report.reportCacheHit(t),onMiss:()=>r.report.reportCacheMiss(t,`${j.prettyLocator(r.project.configuration,t)} can't be found in the cache and will be fetched from the remote server`),loader:()=>this.fetchFromNetwork(t,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:j.getIdentVendorPath(t),checksum:c}}async fetchFromNetwork(t,r){let s=await nn.get(t.reference,{configuration:r.project.configuration});return await gs.convertToZip(s,{configuration:r.project.configuration,prefixPath:j.getIdentVendorPath(t),stripComponents:1})}};qe();qe();var wS=class{supportsDescriptor(t,r){return IS(t.range)}supportsLocator(t,r){return IS(t.reference)}shouldPersistResolution(t,r){return!0}bindDescriptor(t,r,s){return t}getResolutionDependencies(t,r){return{}}async getCandidates(t,r,s){return[j.convertDescriptorToLocator(t)]}async getSatisfying(t,r,s,a){let[n]=await this.getCandidates(t,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(t,r){if(!r.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let s=await r.fetchOptions.fetcher.fetch(t,r.fetchOptions),a=await Ge.releaseAfterUseAsync(async()=>await _t.find(s.prefixPath,{baseFs:s.packageFs}),s.releaseFs);return{...t,version:a.version||"0.0.0",languageName:a.languageName||r.project.configuration.get("defaultLanguageName"),linkType:"HARD",conditions:a.getConditions(),dependencies:r.project.configuration.normalizeDependencyMap(a.dependencies),peerDependencies:a.peerDependencies,dependenciesMeta:a.dependenciesMeta,peerDependenciesMeta:a.peerDependenciesMeta,bin:a.bin}}};var xat={fetchers:[CS],resolvers:[wS]},kat=xat;var g9={};Vt(g9,{InitCommand:()=>J0,InitInitializerCommand:()=>JC,default:()=>Rat});Yt();qe();qe();Dt();Yt();var J0=class extends At{constructor(){super(...arguments);this.private=he.Boolean("-p,--private",!1,{description:"Initialize a private package"});this.workspace=he.Boolean("-w,--workspace",!1,{description:"Initialize a workspace root with a `packages/` directory"});this.install=he.String("-i,--install",!1,{tolerateBoolean:!0,description:"Initialize a package with a specific bundle that will be locked in the project"});this.name=he.String("-n,--name",{description:"Initialize a package with the given name"});this.usev2=he.Boolean("-2",!1,{hidden:!0});this.yes=he.Boolean("-y,--yes",{hidden:!0})}static{this.paths=[["init"]]}static{this.usage=at.Usage({description:"create a new package",details:"\n This command will setup a new package in your local directory.\n\n If the `-p,--private` or `-w,--workspace` options are set, the package will be private by default.\n\n If the `-w,--workspace` option is set, the package will be configured to accept a set of workspaces in the `packages/` directory.\n\n If the `-i,--install` option is given a value, Yarn will first download it using `yarn set version` and only then forward the init call to the newly downloaded bundle. Without arguments, the downloaded bundle will be `latest`.\n\n The initial settings of the manifest can be changed by using the `initScope` and `initFields` configuration values. Additionally, Yarn will generate an EditorConfig file whose rules can be altered via `initEditorConfig`, and will initialize a Git repository in the current directory.\n ",examples:[["Create a new package in the local directory","yarn init"],["Create a new private package in the local directory","yarn init -p"],["Create a new package and store the Yarn release inside","yarn init -i=latest"],["Create a new private package and defines it as a workspace root","yarn init -w"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=typeof this.install=="string"?this.install:this.usev2||this.install===!0?"latest":null;return s!==null?await this.executeProxy(r,s):await this.executeRegular(r)}async executeProxy(r,s){if(r.projectCwd!==null&&r.projectCwd!==this.context.cwd)throw new it("Cannot use the --install flag from within a project subdirectory");le.existsSync(this.context.cwd)||await le.mkdirPromise(this.context.cwd,{recursive:!0});let a=J.join(this.context.cwd,Er.lockfile);le.existsSync(a)||await le.writeFilePromise(a,"");let n=await this.cli.run(["set","version",s],{quiet:!0});if(n!==0)return n;let c=[];return this.private&&c.push("-p"),this.workspace&&c.push("-w"),this.name&&c.push(`-n=${this.name}`),this.yes&&c.push("-y"),await le.mktempPromise(async f=>{let{code:p}=await qr.pipevp("yarn",["init",...c],{cwd:this.context.cwd,stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr,env:await Cn.makeScriptEnv({binFolder:f})});return p})}async initialize(){}async executeRegular(r){let s=null;try{s=(await Rt.find(r,this.context.cwd)).project}catch{s=null}le.existsSync(this.context.cwd)||await le.mkdirPromise(this.context.cwd,{recursive:!0});let a=await _t.tryFind(this.context.cwd),n=a??new _t,c=Object.fromEntries(r.get("initFields").entries());n.load(c),n.name=n.name??j.makeIdent(r.get("initScope"),this.name??J.basename(this.context.cwd)),n.packageManager=An&&Ge.isTaggedYarnVersion(An)?`yarn@${An}`:null,(!a&&this.workspace||this.private)&&(n.private=!0),this.workspace&&n.workspaceDefinitions.length===0&&(await le.mkdirPromise(J.join(this.context.cwd,"packages"),{recursive:!0}),n.workspaceDefinitions=[{pattern:"packages/*"}]);let f={};n.exportTo(f);let p=J.join(this.context.cwd,_t.fileName);await le.changeFilePromise(p,`${JSON.stringify(f,null,2)} `,{automaticNewlines:!0});let h=[p],E=J.join(this.context.cwd,"README.md");if(le.existsSync(E)||(await le.writeFilePromise(E,`# ${j.stringifyIdent(n.name)} `),h.push(E)),!s||s.cwd===this.context.cwd){let C=J.join(this.context.cwd,Er.lockfile);le.existsSync(C)||(await le.writeFilePromise(C,""),h.push(C));let x=[".yarn/*","!.yarn/patches","!.yarn/plugins","!.yarn/releases","!.yarn/sdks","!.yarn/versions","","# Whether you use PnP or not, the node_modules folder is often used to store","# build artifacts that should be gitignored","node_modules","","# Swap the comments on the following lines if you wish to use zero-installs","# In that case, don't forget to run `yarn config set enableGlobalCache false`!","# Documentation here: https://yarnpkg.com/features/caching#zero-installs","","#!.yarn/cache",".pnp.*"].map(ue=>`${ue} `).join(""),I=J.join(this.context.cwd,".gitignore");le.existsSync(I)||(await le.writeFilePromise(I,x),h.push(I));let O=["/.yarn/** linguist-vendored","/.yarn/releases/* binary","/.yarn/plugins/**/* binary","/.pnp.* binary linguist-generated"].map(ue=>`${ue} `).join(""),U=J.join(this.context.cwd,".gitattributes");le.existsSync(U)||(await le.writeFilePromise(U,O),h.push(U));let V={"*":{charset:"utf-8",endOfLine:"lf",indentSize:2,indentStyle:"space",insertFinalNewline:!0}};Ge.mergeIntoTarget(V,r.get("initEditorConfig"));let te=`root = true `;for(let[ue,ae]of Object.entries(V)){te+=` [${ue}] `;for(let[ge,Ae]of Object.entries(ae)){let Ce=ge.replace(/[A-Z]/g,Ee=>`_${Ee.toLowerCase()}`);te+=`${Ce} = ${Ae} `}}let ie=J.join(this.context.cwd,".editorconfig");le.existsSync(ie)||(await le.writeFilePromise(ie,te),h.push(ie)),await this.cli.run(["install"],{quiet:!0}),await this.initialize(),le.existsSync(J.join(this.context.cwd,".git"))||(await qr.execvp("git",["init"],{cwd:this.context.cwd}),await qr.execvp("git",["add","--",...h],{cwd:this.context.cwd}),await qr.execvp("git",["commit","--allow-empty","-m","First commit"],{cwd:this.context.cwd}))}}};var JC=class extends J0{constructor(){super(...arguments);this.initializer=he.String();this.argv=he.Proxy()}static{this.paths=[["init"]]}async initialize(){this.context.stdout.write(` `),await this.cli.run(["dlx",this.initializer,...this.argv],{quiet:!0})}};var Qat={configuration:{initScope:{description:"Scope used when creating packages via the init command",type:"STRING",default:null},initFields:{description:"Additional fields to set when creating packages via the init command",type:"MAP",valueDefinition:{description:"",type:"ANY"}},initEditorConfig:{description:"Extra rules to define in the generator editorconfig",type:"MAP",valueDefinition:{description:"",type:"ANY"}}},commands:[J0,JC]},Rat=Qat;var pW={};Vt(pW,{SearchCommand:()=>Aw,UpgradeInteractiveCommand:()=>pw,default:()=>jpt});qe();var Bme=et(Ie("os"));function KC({stdout:e}){if(Bme.default.endianness()==="BE")throw new Error("Interactive commands cannot be used on big-endian systems because ink depends on yoga-layout-prebuilt which only supports little-endian architectures");if(!e.isTTY)throw new Error("Interactive commands can only be used inside a TTY environment")}Yt();var Nye=et(F9()),N9={appId:"OFCNCOG2CU",apiKey:"6fe4476ee5a1832882e326b506d14126",indexName:"npm-search"},xct=(0,Nye.default)(N9.appId,N9.apiKey).initIndex(N9.indexName),O9=async(e,t=0)=>await xct.search(e,{analyticsTags:["yarn-plugin-interactive-tools"],attributesToRetrieve:["name","version","owner","repository","humanDownloadsLast30Days"],page:t,hitsPerPage:10});var CD=["regular","dev","peer"],Aw=class extends At{static{this.paths=[["search"]]}static{this.usage=at.Usage({category:"Interactive commands",description:"open the search interface",details:` This command opens a fullscreen terminal interface where you can search for and install packages from the npm registry. `,examples:[["Open the search window","yarn search"]]})}async execute(){KC(this.context);let{Gem:t}=await Promise.resolve().then(()=>(xF(),rW)),{ScrollableItems:r}=await Promise.resolve().then(()=>(TF(),RF)),{useKeypress:s}=await Promise.resolve().then(()=>(fw(),nW)),{useMinistore:a}=await Promise.resolve().then(()=>(lW(),aW)),{renderForm:n}=await Promise.resolve().then(()=>(LF(),OF)),{default:c}=await Promise.resolve().then(()=>et(Iwe())),{Box:f,Text:p}=await Promise.resolve().then(()=>et(qc())),{default:h,useEffect:E,useState:C}=await Promise.resolve().then(()=>et(dn())),S=await ze.find(this.context.cwd,this.context.plugins),x=()=>h.createElement(f,{flexDirection:"row"},h.createElement(f,{flexDirection:"column",width:48},h.createElement(f,null,h.createElement(p,null,"Press ",h.createElement(p,{bold:!0,color:"cyanBright"},""),"/",h.createElement(p,{bold:!0,color:"cyanBright"},"")," to move between packages.")),h.createElement(f,null,h.createElement(p,null,"Press ",h.createElement(p,{bold:!0,color:"cyanBright"},"")," to select a package.")),h.createElement(f,null,h.createElement(p,null,"Press ",h.createElement(p,{bold:!0,color:"cyanBright"},"")," again to change the target."))),h.createElement(f,{flexDirection:"column"},h.createElement(f,{marginLeft:1},h.createElement(p,null,"Press ",h.createElement(p,{bold:!0,color:"cyanBright"},"")," to install the selected packages.")),h.createElement(f,{marginLeft:1},h.createElement(p,null,"Press ",h.createElement(p,{bold:!0,color:"cyanBright"},"")," to abort.")))),I=()=>h.createElement(h.Fragment,null,h.createElement(f,{width:15},h.createElement(p,{bold:!0,underline:!0,color:"gray"},"Owner")),h.createElement(f,{width:11},h.createElement(p,{bold:!0,underline:!0,color:"gray"},"Version")),h.createElement(f,{width:10},h.createElement(p,{bold:!0,underline:!0,color:"gray"},"Downloads"))),T=()=>h.createElement(f,{width:17},h.createElement(p,{bold:!0,underline:!0,color:"gray"},"Target")),O=({hit:Ae,active:Ce})=>{let[Ee,d]=a(Ae.name,null);s({active:Ce},(me,ce)=>{if(ce.name!=="space")return;if(!Ee){d(CD[0]);return}let Z=CD.indexOf(Ee)+1;Z===CD.length?d(null):d(CD[Z])},[Ee,d]);let Se=j.parseIdent(Ae.name),Be=j.prettyIdent(S,Se);return h.createElement(f,null,h.createElement(f,{width:45},h.createElement(p,{bold:!0,wrap:"wrap"},Be)),h.createElement(f,{width:14,marginLeft:1},h.createElement(p,{bold:!0,wrap:"truncate"},Ae.owner.name)),h.createElement(f,{width:10,marginLeft:1},h.createElement(p,{italic:!0,wrap:"truncate"},Ae.version)),h.createElement(f,{width:16,marginLeft:1},h.createElement(p,null,Ae.humanDownloadsLast30Days)))},U=({name:Ae,active:Ce})=>{let[Ee]=a(Ae,null),d=j.parseIdent(Ae);return h.createElement(f,null,h.createElement(f,{width:47},h.createElement(p,{bold:!0}," - ",j.prettyIdent(S,d))),CD.map(Se=>h.createElement(f,{key:Se,width:14,marginLeft:1},h.createElement(p,null," ",h.createElement(t,{active:Ee===Se})," ",h.createElement(p,{bold:!0},Se)))))},V=()=>h.createElement(f,{marginTop:1},h.createElement(p,null,"Powered by Algolia.")),ie=await n(({useSubmit:Ae})=>{let Ce=a();Ae(Ce);let Ee=Array.from(Ce.keys()).filter(_=>Ce.get(_)!==null),[d,Se]=C(""),[Be,me]=C(0),[ce,Z]=C([]),De=_=>{_.match(/\t| /)||Se(_)},Qe=async()=>{me(0);let _=await O9(d);_.query===d&&Z(_.hits)},st=async()=>{let _=await O9(d,Be+1);_.query===d&&_.page-1===Be&&(me(_.page),Z([...ce,..._.hits]))};return E(()=>{d?Qe():Z([])},[d]),h.createElement(f,{flexDirection:"column"},h.createElement(x,null),h.createElement(f,{flexDirection:"row",marginTop:1},h.createElement(p,{bold:!0},"Search: "),h.createElement(f,{width:41},h.createElement(c,{value:d,onChange:De,placeholder:"i.e. babel, webpack, react...",showCursor:!1})),h.createElement(I,null)),ce.length?h.createElement(r,{radius:2,loop:!1,children:ce.map(_=>h.createElement(O,{key:_.name,hit:_,active:!1})),willReachEnd:st}):h.createElement(p,{color:"gray"},"Start typing..."),h.createElement(f,{flexDirection:"row",marginTop:1},h.createElement(f,{width:49},h.createElement(p,{bold:!0},"Selected:")),h.createElement(T,null)),Ee.length?Ee.map(_=>h.createElement(U,{key:_,name:_,active:!1})):h.createElement(p,{color:"gray"},"No selected packages..."),h.createElement(V,null))},{},{stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr});if(typeof ie>"u")return 1;let ue=Array.from(ie.keys()).filter(Ae=>ie.get(Ae)==="regular"),ae=Array.from(ie.keys()).filter(Ae=>ie.get(Ae)==="dev"),ge=Array.from(ie.keys()).filter(Ae=>ie.get(Ae)==="peer");return ue.length&&await this.cli.run(["add",...ue]),ae.length&&await this.cli.run(["add","--dev",...ae]),ge&&await this.cli.run(["add","--peer",...ge]),0}};qe();Yt();fG();var bwe=et(pi());Al();var Dwe=/^((?:[\^~]|>=?)?)([0-9]+)(\.[0-9]+)(\.[0-9]+)((?:-\S+)?)$/;function Pwe(e,t){return e.length>0?[e.slice(0,t)].concat(Pwe(e.slice(t),t)):[]}var pw=class extends At{constructor(){super(...arguments);this.mode=he.String("--mode",{description:"Change what artifacts installs generate",validator:ks(Oa)})}static{this.paths=[["upgrade-interactive"]]}static{this.usage=at.Usage({category:"Interactive commands",description:"open the upgrade interface",details:"\n This command opens a fullscreen terminal interface where you can see any out of date packages used by your application, their status compared to the latest versions available on the remote registry, and select packages to upgrade.\n\n If the `--mode=` option is set, Yarn will change which artifacts are generated. The modes currently supported are:\n\n - `skip-build` will not run the build scripts at all. Note that this is different from setting `enableScripts` to false because the latter will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.\n\n - `update-lockfile` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.\n ",examples:[["Open the upgrade window","yarn upgrade-interactive"]]})}async execute(){KC(this.context);let{ItemOptions:r}=await Promise.resolve().then(()=>(Swe(),vwe)),{Pad:s}=await Promise.resolve().then(()=>(AW(),Bwe)),{ScrollableItems:a}=await Promise.resolve().then(()=>(TF(),RF)),{useMinistore:n,useMinistoreSetAll:c}=await Promise.resolve().then(()=>(lW(),aW)),{useKeypress:f}=await Promise.resolve().then(()=>(fw(),nW)),{renderForm:p}=await Promise.resolve().then(()=>(LF(),OF)),{Box:h,Text:E}=await Promise.resolve().then(()=>et(qc())),{default:C,useCallback:S,useEffect:x,useRef:I,useState:T}=await Promise.resolve().then(()=>et(dn())),O=await ze.find(this.context.cwd,this.context.plugins),{project:U,workspace:V}=await Rt.find(O,this.context.cwd),te=await Kr.find(O);if(!V)throw new ar(U.cwd,this.context.cwd);await U.restoreInstallState({restoreResolutions:!1});let ie=this.context.stdout.rows-8,ue=(Z,De)=>{let Qe=o0e(Z,De),st="";for(let _ of Qe)_.added?st+=pe.pretty(O,_.value,"green"):_.removed||(st+=_.value);return st},ae=(Z,De)=>{if(Z===De)return De;let Qe=j.parseRange(Z),st=j.parseRange(De),_=Qe.selector.match(Dwe),tt=st.selector.match(Dwe);if(!_||!tt)return ue(Z,De);let Ne=["gray","red","yellow","green","magenta"],ke=null,be="";for(let je=1;je{let st=await $u.fetchDescriptorFrom(Z,Qe,{project:U,cache:te,preserveModifier:De,workspace:V});return st!==null?st.range:Z.range},Ae=async Z=>{let De=bwe.default.valid(Z.range)?`^${Z.range}`:Z.range,[Qe,st]=await Promise.all([ge(Z,Z.range,De).catch(()=>null),ge(Z,Z.range,"latest").catch(()=>null)]),_=[{value:null,label:Z.range}];return Qe&&Qe!==Z.range?_.push({value:Qe,label:ae(Z.range,Qe)}):_.push({value:null,label:""}),st&&st!==Qe&&st!==Z.range?_.push({value:st,label:ae(Z.range,st)}):_.push({value:null,label:""}),_},Ce=()=>C.createElement(h,{flexDirection:"row"},C.createElement(h,{flexDirection:"column",width:49},C.createElement(h,{marginLeft:1},C.createElement(E,null,"Press ",C.createElement(E,{bold:!0,color:"cyanBright"},""),"/",C.createElement(E,{bold:!0,color:"cyanBright"},"")," to select packages.")),C.createElement(h,{marginLeft:1},C.createElement(E,null,"Press ",C.createElement(E,{bold:!0,color:"cyanBright"},""),"/",C.createElement(E,{bold:!0,color:"cyanBright"},"")," to select versions.")),C.createElement(h,{marginLeft:1},C.createElement(E,null,"Press ",C.createElement(E,{bold:!0,color:"cyanBright"},"c"),"/",C.createElement(E,{bold:!0,color:"cyanBright"},"r"),"/",C.createElement(E,{bold:!0,color:"cyanBright"},"l")," to select all ",C.createElement(E,{bold:!0,color:"cyanBright"},"current"),"/",C.createElement(E,{bold:!0,color:"cyanBright"},"range"),"/",C.createElement(E,{bold:!0,color:"cyanBright"},"latest"),"."))),C.createElement(h,{flexDirection:"column"},C.createElement(h,{marginLeft:1},C.createElement(E,null,"Press ",C.createElement(E,{bold:!0,color:"cyanBright"},"")," to install.")),C.createElement(h,{marginLeft:1},C.createElement(E,null,"Press ",C.createElement(E,{bold:!0,color:"cyanBright"},"")," to abort.")))),Ee=()=>C.createElement(h,{flexDirection:"row",paddingTop:1,paddingBottom:1},C.createElement(h,{width:50},C.createElement(E,{bold:!0},C.createElement(E,{color:"greenBright"},"?")," Pick the packages you want to upgrade.")),C.createElement(h,{width:17},C.createElement(E,{bold:!0,underline:!0,color:"gray"},"Current")),C.createElement(h,{width:17},C.createElement(E,{bold:!0,underline:!0,color:"gray"},"Range")),C.createElement(h,{width:17},C.createElement(E,{bold:!0,underline:!0,color:"gray"},"Latest"))),d=({active:Z,descriptor:De,suggestions:Qe})=>{let[st,_]=n(De.descriptorHash,null),tt=j.stringifyIdent(De),Ne=Math.max(0,45-tt.length);return C.createElement(C.Fragment,null,C.createElement(h,null,C.createElement(h,{width:45},C.createElement(E,{bold:!0},j.prettyIdent(O,De)),C.createElement(s,{active:Z,length:Ne})),C.createElement(r,{active:Z,options:Qe,value:st,skewer:!0,onChange:_,sizes:[17,17,17]})))},Se=({dependencies:Z})=>{let De=c(),[Qe,st]=T(Z.map(()=>null)),_=I(!0),tt=async ke=>{let be=await Ae(ke);return be.filter(je=>je.label!=="").length<=1?null:{descriptor:ke,suggestions:be}};x(()=>()=>{_.current=!1},[]),x(()=>{let ke=Math.trunc(ie*1.75),be=Z.slice(0,ke),je=Z.slice(ke),Re=Pwe(je,ie),ct=be.map(tt).reduce(async(Me,P)=>{await Me;let w=await P;w!==null&&_.current&&st(b=>{let y=b.findIndex(z=>z===null),F=[...b];return F[y]=w,F})},Promise.resolve());Re.reduce((Me,P)=>Promise.all(P.map(w=>Promise.resolve().then(()=>tt(w)))).then(async w=>{w=w.filter(b=>b!==null),await Me,_.current&&st(b=>{let y=b.findIndex(F=>F===null);return b.slice(0,y).concat(w).concat(b.slice(y+w.length))})}),ct).then(()=>{_.current&&st(Me=>Me.filter(P=>P!==null))})},[]);let Ne=S(ke=>{if(ke!=="c"&&ke!=="r"&&ke!=="l")return;let be=[];for(let je of Qe){if(je===null)continue;let Re;ke==="c"?Re=null:ke==="r"?Re=je.suggestions[1].value:Re=je.suggestions[2].value??je.suggestions[1].value,be.push([je.descriptor.descriptorHash,Re])}De(be)},[Qe,De]);return f({active:!0},Ne,[Ne]),Qe.length?C.createElement(a,{radius:ie>>1,children:Qe.map((ke,be)=>ke!==null?C.createElement(d,{key:be,active:!1,descriptor:ke.descriptor,suggestions:ke.suggestions}):C.createElement(E,{key:be},"Loading..."))}):C.createElement(E,null,"No upgrades found")},me=await p(({useSubmit:Z})=>{Z(n());let De=new Map;for(let st of U.workspaces)for(let _ of["dependencies","devDependencies"])for(let tt of st.manifest[_].values())U.tryWorkspaceByDescriptor(tt)===null&&(tt.range.startsWith("link:")||De.set(tt.descriptorHash,tt));let Qe=Ge.sortMap(De.values(),st=>j.stringifyDescriptor(st));return C.createElement(h,{flexDirection:"column"},C.createElement(Ce,null),C.createElement(Ee,null),C.createElement(Se,{dependencies:Qe}))},{},{stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr});if(typeof me>"u")return 1;let ce=!1;for(let Z of U.workspaces)for(let De of["dependencies","devDependencies"]){let Qe=Z.manifest[De];for(let st of Qe.values()){let _=me.get(st.descriptorHash);typeof _<"u"&&_!==null&&(Qe.set(st.identHash,j.makeDescriptor(st,_)),ce=!0)}}return ce?await U.installWithNewReport({quiet:this.context.quiet,stdout:this.context.stdout},{cache:te,mode:this.mode}):0}};var Hpt={commands:[Aw,pw]},jpt=Hpt;var dW={};Vt(dW,{default:()=>Ypt});qe();var BD="jsr:";qe();qe();function hw(e){let t=e.range.slice(4);if(kr.validRange(t))return j.makeDescriptor(e,`npm:${j.stringifyIdent(j.wrapIdentIntoScope(e,"jsr"))}@${t}`);let r=j.tryParseDescriptor(t,!0);if(r!==null)return j.makeDescriptor(e,`npm:${j.stringifyIdent(j.wrapIdentIntoScope(r,"jsr"))}@${r.range}`);throw new Error(`Invalid range: ${e.range}`)}function dw(e){return j.makeLocator(j.wrapIdentIntoScope(e,"jsr"),`npm:${e.reference.slice(4)}`)}function hW(e){return j.makeLocator(j.unwrapIdentFromScope(e,"jsr"),`jsr:${e.reference.slice(4)}`)}var MF=class{supports(t,r){return t.reference.startsWith(BD)}getLocalPath(t,r){let s=dw(t);return r.fetcher.getLocalPath(s,r)}fetch(t,r){let s=dw(t);return r.fetcher.fetch(s,r)}};var UF=class{supportsDescriptor(t,r){return!!t.range.startsWith(BD)}supportsLocator(t,r){return!!t.reference.startsWith(BD)}shouldPersistResolution(t,r){let s=dw(t);return r.resolver.shouldPersistResolution(s,r)}bindDescriptor(t,r,s){return t}getResolutionDependencies(t,r){return{inner:hw(t)}}async getCandidates(t,r,s){let a=s.project.configuration.normalizeDependency(hw(t));return(await s.resolver.getCandidates(a,r,s)).map(c=>hW(c))}async getSatisfying(t,r,s,a){let n=a.project.configuration.normalizeDependency(hw(t));return a.resolver.getSatisfying(n,r,s,a)}async resolve(t,r){let s=dw(t),a=await r.resolver.resolve(s,r);return{...a,...hW(a)}}};var Gpt=["dependencies","devDependencies","peerDependencies"];function qpt(e,t){for(let r of Gpt)for(let s of e.manifest.getForScope(r).values()){if(!s.range.startsWith("jsr:"))continue;let a=hw(s),n=r==="dependencies"?j.makeDescriptor(s,"unknown"):null,c=n!==null&&e.manifest.ensureDependencyMeta(n).optional?"optionalDependencies":r;t[c][j.stringifyIdent(s)]=a.range}}var Wpt={hooks:{beforeWorkspacePacking:qpt},resolvers:[UF],fetchers:[MF]},Ypt=Wpt;var gW={};Vt(gW,{LinkFetcher:()=>vD,LinkResolver:()=>SD,PortalFetcher:()=>DD,PortalResolver:()=>bD,default:()=>Jpt});qe();Dt();var rh="portal:",nh="link:";var vD=class{supports(t,r){return!!t.reference.startsWith(nh)}getLocalPath(t,r){let{parentLocator:s,path:a}=j.parseFileStyleRange(t.reference,{protocol:nh});if(J.isAbsolute(a))return a;let n=r.fetcher.getLocalPath(s,r);return n===null?null:J.resolve(n,a)}async fetch(t,r){let{parentLocator:s,path:a}=j.parseFileStyleRange(t.reference,{protocol:nh}),n=J.isAbsolute(a)?{packageFs:new bn(vt.root),prefixPath:vt.dot,localPath:vt.root}:await r.fetcher.fetch(s,r),c=n.localPath?{packageFs:new bn(vt.root),prefixPath:J.relative(vt.root,n.localPath),localPath:vt.root}:n;n!==c&&n.releaseFs&&n.releaseFs();let f=c.packageFs,p=J.resolve(c.localPath??c.packageFs.getRealPath(),c.prefixPath,a);return n.localPath?{packageFs:new bn(p,{baseFs:f}),releaseFs:c.releaseFs,prefixPath:vt.dot,discardFromLookup:!0,localPath:p}:{packageFs:new qf(p,{baseFs:f}),releaseFs:c.releaseFs,prefixPath:vt.dot,discardFromLookup:!0}}};qe();Dt();var SD=class{supportsDescriptor(t,r){return!!t.range.startsWith(nh)}supportsLocator(t,r){return!!t.reference.startsWith(nh)}shouldPersistResolution(t,r){return!1}bindDescriptor(t,r,s){return j.bindDescriptor(t,{locator:j.stringifyLocator(r)})}getResolutionDependencies(t,r){return{}}async getCandidates(t,r,s){let a=t.range.slice(nh.length);return[j.makeLocator(t,`${nh}${fe.toPortablePath(a)}`)]}async getSatisfying(t,r,s,a){let[n]=await this.getCandidates(t,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(t,r){return{...t,version:"0.0.0",languageName:r.project.configuration.get("defaultLanguageName"),linkType:"SOFT",conditions:null,dependencies:new Map,peerDependencies:new Map,dependenciesMeta:new Map,peerDependenciesMeta:new Map,bin:new Map}}};qe();Dt();var DD=class{supports(t,r){return!!t.reference.startsWith(rh)}getLocalPath(t,r){let{parentLocator:s,path:a}=j.parseFileStyleRange(t.reference,{protocol:rh});if(J.isAbsolute(a))return a;let n=r.fetcher.getLocalPath(s,r);return n===null?null:J.resolve(n,a)}async fetch(t,r){let{parentLocator:s,path:a}=j.parseFileStyleRange(t.reference,{protocol:rh}),n=J.isAbsolute(a)?{packageFs:new bn(vt.root),prefixPath:vt.dot,localPath:vt.root}:await r.fetcher.fetch(s,r),c=n.localPath?{packageFs:new bn(vt.root),prefixPath:J.relative(vt.root,n.localPath),localPath:vt.root}:n;n!==c&&n.releaseFs&&n.releaseFs();let f=c.packageFs,p=J.resolve(c.localPath??c.packageFs.getRealPath(),c.prefixPath,a);return n.localPath?{packageFs:new bn(p,{baseFs:f}),releaseFs:c.releaseFs,prefixPath:vt.dot,localPath:p}:{packageFs:new qf(p,{baseFs:f}),releaseFs:c.releaseFs,prefixPath:vt.dot}}};qe();qe();Dt();var bD=class{supportsDescriptor(t,r){return!!t.range.startsWith(rh)}supportsLocator(t,r){return!!t.reference.startsWith(rh)}shouldPersistResolution(t,r){return!1}bindDescriptor(t,r,s){return j.bindDescriptor(t,{locator:j.stringifyLocator(r)})}getResolutionDependencies(t,r){return{}}async getCandidates(t,r,s){let a=t.range.slice(rh.length);return[j.makeLocator(t,`${rh}${fe.toPortablePath(a)}`)]}async getSatisfying(t,r,s,a){let[n]=await this.getCandidates(t,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(t,r){if(!r.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let s=await r.fetchOptions.fetcher.fetch(t,r.fetchOptions),a=await Ge.releaseAfterUseAsync(async()=>await _t.find(s.prefixPath,{baseFs:s.packageFs}),s.releaseFs);return{...t,version:a.version||"0.0.0",languageName:a.languageName||r.project.configuration.get("defaultLanguageName"),linkType:"SOFT",conditions:a.getConditions(),dependencies:r.project.configuration.normalizeDependencyMap(a.dependencies),peerDependencies:a.peerDependencies,dependenciesMeta:a.dependenciesMeta,peerDependenciesMeta:a.peerDependenciesMeta,bin:a.bin}}};var Vpt={fetchers:[vD,DD],resolvers:[SD,bD]},Jpt=Vpt;var eY={};Vt(eY,{NodeModulesLinker:()=>jD,NodeModulesMode:()=>zW,PnpLooseLinker:()=>GD,default:()=>f0t});Dt();qe();Dt();Dt();var yW=(e,t)=>`${e}@${t}`,xwe=(e,t)=>{let r=t.indexOf("#"),s=r>=0?t.substring(r+1):t;return yW(e,s)};var Qwe=(e,t={})=>{let r=t.debugLevel||Number(process.env.NM_DEBUG_LEVEL||-1),s=t.check||r>=9,a=t.hoistingLimits||new Map,n={check:s,debugLevel:r,hoistingLimits:a,fastLookupPossible:!0},c;n.debugLevel>=0&&(c=Date.now());let f=tht(e,n),p=!1,h=0;do{let E=EW(f,[f],new Set([f.locator]),new Map,n);p=E.anotherRoundNeeded||E.isGraphChanged,n.fastLookupPossible=!1,h++}while(p);if(n.debugLevel>=0&&console.log(`hoist time: ${Date.now()-c}ms, rounds: ${h}`),n.debugLevel>=1){let E=PD(f);if(EW(f,[f],new Set([f.locator]),new Map,n).isGraphChanged)throw new Error(`The hoisting result is not terminal, prev tree: ${E}, next tree: ${PD(f)}`);let S=Rwe(f);if(S)throw new Error(`${S}, after hoisting finished: ${PD(f)}`)}return n.debugLevel>=2&&console.log(PD(f)),rht(f)},Kpt=e=>{let t=e[e.length-1],r=new Map,s=new Set,a=n=>{if(!s.has(n)){s.add(n);for(let c of n.hoistedDependencies.values())r.set(c.name,c);for(let c of n.dependencies.values())n.peerNames.has(c.name)||a(c)}};return a(t),r},zpt=e=>{let t=e[e.length-1],r=new Map,s=new Set,a=new Set,n=(c,f)=>{if(s.has(c))return;s.add(c);for(let h of c.hoistedDependencies.values())if(!f.has(h.name)){let E;for(let C of e)E=C.dependencies.get(h.name),E&&r.set(E.name,E)}let p=new Set;for(let h of c.dependencies.values())p.add(h.name);for(let h of c.dependencies.values())c.peerNames.has(h.name)||n(h,p)};return n(t,a),r},kwe=(e,t)=>{if(t.decoupled)return t;let{name:r,references:s,ident:a,locator:n,dependencies:c,originalDependencies:f,hoistedDependencies:p,peerNames:h,reasons:E,isHoistBorder:C,hoistPriority:S,dependencyKind:x,hoistedFrom:I,hoistedTo:T}=t,O={name:r,references:new Set(s),ident:a,locator:n,dependencies:new Map(c),originalDependencies:new Map(f),hoistedDependencies:new Map(p),peerNames:new Set(h),reasons:new Map(E),decoupled:!0,isHoistBorder:C,hoistPriority:S,dependencyKind:x,hoistedFrom:new Map(I),hoistedTo:new Map(T)},U=O.dependencies.get(r);return U&&U.ident==O.ident&&O.dependencies.set(r,O),e.dependencies.set(O.name,O),O},Xpt=(e,t)=>{let r=new Map([[e.name,[e.ident]]]);for(let a of e.dependencies.values())e.peerNames.has(a.name)||r.set(a.name,[a.ident]);let s=Array.from(t.keys());s.sort((a,n)=>{let c=t.get(a),f=t.get(n);if(f.hoistPriority!==c.hoistPriority)return f.hoistPriority-c.hoistPriority;{let p=c.dependents.size+c.peerDependents.size;return f.dependents.size+f.peerDependents.size-p}});for(let a of s){let n=a.substring(0,a.indexOf("@",1)),c=a.substring(n.length+1);if(!e.peerNames.has(n)){let f=r.get(n);f||(f=[],r.set(n,f)),f.indexOf(c)<0&&f.push(c)}}return r},mW=e=>{let t=new Set,r=(s,a=new Set)=>{if(!a.has(s)){a.add(s);for(let n of s.peerNames)if(!e.peerNames.has(n)){let c=e.dependencies.get(n);c&&!t.has(c)&&r(c,a)}t.add(s)}};for(let s of e.dependencies.values())e.peerNames.has(s.name)||r(s);return t},EW=(e,t,r,s,a,n=new Set)=>{let c=t[t.length-1];if(n.has(c))return{anotherRoundNeeded:!1,isGraphChanged:!1};n.add(c);let f=nht(c),p=Xpt(c,f),h=e==c?new Map:a.fastLookupPossible?Kpt(t):zpt(t),E,C=!1,S=!1,x=new Map(Array.from(p.entries()).map(([T,O])=>[T,O[0]])),I=new Map;do{let T=eht(e,t,r,h,x,p,s,I,a);T.isGraphChanged&&(S=!0),T.anotherRoundNeeded&&(C=!0),E=!1;for(let[O,U]of p)U.length>1&&!c.dependencies.has(O)&&(x.delete(O),U.shift(),x.set(O,U[0]),E=!0)}while(E);for(let T of c.dependencies.values())if(!c.peerNames.has(T.name)&&!r.has(T.locator)){r.add(T.locator);let O=EW(e,[...t,T],r,I,a);O.isGraphChanged&&(S=!0),O.anotherRoundNeeded&&(C=!0),r.delete(T.locator)}return{anotherRoundNeeded:C,isGraphChanged:S}},Zpt=e=>{for(let[t,r]of e.dependencies)if(!e.peerNames.has(t)&&r.ident!==e.ident)return!0;return!1},$pt=(e,t,r,s,a,n,c,f,{outputReason:p,fastLookupPossible:h})=>{let E,C=null,S=new Set;p&&(E=`${Array.from(t).map(O=>Bo(O)).join("\u2192")}`);let x=r[r.length-1],T=!(s.ident===x.ident);if(p&&!T&&(C="- self-reference"),T&&(T=s.dependencyKind!==1,p&&!T&&(C="- workspace")),T&&s.dependencyKind===2&&(T=!Zpt(s),p&&!T&&(C="- external soft link with unhoisted dependencies")),T&&(T=!e.peerNames.has(s.name),p&&!T&&(C=`- cannot shadow peer: ${Bo(e.originalDependencies.get(s.name).locator)} at ${E}`)),T){let O=!1,U=a.get(s.name);if(O=!U||U.ident===s.ident,p&&!O&&(C=`- filled by: ${Bo(U.locator)} at ${E}`),O)for(let V=r.length-1;V>=1;V--){let ie=r[V].dependencies.get(s.name);if(ie&&ie.ident!==s.ident){O=!1;let ue=f.get(x);ue||(ue=new Set,f.set(x,ue)),ue.add(s.name),p&&(C=`- filled by ${Bo(ie.locator)} at ${r.slice(0,V).map(ae=>Bo(ae.locator)).join("\u2192")}`);break}}T=O}if(T&&(T=n.get(s.name)===s.ident,p&&!T&&(C=`- filled by: ${Bo(c.get(s.name)[0])} at ${E}`)),T){let O=!0,U=new Set(s.peerNames);for(let V=r.length-1;V>=1;V--){let te=r[V];for(let ie of U){if(te.peerNames.has(ie)&&te.originalDependencies.has(ie))continue;let ue=te.dependencies.get(ie);ue&&e.dependencies.get(ie)!==ue&&(V===r.length-1?S.add(ue):(S=null,O=!1,p&&(C=`- peer dependency ${Bo(ue.locator)} from parent ${Bo(te.locator)} was not hoisted to ${E}`))),U.delete(ie)}if(!O)break}T=O}if(T&&!h)for(let O of s.hoistedDependencies.values()){let U=a.get(O.name)||e.dependencies.get(O.name);if(!U||O.ident!==U.ident){T=!1,p&&(C=`- previously hoisted dependency mismatch, needed: ${Bo(O.locator)}, available: ${Bo(U?.locator)}`);break}}return S!==null&&S.size>0?{isHoistable:2,dependsOn:S,reason:C}:{isHoistable:T?0:1,reason:C}},_F=e=>`${e.name}@${e.locator}`,eht=(e,t,r,s,a,n,c,f,p)=>{let h=t[t.length-1],E=new Set,C=!1,S=!1,x=(U,V,te,ie,ue)=>{if(E.has(ie))return;let ae=[...V,_F(ie)],ge=[...te,_F(ie)],Ae=new Map,Ce=new Map;for(let me of mW(ie)){let ce=$pt(h,r,[h,...U,ie],me,s,a,n,f,{outputReason:p.debugLevel>=2,fastLookupPossible:p.fastLookupPossible});if(Ce.set(me,ce),ce.isHoistable===2)for(let Z of ce.dependsOn){let De=Ae.get(Z.name)||new Set;De.add(me.name),Ae.set(Z.name,De)}}let Ee=new Set,d=(me,ce,Z)=>{if(!Ee.has(me)){Ee.add(me),Ce.set(me,{isHoistable:1,reason:Z});for(let De of Ae.get(me.name)||[])d(ie.dependencies.get(De),ce,p.debugLevel>=2?`- peer dependency ${Bo(me.locator)} from parent ${Bo(ie.locator)} was not hoisted`:"")}};for(let[me,ce]of Ce)ce.isHoistable===1&&d(me,ce,ce.reason);let Se=!1;for(let me of Ce.keys())if(!Ee.has(me)){S=!0;let ce=c.get(ie);ce&&ce.has(me.name)&&(C=!0),Se=!0,ie.dependencies.delete(me.name),ie.hoistedDependencies.set(me.name,me),ie.reasons.delete(me.name);let Z=h.dependencies.get(me.name);if(p.debugLevel>=2){let De=Array.from(V).concat([ie.locator]).map(st=>Bo(st)).join("\u2192"),Qe=h.hoistedFrom.get(me.name);Qe||(Qe=[],h.hoistedFrom.set(me.name,Qe)),Qe.push(De),ie.hoistedTo.set(me.name,Array.from(t).map(st=>Bo(st.locator)).join("\u2192"))}if(!Z)h.ident!==me.ident&&(h.dependencies.set(me.name,me),ue.add(me));else for(let De of me.references)Z.references.add(De)}if(ie.dependencyKind===2&&Se&&(C=!0),p.check){let me=Rwe(e);if(me)throw new Error(`${me}, after hoisting dependencies of ${[h,...U,ie].map(ce=>Bo(ce.locator)).join("\u2192")}: ${PD(e)}`)}let Be=mW(ie);for(let me of Be)if(Ee.has(me)){let ce=Ce.get(me);if((a.get(me.name)===me.ident||!ie.reasons.has(me.name))&&ce.isHoistable!==0&&ie.reasons.set(me.name,ce.reason),!me.isHoistBorder&&ge.indexOf(_F(me))<0){E.add(ie);let De=kwe(ie,me);x([...U,ie],ae,ge,De,T),E.delete(ie)}}},I,T=new Set(mW(h)),O=Array.from(t).map(U=>_F(U));do{I=T,T=new Set;for(let U of I){if(U.locator===h.locator||U.isHoistBorder)continue;let V=kwe(h,U);x([],Array.from(r),O,V,T)}}while(T.size>0);return{anotherRoundNeeded:C,isGraphChanged:S}},Rwe=e=>{let t=[],r=new Set,s=new Set,a=(n,c,f)=>{if(r.has(n)||(r.add(n),s.has(n)))return;let p=new Map(c);for(let h of n.dependencies.values())n.peerNames.has(h.name)||p.set(h.name,h);for(let h of n.originalDependencies.values()){let E=p.get(h.name),C=()=>`${Array.from(s).concat([n]).map(S=>Bo(S.locator)).join("\u2192")}`;if(n.peerNames.has(h.name)){let S=c.get(h.name);(S!==E||!S||S.ident!==h.ident)&&t.push(`${C()} - broken peer promise: expected ${h.ident} but found ${S&&S.ident}`)}else{let S=f.hoistedFrom.get(n.name),x=n.hoistedTo.get(h.name),I=`${S?` hoisted from ${S.join(", ")}`:""}`,T=`${x?` hoisted to ${x}`:""}`,O=`${C()}${I}`;E?E.ident!==h.ident&&t.push(`${O} - broken require promise for ${h.name}${T}: expected ${h.ident}, but found: ${E.ident}`):t.push(`${O} - broken require promise: no required dependency ${h.name}${T} found`)}}s.add(n);for(let h of n.dependencies.values())n.peerNames.has(h.name)||a(h,p,n);s.delete(n)};return a(e,e.dependencies,e),t.join(` `)},tht=(e,t)=>{let{identName:r,name:s,reference:a,peerNames:n}=e,c={name:s,references:new Set([a]),locator:yW(r,a),ident:xwe(r,a),dependencies:new Map,originalDependencies:new Map,hoistedDependencies:new Map,peerNames:new Set(n),reasons:new Map,decoupled:!0,isHoistBorder:!0,hoistPriority:0,dependencyKind:1,hoistedFrom:new Map,hoistedTo:new Map},f=new Map([[e,c]]),p=(h,E)=>{let C=f.get(h),S=!!C;if(!C){let{name:x,identName:I,reference:T,peerNames:O,hoistPriority:U,dependencyKind:V}=h,te=t.hoistingLimits.get(E.locator);C={name:x,references:new Set([T]),locator:yW(I,T),ident:xwe(I,T),dependencies:new Map,originalDependencies:new Map,hoistedDependencies:new Map,peerNames:new Set(O),reasons:new Map,decoupled:!0,isHoistBorder:te?te.has(x):!1,hoistPriority:U||0,dependencyKind:V||0,hoistedFrom:new Map,hoistedTo:new Map},f.set(h,C)}if(E.dependencies.set(h.name,C),E.originalDependencies.set(h.name,C),S){let x=new Set,I=T=>{if(!x.has(T)){x.add(T),T.decoupled=!1;for(let O of T.dependencies.values())T.peerNames.has(O.name)||I(O)}};I(C)}else for(let x of h.dependencies)p(x,C)};for(let h of e.dependencies)p(h,c);return c},IW=e=>e.substring(0,e.indexOf("@",1)),rht=e=>{let t={name:e.name,identName:IW(e.locator),references:new Set(e.references),dependencies:new Set},r=new Set([e]),s=(a,n,c)=>{let f=r.has(a),p;if(n===a)p=c;else{let{name:h,references:E,locator:C}=a;p={name:h,identName:IW(C),references:E,dependencies:new Set}}if(c.dependencies.add(p),!f){r.add(a);for(let h of a.dependencies.values())a.peerNames.has(h.name)||s(h,a,p);r.delete(a)}};for(let a of e.dependencies.values())s(a,e,t);return t},nht=e=>{let t=new Map,r=new Set([e]),s=c=>`${c.name}@${c.ident}`,a=c=>{let f=s(c),p=t.get(f);return p||(p={dependents:new Set,peerDependents:new Set,hoistPriority:0},t.set(f,p)),p},n=(c,f)=>{let p=!!r.has(f);if(a(f).dependents.add(c.ident),!p){r.add(f);for(let E of f.dependencies.values()){let C=a(E);C.hoistPriority=Math.max(C.hoistPriority,E.hoistPriority),f.peerNames.has(E.name)?C.peerDependents.add(f.ident):n(f,E)}}};for(let c of e.dependencies.values())e.peerNames.has(c.name)||n(e,c);return t},Bo=e=>{if(!e)return"none";let t=e.indexOf("@",1),r=e.substring(0,t);r.endsWith("$wsroot$")&&(r=`wh:${r.replace("$wsroot$","")}`);let s=e.substring(t+1);if(s==="workspace:.")return".";if(s){let a=(s.indexOf("#")>0?s.split("#")[1]:s).replace("npm:","");return s.startsWith("virtual")&&(r=`v:${r}`),a.startsWith("workspace")&&(r=`w:${r}`,a=""),`${r}${a?`@${a}`:""}`}else return`${r}`};var PD=e=>{let t=0,r=(a,n,c="")=>{if(t>5e4||n.has(a))return"";t++;let f=Array.from(a.dependencies.values()).sort((h,E)=>h.name===E.name?0:h.name>E.name?1:-1),p="";n.add(a);for(let h=0;h":"")+(S!==E.name?`a:${E.name}:`:"")+Bo(E.locator)+(C?` ${C}`:"")} `,p+=r(E,n,`${c}${h5e4?` Tree is too large, part of the tree has been dunped `:"")};var xD=(s=>(s.WORKSPACES="workspaces",s.DEPENDENCIES="dependencies",s.NONE="none",s))(xD||{}),Twe="node_modules",ed="$wsroot$";var kD=(e,t)=>{let{packageTree:r,hoistingLimits:s,errors:a,preserveSymlinksRequired:n}=sht(e,t),c=null;if(a.length===0){let f=Qwe(r,{hoistingLimits:s});c=aht(e,f,t)}return{tree:c,errors:a,preserveSymlinksRequired:n}},dA=e=>`${e.name}@${e.reference}`,wW=e=>{let t=new Map;for(let[r,s]of e.entries())if(!s.dirList){let a=t.get(s.locator);a||(a={target:s.target,linkType:s.linkType,locations:[],aliases:s.aliases},t.set(s.locator,a)),a.locations.push(r)}for(let r of t.values())r.locations=r.locations.sort((s,a)=>{let n=s.split(J.delimiter).length,c=a.split(J.delimiter).length;return a===s?0:n!==c?c-n:a>s?1:-1});return t},Fwe=(e,t)=>{let r=j.isVirtualLocator(e)?j.devirtualizeLocator(e):e,s=j.isVirtualLocator(t)?j.devirtualizeLocator(t):t;return j.areLocatorsEqual(r,s)},CW=(e,t,r,s)=>{if(e.linkType!=="SOFT")return!1;let a=fe.toPortablePath(r.resolveVirtual&&t.reference&&t.reference.startsWith("virtual:")?r.resolveVirtual(e.packageLocation):e.packageLocation);return J.contains(s,a)===null},iht=e=>{let t=e.getPackageInformation(e.topLevel);if(t===null)throw new Error("Assertion failed: Expected the top-level package to have been registered");if(e.findPackageLocator(t.packageLocation)===null)throw new Error("Assertion failed: Expected the top-level package to have a physical locator");let s=fe.toPortablePath(t.packageLocation.slice(0,-1)),a=new Map,n={children:new Map},c=e.getDependencyTreeRoots(),f=new Map,p=new Set,h=(S,x)=>{let I=dA(S);if(p.has(I))return;p.add(I);let T=e.getPackageInformation(S);if(T){let O=x?dA(x):"";if(dA(S)!==O&&T.linkType==="SOFT"&&!S.reference.startsWith("link:")&&!CW(T,S,e,s)){let U=Nwe(T,S,e);(!f.get(U)||S.reference.startsWith("workspace:"))&&f.set(U,S)}for(let[U,V]of T.packageDependencies)V!==null&&(T.packagePeers.has(U)||h(e.getLocator(U,V),S))}};for(let S of c)h(S,null);let E=s.split(J.sep);for(let S of f.values()){let x=e.getPackageInformation(S),T=fe.toPortablePath(x.packageLocation.slice(0,-1)).split(J.sep).slice(E.length),O=n;for(let U of T){let V=O.children.get(U);V||(V={children:new Map},O.children.set(U,V)),O=V}O.workspaceLocator=S}let C=(S,x)=>{if(S.workspaceLocator){let I=dA(x),T=a.get(I);T||(T=new Set,a.set(I,T)),T.add(S.workspaceLocator)}for(let I of S.children.values())C(I,S.workspaceLocator||x)};for(let S of n.children.values())C(S,n.workspaceLocator);return a},sht=(e,t)=>{let r=[],s=!1,a=new Map,n=iht(e),c=e.getPackageInformation(e.topLevel);if(c===null)throw new Error("Assertion failed: Expected the top-level package to have been registered");let f=e.findPackageLocator(c.packageLocation);if(f===null)throw new Error("Assertion failed: Expected the top-level package to have a physical locator");let p=fe.toPortablePath(c.packageLocation.slice(0,-1)),h={name:f.name,identName:f.name,reference:f.reference,peerNames:c.packagePeers,dependencies:new Set,dependencyKind:1},E=new Map,C=(x,I)=>`${dA(I)}:${x}`,S=(x,I,T,O,U,V,te,ie)=>{let ue=C(x,T),ae=E.get(ue),ge=!!ae;!ge&&T.name===f.name&&T.reference===f.reference&&(ae=h,E.set(ue,h));let Ae=CW(I,T,e,p);if(!ae){let me=0;Ae?me=2:I.linkType==="SOFT"&&T.name.endsWith(ed)&&(me=1),ae={name:x,identName:T.name,reference:T.reference,dependencies:new Set,peerNames:me===1?new Set:I.packagePeers,dependencyKind:me},E.set(ue,ae)}let Ce;if(Ae?Ce=2:U.linkType==="SOFT"?Ce=1:Ce=0,ae.hoistPriority=Math.max(ae.hoistPriority||0,Ce),ie&&!Ae){let me=dA({name:O.identName,reference:O.reference}),ce=a.get(me)||new Set;a.set(me,ce),ce.add(ae.name)}let Ee=new Map(I.packageDependencies);if(t.project){let me=t.project.workspacesByCwd.get(fe.toPortablePath(I.packageLocation.slice(0,-1)));if(me){let ce=new Set([...Array.from(me.manifest.peerDependencies.values(),Z=>j.stringifyIdent(Z)),...Array.from(me.manifest.peerDependenciesMeta.keys())]);for(let Z of ce)Ee.has(Z)||(Ee.set(Z,V.get(Z)||null),ae.peerNames.add(Z))}}let d=dA({name:T.name.replace(ed,""),reference:T.reference}),Se=n.get(d);if(Se)for(let me of Se)Ee.set(`${me.name}${ed}`,me.reference);(I!==U||I.linkType!=="SOFT"||!Ae&&(!t.selfReferencesByCwd||t.selfReferencesByCwd.get(te)))&&O.dependencies.add(ae);let Be=T!==f&&I.linkType==="SOFT"&&!T.name.endsWith(ed)&&!Ae;if(!ge&&!Be){let me=new Map;for(let[ce,Z]of Ee)if(Z!==null){let De=e.getLocator(ce,Z),Qe=e.getLocator(ce.replace(ed,""),Z),st=e.getPackageInformation(Qe);if(st===null)throw new Error("Assertion failed: Expected the package to have been registered");let _=CW(st,De,e,p);if(t.validateExternalSoftLinks&&t.project&&_){st.packageDependencies.size>0&&(s=!0);for(let[je,Re]of st.packageDependencies)if(Re!==null){let ct=j.parseLocator(Array.isArray(Re)?`${Re[0]}@${Re[1]}`:`${je}@${Re}`);if(dA(ct)!==dA(De)){let Me=Ee.get(je);if(Me){let P=j.parseLocator(Array.isArray(Me)?`${Me[0]}@${Me[1]}`:`${je}@${Me}`);Fwe(P,ct)||r.push({messageName:71,text:`Cannot link ${j.prettyIdent(t.project.configuration,j.parseIdent(De.name))} into ${j.prettyLocator(t.project.configuration,j.parseLocator(`${T.name}@${T.reference}`))} dependency ${j.prettyLocator(t.project.configuration,ct)} conflicts with parent dependency ${j.prettyLocator(t.project.configuration,P)}`})}else{let P=me.get(je);if(P){let w=P.target,b=j.parseLocator(Array.isArray(w)?`${w[0]}@${w[1]}`:`${je}@${w}`);Fwe(b,ct)||r.push({messageName:71,text:`Cannot link ${j.prettyIdent(t.project.configuration,j.parseIdent(De.name))} into ${j.prettyLocator(t.project.configuration,j.parseLocator(`${T.name}@${T.reference}`))} dependency ${j.prettyLocator(t.project.configuration,ct)} conflicts with dependency ${j.prettyLocator(t.project.configuration,b)} from sibling portal ${j.prettyIdent(t.project.configuration,j.parseIdent(P.portal.name))}`})}else me.set(je,{target:ct.reference,portal:De})}}}}let tt=t.hoistingLimitsByCwd?.get(te),Ne=_?te:J.relative(p,fe.toPortablePath(st.packageLocation))||vt.dot,ke=t.hoistingLimitsByCwd?.get(Ne);S(ce,st,De,ae,I,Ee,Ne,tt==="dependencies"||ke==="dependencies"||ke==="workspaces")}}};return S(f.name,c,f,h,c,c.packageDependencies,vt.dot,!1),{packageTree:h,hoistingLimits:a,errors:r,preserveSymlinksRequired:s}};function Nwe(e,t,r){let s=r.resolveVirtual&&t.reference&&t.reference.startsWith("virtual:")?r.resolveVirtual(e.packageLocation):e.packageLocation;return fe.toPortablePath(s||e.packageLocation)}function oht(e,t,r){let s=t.getLocator(e.name.replace(ed,""),e.reference),a=t.getPackageInformation(s);if(a===null)throw new Error("Assertion failed: Expected the package to be registered");return r.pnpifyFs?{linkType:"SOFT",target:fe.toPortablePath(a.packageLocation)}:{linkType:a.linkType,target:Nwe(a,e,t)}}var aht=(e,t,r)=>{let s=new Map,a=(E,C,S)=>{let{linkType:x,target:I}=oht(E,e,r);return{locator:dA(E),nodePath:C,target:I,linkType:x,aliases:S}},n=E=>{let[C,S]=E.split("/");return S?{scope:C,name:S}:{scope:null,name:C}},c=new Set,f=(E,C,S)=>{if(c.has(E))return;c.add(E);let x=Array.from(E.references).sort().join("#");for(let I of E.dependencies){let T=Array.from(I.references).sort().join("#");if(I.identName===E.identName.replace(ed,"")&&T===x)continue;let O=Array.from(I.references).sort(),U={name:I.identName,reference:O[0]},{name:V,scope:te}=n(I.name),ie=te?[te,V]:[V],ue=J.join(C,Twe),ae=J.join(ue,...ie),ge=`${S}/${U.name}`,Ae=a(U,S,O.slice(1)),Ce=!1;if(Ae.linkType==="SOFT"&&r.project){let Ee=r.project.workspacesByCwd.get(Ae.target.slice(0,-1));Ce=!!(Ee&&!Ee.manifest.name)}if(!I.name.endsWith(ed)&&!Ce){let Ee=s.get(ae);if(Ee){if(Ee.dirList)throw new Error(`Assertion failed: ${ae} cannot merge dir node with leaf node`);{let Be=j.parseLocator(Ee.locator),me=j.parseLocator(Ae.locator);if(Ee.linkType!==Ae.linkType)throw new Error(`Assertion failed: ${ae} cannot merge nodes with different link types ${Ee.nodePath}/${j.stringifyLocator(Be)} and ${S}/${j.stringifyLocator(me)}`);if(Be.identHash!==me.identHash)throw new Error(`Assertion failed: ${ae} cannot merge nodes with different idents ${Ee.nodePath}/${j.stringifyLocator(Be)} and ${S}/s${j.stringifyLocator(me)}`);Ae.aliases=[...Ae.aliases,...Ee.aliases,j.parseLocator(Ee.locator).reference]}}s.set(ae,Ae);let d=ae.split("/"),Se=d.indexOf(Twe);for(let Be=d.length-1;Se>=0&&Be>Se;Be--){let me=fe.toPortablePath(d.slice(0,Be).join(J.sep)),ce=d[Be],Z=s.get(me);if(!Z)s.set(me,{dirList:new Set([ce])});else if(Z.dirList){if(Z.dirList.has(ce))break;Z.dirList.add(ce)}}}f(I,Ae.linkType==="SOFT"?Ae.target:ae,ge)}},p=a({name:t.name,reference:Array.from(t.references)[0]},"",[]),h=p.target;return s.set(h,p),f(t,h,""),s};qe();qe();Dt();Dt();nA();vc();var jW={};Vt(jW,{PnpInstaller:()=>Ym,PnpLinker:()=>nd,UnplugCommand:()=>mw,default:()=>Lht,getPnpPath:()=>id,jsInstallUtils:()=>mA,pnpUtils:()=>HD,quotePathIfNeeded:()=>y1e});Dt();var m1e=Ie("url");qe();qe();Dt();Dt();var Owe={DEFAULT:{collapsed:!1,next:{"*":"DEFAULT"}},TOP_LEVEL:{collapsed:!1,next:{fallbackExclusionList:"FALLBACK_EXCLUSION_LIST",packageRegistryData:"PACKAGE_REGISTRY_DATA","*":"DEFAULT"}},FALLBACK_EXCLUSION_LIST:{collapsed:!1,next:{"*":"FALLBACK_EXCLUSION_ENTRIES"}},FALLBACK_EXCLUSION_ENTRIES:{collapsed:!0,next:{"*":"FALLBACK_EXCLUSION_DATA"}},FALLBACK_EXCLUSION_DATA:{collapsed:!0,next:{"*":"DEFAULT"}},PACKAGE_REGISTRY_DATA:{collapsed:!1,next:{"*":"PACKAGE_REGISTRY_ENTRIES"}},PACKAGE_REGISTRY_ENTRIES:{collapsed:!0,next:{"*":"PACKAGE_STORE_DATA"}},PACKAGE_STORE_DATA:{collapsed:!1,next:{"*":"PACKAGE_STORE_ENTRIES"}},PACKAGE_STORE_ENTRIES:{collapsed:!0,next:{"*":"PACKAGE_INFORMATION_DATA"}},PACKAGE_INFORMATION_DATA:{collapsed:!1,next:{packageDependencies:"PACKAGE_DEPENDENCIES","*":"DEFAULT"}},PACKAGE_DEPENDENCIES:{collapsed:!1,next:{"*":"PACKAGE_DEPENDENCY"}},PACKAGE_DEPENDENCY:{collapsed:!0,next:{"*":"DEFAULT"}}};function lht(e,t,r){let s="";s+="[";for(let a=0,n=e.length;a"u"||(f!==0&&(a+=", "),a+=JSON.stringify(p),a+=": ",a+=HF(p,h,t,r).replace(/^ +/g,""),f+=1)}return a+="}",a}function fht(e,t,r){let s=Object.keys(e),a=`${r} `,n="";n+=r,n+=`{ `;let c=0;for(let f=0,p=s.length;f"u"||(c!==0&&(n+=",",n+=` `),n+=a,n+=JSON.stringify(h),n+=": ",n+=HF(h,E,t,a).replace(/^ +/g,""),c+=1)}return c!==0&&(n+=` `),n+=r,n+="}",n}function HF(e,t,r,s){let{next:a}=Owe[r],n=a[e]||a["*"];return Lwe(t,n,s)}function Lwe(e,t,r){let{collapsed:s}=Owe[t];return Array.isArray(e)?s?lht(e,t,r):cht(e,t,r):typeof e=="object"&&e!==null?s?uht(e,t,r):fht(e,t,r):JSON.stringify(e)}function Mwe(e){return Lwe(e,"TOP_LEVEL","")}function QD(e,t){let r=Array.from(e);Array.isArray(t)||(t=[t]);let s=[];for(let n of t)s.push(r.map(c=>n(c)));let a=r.map((n,c)=>c);return a.sort((n,c)=>{for(let f of s){let p=f[n]f[c]?1:0;if(p!==0)return p}return 0}),a.map(n=>r[n])}function Aht(e){let t=new Map,r=QD(e.fallbackExclusionList||[],[({name:s,reference:a})=>s,({name:s,reference:a})=>a]);for(let{name:s,reference:a}of r){let n=t.get(s);typeof n>"u"&&t.set(s,n=new Set),n.add(a)}return Array.from(t).map(([s,a])=>[s,Array.from(a)])}function pht(e){return QD(e.fallbackPool||[],([t])=>t)}function hht(e){let t=[],r=e.dependencyTreeRoots.find(s=>e.packageRegistry.get(s.name)?.get(s.reference)?.packageLocation==="./");for(let[s,a]of QD(e.packageRegistry,([n])=>n===null?"0":`1${n}`)){if(s===null)continue;let n=[];t.push([s,n]);for(let[c,{packageLocation:f,packageDependencies:p,packagePeers:h,linkType:E,discardFromLookup:C}]of QD(a,([S])=>S===null?"0":`1${S}`)){if(c===null)continue;let S=[];s!==null&&c!==null&&!p.has(s)&&S.push([s,c]);for(let[U,V]of p)S.push([U,V]);let x=QD(S,([U])=>U),I=h&&h.size>0?Array.from(h):void 0,O={packageLocation:f,packageDependencies:x,packagePeers:I,linkType:E,discardFromLookup:C||void 0};n.push([c,O]),r&&s===r.name&&c===r.reference&&t.unshift([null,[[null,O]]])}}return t}function RD(e){return{__info:["This file is automatically generated. Do not touch it, or risk","your modifications being lost."],dependencyTreeRoots:e.dependencyTreeRoots,enableTopLevelFallback:e.enableTopLevelFallback||!1,ignorePatternData:e.ignorePattern||null,pnpZipBackend:e.pnpZipBackend,fallbackExclusionList:Aht(e),fallbackPool:pht(e),packageRegistryData:hht(e)}}var Hwe=et(_we());function jwe(e,t){return[e?`${e} `:"",`/* eslint-disable */ `,`// @ts-nocheck `,`"use strict"; `,` `,t,` `,(0,Hwe.default)()].join("")}function dht(e){return JSON.stringify(e,null,2)}function ght(e){return`'${e.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(/\n/g,`\\ `)}'`}function mht(e){return[`const RAW_RUNTIME_STATE = `,`${ght(Mwe(e))}; `,`function $$SETUP_STATE(hydrateRuntimeState, basePath) { `,` return hydrateRuntimeState(JSON.parse(RAW_RUNTIME_STATE), {basePath: basePath || __dirname}); `,`} `].join("")}function yht(){return[`function $$SETUP_STATE(hydrateRuntimeState, basePath) { `,` const fs = require('fs'); `,` const path = require('path'); `,` const pnpDataFilepath = path.resolve(__dirname, ${JSON.stringify(Er.pnpData)}); `,` return hydrateRuntimeState(JSON.parse(fs.readFileSync(pnpDataFilepath, 'utf8')), {basePath: basePath || __dirname}); `,`} `].join("")}function Gwe(e){let t=RD(e),r=mht(t);return jwe(e.shebang,r)}function qwe(e){let t=RD(e),r=yht(),s=jwe(e.shebang,r);return{dataFile:dht(t),loaderFile:s}}Dt();function vW(e,{basePath:t}){let r=fe.toPortablePath(t),s=J.resolve(r),a=e.ignorePatternData!==null?new RegExp(e.ignorePatternData):null,n=new Map,c=new Map(e.packageRegistryData.map(([C,S])=>[C,new Map(S.map(([x,I])=>{if(C===null!=(x===null))throw new Error("Assertion failed: The name and reference should be null, or neither should");let T=I.discardFromLookup??!1,O={name:C,reference:x},U=n.get(I.packageLocation);U?(U.discardFromLookup=U.discardFromLookup&&T,T||(U.locator=O)):n.set(I.packageLocation,{locator:O,discardFromLookup:T});let V=null;return[x,{packageDependencies:new Map(I.packageDependencies),packagePeers:new Set(I.packagePeers),linkType:I.linkType,discardFromLookup:T,get packageLocation(){return V||(V=J.join(s,I.packageLocation))}}]}))])),f=new Map(e.fallbackExclusionList.map(([C,S])=>[C,new Set(S)])),p=new Map(e.fallbackPool),h=e.dependencyTreeRoots,E=e.enableTopLevelFallback;return{basePath:r,dependencyTreeRoots:h,enableTopLevelFallback:E,fallbackExclusionList:f,pnpZipBackend:e.pnpZipBackend,fallbackPool:p,ignorePattern:a,packageLocatorsByLocations:n,packageRegistry:c}}Dt();Dt();var ih=Ie("module"),Wm=Ie("url"),NW=Ie("util");var oa=Ie("url");var Jwe=et(Ie("assert"));var SW=Array.isArray,TD=JSON.stringify,FD=Object.getOwnPropertyNames,qm=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),DW=(e,t)=>RegExp.prototype.exec.call(e,t),bW=(e,...t)=>RegExp.prototype[Symbol.replace].apply(e,t),td=(e,...t)=>String.prototype.endsWith.apply(e,t),PW=(e,...t)=>String.prototype.includes.apply(e,t),xW=(e,...t)=>String.prototype.lastIndexOf.apply(e,t),ND=(e,...t)=>String.prototype.indexOf.apply(e,t),Wwe=(e,...t)=>String.prototype.replace.apply(e,t),rd=(e,...t)=>String.prototype.slice.apply(e,t),gA=(e,...t)=>String.prototype.startsWith.apply(e,t),Ywe=Map,Vwe=JSON.parse;function OD(e,t,r){return class extends r{constructor(...s){super(t(...s)),this.code=e,this.name=`${r.name} [${e}]`}}}var Kwe=OD("ERR_PACKAGE_IMPORT_NOT_DEFINED",(e,t,r)=>`Package import specifier "${e}" is not defined${t?` in package ${t}package.json`:""} imported from ${r}`,TypeError),kW=OD("ERR_INVALID_MODULE_SPECIFIER",(e,t,r=void 0)=>`Invalid module "${e}" ${t}${r?` imported from ${r}`:""}`,TypeError),zwe=OD("ERR_INVALID_PACKAGE_TARGET",(e,t,r,s=!1,a=void 0)=>{let n=typeof r=="string"&&!s&&r.length&&!gA(r,"./");return t==="."?((0,Jwe.default)(s===!1),`Invalid "exports" main target ${TD(r)} defined in the package config ${e}package.json${a?` imported from ${a}`:""}${n?'; targets must start with "./"':""}`):`Invalid "${s?"imports":"exports"}" target ${TD(r)} defined for '${t}' in the package config ${e}package.json${a?` imported from ${a}`:""}${n?'; targets must start with "./"':""}`},Error),LD=OD("ERR_INVALID_PACKAGE_CONFIG",(e,t,r)=>`Invalid package config ${e}${t?` while importing ${t}`:""}${r?`. ${r}`:""}`,Error),Xwe=OD("ERR_PACKAGE_PATH_NOT_EXPORTED",(e,t,r=void 0)=>t==="."?`No "exports" main defined in ${e}package.json${r?` imported from ${r}`:""}`:`Package subpath '${t}' is not defined by "exports" in ${e}package.json${r?` imported from ${r}`:""}`,Error);var GF=Ie("url");function Zwe(e,t){let r=Object.create(null);for(let s=0;st):e+t}MD(r,e,s,c,a)}DW(e1e,rd(e,2))!==null&&MD(r,e,s,c,a);let p=new URL(e,s),h=p.pathname,E=new URL(".",s).pathname;if(gA(h,E)||MD(r,e,s,c,a),t==="")return p;if(DW(e1e,t)!==null){let C=n?Wwe(r,"*",()=>t):r+t;Cht(C,s,c,a)}return n?new URL(bW(t1e,p.href,()=>t)):new URL(t,p)}function Bht(e){let t=+e;return`${t}`!==e?!1:t>=0&&t<4294967295}function gw(e,t,r,s,a,n,c,f){if(typeof t=="string")return wht(t,r,s,e,a,n,c,f);if(SW(t)){if(t.length===0)return null;let p;for(let h=0;hn?-1:n>a||r===-1?1:s===-1||e.length>t.length?-1:t.length>e.length?1:0}function vht(e,t,r){if(typeof e=="string"||SW(e))return!0;if(typeof e!="object"||e===null)return!1;let s=FD(e),a=!1,n=0;for(let c=0;c=h.length&&td(t,C)&&n1e(n,h)===1&&xW(h,"*")===E&&(n=h,c=rd(t,E,t.length-C.length))}}if(n){let p=r[n],h=gw(e,p,c,n,s,!0,!1,a);return h==null&&QW(t,e,s),h}QW(t,e,s)}function s1e({name:e,base:t,conditions:r,readFileSyncFn:s}){if(e==="#"||gA(e,"#/")||td(e,"/")){let c="is not a valid internal imports specifier name";throw new kW(e,c,(0,oa.fileURLToPath)(t))}let a,n=$we(t,s);if(n.exists){a=(0,oa.pathToFileURL)(n.pjsonPath);let c=n.imports;if(c)if(qm(c,e)&&!PW(e,"*")){let f=gw(a,c[e],"",e,t,!1,!0,r);if(f!=null)return f}else{let f="",p,h=FD(c);for(let E=0;E=C.length&&td(e,x)&&n1e(f,C)===1&&xW(C,"*")===S&&(f=C,p=rd(e,S,e.length-x.length))}}if(f){let E=c[f],C=gw(a,E,p,f,t,!0,!0,r);if(C!=null)return C}}}Iht(e,a,t)}Dt();var Dht=new Set(["BUILTIN_NODE_RESOLUTION_FAILED","MISSING_DEPENDENCY","MISSING_PEER_DEPENDENCY","QUALIFIED_PATH_RESOLUTION_FAILED","UNDECLARED_DEPENDENCY"]);function ys(e,t,r={},s){s??=Dht.has(e)?"MODULE_NOT_FOUND":e;let a={configurable:!0,writable:!0,enumerable:!1};return Object.defineProperties(new Error(t),{code:{...a,value:s},pnpCode:{...a,value:e},data:{...a,value:r}})}function uf(e){return fe.normalize(fe.fromPortablePath(e))}var c1e=et(a1e());function u1e(e){return bht(),TW[e]}var TW;function bht(){TW||(TW={"--conditions":[],...l1e(Pht()),...l1e(process.execArgv)})}function l1e(e){return(0,c1e.default)({"--conditions":[String],"-C":"--conditions"},{argv:e,permissive:!0})}function Pht(){let e=[],t=xht(process.env.NODE_OPTIONS||"",e);return e.length,t}function xht(e,t){let r=[],s=!1,a=!0;for(let n=0;nparseInt(e,10)),f1e=vo>19||vo===19&&ff>=2||vo===18&&ff>=13,WKt=vo===20&&ff<6||vo===19&&ff>=3,YKt=vo>19||vo===19&&ff>=6,VKt=vo>=21||vo===20&&ff>=10||vo===18&&ff>=19,JKt=vo>=21||vo===20&&ff>=10||vo===18&&ff>=20,KKt=vo>=22,zKt=vo>25||vo===25&&ff>=7||vo===24&&ff>=15;function FW(e){if(process.env.WATCH_REPORT_DEPENDENCIES&&process.send){let t=e.map(r=>fe.fromPortablePath(mo.resolveVirtual(r)));if(f1e)process.send({"watch:require":t});else for(let r of t)process.send({"watch:require":r})}}function OW(e,t){let r=Number(process.env.PNP_ALWAYS_WARN_ON_FALLBACK)>0,s=Number(process.env.PNP_DEBUG_LEVEL),a=/^(?![a-zA-Z]:[\\/]|\\\\|\.{0,2}(?:\/|$))((?:node:)?(?:@[^/]+\/)?[^/]+)\/*(.*|)$/,n=/^(\/|\.{1,2}(\/|$))/,c=/\/$/,f=/^\.{0,2}\//,p={name:null,reference:null},h=[],E=new Set;if(e.enableTopLevelFallback===!0&&h.push(p),t.compatibilityMode!==!1)for(let Ne of["react-scripts","gatsby"]){let ke=e.packageRegistry.get(Ne);if(ke)for(let be of ke.keys()){if(be===null)throw new Error("Assertion failed: This reference shouldn't be null");h.push({name:Ne,reference:be})}}let{ignorePattern:C,packageRegistry:S,packageLocatorsByLocations:x}=e;function I(Ne,ke){return{fn:Ne,args:ke,error:null,result:null}}function T(Ne){let ke=process.stderr?.hasColors?.()??process.stdout.isTTY,be=(ct,Me)=>`\x1B[${ct}m${Me}\x1B[0m`,je=Ne.error;console.error(je?be("31;1",`\u2716 ${Ne.error?.message.replace(/\n.*/s,"")}`):be("33;1","\u203C Resolution")),Ne.args.length>0&&console.error();for(let ct of Ne.args)console.error(` ${be("37;1","In \u2190")} ${(0,NW.inspect)(ct,{colors:ke,compact:!0})}`);Ne.result&&(console.error(),console.error(` ${be("37;1","Out \u2192")} ${(0,NW.inspect)(Ne.result,{colors:ke,compact:!0})}`));let Re=new Error().stack.match(/(?<=^ +)at.*/gm)?.slice(2)??[];if(Re.length>0){console.error();for(let ct of Re)console.error(` ${be("38;5;244",ct)}`)}console.error()}function O(Ne,ke){if(t.allowDebug===!1)return ke;if(Number.isFinite(s)){if(s>=2)return(...be)=>{let je=I(Ne,be);try{return je.result=ke(...be)}catch(Re){throw je.error=Re}finally{T(je)}};if(s>=1)return(...be)=>{try{return ke(...be)}catch(je){let Re=I(Ne,be);throw Re.error=je,T(Re),je}}}return ke}function U(Ne){let ke=d(Ne);if(!ke)throw ys("INTERNAL","Couldn't find a matching entry in the dependency tree for the specified parent (this is probably an internal error)");return ke}function V(Ne){if(Ne.name===null)return!0;for(let ke of e.dependencyTreeRoots)if(ke.name===Ne.name&&ke.reference===Ne.reference)return!0;return!1}let te=new Set(["node","require",...u1e("--conditions")]);function ie(Ne,ke=te,be){let je=me(J.join(Ne,"internal.js"),{resolveIgnored:!0,includeDiscardFromLookup:!0});if(je===null)throw ys("INTERNAL",`The locator that owns the "${Ne}" path can't be found inside the dependency tree (this is probably an internal error)`);let{packageLocation:Re}=U(je),ct=J.join(Re,Er.manifest);if(!t.fakeFs.existsSync(ct))return null;let Me=JSON.parse(t.fakeFs.readFileSync(ct,"utf8"));if(Me.exports==null)return null;let P=J.contains(Re,Ne);if(P===null)throw ys("INTERNAL","unqualifiedPath doesn't contain the packageLocation (this is probably an internal error)");P!=="."&&!f.test(P)&&(P=`./${P}`);try{let w=i1e({packageJSONUrl:(0,Wm.pathToFileURL)(fe.fromPortablePath(ct)),packageSubpath:P,exports:Me.exports,base:be?(0,Wm.pathToFileURL)(fe.fromPortablePath(be)):null,conditions:ke});return fe.toPortablePath((0,Wm.fileURLToPath)(w))}catch(w){throw ys("EXPORTS_RESOLUTION_FAILED",w.message,{unqualifiedPath:uf(Ne),locator:je,pkgJson:Me,subpath:uf(P),conditions:ke},w.code)}}function ue(Ne,ke,{extensions:be}){let je;try{ke.push(Ne),je=t.fakeFs.statSync(Ne)}catch{}if(je&&!je.isDirectory())return t.fakeFs.realpathSync(Ne);if(je&&je.isDirectory()){let Re;try{Re=JSON.parse(t.fakeFs.readFileSync(J.join(Ne,Er.manifest),"utf8"))}catch{}let ct;if(Re&&Re.main&&(ct=J.resolve(Ne,Re.main)),ct&&ct!==Ne){let Me=ue(ct,ke,{extensions:be});if(Me!==null)return Me}}for(let Re=0,ct=be.length;Re{let P=JSON.stringify(Me.name);if(je.has(P))return;je.add(P);let w=Se(Me);for(let b of w)if(U(b).packagePeers.has(Ne))Re(b);else{let F=be.get(b.name);typeof F>"u"&&be.set(b.name,F=new Set),F.add(b.reference)}};Re(ke);let ct=[];for(let Me of[...be.keys()].sort())for(let P of[...be.get(Me)].sort())ct.push({name:Me,reference:P});return ct}function me(Ne,{resolveIgnored:ke=!1,includeDiscardFromLookup:be=!1}={}){if(Ae(Ne)&&!ke)return null;let je=J.relative(e.basePath,Ne);je.match(n)||(je=`./${je}`),je.endsWith("/")||(je=`${je}/`);do{let Re=x.get(je);if(typeof Re>"u"||Re.discardFromLookup&&!be){je=je.substring(0,je.lastIndexOf("/",je.length-2)+1);continue}return Re.locator}while(je!=="");return null}function ce(Ne){try{return t.fakeFs.readFileSync(fe.toPortablePath(Ne),"utf8")}catch(ke){if(ke.code==="ENOENT")return;throw ke}}function Z(Ne,ke,{considerBuiltins:be=!0}={}){if(Ne.startsWith("#"))throw new Error("resolveToUnqualified can not handle private import mappings");if(Ne==="pnpapi")return fe.toPortablePath(t.pnpapiResolution);if(be&&(0,ih.isBuiltin)(Ne))return null;let je=uf(Ne),Re=ke&&uf(ke);if(ke&&Ae(ke)&&(!J.isAbsolute(Ne)||me(Ne)===null)){let P=ge(Ne,ke);if(P===!1)throw ys("BUILTIN_NODE_RESOLUTION_FAILED",`The builtin node resolution algorithm was unable to resolve the requested module (it didn't go through the pnp resolver because the issuer was explicitely ignored by the regexp) Require request: "${je}" Required by: ${Re} `,{request:je,issuer:Re});return fe.toPortablePath(P)}let ct,Me=Ne.match(a);if(Me){if(!ke)throw ys("API_ERROR","The resolveToUnqualified function must be called with a valid issuer when the path isn't a builtin nor absolute",{request:je,issuer:Re});let[,P,w]=Me,b=me(ke);if(!b){let Fe=ge(Ne,ke);if(Fe===!1)throw ys("BUILTIN_NODE_RESOLUTION_FAILED",`The builtin node resolution algorithm was unable to resolve the requested module (it didn't go through the pnp resolver because the issuer doesn't seem to be part of the Yarn-managed dependency tree). Require path: "${je}" Required by: ${Re} `,{request:je,issuer:Re});return fe.toPortablePath(Fe)}let F=U(b).packageDependencies.get(P),z=null;if(F==null&&b.name!==null){let Fe=e.fallbackExclusionList.get(b.name);if(!Fe||!Fe.has(b.reference)){for(let Ct=0,qt=h.length;CtV(ut))?X=ys("MISSING_PEER_DEPENDENCY",`${b.name} tried to access ${P} (a peer dependency) but it isn't provided by your application; this makes the require call ambiguous and unsound. Required package: ${P}${P!==je?` (via "${je}")`:""} Required by: ${b.name}@${b.reference} (via ${Re}) ${Fe.map(ut=>`Ancestor breaking the chain: ${ut.name}@${ut.reference} `).join("")} `,{request:je,issuer:Re,issuerLocator:Object.assign({},b),dependencyName:P,brokenAncestors:Fe}):X=ys("MISSING_PEER_DEPENDENCY",`${b.name} tried to access ${P} (a peer dependency) but it isn't provided by its ancestors; this makes the require call ambiguous and unsound. Required package: ${P}${P!==je?` (via "${je}")`:""} Required by: ${b.name}@${b.reference} (via ${Re}) ${Fe.map(ut=>`Ancestor breaking the chain: ${ut.name}@${ut.reference} `).join("")} `,{request:je,issuer:Re,issuerLocator:Object.assign({},b),dependencyName:P,brokenAncestors:Fe})}else F===void 0&&(!be&&(0,ih.isBuiltin)(Ne)?V(b)?X=ys("UNDECLARED_DEPENDENCY",`Your application tried to access ${P}. While this module is usually interpreted as a Node builtin, your resolver is running inside a non-Node resolution context where such builtins are ignored. Since ${P} isn't otherwise declared in your dependencies, this makes the require call ambiguous and unsound. Required package: ${P}${P!==je?` (via "${je}")`:""} Required by: ${Re} `,{request:je,issuer:Re,dependencyName:P}):X=ys("UNDECLARED_DEPENDENCY",`${b.name} tried to access ${P}. While this module is usually interpreted as a Node builtin, your resolver is running inside a non-Node resolution context where such builtins are ignored. Since ${P} isn't otherwise declared in ${b.name}'s dependencies, this makes the require call ambiguous and unsound. Required package: ${P}${P!==je?` (via "${je}")`:""} Required by: ${Re} `,{request:je,issuer:Re,issuerLocator:Object.assign({},b),dependencyName:P}):V(b)?X=ys("UNDECLARED_DEPENDENCY",`Your application tried to access ${P}, but it isn't declared in your dependencies; this makes the require call ambiguous and unsound. Required package: ${P}${P!==je?` (via "${je}")`:""} Required by: ${Re} `,{request:je,issuer:Re,dependencyName:P}):X=ys("UNDECLARED_DEPENDENCY",`${b.name} tried to access ${P}, but it isn't declared in its dependencies; this makes the require call ambiguous and unsound. Required package: ${P}${P!==je?` (via "${je}")`:""} Required by: ${b.name}@${b.reference} (via ${Re}) `,{request:je,issuer:Re,issuerLocator:Object.assign({},b),dependencyName:P}));if(F==null){if(z===null||X===null)throw X||new Error("Assertion failed: Expected an error to have been set");F=z;let Fe=X.message.replace(/\n.*/g,"");X.message=Fe,!E.has(Fe)&&s!==0&&(E.add(Fe),process.emitWarning(X))}let $=Array.isArray(F)?{name:F[0],reference:F[1]}:{name:P,reference:F},se=U($);if(!se.packageLocation)throw ys("MISSING_DEPENDENCY",`A dependency seems valid but didn't get installed for some reason. This might be caused by a partial install, such as dev vs prod. Required package: ${$.name}@${$.reference}${$.name!==je?` (via "${je}")`:""} Required by: ${b.name}@${b.reference} (via ${Re}) `,{request:je,issuer:Re,dependencyLocator:Object.assign({},$)});let xe=se.packageLocation;w?ct=J.join(xe,w):ct=xe}else if(J.isAbsolute(Ne))ct=J.normalize(Ne);else{if(!ke)throw ys("API_ERROR","The resolveToUnqualified function must be called with a valid issuer when the path isn't a builtin nor absolute",{request:je,issuer:Re});let P=J.resolve(ke);ke.match(c)?ct=J.normalize(J.join(P,Ne)):ct=J.normalize(J.join(J.dirname(P),Ne))}return J.normalize(ct)}function De(Ne,ke,be=te,je){if(n.test(Ne))return ke;let Re=ie(ke,be,je);return Re?J.normalize(Re):ke}function Qe(Ne,{extensions:ke=Object.keys(ih.Module._extensions)}={}){let be=[],je=ue(Ne,be,{extensions:ke});if(je)return FW([je]),J.normalize(je);{FW(be);let Re=uf(Ne),ct=me(Ne);if(ct){let{packageLocation:Me}=U(ct),P=!0;try{t.fakeFs.accessSync(Me)}catch(w){if(w?.code==="ENOENT")P=!1;else{let b=(w?.message??w??"empty exception thrown").replace(/^[A-Z]/,y=>y.toLowerCase());throw ys("QUALIFIED_PATH_RESOLUTION_FAILED",`Required package exists but could not be accessed (${b}). Missing package: ${ct.name}@${ct.reference} Expected package location: ${uf(Me)} `,{unqualifiedPath:Re,extensions:ke})}}if(!P){let w=Me.includes("/unplugged/")?"Required unplugged package missing from disk. This may happen when switching branches without running installs (unplugged packages must be fully materialized on disk to work).":"Required package missing from disk. If you keep your packages inside your repository then restarting the Node process may be enough. Otherwise, try to run an install first.";throw ys("QUALIFIED_PATH_RESOLUTION_FAILED",`${w} Missing package: ${ct.name}@${ct.reference} Expected package location: ${uf(Me)} `,{unqualifiedPath:Re,extensions:ke})}}throw ys("QUALIFIED_PATH_RESOLUTION_FAILED",`Qualified path resolution failed: we looked for the following paths, but none could be accessed. Source path: ${Re} ${be.map(Me=>`Not found: ${uf(Me)} `).join("")}`,{unqualifiedPath:Re,extensions:ke})}}function st(Ne,ke,be){if(!ke)throw new Error("Assertion failed: An issuer is required to resolve private import mappings");let je=s1e({name:Ne,base:(0,Wm.pathToFileURL)(fe.fromPortablePath(ke)),conditions:be.conditions??te,readFileSyncFn:ce});if(je instanceof URL)return Qe(fe.toPortablePath((0,Wm.fileURLToPath)(je)),{extensions:be.extensions});if(je.startsWith("#"))throw new Error("Mapping from one private import to another isn't allowed");return _(je,ke,be)}function _(Ne,ke,be={}){try{if(Ne.startsWith("#"))return st(Ne,ke,be);let{considerBuiltins:je,extensions:Re,conditions:ct}=be,Me=Z(Ne,ke,{considerBuiltins:je});if(Ne==="pnpapi")return Me;if(Me===null)return null;let P=()=>ke!==null?Ae(ke):!1,w=(!je||!(0,ih.isBuiltin)(Ne))&&!P()?De(Ne,Me,ct,ke):Me;return Qe(w,{extensions:Re})}catch(je){throw Object.hasOwn(je,"pnpCode")&&Object.assign(je.data,{request:uf(Ne),issuer:ke&&uf(ke)}),je}}function tt(Ne){let ke=J.normalize(Ne),be=mo.resolveVirtual(ke);return be!==ke?be:null}return{VERSIONS:Ce,topLevel:Ee,getLocator:(Ne,ke)=>Array.isArray(ke)?{name:ke[0],reference:ke[1]}:{name:Ne,reference:ke},getDependencyTreeRoots:()=>[...e.dependencyTreeRoots],getAllLocators(){let Ne=[];for(let[ke,be]of S)for(let je of be.keys())ke!==null&&je!==null&&Ne.push({name:ke,reference:je});return Ne},getPackageInformation:Ne=>{let ke=d(Ne);if(ke===null)return null;let be=fe.fromPortablePath(ke.packageLocation);return{...ke,packageLocation:be}},findPackageLocator:Ne=>me(fe.toPortablePath(Ne)),resolveToUnqualified:O("resolveToUnqualified",(Ne,ke,be)=>{let je=ke!==null?fe.toPortablePath(ke):null,Re=Z(fe.toPortablePath(Ne),je,be);return Re===null?null:fe.fromPortablePath(Re)}),resolveUnqualified:O("resolveUnqualified",(Ne,ke)=>fe.fromPortablePath(Qe(fe.toPortablePath(Ne),ke))),resolveRequest:O("resolveRequest",(Ne,ke,be)=>{let je=ke!==null?fe.toPortablePath(ke):null,Re=_(fe.toPortablePath(Ne),je,be);return Re===null?null:fe.fromPortablePath(Re)}),resolveVirtual:O("resolveVirtual",Ne=>{let ke=tt(fe.toPortablePath(Ne));return ke!==null?fe.fromPortablePath(ke):null})}}Dt();var A1e=(e,t,r)=>{let s=RD(e),a=vW(s,{basePath:t}),n=fe.join(t,Er.pnpCjs);return OW(a,{fakeFs:r,pnpapiResolution:n})};var MW=et(h1e());Yt();var mA={};Vt(mA,{checkManifestCompatibility:()=>d1e,extractBuildRequest:()=>qF,getExtractHint:()=>UW,hasBindingGyp:()=>_W});qe();Dt();function d1e(e){return j.isPackageCompatible(e,Ui.getArchitectureSet())}function qF(e,t,r,{configuration:s}){let a=[];for(let n of["preinstall","install","postinstall"])t.manifest.scripts.has(n)&&a.push({type:0,script:n});return!t.manifest.scripts.has("install")&&t.misc.hasBindingGyp&&a.push({type:1,script:"node-gyp rebuild"}),a.length===0?null:e.linkType!=="HARD"?{skipped:!0,explain:n=>n.reportWarningOnce(6,`${j.prettyLocator(s,e)} lists build scripts, but is referenced through a soft link. Soft links don't support build scripts, so they'll be ignored.`)}:r&&r.built===!1?{skipped:!0,explain:n=>n.reportInfoOnce(5,`${j.prettyLocator(s,e)} lists build scripts, but its build has been explicitly disabled through configuration.`)}:!s.get("enableScripts")&&!r.built?{skipped:!0,explain:n=>n.reportWarningOnce(4,`${j.prettyLocator(s,e)} lists build scripts, but all build scripts have been disabled.`)}:d1e(e)?{skipped:!1,directives:a}:{skipped:!0,explain:n=>n.reportWarningOnce(76,`${j.prettyLocator(s,e)} The ${Ui.getArchitectureName()} architecture is incompatible with this package, build skipped.`)}}var Qht=new Set([".exe",".bin",".h",".hh",".hpp",".c",".cc",".cpp",".java",".jar",".node"]);function UW(e){return e.packageFs.getExtractHint({relevantExtensions:Qht})}function _W(e){let t=J.join(e.prefixPath,"binding.gyp");return e.packageFs.existsSync(t)}var HD={};Vt(HD,{getUnpluggedPath:()=>_D});qe();Dt();function _D(e,{configuration:t}){return J.resolve(t.get("pnpUnpluggedFolder"),j.slugifyLocator(e))}var Rht=new Set([j.makeIdent(null,"open").identHash,j.makeIdent(null,"opn").identHash]),nd=class{constructor(){this.mode="strict";this.pnpCache=new Map}getCustomDataKey(){return JSON.stringify({name:"PnpLinker",version:2})}supportsPackage(t,r){return this.isEnabled(r)}async findPackageLocation(t,r){if(!this.isEnabled(r))throw new Error("Assertion failed: Expected the PnP linker to be enabled");let s=id(r.project).cjs;if(!le.existsSync(s))throw new it(`The project in ${pe.pretty(r.project.configuration,`${r.project.cwd}/package.json`,pe.Type.PATH)} doesn't seem to have been installed - running an install there might help`);let a=Ge.getFactoryWithDefault(this.pnpCache,s,()=>Ge.dynamicRequire(s,{cachingStrategy:Ge.CachingStrategy.FsTime})),n={name:j.stringifyIdent(t),reference:t.reference},c=a.getPackageInformation(n);if(!c)throw new it(`Couldn't find ${j.prettyLocator(r.project.configuration,t)} in the currently installed PnP map - running an install might help`);return fe.toPortablePath(c.packageLocation)}async findPackageLocator(t,r){if(!this.isEnabled(r))return null;let s=id(r.project).cjs;if(!le.existsSync(s))return null;let n=Ge.getFactoryWithDefault(this.pnpCache,s,()=>Ge.dynamicRequire(s,{cachingStrategy:Ge.CachingStrategy.FsTime})).findPackageLocator(fe.fromPortablePath(t));return n?j.makeLocator(j.parseIdent(n.name),n.reference):null}makeInstaller(t){return new Ym(t)}isEnabled(t){return!(t.project.configuration.get("nodeLinker")!=="pnp"||t.project.configuration.get("pnpMode")!==this.mode)}},Ym=class{constructor(t){this.opts=t;this.mode="strict";this.asyncActions=new Ge.AsyncActions(10);this.packageRegistry=new Map;this.virtualTemplates=new Map;this.isESMLoaderRequired=!1;this.customData={store:new Map};this.unpluggedPaths=new Set;this.opts=t}attachCustomData(t){this.customData=t}async installPackage(t,r,s){let a=j.stringifyIdent(t),n=t.reference,c=!!this.opts.project.tryWorkspaceByLocator(t),f=j.isVirtualLocator(t),p=t.peerDependencies.size>0&&!f,h=!p&&!c,E=!p&&t.linkType!=="SOFT",C,S;if(h||E){let te=f?j.devirtualizeLocator(t):t;C=this.customData.store.get(te.locatorHash),typeof C>"u"&&(C=await Tht(r),t.linkType==="HARD"&&this.customData.store.set(te.locatorHash,C)),C.manifest.type==="module"&&(this.isESMLoaderRequired=!0),S=this.opts.project.getDependencyMeta(te,t.version)}let x=h?qF(t,C,S,{configuration:this.opts.project.configuration}):null,I=E?await this.unplugPackageIfNeeded(t,C,r,S,s):r.packageFs;if(J.isAbsolute(r.prefixPath))throw new Error(`Assertion failed: Expected the prefix path (${r.prefixPath}) to be relative to the parent`);let T=J.resolve(I.getRealPath(),r.prefixPath),O=HW(this.opts.project.cwd,T),U=new Map,V=new Set;if(f){for(let te of t.peerDependencies.values())U.set(j.stringifyIdent(te),null),V.add(j.stringifyIdent(te));if(!c){let te=j.devirtualizeLocator(t);this.virtualTemplates.set(te.locatorHash,{location:HW(this.opts.project.cwd,mo.resolveVirtual(T)),locator:te})}}return Ge.getMapWithDefault(this.packageRegistry,a).set(n,{packageLocation:O,packageDependencies:U,packagePeers:V,linkType:t.linkType,discardFromLookup:r.discardFromLookup||!1}),{packageLocation:T,buildRequest:x}}async attachInternalDependencies(t,r){let s=this.getPackageInformation(t);for(let[a,n]of r){let c=j.areIdentsEqual(a,n)?n.reference:[j.stringifyIdent(n),n.reference];s.packageDependencies.set(j.stringifyIdent(a),c)}}async attachExternalDependents(t,r){for(let s of r)this.getDiskInformation(s).packageDependencies.set(j.stringifyIdent(t),t.reference)}async finalizeInstall(){if(this.opts.project.configuration.get("pnpMode")!==this.mode)return;let t=id(this.opts.project);if(this.isEsmEnabled()||await le.removePromise(t.esmLoader),this.opts.project.configuration.get("nodeLinker")!=="pnp"){await le.removePromise(t.cjs),await le.removePromise(t.data),await le.removePromise(t.esmLoader),await le.removePromise(this.opts.project.configuration.get("pnpUnpluggedFolder"));return}for(let{locator:C,location:S}of this.virtualTemplates.values())Ge.getMapWithDefault(this.packageRegistry,j.stringifyIdent(C)).set(C.reference,{packageLocation:S,packageDependencies:new Map,packagePeers:new Set,linkType:"SOFT",discardFromLookup:!1});let r=this.opts.project.configuration.get("pnpFallbackMode"),s=this.opts.project.workspaces.map(({anchoredLocator:C})=>({name:j.stringifyIdent(C),reference:C.reference})),a=r!=="none",n=[],c=new Map,f=Ge.buildIgnorePattern([".yarn/sdks/**",...this.opts.project.configuration.get("pnpIgnorePatterns")]),p=this.packageRegistry,h=this.opts.project.configuration.get("pnpShebang"),E=this.opts.project.configuration.get("pnpZipBackend");if(r==="dependencies-only")for(let C of this.opts.project.storedPackages.values())this.opts.project.tryWorkspaceByLocator(C)&&n.push({name:j.stringifyIdent(C),reference:C.reference});return await this.asyncActions.wait(),await this.finalizeInstallWithPnp({dependencyTreeRoots:s,enableTopLevelFallback:a,fallbackExclusionList:n,fallbackPool:c,ignorePattern:f,pnpZipBackend:E,packageRegistry:p,shebang:h}),{customData:this.customData}}async transformPnpSettings(t){}isEsmEnabled(){if(this.opts.project.configuration.sources.has("pnpEnableEsmLoader"))return this.opts.project.configuration.get("pnpEnableEsmLoader");if(this.isESMLoaderRequired)return!0;for(let t of this.opts.project.workspaces)if(t.manifest.type==="module")return!0;return!1}async finalizeInstallWithPnp(t){let r=id(this.opts.project),s=await this.locateNodeModules(t.ignorePattern);if(s.length>0){this.opts.report.reportWarning(31,"One or more node_modules have been detected and will be removed. This operation may take some time.");for(let n of s)await le.removePromise(n)}if(await this.transformPnpSettings(t),this.opts.project.configuration.get("pnpEnableInlining")){let n=Gwe(t);await le.changeFilePromise(r.cjs,n,{automaticNewlines:!0,mode:493}),await le.removePromise(r.data)}else{let{dataFile:n,loaderFile:c}=qwe(t);await le.changeFilePromise(r.cjs,c,{automaticNewlines:!0,mode:493}),await le.changeFilePromise(r.data,n,{automaticNewlines:!0,mode:420})}this.isEsmEnabled()&&(this.opts.report.reportWarning(92,"ESM support for PnP uses the experimental loader API and is therefore experimental"),await le.changeFilePromise(r.esmLoader,(0,MW.default)(),{automaticNewlines:!0,mode:420}));let a=this.opts.project.configuration.get("pnpUnpluggedFolder");if(this.unpluggedPaths.size===0)await le.removePromise(a);else for(let n of await le.readdirPromise(a)){let c=J.resolve(a,n);this.unpluggedPaths.has(c)||await le.removePromise(c)}}async locateNodeModules(t){let r=[],s=t?new RegExp(t):null;for(let a of this.opts.project.workspaces){let n=J.join(a.cwd,"node_modules");if(s&&s.test(J.relative(this.opts.project.cwd,a.cwd))||!le.existsSync(n))continue;let c=await le.readdirPromise(n,{withFileTypes:!0}),f=c.filter(p=>!p.isDirectory()||p.name===".bin"||!p.name.startsWith("."));if(f.length===c.length)r.push(n);else for(let p of f)r.push(J.join(n,p.name))}return r}async unplugPackageIfNeeded(t,r,s,a,n){return this.shouldBeUnplugged(t,r,a)?this.unplugPackage(t,s,n):s.packageFs}shouldBeUnplugged(t,r,s){return typeof s.unplugged<"u"?s.unplugged:Rht.has(t.identHash)||t.conditions!=null?!0:r.manifest.preferUnplugged!==null?r.manifest.preferUnplugged:!!(qF(t,r,s,{configuration:this.opts.project.configuration})?.skipped===!1||r.misc.extractHint)}async unplugPackage(t,r,s){let a=_D(t,{configuration:this.opts.project.configuration});return this.opts.project.disabledLocators.has(t.locatorHash)?new Gf(a,{baseFs:r.packageFs,pathUtils:J}):(this.unpluggedPaths.add(a),s.holdFetchResult(this.asyncActions.set(t.locatorHash,async()=>{let n=J.join(a,r.prefixPath,".ready");await le.existsPromise(n)||(this.opts.project.storedBuildState.delete(t.locatorHash),await le.mkdirPromise(a,{recursive:!0}),await le.copyPromise(a,vt.dot,{baseFs:r.packageFs,overwrite:!1}),await le.writeFilePromise(n,""))})),new bn(a))}getPackageInformation(t){let r=j.stringifyIdent(t),s=t.reference,a=this.packageRegistry.get(r);if(!a)throw new Error(`Assertion failed: The package information store should have been available (for ${j.prettyIdent(this.opts.project.configuration,t)})`);let n=a.get(s);if(!n)throw new Error(`Assertion failed: The package information should have been available (for ${j.prettyLocator(this.opts.project.configuration,t)})`);return n}getDiskInformation(t){let r=Ge.getMapWithDefault(this.packageRegistry,"@@disk"),s=HW(this.opts.project.cwd,t);return Ge.getFactoryWithDefault(r,s,()=>({packageLocation:s,packageDependencies:new Map,packagePeers:new Set,linkType:"SOFT",discardFromLookup:!1}))}};function HW(e,t){let r=J.relative(e,t);return r.match(/^\.{0,2}\//)||(r=`./${r}`),r.replace(/\/?$/,"/")}async function Tht(e){let t=await _t.tryFind(e.prefixPath,{baseFs:e.packageFs})??new _t,r=new Set(["preinstall","install","postinstall"]);for(let s of t.scripts.keys())r.has(s)||t.scripts.delete(s);return{manifest:{scripts:t.scripts,preferUnplugged:t.preferUnplugged,type:t.type},misc:{extractHint:UW(e),hasBindingGyp:_W(e)}}}qe();qe();Yt();var g1e=et(zo());var mw=class extends At{constructor(){super(...arguments);this.all=he.Boolean("-A,--all",!1,{description:"Unplug direct dependencies from the entire project"});this.recursive=he.Boolean("-R,--recursive",!1,{description:"Unplug both direct and transitive dependencies"});this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.patterns=he.Rest()}static{this.paths=[["unplug"]]}static{this.usage=at.Usage({description:"force the unpacking of a list of packages",details:"\n This command will add the selectors matching the specified patterns to the list of packages that must be unplugged when installed.\n\n A package being unplugged means that instead of being referenced directly through its archive, it will be unpacked at install time in the directory configured via `pnpUnpluggedFolder`. Note that unpacking packages this way is generally not recommended because it'll make it harder to store your packages within the repository. However, it's a good approach to quickly and safely debug some packages, and can even sometimes be required depending on the context (for example when the package contains shellscripts).\n\n Running the command will set a persistent flag inside your top-level `package.json`, in the `dependenciesMeta` field. As such, to undo its effects, you'll need to revert the changes made to the manifest and run `yarn install` to apply the modification.\n\n By default, only direct dependencies from the current workspace are affected. If `-A,--all` is set, direct dependencies from the entire project are affected. Using the `-R,--recursive` flag will affect transitive dependencies as well as direct ones.\n\n This command accepts glob patterns inside the scope and name components (not the range). Make sure to escape the patterns to prevent your own shell from trying to expand them.\n ",examples:[["Unplug the lodash dependency from the active workspace","yarn unplug lodash"],["Unplug all instances of lodash referenced by any workspace","yarn unplug lodash -A"],["Unplug all instances of lodash referenced by the active workspace and its dependencies","yarn unplug lodash -R"],["Unplug all instances of lodash, anywhere","yarn unplug lodash -AR"],["Unplug one specific version of lodash","yarn unplug lodash@1.2.3"],["Unplug all packages with the `@babel` scope","yarn unplug '@babel/*'"],["Unplug all packages (only for testing, not recommended)","yarn unplug -R '*'"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);if(r.get("nodeLinker")!=="pnp")throw new it("This command can only be used if the `nodeLinker` option is set to `pnp`");await s.restoreInstallState();let c=new Set(this.patterns),f=this.patterns.map(x=>{let I=j.parseDescriptor(x),T=I.range!=="unknown"?I:j.makeDescriptor(I,"*");if(!kr.validRange(T.range))throw new it(`The range of the descriptor patterns must be a valid semver range (${j.prettyDescriptor(r,T)})`);return O=>{let U=j.stringifyIdent(O);return!g1e.default.isMatch(U,j.stringifyIdent(T))||O.version&&!kr.satisfiesWithPrereleases(O.version,T.range)?!1:(c.delete(x),!0)}}),p=()=>{let x=[];for(let I of s.storedPackages.values())!s.tryWorkspaceByLocator(I)&&!j.isVirtualLocator(I)&&f.some(T=>T(I))&&x.push(I);return x},h=x=>{let I=new Set,T=[],O=(U,V)=>{if(I.has(U.locatorHash))return;let te=!!s.tryWorkspaceByLocator(U);if(!(V>0&&!this.recursive&&te)&&(I.add(U.locatorHash),!s.tryWorkspaceByLocator(U)&&f.some(ie=>ie(U))&&T.push(U),!(V>0&&!this.recursive)))for(let ie of U.dependencies.values()){let ue=s.storedResolutions.get(ie.descriptorHash);if(!ue)throw new Error("Assertion failed: The resolution should have been registered");let ae=s.storedPackages.get(ue);if(!ae)throw new Error("Assertion failed: The package should have been registered");O(ae,V+1)}};for(let U of x)O(U.anchoredPackage,0);return T},E,C;if(this.all&&this.recursive?(E=p(),C="the project"):this.all?(E=h(s.workspaces),C="any workspace"):(E=h([a]),C="this workspace"),c.size>1)throw new it(`Patterns ${pe.prettyList(r,c,pe.Type.CODE)} don't match any packages referenced by ${C}`);if(c.size>0)throw new it(`Pattern ${pe.prettyList(r,c,pe.Type.CODE)} doesn't match any packages referenced by ${C}`);E=Ge.sortMap(E,x=>j.stringifyLocator(x));let S=await Ot.start({configuration:r,stdout:this.context.stdout,json:this.json},async x=>{for(let I of E){let T=I.version??"unknown",O=s.topLevelWorkspace.manifest.ensureDependencyMeta(j.makeDescriptor(I,T));O.unplugged=!0,x.reportInfo(0,`Will unpack ${j.prettyLocator(r,I)} to ${pe.pretty(r,_D(I,{configuration:r}),pe.Type.PATH)}`),x.reportJson({locator:j.stringifyLocator(I),version:T})}await s.topLevelWorkspace.persistManifest(),this.json||x.reportSeparator()});return S.hasErrors()?S.exitCode():await s.installWithNewReport({json:this.json,stdout:this.context.stdout},{cache:n})}};var id=e=>({cjs:J.join(e.cwd,Er.pnpCjs),data:J.join(e.cwd,Er.pnpData),esmLoader:J.join(e.cwd,Er.pnpEsmLoader)}),y1e=e=>/\s/.test(e)?JSON.stringify(e):e;async function Fht(e,t,r){let s=/\s*--require\s+\S*\.pnp\.c?js\s*/g,a=/\s*--experimental-loader\s+\S*\.pnp\.loader\.mjs\s*/,n=(t.NODE_OPTIONS??"").replace(s," ").replace(a," ").trim();if(e.configuration.get("nodeLinker")!=="pnp"){t.NODE_OPTIONS=n||void 0;return}let c=id(e),f=`--require ${y1e(fe.fromPortablePath(c.cjs))}`;le.existsSync(c.esmLoader)&&(f=`${f} --experimental-loader ${(0,m1e.pathToFileURL)(fe.fromPortablePath(c.esmLoader)).href}`),le.existsSync(c.cjs)&&(t.NODE_OPTIONS=n?`${f} ${n}`:f)}async function Nht(e,t){let r=id(e);t(r.cjs),t(r.data),t(r.esmLoader),t(e.configuration.get("pnpUnpluggedFolder"))}var Oht={hooks:{populateYarnPaths:Nht,setupScriptEnvironment:Fht},configuration:{nodeLinker:{description:'The linker used for installing Node packages, one of: "pnp", "pnpm", or "node-modules"',type:"STRING",default:"pnp"},minizip:{description:"Whether Yarn should use minizip to extract archives",type:"BOOLEAN",default:!1},winLinkType:{description:"Whether Yarn should use Windows Junctions or symlinks when creating links on Windows.",type:"STRING",values:["junctions","symlinks"],default:"junctions"},pnpMode:{description:"If 'strict', generates standard PnP maps. If 'loose', merges them with the n_m resolution.",type:"STRING",default:"strict"},pnpShebang:{description:"String to prepend to the generated PnP script",type:"STRING",default:"#!/usr/bin/env node"},pnpIgnorePatterns:{description:"Array of glob patterns; files matching them will use the classic resolution",type:"STRING",default:[],isArray:!0},pnpZipBackend:{description:"Whether to use the experimental js implementation for the ZipFS",type:"STRING",values:["libzip","js"],default:"libzip"},pnpEnableEsmLoader:{description:"If true, Yarn will generate an ESM loader (`.pnp.loader.mjs`). If this is not explicitly set Yarn tries to automatically detect whether ESM support is required.",type:"BOOLEAN",default:!1},pnpEnableInlining:{description:"If true, the PnP data will be inlined along with the generated loader",type:"BOOLEAN",default:!0},pnpFallbackMode:{description:"If true, the generated PnP loader will follow the top-level fallback rule",type:"STRING",default:"dependencies-only"},pnpUnpluggedFolder:{description:"Folder where the unplugged packages must be stored",type:"ABSOLUTE_PATH",default:"./.yarn/unplugged"}},linkers:[nd],commands:[mw]},Lht=Oht;var D1e=et(B1e());Yt();var KW=et(Ie("crypto")),b1e=et(Ie("fs")),P1e=1,Ri="node_modules",WF=".bin",x1e=".yarn-state.yml",e0t=1e3,zW=(s=>(s.CLASSIC="classic",s.HARDLINKS_LOCAL="hardlinks-local",s.HARDLINKS_GLOBAL="hardlinks-global",s))(zW||{}),jD=class{constructor(){this.installStateCache=new Map}getCustomDataKey(){return JSON.stringify({name:"NodeModulesLinker",version:3})}supportsPackage(t,r){return this.isEnabled(r)}async findPackageLocation(t,r){if(!this.isEnabled(r))throw new Error("Assertion failed: Expected the node-modules linker to be enabled");let s=r.project.tryWorkspaceByLocator(t);if(s)return s.cwd;let a=await Ge.getFactoryWithDefault(this.installStateCache,r.project.cwd,async()=>await JW(r.project,{unrollAliases:!0}));if(a===null)throw new it("Couldn't find the node_modules state file - running an install might help (findPackageLocation)");let n=a.locatorMap.get(j.stringifyLocator(t));if(!n){let p=new it(`Couldn't find ${j.prettyLocator(r.project.configuration,t)} in the currently installed node_modules map - running an install might help`);throw p.code="LOCATOR_NOT_INSTALLED",p}let c=n.locations.sort((p,h)=>p.split(J.sep).length-h.split(J.sep).length),f=J.join(r.project.configuration.startingCwd,Ri);return c.find(p=>J.contains(f,p))||n.locations[0]}async findPackageLocator(t,r){if(!this.isEnabled(r))return null;let s=await Ge.getFactoryWithDefault(this.installStateCache,r.project.cwd,async()=>await JW(r.project,{unrollAliases:!0}));if(s===null)return null;let{locationRoot:a,segments:n}=YF(J.resolve(t),{skipPrefix:r.project.cwd}),c=s.locationTree.get(a);if(!c)return null;let f=c.locator;for(let p of n){if(c=c.children.get(p),!c)break;f=c.locator||f}return j.parseLocator(f)}makeInstaller(t){return new VW(t)}isEnabled(t){return t.project.configuration.get("nodeLinker")==="node-modules"}},VW=class{constructor(t){this.opts=t;this.localStore=new Map;this.realLocatorChecksums=new Map;this.customData={store:new Map}}attachCustomData(t){this.customData=t}async installPackage(t,r){let s=J.resolve(r.packageFs.getRealPath(),r.prefixPath),a=this.customData.store.get(t.locatorHash);if(typeof a>"u"&&(a=await t0t(t,r),t.linkType==="HARD"&&this.customData.store.set(t.locatorHash,a)),!j.isPackageCompatible(t,this.opts.project.configuration.getSupportedArchitectures()))return{packageLocation:null,buildRequest:null};let n=new Map,c=new Set;n.has(j.stringifyIdent(t))||n.set(j.stringifyIdent(t),t.reference);let f=t;if(j.isVirtualLocator(t)){f=j.devirtualizeLocator(t);for(let E of t.peerDependencies.values())n.set(j.stringifyIdent(E),null),c.add(j.stringifyIdent(E))}let p={packageLocation:`${fe.fromPortablePath(s)}/`,packageDependencies:n,packagePeers:c,linkType:t.linkType,discardFromLookup:r.discardFromLookup??!1};this.localStore.set(t.locatorHash,{pkg:t,customPackageData:a,dependencyMeta:this.opts.project.getDependencyMeta(t,t.version),pnpNode:p});let h=r.checksum?r.checksum.substring(r.checksum.indexOf("/")+1):null;return this.realLocatorChecksums.set(f.locatorHash,h),{packageLocation:s,buildRequest:null}}async attachInternalDependencies(t,r){let s=this.localStore.get(t.locatorHash);if(typeof s>"u")throw new Error("Assertion failed: Expected information object to have been registered");for(let[a,n]of r){let c=j.areIdentsEqual(a,n)?n.reference:[j.stringifyIdent(n),n.reference];s.pnpNode.packageDependencies.set(j.stringifyIdent(a),c)}}async attachExternalDependents(t,r){throw new Error("External dependencies haven't been implemented for the node-modules linker")}async finalizeInstall(){if(this.opts.project.configuration.get("nodeLinker")!=="node-modules")return;let t=new mo({baseFs:new rA({maxOpenFiles:80,readOnlyArchives:!0})}),r=await JW(this.opts.project),s=this.opts.project.configuration.get("nmMode");(r===null||s!==r.nmMode)&&(this.opts.project.storedBuildState.clear(),r={locatorMap:new Map,binSymlinks:new Map,locationTree:new Map,nmMode:s,mtimeMs:0});let a=new Map(this.opts.project.workspaces.map(S=>{let x=this.opts.project.configuration.get("nmHoistingLimits");try{x=Ge.validateEnum(xD,S.manifest.installConfig?.hoistingLimits??x)}catch{let I=j.prettyWorkspace(this.opts.project.configuration,S);this.opts.report.reportWarning(57,`${I}: Invalid 'installConfig.hoistingLimits' value. Expected one of ${Object.values(xD).join(", ")}, using default: "${x}"`)}return[S.relativeCwd,x]})),n=new Map(this.opts.project.workspaces.map(S=>{let x=this.opts.project.configuration.get("nmSelfReferences");return x=S.manifest.installConfig?.selfReferences??x,[S.relativeCwd,x]})),c={VERSIONS:{std:1},topLevel:{name:null,reference:null},getLocator:(S,x)=>Array.isArray(x)?{name:x[0],reference:x[1]}:{name:S,reference:x},getDependencyTreeRoots:()=>this.opts.project.workspaces.map(S=>{let x=S.anchoredLocator;return{name:j.stringifyIdent(x),reference:x.reference}}),getPackageInformation:S=>{let x=S.reference===null?this.opts.project.topLevelWorkspace.anchoredLocator:j.makeLocator(j.parseIdent(S.name),S.reference),I=this.localStore.get(x.locatorHash);if(typeof I>"u")throw new Error("Assertion failed: Expected the package reference to have been registered");return I.pnpNode},findPackageLocator:S=>{let x=this.opts.project.tryWorkspaceByCwd(fe.toPortablePath(S));if(x!==null){let I=x.anchoredLocator;return{name:j.stringifyIdent(I),reference:I.reference}}throw new Error("Assertion failed: Unimplemented")},resolveToUnqualified:()=>{throw new Error("Assertion failed: Unimplemented")},resolveUnqualified:()=>{throw new Error("Assertion failed: Unimplemented")},resolveRequest:()=>{throw new Error("Assertion failed: Unimplemented")},resolveVirtual:S=>fe.fromPortablePath(mo.resolveVirtual(fe.toPortablePath(S)))},{tree:f,errors:p,preserveSymlinksRequired:h}=kD(c,{pnpifyFs:!1,validateExternalSoftLinks:!0,hoistingLimitsByCwd:a,project:this.opts.project,selfReferencesByCwd:n});if(!f){for(let{messageName:S,text:x}of p)this.opts.report.reportError(S,x);return}let E=wW(f);await l0t(r,E,{baseFs:t,project:this.opts.project,report:this.opts.report,realLocatorChecksums:this.realLocatorChecksums,loadManifest:async S=>{let x=j.parseLocator(S),I=this.localStore.get(x.locatorHash);if(typeof I>"u")throw new Error("Assertion failed: Expected the slot to exist");return I.customPackageData.manifest}});let C=[];for(let[S,x]of E.entries()){if(R1e(S))continue;let I=j.parseLocator(S),T=this.localStore.get(I.locatorHash);if(typeof T>"u")throw new Error("Assertion failed: Expected the slot to exist");if(this.opts.project.tryWorkspaceByLocator(T.pkg))continue;let O=mA.extractBuildRequest(T.pkg,T.customPackageData,T.dependencyMeta,{configuration:this.opts.project.configuration});O&&C.push({buildLocations:x.locations,locator:I,buildRequest:O})}return h&&this.opts.report.reportWarning(72,`The application uses portals and that's why ${pe.pretty(this.opts.project.configuration,"--preserve-symlinks",pe.Type.CODE)} Node option is required for launching it`),{customData:this.customData,records:C}}};async function t0t(e,t){let r=await _t.tryFind(t.prefixPath,{baseFs:t.packageFs})??new _t,s=new Set(["preinstall","install","postinstall"]);for(let a of r.scripts.keys())s.has(a)||r.scripts.delete(a);return{manifest:{bin:r.bin,scripts:r.scripts},misc:{hasBindingGyp:mA.hasBindingGyp(t)}}}async function r0t(e,t,r,s,{installChangedByUser:a}){let n="";n+=`# Warning: This file is automatically generated. Removing it is fine, but will `,n+=`# cause your node_modules installation to become invalidated. `,n+=` `,n+=`__metadata: `,n+=` version: ${P1e} `,n+=` nmMode: ${s.value} `;let c=Array.from(t.keys()).sort(),f=j.stringifyLocator(e.topLevelWorkspace.anchoredLocator);for(let E of c){let C=t.get(E);n+=` `,n+=`${JSON.stringify(E)}: `,n+=` locations: `;for(let S of C.locations){let x=J.contains(e.cwd,S);if(x===null)throw new Error(`Assertion failed: Expected the path to be within the project (${S})`);n+=` - ${JSON.stringify(x)} `}if(C.aliases.length>0){n+=` aliases: `;for(let S of C.aliases)n+=` - ${JSON.stringify(S)} `}if(E===f&&r.size>0){n+=` bin: `;for(let[S,x]of r){let I=J.contains(e.cwd,S);if(I===null)throw new Error(`Assertion failed: Expected the path to be within the project (${S})`);n+=` ${JSON.stringify(I)}: `;for(let[T,O]of x){let U=J.relative(J.join(S,Ri),O);n+=` ${JSON.stringify(T)}: ${JSON.stringify(U)} `}}}}let p=e.cwd,h=J.join(p,Ri,x1e);a&&await le.removePromise(h),await le.changeFilePromise(h,n,{automaticNewlines:!0})}async function JW(e,{unrollAliases:t=!1}={}){let r=e.cwd,s=J.join(r,Ri,x1e),a;try{a=await le.statPromise(s)}catch{}if(!a)return null;let n=cs(await le.readFilePromise(s,"utf8"));if(n.__metadata.version>P1e)return null;let c=n.__metadata.nmMode||"classic",f=new Map,p=new Map;delete n.__metadata;for(let[h,E]of Object.entries(n)){let C=E.locations.map(x=>J.join(r,x)),S=E.bin;if(S)for(let[x,I]of Object.entries(S)){let T=J.join(r,fe.toPortablePath(x)),O=Ge.getMapWithDefault(p,T);for(let[U,V]of Object.entries(I))O.set(U,fe.toPortablePath([T,Ri,V].join(J.sep)))}if(f.set(h,{target:vt.dot,linkType:"HARD",locations:C,aliases:E.aliases||[]}),t&&E.aliases)for(let x of E.aliases){let{scope:I,name:T}=j.parseLocator(h),O=j.makeLocator(j.makeIdent(I,T),x),U=j.stringifyLocator(O);f.set(U,{target:vt.dot,linkType:"HARD",locations:C,aliases:[]})}}return{locatorMap:f,binSymlinks:p,locationTree:k1e(f,{skipPrefix:e.cwd}),nmMode:c,mtimeMs:a.mtimeMs}}var Ew=async(e,t)=>{if(e.split(J.sep).indexOf(Ri)<0)throw new Error(`Assertion failed: trying to remove dir that doesn't contain node_modules: ${e}`);try{let r;if(!t.innerLoop&&(r=await le.lstatPromise(e),!r.isDirectory()&&!r.isSymbolicLink()||r.isSymbolicLink()&&!t.isWorkspaceDir)){await le.unlinkPromise(e);return}let s=await le.readdirPromise(e,{withFileTypes:!0});for(let n of s){let c=J.join(e,n.name);n.isDirectory()?(n.name!==Ri||t&&t.innerLoop)&&await Ew(c,{innerLoop:!0,contentsOnly:!1}):await le.unlinkPromise(c)}let a=!t.innerLoop&&t.isWorkspaceDir&&r?.isSymbolicLink();!t.contentsOnly&&!a&&await le.rmdirPromise(e)}catch(r){if(r.code!=="ENOENT"&&r.code!=="ENOTEMPTY")throw r}},v1e=4,YF=(e,{skipPrefix:t})=>{let r=J.contains(t,e);if(r===null)throw new Error(`Assertion failed: Writing attempt prevented to ${e} which is outside project root: ${t}`);let s=r.split(J.sep).filter(p=>p!==""),a=s.indexOf(Ri),n=s.slice(0,a).join(J.sep),c=J.join(t,n),f=s.slice(a);return{locationRoot:c,segments:f}},k1e=(e,{skipPrefix:t})=>{let r=new Map;if(e===null)return r;let s=()=>({children:new Map,linkType:"HARD"});for(let[a,n]of e.entries()){if(n.linkType==="SOFT"&&J.contains(t,n.target)!==null){let f=Ge.getFactoryWithDefault(r,n.target,s);f.locator=a,f.linkType=n.linkType}for(let c of n.locations){let{locationRoot:f,segments:p}=YF(c,{skipPrefix:t}),h=Ge.getFactoryWithDefault(r,f,s);for(let E=0;E{if(process.platform==="win32"&&r==="junctions"){let s;try{s=await le.lstatPromise(e)}catch{}if(!s||s.isDirectory()){await le.symlinkPromise(e,t,"junction");return}}await le.symlinkPromise(J.relative(J.dirname(t),e),t)};async function Q1e(e,t,r){let s=J.join(e,`${KW.default.randomBytes(16).toString("hex")}.tmp`);try{await le.writeFilePromise(s,r);try{await le.linkPromise(s,t)}catch{}}finally{await le.unlinkPromise(s)}}async function n0t({srcPath:e,dstPath:t,entry:r,globalHardlinksStore:s,baseFs:a,nmMode:n}){if(r.kind==="file"){if(n.value==="hardlinks-global"&&s&&r.digest){let f=J.join(s,r.digest.substring(0,2),`${r.digest.substring(2)}.dat`),p;try{let h=await le.statPromise(f);if(h&&(!r.mtimeMs||h.mtimeMs>r.mtimeMs||h.mtimeMs{await le.mkdirPromise(e,{recursive:!0});let f=async(E=vt.dot)=>{let C=J.join(t,E),S=await r.readdirPromise(C,{withFileTypes:!0}),x=new Map;for(let I of S){let T=J.join(E,I.name),O,U=J.join(C,I.name);if(I.isFile()){if(O={kind:"file",mode:(await r.lstatPromise(U)).mode},a.value==="hardlinks-global"){let V=await Ln.checksumFile(U,{baseFs:r,algorithm:"sha1"});O.digest=V}}else if(I.isDirectory())O={kind:"directory"};else if(I.isSymbolicLink())O={kind:"symlink",symlinkTo:await r.readlinkPromise(U)};else throw new Error(`Unsupported file type (file: ${U}, mode: 0o${await r.statSync(U).mode.toString(8).padStart(6,"0")})`);if(x.set(T,O),I.isDirectory()&&T!==Ri){let V=await f(T);for(let[te,ie]of V)x.set(te,ie)}}return x},p;if(a.value==="hardlinks-global"&&s&&c){let E=J.join(s,c.substring(0,2),`${c.substring(2)}.json`);try{p=new Map(Object.entries(JSON.parse(await le.readFilePromise(E,"utf8"))))}catch{p=await f()}}else p=await f();let h=!1;for(let[E,C]of p){let S=J.join(t,E),x=J.join(e,E);if(C.kind==="directory")await le.mkdirPromise(x,{recursive:!0});else if(C.kind==="file"){let I=C.mtimeMs;await n0t({srcPath:S,dstPath:x,entry:C,nmMode:a,baseFs:r,globalHardlinksStore:s}),C.mtimeMs!==I&&(h=!0)}else C.kind==="symlink"&&await XW(J.resolve(J.dirname(x),C.symlinkTo),x,n)}if(a.value==="hardlinks-global"&&s&&h&&c){let E=J.join(s,c.substring(0,2),`${c.substring(2)}.json`);await le.removePromise(E),await Q1e(s,E,Buffer.from(JSON.stringify(Object.fromEntries(p))))}};function s0t(e,t,r,s){let a=new Map,n=new Map,c=new Map,f=!1,p=(h,E,C,S,x)=>{let I=!0,T=J.join(h,E),O=new Set;if(E===Ri||E.startsWith("@")){let V;try{V=le.statSync(T)}catch{}I=!!V,V?V.mtimeMs>r?(f=!0,O=new Set(le.readdirSync(T))):O=new Set(C.children.get(E).children.keys()):f=!0;let te=t.get(h);if(te){let ie=J.join(h,Ri,WF),ue;try{ue=le.statSync(ie)}catch{}if(!ue)f=!0;else if(ue.mtimeMs>r){f=!0;let ae=new Set(le.readdirSync(ie)),ge=new Map;n.set(h,ge);for(let[Ae,Ce]of te)ae.has(Ae)&&ge.set(Ae,Ce)}else n.set(h,te)}}else I=x.has(E);let U=C.children.get(E);if(I){let{linkType:V,locator:te}=U,ie={children:new Map,linkType:V,locator:te};if(S.children.set(E,ie),te){let ue=Ge.getSetWithDefault(c,te);ue.add(T),c.set(te,ue)}for(let ue of U.children.keys())p(T,ue,U,ie,O)}else U.locator&&s.storedBuildState.delete(j.parseLocator(U.locator).locatorHash)};for(let[h,E]of e){let{linkType:C,locator:S}=E,x={children:new Map,linkType:C,locator:S};if(a.set(h,x),S){let I=Ge.getSetWithDefault(c,E.locator);I.add(h),c.set(E.locator,I)}E.children.has(Ri)&&p(h,Ri,E,x,new Set)}return{locationTree:a,binSymlinks:n,locatorLocations:c,installChangedByUser:f}}function R1e(e){let t=j.parseDescriptor(e);return j.isVirtualDescriptor(t)&&(t=j.devirtualizeDescriptor(t)),t.range.startsWith("link:")}async function o0t(e,t,r,{loadManifest:s}){let a=new Map;for(let[f,{locations:p}]of e){let h=R1e(f)?null:await s(f,p[0]),E=new Map;if(h)for(let[C,S]of h.bin){let x=J.join(p[0],S);S!==""&&le.existsSync(x)&&E.set(C,S)}a.set(f,E)}let n=new Map,c=(f,p,h)=>{let E=new Map,C=J.contains(r,f);if(h.locator&&C!==null){let S=a.get(h.locator);for(let[x,I]of S){let T=J.join(f,fe.toPortablePath(I));E.set(x,T)}for(let[x,I]of h.children){let T=J.join(f,x),O=c(T,T,I);O.size>0&&n.set(f,new Map([...n.get(f)||new Map,...O]))}}else for(let[S,x]of h.children){let I=c(J.join(f,S),p,x);for(let[T,O]of I)E.set(T,O)}return E};for(let[f,p]of t){let h=c(f,f,p);h.size>0&&n.set(f,new Map([...n.get(f)||new Map,...h]))}return n}var S1e=(e,t)=>{if(!e||!t)return e===t;let r=j.parseLocator(e);j.isVirtualLocator(r)&&(r=j.devirtualizeLocator(r));let s=j.parseLocator(t);return j.isVirtualLocator(s)&&(s=j.devirtualizeLocator(s)),j.areLocatorsEqual(r,s)};function ZW(e){return J.join(e.get("globalFolder"),"store")}function a0t(e,t){let r=s=>{let a=s.split(J.sep),n=a.lastIndexOf(Ri);if(n<0||n==a.length-1)throw new Error(`Assertion failed. Path is outside of any node_modules package ${s}`);return a.slice(0,n+(a[n+1].startsWith("@")?3:2)).join(J.sep)};for(let s of e.values())for(let[a,n]of s)t.has(r(n))&&s.delete(a)}async function l0t(e,t,{baseFs:r,project:s,report:a,loadManifest:n,realLocatorChecksums:c}){let f=J.join(s.cwd,Ri),{locationTree:p,binSymlinks:h,locatorLocations:E,installChangedByUser:C}=s0t(e.locationTree,e.binSymlinks,e.mtimeMs,s),S=k1e(t,{skipPrefix:s.cwd}),x=[],I=async({srcDir:Ce,dstDir:Ee,linkType:d,globalHardlinksStore:Se,nmMode:Be,windowsLinkType:me,packageChecksum:ce})=>{let Z=(async()=>{try{d==="SOFT"?(await le.mkdirPromise(J.dirname(Ee),{recursive:!0}),await XW(J.resolve(Ce),Ee,me)):await i0t(Ee,Ce,{baseFs:r,globalHardlinksStore:Se,nmMode:Be,windowsLinkType:me,packageChecksum:ce})}catch(De){throw De.message=`While persisting ${Ce} -> ${Ee} ${De.message}`,De}finally{ie.tick()}})().then(()=>x.splice(x.indexOf(Z),1));x.push(Z),x.length>v1e&&await Promise.race(x)},T=async(Ce,Ee,d)=>{let Se=(async()=>{let Be=async(me,ce,Z)=>{try{Z.innerLoop||await le.mkdirPromise(ce,{recursive:!0});let De=await le.readdirPromise(me,{withFileTypes:!0});for(let Qe of De){if(!Z.innerLoop&&Qe.name===WF)continue;let st=J.join(me,Qe.name),_=J.join(ce,Qe.name);Qe.isDirectory()?(Qe.name!==Ri||Z&&Z.innerLoop)&&(await le.mkdirPromise(_,{recursive:!0}),await Be(st,_,{...Z,innerLoop:!0})):ge.value==="hardlinks-local"||ge.value==="hardlinks-global"?await le.linkPromise(st,_):await le.copyFilePromise(st,_,b1e.default.constants.COPYFILE_FICLONE)}}catch(De){throw Z.innerLoop||(De.message=`While cloning ${me} -> ${ce} ${De.message}`),De}finally{Z.innerLoop||ie.tick()}};await Be(Ce,Ee,d)})().then(()=>x.splice(x.indexOf(Se),1));x.push(Se),x.length>v1e&&await Promise.race(x)},O=async(Ce,Ee,d)=>{if(d)for(let[Se,Be]of Ee.children){let me=d.children.get(Se);await O(J.join(Ce,Se),Be,me)}else{Ee.children.has(Ri)&&await Ew(J.join(Ce,Ri),{contentsOnly:!1});let Se=J.basename(Ce)===Ri&&p.has(J.join(J.dirname(Ce)));await Ew(Ce,{contentsOnly:Ce===f,isWorkspaceDir:Se})}};for(let[Ce,Ee]of p){let d=S.get(Ce);for(let[Se,Be]of Ee.children){if(Se===".")continue;let me=d&&d.children.get(Se),ce=J.join(Ce,Se);await O(ce,Be,me)}}let U=async(Ce,Ee,d)=>{if(d){S1e(Ee.locator,d.locator)||await Ew(Ce,{contentsOnly:Ee.linkType==="HARD"});for(let[Se,Be]of Ee.children){let me=d.children.get(Se);await U(J.join(Ce,Se),Be,me)}}else{Ee.children.has(Ri)&&await Ew(J.join(Ce,Ri),{contentsOnly:!0});let Se=J.basename(Ce)===Ri&&S.has(J.join(J.dirname(Ce)));await Ew(Ce,{contentsOnly:Ee.linkType==="HARD",isWorkspaceDir:Se})}};for(let[Ce,Ee]of S){let d=p.get(Ce);for(let[Se,Be]of Ee.children){if(Se===".")continue;let me=d&&d.children.get(Se);await U(J.join(Ce,Se),Be,me)}}let V=new Map,te=[];for(let[Ce,Ee]of E)for(let d of Ee){let{locationRoot:Se,segments:Be}=YF(d,{skipPrefix:s.cwd}),me=S.get(Se),ce=Se;if(me){for(let Z of Be)if(ce=J.join(ce,Z),me=me.children.get(Z),!me)break;if(me){let Z=S1e(me.locator,Ce),De=t.get(me.locator),Qe=De.target,st=ce,_=De.linkType;if(Z)V.has(Qe)||V.set(Qe,st);else if(Qe!==st){let tt=j.parseLocator(me.locator);j.isVirtualLocator(tt)&&(tt=j.devirtualizeLocator(tt)),te.push({srcDir:Qe,dstDir:st,linkType:_,realLocatorHash:tt.locatorHash})}}}}for(let[Ce,{locations:Ee}]of t.entries())for(let d of Ee){let{locationRoot:Se,segments:Be}=YF(d,{skipPrefix:s.cwd}),me=p.get(Se),ce=S.get(Se),Z=Se,De=t.get(Ce),Qe=j.parseLocator(Ce);j.isVirtualLocator(Qe)&&(Qe=j.devirtualizeLocator(Qe));let st=Qe.locatorHash,_=De.target,tt=d;if(_===tt)continue;let Ne=De.linkType;for(let ke of Be)ce=ce.children.get(ke);if(!me)te.push({srcDir:_,dstDir:tt,linkType:Ne,realLocatorHash:st});else for(let ke of Be)if(Z=J.join(Z,ke),me=me.children.get(ke),!me){te.push({srcDir:_,dstDir:tt,linkType:Ne,realLocatorHash:st});break}}let ie=yo.progressViaCounter(te.length),ue=a.reportProgress(ie),ae=s.configuration.get("nmMode"),ge={value:ae},Ae=s.configuration.get("winLinkType");try{let Ce=ge.value==="hardlinks-global"?`${ZW(s.configuration)}/v1`:null;if(Ce&&!await le.existsPromise(Ce)){await le.mkdirpPromise(Ce);for(let d=0;d<256;d++)await le.mkdirPromise(J.join(Ce,d.toString(16).padStart(2,"0")))}for(let d of te)(d.linkType==="SOFT"||!V.has(d.srcDir))&&(V.set(d.srcDir,d.dstDir),await I({...d,globalHardlinksStore:Ce,nmMode:ge,windowsLinkType:Ae,packageChecksum:c.get(d.realLocatorHash)||null}));await Promise.all(x),x.length=0;for(let d of te){let Se=V.get(d.srcDir);d.linkType!=="SOFT"&&d.dstDir!==Se&&await T(Se,d.dstDir,{nmMode:ge})}await Promise.all(x),await le.mkdirPromise(f,{recursive:!0}),a0t(h,new Set(te.map(d=>d.dstDir)));let Ee=await o0t(t,S,s.cwd,{loadManifest:n});await c0t(h,Ee,s.cwd,Ae),await r0t(s,t,Ee,ge,{installChangedByUser:C}),ae=="hardlinks-global"&&ge.value=="hardlinks-local"&&a.reportWarningOnce(74,"'nmMode' has been downgraded to 'hardlinks-local' due to global cache and install folder being on different devices")}finally{ue.stop()}}async function c0t(e,t,r,s){for(let a of e.keys()){if(J.contains(r,a)===null)throw new Error(`Assertion failed. Excepted bin symlink location to be inside project dir, instead it was at ${a}`);if(!t.has(a)){let n=J.join(a,Ri,WF);await le.removePromise(n)}}for(let[a,n]of t){if(J.contains(r,a)===null)throw new Error(`Assertion failed. Excepted bin symlink location to be inside project dir, instead it was at ${a}`);let c=J.join(a,Ri,WF),f=e.get(a)||new Map;await le.mkdirPromise(c,{recursive:!0});for(let p of f.keys())n.has(p)||(await le.removePromise(J.join(c,p)),process.platform==="win32"&&await le.removePromise(J.join(c,`${p}.cmd`)));for(let[p,h]of n){let E=f.get(p),C=J.join(c,p);E!==h&&(process.platform==="win32"?await(0,D1e.default)(fe.fromPortablePath(h),fe.fromPortablePath(C),{createPwshFile:!1}):(await le.removePromise(C),await XW(h,C,s),J.contains(r,await le.realpathPromise(h))!==null&&await le.chmodPromise(h,493)))}}}qe();Dt();nA();var GD=class extends nd{constructor(){super(...arguments);this.mode="loose"}makeInstaller(r){return new $W(r)}},$W=class extends Ym{constructor(){super(...arguments);this.mode="loose"}async transformPnpSettings(r){let s=new mo({baseFs:new rA({maxOpenFiles:80,readOnlyArchives:!0})}),a=A1e(r,this.opts.project.cwd,s),{tree:n,errors:c}=kD(a,{pnpifyFs:!1,project:this.opts.project});if(!n){for(let{messageName:C,text:S}of c)this.opts.report.reportError(C,S);return}let f=new Map;r.fallbackPool=f;let p=(C,S)=>{let x=j.parseLocator(S.locator),I=j.stringifyIdent(x);I===C?f.set(C,x.reference):f.set(C,[I,x.reference])},h=J.join(this.opts.project.cwd,Er.nodeModules),E=n.get(h);if(!(typeof E>"u")){if("target"in E)throw new Error("Assertion failed: Expected the root junction point to be a directory");for(let C of E.dirList){let S=J.join(h,C),x=n.get(S);if(typeof x>"u")throw new Error("Assertion failed: Expected the child to have been registered");if("target"in x)p(C,x);else for(let I of x.dirList){let T=J.join(S,I),O=n.get(T);if(typeof O>"u")throw new Error("Assertion failed: Expected the subchild to have been registered");if("target"in O)p(`${C}/${I}`,O);else throw new Error("Assertion failed: Expected the leaf junction to be a package")}}}}};var u0t={hooks:{cleanGlobalArtifacts:async e=>{let t=ZW(e);await le.removePromise(t)}},configuration:{nmHoistingLimits:{description:"Prevents packages to be hoisted past specific levels",type:"STRING",values:["workspaces","dependencies","none"],default:"none"},nmMode:{description:"Defines in which measure Yarn must use hardlinks and symlinks when generated `node_modules` directories.",type:"STRING",values:["classic","hardlinks-local","hardlinks-global"],default:"classic"},nmSelfReferences:{description:"Defines whether the linker should generate self-referencing symlinks for workspaces.",type:"BOOLEAN",default:!0}},linkers:[jD,GD]},f0t=u0t;var eK={};Vt(eK,{NpmHttpFetcher:()=>VD,NpmRemapResolver:()=>JD,NpmSemverFetcher:()=>sh,NpmSemverResolver:()=>KD,NpmTagResolver:()=>zD,default:()=>Dvt,npmConfigUtils:()=>di,npmHttpUtils:()=>en,npmPublishUtils:()=>g1});qe();var _1e=et(pi());var ei="npm:";var en={};Vt(en,{AuthType:()=>L1e,customPackageError:()=>Vm,del:()=>P0t,get:()=>Jm,getIdentUrl:()=>WD,getPackageMetadata:()=>Bw,handleInvalidAuthenticationError:()=>sd,post:()=>D0t,put:()=>b0t});qe();qe();Dt();var nY=et(Vv());zl();var O1e=et(pi());var di={};Vt(di,{RegistryType:()=>F1e,getAuditRegistry:()=>A0t,getAuthConfiguration:()=>rY,getDefaultRegistry:()=>qD,getPublishRegistry:()=>p0t,getRegistryConfiguration:()=>N1e,getScopeConfiguration:()=>tY,getScopeRegistry:()=>Iw,isPackageApproved:()=>Cw,normalizeRegistry:()=>Vc});qe();var T1e=et(zo()),F1e=(s=>(s.AUDIT_REGISTRY="npmAuditRegistry",s.FETCH_REGISTRY="npmRegistryServer",s.PUBLISH_REGISTRY="npmPublishRegistry",s))(F1e||{});function Vc(e){return e.replace(/\/$/,"")}function A0t({configuration:e}){return qD({configuration:e,type:"npmAuditRegistry"})}function p0t(e,{configuration:t}){return e.publishConfig?.registry?Vc(e.publishConfig.registry):e.name?Iw(e.name.scope,{configuration:t,type:"npmPublishRegistry"}):qD({configuration:t,type:"npmPublishRegistry"})}function Iw(e,{configuration:t,type:r="npmRegistryServer"}){let s=tY(e,{configuration:t});if(s===null)return qD({configuration:t,type:r});let a=s.get(r);return a===null?qD({configuration:t,type:r}):Vc(a)}function qD({configuration:e,type:t="npmRegistryServer"}){let r=e.get(t);return Vc(r!==null?r:e.get("npmRegistryServer"))}function N1e(e,{configuration:t}){let r=t.get("npmRegistries"),s=Vc(e),a=r.get(s);if(typeof a<"u")return a;let n=r.get(s.replace(/^[a-z]+:/,""));return typeof n<"u"?n:null}var h0t=new Map([["npmRegistryServer","https://npm.jsr.io/"]]);function tY(e,{configuration:t}){if(e===null)return null;let s=t.get("npmScopes").get(e);return s||(e==="jsr"?h0t:null)}function rY(e,{configuration:t,ident:r}){let s=r&&tY(r.scope,{configuration:t});return s?.get("npmAuthIdent")||s?.get("npmAuthToken")?s:N1e(e,{configuration:t})||t}function d0t({configuration:e,version:t,publishTimes:r}){let s=e.get("npmMinimalAgeGate");if(s){let a=r?.[t];if(typeof a>"u"||(new Date().getTime()-new Date(a).getTime())/60/1e3g0t(t,r,s))}function Cw(e){return!d0t(e)||m0t(e)}var L1e=(a=>(a[a.NO_AUTH=0]="NO_AUTH",a[a.BEST_EFFORT=1]="BEST_EFFORT",a[a.CONFIGURATION=2]="CONFIGURATION",a[a.ALWAYS_AUTH=3]="ALWAYS_AUTH",a))(L1e||{});async function sd(e,{attemptedAs:t,registry:r,headers:s,configuration:a}){if(JF(e))throw new Lt(41,"Invalid OTP token");if(e.originalError?.name==="HTTPError"&&e.originalError?.response.statusCode===401)throw new Lt(41,`Invalid authentication (${typeof t!="string"?`as ${await k0t(r,s,{configuration:a})}`:`attempted as ${t}`})`)}function Vm(e,t){let r=e.response?.statusCode;return r?r===404?"Package not found":r>=500&&r<600?`The registry appears to be down (using a ${pe.applyHyperlink(t,"local cache","https://yarnpkg.com/advanced/lexicon#local-cache")} might have protected you against such outages)`:null:null}function WD(e){return e.scope?`/@${e.scope}%2f${e.name}`:`/${e.name}`}var M1e=new Map,y0t=new Map;async function E0t(e){return await Ge.getFactoryWithDefault(M1e,e,async()=>{let t=null;try{t=await le.readJsonPromise(e)}catch{}return t})}async function I0t(e,t,{configuration:r,cached:s,registry:a,headers:n,version:c,...f}){return await Ge.getFactoryWithDefault(y0t,e,async()=>await Jm(WD(t),{...f,customErrorMessage:Vm,configuration:r,registry:a,ident:t,headers:{...n,"If-None-Match":s?.etag,"If-Modified-Since":s?.lastModified},wrapNetworkRequest:async p=>async()=>{let h=await p();if(h.statusCode===304){if(s===null)throw new Error("Assertion failed: cachedMetadata should not be null");return{...h,body:s.metadata}}let E=w0t(JSON.parse(h.body.toString())),C={metadata:E,etag:h.headers.etag,lastModified:h.headers["last-modified"]};return M1e.set(e,Promise.resolve(C)),Promise.resolve().then(async()=>{let S=`${e}-${process.pid}.tmp`;await le.mkdirPromise(J.dirname(S),{recursive:!0}),await le.writeJsonPromise(S,C,{compact:!0}),await le.renamePromise(S,e)}).catch(()=>{}),{...h,body:E}}}))}function C0t(e){return e.scope!==null?`@${e.scope}-${e.name}-${e.scope.length}`:e.name}async function Bw(e,{cache:t,project:r,registry:s,headers:a,version:n,...c}){let{configuration:f}=r;s=YD(f,{ident:e,registry:s});let p=v0t(f,s),h=J.join(p,`${C0t(e)}.json`),E=null;if(!r.lockfileNeedsRefresh&&(E=await E0t(h),E)){if(typeof n<"u"&&typeof E.metadata.versions[n]<"u")return E.metadata;if(f.get("enableOfflineMode")){let C=structuredClone(E.metadata),S=new Set;if(t){for(let I of Object.keys(C.versions)){let T=j.makeLocator(e,`npm:${I}`),O=t.getLocatorMirrorPath(T);(!O||!le.existsSync(O))&&(delete C.versions[I],S.add(I))}let x=C["dist-tags"].latest;if(S.has(x)){let I=Object.keys(E.metadata.versions).sort(O1e.default.compare),T=I.indexOf(x);for(;S.has(I[T])&&T>=0;)T-=1;T>=0?C["dist-tags"].latest=I[T]:delete C["dist-tags"].latest}}return C}}return await I0t(h,e,{...c,configuration:f,cached:E,registry:s,headers:a,version:n})}var U1e=["name","dist.tarball","bin","scripts","os","cpu","libc","dependencies","dependenciesMeta","optionalDependencies","peerDependencies","peerDependenciesMeta","deprecated"];function w0t(e){return{"dist-tags":e["dist-tags"],versions:Object.fromEntries(Object.entries(e.versions).map(([t,r])=>[t,Vg(r,U1e)])),time:e.time}}var B0t=Ln.makeHash("time",...U1e).slice(0,6);function v0t(e,t){let r=S0t(e),s=new URL(t);return J.join(r,B0t,s.hostname)}function S0t(e){return J.join(e.get("globalFolder"),"metadata/npm")}async function Jm(e,{configuration:t,headers:r,ident:s,authType:a,allowOidc:n,registry:c,...f}){c=YD(t,{ident:s,registry:c}),s&&s.scope&&typeof a>"u"&&(a=1);let p=await VF(c,{authType:a,allowOidc:n,configuration:t,ident:s});p&&(r={...r,authorization:p});try{return await nn.get(e.charAt(0)==="/"?`${c}${e}`:e,{configuration:t,headers:r,...f})}catch(h){throw await sd(h,{registry:c,configuration:t,headers:r}),h}}async function D0t(e,t,{attemptedAs:r,configuration:s,headers:a,ident:n,authType:c=3,allowOidc:f,registry:p,otp:h,...E}){p=YD(s,{ident:n,registry:p});let C=await VF(p,{authType:c,allowOidc:f,configuration:s,ident:n});C&&(a={...a,authorization:C}),h&&(a={...a,...ww(h)});try{return await nn.post(p+e,t,{configuration:s,headers:a,...E})}catch(S){if(!JF(S)||h)throw await sd(S,{attemptedAs:r,registry:p,configuration:s,headers:a}),S;h=await iY(S,{configuration:s});let x={...a,...ww(h)};try{return await nn.post(`${p}${e}`,t,{configuration:s,headers:x,...E})}catch(I){throw await sd(I,{attemptedAs:r,registry:p,configuration:s,headers:a}),I}}}async function b0t(e,t,{attemptedAs:r,configuration:s,headers:a,ident:n,authType:c=3,allowOidc:f,registry:p,otp:h,...E}){p=YD(s,{ident:n,registry:p});let C=await VF(p,{authType:c,allowOidc:f,configuration:s,ident:n});C&&(a={...a,authorization:C}),h&&(a={...a,...ww(h)});try{return await nn.put(p+e,t,{configuration:s,headers:a,...E})}catch(S){if(!JF(S))throw await sd(S,{attemptedAs:r,registry:p,configuration:s,headers:a}),S;h=await iY(S,{configuration:s});let x={...a,...ww(h)};try{return await nn.put(`${p}${e}`,t,{configuration:s,headers:x,...E})}catch(I){throw await sd(I,{attemptedAs:r,registry:p,configuration:s,headers:a}),I}}}async function P0t(e,{attemptedAs:t,configuration:r,headers:s,ident:a,authType:n=3,allowOidc:c,registry:f,otp:p,...h}){f=YD(r,{ident:a,registry:f});let E=await VF(f,{authType:n,allowOidc:c,configuration:r,ident:a});E&&(s={...s,authorization:E}),p&&(s={...s,...ww(p)});try{return await nn.del(f+e,{configuration:r,headers:s,...h})}catch(C){if(!JF(C)||p)throw await sd(C,{attemptedAs:t,registry:f,configuration:r,headers:s}),C;p=await iY(C,{configuration:r});let S={...s,...ww(p)};try{return await nn.del(`${f}${e}`,{configuration:r,headers:S,...h})}catch(x){throw await sd(x,{attemptedAs:t,registry:f,configuration:r,headers:s}),x}}}function YD(e,{ident:t,registry:r}){if(typeof r>"u"&&t)return Iw(t.scope,{configuration:e});if(typeof r!="string")throw new Error("Assertion failed: The registry should be a string");return Vc(r)}async function VF(e,{authType:t=2,allowOidc:r=!1,configuration:s,ident:a}){let n=rY(e,{configuration:s,ident:a}),c=x0t(n,t);if(!c)return null;let f=await s.reduceHook(p=>p.getNpmAuthenticationHeader,void 0,e,{configuration:s,ident:a});if(f)return f;if(n.get("npmAuthToken"))return`Bearer ${n.get("npmAuthToken")}`;if(n.get("npmAuthIdent")){let p=n.get("npmAuthIdent");return p.includes(":")?`Basic ${Buffer.from(p).toString("base64")}`:`Basic ${p}`}if(r&&a){let p=await Q0t(e,{configuration:s,ident:a});if(p)return`Bearer ${p}`}if(c&&t!==1)throw new Lt(33,"No authentication configured for request");return null}function x0t(e,t){switch(t){case 2:return e.get("npmAlwaysAuth");case 1:case 3:return!0;case 0:return!1;default:throw new Error("Unreachable")}}async function k0t(e,t,{configuration:r}){if(typeof t>"u"||typeof t.authorization>"u")return"an anonymous user";try{return(await nn.get(new URL(`${e}/-/whoami`).href,{configuration:r,headers:t,jsonResponse:!0})).username??"an unknown user"}catch{return"an unknown user"}}async function iY(e,{configuration:t}){let r=e.originalError?.response.headers["npm-notice"];if(r&&(await Ot.start({configuration:t,stdout:process.stdout,includeFooter:!1},async a=>{if(a.reportInfo(0,r.replace(/(https?:\/\/\S+)/g,pe.pretty(t,"$1",pe.Type.URL))),!process.env.YARN_IS_TEST_ENV){let n=r.match(/open (https?:\/\/\S+)/i);if(n&&Ui.openUrl){let{openNow:c}=await(0,nY.prompt)({type:"confirm",name:"openNow",message:"Do you want to try to open this url now?",required:!0,initial:!0,onCancel:()=>process.exit(130)});c&&(await Ui.openUrl(n[1])||(a.reportSeparator(),a.reportWarning(0,"We failed to automatically open the url; you'll have to open it yourself in your browser of choice.")))}}}),process.stdout.write(` `)),process.env.YARN_IS_TEST_ENV)return process.env.YARN_INJECT_NPM_2FA_TOKEN||"";let{otp:s}=await(0,nY.prompt)({type:"password",name:"otp",message:"One-time password:",required:!0,onCancel:()=>process.exit(130)});return process.stdout.write(` `),s}function JF(e){if(e.originalError?.name!=="HTTPError")return!1;try{return(e.originalError?.response.headers["www-authenticate"].split(/,\s*/).map(r=>r.toLowerCase())).includes("otp")}catch{return!1}}function ww(e){return{"npm-otp":e}}async function Q0t(e,{configuration:t,ident:r}){let s=null;if(process.env.GITLAB_CI)s=process.env.NPM_ID_TOKEN||null;else if(process.env.CIRCLECI)s=process.env.NPM_ID_TOKEN||null;else if(process.env.GITHUB_ACTIONS){if(!(process.env.ACTIONS_ID_TOKEN_REQUEST_URL&&process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN))return null;let a=`npm:${new URL(e).host.replace("registry.yarnpkg.com","registry.npmjs.org").replace("yarn.npmjs.org","registry.npmjs.org")}`,n=new URL(process.env.ACTIONS_ID_TOKEN_REQUEST_URL);n.searchParams.append("audience",a),s=(await nn.get(n.href,{configuration:t,jsonResponse:!0,headers:{Authorization:`Bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}`}})).value}if(!s)return null;try{return(await nn.post(`${e}/-/npm/v1/oidc/token/exchange/package${WD(r)}`,null,{configuration:t,jsonResponse:!0,headers:{Authorization:`Bearer ${s}`}})).token||null}catch{}return null}var VD=class{supports(t,r){if(!t.reference.startsWith(ei))return!1;let{selector:s,params:a}=j.parseRange(t.reference);return!(!_1e.default.valid(s)||a===null||typeof a.__archiveUrl!="string")}getLocalPath(t,r){return null}async fetch(t,r){let s=r.checksums.get(t.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(t,s,{onHit:()=>r.report.reportCacheHit(t),onMiss:()=>r.report.reportCacheMiss(t,`${j.prettyLocator(r.project.configuration,t)} can't be found in the cache and will be fetched from the remote server`),loader:()=>this.fetchFromNetwork(t,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:j.getIdentVendorPath(t),checksum:c}}async fetchFromNetwork(t,r){let{params:s}=j.parseRange(t.reference);if(s===null||typeof s.__archiveUrl!="string")throw new Error("Assertion failed: The archiveUrl querystring parameter should have been available");let a=await Jm(s.__archiveUrl,{customErrorMessage:Vm,configuration:r.project.configuration,ident:t});return await gs.convertToZip(a,{configuration:r.project.configuration,prefixPath:j.getIdentVendorPath(t),stripComponents:1})}};qe();var JD=class{supportsDescriptor(t,r){return!(!t.range.startsWith(ei)||!j.tryParseDescriptor(t.range.slice(ei.length),!0))}supportsLocator(t,r){return!1}shouldPersistResolution(t,r){throw new Error("Unreachable")}bindDescriptor(t,r,s){return t}getResolutionDependencies(t,r){let s=r.project.configuration.normalizeDependency(j.parseDescriptor(t.range.slice(ei.length),!0));return r.resolver.getResolutionDependencies(s,r)}async getCandidates(t,r,s){let a=s.project.configuration.normalizeDependency(j.parseDescriptor(t.range.slice(ei.length),!0));return await s.resolver.getCandidates(a,r,s)}async getSatisfying(t,r,s,a){let n=a.project.configuration.normalizeDependency(j.parseDescriptor(t.range.slice(ei.length),!0));return a.resolver.getSatisfying(n,r,s,a)}resolve(t,r){throw new Error("Unreachable")}};qe();qe();var H1e=et(pi());var sh=class e{supports(t,r){if(!t.reference.startsWith(ei))return!1;let s=new URL(t.reference);return!(!H1e.default.valid(s.pathname)||s.searchParams.has("__archiveUrl"))}getLocalPath(t,r){return null}async fetch(t,r){let s=r.checksums.get(t.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(t,s,{onHit:()=>r.report.reportCacheHit(t),onMiss:()=>r.report.reportCacheMiss(t,`${j.prettyLocator(r.project.configuration,t)} can't be found in the cache and will be fetched from the remote registry`),loader:()=>this.fetchFromNetwork(t,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:j.getIdentVendorPath(t),checksum:c}}async fetchFromNetwork(t,r){let s;try{s=await Jm(e.getLocatorUrl(t),{customErrorMessage:Vm,configuration:r.project.configuration,ident:t})}catch{s=await Jm(e.getLocatorUrl(t).replace(/%2f/g,"/"),{customErrorMessage:Vm,configuration:r.project.configuration,ident:t})}return await gs.convertToZip(s,{configuration:r.project.configuration,prefixPath:j.getIdentVendorPath(t),stripComponents:1})}static isConventionalTarballUrl(t,r,{configuration:s}){let a=Iw(t.scope,{configuration:s}),n=e.getLocatorUrl(t);return r=r.replace(/^https?:(\/\/(?:[^/]+\.)?npmjs.org(?:$|\/))/,"https:$1"),a=a.replace(/^https:\/\/registry\.npmjs\.org($|\/)/,"https://registry.yarnpkg.com$1"),r=r.replace(/^https:\/\/registry\.npmjs\.org($|\/)/,"https://registry.yarnpkg.com$1"),r===a+n||r===a+n.replace(/%2f/g,"/")}static getLocatorUrl(t){let r=kr.clean(t.reference.slice(ei.length));if(r===null)throw new Lt(10,"The npm semver resolver got selected, but the version isn't semver");return`${WD(t)}/-/${t.name}-${r}.tgz`}};qe();qe();qe();var sY=et(pi());var KF=j.makeIdent(null,"node-gyp"),R0t=/\b(node-gyp|prebuild-install)\b/,KD=class{supportsDescriptor(t,r){return t.range.startsWith(ei)?!!kr.validRange(t.range.slice(ei.length)):!1}supportsLocator(t,r){if(!t.reference.startsWith(ei))return!1;let{selector:s}=j.parseRange(t.reference);return!!sY.default.valid(s)}shouldPersistResolution(t,r){return!0}bindDescriptor(t,r,s){return t}getResolutionDependencies(t,r){return{}}async getCandidates(t,r,s){let a=kr.validRange(t.range.slice(ei.length));if(a===null)throw new Error(`Expected a valid range, got ${t.range.slice(ei.length)}`);let n=await Bw(t,{cache:s.fetchOptions?.cache,project:s.project,version:sY.default.valid(a.raw)?a.raw:void 0}),c=Ge.mapAndFilter(Object.keys(n.versions),E=>{try{let C=new kr.SemVer(E);if(a.test(C))return C}catch{}return Ge.mapAndFilter.skip}),f=c.filter(E=>Cw({configuration:s.project.configuration,ident:t,version:E.raw,publishTimes:n.time}));if(c.length>0&&f.length===0)throw new Lt(16,`All versions satisfying "${t.range.slice(ei.length)}" are quarantined`);let p=f.filter(E=>!n.versions[E.raw].deprecated),h=p.length>0?p:f;return h.sort((E,C)=>-E.compare(C)),h.map(E=>{let C=j.makeLocator(t,`${ei}${E.raw}`),S=n.versions[E.raw].dist.tarball;return sh.isConventionalTarballUrl(C,S,{configuration:s.project.configuration})?C:j.bindLocator(C,{__archiveUrl:S})})}async getSatisfying(t,r,s,a){let n=kr.validRange(t.range.slice(ei.length));if(n===null)throw new Error(`Expected a valid range, got ${t.range.slice(ei.length)}`);return{locators:Ge.mapAndFilter(s,p=>{if(p.identHash!==t.identHash)return Ge.mapAndFilter.skip;let h=j.tryParseRange(p.reference,{requireProtocol:ei});if(!h)return Ge.mapAndFilter.skip;let E=new kr.SemVer(h.selector);return n.test(E)?{locator:p,version:E}:Ge.mapAndFilter.skip}).sort((p,h)=>-p.version.compare(h.version)).map(({locator:p})=>p),sorted:!0}}async resolve(t,r){let{selector:s}=j.parseRange(t.reference),a=kr.clean(s);if(a===null)throw new Lt(10,"The npm semver resolver got selected, but the version isn't semver");let n=await Bw(t,{cache:r.fetchOptions?.cache,project:r.project,version:a});if(!Object.hasOwn(n,"versions"))throw new Lt(15,'Registry returned invalid data for - missing "versions" field');if(!Object.hasOwn(n.versions,a))throw new Lt(16,`Registry failed to return reference "${a}"`);let c=new _t;if(c.load(n.versions[a]),!c.dependencies.has(KF.identHash)&&!c.peerDependencies.has(KF.identHash)){for(let f of c.scripts.values())if(f.match(R0t)){c.dependencies.set(KF.identHash,j.makeDescriptor(KF,"latest"));break}}return{...t,version:a,languageName:"node",linkType:"HARD",conditions:c.getConditions(),dependencies:r.project.configuration.normalizeDependencyMap(c.dependencies),peerDependencies:c.peerDependencies,dependenciesMeta:c.dependenciesMeta,peerDependenciesMeta:c.peerDependenciesMeta,bin:c.bin}}};qe();qe();var zF=et(pi());var zD=class{supportsDescriptor(t,r){return!(!t.range.startsWith(ei)||!_p.test(t.range.slice(ei.length)))}supportsLocator(t,r){return!1}shouldPersistResolution(t,r){throw new Error("Unreachable")}bindDescriptor(t,r,s){return t}getResolutionDependencies(t,r){return{}}async getCandidates(t,r,s){let a=t.range.slice(ei.length),n=await Bw(t,{cache:s.fetchOptions?.cache,project:s.project});if(!Object.hasOwn(n,"dist-tags"))throw new Lt(15,'Registry returned invalid data - missing "dist-tags" field');let c=n["dist-tags"];if(!Object.hasOwn(c,a))throw new Lt(16,`Registry failed to return tag "${a}"`);let f=Object.keys(n.versions),p=n.time,h=c[a];if(a==="latest"&&!Cw({configuration:s.project.configuration,ident:t,version:h,publishTimes:p})){let S=h.includes("-"),x=zF.default.rsort(f).find(I=>zF.default.lt(I,h)&&(S||!I.includes("-"))&&Cw({configuration:s.project.configuration,ident:t,version:I,publishTimes:p}));if(!x)throw new Lt(16,`The version for tag "${a}" is quarantined, and no lower version is available`);h=x}let E=j.makeLocator(t,`${ei}${h}`),C=n.versions[h].dist.tarball;return sh.isConventionalTarballUrl(E,C,{configuration:s.project.configuration})?[E]:[j.bindLocator(E,{__archiveUrl:C})]}async getSatisfying(t,r,s,a){let n=[];for(let c of s){if(c.identHash!==t.identHash)continue;let f=j.tryParseRange(c.reference,{requireProtocol:ei});if(!(!f||!zF.default.valid(f.selector))){if(f.params?.__archiveUrl){let p=j.makeRange({protocol:ei,selector:f.selector,source:null,params:null}),[h]=await a.resolver.getCandidates(j.makeDescriptor(t,p),r,a);if(c.reference!==h.reference)continue}n.push(c)}}return{locators:n,sorted:!1}}async resolve(t,r){throw new Error("Unreachable")}};var g1={};Vt(g1,{getGitHead:()=>Bvt,getPublishAccess:()=>Qbe,getReadmeContent:()=>Rbe,makePublishBody:()=>wvt});qe();qe();Dt();var VY={};Vt(VY,{PackCommand:()=>Fw,default:()=>fmt,packUtils:()=>IA});qe();qe();qe();Dt();Yt();var IA={};Vt(IA,{genPackList:()=>EN,genPackStream:()=>YY,genPackageManifest:()=>yBe,hasPackScripts:()=>qY,prepareForPack:()=>WY});qe();Dt();var GY=et(zo()),gBe=et(ABe()),mBe=Ie("zlib"),emt=["/package.json","/readme","/readme.*","/license","/license.*","/licence","/licence.*","/changelog","/changelog.*"],tmt=["/package.tgz",".github",".git",".hg","node_modules",".npmignore",".gitignore",".#*",".DS_Store"];async function qY(e){return!!(Cn.hasWorkspaceScript(e,"prepack")||Cn.hasWorkspaceScript(e,"postpack"))}async function WY(e,{report:t},r){await Cn.maybeExecuteWorkspaceLifecycleScript(e,"prepack",{report:t});try{let s=J.join(e.cwd,_t.fileName);await le.existsPromise(s)&&await e.manifest.loadFile(s,{baseFs:le}),await r()}finally{await Cn.maybeExecuteWorkspaceLifecycleScript(e,"postpack",{report:t})}}async function YY(e,t){typeof t>"u"&&(t=await EN(e));let r=new Set;for(let n of e.manifest.publishConfig?.executableFiles??new Set)r.add(J.normalize(n));for(let n of e.manifest.bin.values())r.add(J.normalize(n));let s=gBe.default.pack();process.nextTick(async()=>{for(let n of t){let c=J.normalize(n),f=J.resolve(e.cwd,c),p=J.join("package",c),h=await le.lstatPromise(f),E={name:p,mtime:new Date(Ai.SAFE_TIME*1e3)},C=r.has(c)?493:420,S,x,I=new Promise((O,U)=>{S=O,x=U}),T=O=>{O?x(O):S()};if(h.isFile()){let O;c==="package.json"?O=Buffer.from(JSON.stringify(await yBe(e),null,2)):O=await le.readFilePromise(f),s.entry({...E,mode:C,type:"file"},O,T)}else h.isSymbolicLink()?s.entry({...E,mode:C,type:"symlink",linkname:await le.readlinkPromise(f)},T):T(new Error(`Unsupported file type ${h.mode} for ${fe.fromPortablePath(c)}`));await I}s.finalize()});let a=(0,mBe.createGzip)();return s.pipe(a),a}async function yBe(e){let t=JSON.parse(JSON.stringify(e.manifest.raw));return await e.project.configuration.triggerHook(r=>r.beforeWorkspacePacking,e,t),t}async function EN(e){let t=e.project,r=t.configuration,s={accept:[],reject:[]};for(let C of tmt)s.reject.push(C);for(let C of emt)s.accept.push(C);s.reject.push(r.get("rcFilename"));let a=C=>{if(C===null||!C.startsWith(`${e.cwd}/`))return;let S=J.relative(e.cwd,C),x=J.resolve(vt.root,S);s.reject.push(x)};a(J.resolve(t.cwd,Er.lockfile)),a(r.get("cacheFolder")),a(r.get("globalFolder")),a(r.get("installStatePath")),a(r.get("virtualFolder")),a(r.get("yarnPath")),await r.triggerHook(C=>C.populateYarnPaths,t,C=>{a(C)});for(let C of t.workspaces){let S=J.relative(e.cwd,C.cwd);S!==""&&!S.match(/^(\.\.)?\//)&&s.reject.push(`/${S}`)}let n={accept:[],reject:[]},c=e.manifest.publishConfig?.main??e.manifest.main,f=e.manifest.publishConfig?.module??e.manifest.module,p=e.manifest.publishConfig?.browser??e.manifest.browser,h=e.manifest.publishConfig?.bin??e.manifest.bin;c!=null&&n.accept.push(J.resolve(vt.root,c)),f!=null&&n.accept.push(J.resolve(vt.root,f)),typeof p=="string"&&n.accept.push(J.resolve(vt.root,p));for(let C of h.values())n.accept.push(J.resolve(vt.root,C));if(p instanceof Map)for(let[C,S]of p.entries())n.accept.push(J.resolve(vt.root,C)),typeof S=="string"&&n.accept.push(J.resolve(vt.root,S));let E=e.manifest.files!==null;if(E){n.reject.push("/*");for(let C of e.manifest.files)EBe(n.accept,C,{cwd:vt.root})}return await rmt(e.cwd,{hasExplicitFileList:E,globalList:s,ignoreList:n})}async function rmt(e,{hasExplicitFileList:t,globalList:r,ignoreList:s}){let a=[],n=new qf(e),c=[[vt.root,[s]]];for(;c.length>0;){let[f,p]=c.pop(),h=await n.lstatPromise(f);if(!hBe(f,{globalList:r,ignoreLists:h.isDirectory()?null:p}))if(h.isDirectory()){let E=await n.readdirPromise(f),C=!1,S=!1;if(!t||f!==vt.root)for(let T of E)C=C||T===".gitignore",S=S||T===".npmignore";let x=S?await pBe(n,f,".npmignore"):C?await pBe(n,f,".gitignore"):null,I=x!==null?[x].concat(p):p;hBe(f,{globalList:r,ignoreLists:p})&&(I=[...p,{accept:[],reject:["**/*"]}]);for(let T of E)c.push([J.resolve(f,T),I])}else(h.isFile()||h.isSymbolicLink())&&a.push(J.relative(vt.root,f))}return a.sort()}async function pBe(e,t,r){let s={accept:[],reject:[]},a=await e.readFilePromise(J.join(t,r),"utf8");for(let n of a.split(/\n/g))EBe(s.reject,n,{cwd:t});return s}function nmt(e,{cwd:t}){let r=e[0]==="!";return r&&(e=e.slice(1)),e.match(/\.{0,1}\//)&&(e=J.resolve(t,e)),r&&(e=`!${e}`),e}function EBe(e,t,{cwd:r}){let s=t.trim();s===""||s[0]==="#"||e.push(nmt(s,{cwd:r}))}function hBe(e,{globalList:t,ignoreLists:r}){let s=yN(e,t.accept);if(s!==0)return s===2;let a=yN(e,t.reject);if(a!==0)return a===1;if(r!==null)for(let n of r){let c=yN(e,n.accept);if(c!==0)return c===2;let f=yN(e,n.reject);if(f!==0)return f===1}return!1}function yN(e,t){let r=t,s=[];for(let a=0;a{await WY(a,{report:p},async()=>{p.reportJson({base:fe.fromPortablePath(a.cwd)});let h=await EN(a);for(let E of h)p.reportInfo(null,fe.fromPortablePath(E)),p.reportJson({location:fe.fromPortablePath(E)});if(!this.dryRun){let E=await YY(a,h);await le.mkdirPromise(J.dirname(c),{recursive:!0});let C=le.createWriteStream(c);E.pipe(C),await new Promise(S=>{C.on("finish",S)})}}),this.dryRun||(p.reportInfo(0,`Package archive generated in ${pe.pretty(r,c,pe.Type.PATH)}`),p.reportJson({output:fe.fromPortablePath(c)}))})).exitCode()}};function imt(e,{workspace:t}){let r=e.replace("%s",smt(t)).replace("%v",omt(t));return fe.toPortablePath(r)}function smt(e){return e.manifest.name!==null?j.slugifyIdent(e.manifest.name):"package"}function omt(e){return e.manifest.version!==null?e.manifest.version:"unknown"}var amt=["dependencies","devDependencies","peerDependencies"],lmt="workspace:",cmt=(e,t)=>{t.publishConfig&&(t.publishConfig.type&&(t.type=t.publishConfig.type),t.publishConfig.main&&(t.main=t.publishConfig.main),t.publishConfig.browser&&(t.browser=t.publishConfig.browser),t.publishConfig.module&&(t.module=t.publishConfig.module),t.publishConfig.exports&&(t.exports=t.publishConfig.exports),t.publishConfig.imports&&(t.imports=t.publishConfig.imports),t.publishConfig.bin&&(t.bin=t.publishConfig.bin));let r=e.project;for(let s of amt)for(let a of e.manifest.getForScope(s).values()){let n=r.tryWorkspaceByDescriptor(a),c=j.parseRange(a.range);if(c.protocol===lmt)if(n===null){if(r.tryWorkspaceByIdent(a)===null)throw new Lt(21,`${j.prettyDescriptor(r.configuration,a)}: No local workspace found for this range`)}else{let f;j.areDescriptorsEqual(a,n.anchoredDescriptor)||c.selector==="*"?f=n.manifest.version??"0.0.0":c.selector==="~"||c.selector==="^"?f=`${c.selector}${n.manifest.version??"0.0.0"}`:f=c.selector;let p=s==="dependencies"?j.makeDescriptor(a,"unknown"):null,h=p!==null&&e.manifest.ensureDependencyMeta(p).optional?"optionalDependencies":s;t[h][j.stringifyIdent(a)]=f}}},umt={hooks:{beforeWorkspacePacking:cmt},commands:[Fw]},fmt=umt;var kbe=et(PBe());qe();var Pbe=et(bbe()),{env:Bt}=process,pvt="application/vnd.in-toto+json",hvt="https://in-toto.io/Statement/v0.1",dvt="https://in-toto.io/Statement/v1",gvt="https://slsa.dev/provenance/v0.2",mvt="https://slsa.dev/provenance/v1",yvt="https://github.com/actions/runner",Evt="https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",Ivt="https://github.com/npm/cli/gitlab",Cvt="v0alpha1",xbe=async(e,t)=>{let r;if(Bt.GITHUB_ACTIONS){if(!Bt.ACTIONS_ID_TOKEN_REQUEST_URL)throw new Lt(91,'Provenance generation in GitHub Actions requires "write" access to the "id-token" permission');let s=(Bt.GITHUB_WORKFLOW_REF||"").replace(`${Bt.GITHUB_REPOSITORY}/`,""),a=s.indexOf("@"),n=s.slice(0,a),c=s.slice(a+1);r={_type:dvt,subject:e,predicateType:mvt,predicate:{buildDefinition:{buildType:Evt,externalParameters:{workflow:{ref:c,repository:`${Bt.GITHUB_SERVER_URL}/${Bt.GITHUB_REPOSITORY}`,path:n}},internalParameters:{github:{event_name:Bt.GITHUB_EVENT_NAME,repository_id:Bt.GITHUB_REPOSITORY_ID,repository_owner_id:Bt.GITHUB_REPOSITORY_OWNER_ID}},resolvedDependencies:[{uri:`git+${Bt.GITHUB_SERVER_URL}/${Bt.GITHUB_REPOSITORY}@${Bt.GITHUB_REF}`,digest:{gitCommit:Bt.GITHUB_SHA}}]},runDetails:{builder:{id:`${yvt}/${Bt.RUNNER_ENVIRONMENT}`},metadata:{invocationId:`${Bt.GITHUB_SERVER_URL}/${Bt.GITHUB_REPOSITORY}/actions/runs/${Bt.GITHUB_RUN_ID}/attempts/${Bt.GITHUB_RUN_ATTEMPT}`}}}}}else if(Bt.GITLAB_CI){if(!Bt.SIGSTORE_ID_TOKEN)throw new Lt(91,`Provenance generation in GitLab CI requires "SIGSTORE_ID_TOKEN" with "sigstore" audience to be present in "id_tokens". For more info see: https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html`);r={_type:hvt,subject:e,predicateType:gvt,predicate:{buildType:`${Ivt}/${Cvt}`,builder:{id:`${Bt.CI_PROJECT_URL}/-/runners/${Bt.CI_RUNNER_ID}`},invocation:{configSource:{uri:`git+${Bt.CI_PROJECT_URL}`,digest:{sha1:Bt.CI_COMMIT_SHA},entryPoint:Bt.CI_JOB_NAME},parameters:{CI:Bt.CI,CI_API_GRAPHQL_URL:Bt.CI_API_GRAPHQL_URL,CI_API_V4_URL:Bt.CI_API_V4_URL,CI_BUILD_BEFORE_SHA:Bt.CI_BUILD_BEFORE_SHA,CI_BUILD_ID:Bt.CI_BUILD_ID,CI_BUILD_NAME:Bt.CI_BUILD_NAME,CI_BUILD_REF:Bt.CI_BUILD_REF,CI_BUILD_REF_NAME:Bt.CI_BUILD_REF_NAME,CI_BUILD_REF_SLUG:Bt.CI_BUILD_REF_SLUG,CI_BUILD_STAGE:Bt.CI_BUILD_STAGE,CI_COMMIT_BEFORE_SHA:Bt.CI_COMMIT_BEFORE_SHA,CI_COMMIT_BRANCH:Bt.CI_COMMIT_BRANCH,CI_COMMIT_REF_NAME:Bt.CI_COMMIT_REF_NAME,CI_COMMIT_REF_PROTECTED:Bt.CI_COMMIT_REF_PROTECTED,CI_COMMIT_REF_SLUG:Bt.CI_COMMIT_REF_SLUG,CI_COMMIT_SHA:Bt.CI_COMMIT_SHA,CI_COMMIT_SHORT_SHA:Bt.CI_COMMIT_SHORT_SHA,CI_COMMIT_TIMESTAMP:Bt.CI_COMMIT_TIMESTAMP,CI_COMMIT_TITLE:Bt.CI_COMMIT_TITLE,CI_CONFIG_PATH:Bt.CI_CONFIG_PATH,CI_DEFAULT_BRANCH:Bt.CI_DEFAULT_BRANCH,CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX:Bt.CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX,CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX:Bt.CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX,CI_DEPENDENCY_PROXY_SERVER:Bt.CI_DEPENDENCY_PROXY_SERVER,CI_DEPENDENCY_PROXY_USER:Bt.CI_DEPENDENCY_PROXY_USER,CI_JOB_ID:Bt.CI_JOB_ID,CI_JOB_NAME:Bt.CI_JOB_NAME,CI_JOB_NAME_SLUG:Bt.CI_JOB_NAME_SLUG,CI_JOB_STAGE:Bt.CI_JOB_STAGE,CI_JOB_STARTED_AT:Bt.CI_JOB_STARTED_AT,CI_JOB_URL:Bt.CI_JOB_URL,CI_NODE_TOTAL:Bt.CI_NODE_TOTAL,CI_PAGES_DOMAIN:Bt.CI_PAGES_DOMAIN,CI_PAGES_URL:Bt.CI_PAGES_URL,CI_PIPELINE_CREATED_AT:Bt.CI_PIPELINE_CREATED_AT,CI_PIPELINE_ID:Bt.CI_PIPELINE_ID,CI_PIPELINE_IID:Bt.CI_PIPELINE_IID,CI_PIPELINE_SOURCE:Bt.CI_PIPELINE_SOURCE,CI_PIPELINE_URL:Bt.CI_PIPELINE_URL,CI_PROJECT_CLASSIFICATION_LABEL:Bt.CI_PROJECT_CLASSIFICATION_LABEL,CI_PROJECT_DESCRIPTION:Bt.CI_PROJECT_DESCRIPTION,CI_PROJECT_ID:Bt.CI_PROJECT_ID,CI_PROJECT_NAME:Bt.CI_PROJECT_NAME,CI_PROJECT_NAMESPACE:Bt.CI_PROJECT_NAMESPACE,CI_PROJECT_NAMESPACE_ID:Bt.CI_PROJECT_NAMESPACE_ID,CI_PROJECT_PATH:Bt.CI_PROJECT_PATH,CI_PROJECT_PATH_SLUG:Bt.CI_PROJECT_PATH_SLUG,CI_PROJECT_REPOSITORY_LANGUAGES:Bt.CI_PROJECT_REPOSITORY_LANGUAGES,CI_PROJECT_ROOT_NAMESPACE:Bt.CI_PROJECT_ROOT_NAMESPACE,CI_PROJECT_TITLE:Bt.CI_PROJECT_TITLE,CI_PROJECT_URL:Bt.CI_PROJECT_URL,CI_PROJECT_VISIBILITY:Bt.CI_PROJECT_VISIBILITY,CI_REGISTRY:Bt.CI_REGISTRY,CI_REGISTRY_IMAGE:Bt.CI_REGISTRY_IMAGE,CI_REGISTRY_USER:Bt.CI_REGISTRY_USER,CI_RUNNER_DESCRIPTION:Bt.CI_RUNNER_DESCRIPTION,CI_RUNNER_ID:Bt.CI_RUNNER_ID,CI_RUNNER_TAGS:Bt.CI_RUNNER_TAGS,CI_SERVER_HOST:Bt.CI_SERVER_HOST,CI_SERVER_NAME:Bt.CI_SERVER_NAME,CI_SERVER_PORT:Bt.CI_SERVER_PORT,CI_SERVER_PROTOCOL:Bt.CI_SERVER_PROTOCOL,CI_SERVER_REVISION:Bt.CI_SERVER_REVISION,CI_SERVER_SHELL_SSH_HOST:Bt.CI_SERVER_SHELL_SSH_HOST,CI_SERVER_SHELL_SSH_PORT:Bt.CI_SERVER_SHELL_SSH_PORT,CI_SERVER_URL:Bt.CI_SERVER_URL,CI_SERVER_VERSION:Bt.CI_SERVER_VERSION,CI_SERVER_VERSION_MAJOR:Bt.CI_SERVER_VERSION_MAJOR,CI_SERVER_VERSION_MINOR:Bt.CI_SERVER_VERSION_MINOR,CI_SERVER_VERSION_PATCH:Bt.CI_SERVER_VERSION_PATCH,CI_TEMPLATE_REGISTRY_HOST:Bt.CI_TEMPLATE_REGISTRY_HOST,GITLAB_CI:Bt.GITLAB_CI,GITLAB_FEATURES:Bt.GITLAB_FEATURES,GITLAB_USER_ID:Bt.GITLAB_USER_ID,GITLAB_USER_LOGIN:Bt.GITLAB_USER_LOGIN,RUNNER_GENERATE_ARTIFACTS_METADATA:Bt.RUNNER_GENERATE_ARTIFACTS_METADATA},environment:{name:Bt.CI_RUNNER_DESCRIPTION,architecture:Bt.CI_RUNNER_EXECUTABLE_ARCH,server:Bt.CI_SERVER_URL,project:Bt.CI_PROJECT_PATH,job:{id:Bt.CI_JOB_ID},pipeline:{id:Bt.CI_PIPELINE_ID,ref:Bt.CI_CONFIG_PATH}}},metadata:{buildInvocationId:`${Bt.CI_JOB_URL}`,completeness:{parameters:!0,environment:!0,materials:!1},reproducible:!1},materials:[{uri:`git+${Bt.CI_PROJECT_URL}`,digest:{sha1:Bt.CI_COMMIT_SHA}}]}}}else throw new Lt(91,"Provenance generation is only supported in GitHub Actions and GitLab CI");return Pbe.attest(Buffer.from(JSON.stringify(r)),pvt,t)};async function wvt(e,t,{access:r,tag:s,registry:a,gitHead:n,provenance:c}){let f=e.manifest.name,p=e.manifest.version,h=j.stringifyIdent(f),E=kbe.default.fromData(t,{algorithms:["sha1","sha512"]}),C=r??Qbe(e,f),S=await Rbe(e),x=await IA.genPackageManifest(e),I=`${h}-${p}.tgz`,T=new URL(`${Vc(a)}/${h}/-/${I}`),O={[I]:{content_type:"application/octet-stream",data:t.toString("base64"),length:t.length}};if(c){let U={name:`pkg:npm/${h.replace(/^@/,"%40")}@${p}`,digest:{sha512:E.sha512[0].hexDigest()}},V=await xbe([U]),te=JSON.stringify(V);O[`${h}-${p}.sigstore`]={content_type:V.mediaType,data:te,length:te.length}}return{_id:h,_attachments:O,name:h,access:C,"dist-tags":{[s]:p},versions:{[p]:{...x,_id:`${h}@${p}`,name:h,version:p,gitHead:n,dist:{shasum:E.sha1[0].hexDigest(),integrity:E.sha512[0].toString(),tarball:T.toString()}}},readme:S}}async function Bvt(e){try{let{stdout:t}=await qr.execvp("git",["rev-parse","--revs-only","HEAD"],{cwd:e});return t.trim()===""?void 0:t.trim()}catch{return}}function Qbe(e,t){let r=e.project.configuration;return e.manifest.publishConfig&&typeof e.manifest.publishConfig.access=="string"?e.manifest.publishConfig.access:r.get("npmPublishAccess")!==null?r.get("npmPublishAccess"):t.scope?"restricted":"public"}async function Rbe(e){let t=fe.toPortablePath(`${e.cwd}/README.md`),r=e.manifest.name,a=`# ${j.stringifyIdent(r)} `;try{a=await le.readFilePromise(t,"utf8")}catch(n){if(n.code==="ENOENT")return a;throw n}return a}var $J={npmAlwaysAuth:{description:"URL of the selected npm registry (note: npm enterprise isn't supported)",type:"BOOLEAN",default:!1},npmAuthIdent:{description:"Authentication identity for the npm registry (_auth in npm and yarn v1)",type:"SECRET",default:null},npmAuthToken:{description:"Authentication token for the npm registry (_authToken in npm and yarn v1)",type:"SECRET",default:null}},Tbe={npmAuditRegistry:{description:"Registry to query for audit reports",type:"STRING",default:null},npmPublishRegistry:{description:"Registry to push packages to",type:"STRING",default:null},npmRegistryServer:{description:"URL of the selected npm registry (note: npm enterprise isn't supported)",type:"STRING",default:"https://registry.yarnpkg.com"}},vvt={npmMinimalAgeGate:{description:"Minimum age of a package version according to the publish date on the npm registry to be considered for installation",type:"DURATION",unit:"m",default:"0m"},npmPreapprovedPackages:{description:"Array of package descriptors or package name glob patterns to exclude from the minimum release age check",type:"STRING",isArray:!0,default:[]}},Svt={configuration:{...$J,...Tbe,...vvt,npmScopes:{description:"Settings per package scope",type:"MAP",valueDefinition:{description:"",type:"SHAPE",properties:{...$J,...Tbe}}},npmRegistries:{description:"Settings per registry",type:"MAP",normalizeKeys:Vc,valueDefinition:{description:"",type:"SHAPE",properties:{...$J}}}},fetchers:[VD,sh],resolvers:[JD,KD,zD]},Dvt=Svt;var uK={};Vt(uK,{NpmAuditCommand:()=>y1,NpmInfoCommand:()=>E1,NpmLoginCommand:()=>I1,NpmLogoutCommand:()=>w1,NpmPublishCommand:()=>B1,NpmTagAddCommand:()=>S1,NpmTagListCommand:()=>v1,NpmTagRemoveCommand:()=>D1,NpmWhoamiCommand:()=>b1,default:()=>Mvt,npmAuditTypes:()=>zb,npmAuditUtils:()=>hL});qe();qe();Yt();var sK=et(zo());Al();var zb={};Vt(zb,{Environment:()=>Jb,Severity:()=>Kb});var Jb=(s=>(s.All="all",s.Production="production",s.Development="development",s))(Jb||{}),Kb=(n=>(n.Info="info",n.Low="low",n.Moderate="moderate",n.High="high",n.Critical="critical",n))(Kb||{});var hL={};Vt(hL,{allSeverities:()=>m1,getPackages:()=>iK,getReportTree:()=>rK,getSeverityInclusions:()=>tK,getTopLevelDependencies:()=>nK});qe();var Fbe=et(pi());var m1=["info","low","moderate","high","critical"];function tK(e){if(typeof e>"u")return new Set(m1);let t=m1.indexOf(e),r=m1.slice(t);return new Set(r)}function rK(e){let t={},r={children:t};for(let[s,a]of Ge.sortMap(Object.entries(e),n=>n[0]))for(let n of Ge.sortMap(a,c=>`${c.id}`))t[`${s}/${n.id}`]={value:pe.tuple(pe.Type.IDENT,j.parseIdent(s)),children:{ID:typeof n.id<"u"&&{label:"ID",value:pe.tuple(pe.Type.ID,n.id)},Issue:{label:"Issue",value:pe.tuple(pe.Type.NO_HINT,n.title)},URL:typeof n.url<"u"&&{label:"URL",value:pe.tuple(pe.Type.URL,n.url)},Severity:{label:"Severity",value:pe.tuple(pe.Type.NO_HINT,n.severity)},"Vulnerable Versions":{label:"Vulnerable Versions",value:pe.tuple(pe.Type.RANGE,n.vulnerable_versions)},"Tree Versions":{label:"Tree Versions",children:[...n.versions].sort(Fbe.default.compare).map(c=>({value:pe.tuple(pe.Type.REFERENCE,c)}))},Dependents:{label:"Dependents",children:Ge.sortMap(n.dependents,c=>j.stringifyLocator(c)).map(c=>({value:pe.tuple(pe.Type.LOCATOR,c)}))}}};return r}function nK(e,t,{all:r,environment:s}){let a=[],n=r?e.workspaces:[t],c=["all","production"].includes(s),f=["all","development"].includes(s);for(let p of n)for(let h of p.anchoredPackage.dependencies.values())(p.manifest.devDependencies.has(h.identHash)?!f:!c)||a.push({workspace:p,dependency:h});return a}function iK(e,t,{recursive:r}){let s=new Map,a=new Set,n=[],c=e.configuration.makeResolver(),f={project:e,resolver:c},p=(h,E)=>{let C=e.storedResolutions.get(E.descriptorHash);if(typeof C>"u")throw new Error("Assertion failed: The resolution should have been registered");if(!a.has(C))a.add(C);else return;let S=e.storedPackages.get(C);if(typeof S>"u")throw new Error("Assertion failed: The package should have been registered");let x=j.ensureDevirtualizedDescriptor(E);if(c.supportsDescriptor(x,f)){let T=c.getResolutionDependencies(x,f);if(Object.keys(T).length>0)for(let O of Object.values(T))p(h,O)}if(j.ensureDevirtualizedLocator(S).reference.startsWith("npm:")&&S.version!==null){let T=j.stringifyIdent(S),O=Ge.getMapWithDefault(s,T);Ge.getArrayWithDefault(O,S.version).push(h)}if(r)for(let T of S.dependencies.values())n.push([S,T])};for(let{workspace:h,dependency:E}of t)n.push([h.anchoredLocator,E]);for(;n.length>0;){let[h,E]=n.shift();p(h,E)}return s}var y1=class extends At{constructor(){super(...arguments);this.all=he.Boolean("-A,--all",!1,{description:"Audit dependencies from all workspaces"});this.recursive=he.Boolean("-R,--recursive",!1,{description:"Audit transitive dependencies as well"});this.environment=he.String("--environment","all",{description:"Which environments to cover",validator:ks(Jb)});this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.noDeprecations=he.Boolean("--no-deprecations",!1,{description:"Don't warn about deprecated packages"});this.severity=he.String("--severity","info",{description:"Minimal severity requested for packages to be displayed",validator:ks(Kb)});this.excludes=he.Array("--exclude",[],{description:"Array of glob patterns of packages to exclude from audit"});this.ignores=he.Array("--ignore",[],{description:"Array of glob patterns of advisory ID's to ignore in the audit report"})}static{this.paths=[["npm","audit"]]}static{this.usage=at.Usage({description:"perform a vulnerability audit against the installed packages",details:` This command checks for known security reports on the packages you use. The reports are by default extracted from the npm registry, and may or may not be relevant to your actual program (not all vulnerabilities affect all code paths). For consistency with our other commands the default is to only check the direct dependencies for the active workspace. To extend this search to all workspaces, use \`-A,--all\`. To extend this search to both direct and transitive dependencies, use \`-R,--recursive\`. Applying the \`--severity\` flag will limit the audit table to vulnerabilities of the corresponding severity and above. Valid values are ${m1.map(r=>`\`${r}\``).join(", ")}. If the \`--json\` flag is set, Yarn will print the output exactly as received from the registry. Regardless of this flag, the process will exit with a non-zero exit code if a report is found for the selected packages. If certain packages produce false positives for a particular environment, the \`--exclude\` flag can be used to exclude any number of packages from the audit. This can also be set in the configuration file with the \`npmAuditExcludePackages\` option. If particular advisories are needed to be ignored, the \`--ignore\` flag can be used with Advisory ID's to ignore any number of advisories in the audit report. This can also be set in the configuration file with the \`npmAuditIgnoreAdvisories\` option. To understand the dependency tree requiring vulnerable packages, check the raw report with the \`--json\` flag or use \`yarn why package\` to get more information as to who depends on them. `,examples:[["Checks for known security issues with the installed packages. The output is a list of known issues.","yarn npm audit"],["Audit dependencies in all workspaces","yarn npm audit --all"],["Limit auditing to `dependencies` (excludes `devDependencies`)","yarn npm audit --environment production"],["Show audit report as valid JSON","yarn npm audit --json"],["Audit all direct and transitive dependencies","yarn npm audit --recursive"],["Output moderate (or more severe) vulnerabilities","yarn npm audit --severity moderate"],["Exclude certain packages","yarn npm audit --exclude package1 --exclude package2"],["Ignore specific advisories","yarn npm audit --ignore 1234567 --ignore 7654321"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState();let n=nK(s,a,{all:this.all,environment:this.environment}),c=iK(s,n,{recursive:this.recursive}),f=Array.from(new Set([...r.get("npmAuditExcludePackages"),...this.excludes])),p=Object.create(null);for(let[O,U]of c)f.some(V=>sK.default.isMatch(O,V))||(p[O]=[...U.keys()]);let h=di.getAuditRegistry({configuration:r}),E,C=await uA.start({configuration:r,stdout:this.context.stdout},async()=>{let O=en.post("/-/npm/v1/security/advisories/bulk",p,{authType:en.AuthType.BEST_EFFORT,configuration:r,jsonResponse:!0,registry:h}),U=this.noDeprecations?[]:await Promise.all(Array.from(Object.entries(p),async([te,ie])=>{let ue=await en.getPackageMetadata(j.parseIdent(te),{project:s});return Ge.mapAndFilter(ie,ae=>{let{deprecated:ge}=ue.versions[ae];return ge?[te,ae,ge]:Ge.mapAndFilter.skip})})),V=await O;for(let[te,ie,ue]of U.flat(1))Object.hasOwn(V,te)&&V[te].some(ae=>kr.satisfiesWithPrereleases(ie,ae.vulnerable_versions))||(V[te]??=[],V[te].push({id:`${te} (deprecation)`,title:(typeof ue=="string"?ue:"").trim()||"This package has been deprecated.",severity:"moderate",vulnerable_versions:ie}));E=V});if(C.hasErrors())return C.exitCode();let S=tK(this.severity),x=Array.from(new Set([...r.get("npmAuditIgnoreAdvisories"),...this.ignores])),I=Object.create(null);for(let[O,U]of Object.entries(E)){let V=U.filter(te=>!sK.default.isMatch(`${te.id}`,x)&&S.has(te.severity));V.length>0&&(I[O]=V.map(te=>{let ie=c.get(O);if(typeof ie>"u")throw new Error("Assertion failed: Expected the registry to only return packages that were requested");let ue=[...ie.keys()].filter(ge=>kr.satisfiesWithPrereleases(ge,te.vulnerable_versions)),ae=new Map;for(let ge of ue)for(let Ae of ie.get(ge))ae.set(Ae.locatorHash,Ae);return{...te,versions:ue,dependents:[...ae.values()]}}))}let T=Object.keys(I).length>0;return T?(Rs.emitTree(rK(I),{configuration:r,json:this.json,stdout:this.context.stdout,separators:2}),1):(await Ot.start({configuration:r,includeFooter:!1,json:this.json,stdout:this.context.stdout},async O=>{O.reportInfo(1,"No audit suggestions")}),T?1:0)}};qe();qe();Dt();Yt();var oK=et(pi()),aK=Ie("util"),E1=class extends At{constructor(){super(...arguments);this.fields=he.String("-f,--fields",{description:"A comma-separated list of manifest fields that should be displayed"});this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.packages=he.Rest()}static{this.paths=[["npm","info"]]}static{this.usage=at.Usage({category:"Npm-related commands",description:"show information about a package",details:"\n This command fetches information about a package from the npm registry and prints it in a tree format.\n\n The package does not have to be installed locally, but needs to have been published (in particular, local changes will be ignored even for workspaces).\n\n Append `@` to the package argument to provide information specific to the latest version that satisfies the range or to the corresponding tagged version. If the range is invalid or if there is no version satisfying the range, the command will print a warning and fall back to the latest version.\n\n If the `-f,--fields` option is set, it's a comma-separated list of fields which will be used to only display part of the package information.\n\n By default, this command won't return the `dist`, `readme`, and `users` fields, since they are often very long. To explicitly request those fields, explicitly list them with the `--fields` flag or request the output in JSON mode.\n ",examples:[["Show all available information about react (except the `dist`, `readme`, and `users` fields)","yarn npm info react"],["Show all available information about react as valid JSON (including the `dist`, `readme`, and `users` fields)","yarn npm info react --json"],["Show all available information about react@16.12.0","yarn npm info react@16.12.0"],["Show all available information about react@next","yarn npm info react@next"],["Show the description of react","yarn npm info react --fields description"],["Show all available versions of react","yarn npm info react --fields versions"],["Show the readme of react","yarn npm info react --fields readme"],["Show a few fields of react","yarn npm info react --fields homepage,repository"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Rt.find(r,this.context.cwd),a=typeof this.fields<"u"?new Set(["name",...this.fields.split(/\s*,\s*/)]):null,n=[],c=!1,f=await Ot.start({configuration:r,includeFooter:!1,json:this.json,stdout:this.context.stdout},async p=>{for(let h of this.packages){let E;if(h==="."){let ie=s.topLevelWorkspace;if(!ie.manifest.name)throw new it(`Missing ${pe.pretty(r,"name",pe.Type.CODE)} field in ${fe.fromPortablePath(J.join(ie.cwd,Er.manifest))}`);E=j.makeDescriptor(ie.manifest.name,"unknown")}else E=j.parseDescriptor(h);let C=en.getIdentUrl(E),S=lK(await en.get(C,{configuration:r,ident:E,jsonResponse:!0,customErrorMessage:en.customPackageError})),x=Object.keys(S.versions).sort(oK.default.compareLoose),T=S["dist-tags"].latest||x[x.length-1],O=kr.validRange(E.range);if(O){let ie=oK.default.maxSatisfying(x,O);ie!==null?T=ie:(p.reportWarning(0,`Unmet range ${j.prettyRange(r,E.range)}; falling back to the latest version`),c=!0)}else Object.hasOwn(S["dist-tags"],E.range)?T=S["dist-tags"][E.range]:E.range!=="unknown"&&(p.reportWarning(0,`Unknown tag ${j.prettyRange(r,E.range)}; falling back to the latest version`),c=!0);let U=S.versions[T],V={...S,...U,version:T,versions:x},te;if(a!==null){te={};for(let ie of a){let ue=V[ie];if(typeof ue<"u")te[ie]=ue;else{p.reportWarning(1,`The ${pe.pretty(r,ie,pe.Type.CODE)} field doesn't exist inside ${j.prettyIdent(r,E)}'s information`),c=!0;continue}}}else this.json||(delete V.dist,delete V.readme,delete V.users),te=V;p.reportJson(te),this.json||n.push(te)}});aK.inspect.styles.name="cyan";for(let p of n)(p!==n[0]||c)&&this.context.stdout.write(` `),this.context.stdout.write(`${(0,aK.inspect)(p,{depth:1/0,colors:!0,compact:!1})} `);return f.exitCode()}};function lK(e){if(Array.isArray(e)){let t=[];for(let r of e)r=lK(r),r&&t.push(r);return t}else if(typeof e=="object"&&e!==null){let t={};for(let r of Object.keys(e)){if(r.startsWith("_"))continue;let s=lK(e[r]);s&&(t[r]=s)}return t}else return e||null}qe();qe();Yt();var cK=et(Vv()),I1=class extends At{constructor(){super(...arguments);this.scope=he.String("-s,--scope",{description:"Login to the registry configured for a given scope"});this.publish=he.Boolean("--publish",!1,{description:"Login to the publish registry"});this.alwaysAuth=he.Boolean("--always-auth",{description:"Set the npmAlwaysAuth configuration"});this.webLogin=he.Boolean("--web-login",{description:"Enable web login"})}static{this.paths=[["npm","login"]]}static{this.usage=at.Usage({category:"Npm-related commands",description:"store new login info to access the npm registry",details:"\n This command will ask you for your username, password, and 2FA One-Time-Password (when it applies). It will then modify your local configuration (in your home folder, never in the project itself) to reference the new tokens thus generated.\n\n Adding the `-s,--scope` flag will cause the authentication to be done against whatever registry is configured for the associated scope (see also `npmScopes`).\n\n Adding the `--publish` flag will cause the authentication to be done against the registry used when publishing the package (see also `publishConfig.registry` and `npmPublishRegistry`).\n ",examples:[["Login to the default registry","yarn npm login"],["Login to the registry linked to the @my-scope registry","yarn npm login --scope my-scope"],["Login to the publish registry for the current package","yarn npm login --publish"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=await dL({configuration:r,cwd:this.context.cwd,publish:this.publish,scope:this.scope});return(await Ot.start({configuration:r,stdout:this.context.stdout,includeFooter:!1},async n=>{let c=await Qvt({registry:s,configuration:r,report:n,webLogin:this.webLogin,stdin:this.context.stdin,stdout:this.context.stdout});return await Tvt(s,c,{alwaysAuth:this.alwaysAuth,scope:this.scope}),n.reportInfo(0,"Successfully logged in")})).exitCode()}};async function dL({scope:e,publish:t,configuration:r,cwd:s}){return e&&t?di.getScopeRegistry(e,{configuration:r,type:di.RegistryType.PUBLISH_REGISTRY}):e?di.getScopeRegistry(e,{configuration:r}):t?di.getPublishRegistry((await WI(r,s)).manifest,{configuration:r}):di.getDefaultRegistry({configuration:r})}async function bvt(e,t){let r;try{r=await en.post("/-/v1/login",null,{configuration:t,registry:e,authType:en.AuthType.NO_AUTH,jsonResponse:!0,headers:{"npm-auth-type":"web"}})}catch{return null}return r}async function Pvt(e,t){let r=await nn.request(e,null,{configuration:t,jsonResponse:!0});if(r.statusCode===202){let s=r.headers["retry-after"]??"1";return{type:"waiting",sleep:parseInt(s,10)}}return r.statusCode===200?{type:"success",token:r.body.token}:null}async function xvt({registry:e,configuration:t,report:r}){let s=await bvt(e,t);if(!s)return null;if(Ui.openUrl){r.reportInfo(0,"Starting the web login process..."),r.reportSeparator();let{openNow:a}=await(0,cK.prompt)({type:"confirm",name:"openNow",message:"Do you want to try to open your browser now?",required:!0,initial:!0,onCancel:()=>process.exit(130)});r.reportSeparator(),(!a||!await Ui.openUrl(s.loginUrl))&&(r.reportWarning(0,"We failed to automatically open the url; you'll have to open it yourself in your browser of choice:"),r.reportWarning(0,pe.pretty(t,s.loginUrl,pe.Type.URL)),r.reportSeparator())}for(;;){let a=await Pvt(s.doneUrl,t);if(a===null)return null;if(a.type==="waiting")await new Promise(n=>setTimeout(n,a.sleep*1e3));else return a.token}}var kvt=["https://registry.yarnpkg.com","https://registry.npmjs.org"];async function Qvt(e){if(e.webLogin??kvt.includes(e.registry)){let t=await xvt(e);if(t!==null)return t}return await Rvt(e)}async function Rvt({registry:e,configuration:t,report:r,stdin:s,stdout:a}){let n=await Fvt({configuration:t,registry:e,report:r,stdin:s,stdout:a}),c=`/-/user/org.couchdb.user:${encodeURIComponent(n.name)}`,f={_id:`org.couchdb.user:${n.name}`,name:n.name,password:n.password,type:"user",roles:[],date:new Date().toISOString()},p={attemptedAs:n.name,configuration:t,registry:e,jsonResponse:!0,authType:en.AuthType.NO_AUTH};try{return(await en.put(c,f,p)).token}catch(x){if(!(x.originalError?.name==="HTTPError"&&x.originalError?.response.statusCode===409))throw x}let h={...p,authType:en.AuthType.NO_AUTH,headers:{authorization:`Basic ${Buffer.from(`${n.name}:${n.password}`).toString("base64")}`}},E=await en.get(c,h);for(let[x,I]of Object.entries(E))(!f[x]||x==="roles")&&(f[x]=I);let C=`${c}/-rev/${f._rev}`;return(await en.put(C,f,h)).token}async function Tvt(e,t,{alwaysAuth:r,scope:s}){let a=c=>f=>{let p=Ge.isIndexableObject(f)?f:{},h=p[c],E=Ge.isIndexableObject(h)?h:{};return{...p,[c]:{...E,...r!==void 0?{npmAlwaysAuth:r}:{},npmAuthToken:t}}},n=s?{npmScopes:a(s)}:{npmRegistries:a(e)};return await ze.updateHomeConfiguration(n)}async function Fvt({configuration:e,registry:t,report:r,stdin:s,stdout:a}){r.reportInfo(0,`Logging in to ${pe.pretty(e,t,pe.Type.URL)}`);let n=!1;if(t.match(/^https:\/\/npm\.pkg\.github\.com(\/|$)/)&&(r.reportInfo(0,"You seem to be using the GitHub Package Registry. Tokens must be generated with the 'repo', 'write:packages', and 'read:packages' permissions."),n=!0),r.reportSeparator(),e.env.YARN_IS_TEST_ENV)return{name:e.env.YARN_INJECT_NPM_USER||"",password:e.env.YARN_INJECT_NPM_PASSWORD||""};let c=await(0,cK.prompt)([{type:"input",name:"name",message:"Username:",required:!0,onCancel:()=>process.exit(130),stdin:s,stdout:a},{type:"password",name:"password",message:n?"Token:":"Password:",required:!0,onCancel:()=>process.exit(130),stdin:s,stdout:a}]);return r.reportSeparator(),c}qe();qe();Yt();var C1=new Set(["npmAuthIdent","npmAuthToken"]),w1=class extends At{constructor(){super(...arguments);this.scope=he.String("-s,--scope",{description:"Logout of the registry configured for a given scope"});this.publish=he.Boolean("--publish",!1,{description:"Logout of the publish registry"});this.all=he.Boolean("-A,--all",!1,{description:"Logout of all registries"})}static{this.paths=[["npm","logout"]]}static{this.usage=at.Usage({category:"Npm-related commands",description:"logout of the npm registry",details:"\n This command will log you out by modifying your local configuration (in your home folder, never in the project itself) to delete all credentials linked to a registry.\n\n Adding the `-s,--scope` flag will cause the deletion to be done against whatever registry is configured for the associated scope (see also `npmScopes`).\n\n Adding the `--publish` flag will cause the deletion to be done against the registry used when publishing the package (see also `publishConfig.registry` and `npmPublishRegistry`).\n\n Adding the `-A,--all` flag will cause the deletion to be done against all registries and scopes.\n ",examples:[["Logout of the default registry","yarn npm logout"],["Logout of the @my-scope scope","yarn npm logout --scope my-scope"],["Logout of the publish registry for the current package","yarn npm logout --publish"],["Logout of all registries","yarn npm logout --all"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=async()=>{let n=await dL({configuration:r,cwd:this.context.cwd,publish:this.publish,scope:this.scope}),c=await ze.find(this.context.cwd,this.context.plugins),f=j.makeIdent(this.scope??null,"pkg");return!di.getAuthConfiguration(n,{configuration:c,ident:f}).get("npmAuthToken")};return(await Ot.start({configuration:r,stdout:this.context.stdout},async n=>{if(this.all&&(await Ovt(),n.reportInfo(0,"Successfully logged out from everything")),this.scope){await Nbe("npmScopes",this.scope),await s()?n.reportInfo(0,`Successfully logged out from ${this.scope}`):n.reportWarning(0,"Scope authentication settings removed, but some other ones settings still apply to it");return}let c=await dL({configuration:r,cwd:this.context.cwd,publish:this.publish});await Nbe("npmRegistries",c),await s()?n.reportInfo(0,`Successfully logged out from ${c}`):n.reportWarning(0,"Registry authentication settings removed, but some other ones settings still apply to it")})).exitCode()}};function Nvt(e,t){let r=e[t];if(!Ge.isIndexableObject(r))return!1;let s=new Set(Object.keys(r));if([...C1].every(n=>!s.has(n)))return!1;for(let n of C1)s.delete(n);if(s.size===0)return e[t]=void 0,!0;let a={...r};for(let n of C1)delete a[n];return e[t]=a,!0}async function Ovt(){let e=t=>{let r=!1,s=Ge.isIndexableObject(t)?{...t}:{};s.npmAuthToken&&(delete s.npmAuthToken,r=!0);for(let a of Object.keys(s))Nvt(s,a)&&(r=!0);if(Object.keys(s).length!==0)return r?s:t};return await ze.updateHomeConfiguration({npmRegistries:e,npmScopes:e})}async function Nbe(e,t){return await ze.updateHomeConfiguration({[e]:r=>{let s=Ge.isIndexableObject(r)?r:{};if(!Object.hasOwn(s,t))return r;let a=s[t],n=Ge.isIndexableObject(a)?a:{},c=new Set(Object.keys(n));if([...C1].every(p=>!c.has(p)))return r;for(let p of C1)c.delete(p);if(c.size===0)return Object.keys(s).length===1?void 0:{...s,[t]:void 0};let f={};for(let p of C1)f[p]=void 0;return{...s,[t]:{...n,...f}}}})}qe();Dt();Yt();var B1=class extends At{constructor(){super(...arguments);this.access=he.String("--access",{description:"The access for the published package (public or restricted)"});this.tag=he.String("--tag","latest",{description:"The tag on the registry that the package should be attached to"});this.tolerateRepublish=he.Boolean("--tolerate-republish",!1,{description:"Warn and exit when republishing an already existing version of a package"});this.otp=he.String("--otp",{description:"The OTP token to use with the command"});this.provenance=he.Boolean("--provenance",!1,{description:"Generate provenance for the package. Only available in GitHub Actions and GitLab CI. Can be set globally through the `npmPublishProvenance` setting or the `YARN_NPM_CONFIG_PROVENANCE` environment variable, or per-package through the `publishConfig.provenance` field in package.json."});this.dryRun=he.Boolean("-n,--dry-run",!1,{description:"Show what would be published without actually publishing"});this.json=he.Boolean("--json",!1,{description:"Output the result in JSON format"})}static{this.paths=[["npm","publish"]]}static{this.usage=at.Usage({category:"Npm-related commands",description:"publish the active workspace to the npm registry",details:'\n This command will pack the active workspace into a fresh archive and upload it to the npm registry.\n\n The package will by default be attached to the `latest` tag on the registry, but this behavior can be overridden by using the `--tag` option.\n\n Note that for legacy reasons scoped packages are by default published with an access set to `restricted` (aka "private packages"). This requires you to register for a paid npm plan. In case you simply wish to publish a public scoped package to the registry (for free), just add the `--access public` flag. This behavior can be enabled by default through the `npmPublishAccess` settings.\n ',examples:[["Publish the active workspace","yarn npm publish"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);if(a.manifest.private)throw new it("Private workspaces cannot be published");if(a.manifest.name===null||a.manifest.version===null)throw new it("Workspaces must have valid names and versions to be published on an external registry");await s.restoreInstallState();let n=a.manifest.name,c=a.manifest.version,f=di.getPublishRegistry(a.manifest,{configuration:r});return(await Ot.start({configuration:r,stdout:this.context.stdout,json:this.json},async h=>{if(h.reportInfo(0,`Publishing to ${f} with tag ${this.tag}`),this.tolerateRepublish)try{let E=await en.get(en.getIdentUrl(n),{configuration:r,registry:f,ident:n,jsonResponse:!0});if(!Object.hasOwn(E,"versions"))throw new Lt(15,'Registry returned invalid data for - missing "versions" field');if(Object.hasOwn(E.versions,c)){let C=`Registry already knows about version ${c}; skipping.`;h.reportWarning(0,C),h.reportJson({name:j.stringifyIdent(n),version:c,registry:f,warning:C,skipped:!0});return}}catch(E){if(E.originalError?.response?.statusCode!==404)throw E}await Cn.maybeExecuteWorkspaceLifecycleScript(a,"prepublish",{report:h}),await IA.prepareForPack(a,{report:h},async()=>{let E=await IA.genPackList(a);for(let V of E)h.reportInfo(null,fe.fromPortablePath(V)),h.reportJson({file:fe.fromPortablePath(V)});let C=await IA.genPackStream(a,E),S=await Ge.bufferStream(C),x=await g1.getGitHead(a.cwd),I=!1,T="";a.manifest.publishConfig&&"provenance"in a.manifest.publishConfig?(I=!!a.manifest.publishConfig.provenance,T=I?"Generating provenance statement because `publishConfig.provenance` field is set.":"Skipping provenance statement because `publishConfig.provenance` field is set to false."):this.provenance?(I=!0,T="Generating provenance statement because `--provenance` flag is set."):r.get("npmPublishProvenance")&&(I=!0,T="Generating provenance statement because `npmPublishProvenance` setting is set."),T&&(h.reportInfo(null,T),h.reportJson({type:"provenance",enabled:I,provenanceMessage:T}));let O=await g1.makePublishBody(a,S,{access:this.access,tag:this.tag,registry:f,gitHead:x,provenance:I});this.dryRun||await en.put(en.getIdentUrl(n),O,{configuration:r,registry:f,ident:n,otp:this.otp,jsonResponse:!0,allowOidc:!!(process.env.CI&&(process.env.GITHUB_ACTIONS||process.env.GITLAB_CI))});let U=this.dryRun?"Package archive not published (dry run)":"Package archive published";h.reportInfo(0,U),h.reportJson({name:j.stringifyIdent(n),version:c,registry:f,tag:this.tag||"latest",files:E.map(V=>fe.fromPortablePath(V)),access:this.access||null,dryRun:this.dryRun,published:!this.dryRun,message:U,provenance:!!I})})})).exitCode()}};qe();Yt();var Obe=et(pi());qe();Dt();Yt();var v1=class extends At{constructor(){super(...arguments);this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.package=he.String({required:!1})}static{this.paths=[["npm","tag","list"]]}static{this.usage=at.Usage({category:"Npm-related commands",description:"list all dist-tags of a package",details:` This command will list all tags of a package from the npm registry. If the package is not specified, Yarn will default to the current workspace. `,examples:[["List all tags of package `my-pkg`","yarn npm tag list my-pkg"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n;if(typeof this.package<"u")n=j.parseIdent(this.package);else{if(!a)throw new ar(s.cwd,this.context.cwd);if(!a.manifest.name)throw new it(`Missing 'name' field in ${fe.fromPortablePath(J.join(a.cwd,Er.manifest))}`);n=a.manifest.name}let c=await Xb(n,r),p={children:Ge.sortMap(Object.entries(c),([h])=>h).map(([h,E])=>({value:pe.tuple(pe.Type.RESOLUTION,{descriptor:j.makeDescriptor(n,h),locator:j.makeLocator(n,E)})}))};return Rs.emitTree(p,{configuration:r,json:this.json,stdout:this.context.stdout})}};async function Xb(e,t){let r=`/-/package${en.getIdentUrl(e)}/dist-tags`;return en.get(r,{configuration:t,ident:e,jsonResponse:!0,customErrorMessage:en.customPackageError})}var S1=class extends At{constructor(){super(...arguments);this.package=he.String();this.tag=he.String()}static{this.paths=[["npm","tag","add"]]}static{this.usage=at.Usage({category:"Npm-related commands",description:"add a tag for a specific version of a package",details:` This command will add a tag to the npm registry for a specific version of a package. If the tag already exists, it will be overwritten. `,examples:[["Add a `beta` tag for version `2.3.4-beta.4` of package `my-pkg`","yarn npm tag add my-pkg@2.3.4-beta.4 beta"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);let n=j.parseDescriptor(this.package,!0),c=n.range;if(!Obe.default.valid(c))throw new it(`The range ${pe.pretty(r,n.range,pe.Type.RANGE)} must be a valid semver version`);let f=di.getPublishRegistry(a.manifest,{configuration:r}),p=pe.pretty(r,n,pe.Type.IDENT),h=pe.pretty(r,c,pe.Type.RANGE),E=pe.pretty(r,this.tag,pe.Type.CODE);return(await Ot.start({configuration:r,stdout:this.context.stdout},async S=>{let x=await Xb(n,r);Object.hasOwn(x,this.tag)&&x[this.tag]===c&&S.reportWarning(0,`Tag ${E} is already set to version ${h}`);let I=`/-/package${en.getIdentUrl(n)}/dist-tags/${encodeURIComponent(this.tag)}`;await en.put(I,c,{configuration:r,registry:f,ident:n,jsonRequest:!0,jsonResponse:!0}),S.reportInfo(0,`Tag ${E} added to version ${h} of package ${p}`)})).exitCode()}};qe();Yt();var D1=class extends At{constructor(){super(...arguments);this.package=he.String();this.tag=he.String()}static{this.paths=[["npm","tag","remove"]]}static{this.usage=at.Usage({category:"Npm-related commands",description:"remove a tag from a package",details:` This command will remove a tag from a package from the npm registry. `,examples:[["Remove the `beta` tag from package `my-pkg`","yarn npm tag remove my-pkg beta"]]})}async execute(){if(this.tag==="latest")throw new it("The 'latest' tag cannot be removed.");let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);let n=j.parseIdent(this.package),c=di.getPublishRegistry(a.manifest,{configuration:r}),f=pe.pretty(r,this.tag,pe.Type.CODE),p=pe.pretty(r,n,pe.Type.IDENT),h=await Xb(n,r);if(!Object.hasOwn(h,this.tag))throw new it(`${f} is not a tag of package ${p}`);return(await Ot.start({configuration:r,stdout:this.context.stdout},async C=>{let S=`/-/package${en.getIdentUrl(n)}/dist-tags/${encodeURIComponent(this.tag)}`;await en.del(S,{configuration:r,registry:c,ident:n,jsonResponse:!0}),C.reportInfo(0,`Tag ${f} removed from package ${p}`)})).exitCode()}};qe();qe();Yt();var b1=class extends At{constructor(){super(...arguments);this.scope=he.String("-s,--scope",{description:"Print username for the registry configured for a given scope"});this.publish=he.Boolean("--publish",!1,{description:"Print username for the publish registry"})}static{this.paths=[["npm","whoami"]]}static{this.usage=at.Usage({category:"Npm-related commands",description:"display the name of the authenticated user",details:"\n Print the username associated with the current authentication settings to the standard output.\n\n When using `-s,--scope`, the username printed will be the one that matches the authentication settings of the registry associated with the given scope (those settings can be overriden using the `npmRegistries` map, and the registry associated with the scope is configured via the `npmScopes` map).\n\n When using `--publish`, the registry we'll select will by default be the one used when publishing packages (`publishConfig.registry` or `npmPublishRegistry` if available, otherwise we'll fallback to the regular `npmRegistryServer`).\n ",examples:[["Print username for the default registry","yarn npm whoami"],["Print username for the registry on a given scope","yarn npm whoami --scope company"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s;return this.scope&&this.publish?s=di.getScopeRegistry(this.scope,{configuration:r,type:di.RegistryType.PUBLISH_REGISTRY}):this.scope?s=di.getScopeRegistry(this.scope,{configuration:r}):this.publish?s=di.getPublishRegistry((await WI(r,this.context.cwd)).manifest,{configuration:r}):s=di.getDefaultRegistry({configuration:r}),(await Ot.start({configuration:r,stdout:this.context.stdout},async n=>{let c;try{c=await en.get("/-/whoami",{configuration:r,registry:s,authType:en.AuthType.ALWAYS_AUTH,jsonResponse:!0,ident:this.scope?j.makeIdent(this.scope,""):void 0})}catch(f){if(f.response?.statusCode===401||f.response?.statusCode===403){n.reportError(41,"Authentication failed - your credentials may have expired");return}else throw f}n.reportInfo(0,c.username)})).exitCode()}};var Lvt={configuration:{npmPublishAccess:{description:"Default access of the published packages",type:"STRING",default:null},npmPublishProvenance:{description:"Whether to generate provenance for the published packages",type:"BOOLEAN",default:!1},npmAuditExcludePackages:{description:"Array of glob patterns of packages to exclude from npm audit",type:"STRING",default:[],isArray:!0},npmAuditIgnoreAdvisories:{description:"Array of glob patterns of advisory IDs to exclude from npm audit",type:"STRING",default:[],isArray:!0}},commands:[y1,E1,I1,w1,B1,S1,v1,D1,b1]},Mvt=Lvt;var mK={};Vt(mK,{PatchCommand:()=>T1,PatchCommitCommand:()=>R1,PatchFetcher:()=>rP,PatchResolver:()=>nP,default:()=>rSt,patchUtils:()=>yy});qe();qe();Dt();nA();var yy={};Vt(yy,{applyPatchFile:()=>mL,diffFolders:()=>dK,ensureUnpatchedDescriptor:()=>fK,ensureUnpatchedLocator:()=>EL,extractPackageToDisk:()=>hK,extractPatchFlags:()=>Gbe,isParentRequired:()=>pK,isPatchDescriptor:()=>yL,isPatchLocator:()=>Qd,loadPatchFiles:()=>tP,makeDescriptor:()=>IL,makeLocator:()=>AK,makePatchHash:()=>gK,parseDescriptor:()=>$b,parseLocator:()=>eP,parsePatchFile:()=>Zb,unpatchDescriptor:()=>$vt,unpatchLocator:()=>eSt});qe();Dt();qe();Dt();var Uvt=/^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@.*/;function P1(e){return J.relative(vt.root,J.resolve(vt.root,fe.toPortablePath(e)))}function _vt(e){let t=e.trim().match(Uvt);if(!t)throw new Error(`Bad header line: '${e}'`);return{original:{start:Math.max(Number(t[1]),1),length:Number(t[3]||1)},patched:{start:Math.max(Number(t[4]),1),length:Number(t[6]||1)}}}var Hvt=420,jvt=493;var Lbe=()=>({semverExclusivity:null,diffLineFromPath:null,diffLineToPath:null,oldMode:null,newMode:null,deletedFileMode:null,newFileMode:null,renameFrom:null,renameTo:null,beforeHash:null,afterHash:null,fromPath:null,toPath:null,hunks:null}),Gvt=e=>({header:_vt(e),parts:[]}),qvt={"@":"header","-":"deletion","+":"insertion"," ":"context","\\":"pragma",undefined:"context"};function Wvt(e){let t=[],r=Lbe(),s="parsing header",a=null,n=null;function c(){a&&(n&&(a.parts.push(n),n=null),r.hunks.push(a),a=null)}function f(){c(),t.push(r),r=Lbe()}for(let p=0;p0?"patch":"mode change",V=null;switch(U){case"rename":{if(!E||!C)throw new Error("Bad parser state: rename from & to not given");t.push({type:"rename",semverExclusivity:s,fromPath:P1(E),toPath:P1(C)}),V=C}break;case"file deletion":{let te=a||I;if(!te)throw new Error("Bad parse state: no path given for file deletion");t.push({type:"file deletion",semverExclusivity:s,hunk:O&&O[0]||null,path:P1(te),mode:gL(p),hash:S})}break;case"file creation":{let te=n||T;if(!te)throw new Error("Bad parse state: no path given for file creation");t.push({type:"file creation",semverExclusivity:s,hunk:O&&O[0]||null,path:P1(te),mode:gL(h),hash:x})}break;case"patch":case"mode change":V=T||n;break;default:Ge.assertNever(U);break}V&&c&&f&&c!==f&&t.push({type:"mode change",semverExclusivity:s,path:P1(V),oldMode:gL(c),newMode:gL(f)}),V&&O&&O.length&&t.push({type:"patch",semverExclusivity:s,path:P1(V),hunks:O,beforeHash:S,afterHash:x})}if(t.length===0)throw new Error("Unable to parse patch file: No changes found. Make sure the patch is a valid UTF8 encoded string");return t}function gL(e){let t=parseInt(e,8)&511;if(t!==Hvt&&t!==jvt)throw new Error(`Unexpected file mode string: ${e}`);return t}function Zb(e){let t=e.split(/\n/g);return t[t.length-1]===""&&t.pop(),Yvt(Wvt(t))}function Vvt(e){let t=0,r=0;for(let{type:s,lines:a}of e.parts)switch(s){case"context":r+=a.length,t+=a.length;break;case"deletion":t+=a.length;break;case"insertion":r+=a.length;break;default:Ge.assertNever(s);break}if(t!==e.header.original.length||r!==e.header.patched.length){let s=a=>a<0?a:`+${a}`;throw new Error(`hunk header integrity check failed (expected @@ ${s(e.header.original.length)} ${s(e.header.patched.length)} @@, got @@ ${s(t)} ${s(r)} @@)`)}}qe();Dt();var x1=class extends Error{constructor(r,s){super(`Cannot apply hunk #${r+1}`);this.hunk=s}};async function k1(e,t,r){let s=await e.lstatPromise(t),a=await r();typeof a<"u"&&(t=a),await e.lutimesPromise(t,s.atime,s.mtime)}async function mL(e,{baseFs:t=new Vn,dryRun:r=!1,version:s=null}={}){for(let a of e)if(!(a.semverExclusivity!==null&&s!==null&&!kr.satisfiesWithPrereleases(s,a.semverExclusivity)))switch(a.type){case"file deletion":if(r){if(!t.existsSync(a.path))throw new Error(`Trying to delete a file that doesn't exist: ${a.path}`)}else await k1(t,J.dirname(a.path),async()=>{await t.unlinkPromise(a.path)});break;case"rename":if(r){if(!t.existsSync(a.fromPath))throw new Error(`Trying to move a file that doesn't exist: ${a.fromPath}`)}else await k1(t,J.dirname(a.fromPath),async()=>{await k1(t,J.dirname(a.toPath),async()=>{await k1(t,a.fromPath,async()=>(await t.movePromise(a.fromPath,a.toPath),a.toPath))})});break;case"file creation":if(r){if(t.existsSync(a.path))throw new Error(`Trying to create a file that already exists: ${a.path}`)}else{let n=a.hunk?a.hunk.parts[0].lines.join(` `)+(a.hunk.parts[0].noNewlineAtEndOfFile?"":` `):"";await t.mkdirpPromise(J.dirname(a.path),{chmod:493,utimes:[Ai.SAFE_TIME,Ai.SAFE_TIME]}),await t.writeFilePromise(a.path,n,{mode:a.mode}),await t.utimesPromise(a.path,Ai.SAFE_TIME,Ai.SAFE_TIME)}break;case"patch":await k1(t,a.path,async()=>{await zvt(a,{baseFs:t,dryRun:r})});break;case"mode change":{let c=(await t.statPromise(a.path)).mode;if(Mbe(a.newMode)!==Mbe(c))continue;await k1(t,a.path,async()=>{await t.chmodPromise(a.path,a.newMode)})}break;default:Ge.assertNever(a);break}}function Mbe(e){return(e&64)>0}function Ube(e){return e.replace(/\s+$/,"")}function Kvt(e,t){return Ube(e)===Ube(t)}async function zvt({hunks:e,path:t},{baseFs:r,dryRun:s=!1}){let a=await r.statSync(t).mode,c=(await r.readFileSync(t,"utf8")).split(/\n/),f=[],p=0,h=0;for(let C of e){let S=Math.max(h,C.header.patched.start+p),x=Math.max(0,S-h),I=Math.max(0,c.length-S-C.header.original.length),T=Math.max(x,I),O=0,U=0,V=null;for(;O<=T;){if(O<=x&&(U=S-O,V=_be(C,c,U),V!==null)){O=-O;break}if(O<=I&&(U=S+O,V=_be(C,c,U),V!==null))break;O+=1}if(V===null)throw new x1(e.indexOf(C),C);f.push(V),p+=O,h=U+C.header.original.length}if(s)return;let E=0;for(let C of f)for(let S of C)switch(S.type){case"splice":{let x=S.index+E;c.splice(x,S.numToDelete,...S.linesToInsert),E+=S.linesToInsert.length-S.numToDelete}break;case"pop":c.pop();break;case"push":c.push(S.line);break;default:Ge.assertNever(S);break}await r.writeFilePromise(t,c.join(` `),{mode:a})}function _be(e,t,r){let s=[];for(let a of e.parts)switch(a.type){case"context":case"deletion":{for(let n of a.lines){let c=t[r];if(c==null||!Kvt(c,n))return null;r+=1}a.type==="deletion"&&(s.push({type:"splice",index:r-a.lines.length,numToDelete:a.lines.length,linesToInsert:[]}),a.noNewlineAtEndOfFile&&s.push({type:"push",line:""}))}break;case"insertion":s.push({type:"splice",index:r,numToDelete:0,linesToInsert:a.lines}),a.noNewlineAtEndOfFile&&s.push({type:"pop"});break;default:Ge.assertNever(a.type);break}return s}var Zvt=/^builtin<([^>]+)>$/;function Q1(e,t){let{protocol:r,source:s,selector:a,params:n}=j.parseRange(e);if(r!=="patch:")throw new Error("Invalid patch range");if(s===null)throw new Error("Patch locators must explicitly define their source");let c=a?a.split(/&/).map(E=>fe.toPortablePath(E)):[],f=n&&typeof n.locator=="string"?j.parseLocator(n.locator):null,p=n&&typeof n.version=="string"?n.version:null,h=t(s);return{parentLocator:f,sourceItem:h,patchPaths:c,sourceVersion:p}}function yL(e){return e.range.startsWith("patch:")}function Qd(e){return e.reference.startsWith("patch:")}function $b(e){let{sourceItem:t,...r}=Q1(e.range,j.parseDescriptor);return{...r,sourceDescriptor:t}}function eP(e){let{sourceItem:t,...r}=Q1(e.reference,j.parseLocator);return{...r,sourceLocator:t}}function $vt(e){let{sourceItem:t}=Q1(e.range,j.parseDescriptor);return t}function eSt(e){let{sourceItem:t}=Q1(e.reference,j.parseLocator);return t}function fK(e){if(!yL(e))return e;let{sourceItem:t}=Q1(e.range,j.parseDescriptor);return t}function EL(e){if(!Qd(e))return e;let{sourceItem:t}=Q1(e.reference,j.parseLocator);return t}function Hbe({parentLocator:e,sourceItem:t,patchPaths:r,sourceVersion:s,patchHash:a},n){let c=e!==null?{locator:j.stringifyLocator(e)}:{},f=typeof s<"u"?{version:s}:{},p=typeof a<"u"?{hash:a}:{};return j.makeRange({protocol:"patch:",source:n(t),selector:r.join("&"),params:{...f,...p,...c}})}function IL(e,{parentLocator:t,sourceDescriptor:r,patchPaths:s}){return j.makeDescriptor(e,Hbe({parentLocator:t,sourceItem:r,patchPaths:s},j.stringifyDescriptor))}function AK(e,{parentLocator:t,sourcePackage:r,patchPaths:s,patchHash:a}){return j.makeLocator(e,Hbe({parentLocator:t,sourceItem:r,sourceVersion:r.version,patchPaths:s,patchHash:a},j.stringifyLocator))}function jbe({onAbsolute:e,onRelative:t,onProject:r,onBuiltin:s},a){let n=a.lastIndexOf("!");n!==-1&&(a=a.slice(n+1));let c=a.match(Zvt);return c!==null?s(c[1]):a.startsWith("~/")?r(a.slice(2)):J.isAbsolute(a)?e(a):t(a)}function Gbe(e){let t=e.lastIndexOf("!");return{optional:(t!==-1?new Set(e.slice(0,t).split(/!/)):new Set).has("optional")}}function pK(e){return jbe({onAbsolute:()=>!1,onRelative:()=>!0,onProject:()=>!1,onBuiltin:()=>!1},e)}async function tP(e,t,r){let s=e!==null?await r.fetcher.fetch(e,r):null,a=s&&s.localPath?{packageFs:new bn(vt.root),prefixPath:J.relative(vt.root,s.localPath)}:s;s&&s!==a&&s.releaseFs&&s.releaseFs();let n=await Ge.releaseAfterUseAsync(async()=>await Promise.all(t.map(async c=>{let f=Gbe(c),p=await jbe({onAbsolute:async h=>await le.readFilePromise(h,"utf8"),onRelative:async h=>{if(a===null)throw new Error("Assertion failed: The parent locator should have been fetched");return await a.packageFs.readFilePromise(J.join(a.prefixPath,h),"utf8")},onProject:async h=>await le.readFilePromise(J.join(r.project.cwd,h),"utf8"),onBuiltin:async h=>await r.project.configuration.firstHook(E=>E.getBuiltinPatch,r.project,h)},c);return{...f,source:p}})));for(let c of n)typeof c.source=="string"&&(c.source=c.source.replace(/\r\n?/g,` `));return n}async function hK(e,{cache:t,project:r}){let s=r.storedPackages.get(e.locatorHash);if(typeof s>"u")throw new Error("Assertion failed: Expected the package to be registered");let a=EL(e),n=r.storedChecksums,c=new ki,f=await le.mktempPromise(),p=J.join(f,"source"),h=J.join(f,"user"),E=J.join(f,".yarn-patch.json"),C=r.configuration.makeFetcher(),S=[];try{let x,I;if(e.locatorHash===a.locatorHash){let T=await C.fetch(e,{cache:t,project:r,fetcher:C,checksums:n,report:c});S.push(()=>T.releaseFs?.()),x=T,I=T}else x=await C.fetch(e,{cache:t,project:r,fetcher:C,checksums:n,report:c}),S.push(()=>x.releaseFs?.()),I=await C.fetch(e,{cache:t,project:r,fetcher:C,checksums:n,report:c}),S.push(()=>I.releaseFs?.());await Promise.all([le.copyPromise(p,x.prefixPath,{baseFs:x.packageFs}),le.copyPromise(h,I.prefixPath,{baseFs:I.packageFs}),le.writeJsonPromise(E,{locator:j.stringifyLocator(e),version:s.version})])}finally{for(let x of S)x()}return le.detachTemp(f),h}async function dK(e,t){let r=fe.fromPortablePath(e).replace(/\\/g,"/"),s=fe.fromPortablePath(t).replace(/\\/g,"/"),{stdout:a,stderr:n}=await qr.execvp("git",["-c","core.safecrlf=false","diff","--src-prefix=a/","--dst-prefix=b/","--ignore-cr-at-eol","--full-index","--no-index","--no-renames","--text",r,s],{cwd:fe.toPortablePath(process.cwd()),env:{...process.env,GIT_CONFIG_NOSYSTEM:"1",HOME:"",XDG_CONFIG_HOME:"",USERPROFILE:""}});if(n.length>0)throw new Error(`Unable to diff directories. Make sure you have a recent version of 'git' available in PATH. The following error was reported by 'git': ${n}`);let c=r.startsWith("/")?f=>f.slice(1):f=>f;return a.replace(new RegExp(`(a|b)(${Ge.escapeRegExp(`/${c(r)}/`)})`,"g"),"$1/").replace(new RegExp(`(a|b)${Ge.escapeRegExp(`/${c(s)}/`)}`,"g"),"$1/").replace(new RegExp(Ge.escapeRegExp(`${r}/`),"g"),"").replace(new RegExp(Ge.escapeRegExp(`${s}/`),"g"),"")}function gK(e,t){let r=[];for(let{source:s}of e){if(s===null)continue;let a=Zb(s);for(let n of a){let{semverExclusivity:c,...f}=n;c!==null&&t!==null&&!kr.satisfiesWithPrereleases(t,c)||r.push(JSON.stringify(f))}}return Ln.makeHash(`${3}`,...r).slice(0,6)}qe();function qbe(e,{configuration:t,report:r}){for(let s of e.parts)for(let a of s.lines)switch(s.type){case"context":r.reportInfo(null,` ${pe.pretty(t,a,"grey")}`);break;case"deletion":r.reportError(28,`- ${pe.pretty(t,a,pe.Type.REMOVED)}`);break;case"insertion":r.reportError(28,`+ ${pe.pretty(t,a,pe.Type.ADDED)}`);break;default:Ge.assertNever(s.type)}}var rP=class{supports(t,r){return!!Qd(t)}getLocalPath(t,r){return null}async fetch(t,r){let s=r.checksums.get(t.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(t,s,{onHit:()=>r.report.reportCacheHit(t),onMiss:()=>r.report.reportCacheMiss(t,`${j.prettyLocator(r.project.configuration,t)} can't be found in the cache and will be fetched from the disk`),loader:()=>this.patchPackage(t,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:j.getIdentVendorPath(t),localPath:this.getLocalPath(t,r),checksum:c}}async patchPackage(t,r){let{parentLocator:s,sourceLocator:a,sourceVersion:n,patchPaths:c}=eP(t),f=await tP(s,c,r),p=await le.mktempPromise(),h=J.join(p,"current.zip"),E=await r.fetcher.fetch(a,r),C=j.getIdentVendorPath(t),S=new ps(h,{create:!0,level:r.project.configuration.get("compressionLevel")});await Ge.releaseAfterUseAsync(async()=>{await S.copyPromise(C,E.prefixPath,{baseFs:E.packageFs,stableSort:!0})},E.releaseFs),S.saveAndClose();for(let{source:x,optional:I}of f){if(x===null)continue;let T=new ps(h,{level:r.project.configuration.get("compressionLevel")}),O=new bn(J.resolve(vt.root,C),{baseFs:T});try{await mL(Zb(x),{baseFs:O,version:n})}catch(U){if(!(U instanceof x1))throw U;let V=r.project.configuration.get("enableInlineHunks"),te=!V&&!I?" (set enableInlineHunks for details)":"",ie=`${j.prettyLocator(r.project.configuration,t)}: ${U.message}${te}`,ue=ae=>{V&&qbe(U.hunk,{configuration:r.project.configuration,report:ae})};if(T.discardAndClose(),I){r.report.reportWarningOnce(66,ie,{reportExtra:ue});continue}else throw new Lt(66,ie,ue)}T.saveAndClose()}return new ps(h,{level:r.project.configuration.get("compressionLevel")})}};qe();var nP=class{supportsDescriptor(t,r){return!!yL(t)}supportsLocator(t,r){return!!Qd(t)}shouldPersistResolution(t,r){return!1}bindDescriptor(t,r,s){let{patchPaths:a}=$b(t);return a.every(n=>!pK(n))?t:j.bindDescriptor(t,{locator:j.stringifyLocator(r)})}getResolutionDependencies(t,r){let{sourceDescriptor:s}=$b(t);return{sourceDescriptor:r.project.configuration.normalizeDependency(s)}}async getCandidates(t,r,s){if(!s.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let{parentLocator:a,patchPaths:n}=$b(t),c=await tP(a,n,s.fetchOptions),f=r.sourceDescriptor;if(typeof f>"u")throw new Error("Assertion failed: The dependency should have been resolved");let p=gK(c,f.version);return[AK(t,{parentLocator:a,sourcePackage:f,patchPaths:n,patchHash:p})]}async getSatisfying(t,r,s,a){let[n]=await this.getCandidates(t,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(t,r){let{sourceLocator:s}=eP(t);return{...await r.resolver.resolve(s,r),...t}}};qe();Dt();Yt();var R1=class extends At{constructor(){super(...arguments);this.save=he.Boolean("-s,--save",!1,{description:"Add the patch to your resolution entries"});this.patchFolder=he.String()}static{this.paths=[["patch-commit"]]}static{this.usage=at.Usage({description:"generate a patch out of a directory",details:"\n By default, this will print a patchfile on stdout based on the diff between the folder passed in and the original version of the package. Such file is suitable for consumption with the `patch:` protocol.\n\n With the `-s,--save` option set, the patchfile won't be printed on stdout anymore and will instead be stored within a local file (by default kept within `.yarn/patches`, but configurable via the `patchFolder` setting). A `resolutions` entry will also be added to your top-level manifest, referencing the patched package via the `patch:` protocol.\n\n Note that only folders generated by `yarn patch` are accepted as valid input for `yarn patch-commit`.\n "})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState();let n=J.resolve(this.context.cwd,fe.toPortablePath(this.patchFolder)),c=J.join(n,"../source"),f=J.join(n,"../.yarn-patch.json");if(!le.existsSync(c))throw new it("The argument folder didn't get created by 'yarn patch'");let p=await dK(c,n),h=await le.readJsonPromise(f),E=j.parseLocator(h.locator,!0);if(!s.storedPackages.has(E.locatorHash))throw new it("No package found in the project for the given locator");if(!this.save){this.context.stdout.write(p);return}let C=r.get("patchFolder"),S=J.join(C,`${j.slugifyLocator(E)}.patch`);await le.mkdirPromise(C,{recursive:!0}),await le.writeFilePromise(S,p);let x=[],I=new Map;for(let T of s.storedPackages.values()){if(j.isVirtualLocator(T))continue;let O=T.dependencies.get(E.identHash);if(!O)continue;let U=j.ensureDevirtualizedDescriptor(O),V=fK(U),te=s.storedResolutions.get(V.descriptorHash);if(!te)throw new Error("Assertion failed: Expected the resolution to have been registered");if(!s.storedPackages.get(te))throw new Error("Assertion failed: Expected the package to have been registered");let ue=s.tryWorkspaceByLocator(T);if(ue)x.push(ue);else{let ae=s.originalPackages.get(T.locatorHash);if(!ae)throw new Error("Assertion failed: Expected the original package to have been registered");let ge=ae.dependencies.get(O.identHash);if(!ge)throw new Error("Assertion failed: Expected the original dependency to have been registered");I.set(ge.descriptorHash,ge)}}for(let T of x)for(let O of _t.hardDependencies){let U=T.manifest[O].get(E.identHash);if(!U)continue;let V=IL(U,{parentLocator:null,sourceDescriptor:j.convertLocatorToDescriptor(E),patchPaths:[J.join(Er.home,J.relative(s.cwd,S))]});T.manifest[O].set(U.identHash,V)}for(let T of I.values()){let O=IL(T,{parentLocator:null,sourceDescriptor:j.convertLocatorToDescriptor(E),patchPaths:[J.join(Er.home,J.relative(s.cwd,S))]});s.topLevelWorkspace.manifest.resolutions.push({pattern:{descriptor:{fullName:j.stringifyIdent(O),description:T.range}},reference:O.range})}await s.persist()}};qe();Dt();Yt();var T1=class extends At{constructor(){super(...arguments);this.update=he.Boolean("-u,--update",!1,{description:"Reapply local patches that already apply to this packages"});this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.package=he.String()}static{this.paths=[["patch"]]}static{this.usage=at.Usage({description:"prepare a package for patching",details:"\n This command will cause a package to be extracted in a temporary directory intended to be editable at will.\n\n Once you're done with your changes, run `yarn patch-commit -s path` (with `path` being the temporary directory you received) to generate a patchfile and register it into your top-level manifest via the `patch:` protocol. Run `yarn patch-commit -h` for more details.\n\n Calling the command when you already have a patch won't import it by default (in other words, the default behavior is to reset existing patches). However, adding the `-u,--update` flag will import any current patch.\n "})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState();let c=j.parseLocator(this.package);if(c.reference==="unknown"){let f=Ge.mapAndFilter([...s.storedPackages.values()],p=>p.identHash!==c.identHash?Ge.mapAndFilter.skip:j.isVirtualLocator(p)?Ge.mapAndFilter.skip:Qd(p)!==this.update?Ge.mapAndFilter.skip:p);if(f.length===0)throw new it("No package found in the project for the given locator");if(f.length>1)throw new it(`Multiple candidate packages found; explicitly choose one of them (use \`yarn why \` to get more information as to who depends on them): ${f.map(p=>` - ${j.prettyLocator(r,p)}`).join("")}`);c=f[0]}if(!s.storedPackages.has(c.locatorHash))throw new it("No package found in the project for the given locator");await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout},async f=>{let p=EL(c),h=await hK(c,{cache:n,project:s});f.reportJson({locator:j.stringifyLocator(p),path:fe.fromPortablePath(h)});let E=this.update?" along with its current modifications":"";f.reportInfo(0,`Package ${j.prettyLocator(r,p)} got extracted with success${E}!`),f.reportInfo(0,`You can now edit the following folder: ${pe.pretty(r,fe.fromPortablePath(h),"magenta")}`),f.reportInfo(0,`Once you are done run ${pe.pretty(r,`yarn patch-commit -s ${process.platform==="win32"?'"':""}${fe.fromPortablePath(h)}${process.platform==="win32"?'"':""}`,"cyan")} and Yarn will store a patchfile based on your changes.`)})}};var tSt={configuration:{enableInlineHunks:{description:"If true, the installs will print unmatched patch hunks",type:"BOOLEAN",default:!1},patchFolder:{description:"Folder where the patch files must be written",type:"ABSOLUTE_PATH",default:"./.yarn/patches"}},commands:[R1,T1],fetchers:[rP],resolvers:[nP]},rSt=tSt;var IK={};Vt(IK,{PnpmLinker:()=>iP,default:()=>lSt});qe();Dt();Yt();var iP=class{getCustomDataKey(){return JSON.stringify({name:"PnpmLinker",version:3})}supportsPackage(t,r){return this.isEnabled(r)}async findPackageLocation(t,r){if(!this.isEnabled(r))throw new Error("Assertion failed: Expected the pnpm linker to be enabled");let s=this.getCustomDataKey(),a=r.project.linkersCustomData.get(s);if(!a)throw new it(`The project in ${pe.pretty(r.project.configuration,`${r.project.cwd}/package.json`,pe.Type.PATH)} doesn't seem to have been installed - running an install there might help`);let n=a.pathsByLocator.get(t.locatorHash);if(typeof n>"u")throw new it(`Couldn't find ${j.prettyLocator(r.project.configuration,t)} in the currently installed pnpm map - running an install might help`);return n.packageLocation}async findPackageLocator(t,r){if(!this.isEnabled(r))return null;let s=this.getCustomDataKey(),a=r.project.linkersCustomData.get(s);if(!a)throw new it(`The project in ${pe.pretty(r.project.configuration,`${r.project.cwd}/package.json`,pe.Type.PATH)} doesn't seem to have been installed - running an install there might help`);let n=t.match(/(^.*\/node_modules\/(@[^/]*\/)?[^/]+)(\/.*$)/);if(n){let p=a.locatorByPath.get(n[1]);if(p)return p}let c=t,f=t;do{f=c,c=J.dirname(f);let p=a.locatorByPath.get(f);if(p)return p}while(c!==f);return null}makeInstaller(t){return new yK(t)}isEnabled(t){return t.project.configuration.get("nodeLinker")==="pnpm"}},yK=class{constructor(t){this.opts=t;this.asyncActions=new Ge.AsyncActions(10);this.customData={pathsByLocator:new Map,locatorByPath:new Map};this.indexFolderPromise=$P(le,{indexPath:J.join(t.project.configuration.get("globalFolder"),"index")})}attachCustomData(t){}async installPackage(t,r,s){switch(t.linkType){case"SOFT":return this.installPackageSoft(t,r,s);case"HARD":return this.installPackageHard(t,r,s)}throw new Error("Assertion failed: Unsupported package link type")}async installPackageSoft(t,r,s){let a=J.resolve(r.packageFs.getRealPath(),r.prefixPath),n=this.opts.project.tryWorkspaceByLocator(t)?J.join(a,Er.nodeModules):null;return this.customData.pathsByLocator.set(t.locatorHash,{packageLocation:a,dependenciesLocation:n}),{packageLocation:a,buildRequest:null}}async installPackageHard(t,r,s){let a=iSt(t,{project:this.opts.project}),n=a.packageLocation;this.customData.locatorByPath.set(n,j.stringifyLocator(t)),this.customData.pathsByLocator.set(t.locatorHash,a),s.holdFetchResult(this.asyncActions.set(t.locatorHash,async()=>{await le.mkdirPromise(n,{recursive:!0}),await le.copyPromise(n,r.prefixPath,{baseFs:r.packageFs,overwrite:!1,linkStrategy:{type:"HardlinkFromIndex",indexPath:await this.indexFolderPromise,autoRepair:!0}})}));let f=j.isVirtualLocator(t)?j.devirtualizeLocator(t):t,p={manifest:await _t.tryFind(r.prefixPath,{baseFs:r.packageFs})??new _t,misc:{hasBindingGyp:mA.hasBindingGyp(r)}},h=this.opts.project.getDependencyMeta(f,t.version),E=mA.extractBuildRequest(t,p,h,{configuration:this.opts.project.configuration});return{packageLocation:n,buildRequest:E}}async attachInternalDependencies(t,r){if(this.opts.project.configuration.get("nodeLinker")!=="pnpm"||!Wbe(t,{project:this.opts.project}))return;let s=this.customData.pathsByLocator.get(t.locatorHash);if(typeof s>"u")throw new Error(`Assertion failed: Expected the package to have been registered (${j.stringifyLocator(t)})`);let{dependenciesLocation:a}=s;a&&this.asyncActions.reduce(t.locatorHash,async n=>{await le.mkdirPromise(a,{recursive:!0});let c=await sSt(a),f=new Map(c),p=[n],h=(C,S)=>{let x=S;Wbe(S,{project:this.opts.project})||(this.opts.report.reportWarningOnce(0,"The pnpm linker doesn't support providing different versions to workspaces' peer dependencies"),x=j.devirtualizeLocator(S));let I=this.customData.pathsByLocator.get(x.locatorHash);if(typeof I>"u")throw new Error(`Assertion failed: Expected the package to have been registered (${j.stringifyLocator(S)})`);let T=j.stringifyIdent(C),O=J.join(a,T),U=J.relative(J.dirname(O),I.packageLocation),V=f.get(T);f.delete(T),p.push(Promise.resolve().then(async()=>{if(V){if(V.isSymbolicLink()&&await le.readlinkPromise(O)===U)return;await le.removePromise(O)}await le.mkdirpPromise(J.dirname(O)),process.platform=="win32"&&this.opts.project.configuration.get("winLinkType")==="junctions"?await le.symlinkPromise(I.packageLocation,O,"junction"):await le.symlinkPromise(U,O)}))},E=!1;for(let[C,S]of r)C.identHash===t.identHash&&(E=!0),h(C,S);!E&&!this.opts.project.tryWorkspaceByLocator(t)&&h(j.convertLocatorToDescriptor(t),t),p.push(oSt(a,f)),await Promise.all(p)})}async attachExternalDependents(t,r){throw new Error("External dependencies haven't been implemented for the pnpm linker")}async finalizeInstall(){let t=Ybe(this.opts.project);if(this.opts.project.configuration.get("nodeLinker")!=="pnpm")await le.removePromise(t);else{let r;try{r=new Set(await le.readdirPromise(t))}catch{r=new Set}for(let{dependenciesLocation:s}of this.customData.pathsByLocator.values()){if(!s)continue;let a=J.contains(t,s);if(a===null)continue;let[n]=a.split(J.sep);r.delete(n)}await Promise.all([...r].map(async s=>{await le.removePromise(J.join(t,s))}))}return await this.asyncActions.wait(),await EK(t),this.opts.project.configuration.get("nodeLinker")!=="node-modules"&&await EK(nSt(this.opts.project)),{customData:this.customData}}};function nSt(e){return J.join(e.cwd,Er.nodeModules)}function Ybe(e){return e.configuration.get("pnpmStoreFolder")}function iSt(e,{project:t}){let r=j.slugifyLocator(e),s=Ybe(t),a=J.join(s,r,"package"),n=J.join(s,r,Er.nodeModules);return{packageLocation:a,dependenciesLocation:n}}function Wbe(e,{project:t}){return!j.isVirtualLocator(e)||!t.tryWorkspaceByLocator(e)}async function sSt(e){let t=new Map,r=[];try{r=await le.readdirPromise(e,{withFileTypes:!0})}catch(s){if(s.code!=="ENOENT")throw s}try{for(let s of r)if(!s.name.startsWith("."))if(s.name.startsWith("@")){let a=await le.readdirPromise(J.join(e,s.name),{withFileTypes:!0});if(a.length===0)t.set(s.name,s);else for(let n of a)t.set(`${s.name}/${n.name}`,n)}else t.set(s.name,s)}catch(s){if(s.code!=="ENOENT")throw s}return t}async function oSt(e,t){let r=[],s=new Set;for(let a of t.keys()){r.push(le.removePromise(J.join(e,a)));let n=j.tryParseIdent(a)?.scope;n&&s.add(`@${n}`)}return Promise.all(r).then(()=>Promise.all([...s].map(a=>EK(J.join(e,a)))))}async function EK(e){try{await le.rmdirPromise(e)}catch(t){if(t.code!=="ENOENT"&&t.code!=="ENOTEMPTY"&&t.code!=="EBUSY")throw t}}var aSt={configuration:{pnpmStoreFolder:{description:"By default, the store is stored in the 'node_modules/.store' of the project. Sometimes in CI scenario's it is convenient to store this in a different location so it can be cached and reused.",type:"ABSOLUTE_PATH",default:"./node_modules/.store"}},linkers:[iP]},lSt=aSt;var bK={};Vt(bK,{StageCommand:()=>F1,default:()=>ESt,stageUtils:()=>wL});qe();Dt();Yt();qe();Dt();var wL={};Vt(wL,{ActionType:()=>CK,checkConsensus:()=>CL,expandDirectory:()=>vK,findConsensus:()=>SK,findVcsRoot:()=>wK,genCommitMessage:()=>DK,getCommitPrefix:()=>Vbe,isYarnFile:()=>BK});Dt();var CK=(n=>(n[n.CREATE=0]="CREATE",n[n.DELETE=1]="DELETE",n[n.ADD=2]="ADD",n[n.REMOVE=3]="REMOVE",n[n.MODIFY=4]="MODIFY",n))(CK||{});async function wK(e,{marker:t}){do if(!le.existsSync(J.join(e,t)))e=J.dirname(e);else return e;while(e!=="/");return null}function BK(e,{roots:t,names:r}){if(r.has(J.basename(e)))return!0;do if(!t.has(e))e=J.dirname(e);else return!0;while(e!=="/");return!1}function vK(e){let t=[],r=[e];for(;r.length>0;){let s=r.pop(),a=le.readdirSync(s);for(let n of a){let c=J.resolve(s,n);le.lstatSync(c).isDirectory()?r.push(c):t.push(c)}}return t}function CL(e,t){let r=0,s=0;for(let a of e)a!=="wip"&&(t.test(a)?r+=1:s+=1);return r>=s}function SK(e){let t=CL(e,/^(\w\(\w+\):\s*)?\w+s/),r=CL(e,/^(\w\(\w+\):\s*)?[A-Z]/),s=CL(e,/^\w\(\w+\):/);return{useThirdPerson:t,useUpperCase:r,useComponent:s}}function Vbe(e){return e.useComponent?"chore(yarn): ":""}var cSt=new Map([[0,"create"],[1,"delete"],[2,"add"],[3,"remove"],[4,"update"]]);function DK(e,t){let r=Vbe(e),s=[],a=t.slice().sort((n,c)=>n[0]-c[0]);for(;a.length>0;){let[n,c]=a.shift(),f=cSt.get(n);e.useUpperCase&&s.length===0&&(f=`${f[0].toUpperCase()}${f.slice(1)}`),e.useThirdPerson&&(f+="s");let p=[c];for(;a.length>0&&a[0][0]===n;){let[,E]=a.shift();p.push(E)}p.sort();let h=p.shift();p.length===1?h+=" (and one other)":p.length>1&&(h+=` (and ${p.length} others)`),s.push(`${f} ${h}`)}return`${r}${s.join(", ")}`}var uSt="Commit generated via `yarn stage`",fSt=11;async function Jbe(e){let{code:t,stdout:r}=await qr.execvp("git",["log","-1","--pretty=format:%H"],{cwd:e});return t===0?r.trim():null}async function ASt(e,t){let r=[],s=t.filter(h=>J.basename(h.path)==="package.json");for(let{action:h,path:E}of s){let C=J.relative(e,E);if(h===4){let S=await Jbe(e),{stdout:x}=await qr.execvp("git",["show",`${S}:${C}`],{cwd:e,strict:!0}),I=await _t.fromText(x),T=await _t.fromFile(E),O=new Map([...T.dependencies,...T.devDependencies]),U=new Map([...I.dependencies,...I.devDependencies]);for(let[V,te]of U){let ie=j.stringifyIdent(te),ue=O.get(V);ue?ue.range!==te.range&&r.push([4,`${ie} to ${ue.range}`]):r.push([3,ie])}for(let[V,te]of O)U.has(V)||r.push([2,j.stringifyIdent(te)])}else if(h===0){let S=await _t.fromFile(E);S.name?r.push([0,j.stringifyIdent(S.name)]):r.push([0,"a package"])}else if(h===1){let S=await Jbe(e),{stdout:x}=await qr.execvp("git",["show",`${S}:${C}`],{cwd:e,strict:!0}),I=await _t.fromText(x);I.name?r.push([1,j.stringifyIdent(I.name)]):r.push([1,"a package"])}else throw new Error("Assertion failed: Unsupported action type")}let{code:a,stdout:n}=await qr.execvp("git",["log",`-${fSt}`,"--pretty=format:%s"],{cwd:e}),c=a===0?n.split(/\n/g).filter(h=>h!==""):[],f=SK(c);return DK(f,r)}var pSt={0:[" A ","?? "],4:[" M "],1:[" D "]},hSt={0:["A "],4:["M "],1:["D "]},Kbe={async findRoot(e){return await wK(e,{marker:".git"})},async filterChanges(e,t,r,s){let{stdout:a}=await qr.execvp("git",["status","-s"],{cwd:e,strict:!0}),n=a.toString().split(/\n/g),c=s?.staged?hSt:pSt;return[].concat(...n.map(p=>{if(p==="")return[];let h=p.slice(0,3),E=J.resolve(e,p.slice(3));if(!s?.staged&&h==="?? "&&p.endsWith("/"))return vK(E).map(C=>({action:0,path:C}));{let S=[0,4,1].find(x=>c[x].includes(h));return S!==void 0?[{action:S,path:E}]:[]}})).filter(p=>BK(p.path,{roots:t,names:r}))},async genCommitMessage(e,t){return await ASt(e,t)},async makeStage(e,t){let r=t.map(s=>fe.fromPortablePath(s.path));await qr.execvp("git",["add","--",...r],{cwd:e,strict:!0})},async makeCommit(e,t,r){let s=t.map(a=>fe.fromPortablePath(a.path));await qr.execvp("git",["add","-N","--",...s],{cwd:e,strict:!0}),await qr.execvp("git",["commit","-m",`${r} ${uSt} `,"--",...s],{cwd:e,strict:!0})},async makeReset(e,t){let r=t.map(s=>fe.fromPortablePath(s.path));await qr.execvp("git",["reset","HEAD","--",...r],{cwd:e,strict:!0})}};var dSt=[Kbe],F1=class extends At{constructor(){super(...arguments);this.commit=he.Boolean("-c,--commit",!1,{description:"Commit the staged files"});this.reset=he.Boolean("-r,--reset",!1,{description:"Remove all files from the staging area"});this.dryRun=he.Boolean("-n,--dry-run",!1,{description:"Print the commit message and the list of modified files without staging / committing"});this.update=he.Boolean("-u,--update",!1,{hidden:!0})}static{this.paths=[["stage"]]}static{this.usage=at.Usage({description:"add all yarn files to your vcs",details:"\n This command will add to your staging area the files belonging to Yarn (typically any modified `package.json` and `.yarnrc.yml` files, but also linker-generated files, cache data, etc). It will take your ignore list into account, so the cache files won't be added if the cache is ignored in a `.gitignore` file (assuming you use Git).\n\n Running `--reset` will instead remove them from the staging area (the changes will still be there, but won't be committed until you stage them back).\n\n Since the staging area is a non-existent concept in Mercurial, Yarn will always create a new commit when running this command on Mercurial repositories. You can get this behavior when using Git by using the `--commit` flag which will directly create a commit.\n ",examples:[["Adds all modified project files to the staging area","yarn stage"],["Creates a new commit containing all modified project files","yarn stage --commit"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Rt.find(r,this.context.cwd),{driver:a,root:n}=await gSt(s.cwd),c=[r.get("cacheFolder"),r.get("globalFolder"),r.get("virtualFolder"),r.get("yarnPath")];await r.triggerHook(C=>C.populateYarnPaths,s,C=>{c.push(C)});let f=new Set;for(let C of c)for(let S of mSt(n,C))f.add(S);let p=new Set([r.get("rcFilename"),Er.lockfile,Er.manifest]),h=await a.filterChanges(n,f,p),E=await a.genCommitMessage(n,h);if(this.dryRun)if(this.commit)this.context.stdout.write(`${E} `);else for(let C of h)this.context.stdout.write(`${fe.fromPortablePath(C.path)} `);else if(this.reset){let C=await a.filterChanges(n,f,p,{staged:!0});C.length===0?this.context.stdout.write("No staged changes found!"):await a.makeReset(n,C)}else h.length===0?this.context.stdout.write("No changes found!"):this.commit?await a.makeCommit(n,h,E):(await a.makeStage(n,h),this.context.stdout.write(E))}};async function gSt(e){let t=null,r=null;for(let s of dSt)if((r=await s.findRoot(e))!==null){t=s;break}if(t===null||r===null)throw new it("No stage driver has been found for your current project");return{driver:t,root:r}}function mSt(e,t){let r=[];if(t===null)return r;for(;;){(t===e||t.startsWith(`${e}/`))&&r.push(t);let s;try{s=le.statSync(t)}catch{break}if(s.isSymbolicLink())t=J.resolve(J.dirname(t),le.readlinkSync(t));else break}return r}var ySt={commands:[F1]},ESt=ySt;var PK={};Vt(PK,{default:()=>bSt});qe();qe();Dt();var Zbe=et(pi());qe();var zbe=et(F9()),ISt="e8e1bd300d860104bb8c58453ffa1eb4",CSt="OFCNCOG2CU",Xbe=async(e,t)=>{let r=j.stringifyIdent(e),a=wSt(t).initIndex("npm-search");try{return(await a.getObject(r,{attributesToRetrieve:["types"]})).types?.ts==="definitely-typed"}catch{return!1}},wSt=e=>(0,zbe.default)(CSt,ISt,{requester:{async send(r){try{let s=await nn.request(r.url,r.data||null,{configuration:e,headers:r.headers});return{content:s.body,isTimedOut:!1,status:s.statusCode}}catch(s){return{content:s.response.body,isTimedOut:!1,status:s.response.statusCode}}}}});var $be=e=>e.scope?`${e.scope}__${e.name}`:`${e.name}`,BSt=async(e,t,r,s)=>{if(r.scope==="types")return;let{project:a}=e,{configuration:n}=a;if(!(n.get("tsEnableAutoTypes")??(le.existsSync(J.join(e.cwd,"tsconfig.json"))||le.existsSync(J.join(a.cwd,"tsconfig.json")))))return;let f=n.makeResolver(),p={project:a,resolver:f,report:new ki};if(!await Xbe(r,n))return;let E=$be(r),C=j.parseRange(r.range).selector;if(!kr.validRange(C)){let O=n.normalizeDependency(r),U=await f.getCandidates(O,{},p);C=j.parseRange(U[0].reference).selector}let S=Zbe.default.coerce(C);if(S===null)return;let x=`${$u.Modifier.CARET}${S.major}`,I=j.makeDescriptor(j.makeIdent("types",E),x),T=Ge.mapAndFind(a.workspaces,O=>{let U=O.manifest.dependencies.get(r.identHash)?.descriptorHash,V=O.manifest.devDependencies.get(r.identHash)?.descriptorHash;if(U!==r.descriptorHash&&V!==r.descriptorHash)return Ge.mapAndFind.skip;let te=[];for(let ie of _t.allDependencies){let ue=O.manifest[ie].get(I.identHash);typeof ue>"u"||te.push([ie,ue])}return te.length===0?Ge.mapAndFind.skip:te});if(typeof T<"u")for(let[O,U]of T)e.manifest[O].set(U.identHash,U);else{try{let O=n.normalizeDependency(I);if((await f.getCandidates(O,{},p)).length===0)return}catch{return}e.manifest[$u.Target.DEVELOPMENT].set(I.identHash,I)}},vSt=async(e,t,r)=>{if(r.scope==="types")return;let{project:s}=e,{configuration:a}=s;if(!(a.get("tsEnableAutoTypes")??(le.existsSync(J.join(e.cwd,"tsconfig.json"))||le.existsSync(J.join(s.cwd,"tsconfig.json")))))return;let c=$be(r),f=j.makeIdent("types",c);for(let p of _t.allDependencies)typeof e.manifest[p].get(f.identHash)>"u"||e.manifest[p].delete(f.identHash)},SSt=(e,t)=>{t.publishConfig&&t.publishConfig.typings&&(t.typings=t.publishConfig.typings),t.publishConfig&&t.publishConfig.types&&(t.types=t.publishConfig.types)},DSt={configuration:{tsEnableAutoTypes:{description:"Whether Yarn should auto-install @types/ dependencies on 'yarn add'",type:"BOOLEAN",isNullable:!0,default:null}},hooks:{afterWorkspaceDependencyAddition:BSt,afterWorkspaceDependencyRemoval:vSt,beforeWorkspacePacking:SSt}},bSt=DSt;var TK={};Vt(TK,{VersionApplyCommand:()=>M1,VersionCheckCommand:()=>U1,VersionCommand:()=>_1,default:()=>RSt,versionUtils:()=>L1});qe();qe();Yt();var L1={};Vt(L1,{Decision:()=>N1,applyPrerelease:()=>ePe,applyReleases:()=>RK,applyStrategy:()=>sP,clearVersionFiles:()=>xK,getUndecidedDependentWorkspaces:()=>aP,getUndecidedWorkspaces:()=>BL,openVersionFile:()=>O1,requireMoreDecisions:()=>xSt,resolveVersionFiles:()=>oP,suggestStrategy:()=>QK,updateVersionFiles:()=>kK,validateReleaseDecision:()=>Ey});qe();Dt();vc();Yt();zl();var RA=et(pi()),PSt=/^(>=|[~^]|)(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$/,N1=(h=>(h.UNDECIDED="undecided",h.DECLINE="decline",h.MAJOR="major",h.MINOR="minor",h.PATCH="patch",h.PREMAJOR="premajor",h.PREMINOR="preminor",h.PREPATCH="prepatch",h.PRERELEASE="prerelease",h))(N1||{});function Ey(e){let t=RA.default.valid(e);return t||Ge.validateEnum(I4(N1,"UNDECIDED"),e)}async function oP(e,{prerelease:t=null}={}){let r=new Map,s=e.configuration.get("deferredVersionFolder");if(!le.existsSync(s))return r;let a=await le.readdirPromise(s);for(let n of a){if(!n.endsWith(".yml"))continue;let c=J.join(s,n),f=await le.readFilePromise(c,"utf8"),p=cs(f);for(let[h,E]of Object.entries(p.releases||{})){if(E==="decline")continue;let C=j.parseIdent(h),S=e.tryWorkspaceByIdent(C);if(S===null)throw new Error(`Assertion failed: Expected a release definition file to only reference existing workspaces (${J.basename(c)} references ${h})`);if(S.manifest.version===null)throw new Error(`Assertion failed: Expected the workspace to have a version (${j.prettyLocator(e.configuration,S.anchoredLocator)})`);let x=S.manifest.raw.stableVersion??S.manifest.version,I=r.get(S),T=sP(E==="prerelease"?S.manifest.version:x,Ey(E));if(T===null)throw new Error(`Assertion failed: Expected ${x} to support being bumped via strategy ${E}`);let O=typeof I<"u"?RA.default.gt(T,I)?T:I:T;r.set(S,O)}}return t&&(r=new Map([...r].map(([n,c])=>[n,ePe(c,{current:n.manifest.version,prerelease:t})]))),r}async function xK(e){let t=e.configuration.get("deferredVersionFolder");le.existsSync(t)&&await le.removePromise(t)}async function kK(e,t){let r=new Set(t),s=e.configuration.get("deferredVersionFolder");if(!le.existsSync(s))return;let a=await le.readdirPromise(s);for(let n of a){if(!n.endsWith(".yml"))continue;let c=J.join(s,n),f=await le.readFilePromise(c,"utf8"),p=cs(f),h=p?.releases;if(h){for(let E of Object.keys(h)){let C=j.parseIdent(E),S=e.tryWorkspaceByIdent(C);(S===null||r.has(S))&&delete p.releases[E]}Object.keys(p.releases).length>0?await le.changeFilePromise(c,fl(new fl.PreserveOrdering(p))):await le.unlinkPromise(c)}}}async function O1(e,{allowEmpty:t=!1}={}){let r=e.configuration;if(r.projectCwd===null)throw new it("This command can only be run from within a Yarn project");let s=await La.fetchRoot(r.projectCwd),a=s!==null?await La.fetchBase(s,{baseRefs:r.get("changesetBaseRefs")}):null,n=s!==null?await La.fetchChangedFiles(s,{base:a.hash,project:e}):[],c=r.get("deferredVersionFolder"),f=n.filter(x=>J.contains(c,x)!==null);if(f.length>1)throw new it(`Your current branch contains multiple versioning files; this isn't supported: - ${f.map(x=>fe.fromPortablePath(x)).join(` - `)}`);let p=new Set(Ge.mapAndFilter(n,x=>{let I=e.tryWorkspaceByFilePath(x);return I===null?Ge.mapAndFilter.skip:I}));if(f.length===0&&p.size===0&&!t)return null;let h=f.length===1?f[0]:J.join(c,`${Ln.makeHash(Math.random().toString()).slice(0,8)}.yml`),E=le.existsSync(h)?await le.readFilePromise(h,"utf8"):"{}",C=cs(E),S=new Map;for(let x of C.declined||[]){let I=j.parseIdent(x),T=e.getWorkspaceByIdent(I);S.set(T,"decline")}for(let[x,I]of Object.entries(C.releases||{})){let T=j.parseIdent(x),O=e.getWorkspaceByIdent(T);S.set(O,Ey(I))}return{project:e,root:s,baseHash:a!==null?a.hash:null,baseTitle:a!==null?a.title:null,changedFiles:new Set(n),changedWorkspaces:p,releaseRoots:new Set([...p].filter(x=>x.manifest.version!==null)),releases:S,async saveAll(){let x={},I=[],T=[];for(let O of e.workspaces){if(O.manifest.version===null)continue;let U=j.stringifyIdent(O.anchoredLocator),V=S.get(O);V==="decline"?I.push(U):typeof V<"u"?x[U]=Ey(V):p.has(O)&&T.push(U)}await le.mkdirPromise(J.dirname(h),{recursive:!0}),await le.changeFilePromise(h,fl(new fl.PreserveOrdering({releases:Object.keys(x).length>0?x:void 0,declined:I.length>0?I:void 0,undecided:T.length>0?T:void 0})))}}}function xSt(e){return BL(e).size>0||aP(e).length>0}function BL(e){let t=new Set;for(let r of e.changedWorkspaces)r.manifest.version!==null&&(e.releases.has(r)||t.add(r));return t}function aP(e,{include:t=new Set}={}){let r=[],s=new Map(Ge.mapAndFilter([...e.releases],([n,c])=>c==="decline"?Ge.mapAndFilter.skip:[n.anchoredLocator.locatorHash,n])),a=new Map(Ge.mapAndFilter([...e.releases],([n,c])=>c!=="decline"?Ge.mapAndFilter.skip:[n.anchoredLocator.locatorHash,n]));for(let n of e.project.workspaces)if(!(!t.has(n)&&(a.has(n.anchoredLocator.locatorHash)||s.has(n.anchoredLocator.locatorHash)))&&n.manifest.version!==null)for(let c of _t.hardDependencies)for(let f of n.manifest.getForScope(c).values()){let p=e.project.tryWorkspaceByDescriptor(f);p!==null&&s.has(p.anchoredLocator.locatorHash)&&r.push([n,p])}return r}function QK(e,t){let r=RA.default.clean(t);for(let s of Object.values(N1))if(s!=="undecided"&&s!=="decline"&&RA.default.inc(e,s)===r)return s;return null}function sP(e,t){if(RA.default.valid(t))return t;if(e===null)throw new it(`Cannot apply the release strategy "${t}" unless the workspace already has a valid version`);if(!RA.default.valid(e))throw new it(`Cannot apply the release strategy "${t}" on a non-semver version (${e})`);let r=RA.default.inc(e,t);if(r===null)throw new it(`Cannot apply the release strategy "${t}" on the specified version (${e})`);return r}function RK(e,t,{report:r,exact:s}){let a=new Map;for(let n of e.workspaces)for(let c of _t.allDependencies)for(let f of n.manifest[c].values()){let p=e.tryWorkspaceByDescriptor(f);if(p===null||!t.has(p))continue;Ge.getArrayWithDefault(a,p).push([n,c,f.identHash])}for(let[n,c]of t){let f=n.manifest.version;n.manifest.version=c,RA.default.prerelease(c)===null?delete n.manifest.raw.stableVersion:n.manifest.raw.stableVersion||(n.manifest.raw.stableVersion=f);let p=n.manifest.name!==null?j.stringifyIdent(n.manifest.name):null;r.reportInfo(0,`${j.prettyLocator(e.configuration,n.anchoredLocator)}: Bumped to ${c}`),r.reportJson({cwd:fe.fromPortablePath(n.cwd),ident:p,oldVersion:f,newVersion:c});let h=a.get(n);if(!(typeof h>"u"))for(let[E,C,S]of h){let x=E.manifest[C].get(S);if(typeof x>"u")throw new Error("Assertion failed: The dependency should have existed");let I=x.range,T=!1;if(I.startsWith(Ii.protocol)&&(I=I.slice(Ii.protocol.length),T=!0,I===n.relativeCwd))continue;let O=I.match(PSt);if(!O){r.reportWarning(0,`Couldn't auto-upgrade range ${I} (in ${j.prettyLocator(e.configuration,E.anchoredLocator)})`);continue}let U=s?`${c}`:`${O[1]}${c}`;T&&(U=`${Ii.protocol}${U}`);let V=j.makeDescriptor(x,U);E.manifest[C].set(S,V)}}}var kSt=new Map([["%n",{extract:e=>e.length>=1?[e[0],e.slice(1)]:null,generate:(e=0)=>`${e+1}`}]]);function ePe(e,{current:t,prerelease:r}){let s=new RA.default.SemVer(t),a=s.prerelease.slice(),n=[];s.prerelease=[],s.format()!==e&&(a.length=0);let c=!0,f=r.split(/\./g);for(let p of f){let h=kSt.get(p);if(typeof h>"u")n.push(p),a[0]===p?a.shift():c=!1;else{let E=c?h.extract(a):null;E!==null&&typeof E[0]=="number"?(n.push(h.generate(E[0])),a=E[1]):(n.push(h.generate()),c=!1)}}return s.prerelease&&(s.prerelease=[]),`${e}-${n.join(".")}`}var M1=class extends At{constructor(){super(...arguments);this.all=he.Boolean("--all",!1,{description:"Apply the deferred version changes on all workspaces"});this.dryRun=he.Boolean("--dry-run",!1,{description:"Print the versions without actually generating the package archive"});this.prerelease=he.String("--prerelease",{description:"Add a prerelease identifier to new versions",tolerateBoolean:!0});this.exact=he.Boolean("--exact",!1,{description:"Use the exact version of each package, removes any range. Useful for nightly releases where the range might match another version."});this.recursive=he.Boolean("-R,--recursive",{description:"Release the transitive workspaces as well"});this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}static{this.paths=[["version","apply"]]}static{this.usage=at.Usage({category:"Release-related commands",description:"apply all the deferred version bumps at once",details:` This command will apply the deferred version changes and remove their definitions from the repository. Note that if \`--prerelease\` is set, the given prerelease identifier (by default \`rc.%n\`) will be used on all new versions and the version definitions will be kept as-is. By default only the current workspace will be bumped, but you can configure this behavior by using one of: - \`--recursive\` to also apply the version bump on its dependencies - \`--all\` to apply the version bump on all packages in the repository Note that this command will also update the \`workspace:\` references across all your local workspaces, thus ensuring that they keep referring to the same workspaces even after the version bump. `,examples:[["Apply the version change to the local workspace","yarn version apply"],["Apply the version change to all the workspaces in the local workspace","yarn version apply --all"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState({restoreResolutions:!1});let c=await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout},async f=>{let p=this.prerelease?typeof this.prerelease!="boolean"?this.prerelease:"rc.%n":null,h=await oP(s,{prerelease:p}),E=new Map;if(this.all)E=h;else{let C=this.recursive?a.getRecursiveWorkspaceDependencies():[a];for(let S of C){let x=h.get(S);typeof x<"u"&&E.set(S,x)}}if(E.size===0){let C=h.size>0?" Did you want to add --all?":"";f.reportWarning(0,`The current workspace doesn't seem to require a version bump.${C}`);return}RK(s,E,{report:f,exact:this.exact}),this.dryRun||(p||(this.all?await xK(s):await kK(s,[...E.keys()])),f.reportSeparator())});return this.dryRun||c.hasErrors()?c.exitCode():await s.installWithNewReport({json:this.json,stdout:this.context.stdout},{cache:n})}};qe();Dt();Yt();var vL=et(pi());var U1=class extends At{constructor(){super(...arguments);this.interactive=he.Boolean("-i,--interactive",{description:"Open an interactive interface used to set version bumps"})}static{this.paths=[["version","check"]]}static{this.usage=at.Usage({category:"Release-related commands",description:"check that all the relevant packages have been bumped",details:"\n **Warning:** This command currently requires Git.\n\n This command will check that all the packages covered by the files listed in argument have been properly bumped or declined to bump.\n\n In the case of a bump, the check will also cover transitive packages - meaning that should `Foo` be bumped, a package `Bar` depending on `Foo` will require a decision as to whether `Bar` will need to be bumped. This check doesn't cross packages that have declined to bump.\n\n In case no arguments are passed to the function, the list of modified files will be generated by comparing the HEAD against `master`.\n ",examples:[["Check whether the modified packages need a bump","yarn version check"]]})}async execute(){return this.interactive?await this.executeInteractive():await this.executeStandard()}async executeInteractive(){KC(this.context);let{Gem:r}=await Promise.resolve().then(()=>(xF(),rW)),{ScrollableItems:s}=await Promise.resolve().then(()=>(TF(),RF)),{FocusRequest:a}=await Promise.resolve().then(()=>(sW(),Awe)),{useListInput:n}=await Promise.resolve().then(()=>(QF(),pwe)),{renderForm:c}=await Promise.resolve().then(()=>(LF(),OF)),{Box:f,Text:p}=await Promise.resolve().then(()=>et(qc())),{default:h,useCallback:E,useState:C}=await Promise.resolve().then(()=>et(dn())),S=await ze.find(this.context.cwd,this.context.plugins),{project:x,workspace:I}=await Rt.find(S,this.context.cwd);if(!I)throw new ar(x.cwd,this.context.cwd);await x.restoreInstallState();let T=await O1(x);if(T===null||T.releaseRoots.size===0)return 0;if(T.root===null)throw new it("This command can only be run on Git repositories");let O=()=>h.createElement(f,{flexDirection:"row",paddingBottom:1},h.createElement(f,{flexDirection:"column",width:60},h.createElement(f,null,h.createElement(p,null,"Press ",h.createElement(p,{bold:!0,color:"cyanBright"},""),"/",h.createElement(p,{bold:!0,color:"cyanBright"},"")," to select workspaces.")),h.createElement(f,null,h.createElement(p,null,"Press ",h.createElement(p,{bold:!0,color:"cyanBright"},""),"/",h.createElement(p,{bold:!0,color:"cyanBright"},"")," to select release strategies."))),h.createElement(f,{flexDirection:"column"},h.createElement(f,{marginLeft:1},h.createElement(p,null,"Press ",h.createElement(p,{bold:!0,color:"cyanBright"},"")," to save.")),h.createElement(f,{marginLeft:1},h.createElement(p,null,"Press ",h.createElement(p,{bold:!0,color:"cyanBright"},"")," to abort.")))),U=({workspace:ge,active:Ae,decision:Ce,setDecision:Ee})=>{let d=ge.manifest.raw.stableVersion??ge.manifest.version;if(d===null)throw new Error(`Assertion failed: The version should have been set (${j.prettyLocator(S,ge.anchoredLocator)})`);if(vL.default.prerelease(d)!==null)throw new Error(`Assertion failed: Prerelease identifiers shouldn't be found (${d})`);let Se=["undecided","decline","patch","minor","major"];n(Ce,Se,{active:Ae,minus:"left",plus:"right",set:Ee});let Be=Ce==="undecided"?h.createElement(p,{color:"yellow"},d):Ce==="decline"?h.createElement(p,{color:"green"},d):h.createElement(p,null,h.createElement(p,{color:"magenta"},d)," \u2192 ",h.createElement(p,{color:"green"},vL.default.valid(Ce)?Ce:vL.default.inc(d,Ce)));return h.createElement(f,{flexDirection:"column"},h.createElement(f,null,h.createElement(p,null,j.prettyLocator(S,ge.anchoredLocator)," - ",Be)),h.createElement(f,null,Se.map(me=>h.createElement(f,{key:me,paddingLeft:2},h.createElement(p,null,h.createElement(r,{active:me===Ce})," ",me)))))},V=ge=>{let Ae=new Set(T.releaseRoots),Ce=new Map([...ge].filter(([Ee])=>Ae.has(Ee)));for(;;){let Ee=aP({project:T.project,releases:Ce}),d=!1;if(Ee.length>0){for(let[Se]of Ee)if(!Ae.has(Se)){Ae.add(Se),d=!0;let Be=ge.get(Se);typeof Be<"u"&&Ce.set(Se,Be)}}if(!d)break}return{relevantWorkspaces:Ae,relevantReleases:Ce}},te=()=>{let[ge,Ae]=C(()=>new Map(T.releases)),Ce=E((Ee,d)=>{let Se=new Map(ge);d!=="undecided"?Se.set(Ee,d):Se.delete(Ee);let{relevantReleases:Be}=V(Se);Ae(Be)},[ge,Ae]);return[ge,Ce]},ie=({workspaces:ge,releases:Ae})=>{let Ce=[];Ce.push(`${ge.size} total`);let Ee=0,d=0;for(let Se of ge){let Be=Ae.get(Se);typeof Be>"u"?d+=1:Be!=="decline"&&(Ee+=1)}return Ce.push(`${Ee} release${Ee===1?"":"s"}`),Ce.push(`${d} remaining`),h.createElement(p,{color:"yellow"},Ce.join(", "))},ae=await c(({useSubmit:ge})=>{let[Ae,Ce]=te();ge(Ae);let{relevantWorkspaces:Ee}=V(Ae),d=new Set([...Ee].filter(ce=>!T.releaseRoots.has(ce))),[Se,Be]=C(0),me=E(ce=>{switch(ce){case a.BEFORE:Be(Se-1);break;case a.AFTER:Be(Se+1);break}},[Se,Be]);return h.createElement(f,{flexDirection:"column"},h.createElement(O,null),h.createElement(f,null,h.createElement(p,{wrap:"wrap"},"The following files have been modified in your local checkout.")),h.createElement(f,{flexDirection:"column",marginTop:1,paddingLeft:2},[...T.changedFiles].map(ce=>h.createElement(f,{key:ce},h.createElement(p,null,h.createElement(p,{color:"grey"},fe.fromPortablePath(T.root)),fe.sep,fe.relative(fe.fromPortablePath(T.root),fe.fromPortablePath(ce)))))),T.releaseRoots.size>0&&h.createElement(h.Fragment,null,h.createElement(f,{marginTop:1},h.createElement(p,{wrap:"wrap"},"Because of those files having been modified, the following workspaces may need to be released again (note that private workspaces are also shown here, because even though they won't be published, releasing them will allow us to flag their dependents for potential re-release):")),d.size>3?h.createElement(f,{marginTop:1},h.createElement(ie,{workspaces:T.releaseRoots,releases:Ae})):null,h.createElement(f,{marginTop:1,flexDirection:"column"},h.createElement(s,{active:Se%2===0,radius:1,size:2,onFocusRequest:me},[...T.releaseRoots].map(ce=>h.createElement(U,{key:ce.cwd,workspace:ce,decision:Ae.get(ce)||"undecided",setDecision:Z=>Ce(ce,Z)}))))),d.size>0?h.createElement(h.Fragment,null,h.createElement(f,{marginTop:1},h.createElement(p,{wrap:"wrap"},"The following workspaces depend on other workspaces that have been marked for release, and thus may need to be released as well:")),h.createElement(f,null,h.createElement(p,null,"(Press ",h.createElement(p,{bold:!0,color:"cyanBright"},"")," to move the focus between the workspace groups.)")),d.size>5?h.createElement(f,{marginTop:1},h.createElement(ie,{workspaces:d,releases:Ae})):null,h.createElement(f,{marginTop:1,flexDirection:"column"},h.createElement(s,{active:Se%2===1,radius:2,size:2,onFocusRequest:me},[...d].map(ce=>h.createElement(U,{key:ce.cwd,workspace:ce,decision:Ae.get(ce)||"undecided",setDecision:Z=>Ce(ce,Z)}))))):null)},{versionFile:T},{stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr});if(typeof ae>"u")return 1;T.releases.clear();for(let[ge,Ae]of ae)T.releases.set(ge,Ae);await T.saveAll()}async executeStandard(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);return await s.restoreInstallState(),(await Ot.start({configuration:r,stdout:this.context.stdout},async c=>{let f=await O1(s);if(f===null||f.releaseRoots.size===0)return;if(f.root===null)throw new it("This command can only be run on Git repositories");if(c.reportInfo(0,`Your PR was started right after ${pe.pretty(r,f.baseHash.slice(0,7),"yellow")} ${pe.pretty(r,f.baseTitle,"magenta")}`),f.changedFiles.size>0){c.reportInfo(0,"You have changed the following files since then:"),c.reportSeparator();for(let S of f.changedFiles)c.reportInfo(null,`${pe.pretty(r,fe.fromPortablePath(f.root),"gray")}${fe.sep}${fe.relative(fe.fromPortablePath(f.root),fe.fromPortablePath(S))}`)}let p=!1,h=!1,E=BL(f);if(E.size>0){p||c.reportSeparator();for(let S of E)c.reportError(0,`${j.prettyLocator(r,S.anchoredLocator)} has been modified but doesn't have a release strategy attached`);p=!0}let C=aP(f);for(let[S,x]of C)h||c.reportSeparator(),c.reportError(0,`${j.prettyLocator(r,S.anchoredLocator)} doesn't have a release strategy attached, but depends on ${j.prettyWorkspace(r,x)} which is planned for release.`),h=!0;(p||h)&&(c.reportSeparator(),c.reportInfo(0,"This command detected that at least some workspaces have received modifications without explicit instructions as to how they had to be released (if needed)."),c.reportInfo(0,"To correct these errors, run `yarn version check --interactive` then follow the instructions."))})).exitCode()}};qe();Yt();var SL=et(pi());var _1=class extends At{constructor(){super(...arguments);this.deferred=he.Boolean("-d,--deferred",{description:"Prepare the version to be bumped during the next release cycle"});this.immediate=he.Boolean("-i,--immediate",{description:"Bump the version immediately"});this.strategy=he.String()}static{this.paths=[["version"]]}static{this.usage=at.Usage({category:"Release-related commands",description:"apply a new version to the current package",details:"\n This command will bump the version number for the given package, following the specified strategy:\n\n - If `major`, the first number from the semver range will be increased (`X.0.0`).\n - If `minor`, the second number from the semver range will be increased (`0.X.0`).\n - If `patch`, the third number from the semver range will be increased (`0.0.X`).\n - If prefixed by `pre` (`premajor`, ...), a `-0` suffix will be set (`0.0.0-0`).\n - If `prerelease`, the suffix will be increased (`0.0.0-X`); the third number from the semver range will also be increased if there was no suffix in the previous version.\n - If `decline`, the nonce will be increased for `yarn version check` to pass without version bump.\n - If a valid semver range, it will be used as new version.\n - If unspecified, Yarn will ask you for guidance.\n\n For more information about the `--deferred` flag, consult our documentation (https://yarnpkg.com/features/release-workflow#deferred-versioning).\n ",examples:[["Immediately bump the version to the next major","yarn version major"],["Prepare the version to be bumped to the next major","yarn version major --deferred"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);let n=r.get("preferDeferredVersions");this.deferred&&(n=!0),this.immediate&&(n=!1);let c=SL.default.valid(this.strategy),f=this.strategy==="decline",p;if(c)if(a.manifest.version!==null){let E=QK(a.manifest.version,this.strategy);E!==null?p=E:p=this.strategy}else p=this.strategy;else{let E=a.manifest.version;if(!f){if(E===null)throw new it("Can't bump the version if there wasn't a version to begin with - use 0.0.0 as initial version then run the command again.");if(typeof E!="string"||!SL.default.valid(E))throw new it(`Can't bump the version (${E}) if it's not valid semver`)}p=Ey(this.strategy)}if(!n){let C=(await oP(s)).get(a);if(typeof C<"u"&&p!=="decline"){let S=sP(a.manifest.version,p);if(SL.default.lt(S,C))throw new it(`Can't bump the version to one that would be lower than the current deferred one (${C})`)}}let h=await O1(s,{allowEmpty:!0});return h.releases.set(a,p),await h.saveAll(),n?0:await this.cli.run(["version","apply"])}};var QSt={configuration:{deferredVersionFolder:{description:"Folder where are stored the versioning files",type:"ABSOLUTE_PATH",default:"./.yarn/versions"},preferDeferredVersions:{description:"If true, running `yarn version` will assume the `--deferred` flag unless `--immediate` is set",type:"BOOLEAN",default:!1}},commands:[M1,U1,_1]},RSt=QSt;var FK={};Vt(FK,{WorkspacesFocusCommand:()=>H1,WorkspacesForeachCommand:()=>G1,default:()=>NSt});qe();qe();Yt();var H1=class extends At{constructor(){super(...arguments);this.json=he.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.production=he.Boolean("--production",!1,{description:"Only install regular dependencies by omitting dev dependencies"});this.all=he.Boolean("-A,--all",!1,{description:"Install the entire project"});this.workspaces=he.Rest()}static{this.paths=[["workspaces","focus"]]}static{this.usage=at.Usage({category:"Workspace-related commands",description:"install a single workspace and its dependencies",details:"\n This command will run an install as if the specified workspaces (and all other workspaces they depend on) were the only ones in the project. If no workspaces are explicitly listed, the active one will be assumed.\n\n Note that this command is only very moderately useful when using zero-installs, since the cache will contain all the packages anyway - meaning that the only difference between a full install and a focused install would just be a few extra lines in the `.pnp.cjs` file, at the cost of introducing an extra complexity.\n\n If the `-A,--all` flag is set, the entire project will be installed. Combine with `--production` to replicate the old `yarn install --production`.\n "})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd),n=await Kr.find(r);await s.restoreInstallState({restoreResolutions:!1});let c;if(this.all)c=new Set(s.workspaces);else if(this.workspaces.length===0){if(!a)throw new ar(s.cwd,this.context.cwd);c=new Set([a])}else c=new Set(this.workspaces.map(f=>s.getWorkspaceByIdent(j.parseIdent(f))));for(let f of c)for(let p of this.production?["dependencies"]:_t.hardDependencies)for(let h of f.manifest.getForScope(p).values()){let E=s.tryWorkspaceByDescriptor(h);E!==null&&c.add(E)}for(let f of s.workspaces)c.has(f)?this.production&&f.manifest.devDependencies.clear():(f.manifest.installConfig=f.manifest.installConfig||{},f.manifest.installConfig.selfReferences=!1,f.manifest.dependencies.clear(),f.manifest.devDependencies.clear(),f.manifest.peerDependencies.clear(),f.manifest.scripts.clear());return await s.installWithNewReport({json:this.json,stdout:this.context.stdout},{cache:n,persistProject:!1})}};qe();qe();qe();Yt();var j1=et(zo()),rPe=et(Ng());Al();var G1=class extends At{constructor(){super(...arguments);this.from=he.Array("--from",{description:"An array of glob pattern idents or paths from which to base any recursion"});this.all=he.Boolean("-A,--all",{description:"Run the command on all workspaces of a project"});this.recursive=he.Boolean("-R,--recursive",{description:"Run the command on the current workspace and all of its recursive dependencies"});this.worktree=he.Boolean("-W,--worktree",{description:"Run the command on all workspaces of the current worktree"});this.verbose=he.Counter("-v,--verbose",{description:"Increase level of logging verbosity up to 2 times"});this.parallel=he.Boolean("-p,--parallel",!1,{description:"Run the commands in parallel"});this.interlaced=he.Boolean("-i,--interlaced",!1,{description:"Print the output of commands in real-time instead of buffering it"});this.jobs=he.String("-j,--jobs",{description:"The maximum number of parallel tasks that the execution will be limited to; or `unlimited`",validator:$U([ks(["unlimited"]),W2(ZU(),[t_(),e_(1)])])});this.topological=he.Boolean("-t,--topological",!1,{description:"Run the command after all workspaces it depends on (regular) have finished"});this.topologicalDev=he.Boolean("--topological-dev",!1,{description:"Run the command after all workspaces it depends on (regular + dev) have finished"});this.include=he.Array("--include",[],{description:"An array of glob pattern idents or paths; only matching workspaces will be traversed"});this.exclude=he.Array("--exclude",[],{description:"An array of glob pattern idents or paths; matching workspaces won't be traversed"});this.publicOnly=he.Boolean("--no-private",{description:"Avoid running the command on private workspaces"});this.since=he.String("--since",{description:"Only include workspaces that have been changed since the specified ref.",tolerateBoolean:!0});this.dryRun=he.Boolean("-n,--dry-run",{description:"Print the commands that would be run, without actually running them"});this.commandName=he.String();this.args=he.Proxy()}static{this.paths=[["workspaces","foreach"]]}static{this.usage=at.Usage({category:"Workspace-related commands",description:"run a command on all workspaces",details:"\n This command will run a given sub-command on current and all its descendant workspaces. Various flags can alter the exact behavior of the command:\n\n - If `-p,--parallel` is set, the commands will be ran in parallel; they'll by default be limited to a number of parallel tasks roughly equal to half your core number, but that can be overridden via `-j,--jobs`, or disabled by setting `-j unlimited`.\n\n - If `-p,--parallel` and `-i,--interlaced` are both set, Yarn will print the lines from the output as it receives them. If `-i,--interlaced` wasn't set, it would instead buffer the output from each process and print the resulting buffers only after their source processes have exited.\n\n - If `-t,--topological` is set, Yarn will only run the command after all workspaces that it depends on through the `dependencies` field have successfully finished executing. If `--topological-dev` is set, both the `dependencies` and `devDependencies` fields will be considered when figuring out the wait points.\n\n - If `-A,--all` is set, Yarn will run the command on all the workspaces of a project.\n\n - If `-R,--recursive` is set, Yarn will find workspaces to run the command on by recursively evaluating `dependencies` and `devDependencies` fields, instead of looking at the `workspaces` fields.\n\n - If `-W,--worktree` is set, Yarn will find workspaces to run the command on by looking at the current worktree.\n\n - If `--from` is set, Yarn will use the packages matching the 'from' glob as the starting point for any recursive search.\n\n - If `--since` is set, Yarn will only run the command on workspaces that have been modified since the specified ref. By default Yarn will use the refs specified by the `changesetBaseRefs` configuration option.\n\n - If `--dry-run` is set, Yarn will explain what it would do without actually doing anything.\n\n - The command may apply to only some workspaces through the use of `--include` which acts as a whitelist. The `--exclude` flag will do the opposite and will be a list of packages that mustn't execute the script. Both flags accept glob patterns (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them. You can also use the `--no-private` flag to avoid running the command in private workspaces.\n\n The `-v,--verbose` flag can be passed up to twice: once to prefix output lines with the originating workspace's name, and again to include start/finish/timing log lines. Maximum verbosity is enabled by default in terminal environments.\n\n If the command is `run` and the script being run does not exist the child workspace will be skipped without error.\n ",examples:[["Publish all packages","yarn workspaces foreach -A --no-private npm publish --tolerate-republish"],["Run the build script on all descendant packages","yarn workspaces foreach -A run build"],["Run the build script on current and all descendant packages in parallel, building package dependencies first","yarn workspaces foreach -Apt run build"],["Run the build script on several packages and all their dependencies, building dependencies first","yarn workspaces foreach -Rpt --from '{workspace-a,workspace-b}' run build"]]})}static{this.schema=[V2("all",Vf.Forbids,["from","recursive","since","worktree"],{missingIf:"undefined"}),r_(["all","recursive","since","worktree"],{missingIf:"undefined"})]}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Rt.find(r,this.context.cwd);if(!this.all&&!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState();let n=this.cli.process([this.commandName,...this.args]),c=n.path.length===1&&n.path[0]==="run"&&typeof n.scriptName<"u"?n.scriptName:null;if(n.path.length===0)throw new it("Invalid subcommand name for iteration - use the 'run' keyword if you wish to execute a script");let f=Ee=>{this.dryRun&&this.context.stdout.write(`${Ee} `)},p=()=>{let Ee=this.from.map(d=>j1.default.matcher(d));return s.workspaces.filter(d=>{let Se=j.stringifyIdent(d.anchoredLocator),Be=d.relativeCwd;return Ee.some(me=>me(Se)||me(Be))})},h=[];if(this.since?(f("Option --since is set; selecting the changed workspaces as root for workspace selection"),h=Array.from(await La.fetchChangedWorkspaces({ref:this.since,project:s}))):this.from?(f("Option --from is set; selecting the specified workspaces"),h=[...p()]):this.worktree?(f("Option --worktree is set; selecting the current workspace"),h=[a]):this.recursive?(f("Option --recursive is set; selecting the current workspace"),h=[a]):this.all&&(f("Option --all is set; selecting all workspaces"),h=[...s.workspaces]),this.dryRun&&!this.all){for(let Ee of h)f(` - ${Ee.relativeCwd} ${j.prettyLocator(r,Ee.anchoredLocator)}`);h.length>0&&f("")}let E;if(this.recursive?this.since?(f("Option --recursive --since is set; recursively selecting all dependent workspaces"),E=new Set(h.map(Ee=>[...Ee.getRecursiveWorkspaceDependents()]).flat())):(f("Option --recursive is set; recursively selecting all transitive dependencies"),E=new Set(h.map(Ee=>[...Ee.getRecursiveWorkspaceDependencies()]).flat())):this.worktree?(f("Option --worktree is set; recursively selecting all nested workspaces"),E=new Set(h.map(Ee=>[...Ee.getRecursiveWorkspaceChildren()]).flat())):E=null,E!==null&&(h=[...new Set([...h,...E])],this.dryRun))for(let Ee of E)f(` - ${Ee.relativeCwd} ${j.prettyLocator(r,Ee.anchoredLocator)}`);let C=[],S=!1;if(c?.includes(":")){for(let Ee of s.workspaces)if(Ee.manifest.scripts.has(c)&&(S=!S,S===!1))break}for(let Ee of h){if(c&&!Ee.manifest.scripts.has(c)&&!S&&!(await Cn.getWorkspaceAccessibleBinaries(Ee)).has(c)){f(`Excluding ${Ee.relativeCwd} because it doesn't have a "${c}" script`);continue}if(!(c===r.env.npm_lifecycle_event&&Ee.cwd===a.cwd)){if(this.include.length>0&&!j1.default.isMatch(j.stringifyIdent(Ee.anchoredLocator),this.include)&&!j1.default.isMatch(Ee.relativeCwd,this.include)){f(`Excluding ${Ee.relativeCwd} because it doesn't match the --include filter`);continue}if(this.exclude.length>0&&(j1.default.isMatch(j.stringifyIdent(Ee.anchoredLocator),this.exclude)||j1.default.isMatch(Ee.relativeCwd,this.exclude))){f(`Excluding ${Ee.relativeCwd} because it matches the --exclude filter`);continue}if(this.publicOnly&&Ee.manifest.private===!0){f(`Excluding ${Ee.relativeCwd} because it's a private workspace and --no-private was set`);continue}C.push(Ee)}}if(C.sort((Ee,d)=>j.stringifyIdent(Ee.anchoredLocator).localeCompare(j.stringifyIdent(d.anchoredLocator))),this.dryRun)return 0;let x=this.verbose??(this.context.stdout.isTTY?1/0:0),I=x>0,T=x>1,O=this.parallel?this.jobs==="unlimited"?1/0:Number(this.jobs)||Math.ceil(Ui.availableParallelism()/2):1,U=O===1?!1:this.parallel,V=U?this.interlaced:!0,te=(0,rPe.default)(O),ie=new Map,ue=new Set,ae=0,ge=null,Ae=!1,Ce=await Ot.start({configuration:r,stdout:this.context.stdout,includePrefix:!1},async Ee=>{let d=async(Se,{commandIndex:Be})=>{if(Ae)return-1;!U&&T&&Be>1&&Ee.reportSeparator();let me=TSt(Se,{configuration:r,label:I,commandIndex:Be}),[ce,Z]=tPe(Ee,{prefix:me,interlaced:V}),[De,Qe]=tPe(Ee,{prefix:me,interlaced:V});try{T&&Ee.reportInfo(null,`${me?`${me} `:""}Process started`);let st=Date.now(),_=await this.cli.run([this.commandName,...this.args],{cwd:Se.cwd,stdout:ce,stderr:De})||0;ce.end(),De.end(),await Z,await Qe;let tt=Date.now();if(T){let Ne=r.get("enableTimers")?`, completed in ${pe.pretty(r,tt-st,pe.Type.DURATION)}`:"";Ee.reportInfo(null,`${me?`${me} `:""}Process exited (exit code ${_})${Ne}`)}return _===130&&(Ae=!0,ge=_),_}catch(st){throw ce.end(),De.end(),await Z,await Qe,st}};for(let Se of C)ie.set(Se.anchoredLocator.locatorHash,Se);for(;ie.size>0&&!Ee.hasErrors();){let Se=[];for(let[Z,De]of ie){if(ue.has(De.anchoredDescriptor.descriptorHash))continue;let Qe=!0;if(this.topological||this.topologicalDev){let st=this.topologicalDev?new Map([...De.manifest.dependencies,...De.manifest.devDependencies]):De.manifest.dependencies;for(let _ of st.values()){let tt=s.tryWorkspaceByDescriptor(_);if(Qe=tt===null||!ie.has(tt.anchoredLocator.locatorHash),!Qe)break}}if(Qe&&(ue.add(De.anchoredDescriptor.descriptorHash),Se.push(te(async()=>{let st=await d(De,{commandIndex:++ae});return ie.delete(Z),ue.delete(De.anchoredDescriptor.descriptorHash),{workspace:De,exitCode:st}})),!U))break}if(Se.length===0){let Z=Array.from(ie.values()).map(De=>j.prettyLocator(r,De.anchoredLocator)).join(", ");Ee.reportError(3,`Dependency cycle detected (${Z})`);return}let Be=await Promise.all(Se);Be.forEach(({workspace:Z,exitCode:De})=>{De!==0&&Ee.reportError(0,`The command failed in workspace ${j.prettyLocator(r,Z.anchoredLocator)} with exit code ${De}`)});let ce=Be.map(Z=>Z.exitCode).find(Z=>Z!==0);(this.topological||this.topologicalDev)&&typeof ce<"u"&&Ee.reportError(0,"The command failed for workspaces that are depended upon by other workspaces; can't satisfy the dependency graph")}});return ge!==null?ge:Ce.exitCode()}};function tPe(e,{prefix:t,interlaced:r}){let s=e.createStreamReporter(t),a=new Ge.DefaultStream;a.pipe(s,{end:!1}),a.on("finish",()=>{s.end()});let n=new Promise(f=>{s.on("finish",()=>{f(a.active)})});if(r)return[a,n];let c=new Ge.BufferStream;return c.pipe(a,{end:!1}),c.on("finish",()=>{a.end()}),[c,n]}function TSt(e,{configuration:t,commandIndex:r,label:s}){if(!s)return null;let n=`[${j.stringifyIdent(e.anchoredLocator)}]:`,c=["#2E86AB","#A23B72","#F18F01","#C73E1D","#CCE2A3"],f=c[r%c.length];return pe.pretty(t,n,f)}var FSt={commands:[H1,G1]},NSt=FSt;var YI=()=>({modules:new Map([["@yarnpkg/cli",Gv],["@yarnpkg/core",jv],["@yarnpkg/fslib",R2],["@yarnpkg/libzip",nv],["@yarnpkg/parsers",_2],["@yarnpkg/shell",cv],["clipanion",Z2],["semver",OSt],["typanion",Jo],["@yarnpkg/plugin-essentials",T5],["@yarnpkg/plugin-catalog",L5],["@yarnpkg/plugin-compat",j5],["@yarnpkg/plugin-constraints",s9],["@yarnpkg/plugin-dlx",o9],["@yarnpkg/plugin-exec",c9],["@yarnpkg/plugin-file",f9],["@yarnpkg/plugin-git",R5],["@yarnpkg/plugin-github",h9],["@yarnpkg/plugin-http",d9],["@yarnpkg/plugin-init",g9],["@yarnpkg/plugin-interactive-tools",pW],["@yarnpkg/plugin-jsr",dW],["@yarnpkg/plugin-link",gW],["@yarnpkg/plugin-nm",eY],["@yarnpkg/plugin-npm",eK],["@yarnpkg/plugin-npm-cli",uK],["@yarnpkg/plugin-pack",VY],["@yarnpkg/plugin-patch",mK],["@yarnpkg/plugin-pnp",jW],["@yarnpkg/plugin-pnpm",IK],["@yarnpkg/plugin-stage",bK],["@yarnpkg/plugin-typescript",PK],["@yarnpkg/plugin-version",TK],["@yarnpkg/plugin-workspace-tools",FK]]),plugins:new Set(["@yarnpkg/plugin-essentials","@yarnpkg/plugin-catalog","@yarnpkg/plugin-compat","@yarnpkg/plugin-constraints","@yarnpkg/plugin-dlx","@yarnpkg/plugin-exec","@yarnpkg/plugin-file","@yarnpkg/plugin-git","@yarnpkg/plugin-github","@yarnpkg/plugin-http","@yarnpkg/plugin-init","@yarnpkg/plugin-interactive-tools","@yarnpkg/plugin-jsr","@yarnpkg/plugin-link","@yarnpkg/plugin-nm","@yarnpkg/plugin-npm","@yarnpkg/plugin-npm-cli","@yarnpkg/plugin-pack","@yarnpkg/plugin-patch","@yarnpkg/plugin-pnp","@yarnpkg/plugin-pnpm","@yarnpkg/plugin-stage","@yarnpkg/plugin-typescript","@yarnpkg/plugin-version","@yarnpkg/plugin-workspace-tools"])});function sPe({cwd:e,pluginConfiguration:t}){let r=new Sa({binaryLabel:"Yarn Package Manager",binaryName:"yarn",binaryVersion:An??""});return Object.assign(r,{defaultContext:{...Sa.defaultContext,cwd:e,plugins:t,quiet:!1,stdin:process.stdin,stdout:process.stdout,stderr:process.stderr}})}function LSt(e){if(Ge.parseOptionalBoolean(process.env.YARN_IGNORE_NODE))return!0;let r=process.versions.node,s=">=18.12.0";if(kr.satisfiesWithPrereleases(r,s))return!0;let a=new it(`This tool requires a Node version compatible with ${s} (got ${r}). Upgrade Node, or set \`YARN_IGNORE_NODE=1\` in your environment.`);return Sa.defaultContext.stdout.write(e.error(a)),!1}async function oPe({selfPath:e,pluginConfiguration:t}){return await ze.find(fe.toPortablePath(process.cwd()),t,{strict:!1,usePathCheck:e})}function MSt(e,t,{yarnPath:r}){if(!le.existsSync(r))return e.error(new Error(`The "yarn-path" option has been set, but the specified location doesn't exist (${r}).`)),1;process.on("SIGINT",()=>{});let s={stdio:"inherit",env:{...process.env,YARN_IGNORE_PATH:"1"}};try{(0,nPe.execFileSync)(process.execPath,[fe.fromPortablePath(r),...t],s)}catch(a){return a.status??1}return 0}function USt(e,t){let r=null,s=t;return t.length>=2&&t[0]==="--cwd"?(r=fe.toPortablePath(t[1]),s=t.slice(2)):t.length>=1&&t[0].startsWith("--cwd=")?(r=fe.toPortablePath(t[0].slice(6)),s=t.slice(1)):t[0]==="add"&&t[t.length-2]==="--cwd"&&(r=fe.toPortablePath(t[t.length-1]),s=t.slice(0,t.length-2)),e.defaultContext.cwd=r!==null?J.resolve(r):J.cwd(),s}function _St(e,{configuration:t}){if(!t.get("enableTelemetry")||iPe.isCI||!process.stdout.isTTY)return;ze.telemetry=new GI(t,"puba9cdc10ec5790a2cf4969dd413a47270");let s=/^@yarnpkg\/plugin-(.*)$/;for(let a of t.plugins.keys())qI.has(a.match(s)?.[1]??"")&&ze.telemetry?.reportPluginName(a);e.binaryVersion&&ze.telemetry.reportVersion(e.binaryVersion)}function aPe(e,{configuration:t}){for(let r of t.plugins.values())for(let s of r.commands||[])e.register(s)}async function HSt(e,t,{selfPath:r,pluginConfiguration:s}){if(!LSt(e))return 1;let a=await oPe({selfPath:r,pluginConfiguration:s}),n=a.get("yarnPath"),c=a.get("ignorePath");if(n&&!c)return MSt(e,t,{yarnPath:n});delete process.env.YARN_IGNORE_PATH;let f=USt(e,t);_St(e,{configuration:a}),aPe(e,{configuration:a});let p=e.process(f,e.defaultContext);return p.help||ze.telemetry?.reportCommandName(p.path.join(" ")),await e.run(p,e.defaultContext)}async function g0e({cwd:e=J.cwd(),pluginConfiguration:t=YI()}={}){let r=sPe({cwd:e,pluginConfiguration:t}),s=await oPe({pluginConfiguration:t,selfPath:null});return aPe(r,{configuration:s}),r}async function QT(e,{cwd:t=J.cwd(),selfPath:r,pluginConfiguration:s}){let a=sPe({cwd:t,pluginConfiguration:s});function n(){Sa.defaultContext.stdout.write(`ERROR: Yarn is terminating due to an unexpected empty event loop. Please report this issue at https://github.com/yarnpkg/berry/issues.`)}process.once("beforeExit",n);try{process.exitCode=42,process.exitCode=await HSt(a,e,{selfPath:r,pluginConfiguration:s})}catch(c){Sa.defaultContext.stdout.write(a.error(c)),process.exitCode=1}finally{process.off("beforeExit",n),await le.rmtempPromise()}}QT(process.argv.slice(2),{cwd:J.cwd(),selfPath:fe.toPortablePath(fe.resolve(process.argv[1])),pluginConfiguration:YI()});})(); /** @license Copyright (c) 2015, Rebecca Turner Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ /** @license Copyright Node.js contributors. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /** @license The MIT License (MIT) Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /** @license Copyright Joyent, Inc. and other Node contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*! Bundled license information: is-number/index.js: (*! * is-number * * Copyright (c) 2014-present, Jon Schlinkert. * Released under the MIT License. *) to-regex-range/index.js: (*! * to-regex-range * * Copyright (c) 2015-present, Jon Schlinkert. * Released under the MIT License. *) fill-range/index.js: (*! * fill-range * * Copyright (c) 2014-present, Jon Schlinkert. * Licensed under the MIT License. *) is-extglob/index.js: (*! * is-extglob * * Copyright (c) 2014-2016, Jon Schlinkert. * Licensed under the MIT License. *) is-glob/index.js: (*! * is-glob * * Copyright (c) 2014-2017, Jon Schlinkert. * Released under the MIT License. *) queue-microtask/index.js: (*! queue-microtask. MIT License. Feross Aboukhadijeh *) run-parallel/index.js: (*! run-parallel. MIT License. Feross Aboukhadijeh *) git-url-parse/lib/index.js: (*! * buildToken * Builds OAuth token prefix (helper function) * * @name buildToken * @function * @param {GitUrl} obj The parsed Git url object. * @return {String} token prefix *) object-assign/index.js: (* object-assign (c) Sindre Sorhus @license MIT *) react/cjs/react.production.min.js: (** @license React v17.0.2 * react.production.min.js * * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. *) scheduler/cjs/scheduler.production.min.js: (** @license React v0.20.2 * scheduler.production.min.js * * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. *) react-reconciler/cjs/react-reconciler.production.min.js: (** @license React v0.26.2 * react-reconciler.production.min.js * * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. *) is-windows/index.js: (*! * is-windows * * Copyright © 2015-2018, Jon Schlinkert. * Released under the MIT License. *) */ ================================================ FILE: frontend/.yarnrc.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. yarnPath: .yarn/releases/yarn-4.14.1.cjs nodeLinker: node-modules nmMode: hardlinks-local ================================================ FILE: frontend/LICENSE-binary ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Support. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or support. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================================ THIRD-PARTY COMPONENTS ================================================================================ Apache Texera's binary distribution of the texera-dashboard-service Docker image bundles the following third-party Angular / npm packages, sourced from frontend/node_modules at build time and shipped compiled into the image's frontend assets, grouped by license. -------------------------------------------------------------------------------- Dependencies under the Apache License, Version 2.0 -------------------------------------------------------------------------------- Angular / npm packages: - fuse.js@6.5.3 - jschardet@3.1.3 - rxjs@7.8.1 -------------------------------------------------------------------------------- Dependencies under the MIT License -------------------------------------------------------------------------------- Source files derived from third-party MIT-licensed projects: - Google Angular formly examples frontend/src/app/common/formly/array.type.ts frontend/src/app/common/formly/object.type.ts frontend/src/app/common/formly/multischema.type.ts frontend/src/app/common/formly/null.type.ts https://angular.io - SVGRepo icons frontend/src/assets/svg/operator-view-result.svg frontend/src/assets/svg/operator-reuse-cache-valid.svg frontend/src/assets/svg/operator-reuse-cache-invalid.svg https://www.svgrepo.com Angular / npm packages: - @abacritt/angularx-social-login@2.3.0 - @ali-hm/angular-tree-component@12.0.5 - @angular/animations@21.2.10 - @angular/cdk@21.2.8 - @angular/common@21.2.10 - @angular/core@21.2.10 - @angular/forms@21.2.10 - @angular/platform-browser@21.2.10 - @angular/router@21.2.10 - @ant-design/colors@7.2.1 - @ant-design/fast-color@2.0.6 - @ant-design/icons-angular@21.0.0 - @auth0/angular-jwt@5.1.0 - @babel/runtime@7.29.2 - @codingame/monaco-vscode-api@8.0.4 - @codingame/monaco-vscode-base-service-override@8.0.4 - @codingame/monaco-vscode-configuration-service-override@8.0.4 - @codingame/monaco-vscode-editor-api@8.0.4 - @codingame/monaco-vscode-environment-service-override@8.0.4 - @codingame/monaco-vscode-extensions-service-override@8.0.4 - @codingame/monaco-vscode-files-service-override@8.0.4 - @codingame/monaco-vscode-host-service-override@8.0.4 - @codingame/monaco-vscode-java-default-extension@8.0.4 - @codingame/monaco-vscode-languages-service-override@8.0.4 - @codingame/monaco-vscode-layout-service-override@8.0.4 - @codingame/monaco-vscode-model-service-override@8.0.4 - @codingame/monaco-vscode-monarch-service-override@8.0.4 - @codingame/monaco-vscode-python-default-extension@8.0.4 - @codingame/monaco-vscode-quickaccess-service-override@8.0.4 - @codingame/monaco-vscode-r-default-extension@8.0.4 - @codingame/monaco-vscode-textmate-service-override@8.0.4 - @codingame/monaco-vscode-theme-defaults-default-extension@8.0.4 - @codingame/monaco-vscode-theme-service-override@8.0.4 - @ctrl/tinycolor@3.6.1 - @ngneat/until-destroy@8.1.4 - @ngx-formly/core@6.3.12 - @ngx-formly/ng-zorro-antd@6.3.12 - @vscode/iconv-lite-umd@0.7.0 - ajv@8.10.0 - backbone@1.4.1 - balanced-match@1.0.2 - brace-expansion@2.1.0 - css-loader@6.11.0 - dagre@0.8.5 - date-fns@2.30.0 - fast-deep-equal@3.1.3 - fflate@0.7.4 - file-saver@2.0.5 - graphlib@2.1.8 - html2canvas@1.4.1 - java@1.0.0 - jquery@3.6.4 - json-schema-traverse@1.0.0 - jszip@3.10.1 - lib0@0.2.117 - lodash@4.18.1 - lodash-es@4.18.1 - marked@17.0.1 - mobx@4.14.1 - monaco-breakpoints@0.2.0 - monaco-editor-wrapper@5.5.3 - monaco-languageclient@8.8.3 - ng-zorro-antd@21.2.2 - ngx-color-picker@12.0.1 - ngx-file-drop@16.0.0 - ngx-json-viewer@3.2.1 - ngx-markdown@21.2.0 - papaparse@5.4.1 - plotly.js-basic-dist-min@2.29.0 - point-in-polygon@1.1.0 - python@1.0.0 - quill-cursors@3.1.2 - r@1.0.0 - rbush@4.0.1 - read-excel-file@5.7.1 - ring-buffer-ts@1.0.3 - style-loader@3.3.4 - theme-defaults@1.0.0 - underscore@1.13.8 - uuid@8.3.2 - vscode-jsonrpc@8.2.0 - vscode-languageclient@9.0.1 - vscode-languageserver-protocol@3.17.5 - vscode-languageserver-types@3.17.5 - vscode-oniguruma@1.7.0 - vscode-textmate@9.0.0 - vscode-ws-jsonrpc@3.3.2 - y-monaco@0.1.5 - y-protocols@1.0.7 - y-quill@0.1.5 - y-websocket@1.5.4 - yjs@13.5.41 - zone.js@0.15.1 -------------------------------------------------------------------------------- Dependencies under the BSD 3-Clause License -------------------------------------------------------------------------------- Angular / npm packages: - d3-shape@2.1.0 - quill@1.3.7 -------------------------------------------------------------------------------- Dependencies under the BSD 2-Clause License -------------------------------------------------------------------------------- Angular / npm packages: - uri-js@4.4.1 -------------------------------------------------------------------------------- Dependencies under the ISC License -------------------------------------------------------------------------------- Angular / npm packages: - concaveman@2.0.0 - d3-path@2.0.0 - minimatch@5.1.9 - quickselect@3.0.0 - tinyqueue@2.0.3 -------------------------------------------------------------------------------- Dependencies under the Mozilla Public License, Version 2.0 -------------------------------------------------------------------------------- Angular / npm packages: - jointjs@3.5.4 -------------------------------------------------------------------------------- Dependencies under the Unlicense -------------------------------------------------------------------------------- Angular / npm packages: - robust-predicates@3.0.3 Individual jars may contain their own META-INF/LICENSE and META-INF/NOTICE files that apply to their specific contents; those files continue to govern the use of those components. ================================================ FILE: frontend/README.md ================================================ # NewGui This project was generated with [Angular CLI](https://github.com/angular/angular-cli) ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). ================================================ FILE: frontend/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "projects": { "gui": { "root": "", "sourceRoot": "src", "projectType": "application", "architect": { "build": { "builder": "@angular-builders/custom-webpack:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "tsConfig": "src/tsconfig.app.json", "polyfills": ["zone.js"], "assets": [ "src/assets", { "glob": "**/*", "input": "./node_modules/@ant-design/icons-angular/src/inline-svg/", "output": "/assets/" } ], "styles": [ "node_modules/jointjs/css/layout.css", "node_modules/jointjs/css/themes/material.css", "node_modules/jointjs/css/themes/default.css", "node_modules/ng-zorro-antd/ng-zorro-antd.min.css", "node_modules/ng-zorro-antd/resizable/style/index.min.css", "src/styles.scss" ], "scripts": ["./node_modules/marked/lib/marked.umd.js"], "customWebpackConfig": { "path": "./custom-webpack.config.js" }, "allowedCommonJsDependencies": ["dagre", "graphlib"], "vendorChunk": true, "extractLicenses": false, "buildOptimizer": false, "sourceMap": true, "optimization": false, "namedChunks": true }, "configurations": { "production": { "budgets": [ { "type": "anyComponentStyle", "maximumWarning": "256kb" } ], "optimization": true, "outputHashing": "all", "sourceMap": false, "namedChunks": false, "extractLicenses": true, "vendorChunk": false, "buildOptimizer": true, "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ] }, "test": { "tsConfig": "src/tsconfig.test.json", "main": "src/main.test.ts" } }, "defaultConfiguration": "" }, "serve": { "builder": "@angular-builders/custom-webpack:dev-server", "options": { "buildTarget": "gui:build", "proxyConfig": "proxy.config.json" }, "configurations": { "production": { "buildTarget": "gui:build:production" } } }, "test": { "builder": "@angular/build:unit-test", "options": { "buildTarget": "gui:build:test", "runner": "vitest", "runnerConfig": "vitest.config.ts", "tsConfig": "src/tsconfig.spec.json", "include": ["**/*.spec.ts"], "setupFiles": ["src/jsdom-svg-polyfill.ts"], "exclude": [ "**/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts" ] } }, "test-browser": { "builder": "@angular/build:unit-test", "options": { "buildTarget": "gui:build:test", "runner": "vitest", "runnerConfig": "vitest.browser.config.ts", "tsConfig": "src/tsconfig.spec.json", "include": ["**/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts"] } } } } }, "cli": { "analytics": false }, "schematics": { "@schematics/angular:component": { "type": "component" }, "@schematics/angular:directive": { "type": "directive" }, "@schematics/angular:service": { "type": "service" }, "@schematics/angular:guard": { "typeSeparator": "." }, "@schematics/angular:interceptor": { "typeSeparator": "." }, "@schematics/angular:module": { "typeSeparator": "." }, "@schematics/angular:pipe": { "typeSeparator": "." }, "@schematics/angular:resolver": { "typeSeparator": "." } } } ================================================ FILE: frontend/custom-webpack.config.js ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ const { LicenseWebpackPlugin } = require("license-webpack-plugin"); module.exports = { module: { rules: [ { test: /\.css$/, use: ["style-loader", "css-loader"], include: [ require("path").resolve(__dirname, "node_modules/monaco-editor"), require("path").resolve(__dirname, "node_modules/monaco-breakpoints") ], }, ], // Enable URL handling in webpack's JavaScript parser, required for loading .wasm files. // See https://github.com/angular/angular-cli/issues/24617 parser: { javascript: { url: true, }, }, }, plugins: [ new LicenseWebpackPlugin({ perChunkOutput: false, outputFilename: "3rdpartylicenses.json", renderLicenses: (modules) => JSON.stringify( modules .map((m) => ({ name: m.packageJson && m.packageJson.name, version: m.packageJson && m.packageJson.version, license: m.licenseId, })) .filter((e) => e.name && e.version) .sort((a, b) => a.name === b.name ? a.version.localeCompare(b.version) : a.name.localeCompare(b.name), ), null, 2, ), }), ], }; ================================================ FILE: frontend/git-version.js ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ const { gitDescribeSync } = require("git-describe"); const { version } = require("./package.json"); const { resolve, relative } = require("path"); const { writeFileSync, existsSync, mkdirSync } = require("fs-extra"); const gitInfo = gitDescribeSync({ dirtyMark: false, dirtySemver: false, }); gitInfo.version = version; if (!existsSync(__dirname + "/src/environments")) { mkdirSync(__dirname + "/src/environments"); } const file = resolve(__dirname, "src", "environments", "version.ts"); writeFileSync( file, `// IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN! /* tslint:disable */ export const Version = ${JSON.stringify(gitInfo, null, 4)}; /* tslint:enable */ `, { encoding: "utf-8" } ); console.log(`Wrote version info ${gitInfo.raw} to ${relative(resolve(__dirname, ".."), file)}`); ================================================ FILE: frontend/nx.json ================================================ { "tasksRunnerOptions": { "default": { "options": { "runtimeCacheInputs": ["node -v", "yarn run nodecat ./src/environments/environment.prod.ts"] } } }, "cli": { "analytics": false }, "generators": { "@schematics/angular:component": { "prefix": "texera", "style": "scss" }, "@schematics/angular:directive": { "prefix": "texera" } }, "namedInputs": { "default": ["{projectRoot}/**/*", "sharedGlobals"], "sharedGlobals": [], "production": ["default"] }, "targetDefaults": { "build": { "inputs": ["production", "^production"], "cache": true } }, "parallel": 1, "useLegacyCache": true } ================================================ FILE: frontend/package.json ================================================ { "name": "gui", "version": "0.0.0", "engines": { "node": ">=24.0.0" }, "license": "Apache-2.0", "scripts": { "start": "concurrently --kill-others \"npx y-websocket\" \"ng serve\"", "build": "node --max-old-space-size=8192 ./node_modules/@angular/cli/bin/ng build --configuration=production --progress=false --source-map=false", "build:ci": "node --max-old-space-size=8192 ./node_modules/nx/dist/bin/nx.js build --configuration=production --progress=false --source-map=false", "analyze": "ng build --configuration=production --stats-json && webpack-bundle-analyzer dist/stats.json", "test": "ng test --watch=false", "test:ci": "node --max-old-space-size=8192 ./node_modules/nx/dist/bin/nx.js test --watch=false --progress=false --coverage --coverage-reporters=lcovonly", "prettier:fix": "prettier --write ./src", "lint": "eslint ./src", "eslint:fix": "yarn eslint --fix ./src", "format:fix": "yarn prettier-eslint --write \"src/**/*.{ts,js,html,scss,less,json}\"", "format:ci": "yarn prettier-eslint --list-different \"src/**/*.{ts,js,html,scss,less,json}\" && yarn eslint ./src", "postinstall": "node git-version.js" }, "private": true, "dependencies": { "@abacritt/angularx-social-login": "2.3.0", "@ai-sdk/openai": "2.0.67", "@ali-hm/angular-tree-component": "12.0.5", "@angular/animations": "21.2.10", "@angular/cdk": "21.2.8", "@angular/common": "21.2.10", "@angular/compiler": "21.2.10", "@angular/core": "21.2.10", "@angular/forms": "21.2.10", "@angular/localize": "21.2.10", "@angular/platform-browser": "21.2.10", "@angular/platform-browser-dynamic": "21.2.10", "@angular/router": "21.2.10", "@auth0/angular-jwt": "5.1.0", "@codingame/monaco-vscode-java-default-extension": "8.0.4", "@codingame/monaco-vscode-python-default-extension": "8.0.4", "@codingame/monaco-vscode-r-default-extension": "8.0.4", "@ngneat/until-destroy": "8.1.4", "@ngx-formly/core": "6.3.12", "@ngx-formly/ng-zorro-antd": "6.3.12", "ai": "5.0.93", "ajv": "8.10.0", "concaveman": "2.0.0", "d3-shape": "2.1.0", "dagre": "0.8.5", "file-saver": "2.0.5", "fuse.js": "6.5.3", "html2canvas": "1.4.1", "jointjs": "3.5.4", "jszip": "3.10.1", "lodash-es": "4.18.1", "marked": "17.0.1", "monaco-breakpoints": "0.2.0", "monaco-editor": "npm:@codingame/monaco-vscode-editor-api@8.0.4", "monaco-editor-wrapper": "5.5.3", "monaco-languageclient": "8.8.3", "ng-zorro-antd": "21.2.2", "ngx-color-picker": "12.0.1", "ngx-file-drop": "16.0.0", "ngx-json-viewer": "3.2.1", "ngx-markdown": "21.2.0", "papaparse": "5.4.1", "path-browserify": "1.0.1", "plotly.js-basic-dist-min": "2.29.0", "quill": "1.3.7", "quill-cursors": "3.1.2", "read-excel-file": "5.7.1", "ring-buffer-ts": "1.0.3", "rxjs": "7.8.1", "tinyqueue": "2.0.3", "tslib": "2.3.1", "uuid": "8.3.2", "vscode": "npm:@codingame/monaco-vscode-api@8.0.4", "y-monaco": "0.1.5", "y-protocols": "1.0.5", "y-quill": "0.1.5", "y-websocket": "1.5.4", "yjs": "13.5.41", "zod": "3.25.76", "zone.js": "0.15.1" }, "resolutions": { "vscode": "npm:@codingame/monaco-vscode-api@8.0.4", "monaco-editor": "npm:@codingame/monaco-vscode-editor-api@8.0.4", "webpack": "5.104.1", "jschardet": "portal:./tools/jschardet-stub" }, "devDependencies": { "@angular-builders/custom-webpack": "21.0.3", "@angular-devkit/build-angular": "21.2.8", "@angular-devkit/core": "21.2.8", "@angular-devkit/schematics": "21.2.8", "@angular-eslint/eslint-plugin": "21.3.1", "@angular-eslint/eslint-plugin-template": "21.3.1", "@angular-eslint/template-parser": "21.3.1", "@angular/cli": "21.2.8", "@angular/compiler-cli": "21.2.10", "@nx/angular": "22.7.0", "@schematics/angular": "21.2.8", "@types/backbone": "1.4.15", "@types/concaveman": "1.1.6", "@types/d3-shape": "2.1.2", "@types/dagre": "0.7.47", "@types/file-saver": "2.0.5", "@types/graphlib": "2.1.8", "@types/json-schema": "7.0.9", "@types/lodash": "4.14.179", "@types/lodash-es": "4.17.4", "@types/node": "24.10.1", "@types/papaparse": "5.3.5", "@types/plotly.js-basic-dist-min": "2.12.4", "@types/quill": "2.0.9", "@types/uuid": "8.3.4", "@typescript-eslint/eslint-plugin": "8.59.0", "@typescript-eslint/parser": "8.59.0", "@typescript-eslint/types": "8.59.0", "@typescript-eslint/utils": "8.59.0", "@vitest/browser": "4.1.5", "@vitest/browser-playwright": "4.1.5", "@vitest/coverage-v8": "4.1.5", "concurrently": "7.4.0", "eslint": "8.57.0", "eslint-plugin-rxjs": "5.0.3", "eslint-plugin-rxjs-angular": "2.0.1", "fs-extra": "10.0.1", "git-describe": "4.1.0", "jsdom": "25.0.1", "nodecat": "2.0.0", "nx": "22.7.0", "playwright": "1.59.1", "prettier": "3.2.5", "prettier-eslint-cli": "8.0.1", "rxjs-marbles": "7.0.1", "sass": "1.71.1", "ts-proto": "2.2.0", "typescript": "5.9.3", "vitest": "4.1.5", "webpack-bundle-analyzer": "4.5.0" }, "browserslist": [ "defaults", "not ie <= 11" ], "packageManager": "yarn@4.14.1" } ================================================ FILE: frontend/proxy.config.json ================================================ { "/api/agents/*/react": { "target": "http://localhost:3001", "secure": false, "changeOrigin": true, "ws": true }, "/api/agents": { "target": "http://localhost:3001", "secure": false, "changeOrigin": true }, "/api/models": { "target": "http://localhost:9096", "secure": false, "changeOrigin": true }, "/api/chat/completion": { "target": "http://localhost:9096", "secure": false, "changeOrigin": true }, "/api/compile": { "target": "http://localhost:9090", "secure": false, "changeOrigin": true }, "/api/dataset": { "target": "http://localhost:9092", "secure": false, "changeOrigin": true }, "/api/access/dataset/**": { "target": "http://localhost:9092", "secure": false, "changeOrigin": true }, "/api/access/computing-unit/**": { "target": "http://localhost:8888", "secure": false, "changeOrigin": true }, "/api/config/**": { "target": "http://localhost:9094", "secure": false, "changeOrigin": true }, "/api/computing-unit": { "target": "http://localhost:8888", "secure": false, "changeOrigin": true }, "/api": { "target": "http://localhost:8080", "secure": false, "changeOrigin": false }, "/pve": { "target": "http://localhost:8085", "secure": false, "changeOrigin": false, "pathRewrite": { "^/pve": "/api/pve" } }, "/wsapi": { "target": "http://localhost:8085", "secure": false, "changeOrigin": false, "ws": true }, "/rtc": { "target": "http://localhost:1234", "ws": true, "secure": false, "changeOrigin": false } } ================================================ FILE: frontend/src/app/app-routing.constant.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ export const DASHBOARD = "/dashboard"; export const DASHBOARD_HOME = `${DASHBOARD}/home`; export const DASHBOARD_ABOUT = `${DASHBOARD}/about`; export const DASHBOARD_HUB = `${DASHBOARD}/hub`; export const DASHBOARD_HUB_WORKFLOW = `${DASHBOARD_HUB}/workflow`; export const DASHBOARD_HUB_WORKFLOW_RESULT = `${DASHBOARD_HUB_WORKFLOW}/result`; export const DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL = `${DASHBOARD_HUB_WORKFLOW_RESULT}/detail`; export const DASHBOARD_HUB_DATASET = `${DASHBOARD_HUB}/dataset`; export const DASHBOARD_HUB_DATASET_RESULT = `${DASHBOARD_HUB_DATASET}/result`; export const DASHBOARD_HUB_DATASET_RESULT_DETAIL = `${DASHBOARD_HUB_DATASET_RESULT}/detail`; export const DASHBOARD_USER = `${DASHBOARD}/user`; export const DASHBOARD_USER_PROJECT = `${DASHBOARD_USER}/project`; export const DASHBOARD_USER_WORKSPACE = `${DASHBOARD_USER}/workflow`; export const DASHBOARD_USER_WORKFLOW = `${DASHBOARD_USER}/workflow`; export const DASHBOARD_USER_DATASET = `${DASHBOARD_USER}/dataset`; export const DASHBOARD_USER_DATASET_CREATE = `${DASHBOARD_USER_DATASET}/create`; export const DASHBOARD_USER_COMPUTING_UNIT = `${DASHBOARD_USER}/compute`; export const DASHBOARD_USER_QUOTA = `${DASHBOARD_USER}/quota`; export const DASHBOARD_USER_DISCUSSION = `${DASHBOARD_USER}/discussion`; export const DASHBOARD_ADMIN = `${DASHBOARD}/admin`; export const DASHBOARD_ADMIN_USER = `${DASHBOARD_ADMIN}/user`; export const DASHBOARD_ADMIN_GMAIL = `${DASHBOARD_ADMIN}/gmail`; export const DASHBOARD_ADMIN_EXECUTION = `${DASHBOARD_ADMIN}/execution`; export const DASHBOARD_ADMIN_SETTINGS = `${DASHBOARD_ADMIN}/settings`; export const DASHBOARD_SEARCH = `${DASHBOARD}/search`; ================================================ FILE: frontend/src/app/app-routing.module.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { inject, NgModule } from "@angular/core"; import { CanActivateFn, Router, RouterModule, Routes } from "@angular/router"; import { DashboardComponent } from "./dashboard/component/dashboard.component"; import { UserWorkflowComponent } from "./dashboard/component/user/user-workflow/user-workflow.component"; import { UserQuotaComponent } from "./dashboard/component/user/user-quota/user-quota.component"; import { UserProjectSectionComponent } from "./dashboard/component/user/user-project/user-project-section/user-project-section.component"; import { UserProjectComponent } from "./dashboard/component/user/user-project/user-project.component"; import { UserComputingUnitComponent } from "./dashboard/component/user/user-computing-unit/user-computing-unit.component"; import { WorkspaceComponent } from "./workspace/component/workspace.component"; import { AboutComponent } from "./hub/component/about/about.component"; import { AuthGuardService } from "./common/service/user/auth-guard.service"; import { AdminUserComponent } from "./dashboard/component/admin/user/admin-user.component"; import { AdminExecutionComponent } from "./dashboard/component/admin/execution/admin-execution.component"; import { AdminGuardService } from "./dashboard/service/admin/guard/admin-guard.service"; import { SearchComponent } from "./dashboard/component/user/search/search.component"; import { FlarumComponent } from "./dashboard/component/user/flarum/flarum.component"; import { AdminGmailComponent } from "./dashboard/component/admin/gmail/admin-gmail.component"; import { DatasetDetailComponent } from "./dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component"; import { UserDatasetComponent } from "./dashboard/component/user/user-dataset/user-dataset.component"; import { HubWorkflowDetailComponent } from "./hub/component/workflow/detail/hub-workflow-detail.component"; import { LandingPageComponent } from "./hub/component/landing-page/landing-page.component"; import { DASHBOARD_ABOUT, DASHBOARD_USER_WORKFLOW } from "./app-routing.constant"; import { HubSearchResultComponent } from "./hub/component/hub-search-result/hub-search-result.component"; import { AdminSettingsComponent } from "./dashboard/component/admin/settings/admin-settings.component"; import { GuiConfigService } from "./common/service/gui-config.service"; const rootRedirectGuard: CanActivateFn = () => { const config = inject(GuiConfigService); const router = inject(Router); try { return router.parseUrl(DASHBOARD_ABOUT); } catch { // config not loaded yet, swallow the error and let the app handle it } return true; }; const routes: Routes = []; routes.push({ path: "dashboard", component: DashboardComponent, children: [ { path: "home", component: LandingPageComponent, }, { path: "about", component: AboutComponent, }, { path: "hub", children: [ { path: "workflow", children: [ { path: "result", component: HubSearchResultComponent, }, { path: "result/detail/:id", component: HubWorkflowDetailComponent, }, ], }, { path: "dataset", children: [ { path: "result", component: HubSearchResultComponent, }, { path: "result/detail/:did", component: DatasetDetailComponent, }, ], }, ], }, { path: "user", canActivate: [AuthGuardService], children: [ { path: "project", component: UserProjectComponent, }, { path: "project/:pid", component: UserProjectSectionComponent, }, { path: "workflow", component: UserWorkflowComponent, }, { path: "workflow/:id", component: WorkspaceComponent, }, { path: "dataset", component: UserDatasetComponent, }, { path: "dataset/:did", component: DatasetDetailComponent, }, { path: "dataset/create", component: DatasetDetailComponent, }, { path: "compute", component: UserComputingUnitComponent, }, { path: "quota", component: UserQuotaComponent, }, { path: "discussion", component: FlarumComponent, }, ], }, { path: "admin", canActivate: [AdminGuardService], children: [ { path: "user", component: AdminUserComponent, }, { path: "gmail", component: AdminGmailComponent, }, { path: "execution", component: AdminExecutionComponent, }, { path: "settings", component: AdminSettingsComponent, }, ], }, { path: "search", component: SearchComponent, }, ], }); // default route renders the workspace editor directly; if userSystem is enabled at runtime, // AppComponent will navigate to DASHBOARD_ABOUT instead. routes.push({ path: "", component: WorkspaceComponent, canActivate: [rootRedirectGuard], }); // redirect all other paths to index. routes.push({ path: "**", redirectTo: DASHBOARD_USER_WORKFLOW, }); @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: frontend/src/app/app.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component } from "@angular/core"; import { GuiConfigService } from "./common/service/gui-config.service"; import { UntilDestroy } from "@ngneat/until-destroy"; @UntilDestroy() @Component({ selector: "texera-root", template: `

    Configuration Error

    Failed to load gui's configuration.

    Please ensure the ConfigService is running and accessible.

    `, standalone: false, }) export class AppComponent { configLoaded = false; constructor(private config: GuiConfigService) { // determine whether configuration was successfully loaded by APP_INITIALIZER try { // accessing env will throw if not loaded void this.config.env; this.configLoaded = true; } catch { this.configLoaded = false; } } retry(): void { window.location.reload(); } } ================================================ FILE: frontend/src/app/app.module.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { DatePipe, registerLocaleData } from "@angular/common"; import { HTTP_INTERCEPTORS, HttpClientModule } from "@angular/common/http"; import en from "@angular/common/locales/en"; import { APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA, NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { BrowserModule } from "@angular/platform-browser"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { RouterModule } from "@angular/router"; import { FormlyModule } from "@ngx-formly/core"; import { NzButtonModule } from "ng-zorro-antd/button"; import { NzCollapseModule } from "ng-zorro-antd/collapse"; import { NzDatePickerModule } from "ng-zorro-antd/date-picker"; import { NzDropDownModule } from "ng-zorro-antd/dropdown"; import { NzFormModule } from "ng-zorro-antd/form"; import { NzAutocompleteModule } from "ng-zorro-antd/auto-complete"; import { NzIconModule } from "ng-zorro-antd/icon"; import { NzInputModule } from "ng-zorro-antd/input"; import { NzPopoverModule } from "ng-zorro-antd/popover"; import { NzListModule } from "ng-zorro-antd/list"; import { NzTableModule } from "ng-zorro-antd/table"; import { NzTooltipModule } from "ng-zorro-antd/tooltip"; import { NzSelectModule } from "ng-zorro-antd/select"; import { NzSpaceModule } from "ng-zorro-antd/space"; import { NzBadgeModule } from "ng-zorro-antd/badge"; import { NzUploadModule } from "ng-zorro-antd/upload"; import { NgxJsonViewerModule } from "ngx-json-viewer"; import { ColorPickerModule } from "ngx-color-picker"; import { AppRoutingModule } from "./app-routing.module"; import { AppComponent } from "./app.component"; import { ArrayTypeComponent } from "./common/formly/array.type"; import { TEXERA_FORMLY_CONFIG } from "./common/formly/formly-config"; import { MultiSchemaTypeComponent } from "./common/formly/multischema.type"; import { NullTypeComponent } from "./common/formly/null.type"; import { ObjectTypeComponent } from "./common/formly/object.type"; import { UserService } from "./common/service/user/user.service"; import { GuiConfigService } from "./common/service/gui-config.service"; import { DashboardComponent } from "./dashboard/component/dashboard.component"; import { UserWorkflowComponent } from "./dashboard/component/user/user-workflow/user-workflow.component"; import { ShareAccessComponent } from "./dashboard/component/user/share-access/share-access.component"; import { WorkflowExecutionHistoryComponent } from "./dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-execution-history.component"; import { UserQuotaComponent } from "./dashboard/component/user/user-quota/user-quota.component"; import { UserIconComponent } from "./dashboard/component/user/user-icon/user-icon.component"; import { UserAvatarComponent } from "./dashboard/component/user/user-avatar/user-avatar.component"; import { CodeEditorComponent } from "./workspace/component/code-editor-dialog/code-editor.component"; import { AnnotationSuggestionComponent } from "./workspace/component/code-editor-dialog/annotation-suggestion.component"; import { CodeareaCustomTemplateComponent } from "./workspace/component/codearea-custom-template/codearea-custom-template.component"; import { MiniMapComponent } from "./workspace/component/workflow-editor/mini-map/mini-map.component"; import { MenuComponent } from "./workspace/component/menu/menu.component"; import { OperatorLabelComponent } from "./workspace/component/left-panel/operator-menu/operator-label/operator-label.component"; import { OperatorMenuComponent } from "./workspace/component/left-panel/operator-menu/operator-menu.component"; import { SettingsComponent } from "./workspace/component/left-panel/settings/settings.component"; import { PropertyEditorComponent } from "./workspace/component/property-editor/property-editor.component"; import { TypeCastingDisplayComponent } from "./workspace/component/property-editor/typecasting-display/type-casting-display.component"; import { ResultPanelComponent } from "./workspace/component/result-panel/result-panel.component"; import { VisualizationFrameContentComponent } from "./workspace/component/visualization-panel-content/visualization-frame-content.component"; import { WorkflowEditorComponent } from "./workspace/component/workflow-editor/workflow-editor.component"; import { WorkspaceComponent } from "./workspace/component/workspace.component"; import { NzCardModule } from "ng-zorro-antd/card"; import { NzTagModule } from "ng-zorro-antd/tag"; import { NzAvatarModule } from "ng-zorro-antd/avatar"; import { BlobErrorHttpInterceptor } from "./common/service/blob-error-http-interceptor.service"; import { ConsoleFrameComponent } from "./workspace/component/result-panel/console-frame/console-frame.component"; import { ResultTableFrameComponent } from "./workspace/component/result-panel/result-table-frame/result-table-frame.component"; import { RowModalComponent } from "./workspace/component/result-panel/result-panel-modal.component"; import { OperatorPropertyEditFrameComponent } from "./workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component"; import { NzTabsModule } from "ng-zorro-antd/tabs"; import { VersionsListComponent } from "./workspace/component/left-panel/versions-list/versions-list.component"; import { NzPaginationModule } from "ng-zorro-antd/pagination"; import { JwtModule } from "@auth0/angular-jwt"; import { AuthService } from "./common/service/user/auth.service"; import { UserProjectComponent } from "./dashboard/component/user/user-project/user-project.component"; import { UserProjectSectionComponent } from "./dashboard/component/user/user-project/user-project-section/user-project-section.component"; import { NgbdModalAddProjectWorkflowComponent } from "./dashboard/component/user/user-project/user-project-section/ngbd-modal-add-project-workflow/ngbd-modal-add-project-workflow.component"; import { NgbdModalRemoveProjectWorkflowComponent } from "./dashboard/component/user/user-project/user-project-section/ngbd-modal-remove-project-workflow/ngbd-modal-remove-project-workflow.component"; import { PresetWrapperComponent } from "./common/formly/preset-wrapper/preset-wrapper.component"; import { MarkdownDescriptionComponent } from "./dashboard/component/user/markdown-description/markdown-description.component"; import { NzModalCommentBoxComponent } from "./workspace/component/workflow-editor/comment-box-modal/nz-modal-comment-box.component"; import { NzCommentModule } from "ng-zorro-antd/comment"; import { AdminUserComponent } from "./dashboard/component/admin/user/admin-user.component"; import { AdminExecutionComponent } from "./dashboard/component/admin/execution/admin-execution.component"; import { NzPopconfirmModule } from "ng-zorro-antd/popconfirm"; import { AdminGuardService } from "./dashboard/service/admin/guard/admin-guard.service"; import { ContextMenuComponent } from "./workspace/component/workflow-editor/context-menu/context-menu/context-menu.component"; import { CoeditorUserIconComponent } from "./workspace/component/menu/coeditor-user-icon/coeditor-user-icon.component"; import { AgentPanelComponent } from "./workspace/component/agent/agent-panel/agent-panel.component"; import { AgentChatComponent } from "./workspace/component/agent/agent-panel/agent-chat/agent-chat.component"; import { AgentRegistrationComponent } from "./workspace/component/agent/agent-panel/agent-registration/agent-registration.component"; import { DatasetFileSelectorComponent } from "./workspace/component/dataset-file-selector/dataset-file-selector.component"; import { DatasetVersionSelectorComponent } from "./workspace/component/dataset-version-selector/dataset-version-selector.component"; import { DatasetSelectionModalComponent } from "./workspace/component/dataset-selection-modal/dataset-selection-modal.component"; import { ReActStepDetailModalComponent } from "./workspace/component/agent/agent-panel/react-step-detail-modal/react-step-detail-modal.component"; import { CollabWrapperComponent } from "./common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component"; import { NzSwitchModule } from "ng-zorro-antd/switch"; import { NzRadioModule } from "ng-zorro-antd/radio"; import { AboutComponent } from "./hub/component/about/about.component"; import { NzLayoutModule } from "ng-zorro-antd/layout"; import { AuthGuardService } from "./common/service/user/auth-guard.service"; import { LocalLoginComponent } from "./hub/component/about/local-login/local-login.component"; import { MarkdownModule } from "ngx-markdown"; import { FileSaverService } from "./dashboard/service/user/file/file-saver.service"; import { DragDropModule } from "@angular/cdk/drag-drop"; import { ScrollingModule } from "@angular/cdk/scrolling"; import { UserWorkflowListItemComponent } from "./dashboard/component/user/user-workflow/user-workflow-list-item/user-workflow-list-item.component"; import { UserProjectListItemComponent } from "./dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component"; import { SortButtonComponent } from "./dashboard/component/user/sort-button/sort-button.component"; import { FiltersComponent } from "./dashboard/component/user/filters/filters.component"; import { FiltersInstructionsComponent } from "./dashboard/component/user/filters-instructions/filters-instructions.component"; import { SearchComponent } from "./dashboard/component/user/search/search.component"; import { SearchResultsComponent } from "./dashboard/component/user/search-results/search-results.component"; import { PortPropertyEditFrameComponent } from "./workspace/component/property-editor/port-property-edit-frame/port-property-edit-frame.component"; import { AdminGmailComponent } from "./dashboard/component/admin/gmail/admin-gmail.component"; import { PublicProjectComponent } from "./dashboard/component/user/user-project/public-project/public-project.component"; import { FormlyNgZorroAntdModule } from "@ngx-formly/ng-zorro-antd"; import { FlarumComponent } from "./dashboard/component/user/flarum/flarum.component"; import { NzAlertModule } from "ng-zorro-antd/alert"; import { LeftPanelComponent } from "./workspace/component/left-panel/left-panel.component"; import { ErrorFrameComponent } from "./workspace/component/result-panel/error-frame/error-frame.component"; import { NzResizableModule } from "ng-zorro-antd/resizable"; import { WorkflowRuntimeStatisticsComponent } from "./dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-runtime-statistics/workflow-runtime-statistics.component"; import { TimeTravelComponent } from "./workspace/component/left-panel/time-travel/time-travel.component"; import { NzModalModule } from "ng-zorro-antd/modal"; import { NzDescriptionsModule } from "ng-zorro-antd/descriptions"; import { OverlayModule } from "@angular/cdk/overlay"; import { HighlightSearchTermsPipe } from "./dashboard/component/user/user-workflow/user-workflow-list-item/highlight-search-terms.pipe"; import { en_US, provideNzI18n } from "ng-zorro-antd/i18n"; import { FilesUploaderComponent } from "./dashboard/component/user/files-uploader/files-uploader.component"; import { ConflictingFileModalContentComponent } from "./dashboard/component/user/files-uploader/conflicting-file-modal-content/conflicting-file-modal-content.component"; import { UserDatasetComponent } from "./dashboard/component/user/user-dataset/user-dataset.component"; import { UserDatasetVersionCreatorComponent } from "./dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component"; import { DatasetDetailComponent } from "./dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component"; import { UserDatasetVersionFiletreeComponent } from "./dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component"; import { UserDatasetFileRendererComponent } from "./dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component"; import { NzSpinModule } from "ng-zorro-antd/spin"; import { UserDatasetListItemComponent } from "./dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component"; import { NgxFileDropModule } from "ngx-file-drop"; import { NzTreeModule } from "ng-zorro-antd/tree"; import { NzTreeViewModule } from "ng-zorro-antd/tree-view"; import { NzNoAnimationModule } from "ng-zorro-antd/core/animation"; import { TreeModule } from "@ali-hm/angular-tree-component"; import { ResultExportationComponent } from "./workspace/component/result-exportation/result-exportation.component"; import { ReportGenerationService } from "./workspace/service/report-generation/report-generation.service"; import { SearchBarComponent } from "./dashboard/component/user/search-bar/search-bar.component"; import { ListItemComponent } from "./dashboard/component/user/list-item/list-item.component"; import { HubComponent } from "./hub/component/hub.component"; import { HubWorkflowDetailComponent } from "./hub/component/workflow/detail/hub-workflow-detail.component"; import { LandingPageComponent } from "./hub/component/landing-page/landing-page.component"; import { BrowseSectionComponent } from "./hub/component/browse-section/browse-section.component"; import { BreakpointConditionInputComponent } from "./workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component"; import { CodeDebuggerComponent } from "./workspace/component/code-editor-dialog/code-debugger.component"; import { AgentInteractionComponent } from "./workspace/component/agent/agent-interaction/agent-interaction.component"; import { GoogleAuthService } from "./common/service/user/google-auth.service"; import { GoogleLoginProvider, GoogleSigninButtonModule, SocialAuthServiceConfig, SocialLoginModule, } from "@abacritt/angularx-social-login"; import { catchError, firstValueFrom, lastValueFrom, of } from "rxjs"; import { HubSearchResultComponent } from "./hub/component/hub-search-result/hub-search-result.component"; import { UserDatasetStagedObjectsListComponent } from "./dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-staged-objects-list/user-dataset-staged-objects-list.component"; import { NzEmptyModule } from "ng-zorro-antd/empty"; import { NzDividerModule } from "ng-zorro-antd/divider"; import { NzProgressModule } from "ng-zorro-antd/progress"; import { ComputingUnitSelectionComponent } from "./workspace/component/power-button/computing-unit-selection.component"; import { NzSliderModule } from "ng-zorro-antd/slider"; import { AdminSettingsComponent } from "./dashboard/component/admin/settings/admin-settings.component"; import { FormlyRepeatDndComponent } from "./common/formly/repeat-dnd/repeat-dnd.component"; import { NzInputNumberModule } from "ng-zorro-antd/input-number"; import { NzGridModule } from "ng-zorro-antd/grid"; import { NzCheckboxModule } from "ng-zorro-antd/checkbox"; import { RegistrationRequestModalComponent } from "./common/service/user/registration-request-modal/registration-request-modal.component"; import { UserComputingUnitComponent } from "./dashboard/component/user/user-computing-unit/user-computing-unit.component"; import { UserComputingUnitListItemComponent } from "./dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component"; registerLocaleData(en); @NgModule({ declarations: [AppComponent], imports: [ BrowserModule, AppRoutingModule, HttpClientModule, JwtModule.forRoot({ config: { tokenGetter: AuthService.getAccessToken, skipWhenExpired: false, throwNoTokenError: false, disallowedRoutes: ["forum/api/users"], }, }), BrowserAnimationsModule, RouterModule, FormsModule, ReactiveFormsModule, FormlyModule.forRoot(TEXERA_FORMLY_CONFIG), FormlyNgZorroAntdModule, OverlayModule, NzDatePickerModule, NzDropDownModule, NzButtonModule, NzAutocompleteModule, NzIconModule, NzFormModule, NzListModule, NzInputModule, NzPopoverModule, NzCollapseModule, NzTooltipModule, NzTableModule, NzSelectModule, NzSpaceModule, NzBadgeModule, NzUploadModule, NgxJsonViewerModule, NzModalModule, NzDescriptionsModule, NzCardModule, NzTagModule, NzPopconfirmModule, NzAvatarModule, NzTabsModule, NzPaginationModule, NzCommentModule, ColorPickerModule, NzSwitchModule, NzRadioModule, NzLayoutModule, NzSliderModule, MarkdownModule.forRoot(), DragDropModule, NzAlertModule, NzResizableModule, NzSpinModule, NgxFileDropModule, NzTreeModule, NzTreeViewModule, NzNoAnimationModule, TreeModule, SocialLoginModule, GoogleSigninButtonModule, NzEmptyModule, NzDividerModule, NzProgressModule, NzInputNumberModule, NzCheckboxModule, NzGridModule, ScrollingModule, FormlyRepeatDndComponent, AdminGmailComponent, PublicProjectComponent, WorkspaceComponent, MenuComponent, OperatorMenuComponent, SettingsComponent, PropertyEditorComponent, VersionsListComponent, TimeTravelComponent, WorkflowEditorComponent, ResultPanelComponent, ResultExportationComponent, OperatorLabelComponent, DashboardComponent, AdminUserComponent, AdminExecutionComponent, UserIconComponent, UserAvatarComponent, LocalLoginComponent, UserWorkflowComponent, UserQuotaComponent, RowModalComponent, OperatorLabelComponent, MiniMapComponent, ArrayTypeComponent, ObjectTypeComponent, PresetWrapperComponent, MultiSchemaTypeComponent, NullTypeComponent, VisualizationFrameContentComponent, CodeareaCustomTemplateComponent, CodeEditorComponent, AnnotationSuggestionComponent, TypeCastingDisplayComponent, ShareAccessComponent, WorkflowExecutionHistoryComponent, ConsoleFrameComponent, ErrorFrameComponent, ResultTableFrameComponent, OperatorPropertyEditFrameComponent, UserProjectComponent, UserProjectSectionComponent, NgbdModalAddProjectWorkflowComponent, NgbdModalRemoveProjectWorkflowComponent, FilesUploaderComponent, ConflictingFileModalContentComponent, UserDatasetComponent, UserDatasetVersionCreatorComponent, DatasetDetailComponent, UserDatasetVersionFiletreeComponent, UserDatasetListItemComponent, UserDatasetFileRendererComponent, UserDatasetStagedObjectsListComponent, NzModalCommentBoxComponent, LeftPanelComponent, ContextMenuComponent, CoeditorUserIconComponent, AgentPanelComponent, AgentChatComponent, AgentRegistrationComponent, AgentInteractionComponent, DatasetFileSelectorComponent, DatasetVersionSelectorComponent, DatasetSelectionModalComponent, ReActStepDetailModalComponent, CollabWrapperComponent, AboutComponent, UserWorkflowListItemComponent, UserProjectListItemComponent, SortButtonComponent, FiltersComponent, FiltersInstructionsComponent, SearchComponent, PortPropertyEditFrameComponent, WorkflowRuntimeStatisticsComponent, FlarumComponent, HighlightSearchTermsPipe, SearchBarComponent, ListItemComponent, SearchResultsComponent, HubComponent, HubWorkflowDetailComponent, LandingPageComponent, BrowseSectionComponent, BreakpointConditionInputComponent, CodeDebuggerComponent, HubSearchResultComponent, ComputingUnitSelectionComponent, AdminSettingsComponent, RegistrationRequestModalComponent, MarkdownDescriptionComponent, UserComputingUnitComponent, UserComputingUnitListItemComponent, ], providers: [ provideNzI18n(en_US), AuthGuardService, AdminGuardService, DatePipe, UserService, GuiConfigService, FileSaverService, ReportGenerationService, { provide: HTTP_INTERCEPTORS, useClass: BlobErrorHttpInterceptor, multi: true, }, { provide: "SocialAuthServiceConfig", useFactory: (googleAuthService: GoogleAuthService, userService: UserService) => { return lastValueFrom(googleAuthService.getClientId()).then(clientId => ({ providers: [ { id: GoogleLoginProvider.PROVIDER_ID, provider: new GoogleLoginProvider(clientId, { oneTapEnabled: !userService.isLogin() }), }, ], })) as Promise; }, deps: [GoogleAuthService, UserService], }, { provide: APP_INITIALIZER, useFactory: (configService: GuiConfigService) => () => firstValueFrom( configService.load().pipe( catchError((err: unknown) => { console.error("Failed to load GUI config during app init:", err); // swallow error so the app can still bootstrap; app.component.ts will show the error message as HTML. return of(null); }) ) ), deps: [GuiConfigService], multi: true, }, ], bootstrap: [AppComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class AppModule {} ================================================ FILE: frontend/src/app/common/app-setting.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { environment } from "../../environments/environment"; export class AppSettings { public static getApiEndpoint(): string { return environment.apiUrl; } } ================================================ FILE: frontend/src/app/common/formly/array.type.ts ================================================ /** * Copyright 2018 Google Inc. All Rights Reserved. * Use of this source code is governed by an MIT-style license that * can be found in the LICENSE file at http://angular.io/license * * This file is derived from Angular examples. * Source: https://angular.io */ import { Component } from "@angular/core"; import { FieldArrayType, FormlyModule } from "@ngx-formly/core"; import { NgFor } from "@angular/common"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; @Component({ template: `

    {{ props.label }}

    `, imports: [ NgFor, FormlyModule, NzSpaceCompactItemDirective, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, NzIconDirective, ], }) export class ArrayTypeComponent extends FieldArrayType {} ================================================ FILE: frontend/src/app/common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component.css ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ :host ::ng-deep .ql-clipboard { max-height: 0; } :host ::ng-deep .ql-editor { outline: transparent; overflow: visible; position: relative; } :host ::ng-deep .ql-editor > p { margin-bottom: 0; box-sizing: border-box; } :host ::ng-deep .ql-container { overflow: visible; } ================================================ FILE: frontend/src/app/common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component.html ================================================
    ================================================ FILE: frontend/src/app/common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { AfterContentInit, Component, ElementRef, ViewChild } from "@angular/core"; import { FieldTypeConfig, FieldWrapper, FormlyFieldConfig } from "@ngx-formly/core"; import { WorkflowActionService } from "../../../../workspace/service/workflow-graph/model/workflow-action.service"; import { merge } from "lodash"; import Quill from "quill"; import * as Y from "yjs"; import { QuillBinding } from "y-quill"; import QuillCursors from "quill-cursors"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { NgStyle } from "@angular/common"; // Quill related definitions export const COLLAB_DEBOUNCE_TIME_MS = 10; const Clipboard = Quill.import("modules/clipboard"); const Delta = Quill.import("delta"); /** * Custom clipboard module that removes rich text formats and newline characters */ class PlainClipboard extends Clipboard { onPaste(e: { preventDefault: () => void; clipboardData: { getData: (arg0: string) => any } }) { e.preventDefault(); const range = this.quill.getSelection(); const text = (e.clipboardData.getData("text/plain") as string).replace(/\n/g, ""); const delta = new Delta().retain(range.index).delete(range.length).insert(text); const index = text.length + range.index; const length = 0; this.quill.updateContents(delta, "silent"); this.quill.setSelection(index, length, "silent"); this.quill.scrollIntoView(); } } Quill.register( { "modules/clipboard": PlainClipboard, }, true ); Quill.register("modules/cursors", QuillCursors); /** * CollabWrapperComponent is a custom field wrapper that connects a string/textfield typed form field to a collaborative * text editor based on Yjs and Quill. */ @UntilDestroy() @Component({ templateUrl: "./collab-wrapper.component.html", styleUrls: ["./collab-wrapper.component.css"], imports: [NgStyle], }) export class CollabWrapperComponent extends FieldWrapper implements AfterContentInit { private quill?: Quill; private currentOperatorId: string = ""; private operatorType: string = ""; private quillBinding?: QuillBinding; private sharedText?: Y.Text; @ViewChild("editor", { static: true }) divEditor?: ElementRef; constructor(private workflowActionService: WorkflowActionService) { super(); } ngAfterContentInit(): void { this.setUpYTextEditor(); this.formControl.valueChanges.pipe(untilDestroyed(this)).subscribe(value => { if (this.sharedText !== undefined && value !== this.sharedText.toJSON()) { this.setUpYTextEditor(); } }); this.registerDisableEditorInteractivityHandler(); } private setUpYTextEditor() { setTimeout(() => { if (this.field.key === undefined || this.field.props === undefined) { throw Error( `form collab-wrapper field ${this.field} doesn't contain necessary .key and .templateOptions.presetKey attributes` ); } else { this.currentOperatorId = this.field.props.currentOperatorId; this.operatorType = this.field.props.operatorType; let parents = [this.field.key]; let parent = this.field.parent; while (parent?.key !== undefined) { parents.push(parent.key); parent = parent.parent; } let parentStructure: any = this.workflowActionService .getTexeraGraph() .getSharedOperatorPropertyType(this.currentOperatorId); let structure: any = undefined; let key: any; this.workflowActionService.getTexeraGraph().bundleActions(() => { while (parents.length > 0 && parentStructure !== undefined && parentStructure !== null) { key = parents.pop(); structure = parentStructure.get(key); if (structure === undefined || structure === null) { if (parents.length > 0) { if (parentStructure instanceof Y.Array) { const yArray = parentStructure as Y.Array; if (yArray.length > parseInt(key)) { yArray.delete(parseInt(key), 1); yArray.insert(parseInt(key), [new Y.Map()]); } else { yArray.push([new Y.Map()]); } } else { parentStructure.set(key as string, new Y.Map()); } } else { if (parentStructure instanceof Y.Array) { const yArray = parentStructure as Y.Array; if (yArray.length > parseInt(key)) { yArray.delete(parseInt(key), 1); yArray.insert(parseInt(key), [new Y.Text("")]); } else { yArray.push([new Y.Text("")]); } } else { parentStructure.set(key as string, new Y.Text()); } } structure = parentStructure.get(key); } parentStructure = structure; } }); this.sharedText = structure; this.initializeQuillEditor(); if (this.currentOperatorId && this.sharedText) { this.quillBinding = new QuillBinding( this.sharedText, this.quill, this.workflowActionService.getTexeraGraph().getSharedModelAwareness() ); } } }, COLLAB_DEBOUNCE_TIME_MS); } private initializeQuillEditor() { // Operator name editor const element = this.divEditor as ElementRef; this.quill = new Quill(element.nativeElement, { modules: { cursors: true, toolbar: false, history: { // Local undo shouldn't undo changes // from remote users userOnly: true, }, // Disable newline on enter and instead quit editing keyboard: this.field.type === "textarea" ? {} : { bindings: { enter: { key: 13, handler: () => {}, }, shift_enter: { key: 13, shiftKey: true, handler: () => {}, }, }, }, }, formats: [], placeholder: "Start collaborating...", theme: "bubble", }); this.quill.enable(this.evaluateInteractivity()); } private evaluateInteractivity(): boolean { return this.formControl.enabled; } private setInteractivity(interactive: boolean) { if (interactive !== this.quill?.isEnabled()) this.quill?.enable(interactive); } private registerDisableEditorInteractivityHandler(): void { this.formControl.statusChanges.pipe(untilDestroyed(this)).subscribe(_ => { this.setInteractivity(this.evaluateInteractivity()); }); } static setupFieldConfig( mappedField: FormlyFieldConfig, operatorType: string, currentOperatorId: string, includePresetWrapper: boolean = false ) { const fieldConfig: FormlyFieldConfig = { wrappers: includePresetWrapper ? ["form-field", "preset-wrapper", "collab-wrapper"] : ["form-field", "collab-wrapper"], props: { operatorType: operatorType, currentOperatorId: currentOperatorId, }, }; merge(mappedField, fieldConfig); } } ================================================ FILE: frontend/src/app/common/formly/formly-config.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { NullTypeComponent } from "./null.type"; import { ArrayTypeComponent } from "./array.type"; import { ObjectTypeComponent } from "./object.type"; import { MultiSchemaTypeComponent } from "./multischema.type"; import { FormlyFieldConfig } from "@ngx-formly/core"; import { CodeareaCustomTemplateComponent } from "../../workspace/component/codearea-custom-template/codearea-custom-template.component"; import { PresetWrapperComponent } from "./preset-wrapper/preset-wrapper.component"; import { DatasetFileSelectorComponent } from "../../workspace/component/dataset-file-selector/dataset-file-selector.component"; import { CollabWrapperComponent } from "./collab-wrapper/collab-wrapper/collab-wrapper.component"; import { FormlyRepeatDndComponent } from "./repeat-dnd/repeat-dnd.component"; import { DatasetVersionSelectorComponent } from "../../workspace/component/dataset-version-selector/dataset-version-selector.component"; /** * Configuration for using Json Schema with Formly. * This config is copy-pasted from official documentation, * see https://formly.dev/examples/advanced/json-schema */ export const TEXERA_FORMLY_CONFIG = { validationMessages: [ { name: "required", message: "This field is required" }, { name: "null", message: "should be null" }, { name: "minLength", message: minlengthValidationMessage }, { name: "maxLength", message: maxlengthValidationMessage }, { name: "min", message: minValidationMessage }, { name: "max", message: maxValidationMessage }, { name: "multipleOf", message: multipleOfValidationMessage }, { name: "exclusiveMinimum", message: exclusiveMinimumValidationMessage }, { name: "exclusiveMaximum", message: exclusiveMaximumValidationMessage }, { name: "minItems", message: minItemsValidationMessage }, { name: "maxItems", message: maxItemsValidationMessage }, { name: "uniqueItems", message: "should NOT have duplicate items" }, { name: "const", message: constValidationMessage }, ], types: [ { name: "string", extends: "input", defaultOptions: { defaultValue: "" } }, { name: "number", extends: "input", defaultOptions: { templateOptions: { type: "number", }, }, }, { name: "integer", extends: "input", defaultOptions: { templateOptions: { type: "number", }, }, }, { name: "boolean", extends: "checkbox" }, { name: "enum", extends: "select" }, { name: "null", component: NullTypeComponent, wrappers: ["form-field"] }, { name: "array", component: ArrayTypeComponent }, { name: "object", component: ObjectTypeComponent }, { name: "multischema", component: MultiSchemaTypeComponent }, { name: "codearea", component: CodeareaCustomTemplateComponent }, { name: "inputautocomplete", component: DatasetFileSelectorComponent, wrappers: ["form-field"] }, { name: "datasetversionselector", component: DatasetVersionSelectorComponent, wrappers: ["form-field"] }, { name: "repeat-section-dnd", component: FormlyRepeatDndComponent }, ], wrappers: [ { name: "preset-wrapper", component: PresetWrapperComponent }, { name: "collab-wrapper", component: CollabWrapperComponent }, ], }; export function minItemsValidationMessage(err: any, field: FormlyFieldConfig) { return `should NOT have fewer than ${field.props?.minItems} items`; } export function maxItemsValidationMessage(err: any, field: FormlyFieldConfig) { return `should NOT have more than ${field.props?.maxItems} items`; } export function minlengthValidationMessage(err: any, field: FormlyFieldConfig) { return `should NOT be shorter than ${field.props?.minLength} characters`; } export function maxlengthValidationMessage(err: any, field: FormlyFieldConfig) { return `should NOT be longer than ${field.props?.maxLength} characters`; } export function minValidationMessage(err: any, field: FormlyFieldConfig) { return `should be >= ${field.props?.min}`; } export function maxValidationMessage(err: any, field: FormlyFieldConfig) { return `should be <= ${field.props?.max}`; } export function multipleOfValidationMessage(err: any, field: FormlyFieldConfig) { return `should be multiple of ${field.props?.step}`; } export function exclusiveMinimumValidationMessage(err: any, field: FormlyFieldConfig) { return `should be > ${field.props?.exclusiveMinimum}`; } export function exclusiveMaximumValidationMessage(err: any, field: FormlyFieldConfig) { return `should be < ${field.props?.exclusiveMaximum}`; } export function constValidationMessage(err: any, field: FormlyFieldConfig) { return `should be equal to constant "${field.props?.const}"`; } ================================================ FILE: frontend/src/app/common/formly/formly-utils.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { FormlyFieldConfig } from "@ngx-formly/core"; import { isDefined } from "../util/predicate"; import { Observable } from "rxjs"; import { FORM_DEBOUNCE_TIME_MS } from "../../workspace/service/execute-workflow/execute-workflow.service"; import { debounceTime, distinctUntilChanged, filter, share } from "rxjs/operators"; import { HideType } from "../../workspace/types/custom-json-schema.interface"; import { PortSchema } from "../../workspace/types/workflow-compiling.interface"; export function getFieldByName(fieldName: string, fields: FormlyFieldConfig[]): FormlyFieldConfig | undefined { return fields.filter((field, _, __) => field.key === fieldName)[0]; } export function setHideExpression(toggleHidden: string[], fields: FormlyFieldConfig[], hiddenBy: string): void { toggleHidden.forEach(hiddenFieldName => { const fieldToBeHidden = getFieldByName(hiddenFieldName, fields); if (isDefined(fieldToBeHidden)) { fieldToBeHidden.expressions = { hide: "!field.parent.model." + hiddenBy }; } }); } /* Factory function to make functions that hide expressions for a particular field */ export function createShouldHideFieldFunc( hideTarget: string, hideType: HideType, hideExpectedValue: string, hideOnNull: boolean | undefined ) { let shared_regex: RegExp | null = null; const hideFunc = (field?: FormlyFieldConfig | undefined) => { const model = field?.parent?.model; if (model === null || model === undefined) { console.debug("Formly main model not detected. Hiding will fail."); return false; } let targetFieldValue: any = model[hideTarget]; if (targetFieldValue === null || targetFieldValue === undefined) { // console.debug("Formly model does not contain hide target. Formly does not know what to hide."); return hideOnNull === true; } switch (hideType) { case "regex": if (shared_regex === null) shared_regex = new RegExp(`^(${hideExpectedValue})$`); return shared_regex.test(targetFieldValue); case "equals": return targetFieldValue.toString() === hideExpectedValue; } }; return hideFunc; } export function setChildTypeDependency( attributes: Readonly> | undefined, parentName: string, fields: FormlyFieldConfig[], childName: string ): void { const timestampFieldNames = Object.values(attributes || {}) .flat() .filter(attribute => { return attribute?.attributeType === "timestamp"; }) .map(attribute => attribute?.attributeName); if (timestampFieldNames) { const childField = getFieldByName(childName, fields); if (isDefined(childField)) { childField.expressions = { // 'type': 'string', // 'templateOptions.type': JSON.stringify(timestampFieldNames) + '.includes(model.' + parentName + ')? \'string\' : \'number\'', "templateOptions.description": JSON.stringify(timestampFieldNames) + ".includes(model." + parentName + ")? 'Input a datetime string' : 'Input a positive number'", }; } } } /** * Handles the form change event stream observable, * which corresponds to every event the json schema form library emits. * * Applies rules that transform the event stream to trigger reasonably and less frequently, * such as debounce time and distinct condition. * * Then modifies the operator property to use the new form data. */ export function createOutputFormChangeEventStream( formChangeEvent: Observable>, modelCheck: (formData: Record) => boolean ): Observable> { return ( formChangeEvent // set a debounce time to avoid events triggering too often // and to circumvent a bug of the library - each action triggers event twice .pipe( debounceTime(FORM_DEBOUNCE_TIME_MS), // .do(evt => console.log(evt)) // don't emit the event until the data is changed distinctUntilChanged(), // .do(evt => console.log(evt)) // don't emit the event if form data is same with current actual data // also check for other unlikely circumstances (see below) filter(formData => modelCheck(formData)), // share() because the original observable is a hot observable share() ) ); } ================================================ FILE: frontend/src/app/common/formly/multischema.type.ts ================================================ /** * Copyright 2018 Google Inc. All Rights Reserved. * Use of this source code is governed by an MIT-style license that * can be found in the LICENSE file at http://angular.io/license * * This file is derived from Angular examples. * Source: https://angular.io */ import { Component } from "@angular/core"; import { FieldType, FormlyModule } from "@ngx-formly/core"; import { NgIf, NgFor } from "@angular/common"; @Component({ // selector: 'formly-multi-schema-type', template: `
    {{ props.label }}

    {{ props.description }}

    `, imports: [NgIf, FormlyModule, NgFor], }) export class MultiSchemaTypeComponent extends FieldType {} ================================================ FILE: frontend/src/app/common/formly/null.type.ts ================================================ /** * Copyright 2018 Google Inc. All Rights Reserved. * Use of this source code is governed by an MIT-style license that * can be found in the LICENSE file at http://angular.io/license * * This file is derived from Angular examples. * Source: https://angular.io */ import { Component } from "@angular/core"; import { FieldType, FieldTypeConfig } from "@ngx-formly/core"; @Component({ // selector: 'formly-null-type', template: "", }) export class NullTypeComponent extends FieldType {} ================================================ FILE: frontend/src/app/common/formly/object.type.ts ================================================ /** * Copyright 2018 Google Inc. All Rights Reserved. * Use of this source code is governed by an MIT-style license that * can be found in the LICENSE file at http://angular.io/license * * This file is derived from Angular examples. * Source: https://angular.io */ import { Component } from "@angular/core"; import { FieldType, FieldTypeConfig, FormlyModule } from "@ngx-formly/core"; import { NgIf, NgFor } from "@angular/common"; @Component({ // selector: 'formly-object-type', template: `
    {{ props.label }}

    {{ props.description }}

    `, imports: [NgIf, FormlyModule, NgFor], }) export class ObjectTypeComponent extends FieldType { defaultOptions = { defaultValue: {}, }; } ================================================ FILE: frontend/src/app/common/formly/preset-wrapper/preset-wrapper.component.html ================================================
    • {{this.getEntryTitle(preset)}} {{this.getEntryDescription(preset)}}
    ================================================ FILE: frontend/src/app/common/formly/preset-wrapper/preset-wrapper.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .preset-field { display: flex; .formly-field { flex-grow: 1; } .save-button { margin: calc(1em - 21px) 0 0 0.5em; display: flex; align-items: center; justify-content: center; opacity: 0.1; &:hover { opacity: 1; } } } ::ng-deep .preset-dropdown { transform: translateY(4px) !important; &.no-results { display: none; } .preset-menu { max-height: 150px; overflow: auto; } .preset-dropdown-item { pointer-events: auto; display: flex; .dropdown-entry { flex-grow: 1; display: flex; overflow: hidden; .title { text-align: left; margin-right: 0.8em; font-size: 1.1em; text-overflow: ellipsis; overflow: hidden; } .description { flex-grow: 1; font-size: 0.7em; text-align: right; color: rgba(0, 0, 0, 0.45); font-style: italic; text-overflow: ellipsis; overflow: hidden; padding-right: 0.2em; } } .delete-button { margin: 0 0 0 0.5em; display: flex; align-items: center; justify-content: center; opacity: 0.1; &:hover { opacity: 1; } } } } ================================================ FILE: frontend/src/app/common/formly/preset-wrapper/preset-wrapper.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { FormControl } from "@angular/forms"; import { FormlyFieldConfig } from "@ngx-formly/core"; import { NzMessageService } from "ng-zorro-antd/message"; import { Subject, of } from "rxjs"; import { Preset, PresetService } from "src/app/workspace/service/preset/preset.service"; import { PresetKey, PresetWrapperComponent } from "./preset-wrapper.component"; const fieldKey = "testkey"; const presetKey: PresetKey = { presetType: "testPresetType", saveTarget: "testPresetSaveTarget", applyTarget: "testPresetApplyTarget", }; const testPreset: Preset = { testkey: "testPresetValue", otherkey: "otherPresetValue" }; const otherPreset: Preset = { testkey: "otherPresetValue2", otherkey: "otherPresetValue3" }; describe("PresetWrapperComponent", () => { let component: PresetWrapperComponent; let fixture: ComponentFixture; let formControl: FormControl; let presetServiceStub: { applyPreset: ReturnType; deletePreset: ReturnType; createPreset: ReturnType; getPresets: ReturnType; isValidPreset: ReturnType; savePresetsStream: Subject<{ type: string; target: string; presets: Preset[] }>; applyPresetStream: Subject<{ type: string; target: string; preset: Preset }>; }; let messageStub: { error: ReturnType; success: ReturnType; info: ReturnType; warning: ReturnType; }; // Builds a minimal FormlyFieldConfig sufficient for ngOnInit to run. // ngOnInit also calls filterPresetFromForm(), which iterates // field.parent.fieldGroup looking for sibling preset-wrapper fields, so // we expose a single sibling pointing at an empty model by default. const buildField = (overrides: Partial = {}): FormlyFieldConfig => { const self = { key: fieldKey, wrappers: ["preset-wrapper"], model: { [fieldKey]: "" }, } as FormlyFieldConfig; return { key: fieldKey, formControl, templateOptions: { presetKey }, parent: { fieldGroup: [self] }, ...overrides, } as FormlyFieldConfig; }; beforeEach(async () => { formControl = new FormControl(""); presetServiceStub = { applyPreset: vi.fn(), deletePreset: vi.fn(), createPreset: vi.fn(), getPresets: vi.fn().mockReturnValue(of([])), isValidPreset: vi.fn().mockReturnValue(true), savePresetsStream: new Subject(), applyPresetStream: new Subject(), }; messageStub = { error: vi.fn(), success: vi.fn(), info: vi.fn(), warning: vi.fn(), }; // Override the template so the spec doesn't depend on the ng-zorro // dropdown machinery — we exercise the public component API directly. TestBed.overrideComponent(PresetWrapperComponent, { set: { template: "" } }); await TestBed.configureTestingModule({ imports: [PresetWrapperComponent], providers: [ { provide: PresetService, useValue: presetServiceStub }, { provide: NzMessageService, useValue: messageStub }, ], }).compileComponents(); fixture = TestBed.createComponent(PresetWrapperComponent); component = fixture.componentInstance; }); it("should create", () => { component.field = buildField(); fixture.detectChanges(); expect(component).toBeTruthy(); }); describe("ngOnInit", () => { it("throws when field.key is missing", () => { component.field = buildField({ key: undefined }); expect(() => component.ngOnInit()).toThrow(); }); it("throws when templateOptions is missing", () => { component.field = buildField({ templateOptions: undefined }); expect(() => component.ngOnInit()).toThrow(); }); it("throws when templateOptions.presetKey is missing", () => { component.field = buildField({ templateOptions: {} }); expect(() => component.ngOnInit()).toThrow(); }); it("populates searchResults from presetService.getPresets on init", () => { presetServiceStub.getPresets.mockReturnValue(of([testPreset, otherPreset])); component.field = buildField(); component.ngOnInit(); expect(presetServiceStub.getPresets).toHaveBeenCalledWith(presetKey.presetType, presetKey.saveTarget); expect(component.searchResults).toEqual([testPreset, otherPreset]); }); }); describe("functional api", () => { beforeEach(() => { component.field = buildField(); component.ngOnInit(); }); it("applyPreset forwards to PresetService with the configured presetType + applyTarget", () => { component.applyPreset(testPreset); expect(presetServiceStub.applyPreset).toHaveBeenCalledTimes(1); expect(presetServiceStub.applyPreset).toHaveBeenCalledWith( presetKey.presetType, presetKey.applyTarget, testPreset ); }); it("deletePreset forwards to PresetService with the configured presetType + saveTarget", () => { component.deletePreset(testPreset); expect(presetServiceStub.deletePreset).toHaveBeenCalledTimes(1); const args = presetServiceStub.deletePreset.mock.calls[0]; expect(args.slice(0, 3)).toEqual([presetKey.presetType, presetKey.saveTarget, testPreset]); }); it("getEntryTitle returns the value at field.key", () => { expect(component.getEntryTitle(testPreset)).toBe("testPresetValue"); }); it("getEntryDescription joins all non-key values with commas", () => { expect(component.getEntryDescription(testPreset)).toBe("otherPresetValue"); expect( component.getEntryDescription({ testkey: "title", a: "first", b: "second", }) ).toBe("first, second"); }); describe("getSearchResults", () => { it("returns a copy of all presets when showAllResults is true", () => { const presets: Preset[] = [testPreset, otherPreset]; const results = component.getSearchResults(presets, "anything", true); expect(results).toEqual(presets); expect(results).not.toBe(presets); }); it("returns all presets when showAllResults is true even if the search term doesn't match", () => { expect(component.getSearchResults([testPreset], "no-match", true)).toEqual([testPreset]); }); it("filters by case-insensitive prefix match on the entry title when showAllResults is false", () => { const presets: Preset[] = [testPreset, otherPreset]; // testPreset title 'testPresetValue' starts with 'TEST' expect(component.getSearchResults(presets, "TEST", false)).toEqual([testPreset]); // otherPreset title 'otherPresetValue2' starts with 'other' expect(component.getSearchResults(presets, "other", false)).toEqual([otherPreset]); }); it("returns the full list when search term is empty and showAllResults is false", () => { expect(component.getSearchResults([testPreset], "", false)).toEqual([testPreset]); }); it("returns an empty list when the search term matches nothing", () => { expect(component.getSearchResults([testPreset], "zzzz", false)).toEqual([]); }); }); }); describe("dropdown visibility", () => { beforeEach(() => { component.field = buildField(); component.ngOnInit(); }); it("re-fetches presets and updates searchResults when the dropdown opens", () => { presetServiceStub.getPresets.mockReturnValue(of([testPreset])); // ngOnInit has already called getPresets once. const baseline = presetServiceStub.getPresets.mock.calls.length; component.onDropdownVisibilityEvent(true); expect(presetServiceStub.getPresets.mock.calls.length).toBe(baseline + 1); expect(component.searchResults).toEqual([testPreset]); }); it("does not refetch when the dropdown closes", () => { const baseline = presetServiceStub.getPresets.mock.calls.length; component.onDropdownVisibilityEvent(false); expect(presetServiceStub.getPresets.mock.calls.length).toBe(baseline); }); }); describe("PresetService stream subscriptions", () => { beforeEach(() => { component.field = buildField(); component.ngOnInit(); }); it("updates searchResults when savePresetsStream emits a matching event", () => { component.searchResults = []; const presets: Preset[] = [testPreset, otherPreset]; presetServiceStub.savePresetsStream.next({ type: presetKey.presetType, target: presetKey.saveTarget, presets, }); expect(component.searchResults).toEqual(presets); }); it("ignores savePresetsStream events for a different presetType", () => { component.searchResults = []; presetServiceStub.savePresetsStream.next({ type: "differentType", target: presetKey.saveTarget, presets: [testPreset], }); expect(component.searchResults).toEqual([]); }); it("ignores savePresetsStream events for a different saveTarget", () => { component.searchResults = []; presetServiceStub.savePresetsStream.next({ type: presetKey.presetType, target: "differentTarget", presets: [testPreset], }); expect(component.searchResults).toEqual([]); }); it("does not refresh searchResults from form value changes while the dropdown is closed", () => { const baselineCalls = presetServiceStub.getPresets.mock.calls.length; component.presetMenuVisible = false; formControl.setValue("typing"); // No additional getPresets call because the menu is closed. expect(presetServiceStub.getPresets.mock.calls.length).toBe(baselineCalls); }); it("refreshes searchResults from form value changes while the dropdown is open", async () => { component.presetMenuVisible = true; presetServiceStub.getPresets.mockReturnValue(of([testPreset])); const baselineCalls = presetServiceStub.getPresets.mock.calls.length; formControl.setValue("typing"); // The valueChanges handler is debounced(0) — wait one microtask tick. await new Promise(resolve => setTimeout(resolve, 0)); expect(presetServiceStub.getPresets.mock.calls.length).toBe(baselineCalls + 1); }); it("stops responding to stream events after ngOnDestroy", () => { component.searchResults = []; component.ngOnDestroy(); presetServiceStub.savePresetsStream.next({ type: presetKey.presetType, target: presetKey.saveTarget, presets: [testPreset], }); expect(component.searchResults).toEqual([]); }); }); describe("savePreset", () => { // savePreset() reads sibling preset-wrapper fields off field.parent.fieldGroup // to construct the preset payload. const buildFieldWithSiblings = (model: Record): FormlyFieldConfig => { const fieldGroup: FormlyFieldConfig[] = [ { key: fieldKey, wrappers: ["preset-wrapper"], model } as FormlyFieldConfig, { key: "otherkey", wrappers: ["preset-wrapper"], model } as FormlyFieldConfig, // Non-preset sibling — must be ignored. { key: "ignored", wrappers: ["form-field"], model } as FormlyFieldConfig, ]; return { key: fieldKey, formControl, templateOptions: { presetKey }, parent: { fieldGroup }, } as FormlyFieldConfig; }; it("creates a preset built from sibling preset-wrapper fields when the preset is valid", () => { component.field = buildFieldWithSiblings({ testkey: "v1", otherkey: "v2", ignored: "x" }); component.ngOnInit(); presetServiceStub.isValidPreset.mockReturnValue(true); component.savePreset(); expect(presetServiceStub.isValidPreset).toHaveBeenCalledWith({ testkey: "v1", otherkey: "v2" }); expect(presetServiceStub.createPreset).toHaveBeenCalledWith(presetKey.presetType, presetKey.saveTarget, { testkey: "v1", otherkey: "v2", }); expect(messageStub.error).not.toHaveBeenCalled(); }); it("shows an error toast and does not create a preset when the preset is invalid", () => { component.field = buildFieldWithSiblings({ testkey: "", otherkey: "v2" }); component.ngOnInit(); presetServiceStub.isValidPreset.mockReturnValue(false); component.savePreset(); expect(presetServiceStub.createPreset).not.toHaveBeenCalled(); expect(messageStub.error).toHaveBeenCalledTimes(1); }); }); }); ================================================ FILE: frontend/src/app/common/formly/preset-wrapper/preset-wrapper.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, OnDestroy, OnInit } from "@angular/core"; import { FieldWrapper, FormlyFieldConfig } from "@ngx-formly/core"; import { merge } from "lodash"; import { ReplaySubject } from "rxjs"; import { debounceTime, filter, first, takeUntil } from "rxjs/operators"; import { Preset, PresetService } from "src/app/workspace/service/preset/preset.service"; import { asType } from "../../util/assert"; import { NzMessageService } from "ng-zorro-antd/message"; import { NzDropdownDirective, NzDropdownMenuComponent } from "ng-zorro-antd/dropdown"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzMenuDirective, NzMenuItemComponent } from "ng-zorro-antd/menu"; import { NgFor } from "@angular/common"; /** * PresetWrapperComponent is a custom formly form field wrapper: https://formly.dev/guide/custom-formly-wrapper * It uses PresetService to create a dropdown menu for a form field that includes preset entries that when clicked are * applied through the PresetService (generating an event in PresetService.applyPresetStream). * Currently the PresetService only truly handles operator presets. (i.e. causing the for data to change immediately) * For non-operator presets, an application event is generated, but no action is taken (please implement listeners to apply the presets properly) * USAGE: * Formly field key should match attributes of preset * FormlyFieldConfig.wrappers should include 'preset-wrapper' * FormlyFieldConfig.templateOptions.presetKey should be a PresetKey * @author Albert Liu */ /** * A PresetKey must be passed to PresetWrapperComponent via templateOptions.presetKey (add a template option to the formly field config) * This can be done easily by using PresetWrapperComponent.setupFieldConfig() */ export interface PresetKey { presetType: string; saveTarget: string; applyTarget: string; } @Component({ templateUrl: "./preset-wrapper.component.html", styleUrls: ["./preset-wrapper.component.scss"], imports: [ NzDropdownDirective, NzSpaceCompactItemDirective, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, NzIconDirective, NzDropdownMenuComponent, NzMenuDirective, NgFor, NzMenuItemComponent, ], }) export class PresetWrapperComponent extends FieldWrapper implements OnInit, OnDestroy { public searchResults: Preset[] = []; // the list of presets shown in the dropdown public presetMenuVisible = false; private searchTerm: string = ""; // a copy of the form field value, used as a search term to narrow suggested presets private presetType: string = ""; // corresponds to type used in presetService.getPresets(type, target). Usually "operator" private saveTarget: string = ""; // corresponds to target used in presetService.getPresets(type, target). Usually operator type, i.e. "MySQLSource" private applyTarget: string = ""; // corresponds to target used in presetService.applyPreset(type, target). Usually operatorID, i.e "MySQLSource-operator-8fb88f81-1bb1-4b00-bbd1-3d2f23c5e1d7" private basePreset: Preset = {}; private teardownObservable: ReplaySubject = new ReplaySubject(1); // observable used OnDestroy to tear down subscriptions that takeUntil(teardownObservable) constructor( private presetService: PresetService, private messageService: NzMessageService ) { super(); } ngOnInit(): void { if ( this.field.key === undefined || this.field.templateOptions === undefined || this.field.templateOptions.presetKey === undefined ) { throw Error( `form preset-wrapper field ${this.field} doesn't contain necessary .key and .templateOptions.presetKey attributes` ); } const presetKey = this.field.templateOptions.presetKey; this.searchTerm = this.formControl.value !== null ? this.formControl.value : ""; this.presetType = presetKey.presetType; this.saveTarget = presetKey.saveTarget; this.applyTarget = presetKey.applyTarget; this.updateSearchResults(); this.basePreset = this.filterPresetFromForm(); this.handleSavePresets(); // handles when presets for this saveTarget change this.handleApplyPreset(); // handles when presets for this saveTarget change this.handleFieldValueChanges(); // handles updating search results as the user types } /** * applies preset using PresetService.savePresetsStream event/observable system * @param preset */ public applyPreset(preset: Preset) { this.presetService.applyPreset(this.presetType, this.applyTarget, preset); } public deletePreset(preset: Preset) { this.presetService.deletePreset( this.presetType, this.saveTarget, preset, `Deleted preset: ${this.getEntryTitle(preset)}`, "error" ); } /** * Generates title for dropdown menu entries * @param preset to generate title for * @returns title */ public getEntryTitle(preset: Preset): string { return preset[asType(this.field.key, "string")].toString(); } /** * Generates description of dropdown menu entries * @param preset to generate description of * @returns description */ public getEntryDescription(preset: Preset): string { return Object.keys(preset) .filter(key => key !== asType(this.field.key, "string")) .map(key => preset[key]) .join(", "); } /** * Filters a Preset[], allowing only getEntryTitle(Preset) that start with searchTerm * @param presets Preset[] * @param searchTerm string * @param showAllResults whether or not to filter presets or allow all presets * @returns */ public getSearchResults(presets: Readonly, searchTerm: string, showAllResults: boolean): Preset[] { if (showAllResults) { return presets.slice(); } else { return presets.filter(preset => this.getEntryTitle(preset) .replace(/^\s+|\s+$/g, "") .toLowerCase() .startsWith(searchTerm.toLowerCase()) ); } } /** * updates search results when dropdown is activated (clicking form field opens dropdown) * @param visible Event value, whether or not dropdown is visible */ public onDropdownVisibilityEvent(visible: boolean) { if (visible) { this.updateSearchResults(); } } /** * called when service is destroyed by angular. * tears down subscriptions that takeUntil(teardownObservable) */ public ngOnDestroy() { this.teardownObservable.next(true); this.teardownObservable.complete(); } public savePreset() { const preset = this.filterPresetFromForm(); if (this.presetService.isValidPreset(preset)) { this.presetService.createPreset( this.presetType, this.saveTarget, // this.basePreset, preset ); } else { this.messageService.error("Preset not saved: Fill out all preset fields."); } } /** * handles when presets for the current presetType are changed due to saving new presets * updates search results to account for new presets */ private handleSavePresets() { this.presetService.savePresetsStream .pipe( filter(presets => presets.type === this.presetType && presets.target === this.saveTarget), takeUntil(this.teardownObservable) ) .subscribe({ next: saveEvent => { this.searchResults = this.getSearchResults(saveEvent.presets, this.searchTerm, true); }, }); } /** * handles when presets for the current presetType are changed due to saving new presets * updates search results to account for new presets */ private handleApplyPreset() { this.presetService.applyPresetStream .pipe( filter(presets => presets.type === this.presetType && presets.target === this.applyTarget), takeUntil(this.teardownObservable) ) .subscribe({ next: applyEvent => { this.basePreset = applyEvent.preset; }, }); } /** * Filters formData to only include members that are in the preset schema of the given operatorType * @returns partially finished Preset. use PresetService.isValidOperatorPreset to verify all preset attributes exist */ filterPresetFromForm(): Preset { let preset: Preset = {}; let arr = this.field.parent?.fieldGroup?.filter(formfield => formfield.wrappers?.includes("preset-wrapper")); (arr as FormlyFieldConfig[]).forEach(field => { const key = asType(field.key, "string"); preset[key] = field.model[key]; }); return preset; } /** * handles user typing into form field * updates earch results to account for filtering */ private handleFieldValueChanges() { // WIERD CODE EXPLANATION: debounceTime(0)? // After a preset is applied (by clicking a dropdown entry), it changes a field value and // activates this handler function. // updating the searchResults (which also changes the HTML template due to binding) too quickly // can sometimes interrupt the dropdown closing animation. (dropdown should close after clicking dropdown entry, but instead stays open) // hence the debounceTime(0) to slow this function down. this.formControl.valueChanges.pipe(debounceTime(0), takeUntil(this.teardownObservable)).subscribe({ next: (value: string | number | boolean) => { this.searchTerm = (value ?? "").toString(); if (this.presetMenuVisible) { this.updateSearchResults(false); } }, }); } /** * updates search results */ private updateSearchResults(showAllResults = true) { this.presetService .getPresets(this.presetType, this.saveTarget) .pipe(first(), takeUntil(this.teardownObservable)) .subscribe(presets => { this.searchResults = this.getSearchResults(presets, this.searchTerm, showAllResults); }); } /** * setup FormlyFieldConfig to use PresetWrapperComponent: * adds preset-wrapper and form-field (default wrapper) as wrappers * @param config FormlyFieldConfig to setup * @param presetType corresponds to type used in presetService.getPresets(type, target). Usually "operator" * @param saveTarget corresponds to target used in presetService.getPresets(type, target). Usually operator type, i.e. "MySQLSource" * @param applyTarget corresponds to target used in presetService.applyPreset(type, target). Usually operatorID, i.e "MySQLSource-operator-8fb88f81-1bb1-4b00-bbd1-3d2f23c5e1d7" */ public static setupFieldConfig( config: FormlyFieldConfig, presetType: string, saveTarget: string, applyTarget: string ) { const fieldConfig: FormlyFieldConfig = { wrappers: ["form-field", "preset-wrapper"], // wrap form field in default theme and then preset wrapper templateOptions: { presetKey: { presetType: presetType, saveTarget: saveTarget, applyTarget: applyTarget, }, // disable browser's default autocomplete to not block our preset autocomplete attributes: { autocomplete: "off", }, }, }; merge(config, fieldConfig); } } ================================================ FILE: frontend/src/app/common/formly/repeat-dnd/repeat-dnd.component.css ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .dnd-row { display: flex; align-items: flex-start; gap: 8px; padding: 8px 0; border: none; border-bottom: 1px solid #f0f0f0; background: none; } .dnd-field-wrapper { display: flex; align-items: flex-start; gap: 8px; flex-wrap: wrap; order: 2; } .dnd-field { flex: 0 1 auto; width: 150px; } .dnd-remove-button { margin-left: 0; margin-top: 28px; order: 1; } .drag-handle { cursor: move; color: #888; margin-top: 32px; order: 0; } .drag-handle:hover { color: #333; } :host ::ng-deep .dnd-field .ant-form-item { margin-bottom: 0 !important; } ================================================ FILE: frontend/src/app/common/formly/repeat-dnd/repeat-dnd.component.html ================================================
    ================================================ FILE: frontend/src/app/common/formly/repeat-dnd/repeat-dnd.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component } from "@angular/core"; import { FieldArrayType, FormlyModule } from "@ngx-formly/core"; import { CdkDragDrop, moveItemInArray, CdkDropList, CdkDrag, CdkDragHandle } from "@angular/cdk/drag-drop"; import { NgFor } from "@angular/common"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; @Component({ selector: "texera-formly-repeat-section-dnd", templateUrl: "./repeat-dnd.component.html", styleUrls: ["./repeat-dnd.component.css"], imports: [ CdkDropList, NgFor, CdkDrag, CdkDragHandle, ɵNzTransitionPatchDirective, NzIconDirective, FormlyModule, NzSpaceCompactItemDirective, NzButtonComponent, NzWaveDirective, ], }) export class FormlyRepeatDndComponent extends FieldArrayType { onDrop(event: CdkDragDrop) { if (!this.model || event.previousIndex === event.currentIndex) { return; } // 1. Reorder the data model. This is the source of truth for the backend. moveItemInArray(this.model, event.previousIndex, event.currentIndex); // 2. Reorder the Formly field configurations. This keeps the UI definition in sync with the data. moveItemInArray(this.field.fieldGroup!, event.previousIndex, event.currentIndex); // 3. Reorder the actual Angular FormArray controls. This keeps the live form state in sync. const control = this.formControl.at(event.previousIndex); this.formControl.removeAt(event.previousIndex); this.formControl.insert(event.currentIndex, control); // 4. Notify the parent to save the changes. The parent should NOT redraw the form. if (this.props.reorder) { this.props.reorder(); } } } ================================================ FILE: frontend/src/app/common/service/blob-error-http-interceptor.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http"; import { Observable, throwError } from "rxjs"; import { catchError } from "rxjs/operators"; @Injectable() export class BlobErrorHttpInterceptor implements HttpInterceptor { public intercept(req: HttpRequest, next: HttpHandler): Observable> { return next.handle(req).pipe( catchError((err: unknown) => { if (err instanceof HttpErrorResponse && err.error instanceof Blob && err.error.type === "application/json") { // https://github.com/angular/angular/issues/19888 // When request of type Blob, the error is also in Blob instead of object of the json data return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e: Event) => { try { const errmsg = JSON.parse((e.target).result); reject( new HttpErrorResponse({ error: errmsg, headers: err.headers, status: err.status, statusText: err.statusText, url: err.url !== null ? err.url : undefined, }) ); } catch (_) { reject(err); } }; reader.onerror = _ => { reject(err); }; reader.readAsText(err.error); }); } return throwError(err); }) ); } } ================================================ FILE: frontend/src/app/common/service/computing-unit/computing-unit-actions/computing-unit-actions.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { Observable } from "rxjs"; import { NzModalService } from "ng-zorro-antd/modal"; import { ShareAccessComponent } from "../../../../dashboard/component/user/share-access/share-access.component"; import { WorkflowComputingUnitManagingService } from "../workflow-computing-unit/workflow-computing-unit-managing.service"; import { DashboardWorkflowComputingUnit, WorkflowComputingUnitType } from "../../../type/workflow-computing-unit"; import { NotificationService } from "../../notification/notification.service"; import { unitTypeMessageTemplate } from "../../../util/computing-unit.util"; import { ComputingUnitStatusService } from "../computing-unit-status/computing-unit-status.service"; import { extractErrorMessage } from "../../../util/error"; export interface StartComputingUnitRequest { type: WorkflowComputingUnitType; name: string; cpu: string; memory: string; gpu: string; jvmMemorySize: string; shmSize: string; localUri: string; } @Injectable({ providedIn: "root", }) export class ComputingUnitActionsService { constructor( private modalService: NzModalService, private computingUnitService: WorkflowComputingUnitManagingService, private notificationService: NotificationService, private computingUnitStatusService: ComputingUnitStatusService ) {} openShareAccessModal(cuid: number, inWorkspace: boolean = true): void { this.modalService.create({ nzContent: ShareAccessComponent, nzData: { writeAccess: true, type: "computing-unit", id: cuid, inWorkspace, }, nzFooter: null, nzTitle: "Share this computing unit with others", nzCentered: true, nzWidth: "800px", }); } create(request: StartComputingUnitRequest): Observable { if (request.type === "kubernetes") { return this.computingUnitService.createKubernetesBasedComputingUnit( request.name, request.cpu, request.memory, request.gpu, request.jvmMemorySize, request.shmSize ); } if (request.type === "local") { return this.computingUnitService.createLocalComputingUnit(request.name, request.localUri); } throw new Error("Unsupported computing unit type"); } confirmAndTerminate(cuid: number, unit: DashboardWorkflowComputingUnit): void { if (!unit.computingUnit.uri) { this.notificationService.error("Invalid computing unit."); return; } const unitName = unit.computingUnit.name; const unitType = unit?.computingUnit.type || "kubernetes"; // fallback const templates = unitTypeMessageTemplate[unitType]; // Show confirmation modal this.modalService.confirm({ nzTitle: templates.terminateTitle, nzContent: templates.terminateWarning ? `

    Are you sure you want to terminate ${unitName}?

    ${templates.terminateWarning} ` : `

    Are you sure you want to disconnect from ${unitName}?

    `, nzOkText: unitType === "local" ? "Disconnect" : "Terminate", nzOkType: "primary", nzOnOk: () => { // Use the ComputingUnitStatusService to handle termination // This will properly close the websocket before terminating the unit this.computingUnitStatusService.terminateComputingUnit(cuid).subscribe({ next: (success: boolean) => { if (success) { this.notificationService.success(`Terminated Computing Unit: ${unitName}`); } else { this.notificationService.error("Failed to terminate computing unit"); } }, error: (err: unknown) => { this.notificationService.error(`Failed to terminate computing unit: ${extractErrorMessage(err)}`); }, }); }, nzCancelText: "Cancel", }); } } ================================================ FILE: frontend/src/app/common/service/computing-unit/computing-unit-status/computing-unit-status.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable, OnDestroy } from "@angular/core"; import { BehaviorSubject, interval, Observable, of, Subject, Subscription } from "rxjs"; import { catchError, distinctUntilChanged, filter, map, switchMap, take, tap } from "rxjs/operators"; import { DashboardWorkflowComputingUnit } from "../../../type/workflow-computing-unit"; import { WorkflowComputingUnitManagingService } from "../workflow-computing-unit/workflow-computing-unit-managing.service"; import { WorkflowWebsocketService } from "../../../../workspace/service/workflow-websocket/workflow-websocket.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { ComputingUnitState } from "../../../type/computing-unit-connection.interface"; import { isDefined } from "../../../util/predicate"; import { WorkflowStatusService } from "../../../../workspace/service/workflow-status/workflow-status.service"; import { UserService } from "../../user/user.service"; /** * Service that manages and provides access to computing unit status information * across the application. * * This service is agnostic to whether the computing unit manager is enabled or not. * In local mode, it will provide a default local computing unit with status based on websocket connection. */ @UntilDestroy() @Injectable({ providedIn: "root", }) export class ComputingUnitStatusService implements OnDestroy { // Behavior subjects to track and broadcast state changes private selectedUnitSubject = new BehaviorSubject(null); private readonly allComputingUnitsSubject = new BehaviorSubject([]); private readonly refreshComputingUnitListSignal = new Subject(); // Refresh interval in milliseconds private readonly REFRESH_INTERVAL_MS = 2000; private refreshSubscription: Subscription | null = null; private currentConnectedCuid?: number; private currentConnectedWid?: number; private selectedUnitPoll?: Subscription; constructor( private computingUnitService: WorkflowComputingUnitManagingService, private workflowWebsocketService: WorkflowWebsocketService, private workflowStatusService: WorkflowStatusService, private userService: UserService ) { // Initialize the service by loading computing units this.initializeService(); // Monitor websocket connection status this.monitorConnectionStatus(); } // Initialize the service with available computing units private initializeService(): void { this.computingUnitService .listComputingUnits() .pipe(untilDestroyed(this)) .subscribe(units => { this.setComputingUnitsState(units); }); // Set up periodic refresh this.startRefreshInterval(); } public refreshComputingUnitList(): void { this.refreshComputingUnitListSignal.next(); } private startPollingSelectedUnit(cuid: number): void { // cancel previous poll, if any this.selectedUnitPoll?.unsubscribe(); this.selectedUnitPoll = interval(this.REFRESH_INTERVAL_MS) .pipe( // each tick → get fresh data for *this* cuid switchMap(() => this.computingUnitService.getComputingUnit(cuid)), untilDestroyed(this) ) .subscribe(unit => { this.updateUnitInList(unit); }); // merge into cache } private stopPollingSelectedUnit(): void { this.selectedUnitPoll?.unsubscribe(); this.selectedUnitPoll = undefined; } // Update computing units list and the selected unit private setComputingUnitsState(units: DashboardWorkflowComputingUnit[]): void { this.allComputingUnitsSubject.next(units); const updatedSelectedUnit = units.find( unit => unit.computingUnit.cuid === this.selectedUnitSubject.value?.computingUnit.cuid ); if (updatedSelectedUnit) { this.selectedUnitSubject.next(updatedSelectedUnit); } else if (this.selectedUnitSubject.value) { // The selected unit is no longer in the list this.selectedUnitSubject.next(null); this.stopPollingSelectedUnit(); } } // Monitor the connection status of the websocket service private monitorConnectionStatus(): void { this.workflowWebsocketService // use websocket’s native stream .getConnectionStatusStream() .pipe( distinctUntilChanged(), // react only to real changes untilDestroyed(this) ) .subscribe(isConnected => { this.refreshComputingUnitList(); }); } // Start the interval to refresh computing unit data private startRefreshInterval(): void { if (this.refreshSubscription) { this.refreshSubscription.unsubscribe(); } this.refreshSubscription = this.refreshComputingUnitListSignal .pipe( switchMap(() => this.computingUnitService.listComputingUnits()), untilDestroyed(this) ) .subscribe(units => { this.setComputingUnitsState(units); }); } // /** * Select a computing unit **by its CUID** and emit the updated selection. */ public selectComputingUnit(wid: number | undefined, cuid: number): void { const trySelect = (unit: DashboardWorkflowComputingUnit) => { // open websocket if needed const shouldReconnect = this.currentConnectedCuid !== cuid || this.currentConnectedWid !== wid; if (isDefined(wid) && shouldReconnect) { if (this.workflowWebsocketService.isConnected) { this.workflowWebsocketService.closeWebsocket(); this.workflowStatusService.clearStatus(); } this.workflowWebsocketService.openWebsocket(wid, this.userService.getCurrentUser()?.uid, cuid); this.currentConnectedCuid = cuid; this.currentConnectedWid = wid; this.selectedUnitSubject.next(unit); this.startPollingSelectedUnit(cuid); } }; // try immediate lookup in the current cache const cachedUnit = this.allComputingUnitsSubject.value.find(u => u.computingUnit.cuid === cuid); if (cachedUnit) { trySelect(cachedUnit); return; } // otherwise trigger a refresh and wait until the unit appears once this.refreshComputingUnitList(); this.allComputingUnitsSubject .pipe( filter(units => units.some(u => u.computingUnit.cuid === cuid)), take(1), untilDestroyed(this) ) .subscribe(units => { const unit = units.find(u => u.computingUnit.cuid === cuid)!; trySelect(unit); }); } // Observable for the currently selected computing unit public getSelectedComputingUnit(): Observable { return this.selectedUnitSubject.asObservable(); } // Observable for all available computing units public getAllComputingUnits(): Observable { return this.allComputingUnitsSubject; } // Get the current status of the selected computing unit as string public getStatus(): Observable { return this.selectedUnitSubject.pipe( map((unit: DashboardWorkflowComputingUnit | null) => { if (!unit) { return ComputingUnitState.NoComputingUnit; } // Convert string status to enum switch (unit.status) { case "Running": return ComputingUnitState.Running; case "Pending": return ComputingUnitState.Pending; default: return ComputingUnitState.Pending; } }) ); } // Clean up on service destroy ngOnDestroy(): void { this.refreshSubscription?.unsubscribe(); this.selectedUnitPoll?.unsubscribe(); this.selectedUnitSubject.complete(); this.allComputingUnitsSubject.complete(); } /** * Helper method to update a single unit in the units list */ private updateUnitInList(updatedUnit: DashboardWorkflowComputingUnit): void { const merged: DashboardWorkflowComputingUnit[] = this.allComputingUnitsSubject.value.map(u => u.computingUnit.cuid === updatedUnit.computingUnit.cuid ? updatedUnit : u ); this.setComputingUnitsState(merged); } /** * Terminate a computing unit, ensuring websocket is closed first * @param cuid The ID of the computing unit to terminate * @returns Observable that completes when the termination process is done */ public terminateComputingUnit(cuid: number): Observable { const isSelected = this.selectedUnitSubject.value?.computingUnit.cuid === cuid; if (isSelected && this.workflowWebsocketService.isConnected) { this.workflowWebsocketService.closeWebsocket(); this.workflowStatusService.clearStatus(); } return this.computingUnitService.terminateComputingUnit(cuid).pipe( tap(() => { // trigger a single refresh; the refresh pipeline will // pull the new list and call updateComputingUnits() this.refreshComputingUnitList(); }), map(() => true), catchError((err: unknown) => { return of(false); }), take(1) // complete after first emission ); } /** * Get the current selected computing unit value synchronously */ public getSelectedComputingUnitValue(): DashboardWorkflowComputingUnit | null { return this.selectedUnitSubject.value; } } ================================================ FILE: frontend/src/app/common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { DashboardWorkflowComputingUnit } from "../../../type/workflow-computing-unit"; import { Observable, of } from "rxjs"; @Injectable() export class MockComputingUnitStatusService { listComputingUnits(): Observable { return of([]); } getSelectedComputingUnit(): Observable { return of(null); } getSelectedComputingUnitValue(): DashboardWorkflowComputingUnit | null { return null; } getAllComputingUnits(): Observable { return of([]); } selectComputingUnit(): void {} startPolling(): void {} stopPolling(): void {} } ================================================ FILE: frontend/src/app/common/service/computing-unit/workflow-computing-unit/workflow-computing-unit-managing.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { Observable } from "rxjs"; import { AppSettings } from "../../../app-setting"; import { DashboardWorkflowComputingUnit, WorkflowComputingUnit, WorkflowComputingUnitResourceLimit, WorkflowComputingUnitType, } from "../../../type/workflow-computing-unit"; import { map } from "rxjs/operators"; export const COMPUTING_UNIT_BASE_URL = "computing-unit"; export const COMPUTING_UNIT_CREATE_URL = `${COMPUTING_UNIT_BASE_URL}/create`; export const COMPUTING_UNIT_LIST_URL = `${COMPUTING_UNIT_BASE_URL}`; export const COMPUTING_UNIT_TYPES_URL = `${COMPUTING_UNIT_BASE_URL}/types`; @Injectable({ providedIn: "root", }) export class WorkflowComputingUnitManagingService { constructor(private http: HttpClient) {} /** Ensure the `resource` field is parsed into an object. */ private parseDashboardUnit = (raw: DashboardWorkflowComputingUnit): DashboardWorkflowComputingUnit => { const cu = raw.computingUnit as WorkflowComputingUnit & { resource: string | WorkflowComputingUnitResourceLimit; }; if (typeof cu.resource === "string") { try { cu.resource = JSON.parse(cu.resource) as WorkflowComputingUnitResourceLimit; } catch { // fall back to an empty object, so the UI never crashes cu.resource = { cpuLimit: "NaN", memoryLimit: "NaN", gpuLimit: "NaN", jvmMemorySize: "NaN", shmSize: "NaN", nodeAddresses: [], }; } } return { ...raw, computingUnit: cu }; }; /** * Create a new workflow computing unit (pod). * @param name The name for the computing unit. * @param cpuLimit The cpu resource limit for the computing unit. * @param memoryLimit The memory resource limit for the computing unit. * @param gpuLimit The gpu resource limit for the computing unit. * @param jvmMemorySize The JVM memory size (e.g. "1G", "2G") * @param unitType The type of computing unit (e.g. "local", "kubernetes") * @param shmSize The shared memory size * @param uri The URI of the local computing unit; for kubernetes-based computing units, this is not used in the backend. * @returns An Observable of the created WorkflowComputingUnit. */ private createComputingUnit( name: string, cpuLimit: string, memoryLimit: string, gpuLimit: string, jvmMemorySize: string, shmSize: string, uri: string, unitType: "kubernetes" | "local" ): Observable { const body = { name, cpuLimit, memoryLimit, gpuLimit, jvmMemorySize, shmSize, uri, unitType }; return this.http .post(`${AppSettings.getApiEndpoint()}/${COMPUTING_UNIT_CREATE_URL}`, body) .pipe(map(raw => this.parseDashboardUnit(raw))); } /** * Create a new Kubernetes-based workflow computing unit. * * @param name The name for the computing unit. * @param cpuLimit The cpu resource limit for the computing unit. * @param memoryLimit The memory resource limit for the computing unit. * @param gpuLimit The gpu resource limit for the computing unit. * @param jvmMemorySize The JVM memory size (e.g. "1G", "2G") * @param shmSize The shared memory size * @returns An Observable of the created WorkflowComputingUnit. */ public createKubernetesBasedComputingUnit( name: string, cpuLimit: string, memoryLimit: string, gpuLimit: string, jvmMemorySize: string, shmSize: string ): Observable { return this.createComputingUnit(name, cpuLimit, memoryLimit, gpuLimit, jvmMemorySize, shmSize, "", "kubernetes"); } /** * Create a new local workflow computing unit. * * @param name The name of the computing unit. * @param uri The URI of the local computing unit. * @returns An Observable of the created WorkflowComputingUnit. */ public createLocalComputingUnit(name: string, uri: string): Observable { return this.createComputingUnit(name, "NaN", "NaN", "NaN", "NaN", "NaN", uri, "local"); } /** * Terminate a computing unit (pod) by its URI. * @returns An Observable of the server response. * @param cuid */ public terminateComputingUnit(cuid: number): Observable { return this.http.delete(`${AppSettings.getApiEndpoint()}/${COMPUTING_UNIT_BASE_URL}/${cuid}/terminate`); } /** * Fetch the list of available CPU and memory limit options. * @returns An Observable containing both CPU and memory limit options. */ public getComputingUnitLimitOptions(): Observable<{ cpuLimitOptions: string[]; memoryLimitOptions: string[]; gpuLimitOptions: string[]; }> { return this.http.get<{ cpuLimitOptions: string[]; memoryLimitOptions: string[]; gpuLimitOptions: string[]; }>(`${AppSettings.getApiEndpoint()}/${COMPUTING_UNIT_BASE_URL}/limits`); } /** * Fetch the list of supported computing unit types. * @returns An Observable containing the available computing unit types. */ public getComputingUnitTypes(): Observable<{ typeOptions: WorkflowComputingUnitType[]; }> { return this.http.get<{ typeOptions: WorkflowComputingUnitType[]; }>(`${AppSettings.getApiEndpoint()}/${COMPUTING_UNIT_TYPES_URL}`); } /** * List all active computing units. * @returns An Observable of a list of DashboardWorkflowComputingUnit. */ public listComputingUnits(): Observable { return this.http .get(`${AppSettings.getApiEndpoint()}/${COMPUTING_UNIT_LIST_URL}`) .pipe(map(arr => arr.map(unit => this.parseDashboardUnit(unit)))); } public getComputingUnit(cuid: number): Observable { return this.http .get(`${AppSettings.getApiEndpoint()}/${COMPUTING_UNIT_BASE_URL}/${cuid}`) .pipe(map(raw => this.parseDashboardUnit(raw))); } /** * Rename a computing unit. * @param cuid The ID of the computing unit to rename. * @param name The new name for the computing unit. * @returns An Observable of the server response. */ public renameComputingUnit(cuid: number, name: string): Observable { return this.http.put( `${AppSettings.getApiEndpoint()}/${COMPUTING_UNIT_BASE_URL}/${cuid}/rename/${encodeURIComponent(name)}`, {} ); } } ================================================ FILE: frontend/src/app/common/service/gui-config.service.mock.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { Observable, of } from "rxjs"; import { GuiConfig } from "../type/gui-config"; import { ExecutionMode } from "../type/workflow"; /** * Mock GuiConfigService for testing purposes. * Provides default configuration values without requiring HTTP calls. */ @Injectable() export class MockGuiConfigService { private _config: GuiConfig = { exportExecutionResultEnabled: false, autoAttributeCorrectionEnabled: false, selectingFilesFromDatasetsEnabled: false, localLogin: true, googleLogin: true, inviteOnly: false, userPresetEnabled: true, workflowExecutionsTrackingEnabled: false, linkBreakpointEnabled: false, asyncRenderingEnabled: false, timetravelEnabled: false, productionSharedEditingServer: false, pythonLanguageServerPort: "3000", defaultDataTransferBatchSize: 100, defaultExecutionMode: ExecutionMode.PIPELINED, workflowEmailNotificationEnabled: false, sharingComputingUnitEnabled: false, operatorConsoleMessageBufferSize: 1000, defaultLocalUser: { username: "", password: "" }, expirationTimeInMinutes: 2880, activeTimeInMinutes: 15, copilotEnabled: false, limitColumns: 15, }; get env(): GuiConfig { return this._config; } load(): Observable { return of(this._config); } setConfig(config: Partial): void { this._config = { ...this._config, ...config }; } } ================================================ FILE: frontend/src/app/common/service/gui-config.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { GuiConfig } from "../type/gui-config"; import { catchError, forkJoin, map, Observable, tap, throwError } from "rxjs"; import { AppSettings } from "../app-setting"; @Injectable({ providedIn: "root" }) export class GuiConfigService { private config!: GuiConfig; constructor(private http: HttpClient) {} load(): Observable { // Fetch both GUI config and user system config in parallel const guiConfig$ = this.http.get>(`${AppSettings.getApiEndpoint()}/config/gui`); const userSystemConfig$ = this.http.get<{ inviteOnly: boolean }>( `${AppSettings.getApiEndpoint()}/config/user-system` ); return forkJoin([guiConfig$, userSystemConfig$]).pipe( map(([guiConfig, userSystemConfig]) => { // Merge both configurations return { ...guiConfig, ...userSystemConfig, } as GuiConfig; }), tap(config => { this.config = config; console.log("GUI configuration loaded successfully from backend"); }), catchError((error: unknown) => { console.error("Failed to load GUI configuration:", error); return throwError(() => new Error(`Failed to load GUI configuration from backend: ${error}`)); }) ); } get env(): GuiConfig { if (!this.config) { throw new Error("GUI configuration not loaded yet. Make sure load() is called during app initialization"); } return this.config; } } ================================================ FILE: frontend/src/app/common/service/notification/notification.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { TestBed } from "@angular/core/testing"; import { NotificationService } from "./notification.service"; describe("NotificationService", () => { let service: NotificationService; beforeEach(() => { TestBed.configureTestingModule({}); service = TestBed.inject(NotificationService); }); it("should be created", () => { expect(service).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/common/service/notification/notification.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { NzMessageDataOptions, NzMessageService } from "ng-zorro-antd/message"; import { NzNotificationDataOptions, NzNotificationService } from "ng-zorro-antd/notification"; /** * NotificationService is an entry service for sending notifications */ @Injectable({ providedIn: "root", }) export class NotificationService { constructor( private message: NzMessageService, private notification: NzNotificationService ) {} // Only blank can be removed manually blank(title: string, content: string, options: NzNotificationDataOptions = {}): void { this.notification.blank(title, content, options); } // Remove current blank notification only remove(): void { this.notification.remove(); } success(message: string, options: NzMessageDataOptions = {}) { this.message.success(message, options); } info(message: string, options: NzMessageDataOptions = {}) { this.message.info(message, options); } error(message: string, options: NzMessageDataOptions = {}) { this.message.error(message, options); } warning(message: string, options: NzMessageDataOptions = {}) { this.message.warning(message, options); } loading(message: string, options: NzMessageDataOptions = {}) { return this.message.loading(message, options); } } ================================================ FILE: frontend/src/app/common/service/user/auth-guard.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router"; import { GuiConfigService } from "../gui-config.service"; import { UserService } from "./user.service"; import { DASHBOARD_ABOUT } from "../../../app-routing.constant"; /** * AuthGuardService is a service can tell the router whether * it should allow navigation to a requested route. */ @Injectable() export class AuthGuardService implements CanActivate { constructor( private userService: UserService, private router: Router, private config: GuiConfigService ) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { if (this.userService.isLogin()) { return true; } else { this.router.navigate([DASHBOARD_ABOUT], { queryParams: { returnUrl: state.url === "/" ? null : state.url } }); return false; } } } ================================================ FILE: frontend/src/app/common/service/user/auth.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { firstValueFrom, Observable, Subscription, timer } from "rxjs"; import { AppSettings } from "../../app-setting"; import { Role, User } from "../../type/user"; import { ignoreElements } from "rxjs/operators"; import { JwtHelperService } from "@auth0/angular-jwt"; import { NotificationService } from "../notification/notification.service"; import { GmailService } from "../gmail/gmail.service"; import { GuiConfigService } from "../gui-config.service"; import { NzModalService } from "ng-zorro-antd/modal"; import { RegistrationRequestModalComponent } from "./registration-request-modal/registration-request-modal.component"; export const TOKEN_KEY = "access_token"; /** * User Service contains the function of registering and logging the user. * It will save the user account inside for future use. * * @author Adam */ @Injectable({ providedIn: "root", }) export class AuthService { public static readonly LOGIN_ENDPOINT = "auth/login"; public static readonly REFRESH_TOKEN = "auth/refresh"; public static readonly REGISTER_ENDPOINT = "auth/register"; public static readonly GOOGLE_LOGIN_ENDPOINT = "auth/google/login"; private tokenExpirationSubscription?: Subscription; constructor( private http: HttpClient, private jwtHelperService: JwtHelperService, private notificationService: NotificationService, private gmailService: GmailService, private config: GuiConfigService, private modal: NzModalService ) {} /** * This method will handle the request for user registration. * It will automatically login, save the user account inside and trigger userChangeEvent when success * @param username * @param password */ public register(username: string, password: string): Observable> { return this.http.post>( `${AppSettings.getApiEndpoint()}/${AuthService.REGISTER_ENDPOINT}`, { username, password, } ); } /** * This method will handle the request for Google login. * It will automatically login, save the user account inside and trigger userChangeEvent when success */ public googleAuth(credential: string): Observable> { return this.http.post>( `${AppSettings.getApiEndpoint()}/${AuthService.GOOGLE_LOGIN_ENDPOINT}`, credential, { headers: { "Content-Type": "text/plain", Accept: "application/json", }, } ); } /** * This method will handle the request for user login. * It will automatically login, save the user account inside and trigger userChangeEvent when success * @param username * @param password */ public auth(username: string, password: string): Observable> { return this.http.post>( `${AppSettings.getApiEndpoint()}/${AuthService.LOGIN_ENDPOINT}`, { username, password } ); } /** * this method will clear the saved user account and trigger userChangeEvent */ public logout(): undefined { AuthService.removeAccessToken(); this.tokenExpirationSubscription?.unsubscribe(); return undefined; } public loginWithExistingToken(): User | undefined { this.tokenExpirationSubscription?.unsubscribe(); const token = AuthService.getAccessToken(); if (token == null) { return this.logout(); } if (this.jwtHelperService.isTokenExpired(token)) { this.notificationService.error("Access token is expired!"); return this.logout(); } const role = this.jwtHelperService.decodeToken(token).role; const uid = this.jwtHelperService.decodeToken(token).userId; const email = this.jwtHelperService.decodeToken(token).email; const name = this.jwtHelperService.decodeToken(token).sub; if (this.config.env.inviteOnly && role === Role.INACTIVE) { this.checkRegistrationRequired(uid).subscribe(required => { if (required) { this.openRegistrationModal(uid, email, name); } else { this.modal.info({ nzTitle: "Access Pending", nzContent: ` Your account is still inactive, and we already received your request. Please wait for an admin to approve your access. `, nzOkText: "OK", nzMaskClosable: false, nzClosable: false, nzOnOk: () => { this.logout(); return true; }, }); } }); return this.logout(); } this.registerAutoLogout(); return { uid: this.jwtHelperService.decodeToken(token).userId, name: this.jwtHelperService.decodeToken(token).sub, email: email, googleId: this.jwtHelperService.decodeToken(token).googleId, googleAvatar: this.jwtHelperService.decodeToken(token).googleAvatar, role: role, comment: this.jwtHelperService.decodeToken(token).comment, joiningReason: this.jwtHelperService.decodeToken(token).joiningReason, }; } private registerAutoLogout() { this.tokenExpirationSubscription?.unsubscribe(); const expirationTime = this.jwtHelperService.getTokenExpirationDate()?.getTime(); const token = AuthService.getAccessToken(); if (token !== null && !this.jwtHelperService.isTokenExpired(token) && expirationTime !== undefined) { // In RxJS 7, timer emits immediately then completes. Using ignoreElements() suppresses // the emitted value so the complete callback fires only after the specified delay. this.tokenExpirationSubscription = timer(expirationTime - new Date().getTime()) .pipe(ignoreElements()) .subscribe({ complete: () => this.logout() }); } } static setAccessToken(token: string): void { localStorage.setItem(TOKEN_KEY, token); } static getAccessToken(): string | null { return localStorage.getItem(TOKEN_KEY); } static removeAccessToken(): void { localStorage.removeItem(TOKEN_KEY); } /** * Returns true if the system needs to prompt the user with the registration form * @param uid * @private */ private checkRegistrationRequired(uid: number): Observable { return this.http.get(`${AppSettings.getApiEndpoint()}/user/joining-reason/required`, { params: { uid: uid.toString() }, }); } /** * Submits changes to the backend with affiliation and joining reason * @param uid * @param affiliation * @param reason * @private */ private submitRegistration(uid: number, affiliation: string, reason: string): Observable { return this.http.put(`${AppSettings.getApiEndpoint()}/user/joining-reason`, { uid, affiliation, joiningReason: reason, }); } /** * Opens the registration modal (registration request modal) * @param uid * @param email * @param defaultName * @private */ private openRegistrationModal(uid: number, email: string, defaultName: string): void { const modalRef = this.modal.create({ nzContent: RegistrationRequestModalComponent, nzData: { uid, email, name: defaultName }, nzOkText: "Send request to Admin", nzCancelText: "Cancel", nzMaskClosable: false, nzClosable: false, nzOnOk: async () => { const comp = modalRef.getContentComponent(); const { affiliation, reason } = comp.getValues(); if (!reason) { this.notificationService.error("Reason is required"); return false; } try { await firstValueFrom(this.submitRegistration(uid, affiliation, reason)); this.gmailService.notifyUnauthorizedLogin(email, affiliation, reason); } finally { this.logout(); } return true; }, nzOnCancel: () => this.logout(), }); const comp = modalRef.getContentComponent(); modalRef.updateConfig({ nzTitle: comp.modalTitle, }); } } ================================================ FILE: frontend/src/app/common/service/user/config/user-config.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing"; import { TestBed } from "@angular/core/testing"; import { AppSettings } from "src/app/common/app-setting"; import { UserConfigService, UserConfig } from "./user-config.service"; import { UserService } from "../user.service"; import { StubUserService, MOCK_USER } from "../stub-user.service"; describe("UserConfigService", () => { let service: UserConfigService; let stubUserService: StubUserService; let httpMock: HttpTestingController; const endpoint = `${AppSettings.getApiEndpoint()}/${UserConfigService.USER_DICTIONARY_ENDPOINT}`; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [{ provide: UserService, useClass: StubUserService }, UserConfigService], }); stubUserService = TestBed.inject(UserService) as unknown as StubUserService; service = TestBed.inject(UserConfigService); httpMock = TestBed.inject(HttpTestingController); // The constructor calls fetchAll() because StubUserService starts logged in. // Flush the request with an empty dictionary so each test starts from a clean slate. httpMock.expectOne(endpoint).flush({}); }); afterEach(() => { httpMock.verify(); }); it("should be created", () => { expect(service).toBeTruthy(); }); it("starts with an empty local dictionary after the initial fetch", () => { expect(service.getDict()).toEqual({}); }); describe("fetchAll", () => { it("issues a GET to the config endpoint and replaces the local dictionary", () => { const observable = service.fetchAll(); const req = httpMock.expectOne(endpoint); expect(req.request.method).toEqual("GET"); const payload: UserConfig = { foo: "1", bar: "2" }; req.flush(payload); observable.subscribe(value => expect(value).toEqual(payload)); expect(service.getDict()).toEqual(payload); }); it("notifies dictionaryChanged subscribers when the dictionary is replaced", () => { const next = vi.fn(); const sub = (service as any).dictionaryChangedSubject.subscribe(next); service.fetchAll(); httpMock.expectOne(endpoint).flush({ k: "v" }); expect(next).toHaveBeenCalledTimes(1); sub.unsubscribe(); }); it("throws when the user is not logged in", () => { stubUserService.user = undefined; expect(() => service.fetchAll()).toThrowError("user not logged in"); }); }); describe("fetchKey", () => { it("issues a GET to the per-key endpoint and merges the value into the local dict", () => { const observable = service.fetchKey("alpha"); const req = httpMock.expectOne(`${endpoint}/alpha`); expect(req.request.method).toEqual("GET"); expect(req.request.responseType).toEqual("text"); req.flush("one"); observable.subscribe(value => expect(value).toEqual("one")); expect(service.getDict()).toEqual({ alpha: "one" }); }); it("notifies dictionaryChanged subscribers only when the value actually changes", () => { const next = vi.fn(); const sub = (service as any).dictionaryChangedSubject.subscribe(next); service.fetchKey("alpha"); httpMock.expectOne(`${endpoint}/alpha`).flush("one"); expect(next).toHaveBeenCalledTimes(1); service.fetchKey("alpha"); httpMock.expectOne(`${endpoint}/alpha`).flush("one"); expect(next).toHaveBeenCalledTimes(1); sub.unsubscribe(); }); it("throws when the user is not logged in", () => { stubUserService.user = undefined; expect(() => service.fetchKey("alpha")).toThrowError("user not logged in"); }); it("throws when given an empty key", () => { expect(() => service.fetchKey(" ")).toThrowError(/key cannot be empty/); }); }); describe("set", () => { it("issues a PUT with the value as the body and updates the local dict", () => { service.set("alpha", "one"); const req = httpMock.expectOne(`${endpoint}/alpha`); expect(req.request.method).toEqual("PUT"); expect(req.request.body).toEqual("one"); req.flush(null); expect(service.getDict()).toEqual({ alpha: "one" }); }); it("does not refire dictionaryChanged when setting the same value twice", () => { service.set("alpha", "one"); httpMock.expectOne(`${endpoint}/alpha`).flush(null); const next = vi.fn(); const sub = (service as any).dictionaryChangedSubject.subscribe(next); service.set("alpha", "one"); httpMock.expectOne(`${endpoint}/alpha`).flush(null); expect(next).not.toHaveBeenCalled(); sub.unsubscribe(); }); it("throws when the user is not logged in", () => { stubUserService.user = undefined; expect(() => service.set("alpha", "one")).toThrowError("user not logged in"); }); it("throws when given an empty key", () => { expect(() => service.set(" ", "one")).toThrowError(/key cannot be empty/); }); }); describe("delete", () => { beforeEach(() => { service.set("alpha", "one"); httpMock.expectOne(`${endpoint}/alpha`).flush(null); }); it("issues a DELETE to the per-key endpoint and removes the entry from the local dict", () => { service.delete("alpha"); const req = httpMock.expectOne(`${endpoint}/alpha`); expect(req.request.method).toEqual("DELETE"); req.flush(null); expect(service.getDict()).toEqual({}); }); it("is a no-op (no HTTP request) when the key is not present in the local dict", () => { service.delete("missing"); httpMock.expectNone(`${endpoint}/missing`); }); it("throws when the user is not logged in", () => { stubUserService.user = undefined; expect(() => service.delete("alpha")).toThrowError("user not logged in"); }); it("throws when given an empty key", () => { expect(() => service.delete("")).toThrowError(/key cannot be empty/); }); }); describe("user-change reactions", () => { it("re-fetches the dictionary when a logged-in user is emitted on userChanged", () => { stubUserService.userChangeSubject.next(MOCK_USER); const req = httpMock.expectOne(endpoint); expect(req.request.method).toEqual("GET"); req.flush({ rehydrated: "yes" }); expect(service.getDict()).toEqual({ rehydrated: "yes" }); }); it("clears the local dictionary when the user logs out", () => { service.set("alpha", "one"); httpMock.expectOne(`${endpoint}/alpha`).flush(null); expect(service.getDict()).toEqual({ alpha: "one" }); stubUserService.user = undefined; stubUserService.userChangeSubject.next(undefined); expect(service.getDict()).toEqual({}); httpMock.expectNone(endpoint); }); }); }); ================================================ FILE: frontend/src/app/common/service/user/config/user-config.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable, of, Subject } from "rxjs"; import { AppSettings } from "src/app/common/app-setting"; import { UserService } from "../user.service"; import { shareReplay, tap } from "rxjs/operators"; export type UserConfig = { [key: string]: string; }; @Injectable({ providedIn: "root", }) export class UserConfigService { public static readonly USER_DICTIONARY_ENDPOINT = "user/config"; private dictionaryChangedSubject = new Subject(); private localUserDictionary: UserConfig = {}; constructor( private http: HttpClient, private userService: UserService ) { if (this.userService.isLogin()) { this.fetchAll(); } this.userService.userChanged().subscribe(() => { if (this.userService.isLogin()) { this.fetchAll(); } else { this.updateDict({}); } }); } public getDict(): Readonly { return this.localUserDictionary; } /** * get a value from the backend. * keys and values must be strings. * @param key string key that uniquely identifies a value * @returns string value corresponding to the key from the backend; * throws Error("No such entry") (invalid key) or Error("Invalid session") (not logged in). */ public fetchKey(key: string): Observable { if (!this.userService.isLogin()) { throw new Error("user not logged in"); } if (key.trim().length === 0) { throw new Error("Dictionary Service: key cannot be empty"); } const url = `${AppSettings.getApiEndpoint()}/${UserConfigService.USER_DICTIONARY_ENDPOINT}/${key}`; const req = this.http.get(url, { responseType: "text" }).pipe( tap(res => { this.updateEntry(key, res); }), shareReplay(1) ); req.subscribe(); // causes post request to be sent regardless caller's subscription return req; } /** * get the entire dictionary from the backend. * @returns UserDictionary object with string attributes; */ public fetchAll(): Observable> { if (!this.userService.isLogin()) { throw new Error("user not logged in"); } const url = `${AppSettings.getApiEndpoint()}/${UserConfigService.USER_DICTIONARY_ENDPOINT}`; const req = this.http.get(url).pipe( tap(res => this.updateDict(res)), shareReplay(1) ); req.subscribe(); // causes post request to be sent regardless caller's subscription return req; } /** * saves or updates (if it already exists) an entry (key-value pair) on the backend. * keys and values must be strings. * @param key string key that uniquely identifies a value * @param value string value corresponding to the key from the backend * @returns observable indicating the backend has been successfully updated */ public set(key: string, value: string): Observable { if (!this.userService.isLogin()) { throw new Error("user not logged in"); } if (key.trim().length === 0) { throw new Error("Dictionary Service: key cannot be empty"); } const url = `${AppSettings.getApiEndpoint()}/${UserConfigService.USER_DICTIONARY_ENDPOINT}/${key}`; const req = this.http.put(url, value).pipe( tap(_ => this.updateEntry(key, value)), shareReplay(1) ); req.subscribe(); return req; } /** * delete a value from the backend. * keys and values must be strings. * @param key string key that uniquely identifies a value * @returns observable indicating the backend has been successfully updated */ public delete(key: string): Observable { if (!this.userService.isLogin()) { throw new Error("user not logged in"); } if (key.trim().length === 0) { throw new Error("Dictionary Service: key cannot be empty"); } if (!(key in this.localUserDictionary)) { return of(); } const url = `${AppSettings.getApiEndpoint()}/${UserConfigService.USER_DICTIONARY_ENDPOINT}/${key}`; const req = this.http.delete(url).pipe( tap(_ => this.updateEntry(key, undefined)), shareReplay(1) ); req.subscribe(); return req; } private updateEntry(key: string, value: string | undefined) { if (key.trim().length === 0) { throw new Error("Dictionary Service: key cannot be empty"); } if (value === undefined) { if (key in this.localUserDictionary) { delete this.localUserDictionary[key]; this.dictionaryChangedSubject.next(); } } else { if (this.localUserDictionary[key] !== value) { this.localUserDictionary[key] = value; this.dictionaryChangedSubject.next(); } } } private updateDict(newDict: UserConfig) { this.localUserDictionary = newDict; this.dictionaryChangedSubject.next(); } } ================================================ FILE: frontend/src/app/common/service/user/google-auth.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { Observable } from "rxjs"; import { HttpClient } from "@angular/common/http"; import { AppSettings } from "../../app-setting"; @Injectable({ providedIn: "root", }) export class GoogleAuthService { constructor(private http: HttpClient) {} getClientId(): Observable { return this.http.get(`${AppSettings.getApiEndpoint()}/auth/google/clientid`, { responseType: "text" }); } } ================================================ FILE: frontend/src/app/common/service/user/registration-request-modal/registration-request-modal.component.html ================================================
    Request access

    Please provide the information below so an administrator can review your request.

    ================================================ FILE: frontend/src/app/common/service/user/registration-request-modal/registration-request-modal.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .registration-modal-title { display: flex; align-items: center; justify-content: space-between; padding-right: 12px; } .registration-modal-logo { height: 28px; margin-left: 12px; opacity: 0.9; } ================================================ FILE: frontend/src/app/common/service/user/registration-request-modal/registration-request-modal.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, Inject, Input, TemplateRef, ViewChild } from "@angular/core"; import { NZ_MODAL_DATA } from "ng-zorro-antd/modal"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzInputDirective } from "ng-zorro-antd/input"; import { FormsModule } from "@angular/forms"; @Component({ selector: "texera-registration-request-modal", templateUrl: "./registration-request-modal.component.html", styleUrls: ["./registration-request-modal.component.scss"], imports: [NzSpaceCompactItemDirective, NzInputDirective, FormsModule], }) // Component for registration form modal export class RegistrationRequestModalComponent { name = ""; email = ""; affiliation = ""; reason = ""; @ViewChild("modalTitle", { static: true }) modalTitle!: TemplateRef; constructor(@Inject(NZ_MODAL_DATA) public data: { uid: number; email: string; name: string }) { this.name = data?.name ?? ""; this.email = data?.email ?? ""; } getValues() { return { affiliation: (this.affiliation ?? "").trim(), reason: (this.reason ?? "").trim(), }; } } ================================================ FILE: frontend/src/app/common/service/user/stub-auth.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { Observable, of } from "rxjs"; import { User } from "../../type/user"; import { PublicInterfaceOf } from "../../util/stub"; import { AuthService } from "./auth.service"; import { MOCK_USER } from "./stub-user.service"; export const MOCK_TOKEN = { accessToken: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJZaWNvbmcgSHVhbmciLCJ1c2VySWQiOjMsImV4cCI6OTk5OTk5OTk5OX0.aAM9pw_qIBs0EjD5hiCGHR4GEe2YPXPVenceJ3zaU_g", }; export const MOCK_INVALID_TOKEN = { accessToken: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJZaWNvbmcgSHVhbmciLCJ1c2VySWQiOjMsImV4cCI6MTYzNTEyODc2OX0.L3e93VQx91RMXpoN4sjtXXoX2llXQoEpCYd44oYftSQ", }; /** * This StubUserService is to test other service's functionality that depends on UserService * It will correctly emit UserChangedEvent as the normal UserService do. */ @Injectable() export class StubAuthService implements PublicInterfaceOf { auth(username: string, password: string): Observable> { if (password === "password") { return of(MOCK_TOKEN); } else { return of(MOCK_INVALID_TOKEN); } } googleAuth(): Observable> { return of(MOCK_TOKEN); } loginWithExistingToken(): User | undefined { if (AuthService.getAccessToken() === MOCK_TOKEN.accessToken) { return MOCK_USER; } else { return undefined; } } logout(): undefined { return undefined; } register(username: string, password: string): Observable> { if (username !== "existing_user") { return of(MOCK_TOKEN); } else { return of(MOCK_INVALID_TOKEN); } } validateUsername(username: string): { result: boolean; message: string } { return { message: "", result: false }; } } ================================================ FILE: frontend/src/app/common/service/user/stub-user.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { Observable, of, Subject } from "rxjs"; import { Role, User } from "../../type/user"; import { UserService } from "./user.service"; import { PublicInterfaceOf } from "../../util/stub"; export const MOCK_USER_ID = 1; export const MOCK_USER_NAME = "testUser"; export const MOCK_USER_EMAIL = "testUser@testemail.com"; export const MOCK_USER_COMMENT = "testComent"; export const MOCK_USER_JOININGREASON = "testJoiningReason"; export const MOCK_USER = { uid: MOCK_USER_ID, name: MOCK_USER_NAME, email: MOCK_USER_EMAIL, googleId: undefined, role: Role.REGULAR, comment: MOCK_USER_COMMENT, joiningReason: MOCK_USER_JOININGREASON, }; /** * This StubUserService is to test other service's functionality that depends on UserService * It will correctly emit UserChangedEvent as the normal UserService do. */ @Injectable() export class StubUserService implements PublicInterfaceOf { public userChangeSubject: Subject = new Subject(); public user: User | undefined; constructor() { this.user = MOCK_USER; this.userChangeSubject.next(this.user); } googleLogin(): Observable { throw new Error("Method not implemented."); } isLogin(): boolean { return this.user !== undefined; } isAdmin(): boolean { return this.user?.role === Role.ADMIN; } login(username: string, password: string): Observable { return of(); } logout(): void {} register(username: string, password: string): Observable { return of(); } userChanged(): Observable { return this.userChangeSubject.asObservable(); } getCurrentUser(): User | undefined { return this.user; } getAvatar(googleAvatar: string): Observable { return of(undefined); } checkAffiliation(): Observable { return of(true); } updateAffiliation(_affiliation: string): Observable { return of(void 0); } } ================================================ FILE: frontend/src/app/common/service/user/user.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import "zone.js/testing"; import { fakeAsync, TestBed, tick } from "@angular/core/testing"; import { UserService } from "./user.service"; import { AuthService } from "./auth.service"; import { StubAuthService } from "./stub-auth.service"; import { skip } from "rxjs/operators"; import { commonTestProviders } from "../../testing/test-utils"; import { HttpClientTestingModule } from "@angular/common/http/testing"; describe("UserService", () => { let service: UserService; beforeEach(() => { AuthService.removeAccessToken(); TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [UserService, { provide: AuthService, useClass: StubAuthService }, ...commonTestProviders], }); service = TestBed.inject(UserService); }); afterAll(() => { TestBed.resetTestingModule(); }); it("should be created", () => { expect(service).toBeTruthy(); }); it("should login after register user", () => { expect((service as any).currentUser).toBeFalsy(); service .userChanged() .pipe(skip(1)) .subscribe(user => expect(user).toBeTruthy()); service.register("test", "password").subscribe(() => { expect((service as any).currentUser).toBeTruthy(); }); }); it("should login after login user", () => { expect((service as any).currentUser).toBeFalsy(); service .userChanged() .pipe(skip(1)) .subscribe(user => expect(user).toBeTruthy()); service.login("test", "password").subscribe(() => { expect((service as any).currentUser).toBeTruthy(); }); }); it("should not login after register failed", () => { expect((service as any).currentUser).toBeFalsy(); service .userChanged() .pipe(skip(1)) .subscribe(user => expect(user).toBeFalsy()); service.register("existing_user", "password").subscribe(() => { expect((service as any).currentUser).toBeFalsy(); }); }); it("should not login after login failed", () => { expect((service as any).currentUser).toBeFalsy(); service .userChanged() .pipe(skip(1)) .subscribe(user => expect(user).toBeFalsy()); service.login("test", "wrong_password").subscribe(() => { expect((service as any).currentUser).toBeFalsy(); }); }); it("should log out when called log out function", fakeAsync(() => { expect((service as any).currentUser).toBeFalsy(); service .userChanged() .pipe(skip(2)) .subscribe(user => expect(user).toBeFalsy()); service.login("test", "password").subscribe(() => { expect((service as any).currentUser).toBeTruthy(); tick(10); service.logout(); tick(10); expect((service as any).currentUser).toBeFalsy(); }); })); }); ================================================ FILE: frontend/src/app/common/service/user/user.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { AppSettings } from "../../app-setting"; import { Observable, of, ReplaySubject } from "rxjs"; import { Role, User } from "../../type/user"; import { AuthService } from "./auth.service"; import { GuiConfigService } from "../gui-config.service"; import { catchError, map, shareReplay } from "rxjs/operators"; /** * User Service manages User information. It relies on different * auth services to authenticate a valid User. */ @Injectable({ providedIn: "root", }) export class UserService { private currentUser?: User = undefined; private userChangeSubject: ReplaySubject = new ReplaySubject(1); private cache = new Map(); private readonly cacheDuration = 3600 * 1000; // cache duration: 1h constructor( private authService: AuthService, private config: GuiConfigService, private http: HttpClient ) { const user = this.authService.loginWithExistingToken(); this.changeUser(user); } public getCurrentUser(): User | undefined { return this.currentUser; } public login(username: string, password: string): Observable { // validate the credentials with backend return this.authService .auth(username, password) .pipe(map(({ accessToken }) => this.handleAccessToken(accessToken))); } public googleLogin(credential: string): Observable { return this.authService.googleAuth(credential).pipe(map(({ accessToken }) => this.handleAccessToken(accessToken))); } public isLogin(): boolean { return this.currentUser !== undefined; } public isAdmin(): boolean { return this.currentUser?.role === Role.ADMIN; } public userChanged(): Observable { return this.userChangeSubject.asObservable(); } public logout(): void { this.authService.logout(); this.changeUser(undefined); } public register(username: string, password: string): Observable { return this.authService .register(username, password) .pipe(map(({ accessToken }) => this.handleAccessToken(accessToken))); } /** * changes the current user and triggers currentUserSubject * @param user */ private changeUser(user: User | undefined): void { if (user) { const hue = Math.floor(Math.random() * 360); // Hue (0-360) const sat = Math.floor(60 + Math.random() * 20); // Saturation (60%-80%) const light = 50; // Lightness (50%) this.currentUser = { ...user, color: `hsl(${hue}, ${sat}%, ${light}%)` }; } else { this.currentUser = user; } this.userChangeSubject.next(this.currentUser); } private handleAccessToken(accessToken: string): void { AuthService.setAccessToken(accessToken); this.changeUser(this.authService.loginWithExistingToken()); } /** * check the given parameter is legal for login/registration * @param username */ static validateUsername(username: string): { result: boolean; message: string } { if (username.trim().length === 0) { return { result: false, message: "Username should not be empty." }; } return { result: true, message: "Username frontend validation success." }; } getAvatar(googleAvatar: string): Observable { if (!googleAvatar) return of(undefined); const cached = this.cache.get(googleAvatar); if (cached) { if (Date.now() <= cached.expiry) { return of(cached.url); } else { URL.revokeObjectURL(cached.url); this.cache.delete(googleAvatar); } } const url = `https://lh3.googleusercontent.com/a/${googleAvatar}`; return this.fetchBlob(url).pipe( map(blob => { const blobUrl = URL.createObjectURL(blob); this.cache.set(googleAvatar, { url: blobUrl, expiry: Date.now() + this.cacheDuration, }); return blobUrl; }), catchError(() => of(undefined)), shareReplay(1) ); } private fetchBlob(url: string): Observable { return new Observable(observer => { fetch(url, { referrerPolicy: "no-referrer" }) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.blob(); }) .then(blob => observer.next(blob)) .catch(error => observer.error(error)); }); } } ================================================ FILE: frontend/src/app/common/service/workflow-persist/stub-workflow-persist.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { Observable } from "rxjs"; import { DashboardWorkflow } from "../../../dashboard/type/dashboard-workflow.interface"; import { Workflow } from "../../type/workflow"; import { SearchFilterParameters, searchTestEntries } from "../../../dashboard/type/search-filter-parameters"; import { DashboardEntry } from "../../../dashboard/type/dashboard-entry"; export const WORKFLOW_BASE_URL = "workflow"; export const WORKFLOW_SEARCH_URL = WORKFLOW_BASE_URL + "/search"; @Injectable() export class StubWorkflowPersistService { constructor(private testWorkflows: DashboardEntry[]) {} public retrieveWorkflow(wid: number): Observable { return new Observable(observer => observer.next(this.testWorkflows.find(w => w.workflow.workflow.wid == wid)?.workflow.workflow) ); } public searchWorkflows(keywords: string[], params: SearchFilterParameters): Observable { return new Observable(observer => { return observer.next(searchTestEntries(keywords, params, this.testWorkflows, "workflow").map(i => i.workflow)); }); } /** * retrieves all workflow owners */ public retrieveOwners(): Observable { const names = this.testWorkflows.filter(i => i).map(i => i.workflow.ownerName) as string[]; return new Observable(observer => { observer.next([...new Set(names)]); }); } /** * retrieves all workflow IDs */ public retrieveWorkflowIDs(): Observable { return new Observable(observer => { observer.next(this.testWorkflows.map(i => i.workflow.workflow.wid as number).filter(i => i)); }); } } ================================================ FILE: frontend/src/app/common/service/workflow-persist/workflow-persist.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { TestBed } from "@angular/core/testing"; import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing"; import { WorkflowPersistService } from "./workflow-persist.service"; import { jsonCast } from "../../util/storage"; import { WorkflowContent } from "../../type/workflow"; import { last } from "rxjs/operators"; describe("WorkflowPersistService", () => { let service: WorkflowPersistService; let httpTestingController: HttpTestingController; const testContent = '{"operators":[{"operatorID":"Limit-operator-a11370eb-940a-4f10-8b36-8b413b2396c9",' + '"operatorType":"Limit","operatorProperties":{"limit":2},"inputPorts":[{"portID":"input-0","displayName":""}],' + '"outputPorts":[{"portID":"output-0","displayName":null}],"showAdvanced":false},' + '{"operatorID":"SimpleSink-operator-e4a77a32-e3c9-4c40-a26d-a1aa103cc914","operatorType":"SimpleSink",' + '"operatorProperties":{},"inputPorts":[{"portID":"input-0","displayName":""}],"outputPorts":[],' + '"showAdvanced":false},{"operatorID":"MySQLSource-operator-1ee619b1-8884-4564-a136-29ef77dfcc50",' + '"operatorType":"MySQLSource","operatorProperties":{"port":"default","search":false,"progressive":false,' + '"min":"auto","max":"auto","interval":1000000000,"host":"localhost"},"inputPorts":[],' + '"outputPorts":[{"portID":"output-0","displayName":""}],"showAdvanced":false}],' + '"operatorPositions":{"Limit-operator-a11370eb-940a-4f10-8b36-8b413b2396c9":{"x":200,"y":212},' + '"SimpleSink-operator-e4a77a32-e3c9-4c40-a26d-a1aa103cc914":{"x":392,"y":218},' + '"MySQLSource-operator-1ee619b1-8884-4564-a136-29ef77dfcc50":{"x":36,"y":214}},' + '"links":[{"linkID":"link-ea977a06-3ef5-4c80-b31a-4013cfb8321d",' + '"source":{"operatorID":"Limit-operator-a11370eb-940a-4f10-8b36-8b413b2396c9","portID":"output-0"},' + '"target":{"operatorID":"SimpleSink-operator-e4a77a32-e3c9-4c40-a26d-a1aa103cc914","portID":"input-0"}},' + '{"linkID":"link-c94e24a6-2c77-40cf-ba22-1a7ffba64b7d","source":{"operatorID":' + '"MySQLSource-operator-1ee619b1-8884-4564-a136-29ef77dfcc50","portID":"output-0"},"target":' + '{"operatorID":"Limit-operator-a11370eb-940a-4f10-8b36-8b413b2396c9","portID":"input-0"}}],"breakpoints":{}}'; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], }); service = TestBed.inject(WorkflowPersistService); httpTestingController = TestBed.inject(HttpTestingController); }); it("should be created", () => { expect(service).toBeTruthy(); }); it("should send http post request once", () => { service .createWorkflow(jsonCast(testContent), "testname") .pipe(last()) .subscribe(value => { expect(value).toBeTruthy(); }); httpTestingController.expectOne(request => request.method === "POST"); }); it("should check if workflow content and name returned correctly", () => { service .createWorkflow(jsonCast(testContent), "testname") .pipe(last()) .subscribe(value => { expect(value.workflow.name).toEqual("testname_copy"); expect(value.workflow.content).toEqual(jsonCast(testContent)); }); }); }); ================================================ FILE: frontend/src/app/common/service/workflow-persist/workflow-persist.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { HttpClient, HttpParams } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable, throwError } from "rxjs"; import { catchError, filter, map } from "rxjs/operators"; import { AppSettings } from "../../app-setting"; import { Workflow, WorkflowContent } from "../../type/workflow"; import { DashboardWorkflow } from "../../../dashboard/type/dashboard-workflow.interface"; import { WorkflowUtilService } from "../../../workspace/service/workflow-graph/util/workflow-util.service"; import { NotificationService } from "../notification/notification.service"; import { SearchFilterParameters, toQueryStrings } from "../../../dashboard/type/search-filter-parameters"; import { User } from "../../type/user"; import { checkIfWorkflowBroken } from "../../util/workflow-check"; export const WORKFLOW_BASE_URL = "workflow"; export const WORKFLOW_PERSIST_URL = WORKFLOW_BASE_URL + "/persist"; export const WORKFLOW_LIST_URL = WORKFLOW_BASE_URL + "/list"; export const WORKFLOW_SEARCH_URL = WORKFLOW_BASE_URL + "/search"; export const WORKFLOW_CREATE_URL = WORKFLOW_BASE_URL + "/create"; export const WORKFLOW_DUPLICATE_URL = WORKFLOW_BASE_URL + "/duplicate"; export const WORKFLOW_DELETE_URL = WORKFLOW_BASE_URL + "/delete"; export const WORKFLOW_UPDATENAME_URL = WORKFLOW_BASE_URL + "/update/name"; export const WORKFLOW_UPDATEDESCRIPTION_URL = WORKFLOW_BASE_URL + "/update/description"; export const WORKFLOW_OWNER_URL = WORKFLOW_BASE_URL + "/user-workflow-owners"; export const WORKFLOW_ID_URL = WORKFLOW_BASE_URL + "/user-workflow-ids"; export const WORKFLOW_OWNER_NAME = WORKFLOW_BASE_URL + "/owner_name"; export const WORKFLOW_NAME = WORKFLOW_BASE_URL + "/workflow_name"; export const WORKFLOW_PUBLIC_WORKFLOW = WORKFLOW_BASE_URL + "/publicised"; export const WORKFLOW_DESCRIPTION = WORKFLOW_BASE_URL + "/workflow_description"; export const WORKFLOW_USER_ACCESS = WORKFLOW_BASE_URL + "/workflow_user_access"; export const WORKFLOW_SIZE = WORKFLOW_BASE_URL + "/size"; export const DEFAULT_WORKFLOW_NAME = "Untitled workflow"; @Injectable({ providedIn: "root", }) export class WorkflowPersistService { // flag to disable workflow persist when displaying the read only particular version private workflowPersistFlag = true; constructor( private http: HttpClient, private notificationService: NotificationService ) {} /** * persists a workflow to backend database and returns its updated information (e.g., new wid) * @param workflow */ public persistWorkflow(workflow: Workflow): Observable { if (checkIfWorkflowBroken(workflow)) { this.notificationService.error( "Sorry! The workflow is broken and cannot be persisted. Please contact the system admin." ); } return this.http .post(`${AppSettings.getApiEndpoint()}/${WORKFLOW_PERSIST_URL}`, { wid: workflow.wid, name: workflow.name, description: workflow.description, content: JSON.stringify(workflow.content), isPublic: workflow.isPublished, }) .pipe( filter((updatedWorkflow: Workflow) => updatedWorkflow != null), map(WorkflowUtilService.parseWorkflowInfo) ); } /** * creates a workflow and insert it to backend database and return its information * @param newWorkflowName * @param newWorkflowContent */ public createWorkflow( newWorkflowContent: WorkflowContent, newWorkflowName: string = DEFAULT_WORKFLOW_NAME ): Observable { return this.http .post(`${AppSettings.getApiEndpoint()}/${WORKFLOW_CREATE_URL}`, { name: newWorkflowName, content: JSON.stringify(newWorkflowContent), }) .pipe(filter((createdWorkflow: DashboardWorkflow) => createdWorkflow != null)); } /** * creates a workflow and insert it to backend database and return its information * @param targetWids * @param pid */ public duplicateWorkflow(targetWids: number[], pid?: number): Observable { return this.http .post(`${AppSettings.getApiEndpoint()}/${WORKFLOW_DUPLICATE_URL}`, { wids: targetWids, ...(pid !== undefined && { pid }), }) .pipe(filter((createdWorkflows: DashboardWorkflow[]) => createdWorkflows != null && createdWorkflows.length > 0)); } /** * retrieves a workflow from backend database given its id. The user in the session must have access to the workflow. * @param wid */ public retrieveWorkflow(wid: number): Observable { return this.http.get(`${AppSettings.getApiEndpoint()}/${WORKFLOW_BASE_URL}/${wid}`).pipe( filter((workflow: Workflow) => workflow != null), map(WorkflowUtilService.parseWorkflowInfo) ); } private makeRequestAndFormatWorkflowResponse(url: string): Observable { return this.http.get(url).pipe( map((dashboardWorkflowEntries: DashboardWorkflow[]) => dashboardWorkflowEntries.map((workflowEntry: DashboardWorkflow) => { return { ...workflowEntry, dashboardWorkflowEntry: WorkflowUtilService.parseWorkflowInfo(workflowEntry.workflow), }; }) ) ); } /** * retrieves a list of workflows from backend database that belongs to the user in the session. */ public retrieveWorkflowsBySessionUser(): Observable { return this.makeRequestAndFormatWorkflowResponse(`${AppSettings.getApiEndpoint()}/${WORKFLOW_LIST_URL}`); } /** * Search workflows by a text query from backend database that belongs to the user in the session. */ public searchWorkflows(keywords: string[], params: SearchFilterParameters): Observable { return this.makeRequestAndFormatWorkflowResponse( `${AppSettings.getApiEndpoint()}/${WORKFLOW_SEARCH_URL}?${toQueryStrings(keywords, params)}` ); } /** * deletes the given workflow, the user in the session must own the workflow. */ public deleteWorkflow(wids: number[]): Observable { return this.http.post(`${AppSettings.getApiEndpoint()}/${WORKFLOW_DELETE_URL}`, { wids: wids, }); } /** * updates the name of a given workflow, the user in the session must own the workflow. */ public updateWorkflowName(wid: number, name: string): Observable { return this.http .post(`${AppSettings.getApiEndpoint()}/${WORKFLOW_UPDATENAME_URL}`, { wid: wid, name: name, }) .pipe( catchError((error: unknown) => { // @ts-ignore this.notificationService.error(error.error.message); return throwError(error); }) ); } /** * updates the description of a given workflow */ public updateWorkflowDescription(wid: number, description: string): Observable { return this.http .post(`${AppSettings.getApiEndpoint()}/${WORKFLOW_UPDATEDESCRIPTION_URL}`, { wid: wid, description: description, }) .pipe( catchError((error: unknown) => { // @ts-ignore this.notificationService.error(error.error.message); return throwError(error); }) ); } public getWorkflowIsPublished(wid: number): Observable { return this.http.get(`${AppSettings.getApiEndpoint()}/${WORKFLOW_BASE_URL}/type/${wid}`, { responseType: "text" }); } public updateWorkflowIsPublished(wid: number, isPublished: boolean): Observable { if (isPublished) { return this.http.put(`${AppSettings.getApiEndpoint()}/${WORKFLOW_BASE_URL}/public/${wid}`, null); } else { return this.http.put(`${AppSettings.getApiEndpoint()}/${WORKFLOW_BASE_URL}/private/${wid}`, null); } } public setWorkflowPersistFlag(flag: boolean): void { this.workflowPersistFlag = flag; } public isWorkflowPersistEnabled(): boolean { return this.workflowPersistFlag; } /** * retrieves all workflow owners */ public retrieveOwners(): Observable { return this.http.get(`${AppSettings.getApiEndpoint()}/${WORKFLOW_OWNER_URL}`); } /** * retrieves all workflow IDs */ public retrieveWorkflowIDs(): Observable { return this.http.get(`${AppSettings.getApiEndpoint()}/${WORKFLOW_ID_URL}`); } /** * Retrieve workflow owner name (no login required). * @param wid workflow id */ public getOwnerName(wid: number): Observable { const params = new HttpParams().set("wid", wid); return this.http.get(`${AppSettings.getApiEndpoint()}/${WORKFLOW_OWNER_NAME}`, { params, responseType: "text" }); } /** * retrieve the name of the workflow corresponding to the wid * can be used without logging in * @param wid */ public getWorkflowName(wid: number): Observable { const params = new HttpParams().set("wid", wid); return this.http.get(`${AppSettings.getApiEndpoint()}/${WORKFLOW_NAME}`, { params, responseType: "text" }); } /** * retrieve the complete information of the workflow corresponding to the wid * can be used without logging in * @param wid */ public retrievePublicWorkflow(wid: number): Observable { return this.http.get(`${AppSettings.getApiEndpoint()}/${WORKFLOW_PUBLIC_WORKFLOW}/${wid}`).pipe( filter((workflow: Workflow) => workflow != null), map(WorkflowUtilService.parseWorkflowInfo) ); } /** * retrieve the description of the workflow corresponding to the wid * can be used without logging in * @param wid */ public getWorkflowDescription(wid: number): Observable { const params = new HttpParams().set("wid", wid); return this.http.get(`${AppSettings.getApiEndpoint()}/${WORKFLOW_DESCRIPTION}`, { params, responseType: "text" }); } /** * Batch-fetch the JSON sizes of workflows by their IDs. * Can be used without logging in * * @param wids Array of workflow IDs to query. * @returns An object mapping each workflow ID to its JSON size. */ public getSizes(wids: number[]): Observable> { let params = new HttpParams(); wids.forEach(wid => { params = params.append("wid", wid.toString()); }); return this.http.get>(`${AppSettings.getApiEndpoint()}/${WORKFLOW_SIZE}`, { params }); } } ================================================ FILE: frontend/src/app/common/testing/test-utils.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { HttpClientTestingModule } from "@angular/common/http/testing"; import { Provider } from "@angular/core"; import { GuiConfigService } from "../service/gui-config.service"; import { MockGuiConfigService } from "../service/gui-config.service.mock"; /** * Common test providers that can be used across all spec files */ export const commonTestProviders: Provider[] = [{ provide: GuiConfigService, useClass: MockGuiConfigService }]; /** * Common test module imports */ export const commonTestImports = [HttpClientTestingModule]; ================================================ FILE: frontend/src/app/common/type/computing-unit-connection.interface.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Enum representing the possible states of a computing unit connection. * Used to track the connection status of computing units in the UI. */ export enum ComputingUnitState { Running = "Running", Pending = "Pending", NoComputingUnit = "No Computing Unit", } ================================================ FILE: frontend/src/app/common/type/dataset-file.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // user given filePath is /ownerEmail/datasetName/versionName/fileRelativePath // e.g. /bob@texera.com/twitterDataset/v1/california/irvine/tw1.csv export interface DatasetFile { ownerEmail: string; datasetName: string; versionName: string; fileRelativePath: string; } /** * Parses a file path string to a DatasetFile interface. * @param filePath - The file path string to parse. * @returns The parsed DatasetFile object. */ export function parseFilePathToDatasetFile(filePath: string): DatasetFile { const parts = filePath.split("/").filter(part => part.length > 0); if (parts.length < 4) { throw new Error("Invalid file path format"); } const [ownerEmail, datasetName, versionName, ...fileRelativePathParts] = parts; const fileRelativePath = fileRelativePathParts.join("/"); return { ownerEmail, datasetName, versionName, fileRelativePath, }; } /** * Converts a DatasetFile object to a file path string. * @param datasetFile - The DatasetFile object to convert. * @returns The file path string. */ export function parseDatasetFileToFilePath(datasetFile: DatasetFile): string { const { ownerEmail, datasetName, versionName, fileRelativePath } = datasetFile; return `/${ownerEmail}/${datasetName}/${versionName}/${fileRelativePath}`; } ================================================ FILE: frontend/src/app/common/type/dataset-staged-object.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Represents a staged dataset object change, corresponding to backend Diff export interface DatasetStagedObject { path: string; pathType: "file" | "directory"; diffType: "added" | "removed" | "changed"; sizeBytes?: number; // Optional, only present for files } ================================================ FILE: frontend/src/app/common/type/dataset.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { DatasetFileNode } from "./datasetVersionFileTree"; export interface DatasetVersion { dvid: number | undefined; did: number; creatorUid: number; name: string; versionHash: string | undefined; creationTime: number | undefined; fileNodes: DatasetFileNode[] | undefined; } export interface Dataset { did: number | undefined; ownerUid: number | undefined; name: string; isPublic: boolean; isDownloadable: boolean; storagePath: string | undefined; description: string; creationTime: number | undefined; coverImage: string | undefined; } ================================================ FILE: frontend/src/app/common/type/datasetVersionFileTree.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ export interface DatasetFileNode { name: string; type: "file" | "directory"; children?: DatasetFileNode[]; // Only populated if 'type' is 'directory' parentDir: string; ownerEmail?: string; size?: number; // Only populated if 'type' is 'file' } export function getFullPathFromDatasetFileNode(node: DatasetFileNode): string { return `${node.parentDir}/${node.name}`; } /** * Returns the relative path of a DatasetFileNode by stripping the first three segments. * @param node The DatasetFileNode whose relative path is needed. * @returns The relative path (without the first three segments and without a leading slash). */ export function getRelativePathFromDatasetFileNode(node: DatasetFileNode): string { const fullPath = getFullPathFromDatasetFileNode(node); // Get the full path const pathSegments = fullPath.split("/").filter(segment => segment.length > 0); // Split and remove empty segments if (pathSegments.length <= 3) { return ""; // If there are 3 or fewer segments, return an empty string (no relative path exists) } return pathSegments.slice(3).join("/"); // Join remaining segments as the relative path } export function getPathsUnderOrEqualDatasetFileNode(node: DatasetFileNode): string[] { // Helper function to recursively gather paths const gatherPaths = (node: DatasetFileNode): string[] => { // Base case: if the node is a file, return its path if (node.type === "file") { return [getFullPathFromDatasetFileNode(node)]; } // Recursive case: if the node is a directory, explore its children return node.children ? node.children.flatMap(child => gatherPaths(child)) : []; }; return gatherPaths(node); } // This class convert a list of DatasetVersionTreeNode into a hash map, recursively containing all the paths export class DatasetVersionFileTreeManager { private root: DatasetFileNode = { name: "/", type: "directory", children: [], parentDir: "" }; private treeNodesMap: Map = new Map(); constructor(nodes: DatasetFileNode[] = []) { this.treeNodesMap.set("/", this.root); if (nodes.length > 0) this.initializeWithRootNodes(nodes); } private updateTreeMapWithPath(path: string): DatasetFileNode { const pathParts = path.startsWith("/") ? path.slice(1).split("/") : path.split("/"); let currentPath = "/"; let currentNode = this.root; pathParts.forEach((part, index) => { const previousPath = currentPath; currentPath += part + (index < pathParts.length - 1 ? "/" : ""); // Don't add trailing slash for last part if (!this.treeNodesMap.has(currentPath)) { const isLastPart = index === pathParts.length - 1; const newNode: DatasetFileNode = { name: part, type: isLastPart ? "file" : "directory", parentDir: previousPath.endsWith("/") ? previousPath.slice(0, -1) : previousPath, // Store the full path for parentDir ...(isLastPart ? {} : { children: [] }), // Only add 'children' for directories }; this.treeNodesMap.set(currentPath, newNode); currentNode.children = currentNode.children ?? []; // Ensure 'children' is initialized currentNode.children.push(newNode); } currentNode = this.treeNodesMap.get(currentPath)!; // Get the node for the next iteration }); return currentNode; } private removeNodeAndDescendants(node: DatasetFileNode): void { if (node.type === "directory" && node.children) { node.children.forEach(child => { const childPath = node.parentDir === "/" ? `/${node.name}/${child.name}` : `${node.parentDir}/${node.name}/${child.name}`; this.removeNodeAndDescendants(child); this.treeNodesMap.delete(childPath); // Remove the child from the map }); } // Now that all children are removed, clear the current node's children array node.children = []; } addNodeWithPath(path: string): DatasetFileNode { return this.updateTreeMapWithPath(path); } initializeWithRootNodes(rootNodes: DatasetFileNode[]) { // Clear existing nodes in map except the root this.treeNodesMap.clear(); this.treeNodesMap.set("/", this.root); // Helper function to add nodes recursively const addNodeRecursively = (node: DatasetFileNode, parentDir: string) => { const nodePath = parentDir === "/" ? `/${node.name}` : `${parentDir}/${node.name}`; this.treeNodesMap.set(nodePath, node); // If the node is a directory, recursively add its children if (node.type === "directory" && node.children) { node.children.forEach(child => addNodeRecursively(child, nodePath)); } }; // Add each root node and their children to the tree and map rootNodes.forEach(node => { if (!this.root.children) { this.root.children = []; } this.root.children.push(node); addNodeRecursively(node, "/"); }); } removeNode(targetNode: DatasetFileNode): void { if (targetNode.parentDir === "" && targetNode.name === "/") { // Can't remove root return; } // Queue for BFS const queue: DatasetFileNode[] = [this.root]; while (queue.length > 0) { const node = queue.shift()!; // Check if the current node is the parent of the target node if (node.children && node.children.some(child => child === targetNode)) { // Remove the target node and its descendants this.removeNodeAndDescendants(targetNode); // Remove the target node from the current node's children node.children = node.children.filter(child => child !== targetNode); // Construct the full path of the target node to remove it from the map const pathToRemove = getFullPathFromDatasetFileNode(targetNode); this.treeNodesMap.delete(pathToRemove); return; // Node found and removed, exit the function } // If not found, add the children of the current node to the queue if (node.children) { queue.push(...node.children); } } } removeNodeWithPath(path: string): void { const nodeToRemove = this.treeNodesMap.get(path); if (nodeToRemove) { // First, recursively remove all descendants of the node this.removeNodeAndDescendants(nodeToRemove); // Then, remove the node from its parent's children array const parentNode = this.treeNodesMap.get(nodeToRemove.parentDir); if (parentNode && parentNode.children) { parentNode.children = parentNode.children.filter(child => child.name !== nodeToRemove.name); } // Finally, remove the node from the map this.treeNodesMap.delete(path); } } getRootNodes(): DatasetFileNode[] { return this.root.children ?? []; } } ================================================ FILE: frontend/src/app/common/type/execution.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ export interface Execution { workflowName: string; workflowId: number; userName: string; userId: number; executionId: number; executionStatus: string; executionTime: number; executionName: string; startTime: number; endTime: number; access: boolean; } ================================================ FILE: frontend/src/app/common/type/generic-web-response.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Do not use `const enum` here. Const enums are inlined at compile time and removed * from the emitted JavaScript, which causes runtime errors if the enum is accessed dynamically. */ export enum GenericWebResponseCode { SUCCESS = 0, } export interface GenericWebResponse extends Readonly<{ code: GenericWebResponseCode; message: string; }> {} ================================================ FILE: frontend/src/app/common/type/gui-config.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ExecutionMode } from "./workflow"; // Please refer to core/config/src/main/resources/gui.conf for the definition of each config item export interface GuiConfig { exportExecutionResultEnabled: boolean; autoAttributeCorrectionEnabled: boolean; selectingFilesFromDatasetsEnabled: boolean; localLogin: boolean; googleLogin: boolean; inviteOnly: boolean; userPresetEnabled: boolean; workflowExecutionsTrackingEnabled: boolean; linkBreakpointEnabled: boolean; asyncRenderingEnabled: boolean; timetravelEnabled: boolean; productionSharedEditingServer: boolean; pythonLanguageServerPort: string; defaultDataTransferBatchSize: number; defaultExecutionMode: ExecutionMode; workflowEmailNotificationEnabled: boolean; sharingComputingUnitEnabled: boolean; operatorConsoleMessageBufferSize: number; defaultLocalUser?: { username?: string; password?: string }; expirationTimeInMinutes: number; activeTimeInMinutes: number; copilotEnabled: boolean; limitColumns: number; } export interface SidebarTabs { hub_enabled: boolean; home_enabled: boolean; workflow_enabled: boolean; dataset_enabled: boolean; your_work_enabled: boolean; projects_enabled: boolean; workflows_enabled: boolean; compute_enabled: boolean; datasets_enabled: boolean; quota_enabled: boolean; forum_enabled: boolean; about_enabled: boolean; } ================================================ FILE: frontend/src/app/common/type/physical-plan.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ExecutionIdentity, PhysicalOpIdentity, WorkflowIdentity, } from "./proto/org/apache/texera/amber/core/virtualidentity"; import { PhysicalLink } from "./proto/org/apache/texera/amber/core/workflow"; export interface PhysicalOp { id: PhysicalOpIdentity; workflowId: WorkflowIdentity; executionId: ExecutionIdentity; parallelizable: boolean; isOneToManyOp: boolean; suggestedWorkerNum: number | null; sourceOperator: boolean; sinkOperator: boolean; } export interface PhysicalPlan { operators: Set; links: Set; } ================================================ FILE: frontend/src/app/common/type/proto/google/protobuf/descriptor.ts ================================================ // Code generated by protoc-gen-ts_proto. DO NOT EDIT. // versions: // protoc-gen-ts_proto v2.2.0 // protoc v6.33.4 // source: google/protobuf/descriptor.proto /* eslint-disable */ import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; export const protobufPackage = "google.protobuf"; /** The full set of known editions. */ export enum Edition { /** EDITION_UNKNOWN - A placeholder for an unknown edition value. */ EDITION_UNKNOWN = 0, /** * EDITION_LEGACY - A placeholder edition for specifying default behaviors *before* a feature * was first introduced. This is effectively an "infinite past". */ EDITION_LEGACY = 900, /** * EDITION_PROTO2 - Legacy syntax "editions". These pre-date editions, but behave much like * distinct editions. These can't be used to specify the edition of proto * files, but feature definitions must supply proto2/proto3 defaults for * backwards compatibility. */ EDITION_PROTO2 = 998, EDITION_PROTO3 = 999, /** * EDITION_2023 - Editions that have been released. The specific values are arbitrary and * should not be depended on, but they will always be time-ordered for easy * comparison. */ EDITION_2023 = 1000, EDITION_2024 = 1001, /** EDITION_UNSTABLE - A placeholder edition for developing and testing unscheduled features. */ EDITION_UNSTABLE = 9999, /** * EDITION_1_TEST_ONLY - Placeholder editions for testing feature resolution. These should not be * used or relied on outside of tests. */ EDITION_1_TEST_ONLY = 1, EDITION_2_TEST_ONLY = 2, EDITION_99997_TEST_ONLY = 99997, EDITION_99998_TEST_ONLY = 99998, EDITION_99999_TEST_ONLY = 99999, /** * EDITION_MAX - Placeholder for specifying unbounded edition support. This should only * ever be used by plugins that can expect to never require any changes to * support a new edition. */ EDITION_MAX = 2147483647, UNRECOGNIZED = -1, } export function editionFromJSON(object: any): Edition { switch (object) { case 0: case "EDITION_UNKNOWN": return Edition.EDITION_UNKNOWN; case 900: case "EDITION_LEGACY": return Edition.EDITION_LEGACY; case 998: case "EDITION_PROTO2": return Edition.EDITION_PROTO2; case 999: case "EDITION_PROTO3": return Edition.EDITION_PROTO3; case 1000: case "EDITION_2023": return Edition.EDITION_2023; case 1001: case "EDITION_2024": return Edition.EDITION_2024; case 9999: case "EDITION_UNSTABLE": return Edition.EDITION_UNSTABLE; case 1: case "EDITION_1_TEST_ONLY": return Edition.EDITION_1_TEST_ONLY; case 2: case "EDITION_2_TEST_ONLY": return Edition.EDITION_2_TEST_ONLY; case 99997: case "EDITION_99997_TEST_ONLY": return Edition.EDITION_99997_TEST_ONLY; case 99998: case "EDITION_99998_TEST_ONLY": return Edition.EDITION_99998_TEST_ONLY; case 99999: case "EDITION_99999_TEST_ONLY": return Edition.EDITION_99999_TEST_ONLY; case 2147483647: case "EDITION_MAX": return Edition.EDITION_MAX; case -1: case "UNRECOGNIZED": default: return Edition.UNRECOGNIZED; } } export function editionToJSON(object: Edition): string { switch (object) { case Edition.EDITION_UNKNOWN: return "EDITION_UNKNOWN"; case Edition.EDITION_LEGACY: return "EDITION_LEGACY"; case Edition.EDITION_PROTO2: return "EDITION_PROTO2"; case Edition.EDITION_PROTO3: return "EDITION_PROTO3"; case Edition.EDITION_2023: return "EDITION_2023"; case Edition.EDITION_2024: return "EDITION_2024"; case Edition.EDITION_UNSTABLE: return "EDITION_UNSTABLE"; case Edition.EDITION_1_TEST_ONLY: return "EDITION_1_TEST_ONLY"; case Edition.EDITION_2_TEST_ONLY: return "EDITION_2_TEST_ONLY"; case Edition.EDITION_99997_TEST_ONLY: return "EDITION_99997_TEST_ONLY"; case Edition.EDITION_99998_TEST_ONLY: return "EDITION_99998_TEST_ONLY"; case Edition.EDITION_99999_TEST_ONLY: return "EDITION_99999_TEST_ONLY"; case Edition.EDITION_MAX: return "EDITION_MAX"; case Edition.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } /** * Describes the 'visibility' of a symbol with respect to the proto import * system. Symbols can only be imported when the visibility rules do not prevent * it (ex: local symbols cannot be imported). Visibility modifiers can only set * on `message` and `enum` as they are the only types available to be referenced * from other files. */ export enum SymbolVisibility { VISIBILITY_UNSET = 0, VISIBILITY_LOCAL = 1, VISIBILITY_EXPORT = 2, UNRECOGNIZED = -1, } export function symbolVisibilityFromJSON(object: any): SymbolVisibility { switch (object) { case 0: case "VISIBILITY_UNSET": return SymbolVisibility.VISIBILITY_UNSET; case 1: case "VISIBILITY_LOCAL": return SymbolVisibility.VISIBILITY_LOCAL; case 2: case "VISIBILITY_EXPORT": return SymbolVisibility.VISIBILITY_EXPORT; case -1: case "UNRECOGNIZED": default: return SymbolVisibility.UNRECOGNIZED; } } export function symbolVisibilityToJSON(object: SymbolVisibility): string { switch (object) { case SymbolVisibility.VISIBILITY_UNSET: return "VISIBILITY_UNSET"; case SymbolVisibility.VISIBILITY_LOCAL: return "VISIBILITY_LOCAL"; case SymbolVisibility.VISIBILITY_EXPORT: return "VISIBILITY_EXPORT"; case SymbolVisibility.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } /** * The protocol compiler can output a FileDescriptorSet containing the .proto * files it parses. */ export interface FileDescriptorSet { file: FileDescriptorProto[]; } /** Describes a complete .proto file. */ export interface FileDescriptorProto { /** file name, relative to root of source tree */ name?: | string | undefined; /** e.g. "foo", "foo.bar", etc. */ package?: | string | undefined; /** Names of files imported by this file. */ dependency: string[]; /** Indexes of the public imported files in the dependency list above. */ publicDependency: number[]; /** * Indexes of the weak imported files in the dependency list. * For Google-internal migration only. Do not use. */ weakDependency: number[]; /** * Names of files imported by this file purely for the purpose of providing * option extensions. These are excluded from the dependency list above. */ optionDependency: string[]; /** All top-level definitions in this file. */ messageType: DescriptorProto[]; enumType: EnumDescriptorProto[]; service: ServiceDescriptorProto[]; extension: FieldDescriptorProto[]; options?: | FileOptions | undefined; /** * This field contains optional information about the original source code. * You may safely remove this entire field without harming runtime * functionality of the descriptors -- the information is needed only by * development tools. */ sourceCodeInfo?: | SourceCodeInfo | undefined; /** * The syntax of the proto file. * The supported values are "proto2", "proto3", and "editions". * * If `edition` is present, this value must be "editions". * WARNING: This field should only be used by protobuf plugins or special * cases like the proto compiler. Other uses are discouraged and * developers should rely on the protoreflect APIs for their client language. */ syntax?: | string | undefined; /** * The edition of the proto file. * WARNING: This field should only be used by protobuf plugins or special * cases like the proto compiler. Other uses are discouraged and * developers should rely on the protoreflect APIs for their client language. */ edition?: Edition | undefined; } /** Describes a message type. */ export interface DescriptorProto { name?: string | undefined; field: FieldDescriptorProto[]; extension: FieldDescriptorProto[]; nestedType: DescriptorProto[]; enumType: EnumDescriptorProto[]; extensionRange: DescriptorProto_ExtensionRange[]; oneofDecl: OneofDescriptorProto[]; options?: MessageOptions | undefined; reservedRange: DescriptorProto_ReservedRange[]; /** * Reserved field names, which may not be used by fields in the same message. * A given name may only be reserved once. */ reservedName: string[]; /** Support for `export` and `local` keywords on enums. */ visibility?: SymbolVisibility | undefined; } export interface DescriptorProto_ExtensionRange { /** Inclusive. */ start?: | number | undefined; /** Exclusive. */ end?: number | undefined; options?: ExtensionRangeOptions | undefined; } /** * Range of reserved tag numbers. Reserved tag numbers may not be used by * fields or extension ranges in the same message. Reserved ranges may * not overlap. */ export interface DescriptorProto_ReservedRange { /** Inclusive. */ start?: | number | undefined; /** Exclusive. */ end?: number | undefined; } export interface ExtensionRangeOptions { /** The parser stores options it doesn't recognize here. See above. */ uninterpretedOption: UninterpretedOption[]; /** * For external users: DO NOT USE. We are in the process of open sourcing * extension declaration and executing internal cleanups before it can be * used externally. */ declaration: ExtensionRangeOptions_Declaration[]; /** Any features defined in the specific edition. */ features?: | FeatureSet | undefined; /** * The verification state of the range. * TODO: flip the default to DECLARATION once all empty ranges * are marked as UNVERIFIED. */ verification?: ExtensionRangeOptions_VerificationState | undefined; } /** The verification state of the extension range. */ export enum ExtensionRangeOptions_VerificationState { /** DECLARATION - All the extensions of the range must be declared. */ DECLARATION = 0, UNVERIFIED = 1, UNRECOGNIZED = -1, } export function extensionRangeOptions_VerificationStateFromJSON(object: any): ExtensionRangeOptions_VerificationState { switch (object) { case 0: case "DECLARATION": return ExtensionRangeOptions_VerificationState.DECLARATION; case 1: case "UNVERIFIED": return ExtensionRangeOptions_VerificationState.UNVERIFIED; case -1: case "UNRECOGNIZED": default: return ExtensionRangeOptions_VerificationState.UNRECOGNIZED; } } export function extensionRangeOptions_VerificationStateToJSON(object: ExtensionRangeOptions_VerificationState): string { switch (object) { case ExtensionRangeOptions_VerificationState.DECLARATION: return "DECLARATION"; case ExtensionRangeOptions_VerificationState.UNVERIFIED: return "UNVERIFIED"; case ExtensionRangeOptions_VerificationState.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } export interface ExtensionRangeOptions_Declaration { /** The extension number declared within the extension range. */ number?: | number | undefined; /** * The fully-qualified name of the extension field. There must be a leading * dot in front of the full name. */ fullName?: | string | undefined; /** * The fully-qualified type name of the extension field. Unlike * Metadata.type, Declaration.type must have a leading dot for messages * and enums. */ type?: | string | undefined; /** * If true, indicates that the number is reserved in the extension range, * and any extension field with the number will fail to compile. Set this * when a declared extension field is deleted. */ reserved?: | boolean | undefined; /** * If true, indicates that the extension must be defined as repeated. * Otherwise the extension must be defined as optional. */ repeated?: boolean | undefined; } /** Describes a field within a message. */ export interface FieldDescriptorProto { name?: string | undefined; number?: number | undefined; label?: | FieldDescriptorProto_Label | undefined; /** * If type_name is set, this need not be set. If both this and type_name * are set, this must be one of TYPE_ENUM, TYPE_MESSAGE or TYPE_GROUP. */ type?: | FieldDescriptorProto_Type | undefined; /** * For message and enum types, this is the name of the type. If the name * starts with a '.', it is fully-qualified. Otherwise, C++-like scoping * rules are used to find the type (i.e. first the nested types within this * message are searched, then within the parent, on up to the root * namespace). */ typeName?: | string | undefined; /** * For extensions, this is the name of the type being extended. It is * resolved in the same manner as type_name. */ extendee?: | string | undefined; /** * For numeric types, contains the original text representation of the value. * For booleans, "true" or "false". * For strings, contains the default text contents (not escaped in any way). * For bytes, contains the C escaped value. All bytes >= 128 are escaped. */ defaultValue?: | string | undefined; /** * If set, gives the index of a oneof in the containing type's oneof_decl * list. This field is a member of that oneof. */ oneofIndex?: | number | undefined; /** * JSON name of this field. The value is set by protocol compiler. If the * user has set a "json_name" option on this field, that option's value * will be used. Otherwise, it's deduced from the field's name by converting * it to camelCase. */ jsonName?: string | undefined; options?: | FieldOptions | undefined; /** * If true, this is a proto3 "optional". When a proto3 field is optional, it * tracks presence regardless of field type. * * When proto3_optional is true, this field must belong to a oneof to signal * to old proto3 clients that presence is tracked for this field. This oneof * is known as a "synthetic" oneof, and this field must be its sole member * (each proto3 optional field gets its own synthetic oneof). Synthetic oneofs * exist in the descriptor only, and do not generate any API. Synthetic oneofs * must be ordered after all "real" oneofs. * * For message fields, proto3_optional doesn't create any semantic change, * since non-repeated message fields always track presence. However it still * indicates the semantic detail of whether the user wrote "optional" or not. * This can be useful for round-tripping the .proto file. For consistency we * give message fields a synthetic oneof also, even though it is not required * to track presence. This is especially important because the parser can't * tell if a field is a message or an enum, so it must always create a * synthetic oneof. * * Proto2 optional fields do not set this flag, because they already indicate * optional with `LABEL_OPTIONAL`. */ proto3Optional?: boolean | undefined; } export enum FieldDescriptorProto_Type { /** * TYPE_DOUBLE - 0 is reserved for errors. * Order is weird for historical reasons. */ TYPE_DOUBLE = 1, TYPE_FLOAT = 2, /** * TYPE_INT64 - Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT64 if * negative values are likely. */ TYPE_INT64 = 3, TYPE_UINT64 = 4, /** * TYPE_INT32 - Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT32 if * negative values are likely. */ TYPE_INT32 = 5, TYPE_FIXED64 = 6, TYPE_FIXED32 = 7, TYPE_BOOL = 8, TYPE_STRING = 9, /** * TYPE_GROUP - Tag-delimited aggregate. * Group type is deprecated and not supported after google.protobuf. However, Proto3 * implementations should still be able to parse the group wire format and * treat group fields as unknown fields. In Editions, the group wire format * can be enabled via the `message_encoding` feature. */ TYPE_GROUP = 10, /** TYPE_MESSAGE - Length-delimited aggregate. */ TYPE_MESSAGE = 11, /** TYPE_BYTES - New in version 2. */ TYPE_BYTES = 12, TYPE_UINT32 = 13, TYPE_ENUM = 14, TYPE_SFIXED32 = 15, TYPE_SFIXED64 = 16, /** TYPE_SINT32 - Uses ZigZag encoding. */ TYPE_SINT32 = 17, /** TYPE_SINT64 - Uses ZigZag encoding. */ TYPE_SINT64 = 18, UNRECOGNIZED = -1, } export function fieldDescriptorProto_TypeFromJSON(object: any): FieldDescriptorProto_Type { switch (object) { case 1: case "TYPE_DOUBLE": return FieldDescriptorProto_Type.TYPE_DOUBLE; case 2: case "TYPE_FLOAT": return FieldDescriptorProto_Type.TYPE_FLOAT; case 3: case "TYPE_INT64": return FieldDescriptorProto_Type.TYPE_INT64; case 4: case "TYPE_UINT64": return FieldDescriptorProto_Type.TYPE_UINT64; case 5: case "TYPE_INT32": return FieldDescriptorProto_Type.TYPE_INT32; case 6: case "TYPE_FIXED64": return FieldDescriptorProto_Type.TYPE_FIXED64; case 7: case "TYPE_FIXED32": return FieldDescriptorProto_Type.TYPE_FIXED32; case 8: case "TYPE_BOOL": return FieldDescriptorProto_Type.TYPE_BOOL; case 9: case "TYPE_STRING": return FieldDescriptorProto_Type.TYPE_STRING; case 10: case "TYPE_GROUP": return FieldDescriptorProto_Type.TYPE_GROUP; case 11: case "TYPE_MESSAGE": return FieldDescriptorProto_Type.TYPE_MESSAGE; case 12: case "TYPE_BYTES": return FieldDescriptorProto_Type.TYPE_BYTES; case 13: case "TYPE_UINT32": return FieldDescriptorProto_Type.TYPE_UINT32; case 14: case "TYPE_ENUM": return FieldDescriptorProto_Type.TYPE_ENUM; case 15: case "TYPE_SFIXED32": return FieldDescriptorProto_Type.TYPE_SFIXED32; case 16: case "TYPE_SFIXED64": return FieldDescriptorProto_Type.TYPE_SFIXED64; case 17: case "TYPE_SINT32": return FieldDescriptorProto_Type.TYPE_SINT32; case 18: case "TYPE_SINT64": return FieldDescriptorProto_Type.TYPE_SINT64; case -1: case "UNRECOGNIZED": default: return FieldDescriptorProto_Type.UNRECOGNIZED; } } export function fieldDescriptorProto_TypeToJSON(object: FieldDescriptorProto_Type): string { switch (object) { case FieldDescriptorProto_Type.TYPE_DOUBLE: return "TYPE_DOUBLE"; case FieldDescriptorProto_Type.TYPE_FLOAT: return "TYPE_FLOAT"; case FieldDescriptorProto_Type.TYPE_INT64: return "TYPE_INT64"; case FieldDescriptorProto_Type.TYPE_UINT64: return "TYPE_UINT64"; case FieldDescriptorProto_Type.TYPE_INT32: return "TYPE_INT32"; case FieldDescriptorProto_Type.TYPE_FIXED64: return "TYPE_FIXED64"; case FieldDescriptorProto_Type.TYPE_FIXED32: return "TYPE_FIXED32"; case FieldDescriptorProto_Type.TYPE_BOOL: return "TYPE_BOOL"; case FieldDescriptorProto_Type.TYPE_STRING: return "TYPE_STRING"; case FieldDescriptorProto_Type.TYPE_GROUP: return "TYPE_GROUP"; case FieldDescriptorProto_Type.TYPE_MESSAGE: return "TYPE_MESSAGE"; case FieldDescriptorProto_Type.TYPE_BYTES: return "TYPE_BYTES"; case FieldDescriptorProto_Type.TYPE_UINT32: return "TYPE_UINT32"; case FieldDescriptorProto_Type.TYPE_ENUM: return "TYPE_ENUM"; case FieldDescriptorProto_Type.TYPE_SFIXED32: return "TYPE_SFIXED32"; case FieldDescriptorProto_Type.TYPE_SFIXED64: return "TYPE_SFIXED64"; case FieldDescriptorProto_Type.TYPE_SINT32: return "TYPE_SINT32"; case FieldDescriptorProto_Type.TYPE_SINT64: return "TYPE_SINT64"; case FieldDescriptorProto_Type.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } export enum FieldDescriptorProto_Label { /** LABEL_OPTIONAL - 0 is reserved for errors */ LABEL_OPTIONAL = 1, LABEL_REPEATED = 3, /** * LABEL_REQUIRED - The required label is only allowed in google.protobuf. In proto3 and Editions * it's explicitly prohibited. In Editions, the `field_presence` feature * can be used to get this behavior. */ LABEL_REQUIRED = 2, UNRECOGNIZED = -1, } export function fieldDescriptorProto_LabelFromJSON(object: any): FieldDescriptorProto_Label { switch (object) { case 1: case "LABEL_OPTIONAL": return FieldDescriptorProto_Label.LABEL_OPTIONAL; case 3: case "LABEL_REPEATED": return FieldDescriptorProto_Label.LABEL_REPEATED; case 2: case "LABEL_REQUIRED": return FieldDescriptorProto_Label.LABEL_REQUIRED; case -1: case "UNRECOGNIZED": default: return FieldDescriptorProto_Label.UNRECOGNIZED; } } export function fieldDescriptorProto_LabelToJSON(object: FieldDescriptorProto_Label): string { switch (object) { case FieldDescriptorProto_Label.LABEL_OPTIONAL: return "LABEL_OPTIONAL"; case FieldDescriptorProto_Label.LABEL_REPEATED: return "LABEL_REPEATED"; case FieldDescriptorProto_Label.LABEL_REQUIRED: return "LABEL_REQUIRED"; case FieldDescriptorProto_Label.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } /** Describes a oneof. */ export interface OneofDescriptorProto { name?: string | undefined; options?: OneofOptions | undefined; } /** Describes an enum type. */ export interface EnumDescriptorProto { name?: string | undefined; value: EnumValueDescriptorProto[]; options?: | EnumOptions | undefined; /** * Range of reserved numeric values. Reserved numeric values may not be used * by enum values in the same enum declaration. Reserved ranges may not * overlap. */ reservedRange: EnumDescriptorProto_EnumReservedRange[]; /** * Reserved enum value names, which may not be reused. A given name may only * be reserved once. */ reservedName: string[]; /** Support for `export` and `local` keywords on enums. */ visibility?: SymbolVisibility | undefined; } /** * Range of reserved numeric values. Reserved values may not be used by * entries in the same enum. Reserved ranges may not overlap. * * Note that this is distinct from DescriptorProto.ReservedRange in that it * is inclusive such that it can appropriately represent the entire int32 * domain. */ export interface EnumDescriptorProto_EnumReservedRange { /** Inclusive. */ start?: | number | undefined; /** Inclusive. */ end?: number | undefined; } /** Describes a value within an enum. */ export interface EnumValueDescriptorProto { name?: string | undefined; number?: number | undefined; options?: EnumValueOptions | undefined; } /** Describes a service. */ export interface ServiceDescriptorProto { name?: string | undefined; method: MethodDescriptorProto[]; options?: ServiceOptions | undefined; } /** Describes a method of a service. */ export interface MethodDescriptorProto { name?: | string | undefined; /** * Input and output type names. These are resolved in the same way as * FieldDescriptorProto.type_name, but must refer to a message type. */ inputType?: string | undefined; outputType?: string | undefined; options?: | MethodOptions | undefined; /** Identifies if client streams multiple client messages */ clientStreaming?: | boolean | undefined; /** Identifies if server streams multiple server messages */ serverStreaming?: boolean | undefined; } export interface FileOptions { /** * Sets the Java package where classes generated from this .proto will be * placed. By default, the proto package is used, but this is often * inappropriate because proto packages do not normally start with backwards * domain names. */ javaPackage?: | string | undefined; /** * Controls the name of the wrapper Java class generated for the .proto file. * That class will always contain the .proto file's getDescriptor() method as * well as any top-level extensions defined in the .proto file. * If java_multiple_files is disabled, then all the other classes from the * .proto file will be nested inside the single wrapper outer class. */ javaOuterClassname?: | string | undefined; /** * If enabled, then the Java code generator will generate a separate .java * file for each top-level message, enum, and service defined in the .proto * file. Thus, these types will *not* be nested inside the wrapper class * named by java_outer_classname. However, the wrapper class will still be * generated to contain the file's getDescriptor() method as well as any * top-level extensions defined in the file. */ javaMultipleFiles?: | boolean | undefined; /** * This option does nothing. * * @deprecated */ javaGenerateEqualsAndHash?: | boolean | undefined; /** * A proto2 file can set this to true to opt in to UTF-8 checking for Java, * which will throw an exception if invalid UTF-8 is parsed from the wire or * assigned to a string field. * * TODO: clarify exactly what kinds of field types this option * applies to, and update these docs accordingly. * * Proto3 files already perform these checks. Setting the option explicitly to * false has no effect: it cannot be used to opt proto3 files out of UTF-8 * checks. */ javaStringCheckUtf8?: boolean | undefined; optimizeFor?: | FileOptions_OptimizeMode | undefined; /** * Sets the Go package where structs generated from this .proto will be * placed. If omitted, the Go package will be derived from the following: * - The basename of the package import path, if provided. * - Otherwise, the package statement in the .proto file, if present. * - Otherwise, the basename of the .proto file, without extension. */ goPackage?: | string | undefined; /** * Should generic services be generated in each language? "Generic" services * are not specific to any particular RPC system. They are generated by the * main code generators in each language (without additional plugins). * Generic services were the only kind of service generation supported by * early versions of google.protobuf. * * Generic services are now considered deprecated in favor of using plugins * that generate code specific to your particular RPC system. Therefore, * these default to false. Old code which depends on generic services should * explicitly set them to true. */ ccGenericServices?: boolean | undefined; javaGenericServices?: boolean | undefined; pyGenericServices?: | boolean | undefined; /** * Is this file deprecated? * Depending on the target platform, this can emit Deprecated annotations * for everything in the file, or it will be completely ignored; in the very * least, this is a formalization for deprecating files. */ deprecated?: | boolean | undefined; /** * Enables the use of arenas for the proto messages in this file. This applies * only to generated classes for C++. */ ccEnableArenas?: | boolean | undefined; /** * Sets the objective c class prefix which is prepended to all objective c * generated classes from this .proto. There is no default. */ objcClassPrefix?: | string | undefined; /** Namespace for generated classes; defaults to the package. */ csharpNamespace?: | string | undefined; /** * By default Swift generators will take the proto package and CamelCase it * replacing '.' with underscore and use that to prefix the types/symbols * defined. When this options is provided, they will use this value instead * to prefix the types/symbols defined. */ swiftPrefix?: | string | undefined; /** * Sets the php class prefix which is prepended to all php generated classes * from this .proto. Default is empty. */ phpClassPrefix?: | string | undefined; /** * Use this option to change the namespace of php generated classes. Default * is empty. When this option is empty, the package name will be used for * determining the namespace. */ phpNamespace?: | string | undefined; /** * Use this option to change the namespace of php generated metadata classes. * Default is empty. When this option is empty, the proto file name will be * used for determining the namespace. */ phpMetadataNamespace?: | string | undefined; /** * Use this option to change the package of ruby generated classes. Default * is empty. When this option is not set, the package name will be used for * determining the ruby package. */ rubyPackage?: | string | undefined; /** * Any features defined in the specific edition. * WARNING: This field should only be used by protobuf plugins or special * cases like the proto compiler. Other uses are discouraged and * developers should rely on the protoreflect APIs for their client language. */ features?: | FeatureSet | undefined; /** * The parser stores options it doesn't recognize here. * See the documentation for the "Options" section above. */ uninterpretedOption: UninterpretedOption[]; } /** Generated classes can be optimized for speed or code size. */ export enum FileOptions_OptimizeMode { /** SPEED - Generate complete code for parsing, serialization, */ SPEED = 1, /** CODE_SIZE - etc. */ CODE_SIZE = 2, /** LITE_RUNTIME - Generate code using MessageLite and the lite runtime. */ LITE_RUNTIME = 3, UNRECOGNIZED = -1, } export function fileOptions_OptimizeModeFromJSON(object: any): FileOptions_OptimizeMode { switch (object) { case 1: case "SPEED": return FileOptions_OptimizeMode.SPEED; case 2: case "CODE_SIZE": return FileOptions_OptimizeMode.CODE_SIZE; case 3: case "LITE_RUNTIME": return FileOptions_OptimizeMode.LITE_RUNTIME; case -1: case "UNRECOGNIZED": default: return FileOptions_OptimizeMode.UNRECOGNIZED; } } export function fileOptions_OptimizeModeToJSON(object: FileOptions_OptimizeMode): string { switch (object) { case FileOptions_OptimizeMode.SPEED: return "SPEED"; case FileOptions_OptimizeMode.CODE_SIZE: return "CODE_SIZE"; case FileOptions_OptimizeMode.LITE_RUNTIME: return "LITE_RUNTIME"; case FileOptions_OptimizeMode.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } export interface MessageOptions { /** * Set true to use the old proto1 MessageSet wire format for extensions. * This is provided for backwards-compatibility with the MessageSet wire * format. You should not use this for any other reason: It's less * efficient, has fewer features, and is more complicated. * * The message must be defined exactly as follows: * message Foo { * option message_set_wire_format = true; * extensions 4 to max; * } * Note that the message cannot have any defined fields; MessageSets only * have extensions. * * All extensions of your type must be singular messages; e.g. they cannot * be int32s, enums, or repeated messages. * * Because this is an option, the above two restrictions are not enforced by * the protocol compiler. */ messageSetWireFormat?: | boolean | undefined; /** * Disables the generation of the standard "descriptor()" accessor, which can * conflict with a field of the same name. This is meant to make migration * from proto1 easier; new code should avoid fields named "descriptor". */ noStandardDescriptorAccessor?: | boolean | undefined; /** * Is this message deprecated? * Depending on the target platform, this can emit Deprecated annotations * for the message, or it will be completely ignored; in the very least, * this is a formalization for deprecating messages. */ deprecated?: | boolean | undefined; /** * Whether the message is an automatically generated map entry type for the * maps field. * * For maps fields: * map map_field = 1; * The parsed descriptor looks like: * message MapFieldEntry { * option map_entry = true; * optional KeyType key = 1; * optional ValueType value = 2; * } * repeated MapFieldEntry map_field = 1; * * Implementations may choose not to generate the map_entry=true message, but * use a native map in the target language to hold the keys and values. * The reflection APIs in such implementations still need to work as * if the field is a repeated message field. * * NOTE: Do not set the option in .proto files. Always use the maps syntax * instead. The option should only be implicitly set by the proto compiler * parser. */ mapEntry?: | boolean | undefined; /** * Enable the legacy handling of JSON field name conflicts. This lowercases * and strips underscored from the fields before comparison in proto3 only. * The new behavior takes `json_name` into account and applies to proto2 as * well. * * This should only be used as a temporary measure against broken builds due * to the change in behavior for JSON field name conflicts. * * TODO This is legacy behavior we plan to remove once downstream * teams have had time to migrate. * * @deprecated */ deprecatedLegacyJsonFieldConflicts?: | boolean | undefined; /** * Any features defined in the specific edition. * WARNING: This field should only be used by protobuf plugins or special * cases like the proto compiler. Other uses are discouraged and * developers should rely on the protoreflect APIs for their client language. */ features?: | FeatureSet | undefined; /** The parser stores options it doesn't recognize here. See above. */ uninterpretedOption: UninterpretedOption[]; } export interface FieldOptions { /** * NOTE: ctype is deprecated. Use `features.(pb.cpp).string_type` instead. * The ctype option instructs the C++ code generator to use a different * representation of the field than it normally would. See the specific * options below. This option is only implemented to support use of * [ctype=CORD] and [ctype=STRING] (the default) on non-repeated fields of * type "bytes" in the open source release. * TODO: make ctype actually deprecated. */ ctype?: | FieldOptions_CType | undefined; /** * The packed option can be enabled for repeated primitive fields to enable * a more efficient representation on the wire. Rather than repeatedly * writing the tag and type for each element, the entire array is encoded as * a single length-delimited blob. In proto3, only explicit setting it to * false will avoid using packed encoding. This option is prohibited in * Editions, but the `repeated_field_encoding` feature can be used to control * the behavior. */ packed?: | boolean | undefined; /** * The jstype option determines the JavaScript type used for values of the * field. The option is permitted only for 64 bit integral and fixed types * (int64, uint64, sint64, fixed64, sfixed64). A field with jstype JS_STRING * is represented as JavaScript string, which avoids loss of precision that * can happen when a large value is converted to a floating point JavaScript. * Specifying JS_NUMBER for the jstype causes the generated JavaScript code to * use the JavaScript "number" type. The behavior of the default option * JS_NORMAL is implementation dependent. * * This option is an enum to permit additional types to be added, e.g. * goog.math.Integer. */ jstype?: | FieldOptions_JSType | undefined; /** * Should this field be parsed lazily? Lazy applies only to message-type * fields. It means that when the outer message is initially parsed, the * inner message's contents will not be parsed but instead stored in encoded * form. The inner message will actually be parsed when it is first accessed. * * This is only a hint. Implementations are free to choose whether to use * eager or lazy parsing regardless of the value of this option. However, * setting this option true suggests that the protocol author believes that * using lazy parsing on this field is worth the additional bookkeeping * overhead typically needed to implement it. * * This option does not affect the public interface of any generated code; * all method signatures remain the same. Furthermore, thread-safety of the * interface is not affected by this option; const methods remain safe to * call from multiple threads concurrently, while non-const methods continue * to require exclusive access. * * Note that lazy message fields are still eagerly verified to check * ill-formed wireformat or missing required fields. Calling IsInitialized() * on the outer message would fail if the inner message has missing required * fields. Failed verification would result in parsing failure (except when * uninitialized messages are acceptable). */ lazy?: | boolean | undefined; /** * unverified_lazy does no correctness checks on the byte stream. This should * only be used where lazy with verification is prohibitive for performance * reasons. */ unverifiedLazy?: | boolean | undefined; /** * Is this field deprecated? * Depending on the target platform, this can emit Deprecated annotations * for accessors, or it will be completely ignored; in the very least, this * is a formalization for deprecating fields. */ deprecated?: | boolean | undefined; /** * DEPRECATED. DO NOT USE! * For Google-internal migration only. Do not use. * * @deprecated */ weak?: | boolean | undefined; /** * Indicate that the field value should not be printed out when using debug * formats, e.g. when the field contains sensitive credentials. */ debugRedact?: boolean | undefined; retention?: FieldOptions_OptionRetention | undefined; targets: FieldOptions_OptionTargetType[]; editionDefaults: FieldOptions_EditionDefault[]; /** * Any features defined in the specific edition. * WARNING: This field should only be used by protobuf plugins or special * cases like the proto compiler. Other uses are discouraged and * developers should rely on the protoreflect APIs for their client language. */ features?: FeatureSet | undefined; featureSupport?: | FieldOptions_FeatureSupport | undefined; /** The parser stores options it doesn't recognize here. See above. */ uninterpretedOption: UninterpretedOption[]; } export enum FieldOptions_CType { /** STRING - Default mode. */ STRING = 0, /** * CORD - The option [ctype=CORD] may be applied to a non-repeated field of type * "bytes". It indicates that in C++, the data should be stored in a Cord * instead of a string. For very large strings, this may reduce memory * fragmentation. It may also allow better performance when parsing from a * Cord, or when parsing with aliasing enabled, as the parsed Cord may then * alias the original buffer. */ CORD = 1, STRING_PIECE = 2, UNRECOGNIZED = -1, } export function fieldOptions_CTypeFromJSON(object: any): FieldOptions_CType { switch (object) { case 0: case "STRING": return FieldOptions_CType.STRING; case 1: case "CORD": return FieldOptions_CType.CORD; case 2: case "STRING_PIECE": return FieldOptions_CType.STRING_PIECE; case -1: case "UNRECOGNIZED": default: return FieldOptions_CType.UNRECOGNIZED; } } export function fieldOptions_CTypeToJSON(object: FieldOptions_CType): string { switch (object) { case FieldOptions_CType.STRING: return "STRING"; case FieldOptions_CType.CORD: return "CORD"; case FieldOptions_CType.STRING_PIECE: return "STRING_PIECE"; case FieldOptions_CType.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } export enum FieldOptions_JSType { /** JS_NORMAL - Use the default type. */ JS_NORMAL = 0, /** JS_STRING - Use JavaScript strings. */ JS_STRING = 1, /** JS_NUMBER - Use JavaScript numbers. */ JS_NUMBER = 2, UNRECOGNIZED = -1, } export function fieldOptions_JSTypeFromJSON(object: any): FieldOptions_JSType { switch (object) { case 0: case "JS_NORMAL": return FieldOptions_JSType.JS_NORMAL; case 1: case "JS_STRING": return FieldOptions_JSType.JS_STRING; case 2: case "JS_NUMBER": return FieldOptions_JSType.JS_NUMBER; case -1: case "UNRECOGNIZED": default: return FieldOptions_JSType.UNRECOGNIZED; } } export function fieldOptions_JSTypeToJSON(object: FieldOptions_JSType): string { switch (object) { case FieldOptions_JSType.JS_NORMAL: return "JS_NORMAL"; case FieldOptions_JSType.JS_STRING: return "JS_STRING"; case FieldOptions_JSType.JS_NUMBER: return "JS_NUMBER"; case FieldOptions_JSType.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } /** If set to RETENTION_SOURCE, the option will be omitted from the binary. */ export enum FieldOptions_OptionRetention { RETENTION_UNKNOWN = 0, RETENTION_RUNTIME = 1, RETENTION_SOURCE = 2, UNRECOGNIZED = -1, } export function fieldOptions_OptionRetentionFromJSON(object: any): FieldOptions_OptionRetention { switch (object) { case 0: case "RETENTION_UNKNOWN": return FieldOptions_OptionRetention.RETENTION_UNKNOWN; case 1: case "RETENTION_RUNTIME": return FieldOptions_OptionRetention.RETENTION_RUNTIME; case 2: case "RETENTION_SOURCE": return FieldOptions_OptionRetention.RETENTION_SOURCE; case -1: case "UNRECOGNIZED": default: return FieldOptions_OptionRetention.UNRECOGNIZED; } } export function fieldOptions_OptionRetentionToJSON(object: FieldOptions_OptionRetention): string { switch (object) { case FieldOptions_OptionRetention.RETENTION_UNKNOWN: return "RETENTION_UNKNOWN"; case FieldOptions_OptionRetention.RETENTION_RUNTIME: return "RETENTION_RUNTIME"; case FieldOptions_OptionRetention.RETENTION_SOURCE: return "RETENTION_SOURCE"; case FieldOptions_OptionRetention.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } /** * This indicates the types of entities that the field may apply to when used * as an option. If it is unset, then the field may be freely used as an * option on any kind of entity. */ export enum FieldOptions_OptionTargetType { TARGET_TYPE_UNKNOWN = 0, TARGET_TYPE_FILE = 1, TARGET_TYPE_EXTENSION_RANGE = 2, TARGET_TYPE_MESSAGE = 3, TARGET_TYPE_FIELD = 4, TARGET_TYPE_ONEOF = 5, TARGET_TYPE_ENUM = 6, TARGET_TYPE_ENUM_ENTRY = 7, TARGET_TYPE_SERVICE = 8, TARGET_TYPE_METHOD = 9, UNRECOGNIZED = -1, } export function fieldOptions_OptionTargetTypeFromJSON(object: any): FieldOptions_OptionTargetType { switch (object) { case 0: case "TARGET_TYPE_UNKNOWN": return FieldOptions_OptionTargetType.TARGET_TYPE_UNKNOWN; case 1: case "TARGET_TYPE_FILE": return FieldOptions_OptionTargetType.TARGET_TYPE_FILE; case 2: case "TARGET_TYPE_EXTENSION_RANGE": return FieldOptions_OptionTargetType.TARGET_TYPE_EXTENSION_RANGE; case 3: case "TARGET_TYPE_MESSAGE": return FieldOptions_OptionTargetType.TARGET_TYPE_MESSAGE; case 4: case "TARGET_TYPE_FIELD": return FieldOptions_OptionTargetType.TARGET_TYPE_FIELD; case 5: case "TARGET_TYPE_ONEOF": return FieldOptions_OptionTargetType.TARGET_TYPE_ONEOF; case 6: case "TARGET_TYPE_ENUM": return FieldOptions_OptionTargetType.TARGET_TYPE_ENUM; case 7: case "TARGET_TYPE_ENUM_ENTRY": return FieldOptions_OptionTargetType.TARGET_TYPE_ENUM_ENTRY; case 8: case "TARGET_TYPE_SERVICE": return FieldOptions_OptionTargetType.TARGET_TYPE_SERVICE; case 9: case "TARGET_TYPE_METHOD": return FieldOptions_OptionTargetType.TARGET_TYPE_METHOD; case -1: case "UNRECOGNIZED": default: return FieldOptions_OptionTargetType.UNRECOGNIZED; } } export function fieldOptions_OptionTargetTypeToJSON(object: FieldOptions_OptionTargetType): string { switch (object) { case FieldOptions_OptionTargetType.TARGET_TYPE_UNKNOWN: return "TARGET_TYPE_UNKNOWN"; case FieldOptions_OptionTargetType.TARGET_TYPE_FILE: return "TARGET_TYPE_FILE"; case FieldOptions_OptionTargetType.TARGET_TYPE_EXTENSION_RANGE: return "TARGET_TYPE_EXTENSION_RANGE"; case FieldOptions_OptionTargetType.TARGET_TYPE_MESSAGE: return "TARGET_TYPE_MESSAGE"; case FieldOptions_OptionTargetType.TARGET_TYPE_FIELD: return "TARGET_TYPE_FIELD"; case FieldOptions_OptionTargetType.TARGET_TYPE_ONEOF: return "TARGET_TYPE_ONEOF"; case FieldOptions_OptionTargetType.TARGET_TYPE_ENUM: return "TARGET_TYPE_ENUM"; case FieldOptions_OptionTargetType.TARGET_TYPE_ENUM_ENTRY: return "TARGET_TYPE_ENUM_ENTRY"; case FieldOptions_OptionTargetType.TARGET_TYPE_SERVICE: return "TARGET_TYPE_SERVICE"; case FieldOptions_OptionTargetType.TARGET_TYPE_METHOD: return "TARGET_TYPE_METHOD"; case FieldOptions_OptionTargetType.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } export interface FieldOptions_EditionDefault { edition?: | Edition | undefined; /** Textproto value. */ value?: string | undefined; } /** Information about the support window of a feature. */ export interface FieldOptions_FeatureSupport { /** * The edition that this feature was first available in. In editions * earlier than this one, the default assigned to EDITION_LEGACY will be * used, and proto files will not be able to override it. */ editionIntroduced?: | Edition | undefined; /** * The edition this feature becomes deprecated in. Using this after this * edition may trigger warnings. */ editionDeprecated?: | Edition | undefined; /** * The deprecation warning text if this feature is used after the edition it * was marked deprecated in. */ deprecationWarning?: | string | undefined; /** * The edition this feature is no longer available in. In editions after * this one, the last default assigned will be used, and proto files will * not be able to override it. */ editionRemoved?: Edition | undefined; } export interface OneofOptions { /** * Any features defined in the specific edition. * WARNING: This field should only be used by protobuf plugins or special * cases like the proto compiler. Other uses are discouraged and * developers should rely on the protoreflect APIs for their client language. */ features?: | FeatureSet | undefined; /** The parser stores options it doesn't recognize here. See above. */ uninterpretedOption: UninterpretedOption[]; } export interface EnumOptions { /** * Set this option to true to allow mapping different tag names to the same * value. */ allowAlias?: | boolean | undefined; /** * Is this enum deprecated? * Depending on the target platform, this can emit Deprecated annotations * for the enum, or it will be completely ignored; in the very least, this * is a formalization for deprecating enums. */ deprecated?: | boolean | undefined; /** * Enable the legacy handling of JSON field name conflicts. This lowercases * and strips underscored from the fields before comparison in proto3 only. * The new behavior takes `json_name` into account and applies to proto2 as * well. * TODO Remove this legacy behavior once downstream teams have * had time to migrate. * * @deprecated */ deprecatedLegacyJsonFieldConflicts?: | boolean | undefined; /** * Any features defined in the specific edition. * WARNING: This field should only be used by protobuf plugins or special * cases like the proto compiler. Other uses are discouraged and * developers should rely on the protoreflect APIs for their client language. */ features?: | FeatureSet | undefined; /** The parser stores options it doesn't recognize here. See above. */ uninterpretedOption: UninterpretedOption[]; } export interface EnumValueOptions { /** * Is this enum value deprecated? * Depending on the target platform, this can emit Deprecated annotations * for the enum value, or it will be completely ignored; in the very least, * this is a formalization for deprecating enum values. */ deprecated?: | boolean | undefined; /** * Any features defined in the specific edition. * WARNING: This field should only be used by protobuf plugins or special * cases like the proto compiler. Other uses are discouraged and * developers should rely on the protoreflect APIs for their client language. */ features?: | FeatureSet | undefined; /** * Indicate that fields annotated with this enum value should not be printed * out when using debug formats, e.g. when the field contains sensitive * credentials. */ debugRedact?: | boolean | undefined; /** Information about the support window of a feature value. */ featureSupport?: | FieldOptions_FeatureSupport | undefined; /** The parser stores options it doesn't recognize here. See above. */ uninterpretedOption: UninterpretedOption[]; } export interface ServiceOptions { /** * Any features defined in the specific edition. * WARNING: This field should only be used by protobuf plugins or special * cases like the proto compiler. Other uses are discouraged and * developers should rely on the protoreflect APIs for their client language. */ features?: | FeatureSet | undefined; /** * Is this service deprecated? * Depending on the target platform, this can emit Deprecated annotations * for the service, or it will be completely ignored; in the very least, * this is a formalization for deprecating services. */ deprecated?: | boolean | undefined; /** The parser stores options it doesn't recognize here. See above. */ uninterpretedOption: UninterpretedOption[]; } export interface MethodOptions { /** * Is this method deprecated? * Depending on the target platform, this can emit Deprecated annotations * for the method, or it will be completely ignored; in the very least, * this is a formalization for deprecating methods. */ deprecated?: boolean | undefined; idempotencyLevel?: | MethodOptions_IdempotencyLevel | undefined; /** * Any features defined in the specific edition. * WARNING: This field should only be used by protobuf plugins or special * cases like the proto compiler. Other uses are discouraged and * developers should rely on the protoreflect APIs for their client language. */ features?: | FeatureSet | undefined; /** The parser stores options it doesn't recognize here. See above. */ uninterpretedOption: UninterpretedOption[]; } /** * Is this method side-effect-free (or safe in HTTP parlance), or idempotent, * or neither? HTTP based RPC implementation may choose GET verb for safe * methods, and PUT verb for idempotent methods instead of the default POST. */ export enum MethodOptions_IdempotencyLevel { IDEMPOTENCY_UNKNOWN = 0, /** NO_SIDE_EFFECTS - implies idempotent */ NO_SIDE_EFFECTS = 1, /** IDEMPOTENT - idempotent, but may have side effects */ IDEMPOTENT = 2, UNRECOGNIZED = -1, } export function methodOptions_IdempotencyLevelFromJSON(object: any): MethodOptions_IdempotencyLevel { switch (object) { case 0: case "IDEMPOTENCY_UNKNOWN": return MethodOptions_IdempotencyLevel.IDEMPOTENCY_UNKNOWN; case 1: case "NO_SIDE_EFFECTS": return MethodOptions_IdempotencyLevel.NO_SIDE_EFFECTS; case 2: case "IDEMPOTENT": return MethodOptions_IdempotencyLevel.IDEMPOTENT; case -1: case "UNRECOGNIZED": default: return MethodOptions_IdempotencyLevel.UNRECOGNIZED; } } export function methodOptions_IdempotencyLevelToJSON(object: MethodOptions_IdempotencyLevel): string { switch (object) { case MethodOptions_IdempotencyLevel.IDEMPOTENCY_UNKNOWN: return "IDEMPOTENCY_UNKNOWN"; case MethodOptions_IdempotencyLevel.NO_SIDE_EFFECTS: return "NO_SIDE_EFFECTS"; case MethodOptions_IdempotencyLevel.IDEMPOTENT: return "IDEMPOTENT"; case MethodOptions_IdempotencyLevel.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } /** * A message representing a option the parser does not recognize. This only * appears in options protos created by the compiler::Parser class. * DescriptorPool resolves these when building Descriptor objects. Therefore, * options protos in descriptor objects (e.g. returned by Descriptor::options(), * or produced by Descriptor::CopyTo()) will never have UninterpretedOptions * in them. */ export interface UninterpretedOption { name: UninterpretedOption_NamePart[]; /** * The value of the uninterpreted option, in whatever type the tokenizer * identified it as during parsing. Exactly one of these should be set. */ identifierValue?: string | undefined; positiveIntValue?: number | undefined; negativeIntValue?: number | undefined; doubleValue?: number | undefined; stringValue?: Uint8Array | undefined; aggregateValue?: string | undefined; } /** * The name of the uninterpreted option. Each string represents a segment in * a dot-separated name. is_extension is true iff a segment represents an * extension (denoted with parentheses in options specs in .proto files). * E.g.,{ ["foo", false], ["bar.baz", true], ["moo", false] } represents * "foo.(bar.baz).moo". */ export interface UninterpretedOption_NamePart { namePart: string; isExtension: boolean; } /** * TODO Enums in C++ gencode (and potentially other languages) are * not well scoped. This means that each of the feature enums below can clash * with each other. The short names we've chosen maximize call-site * readability, but leave us very open to this scenario. A future feature will * be designed and implemented to handle this, hopefully before we ever hit a * conflict here. */ export interface FeatureSet { fieldPresence?: FeatureSet_FieldPresence | undefined; enumType?: FeatureSet_EnumType | undefined; repeatedFieldEncoding?: FeatureSet_RepeatedFieldEncoding | undefined; utf8Validation?: FeatureSet_Utf8Validation | undefined; messageEncoding?: FeatureSet_MessageEncoding | undefined; jsonFormat?: FeatureSet_JsonFormat | undefined; enforceNamingStyle?: FeatureSet_EnforceNamingStyle | undefined; defaultSymbolVisibility?: FeatureSet_VisibilityFeature_DefaultSymbolVisibility | undefined; } export enum FeatureSet_FieldPresence { FIELD_PRESENCE_UNKNOWN = 0, EXPLICIT = 1, IMPLICIT = 2, LEGACY_REQUIRED = 3, UNRECOGNIZED = -1, } export function featureSet_FieldPresenceFromJSON(object: any): FeatureSet_FieldPresence { switch (object) { case 0: case "FIELD_PRESENCE_UNKNOWN": return FeatureSet_FieldPresence.FIELD_PRESENCE_UNKNOWN; case 1: case "EXPLICIT": return FeatureSet_FieldPresence.EXPLICIT; case 2: case "IMPLICIT": return FeatureSet_FieldPresence.IMPLICIT; case 3: case "LEGACY_REQUIRED": return FeatureSet_FieldPresence.LEGACY_REQUIRED; case -1: case "UNRECOGNIZED": default: return FeatureSet_FieldPresence.UNRECOGNIZED; } } export function featureSet_FieldPresenceToJSON(object: FeatureSet_FieldPresence): string { switch (object) { case FeatureSet_FieldPresence.FIELD_PRESENCE_UNKNOWN: return "FIELD_PRESENCE_UNKNOWN"; case FeatureSet_FieldPresence.EXPLICIT: return "EXPLICIT"; case FeatureSet_FieldPresence.IMPLICIT: return "IMPLICIT"; case FeatureSet_FieldPresence.LEGACY_REQUIRED: return "LEGACY_REQUIRED"; case FeatureSet_FieldPresence.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } export enum FeatureSet_EnumType { ENUM_TYPE_UNKNOWN = 0, OPEN = 1, CLOSED = 2, UNRECOGNIZED = -1, } export function featureSet_EnumTypeFromJSON(object: any): FeatureSet_EnumType { switch (object) { case 0: case "ENUM_TYPE_UNKNOWN": return FeatureSet_EnumType.ENUM_TYPE_UNKNOWN; case 1: case "OPEN": return FeatureSet_EnumType.OPEN; case 2: case "CLOSED": return FeatureSet_EnumType.CLOSED; case -1: case "UNRECOGNIZED": default: return FeatureSet_EnumType.UNRECOGNIZED; } } export function featureSet_EnumTypeToJSON(object: FeatureSet_EnumType): string { switch (object) { case FeatureSet_EnumType.ENUM_TYPE_UNKNOWN: return "ENUM_TYPE_UNKNOWN"; case FeatureSet_EnumType.OPEN: return "OPEN"; case FeatureSet_EnumType.CLOSED: return "CLOSED"; case FeatureSet_EnumType.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } export enum FeatureSet_RepeatedFieldEncoding { REPEATED_FIELD_ENCODING_UNKNOWN = 0, PACKED = 1, EXPANDED = 2, UNRECOGNIZED = -1, } export function featureSet_RepeatedFieldEncodingFromJSON(object: any): FeatureSet_RepeatedFieldEncoding { switch (object) { case 0: case "REPEATED_FIELD_ENCODING_UNKNOWN": return FeatureSet_RepeatedFieldEncoding.REPEATED_FIELD_ENCODING_UNKNOWN; case 1: case "PACKED": return FeatureSet_RepeatedFieldEncoding.PACKED; case 2: case "EXPANDED": return FeatureSet_RepeatedFieldEncoding.EXPANDED; case -1: case "UNRECOGNIZED": default: return FeatureSet_RepeatedFieldEncoding.UNRECOGNIZED; } } export function featureSet_RepeatedFieldEncodingToJSON(object: FeatureSet_RepeatedFieldEncoding): string { switch (object) { case FeatureSet_RepeatedFieldEncoding.REPEATED_FIELD_ENCODING_UNKNOWN: return "REPEATED_FIELD_ENCODING_UNKNOWN"; case FeatureSet_RepeatedFieldEncoding.PACKED: return "PACKED"; case FeatureSet_RepeatedFieldEncoding.EXPANDED: return "EXPANDED"; case FeatureSet_RepeatedFieldEncoding.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } export enum FeatureSet_Utf8Validation { UTF8_VALIDATION_UNKNOWN = 0, VERIFY = 2, NONE = 3, UNRECOGNIZED = -1, } export function featureSet_Utf8ValidationFromJSON(object: any): FeatureSet_Utf8Validation { switch (object) { case 0: case "UTF8_VALIDATION_UNKNOWN": return FeatureSet_Utf8Validation.UTF8_VALIDATION_UNKNOWN; case 2: case "VERIFY": return FeatureSet_Utf8Validation.VERIFY; case 3: case "NONE": return FeatureSet_Utf8Validation.NONE; case -1: case "UNRECOGNIZED": default: return FeatureSet_Utf8Validation.UNRECOGNIZED; } } export function featureSet_Utf8ValidationToJSON(object: FeatureSet_Utf8Validation): string { switch (object) { case FeatureSet_Utf8Validation.UTF8_VALIDATION_UNKNOWN: return "UTF8_VALIDATION_UNKNOWN"; case FeatureSet_Utf8Validation.VERIFY: return "VERIFY"; case FeatureSet_Utf8Validation.NONE: return "NONE"; case FeatureSet_Utf8Validation.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } export enum FeatureSet_MessageEncoding { MESSAGE_ENCODING_UNKNOWN = 0, LENGTH_PREFIXED = 1, DELIMITED = 2, UNRECOGNIZED = -1, } export function featureSet_MessageEncodingFromJSON(object: any): FeatureSet_MessageEncoding { switch (object) { case 0: case "MESSAGE_ENCODING_UNKNOWN": return FeatureSet_MessageEncoding.MESSAGE_ENCODING_UNKNOWN; case 1: case "LENGTH_PREFIXED": return FeatureSet_MessageEncoding.LENGTH_PREFIXED; case 2: case "DELIMITED": return FeatureSet_MessageEncoding.DELIMITED; case -1: case "UNRECOGNIZED": default: return FeatureSet_MessageEncoding.UNRECOGNIZED; } } export function featureSet_MessageEncodingToJSON(object: FeatureSet_MessageEncoding): string { switch (object) { case FeatureSet_MessageEncoding.MESSAGE_ENCODING_UNKNOWN: return "MESSAGE_ENCODING_UNKNOWN"; case FeatureSet_MessageEncoding.LENGTH_PREFIXED: return "LENGTH_PREFIXED"; case FeatureSet_MessageEncoding.DELIMITED: return "DELIMITED"; case FeatureSet_MessageEncoding.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } export enum FeatureSet_JsonFormat { JSON_FORMAT_UNKNOWN = 0, ALLOW = 1, LEGACY_BEST_EFFORT = 2, UNRECOGNIZED = -1, } export function featureSet_JsonFormatFromJSON(object: any): FeatureSet_JsonFormat { switch (object) { case 0: case "JSON_FORMAT_UNKNOWN": return FeatureSet_JsonFormat.JSON_FORMAT_UNKNOWN; case 1: case "ALLOW": return FeatureSet_JsonFormat.ALLOW; case 2: case "LEGACY_BEST_EFFORT": return FeatureSet_JsonFormat.LEGACY_BEST_EFFORT; case -1: case "UNRECOGNIZED": default: return FeatureSet_JsonFormat.UNRECOGNIZED; } } export function featureSet_JsonFormatToJSON(object: FeatureSet_JsonFormat): string { switch (object) { case FeatureSet_JsonFormat.JSON_FORMAT_UNKNOWN: return "JSON_FORMAT_UNKNOWN"; case FeatureSet_JsonFormat.ALLOW: return "ALLOW"; case FeatureSet_JsonFormat.LEGACY_BEST_EFFORT: return "LEGACY_BEST_EFFORT"; case FeatureSet_JsonFormat.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } export enum FeatureSet_EnforceNamingStyle { ENFORCE_NAMING_STYLE_UNKNOWN = 0, STYLE2024 = 1, STYLE_LEGACY = 2, UNRECOGNIZED = -1, } export function featureSet_EnforceNamingStyleFromJSON(object: any): FeatureSet_EnforceNamingStyle { switch (object) { case 0: case "ENFORCE_NAMING_STYLE_UNKNOWN": return FeatureSet_EnforceNamingStyle.ENFORCE_NAMING_STYLE_UNKNOWN; case 1: case "STYLE2024": return FeatureSet_EnforceNamingStyle.STYLE2024; case 2: case "STYLE_LEGACY": return FeatureSet_EnforceNamingStyle.STYLE_LEGACY; case -1: case "UNRECOGNIZED": default: return FeatureSet_EnforceNamingStyle.UNRECOGNIZED; } } export function featureSet_EnforceNamingStyleToJSON(object: FeatureSet_EnforceNamingStyle): string { switch (object) { case FeatureSet_EnforceNamingStyle.ENFORCE_NAMING_STYLE_UNKNOWN: return "ENFORCE_NAMING_STYLE_UNKNOWN"; case FeatureSet_EnforceNamingStyle.STYLE2024: return "STYLE2024"; case FeatureSet_EnforceNamingStyle.STYLE_LEGACY: return "STYLE_LEGACY"; case FeatureSet_EnforceNamingStyle.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } export interface FeatureSet_VisibilityFeature { } export enum FeatureSet_VisibilityFeature_DefaultSymbolVisibility { DEFAULT_SYMBOL_VISIBILITY_UNKNOWN = 0, /** EXPORT_ALL - Default pre-EDITION_2024, all UNSET visibility are export. */ EXPORT_ALL = 1, /** EXPORT_TOP_LEVEL - All top-level symbols default to export, nested default to local. */ EXPORT_TOP_LEVEL = 2, /** LOCAL_ALL - All symbols default to local. */ LOCAL_ALL = 3, /** * STRICT - All symbols local by default. Nested types cannot be exported. * With special case caveat for message { enum {} reserved 1 to max; } * This is the recommended setting for new protos. */ STRICT = 4, UNRECOGNIZED = -1, } export function featureSet_VisibilityFeature_DefaultSymbolVisibilityFromJSON( object: any, ): FeatureSet_VisibilityFeature_DefaultSymbolVisibility { switch (object) { case 0: case "DEFAULT_SYMBOL_VISIBILITY_UNKNOWN": return FeatureSet_VisibilityFeature_DefaultSymbolVisibility.DEFAULT_SYMBOL_VISIBILITY_UNKNOWN; case 1: case "EXPORT_ALL": return FeatureSet_VisibilityFeature_DefaultSymbolVisibility.EXPORT_ALL; case 2: case "EXPORT_TOP_LEVEL": return FeatureSet_VisibilityFeature_DefaultSymbolVisibility.EXPORT_TOP_LEVEL; case 3: case "LOCAL_ALL": return FeatureSet_VisibilityFeature_DefaultSymbolVisibility.LOCAL_ALL; case 4: case "STRICT": return FeatureSet_VisibilityFeature_DefaultSymbolVisibility.STRICT; case -1: case "UNRECOGNIZED": default: return FeatureSet_VisibilityFeature_DefaultSymbolVisibility.UNRECOGNIZED; } } export function featureSet_VisibilityFeature_DefaultSymbolVisibilityToJSON( object: FeatureSet_VisibilityFeature_DefaultSymbolVisibility, ): string { switch (object) { case FeatureSet_VisibilityFeature_DefaultSymbolVisibility.DEFAULT_SYMBOL_VISIBILITY_UNKNOWN: return "DEFAULT_SYMBOL_VISIBILITY_UNKNOWN"; case FeatureSet_VisibilityFeature_DefaultSymbolVisibility.EXPORT_ALL: return "EXPORT_ALL"; case FeatureSet_VisibilityFeature_DefaultSymbolVisibility.EXPORT_TOP_LEVEL: return "EXPORT_TOP_LEVEL"; case FeatureSet_VisibilityFeature_DefaultSymbolVisibility.LOCAL_ALL: return "LOCAL_ALL"; case FeatureSet_VisibilityFeature_DefaultSymbolVisibility.STRICT: return "STRICT"; case FeatureSet_VisibilityFeature_DefaultSymbolVisibility.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } /** * A compiled specification for the defaults of a set of features. These * messages are generated from FeatureSet extensions and can be used to seed * feature resolution. The resolution with this object becomes a simple search * for the closest matching edition, followed by proto merges. */ export interface FeatureSetDefaults { defaults: FeatureSetDefaults_FeatureSetEditionDefault[]; /** * The minimum supported edition (inclusive) when this was constructed. * Editions before this will not have defaults. */ minimumEdition?: | Edition | undefined; /** * The maximum known edition (inclusive) when this was constructed. Editions * after this will not have reliable defaults. */ maximumEdition?: Edition | undefined; } /** * A map from every known edition with a unique set of defaults to its * defaults. Not all editions may be contained here. For a given edition, * the defaults at the closest matching edition ordered at or before it should * be used. This field must be in strict ascending order by edition. */ export interface FeatureSetDefaults_FeatureSetEditionDefault { edition?: | Edition | undefined; /** Defaults of features that can be overridden in this edition. */ overridableFeatures?: | FeatureSet | undefined; /** Defaults of features that can't be overridden in this edition. */ fixedFeatures?: FeatureSet | undefined; } /** * Encapsulates information about the original source file from which a * FileDescriptorProto was generated. */ export interface SourceCodeInfo { /** * A Location identifies a piece of source code in a .proto file which * corresponds to a particular definition. This information is intended * to be useful to IDEs, code indexers, documentation generators, and similar * tools. * * For example, say we have a file like: * message Foo { * optional string foo = 1; * } * Let's look at just the field definition: * optional string foo = 1; * ^ ^^ ^^ ^ ^^^ * a bc de f ghi * We have the following locations: * span path represents * [a,i) [ 4, 0, 2, 0 ] The whole field definition. * [a,b) [ 4, 0, 2, 0, 4 ] The label (optional). * [c,d) [ 4, 0, 2, 0, 5 ] The type (string). * [e,f) [ 4, 0, 2, 0, 1 ] The name (foo). * [g,h) [ 4, 0, 2, 0, 3 ] The number (1). * * Notes: * - A location may refer to a repeated field itself (i.e. not to any * particular index within it). This is used whenever a set of elements are * logically enclosed in a single code segment. For example, an entire * extend block (possibly containing multiple extension definitions) will * have an outer location whose path refers to the "extensions" repeated * field without an index. * - Multiple locations may have the same path. This happens when a single * logical declaration is spread out across multiple places. The most * obvious example is the "extend" block again -- there may be multiple * extend blocks in the same scope, each of which will have the same path. * - A location's span is not always a subset of its parent's span. For * example, the "extendee" of an extension declaration appears at the * beginning of the "extend" block and is shared by all extensions within * the block. * - Just because a location's span is a subset of some other location's span * does not mean that it is a descendant. For example, a "group" defines * both a type and a field in a single declaration. Thus, the locations * corresponding to the type and field and their components will overlap. * - Code which tries to interpret locations should probably be designed to * ignore those that it doesn't understand, as more types of locations could * be recorded in the future. */ location: SourceCodeInfo_Location[]; } export interface SourceCodeInfo_Location { /** * Identifies which part of the FileDescriptorProto was defined at this * location. * * Each element is a field number or an index. They form a path from * the root FileDescriptorProto to the place where the definition appears. * For example, this path: * [ 4, 3, 2, 7, 1 ] * refers to: * file.message_type(3) // 4, 3 * .field(7) // 2, 7 * .name() // 1 * This is because FileDescriptorProto.message_type has field number 4: * repeated DescriptorProto message_type = 4; * and DescriptorProto.field has field number 2: * repeated FieldDescriptorProto field = 2; * and FieldDescriptorProto.name has field number 1: * optional string name = 1; * * Thus, the above path gives the location of a field name. If we removed * the last element: * [ 4, 3, 2, 7 ] * this path refers to the whole field declaration (from the beginning * of the label to the terminating semicolon). */ path: number[]; /** * Always has exactly three or four elements: start line, start column, * end line (optional, otherwise assumed same as start line), end column. * These are packed into a single field for efficiency. Note that line * and column numbers are zero-based -- typically you will want to add * 1 to each before displaying to a user. */ span: number[]; /** * If this SourceCodeInfo represents a complete declaration, these are any * comments appearing before and after the declaration which appear to be * attached to the declaration. * * A series of line comments appearing on consecutive lines, with no other * tokens appearing on those lines, will be treated as a single comment. * * leading_detached_comments will keep paragraphs of comments that appear * before (but not connected to) the current element. Each paragraph, * separated by empty lines, will be one comment element in the repeated * field. * * Only the comment content is provided; comment markers (e.g. //) are * stripped out. For block comments, leading whitespace and an asterisk * will be stripped from the beginning of each line other than the first. * Newlines are included in the output. * * Examples: * * optional int32 foo = 1; // Comment attached to foo. * // Comment attached to bar. * optional int32 bar = 2; * * optional string baz = 3; * // Comment attached to baz. * // Another line attached to baz. * * // Comment attached to moo. * // * // Another line attached to moo. * optional double moo = 4; * * // Detached comment for corge. This is not leading or trailing comments * // to moo or corge because there are blank lines separating it from * // both. * * // Detached comment for corge paragraph 2. * * optional string corge = 5; * /* Block comment attached * * to corge. Leading asterisks * * will be removed. * / * /* Block comment attached to * * grault. * / * optional int32 grault = 6; * * // ignored detached comments. */ leadingComments?: string | undefined; trailingComments?: string | undefined; leadingDetachedComments: string[]; } /** * Describes the relationship between generated code and its original source * file. A GeneratedCodeInfo message is associated with only one generated * source file, but may contain references to different source .proto files. */ export interface GeneratedCodeInfo { /** * An Annotation connects some span of text in generated code to an element * of its generating .proto file. */ annotation: GeneratedCodeInfo_Annotation[]; } export interface GeneratedCodeInfo_Annotation { /** * Identifies the element in the original source .proto file. This field * is formatted the same as SourceCodeInfo.Location.path. */ path: number[]; /** Identifies the filesystem path to the original source .proto. */ sourceFile?: | string | undefined; /** * Identifies the starting offset in bytes in the generated code * that relates to the identified object. */ begin?: | number | undefined; /** * Identifies the ending offset in bytes in the generated code that * relates to the identified object. The end offset should be one past * the last relevant byte (so the length of the text = end - begin). */ end?: number | undefined; semantic?: GeneratedCodeInfo_Annotation_Semantic | undefined; } /** * Represents the identified object's effect on the element in the original * .proto file. */ export enum GeneratedCodeInfo_Annotation_Semantic { /** NONE - There is no effect or the effect is indescribable. */ NONE = 0, /** SET - The element is set or otherwise mutated. */ SET = 1, /** ALIAS - An alias to the element is returned. */ ALIAS = 2, UNRECOGNIZED = -1, } export function generatedCodeInfo_Annotation_SemanticFromJSON(object: any): GeneratedCodeInfo_Annotation_Semantic { switch (object) { case 0: case "NONE": return GeneratedCodeInfo_Annotation_Semantic.NONE; case 1: case "SET": return GeneratedCodeInfo_Annotation_Semantic.SET; case 2: case "ALIAS": return GeneratedCodeInfo_Annotation_Semantic.ALIAS; case -1: case "UNRECOGNIZED": default: return GeneratedCodeInfo_Annotation_Semantic.UNRECOGNIZED; } } export function generatedCodeInfo_Annotation_SemanticToJSON(object: GeneratedCodeInfo_Annotation_Semantic): string { switch (object) { case GeneratedCodeInfo_Annotation_Semantic.NONE: return "NONE"; case GeneratedCodeInfo_Annotation_Semantic.SET: return "SET"; case GeneratedCodeInfo_Annotation_Semantic.ALIAS: return "ALIAS"; case GeneratedCodeInfo_Annotation_Semantic.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } function createBaseFileDescriptorSet(): FileDescriptorSet { return { file: [] }; } export const FileDescriptorSet: MessageFns = { encode(message: FileDescriptorSet, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { for (const v of message.file) { FileDescriptorProto.encode(v!, writer.uint32(10).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): FileDescriptorSet { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseFileDescriptorSet(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.file.push(FileDescriptorProto.decode(reader, reader.uint32())); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): FileDescriptorSet { return { file: globalThis.Array.isArray(object?.file) ? object.file.map((e: any) => FileDescriptorProto.fromJSON(e)) : [], }; }, toJSON(message: FileDescriptorSet): unknown { const obj: any = {}; if (message.file?.length) { obj.file = message.file.map((e) => FileDescriptorProto.toJSON(e)); } return obj; }, create, I>>(base?: I): FileDescriptorSet { return FileDescriptorSet.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): FileDescriptorSet { const message = createBaseFileDescriptorSet(); message.file = object.file?.map((e) => FileDescriptorProto.fromPartial(e)) || []; return message; }, }; function createBaseFileDescriptorProto(): FileDescriptorProto { return { name: "", package: "", dependency: [], publicDependency: [], weakDependency: [], optionDependency: [], messageType: [], enumType: [], service: [], extension: [], options: undefined, sourceCodeInfo: undefined, syntax: "", edition: 0, }; } export const FileDescriptorProto: MessageFns = { encode(message: FileDescriptorProto, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.name !== undefined && message.name !== "") { writer.uint32(10).string(message.name); } if (message.package !== undefined && message.package !== "") { writer.uint32(18).string(message.package); } for (const v of message.dependency) { writer.uint32(26).string(v!); } writer.uint32(82).fork(); for (const v of message.publicDependency) { writer.int32(v); } writer.join(); writer.uint32(90).fork(); for (const v of message.weakDependency) { writer.int32(v); } writer.join(); for (const v of message.optionDependency) { writer.uint32(122).string(v!); } for (const v of message.messageType) { DescriptorProto.encode(v!, writer.uint32(34).fork()).join(); } for (const v of message.enumType) { EnumDescriptorProto.encode(v!, writer.uint32(42).fork()).join(); } for (const v of message.service) { ServiceDescriptorProto.encode(v!, writer.uint32(50).fork()).join(); } for (const v of message.extension) { FieldDescriptorProto.encode(v!, writer.uint32(58).fork()).join(); } if (message.options !== undefined) { FileOptions.encode(message.options, writer.uint32(66).fork()).join(); } if (message.sourceCodeInfo !== undefined) { SourceCodeInfo.encode(message.sourceCodeInfo, writer.uint32(74).fork()).join(); } if (message.syntax !== undefined && message.syntax !== "") { writer.uint32(98).string(message.syntax); } if (message.edition !== undefined && message.edition !== 0) { writer.uint32(112).int32(message.edition); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): FileDescriptorProto { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseFileDescriptorProto(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.name = reader.string(); continue; case 2: if (tag !== 18) { break; } message.package = reader.string(); continue; case 3: if (tag !== 26) { break; } message.dependency.push(reader.string()); continue; case 10: if (tag === 80) { message.publicDependency.push(reader.int32()); continue; } if (tag === 82) { const end2 = reader.uint32() + reader.pos; while (reader.pos < end2) { message.publicDependency.push(reader.int32()); } continue; } break; case 11: if (tag === 88) { message.weakDependency.push(reader.int32()); continue; } if (tag === 90) { const end2 = reader.uint32() + reader.pos; while (reader.pos < end2) { message.weakDependency.push(reader.int32()); } continue; } break; case 15: if (tag !== 122) { break; } message.optionDependency.push(reader.string()); continue; case 4: if (tag !== 34) { break; } message.messageType.push(DescriptorProto.decode(reader, reader.uint32())); continue; case 5: if (tag !== 42) { break; } message.enumType.push(EnumDescriptorProto.decode(reader, reader.uint32())); continue; case 6: if (tag !== 50) { break; } message.service.push(ServiceDescriptorProto.decode(reader, reader.uint32())); continue; case 7: if (tag !== 58) { break; } message.extension.push(FieldDescriptorProto.decode(reader, reader.uint32())); continue; case 8: if (tag !== 66) { break; } message.options = FileOptions.decode(reader, reader.uint32()); continue; case 9: if (tag !== 74) { break; } message.sourceCodeInfo = SourceCodeInfo.decode(reader, reader.uint32()); continue; case 12: if (tag !== 98) { break; } message.syntax = reader.string(); continue; case 14: if (tag !== 112) { break; } message.edition = reader.int32() as any; continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): FileDescriptorProto { return { name: isSet(object.name) ? globalThis.String(object.name) : "", package: isSet(object.package) ? globalThis.String(object.package) : "", dependency: globalThis.Array.isArray(object?.dependency) ? object.dependency.map((e: any) => globalThis.String(e)) : [], publicDependency: globalThis.Array.isArray(object?.publicDependency) ? object.publicDependency.map((e: any) => globalThis.Number(e)) : [], weakDependency: globalThis.Array.isArray(object?.weakDependency) ? object.weakDependency.map((e: any) => globalThis.Number(e)) : [], optionDependency: globalThis.Array.isArray(object?.optionDependency) ? object.optionDependency.map((e: any) => globalThis.String(e)) : [], messageType: globalThis.Array.isArray(object?.messageType) ? object.messageType.map((e: any) => DescriptorProto.fromJSON(e)) : [], enumType: globalThis.Array.isArray(object?.enumType) ? object.enumType.map((e: any) => EnumDescriptorProto.fromJSON(e)) : [], service: globalThis.Array.isArray(object?.service) ? object.service.map((e: any) => ServiceDescriptorProto.fromJSON(e)) : [], extension: globalThis.Array.isArray(object?.extension) ? object.extension.map((e: any) => FieldDescriptorProto.fromJSON(e)) : [], options: isSet(object.options) ? FileOptions.fromJSON(object.options) : undefined, sourceCodeInfo: isSet(object.sourceCodeInfo) ? SourceCodeInfo.fromJSON(object.sourceCodeInfo) : undefined, syntax: isSet(object.syntax) ? globalThis.String(object.syntax) : "", edition: isSet(object.edition) ? editionFromJSON(object.edition) : 0, }; }, toJSON(message: FileDescriptorProto): unknown { const obj: any = {}; if (message.name !== undefined && message.name !== "") { obj.name = message.name; } if (message.package !== undefined && message.package !== "") { obj.package = message.package; } if (message.dependency?.length) { obj.dependency = message.dependency; } if (message.publicDependency?.length) { obj.publicDependency = message.publicDependency.map((e) => Math.round(e)); } if (message.weakDependency?.length) { obj.weakDependency = message.weakDependency.map((e) => Math.round(e)); } if (message.optionDependency?.length) { obj.optionDependency = message.optionDependency; } if (message.messageType?.length) { obj.messageType = message.messageType.map((e) => DescriptorProto.toJSON(e)); } if (message.enumType?.length) { obj.enumType = message.enumType.map((e) => EnumDescriptorProto.toJSON(e)); } if (message.service?.length) { obj.service = message.service.map((e) => ServiceDescriptorProto.toJSON(e)); } if (message.extension?.length) { obj.extension = message.extension.map((e) => FieldDescriptorProto.toJSON(e)); } if (message.options !== undefined) { obj.options = FileOptions.toJSON(message.options); } if (message.sourceCodeInfo !== undefined) { obj.sourceCodeInfo = SourceCodeInfo.toJSON(message.sourceCodeInfo); } if (message.syntax !== undefined && message.syntax !== "") { obj.syntax = message.syntax; } if (message.edition !== undefined && message.edition !== 0) { obj.edition = editionToJSON(message.edition); } return obj; }, create, I>>(base?: I): FileDescriptorProto { return FileDescriptorProto.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): FileDescriptorProto { const message = createBaseFileDescriptorProto(); message.name = object.name ?? ""; message.package = object.package ?? ""; message.dependency = object.dependency?.map((e) => e) || []; message.publicDependency = object.publicDependency?.map((e) => e) || []; message.weakDependency = object.weakDependency?.map((e) => e) || []; message.optionDependency = object.optionDependency?.map((e) => e) || []; message.messageType = object.messageType?.map((e) => DescriptorProto.fromPartial(e)) || []; message.enumType = object.enumType?.map((e) => EnumDescriptorProto.fromPartial(e)) || []; message.service = object.service?.map((e) => ServiceDescriptorProto.fromPartial(e)) || []; message.extension = object.extension?.map((e) => FieldDescriptorProto.fromPartial(e)) || []; message.options = (object.options !== undefined && object.options !== null) ? FileOptions.fromPartial(object.options) : undefined; message.sourceCodeInfo = (object.sourceCodeInfo !== undefined && object.sourceCodeInfo !== null) ? SourceCodeInfo.fromPartial(object.sourceCodeInfo) : undefined; message.syntax = object.syntax ?? ""; message.edition = object.edition ?? 0; return message; }, }; function createBaseDescriptorProto(): DescriptorProto { return { name: "", field: [], extension: [], nestedType: [], enumType: [], extensionRange: [], oneofDecl: [], options: undefined, reservedRange: [], reservedName: [], visibility: 0, }; } export const DescriptorProto: MessageFns = { encode(message: DescriptorProto, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.name !== undefined && message.name !== "") { writer.uint32(10).string(message.name); } for (const v of message.field) { FieldDescriptorProto.encode(v!, writer.uint32(18).fork()).join(); } for (const v of message.extension) { FieldDescriptorProto.encode(v!, writer.uint32(50).fork()).join(); } for (const v of message.nestedType) { DescriptorProto.encode(v!, writer.uint32(26).fork()).join(); } for (const v of message.enumType) { EnumDescriptorProto.encode(v!, writer.uint32(34).fork()).join(); } for (const v of message.extensionRange) { DescriptorProto_ExtensionRange.encode(v!, writer.uint32(42).fork()).join(); } for (const v of message.oneofDecl) { OneofDescriptorProto.encode(v!, writer.uint32(66).fork()).join(); } if (message.options !== undefined) { MessageOptions.encode(message.options, writer.uint32(58).fork()).join(); } for (const v of message.reservedRange) { DescriptorProto_ReservedRange.encode(v!, writer.uint32(74).fork()).join(); } for (const v of message.reservedName) { writer.uint32(82).string(v!); } if (message.visibility !== undefined && message.visibility !== 0) { writer.uint32(88).int32(message.visibility); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): DescriptorProto { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseDescriptorProto(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.name = reader.string(); continue; case 2: if (tag !== 18) { break; } message.field.push(FieldDescriptorProto.decode(reader, reader.uint32())); continue; case 6: if (tag !== 50) { break; } message.extension.push(FieldDescriptorProto.decode(reader, reader.uint32())); continue; case 3: if (tag !== 26) { break; } message.nestedType.push(DescriptorProto.decode(reader, reader.uint32())); continue; case 4: if (tag !== 34) { break; } message.enumType.push(EnumDescriptorProto.decode(reader, reader.uint32())); continue; case 5: if (tag !== 42) { break; } message.extensionRange.push(DescriptorProto_ExtensionRange.decode(reader, reader.uint32())); continue; case 8: if (tag !== 66) { break; } message.oneofDecl.push(OneofDescriptorProto.decode(reader, reader.uint32())); continue; case 7: if (tag !== 58) { break; } message.options = MessageOptions.decode(reader, reader.uint32()); continue; case 9: if (tag !== 74) { break; } message.reservedRange.push(DescriptorProto_ReservedRange.decode(reader, reader.uint32())); continue; case 10: if (tag !== 82) { break; } message.reservedName.push(reader.string()); continue; case 11: if (tag !== 88) { break; } message.visibility = reader.int32() as any; continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): DescriptorProto { return { name: isSet(object.name) ? globalThis.String(object.name) : "", field: globalThis.Array.isArray(object?.field) ? object.field.map((e: any) => FieldDescriptorProto.fromJSON(e)) : [], extension: globalThis.Array.isArray(object?.extension) ? object.extension.map((e: any) => FieldDescriptorProto.fromJSON(e)) : [], nestedType: globalThis.Array.isArray(object?.nestedType) ? object.nestedType.map((e: any) => DescriptorProto.fromJSON(e)) : [], enumType: globalThis.Array.isArray(object?.enumType) ? object.enumType.map((e: any) => EnumDescriptorProto.fromJSON(e)) : [], extensionRange: globalThis.Array.isArray(object?.extensionRange) ? object.extensionRange.map((e: any) => DescriptorProto_ExtensionRange.fromJSON(e)) : [], oneofDecl: globalThis.Array.isArray(object?.oneofDecl) ? object.oneofDecl.map((e: any) => OneofDescriptorProto.fromJSON(e)) : [], options: isSet(object.options) ? MessageOptions.fromJSON(object.options) : undefined, reservedRange: globalThis.Array.isArray(object?.reservedRange) ? object.reservedRange.map((e: any) => DescriptorProto_ReservedRange.fromJSON(e)) : [], reservedName: globalThis.Array.isArray(object?.reservedName) ? object.reservedName.map((e: any) => globalThis.String(e)) : [], visibility: isSet(object.visibility) ? symbolVisibilityFromJSON(object.visibility) : 0, }; }, toJSON(message: DescriptorProto): unknown { const obj: any = {}; if (message.name !== undefined && message.name !== "") { obj.name = message.name; } if (message.field?.length) { obj.field = message.field.map((e) => FieldDescriptorProto.toJSON(e)); } if (message.extension?.length) { obj.extension = message.extension.map((e) => FieldDescriptorProto.toJSON(e)); } if (message.nestedType?.length) { obj.nestedType = message.nestedType.map((e) => DescriptorProto.toJSON(e)); } if (message.enumType?.length) { obj.enumType = message.enumType.map((e) => EnumDescriptorProto.toJSON(e)); } if (message.extensionRange?.length) { obj.extensionRange = message.extensionRange.map((e) => DescriptorProto_ExtensionRange.toJSON(e)); } if (message.oneofDecl?.length) { obj.oneofDecl = message.oneofDecl.map((e) => OneofDescriptorProto.toJSON(e)); } if (message.options !== undefined) { obj.options = MessageOptions.toJSON(message.options); } if (message.reservedRange?.length) { obj.reservedRange = message.reservedRange.map((e) => DescriptorProto_ReservedRange.toJSON(e)); } if (message.reservedName?.length) { obj.reservedName = message.reservedName; } if (message.visibility !== undefined && message.visibility !== 0) { obj.visibility = symbolVisibilityToJSON(message.visibility); } return obj; }, create, I>>(base?: I): DescriptorProto { return DescriptorProto.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): DescriptorProto { const message = createBaseDescriptorProto(); message.name = object.name ?? ""; message.field = object.field?.map((e) => FieldDescriptorProto.fromPartial(e)) || []; message.extension = object.extension?.map((e) => FieldDescriptorProto.fromPartial(e)) || []; message.nestedType = object.nestedType?.map((e) => DescriptorProto.fromPartial(e)) || []; message.enumType = object.enumType?.map((e) => EnumDescriptorProto.fromPartial(e)) || []; message.extensionRange = object.extensionRange?.map((e) => DescriptorProto_ExtensionRange.fromPartial(e)) || []; message.oneofDecl = object.oneofDecl?.map((e) => OneofDescriptorProto.fromPartial(e)) || []; message.options = (object.options !== undefined && object.options !== null) ? MessageOptions.fromPartial(object.options) : undefined; message.reservedRange = object.reservedRange?.map((e) => DescriptorProto_ReservedRange.fromPartial(e)) || []; message.reservedName = object.reservedName?.map((e) => e) || []; message.visibility = object.visibility ?? 0; return message; }, }; function createBaseDescriptorProto_ExtensionRange(): DescriptorProto_ExtensionRange { return { start: 0, end: 0, options: undefined }; } export const DescriptorProto_ExtensionRange: MessageFns = { encode(message: DescriptorProto_ExtensionRange, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.start !== undefined && message.start !== 0) { writer.uint32(8).int32(message.start); } if (message.end !== undefined && message.end !== 0) { writer.uint32(16).int32(message.end); } if (message.options !== undefined) { ExtensionRangeOptions.encode(message.options, writer.uint32(26).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): DescriptorProto_ExtensionRange { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseDescriptorProto_ExtensionRange(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) { break; } message.start = reader.int32(); continue; case 2: if (tag !== 16) { break; } message.end = reader.int32(); continue; case 3: if (tag !== 26) { break; } message.options = ExtensionRangeOptions.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): DescriptorProto_ExtensionRange { return { start: isSet(object.start) ? globalThis.Number(object.start) : 0, end: isSet(object.end) ? globalThis.Number(object.end) : 0, options: isSet(object.options) ? ExtensionRangeOptions.fromJSON(object.options) : undefined, }; }, toJSON(message: DescriptorProto_ExtensionRange): unknown { const obj: any = {}; if (message.start !== undefined && message.start !== 0) { obj.start = Math.round(message.start); } if (message.end !== undefined && message.end !== 0) { obj.end = Math.round(message.end); } if (message.options !== undefined) { obj.options = ExtensionRangeOptions.toJSON(message.options); } return obj; }, create, I>>(base?: I): DescriptorProto_ExtensionRange { return DescriptorProto_ExtensionRange.fromPartial(base ?? ({} as any)); }, fromPartial, I>>( object: I, ): DescriptorProto_ExtensionRange { const message = createBaseDescriptorProto_ExtensionRange(); message.start = object.start ?? 0; message.end = object.end ?? 0; message.options = (object.options !== undefined && object.options !== null) ? ExtensionRangeOptions.fromPartial(object.options) : undefined; return message; }, }; function createBaseDescriptorProto_ReservedRange(): DescriptorProto_ReservedRange { return { start: 0, end: 0 }; } export const DescriptorProto_ReservedRange: MessageFns = { encode(message: DescriptorProto_ReservedRange, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.start !== undefined && message.start !== 0) { writer.uint32(8).int32(message.start); } if (message.end !== undefined && message.end !== 0) { writer.uint32(16).int32(message.end); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): DescriptorProto_ReservedRange { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseDescriptorProto_ReservedRange(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) { break; } message.start = reader.int32(); continue; case 2: if (tag !== 16) { break; } message.end = reader.int32(); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): DescriptorProto_ReservedRange { return { start: isSet(object.start) ? globalThis.Number(object.start) : 0, end: isSet(object.end) ? globalThis.Number(object.end) : 0, }; }, toJSON(message: DescriptorProto_ReservedRange): unknown { const obj: any = {}; if (message.start !== undefined && message.start !== 0) { obj.start = Math.round(message.start); } if (message.end !== undefined && message.end !== 0) { obj.end = Math.round(message.end); } return obj; }, create, I>>(base?: I): DescriptorProto_ReservedRange { return DescriptorProto_ReservedRange.fromPartial(base ?? ({} as any)); }, fromPartial, I>>( object: I, ): DescriptorProto_ReservedRange { const message = createBaseDescriptorProto_ReservedRange(); message.start = object.start ?? 0; message.end = object.end ?? 0; return message; }, }; function createBaseExtensionRangeOptions(): ExtensionRangeOptions { return { uninterpretedOption: [], declaration: [], features: undefined, verification: 1 }; } export const ExtensionRangeOptions: MessageFns = { encode(message: ExtensionRangeOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { for (const v of message.uninterpretedOption) { UninterpretedOption.encode(v!, writer.uint32(7994).fork()).join(); } for (const v of message.declaration) { ExtensionRangeOptions_Declaration.encode(v!, writer.uint32(18).fork()).join(); } if (message.features !== undefined) { FeatureSet.encode(message.features, writer.uint32(402).fork()).join(); } if (message.verification !== undefined && message.verification !== 1) { writer.uint32(24).int32(message.verification); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): ExtensionRangeOptions { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseExtensionRangeOptions(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 999: if (tag !== 7994) { break; } message.uninterpretedOption.push(UninterpretedOption.decode(reader, reader.uint32())); continue; case 2: if (tag !== 18) { break; } message.declaration.push(ExtensionRangeOptions_Declaration.decode(reader, reader.uint32())); continue; case 50: if (tag !== 402) { break; } message.features = FeatureSet.decode(reader, reader.uint32()); continue; case 3: if (tag !== 24) { break; } message.verification = reader.int32() as any; continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): ExtensionRangeOptions { return { uninterpretedOption: globalThis.Array.isArray(object?.uninterpretedOption) ? object.uninterpretedOption.map((e: any) => UninterpretedOption.fromJSON(e)) : [], declaration: globalThis.Array.isArray(object?.declaration) ? object.declaration.map((e: any) => ExtensionRangeOptions_Declaration.fromJSON(e)) : [], features: isSet(object.features) ? FeatureSet.fromJSON(object.features) : undefined, verification: isSet(object.verification) ? extensionRangeOptions_VerificationStateFromJSON(object.verification) : 1, }; }, toJSON(message: ExtensionRangeOptions): unknown { const obj: any = {}; if (message.uninterpretedOption?.length) { obj.uninterpretedOption = message.uninterpretedOption.map((e) => UninterpretedOption.toJSON(e)); } if (message.declaration?.length) { obj.declaration = message.declaration.map((e) => ExtensionRangeOptions_Declaration.toJSON(e)); } if (message.features !== undefined) { obj.features = FeatureSet.toJSON(message.features); } if (message.verification !== undefined && message.verification !== 1) { obj.verification = extensionRangeOptions_VerificationStateToJSON(message.verification); } return obj; }, create, I>>(base?: I): ExtensionRangeOptions { return ExtensionRangeOptions.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): ExtensionRangeOptions { const message = createBaseExtensionRangeOptions(); message.uninterpretedOption = object.uninterpretedOption?.map((e) => UninterpretedOption.fromPartial(e)) || []; message.declaration = object.declaration?.map((e) => ExtensionRangeOptions_Declaration.fromPartial(e)) || []; message.features = (object.features !== undefined && object.features !== null) ? FeatureSet.fromPartial(object.features) : undefined; message.verification = object.verification ?? 1; return message; }, }; function createBaseExtensionRangeOptions_Declaration(): ExtensionRangeOptions_Declaration { return { number: 0, fullName: "", type: "", reserved: false, repeated: false }; } export const ExtensionRangeOptions_Declaration: MessageFns = { encode(message: ExtensionRangeOptions_Declaration, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.number !== undefined && message.number !== 0) { writer.uint32(8).int32(message.number); } if (message.fullName !== undefined && message.fullName !== "") { writer.uint32(18).string(message.fullName); } if (message.type !== undefined && message.type !== "") { writer.uint32(26).string(message.type); } if (message.reserved !== undefined && message.reserved !== false) { writer.uint32(40).bool(message.reserved); } if (message.repeated !== undefined && message.repeated !== false) { writer.uint32(48).bool(message.repeated); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): ExtensionRangeOptions_Declaration { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseExtensionRangeOptions_Declaration(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) { break; } message.number = reader.int32(); continue; case 2: if (tag !== 18) { break; } message.fullName = reader.string(); continue; case 3: if (tag !== 26) { break; } message.type = reader.string(); continue; case 5: if (tag !== 40) { break; } message.reserved = reader.bool(); continue; case 6: if (tag !== 48) { break; } message.repeated = reader.bool(); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): ExtensionRangeOptions_Declaration { return { number: isSet(object.number) ? globalThis.Number(object.number) : 0, fullName: isSet(object.fullName) ? globalThis.String(object.fullName) : "", type: isSet(object.type) ? globalThis.String(object.type) : "", reserved: isSet(object.reserved) ? globalThis.Boolean(object.reserved) : false, repeated: isSet(object.repeated) ? globalThis.Boolean(object.repeated) : false, }; }, toJSON(message: ExtensionRangeOptions_Declaration): unknown { const obj: any = {}; if (message.number !== undefined && message.number !== 0) { obj.number = Math.round(message.number); } if (message.fullName !== undefined && message.fullName !== "") { obj.fullName = message.fullName; } if (message.type !== undefined && message.type !== "") { obj.type = message.type; } if (message.reserved !== undefined && message.reserved !== false) { obj.reserved = message.reserved; } if (message.repeated !== undefined && message.repeated !== false) { obj.repeated = message.repeated; } return obj; }, create, I>>( base?: I, ): ExtensionRangeOptions_Declaration { return ExtensionRangeOptions_Declaration.fromPartial(base ?? ({} as any)); }, fromPartial, I>>( object: I, ): ExtensionRangeOptions_Declaration { const message = createBaseExtensionRangeOptions_Declaration(); message.number = object.number ?? 0; message.fullName = object.fullName ?? ""; message.type = object.type ?? ""; message.reserved = object.reserved ?? false; message.repeated = object.repeated ?? false; return message; }, }; function createBaseFieldDescriptorProto(): FieldDescriptorProto { return { name: "", number: 0, label: 1, type: 1, typeName: "", extendee: "", defaultValue: "", oneofIndex: 0, jsonName: "", options: undefined, proto3Optional: false, }; } export const FieldDescriptorProto: MessageFns = { encode(message: FieldDescriptorProto, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.name !== undefined && message.name !== "") { writer.uint32(10).string(message.name); } if (message.number !== undefined && message.number !== 0) { writer.uint32(24).int32(message.number); } if (message.label !== undefined && message.label !== 1) { writer.uint32(32).int32(message.label); } if (message.type !== undefined && message.type !== 1) { writer.uint32(40).int32(message.type); } if (message.typeName !== undefined && message.typeName !== "") { writer.uint32(50).string(message.typeName); } if (message.extendee !== undefined && message.extendee !== "") { writer.uint32(18).string(message.extendee); } if (message.defaultValue !== undefined && message.defaultValue !== "") { writer.uint32(58).string(message.defaultValue); } if (message.oneofIndex !== undefined && message.oneofIndex !== 0) { writer.uint32(72).int32(message.oneofIndex); } if (message.jsonName !== undefined && message.jsonName !== "") { writer.uint32(82).string(message.jsonName); } if (message.options !== undefined) { FieldOptions.encode(message.options, writer.uint32(66).fork()).join(); } if (message.proto3Optional !== undefined && message.proto3Optional !== false) { writer.uint32(136).bool(message.proto3Optional); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): FieldDescriptorProto { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseFieldDescriptorProto(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.name = reader.string(); continue; case 3: if (tag !== 24) { break; } message.number = reader.int32(); continue; case 4: if (tag !== 32) { break; } message.label = reader.int32() as any; continue; case 5: if (tag !== 40) { break; } message.type = reader.int32() as any; continue; case 6: if (tag !== 50) { break; } message.typeName = reader.string(); continue; case 2: if (tag !== 18) { break; } message.extendee = reader.string(); continue; case 7: if (tag !== 58) { break; } message.defaultValue = reader.string(); continue; case 9: if (tag !== 72) { break; } message.oneofIndex = reader.int32(); continue; case 10: if (tag !== 82) { break; } message.jsonName = reader.string(); continue; case 8: if (tag !== 66) { break; } message.options = FieldOptions.decode(reader, reader.uint32()); continue; case 17: if (tag !== 136) { break; } message.proto3Optional = reader.bool(); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): FieldDescriptorProto { return { name: isSet(object.name) ? globalThis.String(object.name) : "", number: isSet(object.number) ? globalThis.Number(object.number) : 0, label: isSet(object.label) ? fieldDescriptorProto_LabelFromJSON(object.label) : 1, type: isSet(object.type) ? fieldDescriptorProto_TypeFromJSON(object.type) : 1, typeName: isSet(object.typeName) ? globalThis.String(object.typeName) : "", extendee: isSet(object.extendee) ? globalThis.String(object.extendee) : "", defaultValue: isSet(object.defaultValue) ? globalThis.String(object.defaultValue) : "", oneofIndex: isSet(object.oneofIndex) ? globalThis.Number(object.oneofIndex) : 0, jsonName: isSet(object.jsonName) ? globalThis.String(object.jsonName) : "", options: isSet(object.options) ? FieldOptions.fromJSON(object.options) : undefined, proto3Optional: isSet(object.proto3Optional) ? globalThis.Boolean(object.proto3Optional) : false, }; }, toJSON(message: FieldDescriptorProto): unknown { const obj: any = {}; if (message.name !== undefined && message.name !== "") { obj.name = message.name; } if (message.number !== undefined && message.number !== 0) { obj.number = Math.round(message.number); } if (message.label !== undefined && message.label !== 1) { obj.label = fieldDescriptorProto_LabelToJSON(message.label); } if (message.type !== undefined && message.type !== 1) { obj.type = fieldDescriptorProto_TypeToJSON(message.type); } if (message.typeName !== undefined && message.typeName !== "") { obj.typeName = message.typeName; } if (message.extendee !== undefined && message.extendee !== "") { obj.extendee = message.extendee; } if (message.defaultValue !== undefined && message.defaultValue !== "") { obj.defaultValue = message.defaultValue; } if (message.oneofIndex !== undefined && message.oneofIndex !== 0) { obj.oneofIndex = Math.round(message.oneofIndex); } if (message.jsonName !== undefined && message.jsonName !== "") { obj.jsonName = message.jsonName; } if (message.options !== undefined) { obj.options = FieldOptions.toJSON(message.options); } if (message.proto3Optional !== undefined && message.proto3Optional !== false) { obj.proto3Optional = message.proto3Optional; } return obj; }, create, I>>(base?: I): FieldDescriptorProto { return FieldDescriptorProto.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): FieldDescriptorProto { const message = createBaseFieldDescriptorProto(); message.name = object.name ?? ""; message.number = object.number ?? 0; message.label = object.label ?? 1; message.type = object.type ?? 1; message.typeName = object.typeName ?? ""; message.extendee = object.extendee ?? ""; message.defaultValue = object.defaultValue ?? ""; message.oneofIndex = object.oneofIndex ?? 0; message.jsonName = object.jsonName ?? ""; message.options = (object.options !== undefined && object.options !== null) ? FieldOptions.fromPartial(object.options) : undefined; message.proto3Optional = object.proto3Optional ?? false; return message; }, }; function createBaseOneofDescriptorProto(): OneofDescriptorProto { return { name: "", options: undefined }; } export const OneofDescriptorProto: MessageFns = { encode(message: OneofDescriptorProto, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.name !== undefined && message.name !== "") { writer.uint32(10).string(message.name); } if (message.options !== undefined) { OneofOptions.encode(message.options, writer.uint32(18).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): OneofDescriptorProto { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseOneofDescriptorProto(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.name = reader.string(); continue; case 2: if (tag !== 18) { break; } message.options = OneofOptions.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): OneofDescriptorProto { return { name: isSet(object.name) ? globalThis.String(object.name) : "", options: isSet(object.options) ? OneofOptions.fromJSON(object.options) : undefined, }; }, toJSON(message: OneofDescriptorProto): unknown { const obj: any = {}; if (message.name !== undefined && message.name !== "") { obj.name = message.name; } if (message.options !== undefined) { obj.options = OneofOptions.toJSON(message.options); } return obj; }, create, I>>(base?: I): OneofDescriptorProto { return OneofDescriptorProto.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): OneofDescriptorProto { const message = createBaseOneofDescriptorProto(); message.name = object.name ?? ""; message.options = (object.options !== undefined && object.options !== null) ? OneofOptions.fromPartial(object.options) : undefined; return message; }, }; function createBaseEnumDescriptorProto(): EnumDescriptorProto { return { name: "", value: [], options: undefined, reservedRange: [], reservedName: [], visibility: 0 }; } export const EnumDescriptorProto: MessageFns = { encode(message: EnumDescriptorProto, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.name !== undefined && message.name !== "") { writer.uint32(10).string(message.name); } for (const v of message.value) { EnumValueDescriptorProto.encode(v!, writer.uint32(18).fork()).join(); } if (message.options !== undefined) { EnumOptions.encode(message.options, writer.uint32(26).fork()).join(); } for (const v of message.reservedRange) { EnumDescriptorProto_EnumReservedRange.encode(v!, writer.uint32(34).fork()).join(); } for (const v of message.reservedName) { writer.uint32(42).string(v!); } if (message.visibility !== undefined && message.visibility !== 0) { writer.uint32(48).int32(message.visibility); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): EnumDescriptorProto { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseEnumDescriptorProto(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.name = reader.string(); continue; case 2: if (tag !== 18) { break; } message.value.push(EnumValueDescriptorProto.decode(reader, reader.uint32())); continue; case 3: if (tag !== 26) { break; } message.options = EnumOptions.decode(reader, reader.uint32()); continue; case 4: if (tag !== 34) { break; } message.reservedRange.push(EnumDescriptorProto_EnumReservedRange.decode(reader, reader.uint32())); continue; case 5: if (tag !== 42) { break; } message.reservedName.push(reader.string()); continue; case 6: if (tag !== 48) { break; } message.visibility = reader.int32() as any; continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): EnumDescriptorProto { return { name: isSet(object.name) ? globalThis.String(object.name) : "", value: globalThis.Array.isArray(object?.value) ? object.value.map((e: any) => EnumValueDescriptorProto.fromJSON(e)) : [], options: isSet(object.options) ? EnumOptions.fromJSON(object.options) : undefined, reservedRange: globalThis.Array.isArray(object?.reservedRange) ? object.reservedRange.map((e: any) => EnumDescriptorProto_EnumReservedRange.fromJSON(e)) : [], reservedName: globalThis.Array.isArray(object?.reservedName) ? object.reservedName.map((e: any) => globalThis.String(e)) : [], visibility: isSet(object.visibility) ? symbolVisibilityFromJSON(object.visibility) : 0, }; }, toJSON(message: EnumDescriptorProto): unknown { const obj: any = {}; if (message.name !== undefined && message.name !== "") { obj.name = message.name; } if (message.value?.length) { obj.value = message.value.map((e) => EnumValueDescriptorProto.toJSON(e)); } if (message.options !== undefined) { obj.options = EnumOptions.toJSON(message.options); } if (message.reservedRange?.length) { obj.reservedRange = message.reservedRange.map((e) => EnumDescriptorProto_EnumReservedRange.toJSON(e)); } if (message.reservedName?.length) { obj.reservedName = message.reservedName; } if (message.visibility !== undefined && message.visibility !== 0) { obj.visibility = symbolVisibilityToJSON(message.visibility); } return obj; }, create, I>>(base?: I): EnumDescriptorProto { return EnumDescriptorProto.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): EnumDescriptorProto { const message = createBaseEnumDescriptorProto(); message.name = object.name ?? ""; message.value = object.value?.map((e) => EnumValueDescriptorProto.fromPartial(e)) || []; message.options = (object.options !== undefined && object.options !== null) ? EnumOptions.fromPartial(object.options) : undefined; message.reservedRange = object.reservedRange?.map((e) => EnumDescriptorProto_EnumReservedRange.fromPartial(e)) || []; message.reservedName = object.reservedName?.map((e) => e) || []; message.visibility = object.visibility ?? 0; return message; }, }; function createBaseEnumDescriptorProto_EnumReservedRange(): EnumDescriptorProto_EnumReservedRange { return { start: 0, end: 0 }; } export const EnumDescriptorProto_EnumReservedRange: MessageFns = { encode(message: EnumDescriptorProto_EnumReservedRange, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.start !== undefined && message.start !== 0) { writer.uint32(8).int32(message.start); } if (message.end !== undefined && message.end !== 0) { writer.uint32(16).int32(message.end); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): EnumDescriptorProto_EnumReservedRange { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseEnumDescriptorProto_EnumReservedRange(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) { break; } message.start = reader.int32(); continue; case 2: if (tag !== 16) { break; } message.end = reader.int32(); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): EnumDescriptorProto_EnumReservedRange { return { start: isSet(object.start) ? globalThis.Number(object.start) : 0, end: isSet(object.end) ? globalThis.Number(object.end) : 0, }; }, toJSON(message: EnumDescriptorProto_EnumReservedRange): unknown { const obj: any = {}; if (message.start !== undefined && message.start !== 0) { obj.start = Math.round(message.start); } if (message.end !== undefined && message.end !== 0) { obj.end = Math.round(message.end); } return obj; }, create, I>>( base?: I, ): EnumDescriptorProto_EnumReservedRange { return EnumDescriptorProto_EnumReservedRange.fromPartial(base ?? ({} as any)); }, fromPartial, I>>( object: I, ): EnumDescriptorProto_EnumReservedRange { const message = createBaseEnumDescriptorProto_EnumReservedRange(); message.start = object.start ?? 0; message.end = object.end ?? 0; return message; }, }; function createBaseEnumValueDescriptorProto(): EnumValueDescriptorProto { return { name: "", number: 0, options: undefined }; } export const EnumValueDescriptorProto: MessageFns = { encode(message: EnumValueDescriptorProto, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.name !== undefined && message.name !== "") { writer.uint32(10).string(message.name); } if (message.number !== undefined && message.number !== 0) { writer.uint32(16).int32(message.number); } if (message.options !== undefined) { EnumValueOptions.encode(message.options, writer.uint32(26).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): EnumValueDescriptorProto { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseEnumValueDescriptorProto(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.name = reader.string(); continue; case 2: if (tag !== 16) { break; } message.number = reader.int32(); continue; case 3: if (tag !== 26) { break; } message.options = EnumValueOptions.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): EnumValueDescriptorProto { return { name: isSet(object.name) ? globalThis.String(object.name) : "", number: isSet(object.number) ? globalThis.Number(object.number) : 0, options: isSet(object.options) ? EnumValueOptions.fromJSON(object.options) : undefined, }; }, toJSON(message: EnumValueDescriptorProto): unknown { const obj: any = {}; if (message.name !== undefined && message.name !== "") { obj.name = message.name; } if (message.number !== undefined && message.number !== 0) { obj.number = Math.round(message.number); } if (message.options !== undefined) { obj.options = EnumValueOptions.toJSON(message.options); } return obj; }, create, I>>(base?: I): EnumValueDescriptorProto { return EnumValueDescriptorProto.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): EnumValueDescriptorProto { const message = createBaseEnumValueDescriptorProto(); message.name = object.name ?? ""; message.number = object.number ?? 0; message.options = (object.options !== undefined && object.options !== null) ? EnumValueOptions.fromPartial(object.options) : undefined; return message; }, }; function createBaseServiceDescriptorProto(): ServiceDescriptorProto { return { name: "", method: [], options: undefined }; } export const ServiceDescriptorProto: MessageFns = { encode(message: ServiceDescriptorProto, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.name !== undefined && message.name !== "") { writer.uint32(10).string(message.name); } for (const v of message.method) { MethodDescriptorProto.encode(v!, writer.uint32(18).fork()).join(); } if (message.options !== undefined) { ServiceOptions.encode(message.options, writer.uint32(26).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): ServiceDescriptorProto { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseServiceDescriptorProto(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.name = reader.string(); continue; case 2: if (tag !== 18) { break; } message.method.push(MethodDescriptorProto.decode(reader, reader.uint32())); continue; case 3: if (tag !== 26) { break; } message.options = ServiceOptions.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): ServiceDescriptorProto { return { name: isSet(object.name) ? globalThis.String(object.name) : "", method: globalThis.Array.isArray(object?.method) ? object.method.map((e: any) => MethodDescriptorProto.fromJSON(e)) : [], options: isSet(object.options) ? ServiceOptions.fromJSON(object.options) : undefined, }; }, toJSON(message: ServiceDescriptorProto): unknown { const obj: any = {}; if (message.name !== undefined && message.name !== "") { obj.name = message.name; } if (message.method?.length) { obj.method = message.method.map((e) => MethodDescriptorProto.toJSON(e)); } if (message.options !== undefined) { obj.options = ServiceOptions.toJSON(message.options); } return obj; }, create, I>>(base?: I): ServiceDescriptorProto { return ServiceDescriptorProto.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): ServiceDescriptorProto { const message = createBaseServiceDescriptorProto(); message.name = object.name ?? ""; message.method = object.method?.map((e) => MethodDescriptorProto.fromPartial(e)) || []; message.options = (object.options !== undefined && object.options !== null) ? ServiceOptions.fromPartial(object.options) : undefined; return message; }, }; function createBaseMethodDescriptorProto(): MethodDescriptorProto { return { name: "", inputType: "", outputType: "", options: undefined, clientStreaming: false, serverStreaming: false, }; } export const MethodDescriptorProto: MessageFns = { encode(message: MethodDescriptorProto, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.name !== undefined && message.name !== "") { writer.uint32(10).string(message.name); } if (message.inputType !== undefined && message.inputType !== "") { writer.uint32(18).string(message.inputType); } if (message.outputType !== undefined && message.outputType !== "") { writer.uint32(26).string(message.outputType); } if (message.options !== undefined) { MethodOptions.encode(message.options, writer.uint32(34).fork()).join(); } if (message.clientStreaming !== undefined && message.clientStreaming !== false) { writer.uint32(40).bool(message.clientStreaming); } if (message.serverStreaming !== undefined && message.serverStreaming !== false) { writer.uint32(48).bool(message.serverStreaming); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): MethodDescriptorProto { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseMethodDescriptorProto(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.name = reader.string(); continue; case 2: if (tag !== 18) { break; } message.inputType = reader.string(); continue; case 3: if (tag !== 26) { break; } message.outputType = reader.string(); continue; case 4: if (tag !== 34) { break; } message.options = MethodOptions.decode(reader, reader.uint32()); continue; case 5: if (tag !== 40) { break; } message.clientStreaming = reader.bool(); continue; case 6: if (tag !== 48) { break; } message.serverStreaming = reader.bool(); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): MethodDescriptorProto { return { name: isSet(object.name) ? globalThis.String(object.name) : "", inputType: isSet(object.inputType) ? globalThis.String(object.inputType) : "", outputType: isSet(object.outputType) ? globalThis.String(object.outputType) : "", options: isSet(object.options) ? MethodOptions.fromJSON(object.options) : undefined, clientStreaming: isSet(object.clientStreaming) ? globalThis.Boolean(object.clientStreaming) : false, serverStreaming: isSet(object.serverStreaming) ? globalThis.Boolean(object.serverStreaming) : false, }; }, toJSON(message: MethodDescriptorProto): unknown { const obj: any = {}; if (message.name !== undefined && message.name !== "") { obj.name = message.name; } if (message.inputType !== undefined && message.inputType !== "") { obj.inputType = message.inputType; } if (message.outputType !== undefined && message.outputType !== "") { obj.outputType = message.outputType; } if (message.options !== undefined) { obj.options = MethodOptions.toJSON(message.options); } if (message.clientStreaming !== undefined && message.clientStreaming !== false) { obj.clientStreaming = message.clientStreaming; } if (message.serverStreaming !== undefined && message.serverStreaming !== false) { obj.serverStreaming = message.serverStreaming; } return obj; }, create, I>>(base?: I): MethodDescriptorProto { return MethodDescriptorProto.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): MethodDescriptorProto { const message = createBaseMethodDescriptorProto(); message.name = object.name ?? ""; message.inputType = object.inputType ?? ""; message.outputType = object.outputType ?? ""; message.options = (object.options !== undefined && object.options !== null) ? MethodOptions.fromPartial(object.options) : undefined; message.clientStreaming = object.clientStreaming ?? false; message.serverStreaming = object.serverStreaming ?? false; return message; }, }; function createBaseFileOptions(): FileOptions { return { javaPackage: "", javaOuterClassname: "", javaMultipleFiles: false, javaGenerateEqualsAndHash: false, javaStringCheckUtf8: false, optimizeFor: 1, goPackage: "", ccGenericServices: false, javaGenericServices: false, pyGenericServices: false, deprecated: false, ccEnableArenas: true, objcClassPrefix: "", csharpNamespace: "", swiftPrefix: "", phpClassPrefix: "", phpNamespace: "", phpMetadataNamespace: "", rubyPackage: "", features: undefined, uninterpretedOption: [], }; } export const FileOptions: MessageFns = { encode(message: FileOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.javaPackage !== undefined && message.javaPackage !== "") { writer.uint32(10).string(message.javaPackage); } if (message.javaOuterClassname !== undefined && message.javaOuterClassname !== "") { writer.uint32(66).string(message.javaOuterClassname); } if (message.javaMultipleFiles !== undefined && message.javaMultipleFiles !== false) { writer.uint32(80).bool(message.javaMultipleFiles); } if (message.javaGenerateEqualsAndHash !== undefined && message.javaGenerateEqualsAndHash !== false) { writer.uint32(160).bool(message.javaGenerateEqualsAndHash); } if (message.javaStringCheckUtf8 !== undefined && message.javaStringCheckUtf8 !== false) { writer.uint32(216).bool(message.javaStringCheckUtf8); } if (message.optimizeFor !== undefined && message.optimizeFor !== 1) { writer.uint32(72).int32(message.optimizeFor); } if (message.goPackage !== undefined && message.goPackage !== "") { writer.uint32(90).string(message.goPackage); } if (message.ccGenericServices !== undefined && message.ccGenericServices !== false) { writer.uint32(128).bool(message.ccGenericServices); } if (message.javaGenericServices !== undefined && message.javaGenericServices !== false) { writer.uint32(136).bool(message.javaGenericServices); } if (message.pyGenericServices !== undefined && message.pyGenericServices !== false) { writer.uint32(144).bool(message.pyGenericServices); } if (message.deprecated !== undefined && message.deprecated !== false) { writer.uint32(184).bool(message.deprecated); } if (message.ccEnableArenas !== undefined && message.ccEnableArenas !== true) { writer.uint32(248).bool(message.ccEnableArenas); } if (message.objcClassPrefix !== undefined && message.objcClassPrefix !== "") { writer.uint32(290).string(message.objcClassPrefix); } if (message.csharpNamespace !== undefined && message.csharpNamespace !== "") { writer.uint32(298).string(message.csharpNamespace); } if (message.swiftPrefix !== undefined && message.swiftPrefix !== "") { writer.uint32(314).string(message.swiftPrefix); } if (message.phpClassPrefix !== undefined && message.phpClassPrefix !== "") { writer.uint32(322).string(message.phpClassPrefix); } if (message.phpNamespace !== undefined && message.phpNamespace !== "") { writer.uint32(330).string(message.phpNamespace); } if (message.phpMetadataNamespace !== undefined && message.phpMetadataNamespace !== "") { writer.uint32(354).string(message.phpMetadataNamespace); } if (message.rubyPackage !== undefined && message.rubyPackage !== "") { writer.uint32(362).string(message.rubyPackage); } if (message.features !== undefined) { FeatureSet.encode(message.features, writer.uint32(402).fork()).join(); } for (const v of message.uninterpretedOption) { UninterpretedOption.encode(v!, writer.uint32(7994).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): FileOptions { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseFileOptions(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.javaPackage = reader.string(); continue; case 8: if (tag !== 66) { break; } message.javaOuterClassname = reader.string(); continue; case 10: if (tag !== 80) { break; } message.javaMultipleFiles = reader.bool(); continue; case 20: if (tag !== 160) { break; } message.javaGenerateEqualsAndHash = reader.bool(); continue; case 27: if (tag !== 216) { break; } message.javaStringCheckUtf8 = reader.bool(); continue; case 9: if (tag !== 72) { break; } message.optimizeFor = reader.int32() as any; continue; case 11: if (tag !== 90) { break; } message.goPackage = reader.string(); continue; case 16: if (tag !== 128) { break; } message.ccGenericServices = reader.bool(); continue; case 17: if (tag !== 136) { break; } message.javaGenericServices = reader.bool(); continue; case 18: if (tag !== 144) { break; } message.pyGenericServices = reader.bool(); continue; case 23: if (tag !== 184) { break; } message.deprecated = reader.bool(); continue; case 31: if (tag !== 248) { break; } message.ccEnableArenas = reader.bool(); continue; case 36: if (tag !== 290) { break; } message.objcClassPrefix = reader.string(); continue; case 37: if (tag !== 298) { break; } message.csharpNamespace = reader.string(); continue; case 39: if (tag !== 314) { break; } message.swiftPrefix = reader.string(); continue; case 40: if (tag !== 322) { break; } message.phpClassPrefix = reader.string(); continue; case 41: if (tag !== 330) { break; } message.phpNamespace = reader.string(); continue; case 44: if (tag !== 354) { break; } message.phpMetadataNamespace = reader.string(); continue; case 45: if (tag !== 362) { break; } message.rubyPackage = reader.string(); continue; case 50: if (tag !== 402) { break; } message.features = FeatureSet.decode(reader, reader.uint32()); continue; case 999: if (tag !== 7994) { break; } message.uninterpretedOption.push(UninterpretedOption.decode(reader, reader.uint32())); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): FileOptions { return { javaPackage: isSet(object.javaPackage) ? globalThis.String(object.javaPackage) : "", javaOuterClassname: isSet(object.javaOuterClassname) ? globalThis.String(object.javaOuterClassname) : "", javaMultipleFiles: isSet(object.javaMultipleFiles) ? globalThis.Boolean(object.javaMultipleFiles) : false, javaGenerateEqualsAndHash: isSet(object.javaGenerateEqualsAndHash) ? globalThis.Boolean(object.javaGenerateEqualsAndHash) : false, javaStringCheckUtf8: isSet(object.javaStringCheckUtf8) ? globalThis.Boolean(object.javaStringCheckUtf8) : false, optimizeFor: isSet(object.optimizeFor) ? fileOptions_OptimizeModeFromJSON(object.optimizeFor) : 1, goPackage: isSet(object.goPackage) ? globalThis.String(object.goPackage) : "", ccGenericServices: isSet(object.ccGenericServices) ? globalThis.Boolean(object.ccGenericServices) : false, javaGenericServices: isSet(object.javaGenericServices) ? globalThis.Boolean(object.javaGenericServices) : false, pyGenericServices: isSet(object.pyGenericServices) ? globalThis.Boolean(object.pyGenericServices) : false, deprecated: isSet(object.deprecated) ? globalThis.Boolean(object.deprecated) : false, ccEnableArenas: isSet(object.ccEnableArenas) ? globalThis.Boolean(object.ccEnableArenas) : true, objcClassPrefix: isSet(object.objcClassPrefix) ? globalThis.String(object.objcClassPrefix) : "", csharpNamespace: isSet(object.csharpNamespace) ? globalThis.String(object.csharpNamespace) : "", swiftPrefix: isSet(object.swiftPrefix) ? globalThis.String(object.swiftPrefix) : "", phpClassPrefix: isSet(object.phpClassPrefix) ? globalThis.String(object.phpClassPrefix) : "", phpNamespace: isSet(object.phpNamespace) ? globalThis.String(object.phpNamespace) : "", phpMetadataNamespace: isSet(object.phpMetadataNamespace) ? globalThis.String(object.phpMetadataNamespace) : "", rubyPackage: isSet(object.rubyPackage) ? globalThis.String(object.rubyPackage) : "", features: isSet(object.features) ? FeatureSet.fromJSON(object.features) : undefined, uninterpretedOption: globalThis.Array.isArray(object?.uninterpretedOption) ? object.uninterpretedOption.map((e: any) => UninterpretedOption.fromJSON(e)) : [], }; }, toJSON(message: FileOptions): unknown { const obj: any = {}; if (message.javaPackage !== undefined && message.javaPackage !== "") { obj.javaPackage = message.javaPackage; } if (message.javaOuterClassname !== undefined && message.javaOuterClassname !== "") { obj.javaOuterClassname = message.javaOuterClassname; } if (message.javaMultipleFiles !== undefined && message.javaMultipleFiles !== false) { obj.javaMultipleFiles = message.javaMultipleFiles; } if (message.javaGenerateEqualsAndHash !== undefined && message.javaGenerateEqualsAndHash !== false) { obj.javaGenerateEqualsAndHash = message.javaGenerateEqualsAndHash; } if (message.javaStringCheckUtf8 !== undefined && message.javaStringCheckUtf8 !== false) { obj.javaStringCheckUtf8 = message.javaStringCheckUtf8; } if (message.optimizeFor !== undefined && message.optimizeFor !== 1) { obj.optimizeFor = fileOptions_OptimizeModeToJSON(message.optimizeFor); } if (message.goPackage !== undefined && message.goPackage !== "") { obj.goPackage = message.goPackage; } if (message.ccGenericServices !== undefined && message.ccGenericServices !== false) { obj.ccGenericServices = message.ccGenericServices; } if (message.javaGenericServices !== undefined && message.javaGenericServices !== false) { obj.javaGenericServices = message.javaGenericServices; } if (message.pyGenericServices !== undefined && message.pyGenericServices !== false) { obj.pyGenericServices = message.pyGenericServices; } if (message.deprecated !== undefined && message.deprecated !== false) { obj.deprecated = message.deprecated; } if (message.ccEnableArenas !== undefined && message.ccEnableArenas !== true) { obj.ccEnableArenas = message.ccEnableArenas; } if (message.objcClassPrefix !== undefined && message.objcClassPrefix !== "") { obj.objcClassPrefix = message.objcClassPrefix; } if (message.csharpNamespace !== undefined && message.csharpNamespace !== "") { obj.csharpNamespace = message.csharpNamespace; } if (message.swiftPrefix !== undefined && message.swiftPrefix !== "") { obj.swiftPrefix = message.swiftPrefix; } if (message.phpClassPrefix !== undefined && message.phpClassPrefix !== "") { obj.phpClassPrefix = message.phpClassPrefix; } if (message.phpNamespace !== undefined && message.phpNamespace !== "") { obj.phpNamespace = message.phpNamespace; } if (message.phpMetadataNamespace !== undefined && message.phpMetadataNamespace !== "") { obj.phpMetadataNamespace = message.phpMetadataNamespace; } if (message.rubyPackage !== undefined && message.rubyPackage !== "") { obj.rubyPackage = message.rubyPackage; } if (message.features !== undefined) { obj.features = FeatureSet.toJSON(message.features); } if (message.uninterpretedOption?.length) { obj.uninterpretedOption = message.uninterpretedOption.map((e) => UninterpretedOption.toJSON(e)); } return obj; }, create, I>>(base?: I): FileOptions { return FileOptions.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): FileOptions { const message = createBaseFileOptions(); message.javaPackage = object.javaPackage ?? ""; message.javaOuterClassname = object.javaOuterClassname ?? ""; message.javaMultipleFiles = object.javaMultipleFiles ?? false; message.javaGenerateEqualsAndHash = object.javaGenerateEqualsAndHash ?? false; message.javaStringCheckUtf8 = object.javaStringCheckUtf8 ?? false; message.optimizeFor = object.optimizeFor ?? 1; message.goPackage = object.goPackage ?? ""; message.ccGenericServices = object.ccGenericServices ?? false; message.javaGenericServices = object.javaGenericServices ?? false; message.pyGenericServices = object.pyGenericServices ?? false; message.deprecated = object.deprecated ?? false; message.ccEnableArenas = object.ccEnableArenas ?? true; message.objcClassPrefix = object.objcClassPrefix ?? ""; message.csharpNamespace = object.csharpNamespace ?? ""; message.swiftPrefix = object.swiftPrefix ?? ""; message.phpClassPrefix = object.phpClassPrefix ?? ""; message.phpNamespace = object.phpNamespace ?? ""; message.phpMetadataNamespace = object.phpMetadataNamespace ?? ""; message.rubyPackage = object.rubyPackage ?? ""; message.features = (object.features !== undefined && object.features !== null) ? FeatureSet.fromPartial(object.features) : undefined; message.uninterpretedOption = object.uninterpretedOption?.map((e) => UninterpretedOption.fromPartial(e)) || []; return message; }, }; function createBaseMessageOptions(): MessageOptions { return { messageSetWireFormat: false, noStandardDescriptorAccessor: false, deprecated: false, mapEntry: false, deprecatedLegacyJsonFieldConflicts: false, features: undefined, uninterpretedOption: [], }; } export const MessageOptions: MessageFns = { encode(message: MessageOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.messageSetWireFormat !== undefined && message.messageSetWireFormat !== false) { writer.uint32(8).bool(message.messageSetWireFormat); } if (message.noStandardDescriptorAccessor !== undefined && message.noStandardDescriptorAccessor !== false) { writer.uint32(16).bool(message.noStandardDescriptorAccessor); } if (message.deprecated !== undefined && message.deprecated !== false) { writer.uint32(24).bool(message.deprecated); } if (message.mapEntry !== undefined && message.mapEntry !== false) { writer.uint32(56).bool(message.mapEntry); } if ( message.deprecatedLegacyJsonFieldConflicts !== undefined && message.deprecatedLegacyJsonFieldConflicts !== false ) { writer.uint32(88).bool(message.deprecatedLegacyJsonFieldConflicts); } if (message.features !== undefined) { FeatureSet.encode(message.features, writer.uint32(98).fork()).join(); } for (const v of message.uninterpretedOption) { UninterpretedOption.encode(v!, writer.uint32(7994).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): MessageOptions { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseMessageOptions(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) { break; } message.messageSetWireFormat = reader.bool(); continue; case 2: if (tag !== 16) { break; } message.noStandardDescriptorAccessor = reader.bool(); continue; case 3: if (tag !== 24) { break; } message.deprecated = reader.bool(); continue; case 7: if (tag !== 56) { break; } message.mapEntry = reader.bool(); continue; case 11: if (tag !== 88) { break; } message.deprecatedLegacyJsonFieldConflicts = reader.bool(); continue; case 12: if (tag !== 98) { break; } message.features = FeatureSet.decode(reader, reader.uint32()); continue; case 999: if (tag !== 7994) { break; } message.uninterpretedOption.push(UninterpretedOption.decode(reader, reader.uint32())); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): MessageOptions { return { messageSetWireFormat: isSet(object.messageSetWireFormat) ? globalThis.Boolean(object.messageSetWireFormat) : false, noStandardDescriptorAccessor: isSet(object.noStandardDescriptorAccessor) ? globalThis.Boolean(object.noStandardDescriptorAccessor) : false, deprecated: isSet(object.deprecated) ? globalThis.Boolean(object.deprecated) : false, mapEntry: isSet(object.mapEntry) ? globalThis.Boolean(object.mapEntry) : false, deprecatedLegacyJsonFieldConflicts: isSet(object.deprecatedLegacyJsonFieldConflicts) ? globalThis.Boolean(object.deprecatedLegacyJsonFieldConflicts) : false, features: isSet(object.features) ? FeatureSet.fromJSON(object.features) : undefined, uninterpretedOption: globalThis.Array.isArray(object?.uninterpretedOption) ? object.uninterpretedOption.map((e: any) => UninterpretedOption.fromJSON(e)) : [], }; }, toJSON(message: MessageOptions): unknown { const obj: any = {}; if (message.messageSetWireFormat !== undefined && message.messageSetWireFormat !== false) { obj.messageSetWireFormat = message.messageSetWireFormat; } if (message.noStandardDescriptorAccessor !== undefined && message.noStandardDescriptorAccessor !== false) { obj.noStandardDescriptorAccessor = message.noStandardDescriptorAccessor; } if (message.deprecated !== undefined && message.deprecated !== false) { obj.deprecated = message.deprecated; } if (message.mapEntry !== undefined && message.mapEntry !== false) { obj.mapEntry = message.mapEntry; } if ( message.deprecatedLegacyJsonFieldConflicts !== undefined && message.deprecatedLegacyJsonFieldConflicts !== false ) { obj.deprecatedLegacyJsonFieldConflicts = message.deprecatedLegacyJsonFieldConflicts; } if (message.features !== undefined) { obj.features = FeatureSet.toJSON(message.features); } if (message.uninterpretedOption?.length) { obj.uninterpretedOption = message.uninterpretedOption.map((e) => UninterpretedOption.toJSON(e)); } return obj; }, create, I>>(base?: I): MessageOptions { return MessageOptions.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): MessageOptions { const message = createBaseMessageOptions(); message.messageSetWireFormat = object.messageSetWireFormat ?? false; message.noStandardDescriptorAccessor = object.noStandardDescriptorAccessor ?? false; message.deprecated = object.deprecated ?? false; message.mapEntry = object.mapEntry ?? false; message.deprecatedLegacyJsonFieldConflicts = object.deprecatedLegacyJsonFieldConflicts ?? false; message.features = (object.features !== undefined && object.features !== null) ? FeatureSet.fromPartial(object.features) : undefined; message.uninterpretedOption = object.uninterpretedOption?.map((e) => UninterpretedOption.fromPartial(e)) || []; return message; }, }; function createBaseFieldOptions(): FieldOptions { return { ctype: 0, packed: false, jstype: 0, lazy: false, unverifiedLazy: false, deprecated: false, weak: false, debugRedact: false, retention: 0, targets: [], editionDefaults: [], features: undefined, featureSupport: undefined, uninterpretedOption: [], }; } export const FieldOptions: MessageFns = { encode(message: FieldOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.ctype !== undefined && message.ctype !== 0) { writer.uint32(8).int32(message.ctype); } if (message.packed !== undefined && message.packed !== false) { writer.uint32(16).bool(message.packed); } if (message.jstype !== undefined && message.jstype !== 0) { writer.uint32(48).int32(message.jstype); } if (message.lazy !== undefined && message.lazy !== false) { writer.uint32(40).bool(message.lazy); } if (message.unverifiedLazy !== undefined && message.unverifiedLazy !== false) { writer.uint32(120).bool(message.unverifiedLazy); } if (message.deprecated !== undefined && message.deprecated !== false) { writer.uint32(24).bool(message.deprecated); } if (message.weak !== undefined && message.weak !== false) { writer.uint32(80).bool(message.weak); } if (message.debugRedact !== undefined && message.debugRedact !== false) { writer.uint32(128).bool(message.debugRedact); } if (message.retention !== undefined && message.retention !== 0) { writer.uint32(136).int32(message.retention); } writer.uint32(154).fork(); for (const v of message.targets) { writer.int32(v); } writer.join(); for (const v of message.editionDefaults) { FieldOptions_EditionDefault.encode(v!, writer.uint32(162).fork()).join(); } if (message.features !== undefined) { FeatureSet.encode(message.features, writer.uint32(170).fork()).join(); } if (message.featureSupport !== undefined) { FieldOptions_FeatureSupport.encode(message.featureSupport, writer.uint32(178).fork()).join(); } for (const v of message.uninterpretedOption) { UninterpretedOption.encode(v!, writer.uint32(7994).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): FieldOptions { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseFieldOptions(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) { break; } message.ctype = reader.int32() as any; continue; case 2: if (tag !== 16) { break; } message.packed = reader.bool(); continue; case 6: if (tag !== 48) { break; } message.jstype = reader.int32() as any; continue; case 5: if (tag !== 40) { break; } message.lazy = reader.bool(); continue; case 15: if (tag !== 120) { break; } message.unverifiedLazy = reader.bool(); continue; case 3: if (tag !== 24) { break; } message.deprecated = reader.bool(); continue; case 10: if (tag !== 80) { break; } message.weak = reader.bool(); continue; case 16: if (tag !== 128) { break; } message.debugRedact = reader.bool(); continue; case 17: if (tag !== 136) { break; } message.retention = reader.int32() as any; continue; case 19: if (tag === 152) { message.targets.push(reader.int32() as any); continue; } if (tag === 154) { const end2 = reader.uint32() + reader.pos; while (reader.pos < end2) { message.targets.push(reader.int32() as any); } continue; } break; case 20: if (tag !== 162) { break; } message.editionDefaults.push(FieldOptions_EditionDefault.decode(reader, reader.uint32())); continue; case 21: if (tag !== 170) { break; } message.features = FeatureSet.decode(reader, reader.uint32()); continue; case 22: if (tag !== 178) { break; } message.featureSupport = FieldOptions_FeatureSupport.decode(reader, reader.uint32()); continue; case 999: if (tag !== 7994) { break; } message.uninterpretedOption.push(UninterpretedOption.decode(reader, reader.uint32())); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): FieldOptions { return { ctype: isSet(object.ctype) ? fieldOptions_CTypeFromJSON(object.ctype) : 0, packed: isSet(object.packed) ? globalThis.Boolean(object.packed) : false, jstype: isSet(object.jstype) ? fieldOptions_JSTypeFromJSON(object.jstype) : 0, lazy: isSet(object.lazy) ? globalThis.Boolean(object.lazy) : false, unverifiedLazy: isSet(object.unverifiedLazy) ? globalThis.Boolean(object.unverifiedLazy) : false, deprecated: isSet(object.deprecated) ? globalThis.Boolean(object.deprecated) : false, weak: isSet(object.weak) ? globalThis.Boolean(object.weak) : false, debugRedact: isSet(object.debugRedact) ? globalThis.Boolean(object.debugRedact) : false, retention: isSet(object.retention) ? fieldOptions_OptionRetentionFromJSON(object.retention) : 0, targets: globalThis.Array.isArray(object?.targets) ? object.targets.map((e: any) => fieldOptions_OptionTargetTypeFromJSON(e)) : [], editionDefaults: globalThis.Array.isArray(object?.editionDefaults) ? object.editionDefaults.map((e: any) => FieldOptions_EditionDefault.fromJSON(e)) : [], features: isSet(object.features) ? FeatureSet.fromJSON(object.features) : undefined, featureSupport: isSet(object.featureSupport) ? FieldOptions_FeatureSupport.fromJSON(object.featureSupport) : undefined, uninterpretedOption: globalThis.Array.isArray(object?.uninterpretedOption) ? object.uninterpretedOption.map((e: any) => UninterpretedOption.fromJSON(e)) : [], }; }, toJSON(message: FieldOptions): unknown { const obj: any = {}; if (message.ctype !== undefined && message.ctype !== 0) { obj.ctype = fieldOptions_CTypeToJSON(message.ctype); } if (message.packed !== undefined && message.packed !== false) { obj.packed = message.packed; } if (message.jstype !== undefined && message.jstype !== 0) { obj.jstype = fieldOptions_JSTypeToJSON(message.jstype); } if (message.lazy !== undefined && message.lazy !== false) { obj.lazy = message.lazy; } if (message.unverifiedLazy !== undefined && message.unverifiedLazy !== false) { obj.unverifiedLazy = message.unverifiedLazy; } if (message.deprecated !== undefined && message.deprecated !== false) { obj.deprecated = message.deprecated; } if (message.weak !== undefined && message.weak !== false) { obj.weak = message.weak; } if (message.debugRedact !== undefined && message.debugRedact !== false) { obj.debugRedact = message.debugRedact; } if (message.retention !== undefined && message.retention !== 0) { obj.retention = fieldOptions_OptionRetentionToJSON(message.retention); } if (message.targets?.length) { obj.targets = message.targets.map((e) => fieldOptions_OptionTargetTypeToJSON(e)); } if (message.editionDefaults?.length) { obj.editionDefaults = message.editionDefaults.map((e) => FieldOptions_EditionDefault.toJSON(e)); } if (message.features !== undefined) { obj.features = FeatureSet.toJSON(message.features); } if (message.featureSupport !== undefined) { obj.featureSupport = FieldOptions_FeatureSupport.toJSON(message.featureSupport); } if (message.uninterpretedOption?.length) { obj.uninterpretedOption = message.uninterpretedOption.map((e) => UninterpretedOption.toJSON(e)); } return obj; }, create, I>>(base?: I): FieldOptions { return FieldOptions.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): FieldOptions { const message = createBaseFieldOptions(); message.ctype = object.ctype ?? 0; message.packed = object.packed ?? false; message.jstype = object.jstype ?? 0; message.lazy = object.lazy ?? false; message.unverifiedLazy = object.unverifiedLazy ?? false; message.deprecated = object.deprecated ?? false; message.weak = object.weak ?? false; message.debugRedact = object.debugRedact ?? false; message.retention = object.retention ?? 0; message.targets = object.targets?.map((e) => e) || []; message.editionDefaults = object.editionDefaults?.map((e) => FieldOptions_EditionDefault.fromPartial(e)) || []; message.features = (object.features !== undefined && object.features !== null) ? FeatureSet.fromPartial(object.features) : undefined; message.featureSupport = (object.featureSupport !== undefined && object.featureSupport !== null) ? FieldOptions_FeatureSupport.fromPartial(object.featureSupport) : undefined; message.uninterpretedOption = object.uninterpretedOption?.map((e) => UninterpretedOption.fromPartial(e)) || []; return message; }, }; function createBaseFieldOptions_EditionDefault(): FieldOptions_EditionDefault { return { edition: 0, value: "" }; } export const FieldOptions_EditionDefault: MessageFns = { encode(message: FieldOptions_EditionDefault, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.edition !== undefined && message.edition !== 0) { writer.uint32(24).int32(message.edition); } if (message.value !== undefined && message.value !== "") { writer.uint32(18).string(message.value); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): FieldOptions_EditionDefault { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseFieldOptions_EditionDefault(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 3: if (tag !== 24) { break; } message.edition = reader.int32() as any; continue; case 2: if (tag !== 18) { break; } message.value = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): FieldOptions_EditionDefault { return { edition: isSet(object.edition) ? editionFromJSON(object.edition) : 0, value: isSet(object.value) ? globalThis.String(object.value) : "", }; }, toJSON(message: FieldOptions_EditionDefault): unknown { const obj: any = {}; if (message.edition !== undefined && message.edition !== 0) { obj.edition = editionToJSON(message.edition); } if (message.value !== undefined && message.value !== "") { obj.value = message.value; } return obj; }, create, I>>(base?: I): FieldOptions_EditionDefault { return FieldOptions_EditionDefault.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): FieldOptions_EditionDefault { const message = createBaseFieldOptions_EditionDefault(); message.edition = object.edition ?? 0; message.value = object.value ?? ""; return message; }, }; function createBaseFieldOptions_FeatureSupport(): FieldOptions_FeatureSupport { return { editionIntroduced: 0, editionDeprecated: 0, deprecationWarning: "", editionRemoved: 0 }; } export const FieldOptions_FeatureSupport: MessageFns = { encode(message: FieldOptions_FeatureSupport, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.editionIntroduced !== undefined && message.editionIntroduced !== 0) { writer.uint32(8).int32(message.editionIntroduced); } if (message.editionDeprecated !== undefined && message.editionDeprecated !== 0) { writer.uint32(16).int32(message.editionDeprecated); } if (message.deprecationWarning !== undefined && message.deprecationWarning !== "") { writer.uint32(26).string(message.deprecationWarning); } if (message.editionRemoved !== undefined && message.editionRemoved !== 0) { writer.uint32(32).int32(message.editionRemoved); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): FieldOptions_FeatureSupport { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseFieldOptions_FeatureSupport(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) { break; } message.editionIntroduced = reader.int32() as any; continue; case 2: if (tag !== 16) { break; } message.editionDeprecated = reader.int32() as any; continue; case 3: if (tag !== 26) { break; } message.deprecationWarning = reader.string(); continue; case 4: if (tag !== 32) { break; } message.editionRemoved = reader.int32() as any; continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): FieldOptions_FeatureSupport { return { editionIntroduced: isSet(object.editionIntroduced) ? editionFromJSON(object.editionIntroduced) : 0, editionDeprecated: isSet(object.editionDeprecated) ? editionFromJSON(object.editionDeprecated) : 0, deprecationWarning: isSet(object.deprecationWarning) ? globalThis.String(object.deprecationWarning) : "", editionRemoved: isSet(object.editionRemoved) ? editionFromJSON(object.editionRemoved) : 0, }; }, toJSON(message: FieldOptions_FeatureSupport): unknown { const obj: any = {}; if (message.editionIntroduced !== undefined && message.editionIntroduced !== 0) { obj.editionIntroduced = editionToJSON(message.editionIntroduced); } if (message.editionDeprecated !== undefined && message.editionDeprecated !== 0) { obj.editionDeprecated = editionToJSON(message.editionDeprecated); } if (message.deprecationWarning !== undefined && message.deprecationWarning !== "") { obj.deprecationWarning = message.deprecationWarning; } if (message.editionRemoved !== undefined && message.editionRemoved !== 0) { obj.editionRemoved = editionToJSON(message.editionRemoved); } return obj; }, create, I>>(base?: I): FieldOptions_FeatureSupport { return FieldOptions_FeatureSupport.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): FieldOptions_FeatureSupport { const message = createBaseFieldOptions_FeatureSupport(); message.editionIntroduced = object.editionIntroduced ?? 0; message.editionDeprecated = object.editionDeprecated ?? 0; message.deprecationWarning = object.deprecationWarning ?? ""; message.editionRemoved = object.editionRemoved ?? 0; return message; }, }; function createBaseOneofOptions(): OneofOptions { return { features: undefined, uninterpretedOption: [] }; } export const OneofOptions: MessageFns = { encode(message: OneofOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.features !== undefined) { FeatureSet.encode(message.features, writer.uint32(10).fork()).join(); } for (const v of message.uninterpretedOption) { UninterpretedOption.encode(v!, writer.uint32(7994).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): OneofOptions { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseOneofOptions(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.features = FeatureSet.decode(reader, reader.uint32()); continue; case 999: if (tag !== 7994) { break; } message.uninterpretedOption.push(UninterpretedOption.decode(reader, reader.uint32())); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): OneofOptions { return { features: isSet(object.features) ? FeatureSet.fromJSON(object.features) : undefined, uninterpretedOption: globalThis.Array.isArray(object?.uninterpretedOption) ? object.uninterpretedOption.map((e: any) => UninterpretedOption.fromJSON(e)) : [], }; }, toJSON(message: OneofOptions): unknown { const obj: any = {}; if (message.features !== undefined) { obj.features = FeatureSet.toJSON(message.features); } if (message.uninterpretedOption?.length) { obj.uninterpretedOption = message.uninterpretedOption.map((e) => UninterpretedOption.toJSON(e)); } return obj; }, create, I>>(base?: I): OneofOptions { return OneofOptions.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): OneofOptions { const message = createBaseOneofOptions(); message.features = (object.features !== undefined && object.features !== null) ? FeatureSet.fromPartial(object.features) : undefined; message.uninterpretedOption = object.uninterpretedOption?.map((e) => UninterpretedOption.fromPartial(e)) || []; return message; }, }; function createBaseEnumOptions(): EnumOptions { return { allowAlias: false, deprecated: false, deprecatedLegacyJsonFieldConflicts: false, features: undefined, uninterpretedOption: [], }; } export const EnumOptions: MessageFns = { encode(message: EnumOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.allowAlias !== undefined && message.allowAlias !== false) { writer.uint32(16).bool(message.allowAlias); } if (message.deprecated !== undefined && message.deprecated !== false) { writer.uint32(24).bool(message.deprecated); } if ( message.deprecatedLegacyJsonFieldConflicts !== undefined && message.deprecatedLegacyJsonFieldConflicts !== false ) { writer.uint32(48).bool(message.deprecatedLegacyJsonFieldConflicts); } if (message.features !== undefined) { FeatureSet.encode(message.features, writer.uint32(58).fork()).join(); } for (const v of message.uninterpretedOption) { UninterpretedOption.encode(v!, writer.uint32(7994).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): EnumOptions { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseEnumOptions(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 2: if (tag !== 16) { break; } message.allowAlias = reader.bool(); continue; case 3: if (tag !== 24) { break; } message.deprecated = reader.bool(); continue; case 6: if (tag !== 48) { break; } message.deprecatedLegacyJsonFieldConflicts = reader.bool(); continue; case 7: if (tag !== 58) { break; } message.features = FeatureSet.decode(reader, reader.uint32()); continue; case 999: if (tag !== 7994) { break; } message.uninterpretedOption.push(UninterpretedOption.decode(reader, reader.uint32())); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): EnumOptions { return { allowAlias: isSet(object.allowAlias) ? globalThis.Boolean(object.allowAlias) : false, deprecated: isSet(object.deprecated) ? globalThis.Boolean(object.deprecated) : false, deprecatedLegacyJsonFieldConflicts: isSet(object.deprecatedLegacyJsonFieldConflicts) ? globalThis.Boolean(object.deprecatedLegacyJsonFieldConflicts) : false, features: isSet(object.features) ? FeatureSet.fromJSON(object.features) : undefined, uninterpretedOption: globalThis.Array.isArray(object?.uninterpretedOption) ? object.uninterpretedOption.map((e: any) => UninterpretedOption.fromJSON(e)) : [], }; }, toJSON(message: EnumOptions): unknown { const obj: any = {}; if (message.allowAlias !== undefined && message.allowAlias !== false) { obj.allowAlias = message.allowAlias; } if (message.deprecated !== undefined && message.deprecated !== false) { obj.deprecated = message.deprecated; } if ( message.deprecatedLegacyJsonFieldConflicts !== undefined && message.deprecatedLegacyJsonFieldConflicts !== false ) { obj.deprecatedLegacyJsonFieldConflicts = message.deprecatedLegacyJsonFieldConflicts; } if (message.features !== undefined) { obj.features = FeatureSet.toJSON(message.features); } if (message.uninterpretedOption?.length) { obj.uninterpretedOption = message.uninterpretedOption.map((e) => UninterpretedOption.toJSON(e)); } return obj; }, create, I>>(base?: I): EnumOptions { return EnumOptions.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): EnumOptions { const message = createBaseEnumOptions(); message.allowAlias = object.allowAlias ?? false; message.deprecated = object.deprecated ?? false; message.deprecatedLegacyJsonFieldConflicts = object.deprecatedLegacyJsonFieldConflicts ?? false; message.features = (object.features !== undefined && object.features !== null) ? FeatureSet.fromPartial(object.features) : undefined; message.uninterpretedOption = object.uninterpretedOption?.map((e) => UninterpretedOption.fromPartial(e)) || []; return message; }, }; function createBaseEnumValueOptions(): EnumValueOptions { return { deprecated: false, features: undefined, debugRedact: false, featureSupport: undefined, uninterpretedOption: [], }; } export const EnumValueOptions: MessageFns = { encode(message: EnumValueOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.deprecated !== undefined && message.deprecated !== false) { writer.uint32(8).bool(message.deprecated); } if (message.features !== undefined) { FeatureSet.encode(message.features, writer.uint32(18).fork()).join(); } if (message.debugRedact !== undefined && message.debugRedact !== false) { writer.uint32(24).bool(message.debugRedact); } if (message.featureSupport !== undefined) { FieldOptions_FeatureSupport.encode(message.featureSupport, writer.uint32(34).fork()).join(); } for (const v of message.uninterpretedOption) { UninterpretedOption.encode(v!, writer.uint32(7994).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): EnumValueOptions { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseEnumValueOptions(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) { break; } message.deprecated = reader.bool(); continue; case 2: if (tag !== 18) { break; } message.features = FeatureSet.decode(reader, reader.uint32()); continue; case 3: if (tag !== 24) { break; } message.debugRedact = reader.bool(); continue; case 4: if (tag !== 34) { break; } message.featureSupport = FieldOptions_FeatureSupport.decode(reader, reader.uint32()); continue; case 999: if (tag !== 7994) { break; } message.uninterpretedOption.push(UninterpretedOption.decode(reader, reader.uint32())); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): EnumValueOptions { return { deprecated: isSet(object.deprecated) ? globalThis.Boolean(object.deprecated) : false, features: isSet(object.features) ? FeatureSet.fromJSON(object.features) : undefined, debugRedact: isSet(object.debugRedact) ? globalThis.Boolean(object.debugRedact) : false, featureSupport: isSet(object.featureSupport) ? FieldOptions_FeatureSupport.fromJSON(object.featureSupport) : undefined, uninterpretedOption: globalThis.Array.isArray(object?.uninterpretedOption) ? object.uninterpretedOption.map((e: any) => UninterpretedOption.fromJSON(e)) : [], }; }, toJSON(message: EnumValueOptions): unknown { const obj: any = {}; if (message.deprecated !== undefined && message.deprecated !== false) { obj.deprecated = message.deprecated; } if (message.features !== undefined) { obj.features = FeatureSet.toJSON(message.features); } if (message.debugRedact !== undefined && message.debugRedact !== false) { obj.debugRedact = message.debugRedact; } if (message.featureSupport !== undefined) { obj.featureSupport = FieldOptions_FeatureSupport.toJSON(message.featureSupport); } if (message.uninterpretedOption?.length) { obj.uninterpretedOption = message.uninterpretedOption.map((e) => UninterpretedOption.toJSON(e)); } return obj; }, create, I>>(base?: I): EnumValueOptions { return EnumValueOptions.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): EnumValueOptions { const message = createBaseEnumValueOptions(); message.deprecated = object.deprecated ?? false; message.features = (object.features !== undefined && object.features !== null) ? FeatureSet.fromPartial(object.features) : undefined; message.debugRedact = object.debugRedact ?? false; message.featureSupport = (object.featureSupport !== undefined && object.featureSupport !== null) ? FieldOptions_FeatureSupport.fromPartial(object.featureSupport) : undefined; message.uninterpretedOption = object.uninterpretedOption?.map((e) => UninterpretedOption.fromPartial(e)) || []; return message; }, }; function createBaseServiceOptions(): ServiceOptions { return { features: undefined, deprecated: false, uninterpretedOption: [] }; } export const ServiceOptions: MessageFns = { encode(message: ServiceOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.features !== undefined) { FeatureSet.encode(message.features, writer.uint32(274).fork()).join(); } if (message.deprecated !== undefined && message.deprecated !== false) { writer.uint32(264).bool(message.deprecated); } for (const v of message.uninterpretedOption) { UninterpretedOption.encode(v!, writer.uint32(7994).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): ServiceOptions { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseServiceOptions(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 34: if (tag !== 274) { break; } message.features = FeatureSet.decode(reader, reader.uint32()); continue; case 33: if (tag !== 264) { break; } message.deprecated = reader.bool(); continue; case 999: if (tag !== 7994) { break; } message.uninterpretedOption.push(UninterpretedOption.decode(reader, reader.uint32())); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): ServiceOptions { return { features: isSet(object.features) ? FeatureSet.fromJSON(object.features) : undefined, deprecated: isSet(object.deprecated) ? globalThis.Boolean(object.deprecated) : false, uninterpretedOption: globalThis.Array.isArray(object?.uninterpretedOption) ? object.uninterpretedOption.map((e: any) => UninterpretedOption.fromJSON(e)) : [], }; }, toJSON(message: ServiceOptions): unknown { const obj: any = {}; if (message.features !== undefined) { obj.features = FeatureSet.toJSON(message.features); } if (message.deprecated !== undefined && message.deprecated !== false) { obj.deprecated = message.deprecated; } if (message.uninterpretedOption?.length) { obj.uninterpretedOption = message.uninterpretedOption.map((e) => UninterpretedOption.toJSON(e)); } return obj; }, create, I>>(base?: I): ServiceOptions { return ServiceOptions.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): ServiceOptions { const message = createBaseServiceOptions(); message.features = (object.features !== undefined && object.features !== null) ? FeatureSet.fromPartial(object.features) : undefined; message.deprecated = object.deprecated ?? false; message.uninterpretedOption = object.uninterpretedOption?.map((e) => UninterpretedOption.fromPartial(e)) || []; return message; }, }; function createBaseMethodOptions(): MethodOptions { return { deprecated: false, idempotencyLevel: 0, features: undefined, uninterpretedOption: [] }; } export const MethodOptions: MessageFns = { encode(message: MethodOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.deprecated !== undefined && message.deprecated !== false) { writer.uint32(264).bool(message.deprecated); } if (message.idempotencyLevel !== undefined && message.idempotencyLevel !== 0) { writer.uint32(272).int32(message.idempotencyLevel); } if (message.features !== undefined) { FeatureSet.encode(message.features, writer.uint32(282).fork()).join(); } for (const v of message.uninterpretedOption) { UninterpretedOption.encode(v!, writer.uint32(7994).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): MethodOptions { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseMethodOptions(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 33: if (tag !== 264) { break; } message.deprecated = reader.bool(); continue; case 34: if (tag !== 272) { break; } message.idempotencyLevel = reader.int32() as any; continue; case 35: if (tag !== 282) { break; } message.features = FeatureSet.decode(reader, reader.uint32()); continue; case 999: if (tag !== 7994) { break; } message.uninterpretedOption.push(UninterpretedOption.decode(reader, reader.uint32())); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): MethodOptions { return { deprecated: isSet(object.deprecated) ? globalThis.Boolean(object.deprecated) : false, idempotencyLevel: isSet(object.idempotencyLevel) ? methodOptions_IdempotencyLevelFromJSON(object.idempotencyLevel) : 0, features: isSet(object.features) ? FeatureSet.fromJSON(object.features) : undefined, uninterpretedOption: globalThis.Array.isArray(object?.uninterpretedOption) ? object.uninterpretedOption.map((e: any) => UninterpretedOption.fromJSON(e)) : [], }; }, toJSON(message: MethodOptions): unknown { const obj: any = {}; if (message.deprecated !== undefined && message.deprecated !== false) { obj.deprecated = message.deprecated; } if (message.idempotencyLevel !== undefined && message.idempotencyLevel !== 0) { obj.idempotencyLevel = methodOptions_IdempotencyLevelToJSON(message.idempotencyLevel); } if (message.features !== undefined) { obj.features = FeatureSet.toJSON(message.features); } if (message.uninterpretedOption?.length) { obj.uninterpretedOption = message.uninterpretedOption.map((e) => UninterpretedOption.toJSON(e)); } return obj; }, create, I>>(base?: I): MethodOptions { return MethodOptions.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): MethodOptions { const message = createBaseMethodOptions(); message.deprecated = object.deprecated ?? false; message.idempotencyLevel = object.idempotencyLevel ?? 0; message.features = (object.features !== undefined && object.features !== null) ? FeatureSet.fromPartial(object.features) : undefined; message.uninterpretedOption = object.uninterpretedOption?.map((e) => UninterpretedOption.fromPartial(e)) || []; return message; }, }; function createBaseUninterpretedOption(): UninterpretedOption { return { name: [], identifierValue: "", positiveIntValue: 0, negativeIntValue: 0, doubleValue: 0, stringValue: new Uint8Array(0), aggregateValue: "", }; } export const UninterpretedOption: MessageFns = { encode(message: UninterpretedOption, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { for (const v of message.name) { UninterpretedOption_NamePart.encode(v!, writer.uint32(18).fork()).join(); } if (message.identifierValue !== undefined && message.identifierValue !== "") { writer.uint32(26).string(message.identifierValue); } if (message.positiveIntValue !== undefined && message.positiveIntValue !== 0) { writer.uint32(32).uint64(message.positiveIntValue); } if (message.negativeIntValue !== undefined && message.negativeIntValue !== 0) { writer.uint32(40).int64(message.negativeIntValue); } if (message.doubleValue !== undefined && message.doubleValue !== 0) { writer.uint32(49).double(message.doubleValue); } if (message.stringValue !== undefined && message.stringValue.length !== 0) { writer.uint32(58).bytes(message.stringValue); } if (message.aggregateValue !== undefined && message.aggregateValue !== "") { writer.uint32(66).string(message.aggregateValue); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): UninterpretedOption { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseUninterpretedOption(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 2: if (tag !== 18) { break; } message.name.push(UninterpretedOption_NamePart.decode(reader, reader.uint32())); continue; case 3: if (tag !== 26) { break; } message.identifierValue = reader.string(); continue; case 4: if (tag !== 32) { break; } message.positiveIntValue = longToNumber(reader.uint64()); continue; case 5: if (tag !== 40) { break; } message.negativeIntValue = longToNumber(reader.int64()); continue; case 6: if (tag !== 49) { break; } message.doubleValue = reader.double(); continue; case 7: if (tag !== 58) { break; } message.stringValue = reader.bytes(); continue; case 8: if (tag !== 66) { break; } message.aggregateValue = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): UninterpretedOption { return { name: globalThis.Array.isArray(object?.name) ? object.name.map((e: any) => UninterpretedOption_NamePart.fromJSON(e)) : [], identifierValue: isSet(object.identifierValue) ? globalThis.String(object.identifierValue) : "", positiveIntValue: isSet(object.positiveIntValue) ? globalThis.Number(object.positiveIntValue) : 0, negativeIntValue: isSet(object.negativeIntValue) ? globalThis.Number(object.negativeIntValue) : 0, doubleValue: isSet(object.doubleValue) ? globalThis.Number(object.doubleValue) : 0, stringValue: isSet(object.stringValue) ? bytesFromBase64(object.stringValue) : new Uint8Array(0), aggregateValue: isSet(object.aggregateValue) ? globalThis.String(object.aggregateValue) : "", }; }, toJSON(message: UninterpretedOption): unknown { const obj: any = {}; if (message.name?.length) { obj.name = message.name.map((e) => UninterpretedOption_NamePart.toJSON(e)); } if (message.identifierValue !== undefined && message.identifierValue !== "") { obj.identifierValue = message.identifierValue; } if (message.positiveIntValue !== undefined && message.positiveIntValue !== 0) { obj.positiveIntValue = Math.round(message.positiveIntValue); } if (message.negativeIntValue !== undefined && message.negativeIntValue !== 0) { obj.negativeIntValue = Math.round(message.negativeIntValue); } if (message.doubleValue !== undefined && message.doubleValue !== 0) { obj.doubleValue = message.doubleValue; } if (message.stringValue !== undefined && message.stringValue.length !== 0) { obj.stringValue = base64FromBytes(message.stringValue); } if (message.aggregateValue !== undefined && message.aggregateValue !== "") { obj.aggregateValue = message.aggregateValue; } return obj; }, create, I>>(base?: I): UninterpretedOption { return UninterpretedOption.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): UninterpretedOption { const message = createBaseUninterpretedOption(); message.name = object.name?.map((e) => UninterpretedOption_NamePart.fromPartial(e)) || []; message.identifierValue = object.identifierValue ?? ""; message.positiveIntValue = object.positiveIntValue ?? 0; message.negativeIntValue = object.negativeIntValue ?? 0; message.doubleValue = object.doubleValue ?? 0; message.stringValue = object.stringValue ?? new Uint8Array(0); message.aggregateValue = object.aggregateValue ?? ""; return message; }, }; function createBaseUninterpretedOption_NamePart(): UninterpretedOption_NamePart { return { namePart: "", isExtension: false }; } export const UninterpretedOption_NamePart: MessageFns = { encode(message: UninterpretedOption_NamePart, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.namePart !== "") { writer.uint32(10).string(message.namePart); } if (message.isExtension !== false) { writer.uint32(16).bool(message.isExtension); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): UninterpretedOption_NamePart { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseUninterpretedOption_NamePart(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.namePart = reader.string(); continue; case 2: if (tag !== 16) { break; } message.isExtension = reader.bool(); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): UninterpretedOption_NamePart { return { namePart: isSet(object.namePart) ? globalThis.String(object.namePart) : "", isExtension: isSet(object.isExtension) ? globalThis.Boolean(object.isExtension) : false, }; }, toJSON(message: UninterpretedOption_NamePart): unknown { const obj: any = {}; if (message.namePart !== "") { obj.namePart = message.namePart; } if (message.isExtension !== false) { obj.isExtension = message.isExtension; } return obj; }, create, I>>(base?: I): UninterpretedOption_NamePart { return UninterpretedOption_NamePart.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): UninterpretedOption_NamePart { const message = createBaseUninterpretedOption_NamePart(); message.namePart = object.namePart ?? ""; message.isExtension = object.isExtension ?? false; return message; }, }; function createBaseFeatureSet(): FeatureSet { return { fieldPresence: 0, enumType: 0, repeatedFieldEncoding: 0, utf8Validation: 0, messageEncoding: 0, jsonFormat: 0, enforceNamingStyle: 0, defaultSymbolVisibility: 0, }; } export const FeatureSet: MessageFns = { encode(message: FeatureSet, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.fieldPresence !== undefined && message.fieldPresence !== 0) { writer.uint32(8).int32(message.fieldPresence); } if (message.enumType !== undefined && message.enumType !== 0) { writer.uint32(16).int32(message.enumType); } if (message.repeatedFieldEncoding !== undefined && message.repeatedFieldEncoding !== 0) { writer.uint32(24).int32(message.repeatedFieldEncoding); } if (message.utf8Validation !== undefined && message.utf8Validation !== 0) { writer.uint32(32).int32(message.utf8Validation); } if (message.messageEncoding !== undefined && message.messageEncoding !== 0) { writer.uint32(40).int32(message.messageEncoding); } if (message.jsonFormat !== undefined && message.jsonFormat !== 0) { writer.uint32(48).int32(message.jsonFormat); } if (message.enforceNamingStyle !== undefined && message.enforceNamingStyle !== 0) { writer.uint32(56).int32(message.enforceNamingStyle); } if (message.defaultSymbolVisibility !== undefined && message.defaultSymbolVisibility !== 0) { writer.uint32(64).int32(message.defaultSymbolVisibility); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): FeatureSet { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseFeatureSet(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) { break; } message.fieldPresence = reader.int32() as any; continue; case 2: if (tag !== 16) { break; } message.enumType = reader.int32() as any; continue; case 3: if (tag !== 24) { break; } message.repeatedFieldEncoding = reader.int32() as any; continue; case 4: if (tag !== 32) { break; } message.utf8Validation = reader.int32() as any; continue; case 5: if (tag !== 40) { break; } message.messageEncoding = reader.int32() as any; continue; case 6: if (tag !== 48) { break; } message.jsonFormat = reader.int32() as any; continue; case 7: if (tag !== 56) { break; } message.enforceNamingStyle = reader.int32() as any; continue; case 8: if (tag !== 64) { break; } message.defaultSymbolVisibility = reader.int32() as any; continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): FeatureSet { return { fieldPresence: isSet(object.fieldPresence) ? featureSet_FieldPresenceFromJSON(object.fieldPresence) : 0, enumType: isSet(object.enumType) ? featureSet_EnumTypeFromJSON(object.enumType) : 0, repeatedFieldEncoding: isSet(object.repeatedFieldEncoding) ? featureSet_RepeatedFieldEncodingFromJSON(object.repeatedFieldEncoding) : 0, utf8Validation: isSet(object.utf8Validation) ? featureSet_Utf8ValidationFromJSON(object.utf8Validation) : 0, messageEncoding: isSet(object.messageEncoding) ? featureSet_MessageEncodingFromJSON(object.messageEncoding) : 0, jsonFormat: isSet(object.jsonFormat) ? featureSet_JsonFormatFromJSON(object.jsonFormat) : 0, enforceNamingStyle: isSet(object.enforceNamingStyle) ? featureSet_EnforceNamingStyleFromJSON(object.enforceNamingStyle) : 0, defaultSymbolVisibility: isSet(object.defaultSymbolVisibility) ? featureSet_VisibilityFeature_DefaultSymbolVisibilityFromJSON(object.defaultSymbolVisibility) : 0, }; }, toJSON(message: FeatureSet): unknown { const obj: any = {}; if (message.fieldPresence !== undefined && message.fieldPresence !== 0) { obj.fieldPresence = featureSet_FieldPresenceToJSON(message.fieldPresence); } if (message.enumType !== undefined && message.enumType !== 0) { obj.enumType = featureSet_EnumTypeToJSON(message.enumType); } if (message.repeatedFieldEncoding !== undefined && message.repeatedFieldEncoding !== 0) { obj.repeatedFieldEncoding = featureSet_RepeatedFieldEncodingToJSON(message.repeatedFieldEncoding); } if (message.utf8Validation !== undefined && message.utf8Validation !== 0) { obj.utf8Validation = featureSet_Utf8ValidationToJSON(message.utf8Validation); } if (message.messageEncoding !== undefined && message.messageEncoding !== 0) { obj.messageEncoding = featureSet_MessageEncodingToJSON(message.messageEncoding); } if (message.jsonFormat !== undefined && message.jsonFormat !== 0) { obj.jsonFormat = featureSet_JsonFormatToJSON(message.jsonFormat); } if (message.enforceNamingStyle !== undefined && message.enforceNamingStyle !== 0) { obj.enforceNamingStyle = featureSet_EnforceNamingStyleToJSON(message.enforceNamingStyle); } if (message.defaultSymbolVisibility !== undefined && message.defaultSymbolVisibility !== 0) { obj.defaultSymbolVisibility = featureSet_VisibilityFeature_DefaultSymbolVisibilityToJSON( message.defaultSymbolVisibility, ); } return obj; }, create, I>>(base?: I): FeatureSet { return FeatureSet.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): FeatureSet { const message = createBaseFeatureSet(); message.fieldPresence = object.fieldPresence ?? 0; message.enumType = object.enumType ?? 0; message.repeatedFieldEncoding = object.repeatedFieldEncoding ?? 0; message.utf8Validation = object.utf8Validation ?? 0; message.messageEncoding = object.messageEncoding ?? 0; message.jsonFormat = object.jsonFormat ?? 0; message.enforceNamingStyle = object.enforceNamingStyle ?? 0; message.defaultSymbolVisibility = object.defaultSymbolVisibility ?? 0; return message; }, }; function createBaseFeatureSet_VisibilityFeature(): FeatureSet_VisibilityFeature { return {}; } export const FeatureSet_VisibilityFeature: MessageFns = { encode(_: FeatureSet_VisibilityFeature, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): FeatureSet_VisibilityFeature { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseFeatureSet_VisibilityFeature(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(_: any): FeatureSet_VisibilityFeature { return {}; }, toJSON(_: FeatureSet_VisibilityFeature): unknown { const obj: any = {}; return obj; }, create, I>>(base?: I): FeatureSet_VisibilityFeature { return FeatureSet_VisibilityFeature.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(_: I): FeatureSet_VisibilityFeature { const message = createBaseFeatureSet_VisibilityFeature(); return message; }, }; function createBaseFeatureSetDefaults(): FeatureSetDefaults { return { defaults: [], minimumEdition: 0, maximumEdition: 0 }; } export const FeatureSetDefaults: MessageFns = { encode(message: FeatureSetDefaults, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { for (const v of message.defaults) { FeatureSetDefaults_FeatureSetEditionDefault.encode(v!, writer.uint32(10).fork()).join(); } if (message.minimumEdition !== undefined && message.minimumEdition !== 0) { writer.uint32(32).int32(message.minimumEdition); } if (message.maximumEdition !== undefined && message.maximumEdition !== 0) { writer.uint32(40).int32(message.maximumEdition); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): FeatureSetDefaults { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseFeatureSetDefaults(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.defaults.push(FeatureSetDefaults_FeatureSetEditionDefault.decode(reader, reader.uint32())); continue; case 4: if (tag !== 32) { break; } message.minimumEdition = reader.int32() as any; continue; case 5: if (tag !== 40) { break; } message.maximumEdition = reader.int32() as any; continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): FeatureSetDefaults { return { defaults: globalThis.Array.isArray(object?.defaults) ? object.defaults.map((e: any) => FeatureSetDefaults_FeatureSetEditionDefault.fromJSON(e)) : [], minimumEdition: isSet(object.minimumEdition) ? editionFromJSON(object.minimumEdition) : 0, maximumEdition: isSet(object.maximumEdition) ? editionFromJSON(object.maximumEdition) : 0, }; }, toJSON(message: FeatureSetDefaults): unknown { const obj: any = {}; if (message.defaults?.length) { obj.defaults = message.defaults.map((e) => FeatureSetDefaults_FeatureSetEditionDefault.toJSON(e)); } if (message.minimumEdition !== undefined && message.minimumEdition !== 0) { obj.minimumEdition = editionToJSON(message.minimumEdition); } if (message.maximumEdition !== undefined && message.maximumEdition !== 0) { obj.maximumEdition = editionToJSON(message.maximumEdition); } return obj; }, create, I>>(base?: I): FeatureSetDefaults { return FeatureSetDefaults.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): FeatureSetDefaults { const message = createBaseFeatureSetDefaults(); message.defaults = object.defaults?.map((e) => FeatureSetDefaults_FeatureSetEditionDefault.fromPartial(e)) || []; message.minimumEdition = object.minimumEdition ?? 0; message.maximumEdition = object.maximumEdition ?? 0; return message; }, }; function createBaseFeatureSetDefaults_FeatureSetEditionDefault(): FeatureSetDefaults_FeatureSetEditionDefault { return { edition: 0, overridableFeatures: undefined, fixedFeatures: undefined }; } export const FeatureSetDefaults_FeatureSetEditionDefault: MessageFns = { encode( message: FeatureSetDefaults_FeatureSetEditionDefault, writer: BinaryWriter = new BinaryWriter(), ): BinaryWriter { if (message.edition !== undefined && message.edition !== 0) { writer.uint32(24).int32(message.edition); } if (message.overridableFeatures !== undefined) { FeatureSet.encode(message.overridableFeatures, writer.uint32(34).fork()).join(); } if (message.fixedFeatures !== undefined) { FeatureSet.encode(message.fixedFeatures, writer.uint32(42).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): FeatureSetDefaults_FeatureSetEditionDefault { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseFeatureSetDefaults_FeatureSetEditionDefault(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 3: if (tag !== 24) { break; } message.edition = reader.int32() as any; continue; case 4: if (tag !== 34) { break; } message.overridableFeatures = FeatureSet.decode(reader, reader.uint32()); continue; case 5: if (tag !== 42) { break; } message.fixedFeatures = FeatureSet.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): FeatureSetDefaults_FeatureSetEditionDefault { return { edition: isSet(object.edition) ? editionFromJSON(object.edition) : 0, overridableFeatures: isSet(object.overridableFeatures) ? FeatureSet.fromJSON(object.overridableFeatures) : undefined, fixedFeatures: isSet(object.fixedFeatures) ? FeatureSet.fromJSON(object.fixedFeatures) : undefined, }; }, toJSON(message: FeatureSetDefaults_FeatureSetEditionDefault): unknown { const obj: any = {}; if (message.edition !== undefined && message.edition !== 0) { obj.edition = editionToJSON(message.edition); } if (message.overridableFeatures !== undefined) { obj.overridableFeatures = FeatureSet.toJSON(message.overridableFeatures); } if (message.fixedFeatures !== undefined) { obj.fixedFeatures = FeatureSet.toJSON(message.fixedFeatures); } return obj; }, create, I>>( base?: I, ): FeatureSetDefaults_FeatureSetEditionDefault { return FeatureSetDefaults_FeatureSetEditionDefault.fromPartial(base ?? ({} as any)); }, fromPartial, I>>( object: I, ): FeatureSetDefaults_FeatureSetEditionDefault { const message = createBaseFeatureSetDefaults_FeatureSetEditionDefault(); message.edition = object.edition ?? 0; message.overridableFeatures = (object.overridableFeatures !== undefined && object.overridableFeatures !== null) ? FeatureSet.fromPartial(object.overridableFeatures) : undefined; message.fixedFeatures = (object.fixedFeatures !== undefined && object.fixedFeatures !== null) ? FeatureSet.fromPartial(object.fixedFeatures) : undefined; return message; }, }; function createBaseSourceCodeInfo(): SourceCodeInfo { return { location: [] }; } export const SourceCodeInfo: MessageFns = { encode(message: SourceCodeInfo, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { for (const v of message.location) { SourceCodeInfo_Location.encode(v!, writer.uint32(10).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): SourceCodeInfo { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseSourceCodeInfo(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.location.push(SourceCodeInfo_Location.decode(reader, reader.uint32())); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): SourceCodeInfo { return { location: globalThis.Array.isArray(object?.location) ? object.location.map((e: any) => SourceCodeInfo_Location.fromJSON(e)) : [], }; }, toJSON(message: SourceCodeInfo): unknown { const obj: any = {}; if (message.location?.length) { obj.location = message.location.map((e) => SourceCodeInfo_Location.toJSON(e)); } return obj; }, create, I>>(base?: I): SourceCodeInfo { return SourceCodeInfo.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): SourceCodeInfo { const message = createBaseSourceCodeInfo(); message.location = object.location?.map((e) => SourceCodeInfo_Location.fromPartial(e)) || []; return message; }, }; function createBaseSourceCodeInfo_Location(): SourceCodeInfo_Location { return { path: [], span: [], leadingComments: "", trailingComments: "", leadingDetachedComments: [] }; } export const SourceCodeInfo_Location: MessageFns = { encode(message: SourceCodeInfo_Location, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { writer.uint32(10).fork(); for (const v of message.path) { writer.int32(v); } writer.join(); writer.uint32(18).fork(); for (const v of message.span) { writer.int32(v); } writer.join(); if (message.leadingComments !== undefined && message.leadingComments !== "") { writer.uint32(26).string(message.leadingComments); } if (message.trailingComments !== undefined && message.trailingComments !== "") { writer.uint32(34).string(message.trailingComments); } for (const v of message.leadingDetachedComments) { writer.uint32(50).string(v!); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): SourceCodeInfo_Location { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseSourceCodeInfo_Location(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag === 8) { message.path.push(reader.int32()); continue; } if (tag === 10) { const end2 = reader.uint32() + reader.pos; while (reader.pos < end2) { message.path.push(reader.int32()); } continue; } break; case 2: if (tag === 16) { message.span.push(reader.int32()); continue; } if (tag === 18) { const end2 = reader.uint32() + reader.pos; while (reader.pos < end2) { message.span.push(reader.int32()); } continue; } break; case 3: if (tag !== 26) { break; } message.leadingComments = reader.string(); continue; case 4: if (tag !== 34) { break; } message.trailingComments = reader.string(); continue; case 6: if (tag !== 50) { break; } message.leadingDetachedComments.push(reader.string()); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): SourceCodeInfo_Location { return { path: globalThis.Array.isArray(object?.path) ? object.path.map((e: any) => globalThis.Number(e)) : [], span: globalThis.Array.isArray(object?.span) ? object.span.map((e: any) => globalThis.Number(e)) : [], leadingComments: isSet(object.leadingComments) ? globalThis.String(object.leadingComments) : "", trailingComments: isSet(object.trailingComments) ? globalThis.String(object.trailingComments) : "", leadingDetachedComments: globalThis.Array.isArray(object?.leadingDetachedComments) ? object.leadingDetachedComments.map((e: any) => globalThis.String(e)) : [], }; }, toJSON(message: SourceCodeInfo_Location): unknown { const obj: any = {}; if (message.path?.length) { obj.path = message.path.map((e) => Math.round(e)); } if (message.span?.length) { obj.span = message.span.map((e) => Math.round(e)); } if (message.leadingComments !== undefined && message.leadingComments !== "") { obj.leadingComments = message.leadingComments; } if (message.trailingComments !== undefined && message.trailingComments !== "") { obj.trailingComments = message.trailingComments; } if (message.leadingDetachedComments?.length) { obj.leadingDetachedComments = message.leadingDetachedComments; } return obj; }, create, I>>(base?: I): SourceCodeInfo_Location { return SourceCodeInfo_Location.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): SourceCodeInfo_Location { const message = createBaseSourceCodeInfo_Location(); message.path = object.path?.map((e) => e) || []; message.span = object.span?.map((e) => e) || []; message.leadingComments = object.leadingComments ?? ""; message.trailingComments = object.trailingComments ?? ""; message.leadingDetachedComments = object.leadingDetachedComments?.map((e) => e) || []; return message; }, }; function createBaseGeneratedCodeInfo(): GeneratedCodeInfo { return { annotation: [] }; } export const GeneratedCodeInfo: MessageFns = { encode(message: GeneratedCodeInfo, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { for (const v of message.annotation) { GeneratedCodeInfo_Annotation.encode(v!, writer.uint32(10).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): GeneratedCodeInfo { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseGeneratedCodeInfo(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.annotation.push(GeneratedCodeInfo_Annotation.decode(reader, reader.uint32())); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): GeneratedCodeInfo { return { annotation: globalThis.Array.isArray(object?.annotation) ? object.annotation.map((e: any) => GeneratedCodeInfo_Annotation.fromJSON(e)) : [], }; }, toJSON(message: GeneratedCodeInfo): unknown { const obj: any = {}; if (message.annotation?.length) { obj.annotation = message.annotation.map((e) => GeneratedCodeInfo_Annotation.toJSON(e)); } return obj; }, create, I>>(base?: I): GeneratedCodeInfo { return GeneratedCodeInfo.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): GeneratedCodeInfo { const message = createBaseGeneratedCodeInfo(); message.annotation = object.annotation?.map((e) => GeneratedCodeInfo_Annotation.fromPartial(e)) || []; return message; }, }; function createBaseGeneratedCodeInfo_Annotation(): GeneratedCodeInfo_Annotation { return { path: [], sourceFile: "", begin: 0, end: 0, semantic: 0 }; } export const GeneratedCodeInfo_Annotation: MessageFns = { encode(message: GeneratedCodeInfo_Annotation, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { writer.uint32(10).fork(); for (const v of message.path) { writer.int32(v); } writer.join(); if (message.sourceFile !== undefined && message.sourceFile !== "") { writer.uint32(18).string(message.sourceFile); } if (message.begin !== undefined && message.begin !== 0) { writer.uint32(24).int32(message.begin); } if (message.end !== undefined && message.end !== 0) { writer.uint32(32).int32(message.end); } if (message.semantic !== undefined && message.semantic !== 0) { writer.uint32(40).int32(message.semantic); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): GeneratedCodeInfo_Annotation { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseGeneratedCodeInfo_Annotation(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag === 8) { message.path.push(reader.int32()); continue; } if (tag === 10) { const end2 = reader.uint32() + reader.pos; while (reader.pos < end2) { message.path.push(reader.int32()); } continue; } break; case 2: if (tag !== 18) { break; } message.sourceFile = reader.string(); continue; case 3: if (tag !== 24) { break; } message.begin = reader.int32(); continue; case 4: if (tag !== 32) { break; } message.end = reader.int32(); continue; case 5: if (tag !== 40) { break; } message.semantic = reader.int32() as any; continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): GeneratedCodeInfo_Annotation { return { path: globalThis.Array.isArray(object?.path) ? object.path.map((e: any) => globalThis.Number(e)) : [], sourceFile: isSet(object.sourceFile) ? globalThis.String(object.sourceFile) : "", begin: isSet(object.begin) ? globalThis.Number(object.begin) : 0, end: isSet(object.end) ? globalThis.Number(object.end) : 0, semantic: isSet(object.semantic) ? generatedCodeInfo_Annotation_SemanticFromJSON(object.semantic) : 0, }; }, toJSON(message: GeneratedCodeInfo_Annotation): unknown { const obj: any = {}; if (message.path?.length) { obj.path = message.path.map((e) => Math.round(e)); } if (message.sourceFile !== undefined && message.sourceFile !== "") { obj.sourceFile = message.sourceFile; } if (message.begin !== undefined && message.begin !== 0) { obj.begin = Math.round(message.begin); } if (message.end !== undefined && message.end !== 0) { obj.end = Math.round(message.end); } if (message.semantic !== undefined && message.semantic !== 0) { obj.semantic = generatedCodeInfo_Annotation_SemanticToJSON(message.semantic); } return obj; }, create, I>>(base?: I): GeneratedCodeInfo_Annotation { return GeneratedCodeInfo_Annotation.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): GeneratedCodeInfo_Annotation { const message = createBaseGeneratedCodeInfo_Annotation(); message.path = object.path?.map((e) => e) || []; message.sourceFile = object.sourceFile ?? ""; message.begin = object.begin ?? 0; message.end = object.end ?? 0; message.semantic = object.semantic ?? 0; return message; }, }; function bytesFromBase64(b64: string): Uint8Array { if ((globalThis as any).Buffer) { return Uint8Array.from(globalThis.Buffer.from(b64, "base64")); } else { const bin = globalThis.atob(b64); const arr = new Uint8Array(bin.length); for (let i = 0; i < bin.length; ++i) { arr[i] = bin.charCodeAt(i); } return arr; } } function base64FromBytes(arr: Uint8Array): string { if ((globalThis as any).Buffer) { return globalThis.Buffer.from(arr).toString("base64"); } else { const bin: string[] = []; arr.forEach((byte) => { bin.push(globalThis.String.fromCharCode(byte)); }); return globalThis.btoa(bin.join("")); } } type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; export type DeepPartial = T extends Builtin ? T : T extends globalThis.Array ? globalThis.Array> : T extends ReadonlyArray ? ReadonlyArray> : T extends {} ? { [K in keyof T]?: DeepPartial } : Partial; type KeysOfUnion = T extends T ? keyof T : never; export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function longToNumber(int64: { toString(): string }): number { const num = globalThis.Number(int64.toString()); if (num > globalThis.Number.MAX_SAFE_INTEGER) { throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); } if (num < globalThis.Number.MIN_SAFE_INTEGER) { throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER"); } return num; } function isSet(value: any): boolean { return value !== null && value !== undefined; } export interface MessageFns { encode(message: T, writer?: BinaryWriter): BinaryWriter; decode(input: BinaryReader | Uint8Array, length?: number): T; fromJSON(object: any): T; toJSON(message: T): unknown; create, I>>(base?: I): T; fromPartial, I>>(object: I): T; } ================================================ FILE: frontend/src/app/common/type/proto/org/apache/texera/amber/core/virtualidentity.ts ================================================ // Code generated by protoc-gen-ts_proto. DO NOT EDIT. // versions: // protoc-gen-ts_proto v2.2.0 // protoc v6.33.4 // source: org/apache/texera/amber/core/virtualidentity.proto /* eslint-disable */ import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; export const protobufPackage = "org.apache.texera.amber.core"; export interface WorkflowIdentity { id: number; } export interface ExecutionIdentity { id: number; } export interface ActorVirtualIdentity { name: string; } export interface ChannelIdentity { fromWorkerId: ActorVirtualIdentity | undefined; toWorkerId: ActorVirtualIdentity | undefined; isControl: boolean; } export interface OperatorIdentity { id: string; } export interface PhysicalOpIdentity { logicalOpId: OperatorIdentity | undefined; layerName: string; } export interface EmbeddedControlMessageIdentity { id: string; } function createBaseWorkflowIdentity(): WorkflowIdentity { return { id: 0 }; } export const WorkflowIdentity: MessageFns = { encode(message: WorkflowIdentity, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.id !== 0) { writer.uint32(8).int64(message.id); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): WorkflowIdentity { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseWorkflowIdentity(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) { break; } message.id = longToNumber(reader.int64()); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): WorkflowIdentity { return { id: isSet(object.id) ? globalThis.Number(object.id) : 0 }; }, toJSON(message: WorkflowIdentity): unknown { const obj: any = {}; if (message.id !== 0) { obj.id = Math.round(message.id); } return obj; }, create, I>>(base?: I): WorkflowIdentity { return WorkflowIdentity.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): WorkflowIdentity { const message = createBaseWorkflowIdentity(); message.id = object.id ?? 0; return message; }, }; function createBaseExecutionIdentity(): ExecutionIdentity { return { id: 0 }; } export const ExecutionIdentity: MessageFns = { encode(message: ExecutionIdentity, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.id !== 0) { writer.uint32(8).int64(message.id); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): ExecutionIdentity { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseExecutionIdentity(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) { break; } message.id = longToNumber(reader.int64()); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): ExecutionIdentity { return { id: isSet(object.id) ? globalThis.Number(object.id) : 0 }; }, toJSON(message: ExecutionIdentity): unknown { const obj: any = {}; if (message.id !== 0) { obj.id = Math.round(message.id); } return obj; }, create, I>>(base?: I): ExecutionIdentity { return ExecutionIdentity.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): ExecutionIdentity { const message = createBaseExecutionIdentity(); message.id = object.id ?? 0; return message; }, }; function createBaseActorVirtualIdentity(): ActorVirtualIdentity { return { name: "" }; } export const ActorVirtualIdentity: MessageFns = { encode(message: ActorVirtualIdentity, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.name !== "") { writer.uint32(10).string(message.name); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): ActorVirtualIdentity { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseActorVirtualIdentity(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.name = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): ActorVirtualIdentity { return { name: isSet(object.name) ? globalThis.String(object.name) : "" }; }, toJSON(message: ActorVirtualIdentity): unknown { const obj: any = {}; if (message.name !== "") { obj.name = message.name; } return obj; }, create, I>>(base?: I): ActorVirtualIdentity { return ActorVirtualIdentity.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): ActorVirtualIdentity { const message = createBaseActorVirtualIdentity(); message.name = object.name ?? ""; return message; }, }; function createBaseChannelIdentity(): ChannelIdentity { return { fromWorkerId: undefined, toWorkerId: undefined, isControl: false }; } export const ChannelIdentity: MessageFns = { encode(message: ChannelIdentity, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.fromWorkerId !== undefined) { ActorVirtualIdentity.encode(message.fromWorkerId, writer.uint32(10).fork()).join(); } if (message.toWorkerId !== undefined) { ActorVirtualIdentity.encode(message.toWorkerId, writer.uint32(18).fork()).join(); } if (message.isControl !== false) { writer.uint32(24).bool(message.isControl); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): ChannelIdentity { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseChannelIdentity(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.fromWorkerId = ActorVirtualIdentity.decode(reader, reader.uint32()); continue; case 2: if (tag !== 18) { break; } message.toWorkerId = ActorVirtualIdentity.decode(reader, reader.uint32()); continue; case 3: if (tag !== 24) { break; } message.isControl = reader.bool(); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): ChannelIdentity { return { fromWorkerId: isSet(object.fromWorkerId) ? ActorVirtualIdentity.fromJSON(object.fromWorkerId) : undefined, toWorkerId: isSet(object.toWorkerId) ? ActorVirtualIdentity.fromJSON(object.toWorkerId) : undefined, isControl: isSet(object.isControl) ? globalThis.Boolean(object.isControl) : false, }; }, toJSON(message: ChannelIdentity): unknown { const obj: any = {}; if (message.fromWorkerId !== undefined) { obj.fromWorkerId = ActorVirtualIdentity.toJSON(message.fromWorkerId); } if (message.toWorkerId !== undefined) { obj.toWorkerId = ActorVirtualIdentity.toJSON(message.toWorkerId); } if (message.isControl !== false) { obj.isControl = message.isControl; } return obj; }, create, I>>(base?: I): ChannelIdentity { return ChannelIdentity.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): ChannelIdentity { const message = createBaseChannelIdentity(); message.fromWorkerId = (object.fromWorkerId !== undefined && object.fromWorkerId !== null) ? ActorVirtualIdentity.fromPartial(object.fromWorkerId) : undefined; message.toWorkerId = (object.toWorkerId !== undefined && object.toWorkerId !== null) ? ActorVirtualIdentity.fromPartial(object.toWorkerId) : undefined; message.isControl = object.isControl ?? false; return message; }, }; function createBaseOperatorIdentity(): OperatorIdentity { return { id: "" }; } export const OperatorIdentity: MessageFns = { encode(message: OperatorIdentity, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.id !== "") { writer.uint32(10).string(message.id); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): OperatorIdentity { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseOperatorIdentity(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.id = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): OperatorIdentity { return { id: isSet(object.id) ? globalThis.String(object.id) : "" }; }, toJSON(message: OperatorIdentity): unknown { const obj: any = {}; if (message.id !== "") { obj.id = message.id; } return obj; }, create, I>>(base?: I): OperatorIdentity { return OperatorIdentity.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): OperatorIdentity { const message = createBaseOperatorIdentity(); message.id = object.id ?? ""; return message; }, }; function createBasePhysicalOpIdentity(): PhysicalOpIdentity { return { logicalOpId: undefined, layerName: "" }; } export const PhysicalOpIdentity: MessageFns = { encode(message: PhysicalOpIdentity, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.logicalOpId !== undefined) { OperatorIdentity.encode(message.logicalOpId, writer.uint32(10).fork()).join(); } if (message.layerName !== "") { writer.uint32(18).string(message.layerName); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): PhysicalOpIdentity { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBasePhysicalOpIdentity(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.logicalOpId = OperatorIdentity.decode(reader, reader.uint32()); continue; case 2: if (tag !== 18) { break; } message.layerName = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): PhysicalOpIdentity { return { logicalOpId: isSet(object.logicalOpId) ? OperatorIdentity.fromJSON(object.logicalOpId) : undefined, layerName: isSet(object.layerName) ? globalThis.String(object.layerName) : "", }; }, toJSON(message: PhysicalOpIdentity): unknown { const obj: any = {}; if (message.logicalOpId !== undefined) { obj.logicalOpId = OperatorIdentity.toJSON(message.logicalOpId); } if (message.layerName !== "") { obj.layerName = message.layerName; } return obj; }, create, I>>(base?: I): PhysicalOpIdentity { return PhysicalOpIdentity.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): PhysicalOpIdentity { const message = createBasePhysicalOpIdentity(); message.logicalOpId = (object.logicalOpId !== undefined && object.logicalOpId !== null) ? OperatorIdentity.fromPartial(object.logicalOpId) : undefined; message.layerName = object.layerName ?? ""; return message; }, }; function createBaseEmbeddedControlMessageIdentity(): EmbeddedControlMessageIdentity { return { id: "" }; } export const EmbeddedControlMessageIdentity: MessageFns = { encode(message: EmbeddedControlMessageIdentity, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.id !== "") { writer.uint32(10).string(message.id); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): EmbeddedControlMessageIdentity { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseEmbeddedControlMessageIdentity(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.id = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): EmbeddedControlMessageIdentity { return { id: isSet(object.id) ? globalThis.String(object.id) : "" }; }, toJSON(message: EmbeddedControlMessageIdentity): unknown { const obj: any = {}; if (message.id !== "") { obj.id = message.id; } return obj; }, create, I>>(base?: I): EmbeddedControlMessageIdentity { return EmbeddedControlMessageIdentity.fromPartial(base ?? ({} as any)); }, fromPartial, I>>( object: I, ): EmbeddedControlMessageIdentity { const message = createBaseEmbeddedControlMessageIdentity(); message.id = object.id ?? ""; return message; }, }; type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; export type DeepPartial = T extends Builtin ? T : T extends globalThis.Array ? globalThis.Array> : T extends ReadonlyArray ? ReadonlyArray> : T extends {} ? { [K in keyof T]?: DeepPartial } : Partial; type KeysOfUnion = T extends T ? keyof T : never; export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function longToNumber(int64: { toString(): string }): number { const num = globalThis.Number(int64.toString()); if (num > globalThis.Number.MAX_SAFE_INTEGER) { throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); } if (num < globalThis.Number.MIN_SAFE_INTEGER) { throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER"); } return num; } function isSet(value: any): boolean { return value !== null && value !== undefined; } export interface MessageFns { encode(message: T, writer?: BinaryWriter): BinaryWriter; decode(input: BinaryReader | Uint8Array, length?: number): T; fromJSON(object: any): T; toJSON(message: T): unknown; create, I>>(base?: I): T; fromPartial, I>>(object: I): T; } ================================================ FILE: frontend/src/app/common/type/proto/org/apache/texera/amber/core/workflow.ts ================================================ // Code generated by protoc-gen-ts_proto. DO NOT EDIT. // versions: // protoc-gen-ts_proto v2.2.0 // protoc v6.33.4 // source: org/apache/texera/amber/core/workflow.proto /* eslint-disable */ import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; import { PhysicalOpIdentity } from "./virtualidentity"; export const protobufPackage = "org.apache.texera.amber.core"; export interface PortIdentity { id: number; internal: boolean; } export interface GlobalPortIdentity { opId: PhysicalOpIdentity | undefined; portId: PortIdentity | undefined; input: boolean; } export interface InputPort { id: PortIdentity | undefined; displayName: string; disallowMultiLinks: boolean; dependencies: PortIdentity[]; } export interface OutputPort { id: PortIdentity | undefined; displayName: string; blocking: boolean; mode: OutputPort_OutputMode; } export enum OutputPort_OutputMode { /** SET_SNAPSHOT - outputs complete result set snapshot for each update */ SET_SNAPSHOT = 0, /** SET_DELTA - outputs incremental result set delta for each update */ SET_DELTA = 1, /** * SINGLE_SNAPSHOT - outputs a single snapshot for the entire execution, * used explicitly to support visualization operators that may exceed the memory limit * TODO: remove this mode after we have a better solution for output size limit */ SINGLE_SNAPSHOT = 2, UNRECOGNIZED = -1, } export function outputPort_OutputModeFromJSON(object: any): OutputPort_OutputMode { switch (object) { case 0: case "SET_SNAPSHOT": return OutputPort_OutputMode.SET_SNAPSHOT; case 1: case "SET_DELTA": return OutputPort_OutputMode.SET_DELTA; case 2: case "SINGLE_SNAPSHOT": return OutputPort_OutputMode.SINGLE_SNAPSHOT; case -1: case "UNRECOGNIZED": default: return OutputPort_OutputMode.UNRECOGNIZED; } } export function outputPort_OutputModeToJSON(object: OutputPort_OutputMode): string { switch (object) { case OutputPort_OutputMode.SET_SNAPSHOT: return "SET_SNAPSHOT"; case OutputPort_OutputMode.SET_DELTA: return "SET_DELTA"; case OutputPort_OutputMode.SINGLE_SNAPSHOT: return "SINGLE_SNAPSHOT"; case OutputPort_OutputMode.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } export interface PhysicalLink { fromOpId: PhysicalOpIdentity | undefined; fromPortId: PortIdentity | undefined; toOpId: PhysicalOpIdentity | undefined; toPortId: PortIdentity | undefined; } function createBasePortIdentity(): PortIdentity { return { id: 0, internal: false }; } export const PortIdentity: MessageFns = { encode(message: PortIdentity, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.id !== 0) { writer.uint32(8).int32(message.id); } if (message.internal !== false) { writer.uint32(16).bool(message.internal); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): PortIdentity { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBasePortIdentity(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 8) { break; } message.id = reader.int32(); continue; case 2: if (tag !== 16) { break; } message.internal = reader.bool(); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): PortIdentity { return { id: isSet(object.id) ? globalThis.Number(object.id) : 0, internal: isSet(object.internal) ? globalThis.Boolean(object.internal) : false, }; }, toJSON(message: PortIdentity): unknown { const obj: any = {}; if (message.id !== 0) { obj.id = Math.round(message.id); } if (message.internal !== false) { obj.internal = message.internal; } return obj; }, create, I>>(base?: I): PortIdentity { return PortIdentity.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): PortIdentity { const message = createBasePortIdentity(); message.id = object.id ?? 0; message.internal = object.internal ?? false; return message; }, }; function createBaseGlobalPortIdentity(): GlobalPortIdentity { return { opId: undefined, portId: undefined, input: false }; } export const GlobalPortIdentity: MessageFns = { encode(message: GlobalPortIdentity, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.opId !== undefined) { PhysicalOpIdentity.encode(message.opId, writer.uint32(10).fork()).join(); } if (message.portId !== undefined) { PortIdentity.encode(message.portId, writer.uint32(18).fork()).join(); } if (message.input !== false) { writer.uint32(24).bool(message.input); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): GlobalPortIdentity { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseGlobalPortIdentity(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.opId = PhysicalOpIdentity.decode(reader, reader.uint32()); continue; case 2: if (tag !== 18) { break; } message.portId = PortIdentity.decode(reader, reader.uint32()); continue; case 3: if (tag !== 24) { break; } message.input = reader.bool(); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): GlobalPortIdentity { return { opId: isSet(object.opId) ? PhysicalOpIdentity.fromJSON(object.opId) : undefined, portId: isSet(object.portId) ? PortIdentity.fromJSON(object.portId) : undefined, input: isSet(object.input) ? globalThis.Boolean(object.input) : false, }; }, toJSON(message: GlobalPortIdentity): unknown { const obj: any = {}; if (message.opId !== undefined) { obj.opId = PhysicalOpIdentity.toJSON(message.opId); } if (message.portId !== undefined) { obj.portId = PortIdentity.toJSON(message.portId); } if (message.input !== false) { obj.input = message.input; } return obj; }, create, I>>(base?: I): GlobalPortIdentity { return GlobalPortIdentity.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): GlobalPortIdentity { const message = createBaseGlobalPortIdentity(); message.opId = (object.opId !== undefined && object.opId !== null) ? PhysicalOpIdentity.fromPartial(object.opId) : undefined; message.portId = (object.portId !== undefined && object.portId !== null) ? PortIdentity.fromPartial(object.portId) : undefined; message.input = object.input ?? false; return message; }, }; function createBaseInputPort(): InputPort { return { id: undefined, displayName: "", disallowMultiLinks: false, dependencies: [] }; } export const InputPort: MessageFns = { encode(message: InputPort, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.id !== undefined) { PortIdentity.encode(message.id, writer.uint32(10).fork()).join(); } if (message.displayName !== "") { writer.uint32(18).string(message.displayName); } if (message.disallowMultiLinks !== false) { writer.uint32(24).bool(message.disallowMultiLinks); } for (const v of message.dependencies) { PortIdentity.encode(v!, writer.uint32(34).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): InputPort { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseInputPort(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.id = PortIdentity.decode(reader, reader.uint32()); continue; case 2: if (tag !== 18) { break; } message.displayName = reader.string(); continue; case 3: if (tag !== 24) { break; } message.disallowMultiLinks = reader.bool(); continue; case 4: if (tag !== 34) { break; } message.dependencies.push(PortIdentity.decode(reader, reader.uint32())); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): InputPort { return { id: isSet(object.id) ? PortIdentity.fromJSON(object.id) : undefined, displayName: isSet(object.displayName) ? globalThis.String(object.displayName) : "", disallowMultiLinks: isSet(object.disallowMultiLinks) ? globalThis.Boolean(object.disallowMultiLinks) : false, dependencies: globalThis.Array.isArray(object?.dependencies) ? object.dependencies.map((e: any) => PortIdentity.fromJSON(e)) : [], }; }, toJSON(message: InputPort): unknown { const obj: any = {}; if (message.id !== undefined) { obj.id = PortIdentity.toJSON(message.id); } if (message.displayName !== "") { obj.displayName = message.displayName; } if (message.disallowMultiLinks !== false) { obj.disallowMultiLinks = message.disallowMultiLinks; } if (message.dependencies?.length) { obj.dependencies = message.dependencies.map((e) => PortIdentity.toJSON(e)); } return obj; }, create, I>>(base?: I): InputPort { return InputPort.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): InputPort { const message = createBaseInputPort(); message.id = (object.id !== undefined && object.id !== null) ? PortIdentity.fromPartial(object.id) : undefined; message.displayName = object.displayName ?? ""; message.disallowMultiLinks = object.disallowMultiLinks ?? false; message.dependencies = object.dependencies?.map((e) => PortIdentity.fromPartial(e)) || []; return message; }, }; function createBaseOutputPort(): OutputPort { return { id: undefined, displayName: "", blocking: false, mode: 0 }; } export const OutputPort: MessageFns = { encode(message: OutputPort, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.id !== undefined) { PortIdentity.encode(message.id, writer.uint32(10).fork()).join(); } if (message.displayName !== "") { writer.uint32(18).string(message.displayName); } if (message.blocking !== false) { writer.uint32(24).bool(message.blocking); } if (message.mode !== 0) { writer.uint32(32).int32(message.mode); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): OutputPort { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseOutputPort(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.id = PortIdentity.decode(reader, reader.uint32()); continue; case 2: if (tag !== 18) { break; } message.displayName = reader.string(); continue; case 3: if (tag !== 24) { break; } message.blocking = reader.bool(); continue; case 4: if (tag !== 32) { break; } message.mode = reader.int32() as any; continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): OutputPort { return { id: isSet(object.id) ? PortIdentity.fromJSON(object.id) : undefined, displayName: isSet(object.displayName) ? globalThis.String(object.displayName) : "", blocking: isSet(object.blocking) ? globalThis.Boolean(object.blocking) : false, mode: isSet(object.mode) ? outputPort_OutputModeFromJSON(object.mode) : 0, }; }, toJSON(message: OutputPort): unknown { const obj: any = {}; if (message.id !== undefined) { obj.id = PortIdentity.toJSON(message.id); } if (message.displayName !== "") { obj.displayName = message.displayName; } if (message.blocking !== false) { obj.blocking = message.blocking; } if (message.mode !== 0) { obj.mode = outputPort_OutputModeToJSON(message.mode); } return obj; }, create, I>>(base?: I): OutputPort { return OutputPort.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): OutputPort { const message = createBaseOutputPort(); message.id = (object.id !== undefined && object.id !== null) ? PortIdentity.fromPartial(object.id) : undefined; message.displayName = object.displayName ?? ""; message.blocking = object.blocking ?? false; message.mode = object.mode ?? 0; return message; }, }; function createBasePhysicalLink(): PhysicalLink { return { fromOpId: undefined, fromPortId: undefined, toOpId: undefined, toPortId: undefined }; } export const PhysicalLink: MessageFns = { encode(message: PhysicalLink, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.fromOpId !== undefined) { PhysicalOpIdentity.encode(message.fromOpId, writer.uint32(10).fork()).join(); } if (message.fromPortId !== undefined) { PortIdentity.encode(message.fromPortId, writer.uint32(18).fork()).join(); } if (message.toOpId !== undefined) { PhysicalOpIdentity.encode(message.toOpId, writer.uint32(26).fork()).join(); } if (message.toPortId !== undefined) { PortIdentity.encode(message.toPortId, writer.uint32(34).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): PhysicalLink { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBasePhysicalLink(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.fromOpId = PhysicalOpIdentity.decode(reader, reader.uint32()); continue; case 2: if (tag !== 18) { break; } message.fromPortId = PortIdentity.decode(reader, reader.uint32()); continue; case 3: if (tag !== 26) { break; } message.toOpId = PhysicalOpIdentity.decode(reader, reader.uint32()); continue; case 4: if (tag !== 34) { break; } message.toPortId = PortIdentity.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): PhysicalLink { return { fromOpId: isSet(object.fromOpId) ? PhysicalOpIdentity.fromJSON(object.fromOpId) : undefined, fromPortId: isSet(object.fromPortId) ? PortIdentity.fromJSON(object.fromPortId) : undefined, toOpId: isSet(object.toOpId) ? PhysicalOpIdentity.fromJSON(object.toOpId) : undefined, toPortId: isSet(object.toPortId) ? PortIdentity.fromJSON(object.toPortId) : undefined, }; }, toJSON(message: PhysicalLink): unknown { const obj: any = {}; if (message.fromOpId !== undefined) { obj.fromOpId = PhysicalOpIdentity.toJSON(message.fromOpId); } if (message.fromPortId !== undefined) { obj.fromPortId = PortIdentity.toJSON(message.fromPortId); } if (message.toOpId !== undefined) { obj.toOpId = PhysicalOpIdentity.toJSON(message.toOpId); } if (message.toPortId !== undefined) { obj.toPortId = PortIdentity.toJSON(message.toPortId); } return obj; }, create, I>>(base?: I): PhysicalLink { return PhysicalLink.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): PhysicalLink { const message = createBasePhysicalLink(); message.fromOpId = (object.fromOpId !== undefined && object.fromOpId !== null) ? PhysicalOpIdentity.fromPartial(object.fromOpId) : undefined; message.fromPortId = (object.fromPortId !== undefined && object.fromPortId !== null) ? PortIdentity.fromPartial(object.fromPortId) : undefined; message.toOpId = (object.toOpId !== undefined && object.toOpId !== null) ? PhysicalOpIdentity.fromPartial(object.toOpId) : undefined; message.toPortId = (object.toPortId !== undefined && object.toPortId !== null) ? PortIdentity.fromPartial(object.toPortId) : undefined; return message; }, }; type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; export type DeepPartial = T extends Builtin ? T : T extends globalThis.Array ? globalThis.Array> : T extends ReadonlyArray ? ReadonlyArray> : T extends {} ? { [K in keyof T]?: DeepPartial } : Partial; type KeysOfUnion = T extends T ? keyof T : never; export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function isSet(value: any): boolean { return value !== null && value !== undefined; } export interface MessageFns { encode(message: T, writer?: BinaryWriter): BinaryWriter; decode(input: BinaryReader | Uint8Array, length?: number): T; fromJSON(object: any): T; toJSON(message: T): unknown; create, I>>(base?: I): T; fromPartial, I>>(object: I): T; } ================================================ FILE: frontend/src/app/common/type/proto/scalapb/scalapb.ts ================================================ // Code generated by protoc-gen-ts_proto. DO NOT EDIT. // versions: // protoc-gen-ts_proto v2.2.0 // protoc v6.33.4 // source: scalapb/scalapb.proto /* eslint-disable */ import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; import { FieldDescriptorProto, FieldOptions as FieldOptions1 } from "../google/protobuf/descriptor"; export const protobufPackage = "scalapb"; export enum MatchType { CONTAINS = 0, EXACT = 1, PRESENCE = 2, UNRECOGNIZED = -1, } export function matchTypeFromJSON(object: any): MatchType { switch (object) { case 0: case "CONTAINS": return MatchType.CONTAINS; case 1: case "EXACT": return MatchType.EXACT; case 2: case "PRESENCE": return MatchType.PRESENCE; case -1: case "UNRECOGNIZED": default: return MatchType.UNRECOGNIZED; } } export function matchTypeToJSON(object: MatchType): string { switch (object) { case MatchType.CONTAINS: return "CONTAINS"; case MatchType.EXACT: return "EXACT"; case MatchType.PRESENCE: return "PRESENCE"; case MatchType.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } export interface ScalaPbOptions { /** If set then it overrides the java_package and package. */ packageName?: | string | undefined; /** * If true, the compiler does not append the proto base file name * into the generated package name. If false (the default), the * generated scala package name is the package_name.basename where * basename is the proto file name without the .proto extension. */ flatPackage?: | boolean | undefined; /** * Adds the following imports at the top of the file (this is meant * to provide implicit TypeMappers) */ import: string[]; /** * Text to add to the generated scala file. This can be used only * when single_file is true. */ preamble: string[]; /** * If true, all messages and enums (but not services) will be written * to a single Scala file. */ singleFile?: | boolean | undefined; /** * By default, wrappers defined at * https://github.com/google/protobuf/blob/master/src/google/protobuf/wrappers.proto, * are mapped to an Option[T] where T is a primitive type. When this field * is set to true, we do not perform this transformation. */ noPrimitiveWrappers?: | boolean | undefined; /** * DEPRECATED. In ScalaPB <= 0.5.47, it was necessary to explicitly enable * primitive_wrappers. This field remains here for backwards compatibility, * but it has no effect on generated code. It is an error to set both * `primitive_wrappers` and `no_primitive_wrappers`. */ primitiveWrappers?: | boolean | undefined; /** * Scala type to be used for repeated fields. If unspecified, * `scala.collection.Seq` will be used. */ collectionType?: | string | undefined; /** * If set to true, all generated messages in this file will preserve unknown * fields. */ preserveUnknownFields?: | boolean | undefined; /** * If defined, sets the name of the file-level object that would be generated. This * object extends `GeneratedFileObject` and contains descriptors, and list of message * and enum companions. */ objectName?: | string | undefined; /** Experimental: scope to apply the given options. */ scope?: | ScalaPbOptions_OptionsScope | undefined; /** If true, lenses will be generated. */ lenses?: | boolean | undefined; /** * If true, then source-code info information will be included in the * generated code - normally the source code info is cleared out to reduce * code size. The source code info is useful for extracting source code * location from the descriptors as well as comments. */ retainSourceCodeInfo?: | boolean | undefined; /** * Scala type to be used for maps. If unspecified, * `scala.collection.immutable.Map` will be used. */ mapType?: | string | undefined; /** If true, no default values will be generated in message constructors. */ noDefaultValuesInConstructor?: boolean | undefined; enumValueNaming?: | ScalaPbOptions_EnumValueNaming | undefined; /** * Indicate if prefix (enum name + optional underscore) should be removed in scala code * Strip is applied before enum value naming changes. */ enumStripPrefix?: | boolean | undefined; /** Scala type to use for bytes fields. */ bytesType?: | string | undefined; /** Enable java conversions for this file. */ javaConversions?: | boolean | undefined; /** List of message options to apply to some messages. */ auxMessageOptions: ScalaPbOptions_AuxMessageOptions[]; /** List of message options to apply to some fields. */ auxFieldOptions: ScalaPbOptions_AuxFieldOptions[]; /** List of message options to apply to some enums. */ auxEnumOptions: ScalaPbOptions_AuxEnumOptions[]; /** List of enum value options to apply to some enum values. */ auxEnumValueOptions: ScalaPbOptions_AuxEnumValueOptions[]; /** List of preprocessors to apply. */ preprocessors: string[]; fieldTransformations: FieldTransformation[]; /** * Ignores all transformations for this file. This is meant to allow specific files to * opt out from transformations inherited through package-scoped options. */ ignoreAllTransformations?: | boolean | undefined; /** If true, getters will be generated. */ getters?: | boolean | undefined; /** * For use in tests only. Inhibit Java conversions even when when generator parameters * request for it. */ testOnlyNoJavaConversions?: boolean | undefined; } /** Whether to apply the options only to this file, or for the entire package (and its subpackages) */ export enum ScalaPbOptions_OptionsScope { /** FILE - Apply the options for this file only (default) */ FILE = 0, /** PACKAGE - Apply the options for the entire package and its subpackages. */ PACKAGE = 1, UNRECOGNIZED = -1, } export function scalaPbOptions_OptionsScopeFromJSON(object: any): ScalaPbOptions_OptionsScope { switch (object) { case 0: case "FILE": return ScalaPbOptions_OptionsScope.FILE; case 1: case "PACKAGE": return ScalaPbOptions_OptionsScope.PACKAGE; case -1: case "UNRECOGNIZED": default: return ScalaPbOptions_OptionsScope.UNRECOGNIZED; } } export function scalaPbOptions_OptionsScopeToJSON(object: ScalaPbOptions_OptionsScope): string { switch (object) { case ScalaPbOptions_OptionsScope.FILE: return "FILE"; case ScalaPbOptions_OptionsScope.PACKAGE: return "PACKAGE"; case ScalaPbOptions_OptionsScope.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } /** Naming convention for generated enum values */ export enum ScalaPbOptions_EnumValueNaming { /** AS_IN_PROTO - Enum value names in Scala use the same name as in the proto */ AS_IN_PROTO = 0, /** CAMEL_CASE - Convert enum values to CamelCase in Scala. */ CAMEL_CASE = 1, UNRECOGNIZED = -1, } export function scalaPbOptions_EnumValueNamingFromJSON(object: any): ScalaPbOptions_EnumValueNaming { switch (object) { case 0: case "AS_IN_PROTO": return ScalaPbOptions_EnumValueNaming.AS_IN_PROTO; case 1: case "CAMEL_CASE": return ScalaPbOptions_EnumValueNaming.CAMEL_CASE; case -1: case "UNRECOGNIZED": default: return ScalaPbOptions_EnumValueNaming.UNRECOGNIZED; } } export function scalaPbOptions_EnumValueNamingToJSON(object: ScalaPbOptions_EnumValueNaming): string { switch (object) { case ScalaPbOptions_EnumValueNaming.AS_IN_PROTO: return "AS_IN_PROTO"; case ScalaPbOptions_EnumValueNaming.CAMEL_CASE: return "CAMEL_CASE"; case ScalaPbOptions_EnumValueNaming.UNRECOGNIZED: default: return "UNRECOGNIZED"; } } /** * AuxMessageOptions enables you to set message-level options through package-scoped options. * This is useful when you can't add a dependency on scalapb.proto from the proto file that * defines the message. */ export interface ScalaPbOptions_AuxMessageOptions { /** The fully-qualified name of the message in the proto name space. */ target?: | string | undefined; /** * Options to apply to the message. If there are any options defined on the target message * they take precedence over the options. */ options?: MessageOptions | undefined; } /** * AuxFieldOptions enables you to set field-level options through package-scoped options. * This is useful when you can't add a dependency on scalapb.proto from the proto file that * defines the field. */ export interface ScalaPbOptions_AuxFieldOptions { /** The fully-qualified name of the field in the proto name space. */ target?: | string | undefined; /** * Options to apply to the field. If there are any options defined on the target message * they take precedence over the options. */ options?: FieldOptions | undefined; } /** * AuxEnumOptions enables you to set enum-level options through package-scoped options. * This is useful when you can't add a dependency on scalapb.proto from the proto file that * defines the enum. */ export interface ScalaPbOptions_AuxEnumOptions { /** The fully-qualified name of the enum in the proto name space. */ target?: | string | undefined; /** * Options to apply to the enum. If there are any options defined on the target enum * they take precedence over the options. */ options?: EnumOptions | undefined; } /** * AuxEnumValueOptions enables you to set enum value level options through package-scoped * options. This is useful when you can't add a dependency on scalapb.proto from the proto * file that defines the enum. */ export interface ScalaPbOptions_AuxEnumValueOptions { /** The fully-qualified name of the enum value in the proto name space. */ target?: | string | undefined; /** * Options to apply to the enum value. If there are any options defined on * the target enum value they take precedence over the options. */ options?: EnumValueOptions | undefined; } export interface MessageOptions { /** Additional classes and traits to mix in to the case class. */ extends: string[]; /** Additional classes and traits to mix in to the companion object. */ companionExtends: string[]; /** Custom annotations to add to the generated case class. */ annotations: string[]; /** * All instances of this message will be converted to this type. An implicit TypeMapper * must be present. */ type?: | string | undefined; /** Custom annotations to add to the companion object of the generated class. */ companionAnnotations: string[]; /** Additional classes and traits to mix in to generated sealed_oneof base trait. */ sealedOneofExtends: string[]; /** * If true, when this message is used as an optional field, do not wrap it in an `Option`. * This is equivalent of setting `(field).no_box` to true on each field with the message type. */ noBox?: | boolean | undefined; /** Custom annotations to add to the generated `unknownFields` case class field. */ unknownFieldsAnnotations: string[]; } /** * Represents a custom Collection type in Scala. This allows ScalaPB to integrate with * collection types that are different enough from the ones in the standard library. */ export interface Collection { /** Type of the collection */ type?: | string | undefined; /** * Set to true if this collection type is not allowed to be empty, for example * cats.data.NonEmptyList. When true, ScalaPB will not generate `clearX` for the repeated * field and not provide a default argument in the constructor. */ nonEmpty?: | boolean | undefined; /** * An Adapter is a Scala object available at runtime that provides certain static methods * that can operate on this collection type. */ adapter?: string | undefined; } export interface FieldOptions { type?: string | undefined; scalaName?: | string | undefined; /** * Can be specified only if this field is repeated. If unspecified, * it falls back to the file option named `collection_type`, which defaults * to `scala.collection.Seq`. */ collectionType?: string | undefined; collection?: | Collection | undefined; /** * If the field is a map, you can specify custom Scala types for the key * or value. */ keyType?: string | undefined; valueType?: | string | undefined; /** Custom annotations to add to the field. */ annotations: string[]; /** * Can be specified only if this field is a map. If unspecified, * it falls back to the file option named `map_type` which defaults to * `scala.collection.immutable.Map` */ mapType?: | string | undefined; /** Do not box this value in Option[T]. If set, this overrides MessageOptions.no_box */ noBox?: | boolean | undefined; /** * Like no_box it does not box a value in Option[T], but also fails parsing when a value * is not provided. This enables to emulate required fields in proto3. */ required?: boolean | undefined; } export interface EnumOptions { /** Additional classes and traits to mix in to the base trait */ extends: string[]; /** Additional classes and traits to mix in to the companion object. */ companionExtends: string[]; /** * All instances of this enum will be converted to this type. An implicit TypeMapper * must be present. */ type?: | string | undefined; /** Custom annotations to add to the generated enum's base class. */ baseAnnotations: string[]; /** Custom annotations to add to the generated trait. */ recognizedAnnotations: string[]; /** Custom annotations to add to the generated Unrecognized case class. */ unrecognizedAnnotations: string[]; } export interface EnumValueOptions { /** Additional classes and traits to mix in to an individual enum value. */ extends: string[]; /** Name in Scala to use for this enum value. */ scalaName?: | string | undefined; /** Custom annotations to add to the generated case object for this enum value. */ annotations: string[]; } export interface OneofOptions { /** Additional traits to mix in to a oneof. */ extends: string[]; /** Name in Scala to use for this oneof field. */ scalaName?: string | undefined; } export interface FieldTransformation { when?: FieldDescriptorProto | undefined; matchType?: MatchType | undefined; set?: FieldOptions1 | undefined; } export interface PreprocessorOutput { optionsByFile: { [key: string]: ScalaPbOptions }; } export interface PreprocessorOutput_OptionsByFileEntry { key: string; value: ScalaPbOptions | undefined; } function createBaseScalaPbOptions(): ScalaPbOptions { return { packageName: "", flatPackage: false, import: [], preamble: [], singleFile: false, noPrimitiveWrappers: false, primitiveWrappers: false, collectionType: "", preserveUnknownFields: true, objectName: "", scope: 0, lenses: true, retainSourceCodeInfo: false, mapType: "", noDefaultValuesInConstructor: false, enumValueNaming: 0, enumStripPrefix: false, bytesType: "", javaConversions: false, auxMessageOptions: [], auxFieldOptions: [], auxEnumOptions: [], auxEnumValueOptions: [], preprocessors: [], fieldTransformations: [], ignoreAllTransformations: false, getters: true, testOnlyNoJavaConversions: false, }; } export const ScalaPbOptions: MessageFns = { encode(message: ScalaPbOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.packageName !== undefined && message.packageName !== "") { writer.uint32(10).string(message.packageName); } if (message.flatPackage !== undefined && message.flatPackage !== false) { writer.uint32(16).bool(message.flatPackage); } for (const v of message.import) { writer.uint32(26).string(v!); } for (const v of message.preamble) { writer.uint32(34).string(v!); } if (message.singleFile !== undefined && message.singleFile !== false) { writer.uint32(40).bool(message.singleFile); } if (message.noPrimitiveWrappers !== undefined && message.noPrimitiveWrappers !== false) { writer.uint32(56).bool(message.noPrimitiveWrappers); } if (message.primitiveWrappers !== undefined && message.primitiveWrappers !== false) { writer.uint32(48).bool(message.primitiveWrappers); } if (message.collectionType !== undefined && message.collectionType !== "") { writer.uint32(66).string(message.collectionType); } if (message.preserveUnknownFields !== undefined && message.preserveUnknownFields !== true) { writer.uint32(72).bool(message.preserveUnknownFields); } if (message.objectName !== undefined && message.objectName !== "") { writer.uint32(82).string(message.objectName); } if (message.scope !== undefined && message.scope !== 0) { writer.uint32(88).int32(message.scope); } if (message.lenses !== undefined && message.lenses !== true) { writer.uint32(96).bool(message.lenses); } if (message.retainSourceCodeInfo !== undefined && message.retainSourceCodeInfo !== false) { writer.uint32(104).bool(message.retainSourceCodeInfo); } if (message.mapType !== undefined && message.mapType !== "") { writer.uint32(114).string(message.mapType); } if (message.noDefaultValuesInConstructor !== undefined && message.noDefaultValuesInConstructor !== false) { writer.uint32(120).bool(message.noDefaultValuesInConstructor); } if (message.enumValueNaming !== undefined && message.enumValueNaming !== 0) { writer.uint32(128).int32(message.enumValueNaming); } if (message.enumStripPrefix !== undefined && message.enumStripPrefix !== false) { writer.uint32(136).bool(message.enumStripPrefix); } if (message.bytesType !== undefined && message.bytesType !== "") { writer.uint32(170).string(message.bytesType); } if (message.javaConversions !== undefined && message.javaConversions !== false) { writer.uint32(184).bool(message.javaConversions); } for (const v of message.auxMessageOptions) { ScalaPbOptions_AuxMessageOptions.encode(v!, writer.uint32(146).fork()).join(); } for (const v of message.auxFieldOptions) { ScalaPbOptions_AuxFieldOptions.encode(v!, writer.uint32(154).fork()).join(); } for (const v of message.auxEnumOptions) { ScalaPbOptions_AuxEnumOptions.encode(v!, writer.uint32(162).fork()).join(); } for (const v of message.auxEnumValueOptions) { ScalaPbOptions_AuxEnumValueOptions.encode(v!, writer.uint32(178).fork()).join(); } for (const v of message.preprocessors) { writer.uint32(194).string(v!); } for (const v of message.fieldTransformations) { FieldTransformation.encode(v!, writer.uint32(202).fork()).join(); } if (message.ignoreAllTransformations !== undefined && message.ignoreAllTransformations !== false) { writer.uint32(208).bool(message.ignoreAllTransformations); } if (message.getters !== undefined && message.getters !== true) { writer.uint32(216).bool(message.getters); } if (message.testOnlyNoJavaConversions !== undefined && message.testOnlyNoJavaConversions !== false) { writer.uint32(7992).bool(message.testOnlyNoJavaConversions); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): ScalaPbOptions { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseScalaPbOptions(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.packageName = reader.string(); continue; case 2: if (tag !== 16) { break; } message.flatPackage = reader.bool(); continue; case 3: if (tag !== 26) { break; } message.import.push(reader.string()); continue; case 4: if (tag !== 34) { break; } message.preamble.push(reader.string()); continue; case 5: if (tag !== 40) { break; } message.singleFile = reader.bool(); continue; case 7: if (tag !== 56) { break; } message.noPrimitiveWrappers = reader.bool(); continue; case 6: if (tag !== 48) { break; } message.primitiveWrappers = reader.bool(); continue; case 8: if (tag !== 66) { break; } message.collectionType = reader.string(); continue; case 9: if (tag !== 72) { break; } message.preserveUnknownFields = reader.bool(); continue; case 10: if (tag !== 82) { break; } message.objectName = reader.string(); continue; case 11: if (tag !== 88) { break; } message.scope = reader.int32() as any; continue; case 12: if (tag !== 96) { break; } message.lenses = reader.bool(); continue; case 13: if (tag !== 104) { break; } message.retainSourceCodeInfo = reader.bool(); continue; case 14: if (tag !== 114) { break; } message.mapType = reader.string(); continue; case 15: if (tag !== 120) { break; } message.noDefaultValuesInConstructor = reader.bool(); continue; case 16: if (tag !== 128) { break; } message.enumValueNaming = reader.int32() as any; continue; case 17: if (tag !== 136) { break; } message.enumStripPrefix = reader.bool(); continue; case 21: if (tag !== 170) { break; } message.bytesType = reader.string(); continue; case 23: if (tag !== 184) { break; } message.javaConversions = reader.bool(); continue; case 18: if (tag !== 146) { break; } message.auxMessageOptions.push(ScalaPbOptions_AuxMessageOptions.decode(reader, reader.uint32())); continue; case 19: if (tag !== 154) { break; } message.auxFieldOptions.push(ScalaPbOptions_AuxFieldOptions.decode(reader, reader.uint32())); continue; case 20: if (tag !== 162) { break; } message.auxEnumOptions.push(ScalaPbOptions_AuxEnumOptions.decode(reader, reader.uint32())); continue; case 22: if (tag !== 178) { break; } message.auxEnumValueOptions.push(ScalaPbOptions_AuxEnumValueOptions.decode(reader, reader.uint32())); continue; case 24: if (tag !== 194) { break; } message.preprocessors.push(reader.string()); continue; case 25: if (tag !== 202) { break; } message.fieldTransformations.push(FieldTransformation.decode(reader, reader.uint32())); continue; case 26: if (tag !== 208) { break; } message.ignoreAllTransformations = reader.bool(); continue; case 27: if (tag !== 216) { break; } message.getters = reader.bool(); continue; case 999: if (tag !== 7992) { break; } message.testOnlyNoJavaConversions = reader.bool(); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): ScalaPbOptions { return { packageName: isSet(object.packageName) ? globalThis.String(object.packageName) : "", flatPackage: isSet(object.flatPackage) ? globalThis.Boolean(object.flatPackage) : false, import: globalThis.Array.isArray(object?.import) ? object.import.map((e: any) => globalThis.String(e)) : [], preamble: globalThis.Array.isArray(object?.preamble) ? object.preamble.map((e: any) => globalThis.String(e)) : [], singleFile: isSet(object.singleFile) ? globalThis.Boolean(object.singleFile) : false, noPrimitiveWrappers: isSet(object.noPrimitiveWrappers) ? globalThis.Boolean(object.noPrimitiveWrappers) : false, primitiveWrappers: isSet(object.primitiveWrappers) ? globalThis.Boolean(object.primitiveWrappers) : false, collectionType: isSet(object.collectionType) ? globalThis.String(object.collectionType) : "", preserveUnknownFields: isSet(object.preserveUnknownFields) ? globalThis.Boolean(object.preserveUnknownFields) : true, objectName: isSet(object.objectName) ? globalThis.String(object.objectName) : "", scope: isSet(object.scope) ? scalaPbOptions_OptionsScopeFromJSON(object.scope) : 0, lenses: isSet(object.lenses) ? globalThis.Boolean(object.lenses) : true, retainSourceCodeInfo: isSet(object.retainSourceCodeInfo) ? globalThis.Boolean(object.retainSourceCodeInfo) : false, mapType: isSet(object.mapType) ? globalThis.String(object.mapType) : "", noDefaultValuesInConstructor: isSet(object.noDefaultValuesInConstructor) ? globalThis.Boolean(object.noDefaultValuesInConstructor) : false, enumValueNaming: isSet(object.enumValueNaming) ? scalaPbOptions_EnumValueNamingFromJSON(object.enumValueNaming) : 0, enumStripPrefix: isSet(object.enumStripPrefix) ? globalThis.Boolean(object.enumStripPrefix) : false, bytesType: isSet(object.bytesType) ? globalThis.String(object.bytesType) : "", javaConversions: isSet(object.javaConversions) ? globalThis.Boolean(object.javaConversions) : false, auxMessageOptions: globalThis.Array.isArray(object?.auxMessageOptions) ? object.auxMessageOptions.map((e: any) => ScalaPbOptions_AuxMessageOptions.fromJSON(e)) : [], auxFieldOptions: globalThis.Array.isArray(object?.auxFieldOptions) ? object.auxFieldOptions.map((e: any) => ScalaPbOptions_AuxFieldOptions.fromJSON(e)) : [], auxEnumOptions: globalThis.Array.isArray(object?.auxEnumOptions) ? object.auxEnumOptions.map((e: any) => ScalaPbOptions_AuxEnumOptions.fromJSON(e)) : [], auxEnumValueOptions: globalThis.Array.isArray(object?.auxEnumValueOptions) ? object.auxEnumValueOptions.map((e: any) => ScalaPbOptions_AuxEnumValueOptions.fromJSON(e)) : [], preprocessors: globalThis.Array.isArray(object?.preprocessors) ? object.preprocessors.map((e: any) => globalThis.String(e)) : [], fieldTransformations: globalThis.Array.isArray(object?.fieldTransformations) ? object.fieldTransformations.map((e: any) => FieldTransformation.fromJSON(e)) : [], ignoreAllTransformations: isSet(object.ignoreAllTransformations) ? globalThis.Boolean(object.ignoreAllTransformations) : false, getters: isSet(object.getters) ? globalThis.Boolean(object.getters) : true, testOnlyNoJavaConversions: isSet(object.testOnlyNoJavaConversions) ? globalThis.Boolean(object.testOnlyNoJavaConversions) : false, }; }, toJSON(message: ScalaPbOptions): unknown { const obj: any = {}; if (message.packageName !== undefined && message.packageName !== "") { obj.packageName = message.packageName; } if (message.flatPackage !== undefined && message.flatPackage !== false) { obj.flatPackage = message.flatPackage; } if (message.import?.length) { obj.import = message.import; } if (message.preamble?.length) { obj.preamble = message.preamble; } if (message.singleFile !== undefined && message.singleFile !== false) { obj.singleFile = message.singleFile; } if (message.noPrimitiveWrappers !== undefined && message.noPrimitiveWrappers !== false) { obj.noPrimitiveWrappers = message.noPrimitiveWrappers; } if (message.primitiveWrappers !== undefined && message.primitiveWrappers !== false) { obj.primitiveWrappers = message.primitiveWrappers; } if (message.collectionType !== undefined && message.collectionType !== "") { obj.collectionType = message.collectionType; } if (message.preserveUnknownFields !== undefined && message.preserveUnknownFields !== true) { obj.preserveUnknownFields = message.preserveUnknownFields; } if (message.objectName !== undefined && message.objectName !== "") { obj.objectName = message.objectName; } if (message.scope !== undefined && message.scope !== 0) { obj.scope = scalaPbOptions_OptionsScopeToJSON(message.scope); } if (message.lenses !== undefined && message.lenses !== true) { obj.lenses = message.lenses; } if (message.retainSourceCodeInfo !== undefined && message.retainSourceCodeInfo !== false) { obj.retainSourceCodeInfo = message.retainSourceCodeInfo; } if (message.mapType !== undefined && message.mapType !== "") { obj.mapType = message.mapType; } if (message.noDefaultValuesInConstructor !== undefined && message.noDefaultValuesInConstructor !== false) { obj.noDefaultValuesInConstructor = message.noDefaultValuesInConstructor; } if (message.enumValueNaming !== undefined && message.enumValueNaming !== 0) { obj.enumValueNaming = scalaPbOptions_EnumValueNamingToJSON(message.enumValueNaming); } if (message.enumStripPrefix !== undefined && message.enumStripPrefix !== false) { obj.enumStripPrefix = message.enumStripPrefix; } if (message.bytesType !== undefined && message.bytesType !== "") { obj.bytesType = message.bytesType; } if (message.javaConversions !== undefined && message.javaConversions !== false) { obj.javaConversions = message.javaConversions; } if (message.auxMessageOptions?.length) { obj.auxMessageOptions = message.auxMessageOptions.map((e) => ScalaPbOptions_AuxMessageOptions.toJSON(e)); } if (message.auxFieldOptions?.length) { obj.auxFieldOptions = message.auxFieldOptions.map((e) => ScalaPbOptions_AuxFieldOptions.toJSON(e)); } if (message.auxEnumOptions?.length) { obj.auxEnumOptions = message.auxEnumOptions.map((e) => ScalaPbOptions_AuxEnumOptions.toJSON(e)); } if (message.auxEnumValueOptions?.length) { obj.auxEnumValueOptions = message.auxEnumValueOptions.map((e) => ScalaPbOptions_AuxEnumValueOptions.toJSON(e)); } if (message.preprocessors?.length) { obj.preprocessors = message.preprocessors; } if (message.fieldTransformations?.length) { obj.fieldTransformations = message.fieldTransformations.map((e) => FieldTransformation.toJSON(e)); } if (message.ignoreAllTransformations !== undefined && message.ignoreAllTransformations !== false) { obj.ignoreAllTransformations = message.ignoreAllTransformations; } if (message.getters !== undefined && message.getters !== true) { obj.getters = message.getters; } if (message.testOnlyNoJavaConversions !== undefined && message.testOnlyNoJavaConversions !== false) { obj.testOnlyNoJavaConversions = message.testOnlyNoJavaConversions; } return obj; }, create, I>>(base?: I): ScalaPbOptions { return ScalaPbOptions.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): ScalaPbOptions { const message = createBaseScalaPbOptions(); message.packageName = object.packageName ?? ""; message.flatPackage = object.flatPackage ?? false; message.import = object.import?.map((e) => e) || []; message.preamble = object.preamble?.map((e) => e) || []; message.singleFile = object.singleFile ?? false; message.noPrimitiveWrappers = object.noPrimitiveWrappers ?? false; message.primitiveWrappers = object.primitiveWrappers ?? false; message.collectionType = object.collectionType ?? ""; message.preserveUnknownFields = object.preserveUnknownFields ?? true; message.objectName = object.objectName ?? ""; message.scope = object.scope ?? 0; message.lenses = object.lenses ?? true; message.retainSourceCodeInfo = object.retainSourceCodeInfo ?? false; message.mapType = object.mapType ?? ""; message.noDefaultValuesInConstructor = object.noDefaultValuesInConstructor ?? false; message.enumValueNaming = object.enumValueNaming ?? 0; message.enumStripPrefix = object.enumStripPrefix ?? false; message.bytesType = object.bytesType ?? ""; message.javaConversions = object.javaConversions ?? false; message.auxMessageOptions = object.auxMessageOptions?.map((e) => ScalaPbOptions_AuxMessageOptions.fromPartial(e)) || []; message.auxFieldOptions = object.auxFieldOptions?.map((e) => ScalaPbOptions_AuxFieldOptions.fromPartial(e)) || []; message.auxEnumOptions = object.auxEnumOptions?.map((e) => ScalaPbOptions_AuxEnumOptions.fromPartial(e)) || []; message.auxEnumValueOptions = object.auxEnumValueOptions?.map((e) => ScalaPbOptions_AuxEnumValueOptions.fromPartial(e)) || []; message.preprocessors = object.preprocessors?.map((e) => e) || []; message.fieldTransformations = object.fieldTransformations?.map((e) => FieldTransformation.fromPartial(e)) || []; message.ignoreAllTransformations = object.ignoreAllTransformations ?? false; message.getters = object.getters ?? true; message.testOnlyNoJavaConversions = object.testOnlyNoJavaConversions ?? false; return message; }, }; function createBaseScalaPbOptions_AuxMessageOptions(): ScalaPbOptions_AuxMessageOptions { return { target: "", options: undefined }; } export const ScalaPbOptions_AuxMessageOptions: MessageFns = { encode(message: ScalaPbOptions_AuxMessageOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.target !== undefined && message.target !== "") { writer.uint32(10).string(message.target); } if (message.options !== undefined) { MessageOptions.encode(message.options, writer.uint32(18).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): ScalaPbOptions_AuxMessageOptions { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseScalaPbOptions_AuxMessageOptions(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.target = reader.string(); continue; case 2: if (tag !== 18) { break; } message.options = MessageOptions.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): ScalaPbOptions_AuxMessageOptions { return { target: isSet(object.target) ? globalThis.String(object.target) : "", options: isSet(object.options) ? MessageOptions.fromJSON(object.options) : undefined, }; }, toJSON(message: ScalaPbOptions_AuxMessageOptions): unknown { const obj: any = {}; if (message.target !== undefined && message.target !== "") { obj.target = message.target; } if (message.options !== undefined) { obj.options = MessageOptions.toJSON(message.options); } return obj; }, create, I>>( base?: I, ): ScalaPbOptions_AuxMessageOptions { return ScalaPbOptions_AuxMessageOptions.fromPartial(base ?? ({} as any)); }, fromPartial, I>>( object: I, ): ScalaPbOptions_AuxMessageOptions { const message = createBaseScalaPbOptions_AuxMessageOptions(); message.target = object.target ?? ""; message.options = (object.options !== undefined && object.options !== null) ? MessageOptions.fromPartial(object.options) : undefined; return message; }, }; function createBaseScalaPbOptions_AuxFieldOptions(): ScalaPbOptions_AuxFieldOptions { return { target: "", options: undefined }; } export const ScalaPbOptions_AuxFieldOptions: MessageFns = { encode(message: ScalaPbOptions_AuxFieldOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.target !== undefined && message.target !== "") { writer.uint32(10).string(message.target); } if (message.options !== undefined) { FieldOptions.encode(message.options, writer.uint32(18).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): ScalaPbOptions_AuxFieldOptions { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseScalaPbOptions_AuxFieldOptions(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.target = reader.string(); continue; case 2: if (tag !== 18) { break; } message.options = FieldOptions.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): ScalaPbOptions_AuxFieldOptions { return { target: isSet(object.target) ? globalThis.String(object.target) : "", options: isSet(object.options) ? FieldOptions.fromJSON(object.options) : undefined, }; }, toJSON(message: ScalaPbOptions_AuxFieldOptions): unknown { const obj: any = {}; if (message.target !== undefined && message.target !== "") { obj.target = message.target; } if (message.options !== undefined) { obj.options = FieldOptions.toJSON(message.options); } return obj; }, create, I>>(base?: I): ScalaPbOptions_AuxFieldOptions { return ScalaPbOptions_AuxFieldOptions.fromPartial(base ?? ({} as any)); }, fromPartial, I>>( object: I, ): ScalaPbOptions_AuxFieldOptions { const message = createBaseScalaPbOptions_AuxFieldOptions(); message.target = object.target ?? ""; message.options = (object.options !== undefined && object.options !== null) ? FieldOptions.fromPartial(object.options) : undefined; return message; }, }; function createBaseScalaPbOptions_AuxEnumOptions(): ScalaPbOptions_AuxEnumOptions { return { target: "", options: undefined }; } export const ScalaPbOptions_AuxEnumOptions: MessageFns = { encode(message: ScalaPbOptions_AuxEnumOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.target !== undefined && message.target !== "") { writer.uint32(10).string(message.target); } if (message.options !== undefined) { EnumOptions.encode(message.options, writer.uint32(18).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): ScalaPbOptions_AuxEnumOptions { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseScalaPbOptions_AuxEnumOptions(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.target = reader.string(); continue; case 2: if (tag !== 18) { break; } message.options = EnumOptions.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): ScalaPbOptions_AuxEnumOptions { return { target: isSet(object.target) ? globalThis.String(object.target) : "", options: isSet(object.options) ? EnumOptions.fromJSON(object.options) : undefined, }; }, toJSON(message: ScalaPbOptions_AuxEnumOptions): unknown { const obj: any = {}; if (message.target !== undefined && message.target !== "") { obj.target = message.target; } if (message.options !== undefined) { obj.options = EnumOptions.toJSON(message.options); } return obj; }, create, I>>(base?: I): ScalaPbOptions_AuxEnumOptions { return ScalaPbOptions_AuxEnumOptions.fromPartial(base ?? ({} as any)); }, fromPartial, I>>( object: I, ): ScalaPbOptions_AuxEnumOptions { const message = createBaseScalaPbOptions_AuxEnumOptions(); message.target = object.target ?? ""; message.options = (object.options !== undefined && object.options !== null) ? EnumOptions.fromPartial(object.options) : undefined; return message; }, }; function createBaseScalaPbOptions_AuxEnumValueOptions(): ScalaPbOptions_AuxEnumValueOptions { return { target: "", options: undefined }; } export const ScalaPbOptions_AuxEnumValueOptions: MessageFns = { encode(message: ScalaPbOptions_AuxEnumValueOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.target !== undefined && message.target !== "") { writer.uint32(10).string(message.target); } if (message.options !== undefined) { EnumValueOptions.encode(message.options, writer.uint32(18).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): ScalaPbOptions_AuxEnumValueOptions { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseScalaPbOptions_AuxEnumValueOptions(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.target = reader.string(); continue; case 2: if (tag !== 18) { break; } message.options = EnumValueOptions.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): ScalaPbOptions_AuxEnumValueOptions { return { target: isSet(object.target) ? globalThis.String(object.target) : "", options: isSet(object.options) ? EnumValueOptions.fromJSON(object.options) : undefined, }; }, toJSON(message: ScalaPbOptions_AuxEnumValueOptions): unknown { const obj: any = {}; if (message.target !== undefined && message.target !== "") { obj.target = message.target; } if (message.options !== undefined) { obj.options = EnumValueOptions.toJSON(message.options); } return obj; }, create, I>>( base?: I, ): ScalaPbOptions_AuxEnumValueOptions { return ScalaPbOptions_AuxEnumValueOptions.fromPartial(base ?? ({} as any)); }, fromPartial, I>>( object: I, ): ScalaPbOptions_AuxEnumValueOptions { const message = createBaseScalaPbOptions_AuxEnumValueOptions(); message.target = object.target ?? ""; message.options = (object.options !== undefined && object.options !== null) ? EnumValueOptions.fromPartial(object.options) : undefined; return message; }, }; function createBaseMessageOptions(): MessageOptions { return { extends: [], companionExtends: [], annotations: [], type: "", companionAnnotations: [], sealedOneofExtends: [], noBox: false, unknownFieldsAnnotations: [], }; } export const MessageOptions: MessageFns = { encode(message: MessageOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { for (const v of message.extends) { writer.uint32(10).string(v!); } for (const v of message.companionExtends) { writer.uint32(18).string(v!); } for (const v of message.annotations) { writer.uint32(26).string(v!); } if (message.type !== undefined && message.type !== "") { writer.uint32(34).string(message.type); } for (const v of message.companionAnnotations) { writer.uint32(42).string(v!); } for (const v of message.sealedOneofExtends) { writer.uint32(50).string(v!); } if (message.noBox !== undefined && message.noBox !== false) { writer.uint32(56).bool(message.noBox); } for (const v of message.unknownFieldsAnnotations) { writer.uint32(66).string(v!); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): MessageOptions { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseMessageOptions(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.extends.push(reader.string()); continue; case 2: if (tag !== 18) { break; } message.companionExtends.push(reader.string()); continue; case 3: if (tag !== 26) { break; } message.annotations.push(reader.string()); continue; case 4: if (tag !== 34) { break; } message.type = reader.string(); continue; case 5: if (tag !== 42) { break; } message.companionAnnotations.push(reader.string()); continue; case 6: if (tag !== 50) { break; } message.sealedOneofExtends.push(reader.string()); continue; case 7: if (tag !== 56) { break; } message.noBox = reader.bool(); continue; case 8: if (tag !== 66) { break; } message.unknownFieldsAnnotations.push(reader.string()); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): MessageOptions { return { extends: globalThis.Array.isArray(object?.extends) ? object.extends.map((e: any) => globalThis.String(e)) : [], companionExtends: globalThis.Array.isArray(object?.companionExtends) ? object.companionExtends.map((e: any) => globalThis.String(e)) : [], annotations: globalThis.Array.isArray(object?.annotations) ? object.annotations.map((e: any) => globalThis.String(e)) : [], type: isSet(object.type) ? globalThis.String(object.type) : "", companionAnnotations: globalThis.Array.isArray(object?.companionAnnotations) ? object.companionAnnotations.map((e: any) => globalThis.String(e)) : [], sealedOneofExtends: globalThis.Array.isArray(object?.sealedOneofExtends) ? object.sealedOneofExtends.map((e: any) => globalThis.String(e)) : [], noBox: isSet(object.noBox) ? globalThis.Boolean(object.noBox) : false, unknownFieldsAnnotations: globalThis.Array.isArray(object?.unknownFieldsAnnotations) ? object.unknownFieldsAnnotations.map((e: any) => globalThis.String(e)) : [], }; }, toJSON(message: MessageOptions): unknown { const obj: any = {}; if (message.extends?.length) { obj.extends = message.extends; } if (message.companionExtends?.length) { obj.companionExtends = message.companionExtends; } if (message.annotations?.length) { obj.annotations = message.annotations; } if (message.type !== undefined && message.type !== "") { obj.type = message.type; } if (message.companionAnnotations?.length) { obj.companionAnnotations = message.companionAnnotations; } if (message.sealedOneofExtends?.length) { obj.sealedOneofExtends = message.sealedOneofExtends; } if (message.noBox !== undefined && message.noBox !== false) { obj.noBox = message.noBox; } if (message.unknownFieldsAnnotations?.length) { obj.unknownFieldsAnnotations = message.unknownFieldsAnnotations; } return obj; }, create, I>>(base?: I): MessageOptions { return MessageOptions.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): MessageOptions { const message = createBaseMessageOptions(); message.extends = object.extends?.map((e) => e) || []; message.companionExtends = object.companionExtends?.map((e) => e) || []; message.annotations = object.annotations?.map((e) => e) || []; message.type = object.type ?? ""; message.companionAnnotations = object.companionAnnotations?.map((e) => e) || []; message.sealedOneofExtends = object.sealedOneofExtends?.map((e) => e) || []; message.noBox = object.noBox ?? false; message.unknownFieldsAnnotations = object.unknownFieldsAnnotations?.map((e) => e) || []; return message; }, }; function createBaseCollection(): Collection { return { type: "", nonEmpty: false, adapter: "" }; } export const Collection: MessageFns = { encode(message: Collection, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.type !== undefined && message.type !== "") { writer.uint32(10).string(message.type); } if (message.nonEmpty !== undefined && message.nonEmpty !== false) { writer.uint32(16).bool(message.nonEmpty); } if (message.adapter !== undefined && message.adapter !== "") { writer.uint32(26).string(message.adapter); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): Collection { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseCollection(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.type = reader.string(); continue; case 2: if (tag !== 16) { break; } message.nonEmpty = reader.bool(); continue; case 3: if (tag !== 26) { break; } message.adapter = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): Collection { return { type: isSet(object.type) ? globalThis.String(object.type) : "", nonEmpty: isSet(object.nonEmpty) ? globalThis.Boolean(object.nonEmpty) : false, adapter: isSet(object.adapter) ? globalThis.String(object.adapter) : "", }; }, toJSON(message: Collection): unknown { const obj: any = {}; if (message.type !== undefined && message.type !== "") { obj.type = message.type; } if (message.nonEmpty !== undefined && message.nonEmpty !== false) { obj.nonEmpty = message.nonEmpty; } if (message.adapter !== undefined && message.adapter !== "") { obj.adapter = message.adapter; } return obj; }, create, I>>(base?: I): Collection { return Collection.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): Collection { const message = createBaseCollection(); message.type = object.type ?? ""; message.nonEmpty = object.nonEmpty ?? false; message.adapter = object.adapter ?? ""; return message; }, }; function createBaseFieldOptions(): FieldOptions { return { type: "", scalaName: "", collectionType: "", collection: undefined, keyType: "", valueType: "", annotations: [], mapType: "", noBox: false, required: false, }; } export const FieldOptions: MessageFns = { encode(message: FieldOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.type !== undefined && message.type !== "") { writer.uint32(10).string(message.type); } if (message.scalaName !== undefined && message.scalaName !== "") { writer.uint32(18).string(message.scalaName); } if (message.collectionType !== undefined && message.collectionType !== "") { writer.uint32(26).string(message.collectionType); } if (message.collection !== undefined) { Collection.encode(message.collection, writer.uint32(66).fork()).join(); } if (message.keyType !== undefined && message.keyType !== "") { writer.uint32(34).string(message.keyType); } if (message.valueType !== undefined && message.valueType !== "") { writer.uint32(42).string(message.valueType); } for (const v of message.annotations) { writer.uint32(50).string(v!); } if (message.mapType !== undefined && message.mapType !== "") { writer.uint32(58).string(message.mapType); } if (message.noBox !== undefined && message.noBox !== false) { writer.uint32(240).bool(message.noBox); } if (message.required !== undefined && message.required !== false) { writer.uint32(248).bool(message.required); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): FieldOptions { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseFieldOptions(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.type = reader.string(); continue; case 2: if (tag !== 18) { break; } message.scalaName = reader.string(); continue; case 3: if (tag !== 26) { break; } message.collectionType = reader.string(); continue; case 8: if (tag !== 66) { break; } message.collection = Collection.decode(reader, reader.uint32()); continue; case 4: if (tag !== 34) { break; } message.keyType = reader.string(); continue; case 5: if (tag !== 42) { break; } message.valueType = reader.string(); continue; case 6: if (tag !== 50) { break; } message.annotations.push(reader.string()); continue; case 7: if (tag !== 58) { break; } message.mapType = reader.string(); continue; case 30: if (tag !== 240) { break; } message.noBox = reader.bool(); continue; case 31: if (tag !== 248) { break; } message.required = reader.bool(); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): FieldOptions { return { type: isSet(object.type) ? globalThis.String(object.type) : "", scalaName: isSet(object.scalaName) ? globalThis.String(object.scalaName) : "", collectionType: isSet(object.collectionType) ? globalThis.String(object.collectionType) : "", collection: isSet(object.collection) ? Collection.fromJSON(object.collection) : undefined, keyType: isSet(object.keyType) ? globalThis.String(object.keyType) : "", valueType: isSet(object.valueType) ? globalThis.String(object.valueType) : "", annotations: globalThis.Array.isArray(object?.annotations) ? object.annotations.map((e: any) => globalThis.String(e)) : [], mapType: isSet(object.mapType) ? globalThis.String(object.mapType) : "", noBox: isSet(object.noBox) ? globalThis.Boolean(object.noBox) : false, required: isSet(object.required) ? globalThis.Boolean(object.required) : false, }; }, toJSON(message: FieldOptions): unknown { const obj: any = {}; if (message.type !== undefined && message.type !== "") { obj.type = message.type; } if (message.scalaName !== undefined && message.scalaName !== "") { obj.scalaName = message.scalaName; } if (message.collectionType !== undefined && message.collectionType !== "") { obj.collectionType = message.collectionType; } if (message.collection !== undefined) { obj.collection = Collection.toJSON(message.collection); } if (message.keyType !== undefined && message.keyType !== "") { obj.keyType = message.keyType; } if (message.valueType !== undefined && message.valueType !== "") { obj.valueType = message.valueType; } if (message.annotations?.length) { obj.annotations = message.annotations; } if (message.mapType !== undefined && message.mapType !== "") { obj.mapType = message.mapType; } if (message.noBox !== undefined && message.noBox !== false) { obj.noBox = message.noBox; } if (message.required !== undefined && message.required !== false) { obj.required = message.required; } return obj; }, create, I>>(base?: I): FieldOptions { return FieldOptions.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): FieldOptions { const message = createBaseFieldOptions(); message.type = object.type ?? ""; message.scalaName = object.scalaName ?? ""; message.collectionType = object.collectionType ?? ""; message.collection = (object.collection !== undefined && object.collection !== null) ? Collection.fromPartial(object.collection) : undefined; message.keyType = object.keyType ?? ""; message.valueType = object.valueType ?? ""; message.annotations = object.annotations?.map((e) => e) || []; message.mapType = object.mapType ?? ""; message.noBox = object.noBox ?? false; message.required = object.required ?? false; return message; }, }; function createBaseEnumOptions(): EnumOptions { return { extends: [], companionExtends: [], type: "", baseAnnotations: [], recognizedAnnotations: [], unrecognizedAnnotations: [], }; } export const EnumOptions: MessageFns = { encode(message: EnumOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { for (const v of message.extends) { writer.uint32(10).string(v!); } for (const v of message.companionExtends) { writer.uint32(18).string(v!); } if (message.type !== undefined && message.type !== "") { writer.uint32(26).string(message.type); } for (const v of message.baseAnnotations) { writer.uint32(34).string(v!); } for (const v of message.recognizedAnnotations) { writer.uint32(42).string(v!); } for (const v of message.unrecognizedAnnotations) { writer.uint32(50).string(v!); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): EnumOptions { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseEnumOptions(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.extends.push(reader.string()); continue; case 2: if (tag !== 18) { break; } message.companionExtends.push(reader.string()); continue; case 3: if (tag !== 26) { break; } message.type = reader.string(); continue; case 4: if (tag !== 34) { break; } message.baseAnnotations.push(reader.string()); continue; case 5: if (tag !== 42) { break; } message.recognizedAnnotations.push(reader.string()); continue; case 6: if (tag !== 50) { break; } message.unrecognizedAnnotations.push(reader.string()); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): EnumOptions { return { extends: globalThis.Array.isArray(object?.extends) ? object.extends.map((e: any) => globalThis.String(e)) : [], companionExtends: globalThis.Array.isArray(object?.companionExtends) ? object.companionExtends.map((e: any) => globalThis.String(e)) : [], type: isSet(object.type) ? globalThis.String(object.type) : "", baseAnnotations: globalThis.Array.isArray(object?.baseAnnotations) ? object.baseAnnotations.map((e: any) => globalThis.String(e)) : [], recognizedAnnotations: globalThis.Array.isArray(object?.recognizedAnnotations) ? object.recognizedAnnotations.map((e: any) => globalThis.String(e)) : [], unrecognizedAnnotations: globalThis.Array.isArray(object?.unrecognizedAnnotations) ? object.unrecognizedAnnotations.map((e: any) => globalThis.String(e)) : [], }; }, toJSON(message: EnumOptions): unknown { const obj: any = {}; if (message.extends?.length) { obj.extends = message.extends; } if (message.companionExtends?.length) { obj.companionExtends = message.companionExtends; } if (message.type !== undefined && message.type !== "") { obj.type = message.type; } if (message.baseAnnotations?.length) { obj.baseAnnotations = message.baseAnnotations; } if (message.recognizedAnnotations?.length) { obj.recognizedAnnotations = message.recognizedAnnotations; } if (message.unrecognizedAnnotations?.length) { obj.unrecognizedAnnotations = message.unrecognizedAnnotations; } return obj; }, create, I>>(base?: I): EnumOptions { return EnumOptions.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): EnumOptions { const message = createBaseEnumOptions(); message.extends = object.extends?.map((e) => e) || []; message.companionExtends = object.companionExtends?.map((e) => e) || []; message.type = object.type ?? ""; message.baseAnnotations = object.baseAnnotations?.map((e) => e) || []; message.recognizedAnnotations = object.recognizedAnnotations?.map((e) => e) || []; message.unrecognizedAnnotations = object.unrecognizedAnnotations?.map((e) => e) || []; return message; }, }; function createBaseEnumValueOptions(): EnumValueOptions { return { extends: [], scalaName: "", annotations: [] }; } export const EnumValueOptions: MessageFns = { encode(message: EnumValueOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { for (const v of message.extends) { writer.uint32(10).string(v!); } if (message.scalaName !== undefined && message.scalaName !== "") { writer.uint32(18).string(message.scalaName); } for (const v of message.annotations) { writer.uint32(26).string(v!); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): EnumValueOptions { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseEnumValueOptions(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.extends.push(reader.string()); continue; case 2: if (tag !== 18) { break; } message.scalaName = reader.string(); continue; case 3: if (tag !== 26) { break; } message.annotations.push(reader.string()); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): EnumValueOptions { return { extends: globalThis.Array.isArray(object?.extends) ? object.extends.map((e: any) => globalThis.String(e)) : [], scalaName: isSet(object.scalaName) ? globalThis.String(object.scalaName) : "", annotations: globalThis.Array.isArray(object?.annotations) ? object.annotations.map((e: any) => globalThis.String(e)) : [], }; }, toJSON(message: EnumValueOptions): unknown { const obj: any = {}; if (message.extends?.length) { obj.extends = message.extends; } if (message.scalaName !== undefined && message.scalaName !== "") { obj.scalaName = message.scalaName; } if (message.annotations?.length) { obj.annotations = message.annotations; } return obj; }, create, I>>(base?: I): EnumValueOptions { return EnumValueOptions.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): EnumValueOptions { const message = createBaseEnumValueOptions(); message.extends = object.extends?.map((e) => e) || []; message.scalaName = object.scalaName ?? ""; message.annotations = object.annotations?.map((e) => e) || []; return message; }, }; function createBaseOneofOptions(): OneofOptions { return { extends: [], scalaName: "" }; } export const OneofOptions: MessageFns = { encode(message: OneofOptions, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { for (const v of message.extends) { writer.uint32(10).string(v!); } if (message.scalaName !== undefined && message.scalaName !== "") { writer.uint32(18).string(message.scalaName); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): OneofOptions { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseOneofOptions(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.extends.push(reader.string()); continue; case 2: if (tag !== 18) { break; } message.scalaName = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): OneofOptions { return { extends: globalThis.Array.isArray(object?.extends) ? object.extends.map((e: any) => globalThis.String(e)) : [], scalaName: isSet(object.scalaName) ? globalThis.String(object.scalaName) : "", }; }, toJSON(message: OneofOptions): unknown { const obj: any = {}; if (message.extends?.length) { obj.extends = message.extends; } if (message.scalaName !== undefined && message.scalaName !== "") { obj.scalaName = message.scalaName; } return obj; }, create, I>>(base?: I): OneofOptions { return OneofOptions.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): OneofOptions { const message = createBaseOneofOptions(); message.extends = object.extends?.map((e) => e) || []; message.scalaName = object.scalaName ?? ""; return message; }, }; function createBaseFieldTransformation(): FieldTransformation { return { when: undefined, matchType: 0, set: undefined }; } export const FieldTransformation: MessageFns = { encode(message: FieldTransformation, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.when !== undefined) { FieldDescriptorProto.encode(message.when, writer.uint32(10).fork()).join(); } if (message.matchType !== undefined && message.matchType !== 0) { writer.uint32(16).int32(message.matchType); } if (message.set !== undefined) { FieldOptions1.encode(message.set, writer.uint32(26).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): FieldTransformation { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBaseFieldTransformation(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.when = FieldDescriptorProto.decode(reader, reader.uint32()); continue; case 2: if (tag !== 16) { break; } message.matchType = reader.int32() as any; continue; case 3: if (tag !== 26) { break; } message.set = FieldOptions1.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): FieldTransformation { return { when: isSet(object.when) ? FieldDescriptorProto.fromJSON(object.when) : undefined, matchType: isSet(object.matchType) ? matchTypeFromJSON(object.matchType) : 0, set: isSet(object.set) ? FieldOptions1.fromJSON(object.set) : undefined, }; }, toJSON(message: FieldTransformation): unknown { const obj: any = {}; if (message.when !== undefined) { obj.when = FieldDescriptorProto.toJSON(message.when); } if (message.matchType !== undefined && message.matchType !== 0) { obj.matchType = matchTypeToJSON(message.matchType); } if (message.set !== undefined) { obj.set = FieldOptions1.toJSON(message.set); } return obj; }, create, I>>(base?: I): FieldTransformation { return FieldTransformation.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): FieldTransformation { const message = createBaseFieldTransformation(); message.when = (object.when !== undefined && object.when !== null) ? FieldDescriptorProto.fromPartial(object.when) : undefined; message.matchType = object.matchType ?? 0; message.set = (object.set !== undefined && object.set !== null) ? FieldOptions1.fromPartial(object.set) : undefined; return message; }, }; function createBasePreprocessorOutput(): PreprocessorOutput { return { optionsByFile: {} }; } export const PreprocessorOutput: MessageFns = { encode(message: PreprocessorOutput, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { Object.entries(message.optionsByFile).forEach(([key, value]) => { PreprocessorOutput_OptionsByFileEntry.encode({ key: key as any, value }, writer.uint32(10).fork()).join(); }); return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): PreprocessorOutput { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBasePreprocessorOutput(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } const entry1 = PreprocessorOutput_OptionsByFileEntry.decode(reader, reader.uint32()); if (entry1.value !== undefined) { message.optionsByFile[entry1.key] = entry1.value; } continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): PreprocessorOutput { return { optionsByFile: isObject(object.optionsByFile) ? Object.entries(object.optionsByFile).reduce<{ [key: string]: ScalaPbOptions }>((acc, [key, value]) => { acc[key] = ScalaPbOptions.fromJSON(value); return acc; }, {}) : {}, }; }, toJSON(message: PreprocessorOutput): unknown { const obj: any = {}; if (message.optionsByFile) { const entries = Object.entries(message.optionsByFile); if (entries.length > 0) { obj.optionsByFile = {}; entries.forEach(([k, v]) => { obj.optionsByFile[k] = ScalaPbOptions.toJSON(v); }); } } return obj; }, create, I>>(base?: I): PreprocessorOutput { return PreprocessorOutput.fromPartial(base ?? ({} as any)); }, fromPartial, I>>(object: I): PreprocessorOutput { const message = createBasePreprocessorOutput(); message.optionsByFile = Object.entries(object.optionsByFile ?? {}).reduce<{ [key: string]: ScalaPbOptions }>( (acc, [key, value]) => { if (value !== undefined) { acc[key] = ScalaPbOptions.fromPartial(value); } return acc; }, {}, ); return message; }, }; function createBasePreprocessorOutput_OptionsByFileEntry(): PreprocessorOutput_OptionsByFileEntry { return { key: "", value: undefined }; } export const PreprocessorOutput_OptionsByFileEntry: MessageFns = { encode(message: PreprocessorOutput_OptionsByFileEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { if (message.key !== "") { writer.uint32(10).string(message.key); } if (message.value !== undefined) { ScalaPbOptions.encode(message.value, writer.uint32(18).fork()).join(); } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): PreprocessorOutput_OptionsByFileEntry { const reader = input instanceof BinaryReader ? input : new BinaryReader(input); let end = length === undefined ? reader.len : reader.pos + length; const message = createBasePreprocessorOutput_OptionsByFileEntry(); while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { case 1: if (tag !== 10) { break; } message.key = reader.string(); continue; case 2: if (tag !== 18) { break; } message.value = ScalaPbOptions.decode(reader, reader.uint32()); continue; } if ((tag & 7) === 4 || tag === 0) { break; } reader.skip(tag & 7); } return message; }, fromJSON(object: any): PreprocessorOutput_OptionsByFileEntry { return { key: isSet(object.key) ? globalThis.String(object.key) : "", value: isSet(object.value) ? ScalaPbOptions.fromJSON(object.value) : undefined, }; }, toJSON(message: PreprocessorOutput_OptionsByFileEntry): unknown { const obj: any = {}; if (message.key !== "") { obj.key = message.key; } if (message.value !== undefined) { obj.value = ScalaPbOptions.toJSON(message.value); } return obj; }, create, I>>( base?: I, ): PreprocessorOutput_OptionsByFileEntry { return PreprocessorOutput_OptionsByFileEntry.fromPartial(base ?? ({} as any)); }, fromPartial, I>>( object: I, ): PreprocessorOutput_OptionsByFileEntry { const message = createBasePreprocessorOutput_OptionsByFileEntry(); message.key = object.key ?? ""; message.value = (object.value !== undefined && object.value !== null) ? ScalaPbOptions.fromPartial(object.value) : undefined; return message; }, }; type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; export type DeepPartial = T extends Builtin ? T : T extends globalThis.Array ? globalThis.Array> : T extends ReadonlyArray ? ReadonlyArray> : T extends {} ? { [K in keyof T]?: DeepPartial } : Partial; type KeysOfUnion = T extends T ? keyof T : never; export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function isObject(value: any): boolean { return typeof value === "object" && value !== null; } function isSet(value: any): boolean { return value !== null && value !== undefined; } export interface MessageFns { encode(message: T, writer?: BinaryWriter): BinaryWriter; decode(input: BinaryReader | Uint8Array, length?: number): T; fromJSON(object: any): T; toJSON(message: T): unknown; create, I>>(base?: I): T; fromPartial, I>>(object: I): T; } ================================================ FILE: frontend/src/app/common/type/user.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Point } from "../../workspace/types/workflow-common.interface"; /** * This interface stores the information about the user account. * Such information is used to identify users and to save their data * Corresponds to `amber/src/main/scala/org/apache/texera/web/resource/auth/UserResource.scala` */ export enum Role { INACTIVE = "INACTIVE", RESTRICTED = "RESTRICTED", REGULAR = "REGULAR", ADMIN = "ADMIN", } export type Second = number; export type MilliSecond = number; export interface User extends Readonly<{ uid: number; name: string; email: string; googleId?: string; role: Role; color?: string; googleAvatar?: string; comment: string; lastLogin?: number; accountCreation?: Second; affiliation?: string; joiningReason: string; }> {} export interface File extends Readonly<{ userId: number; fileId: number; fileName: string; fileSize: number; uploadedTime: number; description: string; }> {} export interface Workflow extends Readonly<{ userId: number; workflowId: number; workflowName: string; creationTime: number; lastModifiedTime: number; }> {} export interface WorkflowQuota { workflowId: number; workflowName: string; executions: ExecutionQuota[]; } export interface ExecutionQuota extends Readonly<{ eid: number; workflowId: number; workflowName: string; resultBytes: number; runTimeStatsBytes: number; logBytes: number; }> {} /** * Coeditor extends User and adds clientId to differentiate local user and collaborative editor */ export interface Coeditor extends User { clientId: string; } /** * This interface is for user-presence information in shared-editing. */ export interface CoeditorState { user: Coeditor; isActive: boolean; userCursor: Point; highlighted?: readonly string[]; unhighlighted?: readonly string[]; currentlyEditing?: string; changed?: string; editingCode?: boolean; } ================================================ FILE: frontend/src/app/common/type/workflow-computing-unit.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ export interface WorkflowComputingUnitResourceLimit { cpuLimit: string; memoryLimit: string; gpuLimit: string; jvmMemorySize: string; shmSize: string; nodeAddresses: string[]; } export type WorkflowComputingUnitType = "local" | "kubernetes"; export interface WorkflowComputingUnit { cuid: number; uid: number; name: string; creationTime: number; terminateTime: number | undefined; type: WorkflowComputingUnitType; uri: string; resource: WorkflowComputingUnitResourceLimit; } export interface WorkflowComputingUnitMetrics { cpuUsage: string; memoryUsage: string; } export interface DashboardWorkflowComputingUnit { computingUnit: WorkflowComputingUnit; status: "Running" | "Pending"; metrics: WorkflowComputingUnitMetrics; isOwner: boolean; accessPrivilege: "READ" | "WRITE" | "NONE"; ownerGoogleAvatar: string; ownerName: string; } ================================================ FILE: frontend/src/app/common/type/workflow.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { WorkflowMetadata } from "../../dashboard/type/workflow-metadata.interface"; import { CommentBox, OperatorLink, OperatorPredicate, Point } from "../../workspace/types/workflow-common.interface"; export enum ExecutionMode { PIPELINED = "PIPELINED", MATERIALIZED = "MATERIALIZED", } export interface WorkflowSettings { dataTransferBatchSize: number; executionMode: ExecutionMode; } /** * WorkflowContent is used to store the information of the workflow * 1. all existing operators and their properties * 2. operator's position on the JointJS paper * 3. operator link predicates * * When the user refreshes the browser, the CachedWorkflow interface will be * automatically cached and loaded once the refresh completes. This information * will then be used to reload the entire workflow. * */ export interface WorkflowContent extends Readonly<{ operators: OperatorPredicate[]; operatorPositions: { [key: string]: Point }; links: OperatorLink[]; commentBoxes: CommentBox[]; settings: WorkflowSettings; }> {} export type Workflow = { content: WorkflowContent } & WorkflowMetadata; ================================================ FILE: frontend/src/app/common/util/array-utils.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Immutable helpers for array updates. */ // Returns a array with an element edited (original array not mutated) export function replaceOneImmutable( arr: ReadonlyArray, predicate: (t: T, idx: number) => boolean, item: T ): ReadonlyArray { const idx = arr.findIndex(predicate); if (idx < 0) return arr; const next = arr.slice(); (next as T[])[idx] = item; return next; } ================================================ FILE: frontend/src/app/common/util/assert.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * assert.ts maintains a set of useful assertion functions. * They are used to provide type hints to help Typescript analyze code. */ /** * * ex:\ * `let foo = ????;`\ * `assertType(foo);`\ * `bar += foo;` */ export function assertType(val: T | any): asserts val is NonNullable { if (val === undefined || val === null) { throw new TypeError(`Expected 'val' to be defined, but received ${val}`); } } export function assert(condition: boolean, message?: string): void { if (!condition) { throw new Error(message); } } interface Primitives { number: number; boolean: boolean; string: string; } type AnyType = { new (...args: any[]): any } | keyof Primitives; type GuardedType = T extends { new (...args: any[]): infer U } ? U : T extends keyof Primitives ? Primitives[T] : never; export function isType(val: any, type: T): val is GuardedType { const interfaceType: AnyType = type; if (typeof interfaceType === "string") { return typeof val === interfaceType; } return val instanceof interfaceType; } export function asType(val: any, type: T): GuardedType { if (!isType(val, type)) { throw new TypeError(`Type Guard expected value ${val} to be of type ${type}, but received ${typeof val}`); } return val; } export function isNull(val: T | null | undefined): val is null | undefined { return val === undefined || val === null; } export function isNotNull(val: T): val is NonNullable { return val !== undefined && val !== null; } export function nonNull(val: T): NonNullable { if (!isNotNull(val)) { throw new TypeError(`Type Guard expected value ${val} to not be null or undefined`); } return val; } ================================================ FILE: frontend/src/app/common/util/computing-unit.util.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, inject } from "@angular/core"; import { NZ_MODAL_DATA } from "ng-zorro-antd/modal"; import { DashboardWorkflowComputingUnit } from "../type/workflow-computing-unit"; @Component({ template: `
    Name {{ unit.computingUnit.name }}
    Status {{ unit.status }}
    Type {{ unit.computingUnit.type }}
    CPU Limit {{ unit.computingUnit.resource.cpuLimit }}
    Memory Limit {{ unit.computingUnit.resource.memoryLimit }}
    GPU Limit {{ unit.computingUnit.resource.gpuLimit || "None" }}
    JVM Memory {{ unit.computingUnit.resource.jvmMemorySize }}
    Shared Memory {{ unit.computingUnit.resource.shmSize }}
    Created {{ createdAt }}
    Access {{ unit.isOwner ? "Owner" : unit.accessPrivilege }}
    `, standalone: false, }) export class ComputingUnitMetadataComponent { readonly unit: DashboardWorkflowComputingUnit = inject(NZ_MODAL_DATA); readonly createdAt = new Date(this.unit.computingUnit.creationTime).toLocaleString(); } export function parseResourceUnit(resource: string): string { // check if has a capacity (is a number followed by a unit) if (!resource || resource === "NaN") return "NaN"; const re = /^(\d+(\.\d+)?)([a-zA-Z]*)$/; const match = resource.match(re); if (match) { return match[3] || ""; } return ""; } export function parseResourceNumber(resource: string): number { // check if has a capacity (is a number followed by a unit) if (!resource || resource === "NaN") return 0; const re = /^(\d+(\.\d+)?)([a-zA-Z]*)$/; const match = resource.match(re); if (match) { return parseFloat(match[1]); } return 0; } export function cpuResourceConversion(from: string, toUnit: string): string { // cpu conversions type CpuUnit = "n" | "u" | "m" | ""; const cpuScales: { [key in CpuUnit]: number } = { n: 1, u: 1_000, m: 1_000_000, "": 1_000_000_000, }; const fromUnit = parseResourceUnit(from) as CpuUnit; const fromNumber = parseResourceNumber(from); // Handle empty unit in input (means cores) const effectiveFromUnit = (fromUnit || "") as CpuUnit; const effectiveToUnit = (toUnit || "") as CpuUnit; // Convert to base units (nanocores) then to target unit const fromScaled = fromNumber * (cpuScales[effectiveFromUnit] || cpuScales["m"]); const toScaled = fromScaled / (cpuScales[effectiveToUnit] || cpuScales[""]); // For display purposes, use appropriate precision if (effectiveToUnit === "") { return toScaled.toFixed(4); // 4 decimal places for cores } else if (effectiveToUnit === "m") { return toScaled.toFixed(2); // 2 decimal places for millicores } else { return Math.round(toScaled).toString(); // Whole numbers for smaller units } } export function memoryResourceConversion(from: string, toUnit: string): string { // memory conversion type MemoryUnit = "Ki" | "Mi" | "Gi" | ""; const memoryScales: { [key in MemoryUnit]: number } = { "": 1, Ki: 1024, Mi: 1024 * 1024, Gi: 1024 * 1024 * 1024, }; const fromUnit = parseResourceUnit(from) as MemoryUnit; const fromNumber = parseResourceNumber(from); // Handle empty unit in input (means bytes) const effectiveFromUnit = (fromUnit || "") as MemoryUnit; const effectiveToUnit = (toUnit || "") as MemoryUnit; // Convert to base units (bytes) then to target unit const fromScaled = fromNumber * (memoryScales[effectiveFromUnit] || 1); const toScaled = fromScaled / (memoryScales[effectiveToUnit] || 1); // For memory, we want to show in the same format as the limit (typically X.XXX Gi) return toScaled.toFixed(4); } export function cpuPercentage(usage: string, limit: string): number { if (usage === "N/A" || limit === "N/A") return 0; // Convert to the same unit for comparison const displayUnit = ""; // Convert to cores for percentage calculation // Use our existing conversion method to get values in the same unit const usageValue = parseFloat(cpuResourceConversion(usage, displayUnit)); const limitValue = parseFloat(cpuResourceConversion(limit, displayUnit)); if (limitValue <= 0) return 0; // Calculate percentage and ensure it doesn't exceed 100% const percentage = (usageValue / limitValue) * 100; return Math.min(percentage, 100); } export function memoryPercentage(usage: string, limit: string): number { if (usage === "N/A" || limit === "N/A") return 0; // Convert to the same unit for comparison const displayUnit = "Gi"; // Convert to GiB for percentage calculation // Use our existing conversion method to get values in the same unit const usageValue = parseFloat(memoryResourceConversion(usage, displayUnit)); const limitValue = parseFloat(memoryResourceConversion(limit, displayUnit)); if (limitValue <= 0) return 0; // Calculate percentage and ensure it doesn't exceed 100% const percentage = (usageValue / limitValue) * 100; return Math.min(percentage, 100); } export function findNearestValidStep(value: number, jvmMemorySteps: number[]): number { if (jvmMemorySteps.length === 0) return 1; if (jvmMemorySteps.includes(value)) return value; // Find the closest step value return jvmMemorySteps.reduce((prev, curr) => { return Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev; }); } export function validateName(trimmedName: string): string | null { if (!trimmedName) return "Computing unit name cannot be empty"; if (trimmedName.length > 128) return "Computing unit name cannot exceed 128 characters"; return null; } export function getComputingUnitBadgeColor(status: string): string { switch (status) { case "Running": return "green"; case "Pending": return "gold"; default: return "red"; } } export function getComputingUnitStatusTooltip(entry: DashboardWorkflowComputingUnit): string { switch (entry.status) { case "Running": return "Ready to use"; case "Pending": return "Computing unit is starting up"; default: return entry.status; } } export function getComputingUnitCpuStatus(percentage: number): "success" | "exception" | "active" | "normal" { if (percentage > 90) return "exception"; if (percentage > 50) return "normal"; return "success"; } export function getComputingUnitMemoryStatus(percentage: number): "success" | "exception" | "active" | "normal" { if (percentage > 90) return "exception"; if (percentage > 50) return "normal"; return "success"; } export function getComputingUnitCpuLimitUnit(unit: string) { if (unit === "") { return "CPU"; } return unit; } export function isComputingUnitShmTooLarge( selectedMemory: string, shmSizeValue: number, shmSizeUnit: "Mi" | "Gi" ): boolean { const total = parseResourceNumber(selectedMemory); const unit = parseResourceUnit(selectedMemory); const memoryInMi = unit === "Gi" ? total * 1024 : total; const shmInMi = shmSizeUnit === "Gi" ? shmSizeValue * 1024 : shmSizeValue; return shmInMi > memoryInMi; } function memoryToGb(selectedMemory: string): number { const memoryValue = parseResourceNumber(selectedMemory); const memoryUnit = parseResourceUnit(selectedMemory); if (memoryUnit === "Gi") { return memoryValue; } if (memoryUnit === "Mi") { return Math.max(1, Math.floor(memoryValue / 1024)); } return 1; } function buildJvmMemorySteps(maxGb: number, start: number): number[] { const steps: number[] = []; let value = start; while (value <= maxGb) { steps.push(value); value *= 2; } return steps; } function buildJvmMemoryMarks(steps: number[]): Record { return steps.reduce>((marks, step) => { marks[step] = `${step}G`; return marks; }, {}); } export function getJvmMemorySliderConfig(selectedMemory: string): JvmMemorySliderConfig { const cuMemoryInGb = memoryToGb(selectedMemory); if (cuMemoryInGb <= 3) { const defaultValue = cuMemoryInGb === 1 ? 1 : 2; const steps = buildJvmMemorySteps(cuMemoryInGb, 1); return { jvmMemoryMax: cuMemoryInGb, showJvmMemorySlider: false, jvmMemorySteps: steps, jvmMemoryMarks: buildJvmMemoryMarks(steps), jvmMemorySliderValue: defaultValue, selectedJvmMemorySize: `${defaultValue}G`, }; } const steps = buildJvmMemorySteps(cuMemoryInGb, 2); return { jvmMemoryMax: cuMemoryInGb, showJvmMemorySlider: true, jvmMemorySteps: steps, jvmMemoryMarks: buildJvmMemoryMarks(steps), jvmMemorySliderValue: 2, selectedJvmMemorySize: "2G", }; } interface JvmMemorySliderConfig { jvmMemoryMax: number; showJvmMemorySlider: boolean; jvmMemorySteps: number[]; jvmMemoryMarks: Record; jvmMemorySliderValue: number; selectedJvmMemorySize: string; } export const unitTypeMessageTemplate = { local: { createTitle: "Connect to a Local Computing Unit", terminateTitle: "Disconnect from Local Computing Unit", terminateWarning: "", // no red warning createSuccess: "Successfully connected to the local computing unit", createFailure: "Failed to connect to the local computing unit", terminateSuccess: "Disconnected from the local computing unit", terminateFailure: "Failed to disconnect from the local computing unit", terminateTooltip: "Disconnect from this computing unit", }, kubernetes: { createTitle: "Create Computing Unit", terminateTitle: "Terminate Computing Unit", terminateWarning: "

    Warning: All execution results in this computing unit will be lost.

    ", createSuccess: "Successfully created the Kubernetes computing unit", createFailure: "Failed to create the Kubernetes computing unit", terminateSuccess: "Terminated Kubernetes computing unit", terminateFailure: "Failed to terminate Kubernetes computing unit", terminateTooltip: "Terminate this computing unit", }, } as const; ================================================ FILE: frontend/src/app/common/util/context.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { merge, Subject } from "rxjs"; export function ContextManager(defaultContext: Context) { abstract class ContextManager { private static contextStack: Context[] = [defaultContext]; public static getContext() { return this.contextStack[this.contextStack.length - 1]; } public static prevContext() { if (this.contextStack.length < 2) { throw new Error("No previous context to get (you are in the default context already)"); } return this.contextStack[this.contextStack.length - 2]; } public static withContext(context: Context, callable: () => T): T { try { this.enter(context); return callable(); } finally { this.exit(); } } protected static enter(context: Context) { this.contextStack.push(context); } protected static exit() { this.contextStack.pop(); } } return ContextManager; } export function ObservableContextManager(defaultContext: Context) { abstract class ObservableContextManager extends ContextManager(defaultContext) { private static enterStream = new Subject<[exiting: Context, entering: Context]>(); private static exitStream = new Subject<[exiting: Context, entering: Context]>(); private static changeContextStream = ObservableContextManager.createChangeContextStream(); public static getEnterStream() { return this.enterStream.asObservable(); } public static getExitStream() { return this.exitStream.asObservable(); } public static getChangeContextStream() { return this.changeContextStream; } private static createChangeContextStream() { return merge(this.getEnterStream(), this.getExitStream()); } protected static enter(context: Context): void { const oldContext = this.getContext(); const newContext = context; super.enter(context); this.enterStream.next([oldContext, newContext]); } protected static exit(): void { const oldContext = this.getContext(); super.exit(); const newContext = this.getContext(); this.exitStream.next([oldContext, newContext]); } } return ObservableContextManager; } ================================================ FILE: frontend/src/app/common/util/error.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Used to extract error from the backend-thrown error * @param err */ export function extractErrorMessage(err: unknown): string { if (err instanceof Error) { return err.message; } if (typeof err === "object" && err !== null && "error" in err) { const backendErr = (err as any).error; if (typeof backendErr === "string") { return backendErr; } if (typeof backendErr === "object" && "message" in backendErr) { return backendErr.message; } } return "An unknown error occurred."; } ================================================ FILE: frontend/src/app/common/util/format.util.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ const BYTES_PER_UNIT = 1024; /** * Format upload speed */ export const formatSpeed = (bytesPerSecond = 0) => { if (bytesPerSecond <= 0) return "0.0 MB/s"; const mbps = bytesPerSecond / (BYTES_PER_UNIT * BYTES_PER_UNIT); return `${mbps.toFixed(1)} MB/s`; }; /** * Format time duration */ export const formatTime = (seconds?: number): string => { if (!seconds || seconds <= 0) return "1s"; const s = Math.max(1, Math.round(seconds)); // Under 1 minute: show seconds only if (s < 60) { return `${s}s`; } // Under 1 hour: show minutes (and seconds if not zero) if (s < 3600) { const m = Math.floor(s / 60); const sec = s % 60; return sec === 0 ? `${m}m` : `${m}m${sec.toString().padStart(2, "0")}s`; } // 1 hour+: show hours (and minutes if not zero) const h = Math.floor(s / 3600); const min = Math.floor((s % 3600) / 60); return min === 0 ? `${h}h` : `${h}h${min}m`; }; ================================================ FILE: frontend/src/app/common/util/logical-operator-port-serde.ts ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Extracts the port index from a port ID string. * @param portId Port ID like "input-0", "output-1", etc. * @returns undefined if the portId is invalid; port number and the type of the port will be returned */ export function parseLogicalOperatorPortID( portId: string ): { portNumber: number; portType: "input" | "output" } | undefined { const match = portId.match(/^(input|output)-(\d+)$/); if (!match) { return undefined; } const portType = match[1] as "input" | "output"; const portNumber = parseInt(match[2]); return { portNumber, portType }; } ================================================ FILE: frontend/src/app/common/util/map.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Converts ES6 Map object to TS Record object. * This method is used to stringify Map objects. * @param map */ export function mapToRecord(map: Map): Record { const record: Record = {}; map.forEach((value, key) => (record[key] = value)); return record; } /** * Converts TS Record object to ES6 Map object. * This method is used to construct Map objects from JSON. * @param record */ export function recordToMap(record: Record): Map { const map = new Map(); for (const key of Object.keys(record)) { map.set(key, record[key]); } return map; } ================================================ FILE: frontend/src/app/common/util/panel-dock.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ function parseTranslate3d(translate3d: string): [number, number, number] { const regex = /translate3d\((-?\d+\.?\d*)px,\s*(-?\d+\.?\d*)px,\s*(-?\d+\.?\d*)px\)/g; const match = regex.exec(translate3d); if (match) { const x = parseFloat(match[1]); const y = parseFloat(match[2]); const z = parseFloat(match[3]); return [x, y, z]; } return [0, 0, 0]; } export function calculateTotalTranslate3d(translates: string): [number, number, number] { let totalXOffset = 0; let totalYOffset = 0; let totalZOffset = 0; const translate3dArray = translates.match(/translate3d\(.*?\)/g) || []; for (const translate of translate3dArray) { const [x, y, z] = parseTranslate3d(translate); totalXOffset += x; totalYOffset += y; totalZOffset += z; } return [totalXOffset, totalYOffset, totalZOffset]; } ================================================ FILE: frontend/src/app/common/util/port-identity-serde.ts ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { PortIdentity } from "../type/proto/org/apache/texera/amber/core/workflow"; /** * Serializes a PortIdentity object to a string in the format "{isInput}-{id}-{internal}" * @param portIdentity The PortIdentity object to serialize * @returns A string representation of the PortIdentity (e.g., "1-true", "2-false") * This is aligned with the backend serializer: core/workflow-core/src/main/scala/org/apache/amber/util/serde/PortIdentityKeySerializer.scala */ export function serializePortIdentity(portIdentity: PortIdentity): string { return `${portIdentity.id}_${portIdentity.internal}`; } ================================================ FILE: frontend/src/app/common/util/predicate.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * checks if the given parameter is undefined or not. * @param val * @returns {boolean} */ export function isDefined(val: T | undefined | null): val is T { return val !== undefined && val != null; } ================================================ FILE: frontend/src/app/common/util/set.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ export function intersection(setA: ReadonlySet, setB: ReadonlySet): Set { let _intersection = new Set(); for (let elem of setA) { if (setB.has(elem)) { _intersection.add(elem); } } return _intersection; } ================================================ FILE: frontend/src/app/common/util/size-formatter.util.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { formatSize } from "./size-formatter.util"; describe("formatSize", () => { it("should correctly format a valid file size", () => { const result = formatSize(1536); expect(result).toBe("1.50 KB"); }); it('should return "0 Bytes" for undefined or non-positive input', () => { expect(formatSize(undefined)).toBe("0 B"); expect(formatSize(-100)).toBe("0 B"); expect(formatSize(0)).toBe("0 B"); }); it("should correctly format large file sizes", () => { const largeSizeInBytes = 1073741824; const result = formatSize(largeSizeInBytes); expect(result).toBe("1.00 GB"); }); it("should handle decimal places correctly", () => { const result = formatSize(1500); expect(result).toBe("1.46 KB"); }); it("should successfully format a typical file size", () => { const typicalSizeInBytes = 2097152; const result = formatSize(typicalSizeInBytes); expect(result).toBe("2.00 MB"); }); it("should handle extremely large file sizes without failure", () => { const extremelyLargeSizeInBytes = Number.MAX_SAFE_INTEGER; const result = formatSize(extremelyLargeSizeInBytes); expect(result).not.toBe("0 B"); expect(result).toContain("TB"); }); }); ================================================ FILE: frontend/src/app/common/util/size-formatter.util.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ const BYTES_PER_UNIT = 1024; const SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"]; export const formatSize = (bytes?: number): string => { if (bytes === undefined || bytes <= 0) return "0 B"; const unitIndex = Math.min(Math.floor(Math.log(bytes) / Math.log(BYTES_PER_UNIT)), SIZE_UNITS.length - 1); const size = bytes / Math.pow(BYTES_PER_UNIT, unitIndex); return `${size.toFixed(2)} ${SIZE_UNITS[unitIndex]}`; }; ================================================ FILE: frontend/src/app/common/util/storage.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * storage.ts maintains a set of useful static storage-related functions. * They are used to provide easy access to localStorage and sessionStorage. */ /** * Saves an object into the localStorage, in its the JSON format. * @param key - the identifier of the object * @param object - any type, will be JSON.stringify-ed into a string */ export function localSetObject(key: string, object: T): void { localStorage.setItem(key, JSON.stringify(object)); } /** * Retrieves an object from the localStorage, converted from the JSON format into its original type (provided). * @param key - the identifier of the object * @returns T - the converted object (in type) from the JSON string, or null if the key is not found. */ export function localGetObject(key: string): T | undefined { const data: string | null = localStorage.getItem(key); if (!data) { return undefined; } return jsonCast(data); } /** * removes the object from the localStorage * @param {string} key - the identifier of the object */ export function localRemoveObject(key: string): void { localStorage.removeItem(key); } export function jsonCast(data: string): T { return JSON.parse(data); } /** * Saves an object into the sessionStorage, in its the JSON format. * @param key - the identifier of the object * @param object - any type, will be JSON.stringify-ed into a string */ export function sessionSetObject(key: string, object: T): void { sessionStorage.setItem(key, JSON.stringify(object)); } /** * Retrieves an object from the sessionStorage, converted from the JSON format into its original type (provided). * @param key - the identifier of the object * @returns T - the converted object (in type) from the JSON string, or null if the key is not found. */ export function sessionGetObject(key: string): T | null { const data: string | null = sessionStorage.getItem(key); if (!data) { return null; } return jsonCast(data); } export function sessionRemoveObject(key: string): void { sessionStorage.removeItem(key); } ================================================ FILE: frontend/src/app/common/util/stub.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ export type PublicInterfaceOf = { [Member in keyof Class]: Class[Member]; }; ================================================ FILE: frontend/src/app/common/util/switch.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ export function exhaustiveGuard(_value: never): never { throw new Error(`ERROR! Reached forbidden guard function with unexpected value: ${JSON.stringify(_value)}`); } ================================================ FILE: frontend/src/app/common/util/url.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * url.ts maintains common functions related to URL. */ /** * Generate a websocket URL based on a server endpoint. */ export function getWebsocketUrl(endpoint: string, port: string): string { const baseURI = document.baseURI; const hostname = new URL(baseURI).hostname; let webSocketUrl; if (port !== "") { webSocketUrl = new URL(endpoint, `http://${hostname}:${port}`); } else { webSocketUrl = new URL(endpoint, document.baseURI); } // replace protocol, so that http -> ws, https -> wss webSocketUrl.protocol = webSocketUrl.protocol.replace("http", "ws"); return webSocketUrl.toString(); } ================================================ FILE: frontend/src/app/common/util/workflow-check.ts ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { Workflow } from "../../common/type/workflow"; /** * Checks if the given workflow is "broken". * A workflow is considered broken if any of its links reference an operator ID * that does not exist in the list of operators within the workflow. * * @param workflow - The workflow to validate, containing operators and links. * @returns 'true' if the workflow is broken, 'false' otherwise. */ export function checkIfWorkflowBroken(workflow: Workflow): boolean { const validOperatorIDs = new Set(workflow.content.operators.map(o => o.operatorID)); return workflow.content.links.some( link => !validOperatorIDs.has(link.source.operatorID) || !validOperatorIDs.has(link.target.operatorID) ); } ================================================ FILE: frontend/src/app/common/util/workflow-compilation-utils.ts ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { CompilationState, CompilationStateInfo, PortSchema } from "../../workspace/types/workflow-compiling.interface"; import { WorkflowFatalError } from "../../workspace/types/workflow-websocket.interface"; import { isEqual } from "lodash"; /** * Checks if all PortSchemas in an array are equal to each other. * Requires either all schemas to be undefined, or all to be defined and equal. * * @param schemas Array of PortSchemas to compare (can contain undefined values) * @returns true if all schemas are equal, false otherwise */ export function areAllPortSchemasEqual(schemas: (PortSchema | undefined)[]): boolean { if (schemas.length <= 1) { return true; } return schemas.every(schema => isEqual(schemas[0], schema)); } /** * Creates a new CompilationStateInfo with a failed state and adds an error for the specified operator. * * @param currentState The current compilation state info * @param operatorId The ID of the operator that caused the error * @param errorMessage The error message to display * @param errorDetails Additional details about the error * @returns A new CompilationStateInfo with the error added */ export function addCompilationError( currentState: CompilationStateInfo, operatorId: string, errorMessage: string, errorDetails?: string ): CompilationStateInfo { const existingOutputSchemas = currentState.state === CompilationState.Uninitialized ? {} : currentState.operatorOutputPortSchemaMap; const existingErrors = currentState.state === CompilationState.Failed ? currentState.operatorErrors : {}; const newError: WorkflowFatalError = { message: errorMessage, details: errorDetails || "", operatorId: operatorId, workerId: "", type: { name: "COMPILATION_ERROR" }, timestamp: { seconds: Math.floor(Date.now() / 1000), nanos: (Date.now() % 1000) * 1000000, }, }; return { state: CompilationState.Failed, operatorOutputPortSchemaMap: existingOutputSchemas, operatorErrors: { ...existingErrors, [operatorId]: newError, }, }; } ================================================ FILE: frontend/src/app/dashboard/component/admin/execution/admin-execution.component.html ================================================

    Executions

    Workflow (ID) Execution Name (ID) Initiator Status Time Used (hh:mm:ss) End Time Action
    {{ maxStringLength(execution.workflowName, 16) }} ({{ execution.workflowId }})
    {{ maxStringLength(execution.executionName, 20) }} ({{ execution.executionId }}) {{ execution.userName }} {{ execution.executionStatus }}
    {{ convertSecondsToTime(execution.executionTime) }}
    Not Available
    {{ convertTimeToTimestamp(execution.executionStatus, execution.endTime) }}
    Not Available
    ================================================ FILE: frontend/src/app/dashboard/component/admin/execution/admin-execution.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .loading-container { display: flex; justify-content: center; flex-direction: column; height: 300px; } .execution-table { display: block; grid-row-start: 3; grid-row-end: 4; } ================================================ FILE: frontend/src/app/dashboard/component/admin/execution/admin-execution.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, inject, TestBed } from "@angular/core/testing"; import { AdminExecutionComponent } from "./admin-execution.component"; import { AdminExecutionService } from "../../../service/admin/execution/admin-execution.service"; import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing"; import { NzDropDownModule } from "ng-zorro-antd/dropdown"; import { NzModalModule } from "ng-zorro-antd/modal"; import { commonTestProviders } from "../../../../common/testing/test-utils"; describe("AdminDashboardComponent", () => { let component: AdminExecutionComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ providers: [AdminExecutionService, ...commonTestProviders], imports: [AdminExecutionComponent, HttpClientTestingModule, NzDropDownModule, NzModalModule], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(AdminExecutionComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it("should create", inject([HttpTestingController], () => { expect(component).toBeTruthy(); })); }); ================================================ FILE: frontend/src/app/dashboard/component/admin/execution/admin-execution.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, OnDestroy, OnInit } from "@angular/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { AdminExecutionService } from "../../../service/admin/execution/admin-execution.service"; import { Execution } from "../../../../common/type/execution"; import { NzTableFilterFn, NzTableQueryParams, NzTableComponent, NzTheadComponent, NzTrDirective, NzTableCellDirective, NzThMeasureDirective, NzThAddOnComponent, NzTbodyComponent, } from "ng-zorro-antd/table"; import { NzModalService } from "ng-zorro-antd/modal"; import { WorkflowExecutionHistoryComponent } from "../../user/user-workflow/ngbd-modal-workflow-executions/workflow-execution-history.component"; import { WorkflowWebsocketService } from "../../../../workspace/service/workflow-websocket/workflow-websocket.service"; import { GuiConfigService } from "../../../../common/service/gui-config.service"; import { NzCardComponent } from "ng-zorro-antd/card"; import { NgFor, NgIf, NgStyle } from "@angular/common"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzSpinComponent } from "ng-zorro-antd/spin"; export const NO_SORT = "NO_SORTING"; @UntilDestroy() @Component({ templateUrl: "./admin-execution.component.html", styleUrls: ["./admin-execution.component.scss"], imports: [ NzCardComponent, NzTableComponent, NzTheadComponent, NzTrDirective, NzTableCellDirective, NzThMeasureDirective, NzThAddOnComponent, NzTbodyComponent, NgFor, NgIf, NgStyle, NzSpaceCompactItemDirective, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, NzTooltipDirective, NzIconDirective, NzSpinComponent, ], }) export class AdminExecutionComponent implements OnInit, OnDestroy { listOfExecutions: ReadonlyArray = []; isLoading: boolean = true; totalWorkflows: number = 0; pageSize: number = 5; // CurrentPageIndex is 0-indexed, but pageIndex in HTML is 1-indexed. currentPageIndex: number = 0; sortField: string = NO_SORT; sortDirection: string = NO_SORT; filter: string[] = []; // This interval function fetches the latest execution list. // The interval runs every 1 second (1000 milliseconds). timer = setInterval(() => this.ngOnInit(), 1000); // 1 second interval constructor( private adminExecutionService: AdminExecutionService, private modalService: NzModalService, private config: GuiConfigService ) {} ngOnInit() { this.adminExecutionService .getExecutionList(this.pageSize, this.currentPageIndex, this.sortField, this.sortDirection, this.filter) .pipe(untilDestroyed(this)) .subscribe(executionList => { this.listOfExecutions = [...executionList]; this.updateTimeStatus(); this.isLoading = false; }); this.adminExecutionService .getTotalWorkflows() .pipe(untilDestroyed(this)) .subscribe(total => (this.totalWorkflows = total)); } ngOnDestroy(): void { clearInterval(this.timer); } maxStringLength(input: string, length: number): string { if (input.length > length) { return input.substring(0, length) + " . . . "; } return input; } dataCheck(oldExecution: Execution, newExecution: Execution): boolean { // Get the current time in seconds. const currentTime = Date.now() / 1000; // Check if the execution needed to be updated if (oldExecution.executionStatus === "JUST COMPLETED" && currentTime - newExecution.endTime / 1000 <= 5) { return false; } else if (oldExecution.executionStatus != newExecution.executionStatus) { return true; } else if (oldExecution.executionName != newExecution.executionName) { return true; } else if (oldExecution.workflowName != newExecution.workflowName) { return true; } return false; } updateTimeStatus() { this.specifyCompletedStatus(); this.updateTimeDifferences(); } /** * Update the execution status of workflows in the list based on their completion time. * If a workflow was completed within the last 5 seconds, it is updated to "JUST COMPLETED." * If a workflow was completed more than 5 seconds, it is updated back to "COMPLETED." */ specifyCompletedStatus() { const currentTime = Date.now() / 1000; this.listOfExecutions.forEach((workflow, index) => { if (workflow.executionStatus === "COMPLETED" && currentTime - workflow.endTime / 1000 <= 5) { this.listOfExecutions[index].executionStatus = "JUST COMPLETED"; } else if (workflow.executionStatus === "JUST COMPLETED" && currentTime - workflow.endTime / 1000 > 5) { this.listOfExecutions[index].executionStatus = "COMPLETED"; } }); } /** * if status are "RUNNING", "READY", or "PAUSED", the time used would constantly increase. * if status are not list above, there would be a final time used. */ calculateTime(LastUpdateTime: number, StartTime: number, executionStatus: string, name: string): number { if (executionStatus === "RUNNING" || executionStatus === "READY" || executionStatus === "PAUSED") { const currentTime = Date.now() / 1000; // Convert to seconds const currentTimeInteger = Math.floor(currentTime); return currentTimeInteger - StartTime / 1000; } else { return (LastUpdateTime - StartTime) / 1000; } } /** * Update the execution time differences for each execution in the list of executions. * This function calculates and assigns the elapsed time for each execution. */ updateTimeDifferences() { this.listOfExecutions.forEach(workflow => { workflow.executionTime = this.calculateTime( workflow.endTime, workflow.startTime, workflow.executionStatus, workflow.workflowName ); }); } /** * Determine and return the color associated with a given execution status. */ getStatusColor(status: string): string { switch (status) { case "READY": return "lightgreen"; case "RUNNING": return "orange"; case "PAUSED": return "purple"; case "FAILED": return "gray"; case "JUST COMPLETED": return "blue"; case "COMPLETED": return "green"; case "KILLED": return "red"; default: return "black"; } } /** * Convert a numeric timestamp to a human-readable time string. */ convertTimeToTimestamp(executionStatus: string, timeValue: number): string { const date = new Date(timeValue); return date.toLocaleString("en-US", { timeZoneName: "short" }); } /** * Convert a total number of seconds into a formatted time string (HH:MM:SS). */ convertSecondsToTime(seconds: number): string { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const remainingSeconds = seconds % 60; return `${this.padZero(hours)}:${this.padZero(minutes)}:${this.padZero(remainingSeconds)}`; } /** * Pad a number with a leading zero if it is a single digit. */ padZero(value: number): string { return value.toString().padStart(2, "0"); } filterByStatus: NzTableFilterFn = function (list: string[], execution: Execution) { return list.some(function (executionStatus) { return execution.executionStatus.indexOf(executionStatus) !== -1; }); }; clickToViewHistory(wid: number, name: string) { this.modalService.create({ nzContent: WorkflowExecutionHistoryComponent, nzData: { wid: wid }, nzTitle: "Execution results of Workflow: " + name, nzFooter: null, nzWidth: "80%", nzCentered: true, }); } /** Due to the Async nature when setting up the websocket, the socket would be closed before the connection is established. Therefore, commenting the code to ensure the connections is established and request has been sent. */ killExecution(wid: number) { let socket = new WorkflowWebsocketService(this.config); socket.openWebsocket(wid); socket.send("WorkflowKillRequest", {}); // socket.closeWebsocket(); } /** Due to the Async nature when setting up the websocket, the socket would be closed before the connection is established. Therefore, commenting the code to ensure the connections is established and request has been sent. */ pauseExecution(wid: number) { let socket = new WorkflowWebsocketService(this.config); socket.openWebsocket(wid); socket.send("WorkflowPauseRequest", {}); // socket.closeWebsocket(); } /** Due to the Async nature when setting up the websocket, the socket would be closed before the connection is established. Therefore, commenting the code to ensure the connections is established and request has been sent. */ resumeExecution(wid: number) { let socket = new WorkflowWebsocketService(this.config); socket.openWebsocket(wid); socket.send("WorkflowResumeRequest", {}); // socket.closeWebsocket(); } /** * Reorder the executions based on the selected field and order. * The sorting function is implemented in the backend. * @param sortField * @param sortOrder */ onSortChange(sortField: string, sortOrder: string | null): void { if (sortField == this.sortField && sortOrder == null) { this.sortField = NO_SORT; this.sortDirection = NO_SORT; this.ngOnInit(); } else if (sortOrder != null) { this.sortField = sortField; this.sortDirection = sortOrder === "descend" ? "desc" : "asc"; this.ngOnInit(); } } /** * Function that displays executions in respond to page size and page index changes. * @param params */ onQueryParamsChange(params: NzTableQueryParams): void { const { pageSize, pageIndex, sort, filter } = params; if (pageSize != this.pageSize) { this.pageSize = pageSize; // If the user is at the last page, and increment the pageSize, move user to new last page index if necessary. if (Math.ceil(this.totalWorkflows / pageSize) < this.currentPageIndex + 1) { this.currentPageIndex = Math.ceil(this.totalWorkflows / pageSize) - 1; } this.ngOnInit(); } else if (pageIndex - 1 != this.currentPageIndex) { this.currentPageIndex = pageIndex - 1; this.ngOnInit(); } } /** * Filter the executions based on the status user checked. * The filtering function in implemented in the backend. * @param filter */ onFilterChange(filter: any[]): void { this.filter = filter.map(item => String(item)); this.ngOnInit(); } } ================================================ FILE: frontend/src/app/dashboard/component/admin/settings/admin-settings.component.html ================================================

    General Settings

    Logo: Logo Preview
    The logo will appear in the expanded sidebar. Recommended: use a square image.
    Mini Logo: Mini Logo Preview
    The mini logo will appear in the collapsed sidebar as a small icon. Recommended: use a square image.
    Favicon: Favicon Preview
    This image will be used as your browser tab icon. Recommended: use a square image (16×16 or 32×32 pixels).
    Hub
    Your Work
    About
    Dataset
    Concurrent Files:
    Number of files that can be uploaded simultaneously. (Range: 1 - 1000)
    File Size:
    Maximum size allowed for individual file uploads. Range: 1 MiB - {{ MAX_FILE_SIZE_MiB | number }} MiB (5 TiB).
    Concurrent Parts:
    Number of parts uploaded in parallel. Range: 1 - {{ MAX_TOTAL_PARTS | number }}. Current configuration will use approximately {{ partsAtMax | number }} parts for maximum file size.
    Part Size:
    Size of each chunk during multipart upload. Range: {{ MIN_PART_SIZE_MiB }} MiB - {{ MAX_PART_SIZE_MiB | number }} MiB (5 GiB). Minimum required for current configuration: {{ requiredMinPartSizeMiB | number }} MiB.
    In-flight data per file: ~{{ maxConcurrentChunks * chunkSizeMiB | number }} MiB
    Max Columns:
    Maximum number of columns the CSV parser will accept per row. The Univocity parser default is 512. Increase this value if your CSV files have more than 512 columns. (Range: {{ MIN_CSV_MAX_COLUMNS }} - {{ MAX_CSV_MAX_COLUMNS | number }})
    ================================================ FILE: frontend/src/app/dashboard/component/admin/settings/admin-settings.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .section-title { background-color: lightcoral; } .section-title .page-title { color: white; } .upload-row { display: grid; grid-template-columns: 80px 220px 32px; column-gap: 6px; margin-bottom: 4px; } .preview-img { margin-left: 8px; height: 32px; } .help-text { margin-left: 88px; font-size: 12px; color: #888; margin-bottom: 20px; } .button-row { margin-top: 20px; display: flex; gap: 14px; } .tabs-group { width: 200px; } .tab-row { display: flex; gap: 0.5rem; margin-bottom: 8px; } .tab-switch { margin-left: auto; } .arrow { width: 12px; height: 12px; transform-origin: center center; } details[open] .arrow { transform: rotate(90deg); } .tab-row:not(:has(.arrow)) { margin-left: 22px; } .submenu-item { display: flex; margin-left: 2rem; margin-bottom: 8px; } .settings-row { display: grid; grid-template-columns: 130px 320px; column-gap: 6px; margin-bottom: 4px; } .help-text-number { margin-left: 140px; font-size: 12px; color: #888; margin-bottom: 20px; } .icon-info { margin-left: 6px; } ================================================ FILE: frontend/src/app/dashboard/component/admin/settings/admin-settings.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { AdminSettingsComponent } from "./admin-settings.component"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { NzCardModule } from "ng-zorro-antd/card"; describe("AdminSettingsComponent", () => { let component: AdminSettingsComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [AdminSettingsComponent, HttpClientTestingModule, NzCardModule], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(AdminSettingsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/dashboard/component/admin/settings/admin-settings.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, OnInit } from "@angular/core"; import { AdminSettingsService } from "../../../service/admin/settings/admin-settings.service"; import { NzMessageService } from "ng-zorro-antd/message"; import { NotificationService } from "../../../../common/service/notification/notification.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { SidebarTabs } from "../../../../common/type/gui-config"; import { forkJoin } from "rxjs"; import { NzCardComponent } from "ng-zorro-antd/card"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NgIf, DecimalPipe } from "@angular/common"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzSwitchComponent } from "ng-zorro-antd/switch"; import { FormsModule } from "@angular/forms"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzInputNumberComponent } from "ng-zorro-antd/input-number"; @UntilDestroy() @Component({ selector: "texera-settings", templateUrl: "./admin-settings.component.html", styleUrls: ["./admin-settings.component.scss"], imports: [ NzCardComponent, NzSpaceCompactItemDirective, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, NgIf, NzIconDirective, NzSwitchComponent, FormsModule, NzTooltipDirective, NzInputNumberComponent, DecimalPipe, ], }) export class AdminSettingsComponent implements OnInit { logoData: string | null = null; miniLogoData: string | null = null; faviconData: string | null = null; sidebarTabs: SidebarTabs = { hub_enabled: false, home_enabled: false, workflow_enabled: false, dataset_enabled: false, your_work_enabled: false, projects_enabled: false, workflows_enabled: false, compute_enabled: false, datasets_enabled: false, quota_enabled: false, forum_enabled: false, about_enabled: false, }; maxConcurrentFiles: number = 3; maxFileSizeMiB: number = 20; maxConcurrentChunks: number = 10; chunkSizeMiB: number = 50; csvMaxColumns: number = 512; // S3 Multipart Upload Constraints readonly MIN_PART_SIZE_MiB = 5; // 5 MiB minimum for parts (except last part) readonly MAX_PART_SIZE_MiB = 5120; // 5 GiB maximum per part (5 * 1024 MiB) readonly MAX_FILE_SIZE_MiB = 5242880; // 5 TiB maximum object size (5 * 1024 * 1024 MiB) readonly MAX_TOTAL_PARTS = 10000; // S3 maximum parts per upload readonly MIN_CSV_MAX_COLUMNS = 1; readonly MAX_CSV_MAX_COLUMNS = 100000; private readonly RELOAD_DELAY = 1000; constructor( private adminSettingsService: AdminSettingsService, private message: NzMessageService, private notificationService: NotificationService ) {} ngOnInit(): void { this.loadBranding(); this.loadTabs(); this.loadDatasetSettings(); this.loadCsvSettings(); } private loadBranding(): void { this.adminSettingsService .getSetting("logo") .pipe(untilDestroyed(this)) .subscribe(value => (this.logoData = value || null)); this.adminSettingsService .getSetting("mini_logo") .pipe(untilDestroyed(this)) .subscribe(value => (this.miniLogoData = value || null)); this.adminSettingsService .getSetting("favicon") .pipe(untilDestroyed(this)) .subscribe(value => (this.faviconData = value || null)); } private loadTabs(): void { (Object.keys(this.sidebarTabs) as (keyof SidebarTabs)[]).forEach(tab => { this.adminSettingsService .getSetting(tab) .pipe(untilDestroyed(this)) .subscribe(value => (this.sidebarTabs[tab] = value === "true")); }); } private loadDatasetSettings(): void { this.adminSettingsService .getSetting("max_number_of_concurrent_uploading_file") .pipe(untilDestroyed(this)) .subscribe(value => (this.maxConcurrentFiles = parseInt(value))); this.adminSettingsService .getSetting("single_file_upload_max_size_mib") .pipe(untilDestroyed(this)) .subscribe(value => (this.maxFileSizeMiB = parseInt(value))); this.adminSettingsService .getSetting("max_number_of_concurrent_uploading_file_chunks") .pipe(untilDestroyed(this)) .subscribe(value => (this.maxConcurrentChunks = parseInt(value))); this.adminSettingsService .getSetting("multipart_upload_chunk_size_mib") .pipe(untilDestroyed(this)) .subscribe(value => (this.chunkSizeMiB = parseInt(value))); } onFileChange(type: "logo" | "mini_logo" | "favicon", event: Event): void { const file = (event.target as HTMLInputElement).files?.[0]; if (file && file.type.startsWith("image/")) { const reader = new FileReader(); reader.onload = e => { const result = typeof e.target?.result === "string" ? e.target.result : null; if (type === "logo") { this.logoData = result; } else if (type === "mini_logo") { this.miniLogoData = result; } else { this.faviconData = result; } }; reader.readAsDataURL(file); } else { this.message.error("Please upload a valid image file."); } } saveLogos(): void { const saveRequests = []; if (this.logoData) { saveRequests.push(this.adminSettingsService.updateSetting("logo", this.logoData)); } if (this.miniLogoData) { saveRequests.push(this.adminSettingsService.updateSetting("mini_logo", this.miniLogoData)); } if (this.faviconData) { saveRequests.push(this.adminSettingsService.updateSetting("favicon", this.faviconData)); } if (saveRequests.length > 0) { forkJoin(saveRequests) .pipe(untilDestroyed(this)) .subscribe({ next: () => { this.message.success("Branding saved successfully."); setTimeout(() => window.location.reload(), this.RELOAD_DELAY); }, error: () => this.message.error("Failed to save branding."), }); } } resetBranding(): void { ["logo", "mini_logo", "favicon"].forEach(setting => this.adminSettingsService.resetSetting(setting).pipe(untilDestroyed(this)).subscribe({}) ); this.message.info("Resetting branding..."); setTimeout(() => window.location.reload(), this.RELOAD_DELAY); } saveTabs(): void { const saveRequests = (Object.keys(this.sidebarTabs) as (keyof SidebarTabs)[]).map(tab => this.adminSettingsService.updateSetting(tab, this.sidebarTabs[tab].toString()) ); forkJoin(saveRequests) .pipe(untilDestroyed(this)) .subscribe({ next: () => { this.message.success("Tabs saved successfully."); setTimeout(() => window.location.reload(), this.RELOAD_DELAY); }, error: () => this.message.error("Failed to save tabs."), }); } resetTabs(): void { Object.keys(this.sidebarTabs).forEach(tab => { this.adminSettingsService.resetSetting(tab).pipe(untilDestroyed(this)).subscribe({}); }); this.message.info("Resetting tabs..."); setTimeout(() => window.location.reload(), this.RELOAD_DELAY); } // Computed properties get partsAtMax(): number { if (!this.maxFileSizeMiB || !this.chunkSizeMiB) return 0; return Math.ceil(this.maxFileSizeMiB / this.chunkSizeMiB); } get requiredMinPartSizeMiB(): number { if (!this.maxFileSizeMiB) return this.MIN_PART_SIZE_MiB; const byPartsLimit = Math.ceil(this.maxFileSizeMiB / this.MAX_TOTAL_PARTS); return Math.max(this.MIN_PART_SIZE_MiB, byPartsLimit); } saveDatasetSettings(): void { if ( this.maxFileSizeMiB < 1 || this.maxConcurrentFiles < 1 || this.maxConcurrentChunks < 1 || this.chunkSizeMiB < 1 ) { this.message.error("Please enter valid integer values."); return; } if (this.partsAtMax > this.MAX_TOTAL_PARTS) { this.message.error( `This setting would create ${this.partsAtMax.toLocaleString()} parts (exceeds 10,000 limit). ` + `Increase "Part Size" to at least ${this.requiredMinPartSizeMiB} MiB or reduce "File Size".` ); return; } const saveRequests = [ this.adminSettingsService.updateSetting( "max_number_of_concurrent_uploading_file", this.maxConcurrentFiles.toString() ), this.adminSettingsService.updateSetting("single_file_upload_max_size_mib", this.maxFileSizeMiB.toString()), this.adminSettingsService.updateSetting( "max_number_of_concurrent_uploading_file_chunks", this.maxConcurrentChunks.toString() ), this.adminSettingsService.updateSetting("multipart_upload_chunk_size_mib", this.chunkSizeMiB.toString()), ]; forkJoin(saveRequests) .pipe(untilDestroyed(this)) .subscribe({ next: () => this.message.success("Dataset upload settings saved successfully."), error: () => this.message.error("Failed to save dataset settings."), }); } resetDatasetSettings(): void { [ "max_number_of_concurrent_uploading_file", "single_file_upload_max_size_mib", "max_number_of_concurrent_uploading_file_chunks", "multipart_upload_chunk_size_mib", ].forEach(setting => this.adminSettingsService.resetSetting(setting).pipe(untilDestroyed(this)).subscribe({})); this.message.info("Resetting dataset settings..."); setTimeout(() => window.location.reload(), this.RELOAD_DELAY); } private loadCsvSettings(): void { this.adminSettingsService .getSetting("csv_parser_max_columns") .pipe(untilDestroyed(this)) .subscribe(value => (this.csvMaxColumns = parseInt(value) || 512)); } saveCsvSettings(): void { const saveRequests = [ this.adminSettingsService.updateSetting("csv_parser_max_columns", this.csvMaxColumns.toString()), ]; forkJoin(saveRequests) .pipe(untilDestroyed(this)) .subscribe({ next: () => this.notificationService.success("Result panel settings saved."), error: () => this.notificationService.error("Could not save result panel settings."), }); } resetCsvSettings(): void { this.adminSettingsService.resetSetting("csv_parser_max_columns").pipe(untilDestroyed(this)).subscribe({}); this.notificationService.info("Resetting result panel settings..."); setTimeout(() => window.location.reload(), this.RELOAD_DELAY); } } ================================================ FILE: frontend/src/app/dashboard/component/admin/user/admin-user.component.html ================================================

    Users

    Avatar ID Name Email Affiliation Joining Reason Comment User Role Quota Account Creation Time
    {{user.uid}}
    {{user.name}}
    {{user.email}}
    {{ user.affiliation }} {{ user.joiningReason }}
    {{ user.comment }}
    INACTIVE REGULAR ADMIN RESTRICTED {{selected.nzValue}} {{ ac | date:'MM/dd/y, h:mm a' }}
    ================================================ FILE: frontend/src/app/dashboard/component/admin/user/admin-user.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .extra-width { width: 110px; } .role { border-radius: 50px; color: white; display: block; width: 86px; text-align: center; } .c0 { background-color: lightskyblue; } .c1 { background-color: lightseagreen; } .c2 { background-color: lightcoral; } .c3 { background-color: lightslategray; } .search-box { padding: 8px; } .search-box input { width: 188px; margin-bottom: 8px; display: block; } .search-box button { width: 90px; } .search-button { margin-right: 8px; } .container { cursor: pointer; min-height: 1.5em; } .limited-comment { max-width: 250px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; word-break: break-word; white-space: normal; } :host ::ng-deep .user-avatar { display: inline-block; border: 4.5px solid #ccc; border-radius: 50%; padding: 2px; transition: border-color 0.2s ease; } :host ::ng-deep .user-avatar.active { border-color: #4caf50; } ================================================ FILE: frontend/src/app/dashboard/component/admin/user/admin-user.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { NO_ERRORS_SCHEMA } from "@angular/core"; import { ComponentFixture, inject, TestBed } from "@angular/core/testing"; import { AdminUserComponent } from "./admin-user.component"; import { UserService } from "../../../../common/service/user/user.service"; import { StubUserService } from "../../../../common/service/user/stub-user.service"; import { AdminUserService } from "../../../service/admin/user/admin-user.service"; import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing"; import { FormsModule } from "@angular/forms"; import { NzDropDownModule } from "ng-zorro-antd/dropdown"; import { NzModalModule } from "ng-zorro-antd/modal"; import { commonTestProviders } from "../../../../common/testing/test-utils"; describe("AdminUserComponent", () => { let component: AdminUserComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ providers: [{ provide: UserService, useClass: StubUserService }, AdminUserService, ...commonTestProviders], imports: [AdminUserComponent, FormsModule, HttpClientTestingModule, NzDropDownModule, NzModalModule], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(AdminUserComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it("should create", inject([HttpTestingController], () => { expect(component).toBeTruthy(); })); }); ================================================ FILE: frontend/src/app/dashboard/component/admin/user/admin-user.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, ElementRef, OnInit, ViewChild } from "@angular/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { NzTableFilterFn, NzTableSortFn, NzTableComponent, NzTheadComponent, NzTrDirective, NzTableCellDirective, NzThMeasureDirective, NzThAddOnComponent, NzFilterTriggerComponent, NzTbodyComponent, } from "ng-zorro-antd/table"; import { NzModalService } from "ng-zorro-antd/modal"; import { NzMessageService } from "ng-zorro-antd/message"; import { AdminUserService } from "../../../service/admin/user/admin-user.service"; import { MilliSecond, Role, User } from "../../../../common/type/user"; import { UserService } from "../../../../common/service/user/user.service"; import { UserQuotaComponent } from "../../user/user-quota/user-quota.component"; import { GuiConfigService } from "../../../../common/service/gui-config.service"; import { replaceOneImmutable } from "../../../../common/util/array-utils"; import { NzCardComponent } from "ng-zorro-antd/card"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzDropdownMenuComponent } from "ng-zorro-antd/dropdown"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzInputDirective } from "ng-zorro-antd/input"; import { FormsModule } from "@angular/forms"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { NgFor, NgClass, NgIf, DatePipe } from "@angular/common"; import { UserAvatarComponent } from "../../user/user-avatar/user-avatar.component"; import { NzSelectComponent, NzOptionComponent } from "ng-zorro-antd/select"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; @UntilDestroy() @Component({ templateUrl: "./admin-user.component.html", styleUrls: ["./admin-user.component.scss"], imports: [ NzCardComponent, NzTableComponent, NzTheadComponent, NzTrDirective, NzTableCellDirective, NzThMeasureDirective, NzThAddOnComponent, NzFilterTriggerComponent, ɵNzTransitionPatchDirective, NzIconDirective, NzDropdownMenuComponent, NzSpaceCompactItemDirective, NzInputDirective, FormsModule, NzButtonComponent, NzWaveDirective, NzTbodyComponent, NgFor, UserAvatarComponent, NgClass, NgIf, NzSelectComponent, NzOptionComponent, NzTooltipDirective, DatePipe, ], }) export class AdminUserComponent implements OnInit { userList: ReadonlyArray = []; editUid: number = 0; editAttribute: string = ""; editName: string = ""; editEmail: string = ""; editRole: Role = Role.REGULAR; editComment: string = ""; nameSearchValue: string = ""; emailSearchValue: string = ""; commentSearchValue: string = ""; nameSearchVisible = false; emailSearchVisible = false; commentSearchVisible = false; listOfDisplayUser = [...this.userList]; currentUid: number | undefined = 0; @ViewChild("nameInput") nameInputRef?: ElementRef; @ViewChild("emailInput") emailInputRef?: ElementRef; @ViewChild("commentTextarea") commentTextareaRef?: ElementRef; constructor( private adminUserService: AdminUserService, private userService: UserService, private modalService: NzModalService, private messageService: NzMessageService, private config: GuiConfigService ) { this.currentUid = this.userService.getCurrentUser()?.uid; } ngOnInit() { this.adminUserService .getUserList() .pipe(untilDestroyed(this)) .subscribe(userList => { this.userList = userList; this.reset(); }); } public updateRole(user: User, role: Role): void { this.startEdit(user, "role"); this.editRole = role; this.saveEdit(); } addUser(): void { this.adminUserService .addUser() .pipe(untilDestroyed(this)) .subscribe(() => this.ngOnInit()); } startEdit(user: User, attribute: string): void { this.editUid = user.uid; this.editAttribute = attribute; this.editName = user.name; this.editEmail = user.email; this.editRole = user.role; this.editComment = user.comment; setTimeout(() => { if (attribute === "name" && this.nameInputRef) { const input = this.nameInputRef.nativeElement; input.focus(); input.setSelectionRange(input.value.length, input.value.length); } else if (attribute === "email" && this.emailInputRef) { const input = this.emailInputRef.nativeElement; input.focus(); input.setSelectionRange(input.value.length, input.value.length); } else if (attribute === "comment" && this.commentTextareaRef) { const textarea = this.commentTextareaRef.nativeElement; textarea.focus(); textarea.setSelectionRange(textarea.value.length, textarea.value.length); } }, 0); } saveEdit(): void { const originalUser = this.userList.find(u => u.uid === this.editUid); if ( !originalUser || (originalUser.name === this.editName && originalUser.email === this.editEmail && originalUser.comment === this.editComment && originalUser.role === this.editRole) ) { this.stopEdit(); return; } const currentUid = this.editUid; // Edited User const updatedUser: User = { ...originalUser, name: this.editName, email: this.editEmail, comment: this.editComment, role: this.editRole, }; this.stopEdit(); this.adminUserService .updateUser(currentUid, this.editName, this.editEmail, this.editRole, this.editComment) .pipe(untilDestroyed(this)) .subscribe({ next: () => { // Update userList and listOfDisplayUser with updatedUser this.userList = [...replaceOneImmutable(this.userList, u => u.uid === currentUid, updatedUser)]; this.listOfDisplayUser = [ ...replaceOneImmutable(this.listOfDisplayUser, u => u.uid === currentUid, updatedUser), ]; }, error: (err: unknown) => { const errorMessage = (err as any).error?.message || (err as Error).message; this.messageService.error(errorMessage); }, }); } stopEdit(): void { this.editUid = 0; this.editAttribute = ""; } public sortByID: NzTableSortFn = (a: User, b: User) => b.uid - a.uid; public sortByName: NzTableSortFn = (a: User, b: User) => { const compare = (b.name || "").localeCompare(a.name || ""); return compare === 0 ? a.uid - b.uid : compare; }; public sortByEmail: NzTableSortFn = (a: User, b: User) => { const compare = (b.email || "").localeCompare(a.email || ""); return compare === 0 ? a.uid - b.uid : compare; }; public sortByComment: NzTableSortFn = (a: User, b: User) => { const compare = (b.comment || "").localeCompare(a.comment || ""); return compare === 0 ? a.uid - b.uid : compare; }; public sortByRole: NzTableSortFn = (a: User, b: User) => { const compare = b.role.localeCompare(a.role); return compare === 0 ? a.uid - b.uid : compare; }; public sortByAccountCreation: NzTableSortFn = (a: User, b: User) => { const compare = (a.accountCreation || 0) - (b.accountCreation || 0); return compare === 0 ? a.uid - b.uid : compare; }; public sortByAffiliation: NzTableSortFn = (a: User, b: User) => { const compare = (b.affiliation || "").localeCompare(a.affiliation || ""); return compare === 0 ? a.uid - b.uid : compare; }; public sortByJoiningReason: NzTableSortFn = (a: User, b: User) => { const compare = (b.joiningReason || "").localeCompare(a.joiningReason || ""); return compare === 0 ? a.uid - b.uid : compare; }; reset(): void { this.nameSearchValue = ""; this.emailSearchValue = ""; this.commentSearchValue = ""; this.nameSearchVisible = false; this.emailSearchVisible = false; this.commentSearchVisible = false; this.listOfDisplayUser = [...this.userList]; } searchByName(): void { this.nameSearchVisible = false; const q = (this.nameSearchValue ?? "").trim().toLowerCase(); this.listOfDisplayUser = this.userList.filter(u => (u.name ?? "").toLowerCase().includes(q)); } searchByEmail(): void { this.emailSearchVisible = false; this.listOfDisplayUser = this.userList.filter(user => (user.email || "").indexOf(this.emailSearchValue) !== -1); } searchByComment(): void { this.commentSearchVisible = false; this.listOfDisplayUser = this.userList.filter(user => (user.comment || "").indexOf(this.commentSearchValue) !== -1); } clickToViewQuota(uid: number) { this.modalService.create({ nzContent: UserQuotaComponent, nzData: { uid: uid }, nzFooter: null, nzWidth: "80%", nzBodyStyle: { padding: "0" }, nzCentered: true, }); } isUserActive(user: User): boolean { if (!user.lastLogin) { return false; } // Active window set to active-time-in-minutes from gui.conf const active_window = this.config.env.activeTimeInMinutes * 60 * 1000; const lastMs = user.lastLogin * 1000; return Date.now() - lastMs < active_window; } getAccountCreation(user: User): MilliSecond { if (!user.accountCreation) { return 0; } return user.accountCreation * 1000; } sortByActive: NzTableSortFn = (a: User, b: User) => { const aActive = this.isUserActive(a); const bActive = this.isUserActive(b); if (aActive === bActive) return a.uid - b.uid; return aActive ? -1 : 1; }; public filterByRole: NzTableFilterFn = (list: string[], user: User) => list.some(role => user.role.indexOf(role) !== -1); } ================================================ FILE: frontend/src/app/dashboard/component/button-style.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .button-group { display: flex; align-items: center; gap: 12px; texera-filters { margin-left: auto; } button.create-btn { display: inline-flex; align-items: center; gap: 2px; // space between and height: 40px; padding: 0 20px; font-size: 16px; font-weight: 500; background-color: #1a73e8; color: white; border: none; border-radius: 24px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); cursor: pointer; &:hover, &:focus { background-color: #1558b0; } } } ================================================ FILE: frontend/src/app/dashboard/component/dashboard.component.html ================================================
    • logo
      • Projects
      • Workflows
      • Datasets
      • Compute
      • Quota
      • Forum
      • Users
      • Executions
      • Gmail
      • Settings
    • About
    Git hash: {{ gitCommitHash }}
    ================================================ FILE: frontend/src/app/dashboard/component/dashboard.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ #nav { height: 70px; padding: 10px 15px; display: flex; align-items: center; background-color: white; justify-content: space-between; } texera-filters-instructions { margin-right: 10px; } texera-search-bar { flex: 1; max-width: calc(100% - 20px); padding-right: 10px; } texera-user-icon { padding: 0 24px; } button { border: none; } .layout { height: 100vh; } .ant-layout-content { background-color: white; } .page-content-layout { border-left: 1px solid #f0f0f0; } .ant-layout-sider-has-trigger { border-right: none; } .ant-menu-inline, .ant-menu-inline-collapsed { border: none; } .ant-menu { overflow-x: hidden; overflow-y: auto; height: calc(100vh - 70px - 48px); } .logo-section { height: 70px; display: flex; justify-content: center; align-items: center; } .collapsed-logo { width: 32px; height: 32px; object-fit: contain; } nz-content { position: relative; overflow-y: auto; } .page-container { display: flex; flex-direction: column; width: 100%; } #googleButton { float: right; padding: 5px 0; } #git-commit-id { position: absolute; left: 5px; bottom: 5px; font-size: 0.4em; color: gray; z-index: 5; pointer-events: none; } .hidden { display: none; } #nav { max-width: 100%; max-height: 100%; overflow: hidden; } ================================================ FILE: frontend/src/app/dashboard/component/dashboard.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { DashboardComponent } from "./dashboard.component"; import { ChangeDetectorRef, EventEmitter, NgZone } from "@angular/core"; import { By } from "@angular/platform-browser"; import { EMPTY, of } from "rxjs"; import { UserService } from "../../common/service/user/user.service"; import { FlarumService } from "../service/user/flarum/flarum.service"; import { SocialAuthService } from "@abacritt/angularx-social-login"; import { AdminSettingsService } from "../service/admin/settings/admin-settings.service"; import { ActivatedRoute, ActivatedRouteSnapshot, convertToParamMap, Data, NavigationEnd, Params, Router, UrlSegment, } from "@angular/router"; import type { Mock } from "vitest"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { commonTestProviders } from "../../common/testing/test-utils"; describe("DashboardComponent", () => { let component: DashboardComponent; let fixture: ComponentFixture; let userServiceMock: Partial; let routerMock: Partial; let flarumServiceMock: Partial; let cdrMock: Partial; let ngZoneMock: Partial; let socialAuthServiceMock: Partial; let adminSettingsServiceMock: Partial; let activatedRouteMock: Partial; const activatedRouteSnapshotMock: Partial = { queryParams: {}, url: [] as UrlSegment[], params: {} as Params, fragment: null, data: {} as Data, paramMap: convertToParamMap({}), queryParamMap: convertToParamMap({}), outlet: "", routeConfig: null, root: null as any, parent: null as any, firstChild: null as any, children: [], pathFromRoot: [], }; beforeEach(async () => { userServiceMock = { isAdmin: vi.fn().mockReturnValue(false), isLogin: vi.fn().mockReturnValue(false), userChanged: vi.fn().mockReturnValue(of(null)), }; routerMock = { events: of(new NavigationEnd(1, "/dashboard", "/dashboard")), url: "/dashboard", navigateByUrl: vi.fn(), }; flarumServiceMock = { auth: vi.fn().mockReturnValue(of({ token: "dummyToken" })), register: vi.fn().mockReturnValue(of(null)), }; cdrMock = { detectChanges: vi.fn(), }; ngZoneMock = { hasPendingMicrotasks: false, hasPendingMacrotasks: false, onUnstable: new EventEmitter(), onMicrotaskEmpty: new EventEmitter(), onStable: new EventEmitter(), onError: new EventEmitter(), run: (fn: () => any) => fn(), runGuarded: (fn: () => any) => fn(), runOutsideAngular: (fn: () => any) => fn(), runTask: (fn: () => any) => fn(), }; socialAuthServiceMock = { authState: EMPTY, // GoogleSigninButtonDirective subscribes to initState in its constructor; // EMPTY keeps the subscription open without triggering google.accounts.id.renderButton. initState: EMPTY, }; adminSettingsServiceMock = { getSetting: vi.fn().mockReturnValue(EMPTY), }; activatedRouteMock = { snapshot: activatedRouteSnapshotMock as ActivatedRouteSnapshot, }; await TestBed.configureTestingModule({ imports: [DashboardComponent, HttpClientTestingModule], providers: [ { provide: UserService, useValue: userServiceMock }, { provide: Router, useValue: routerMock }, { provide: FlarumService, useValue: flarumServiceMock }, { provide: ChangeDetectorRef, useValue: cdrMock }, { provide: NgZone, useValue: ngZoneMock }, { provide: SocialAuthService, useValue: socialAuthServiceMock }, { provide: AdminSettingsService, useValue: adminSettingsServiceMock }, { provide: ActivatedRoute, useValue: activatedRouteMock }, ...commonTestProviders, ], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(DashboardComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it("should create the component", () => { expect(component).toBeTruthy(); }); it("should render Google sign-in button when user is NOT logged in", () => { (userServiceMock.isLogin as Mock).mockReturnValue(false); fixture.detectChanges(); const googleSignInBtn = fixture.debugElement.query(By.css("asl-google-signin-button")); expect(googleSignInBtn).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/dashboard/component/dashboard.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, NgZone, OnInit, ViewChild } from "@angular/core"; import { UserService } from "../../common/service/user/user.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { FlarumService } from "../service/user/flarum/flarum.service"; import { HttpErrorResponse } from "@angular/common/http"; import { ActivatedRoute, NavigationEnd, Router, RouterLink, RouterOutlet } from "@angular/router"; import { HubComponent } from "../../hub/component/hub.component"; import { SocialAuthService, GoogleSigninButtonModule } from "@abacritt/angularx-social-login"; import { AdminSettingsService } from "../service/admin/settings/admin-settings.service"; import { GuiConfigService } from "../../common/service/gui-config.service"; import { DASHBOARD_ABOUT, DASHBOARD_ADMIN_EXECUTION, DASHBOARD_ADMIN_GMAIL, DASHBOARD_ADMIN_SETTINGS, DASHBOARD_ADMIN_USER, DASHBOARD_USER_COMPUTING_UNIT, DASHBOARD_USER_DATASET, DASHBOARD_USER_DISCUSSION, DASHBOARD_USER_PROJECT, DASHBOARD_USER_QUOTA, DASHBOARD_USER_WORKFLOW, } from "../../app-routing.constant"; import { Version } from "../../../environments/version"; import { SidebarTabs } from "../../common/type/gui-config"; import { User } from "../../common/type/user"; import { Role } from "../../common/type/user"; import { NzLayoutComponent, NzSiderComponent, NzContentComponent } from "ng-zorro-antd/layout"; import { NzMenuDirective, NzSubMenuComponent, NzMenuItemComponent } from "ng-zorro-antd/menu"; import { NgIf } from "@angular/common"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { SearchBarComponent } from "./user/search-bar/search-bar.component"; import { UserIconComponent } from "./user/user-icon/user-icon.component"; @Component({ selector: "texera-dashboard", templateUrl: "dashboard.component.html", styleUrls: ["dashboard.component.scss"], imports: [ NzLayoutComponent, NzSiderComponent, NzMenuDirective, NgIf, NzSubMenuComponent, ɵNzTransitionPatchDirective, HubComponent, NzMenuItemComponent, NzTooltipDirective, RouterLink, NzIconDirective, SearchBarComponent, UserIconComponent, GoogleSigninButtonModule, NzContentComponent, RouterOutlet, ], }) @UntilDestroy() export class DashboardComponent implements OnInit { @ViewChild(HubComponent) hubComponent!: HubComponent; isAdmin: boolean = this.userService.isAdmin(); isLogin = this.userService.isLogin(); public gitCommitHash: string = Version.raw; displayForum: boolean = true; displayNavbar: boolean = true; isCollapsed: boolean = false; showLinks: boolean = false; logo: string = ""; miniLogo: string = ""; sidebarTabs: SidebarTabs = { hub_enabled: false, home_enabled: false, workflow_enabled: false, dataset_enabled: false, your_work_enabled: false, projects_enabled: false, workflows_enabled: false, datasets_enabled: false, compute_enabled: false, quota_enabled: false, forum_enabled: false, about_enabled: false, }; protected readonly DASHBOARD_USER_PROJECT = DASHBOARD_USER_PROJECT; protected readonly DASHBOARD_USER_WORKFLOW = DASHBOARD_USER_WORKFLOW; protected readonly DASHBOARD_USER_DATASET = DASHBOARD_USER_DATASET; protected readonly DASHBOARD_USER_COMPUTING_UNIT = DASHBOARD_USER_COMPUTING_UNIT; protected readonly DASHBOARD_USER_QUOTA = DASHBOARD_USER_QUOTA; protected readonly DASHBOARD_USER_DISCUSSION = DASHBOARD_USER_DISCUSSION; protected readonly DASHBOARD_ADMIN_USER = DASHBOARD_ADMIN_USER; protected readonly DASHBOARD_ADMIN_GMAIL = DASHBOARD_ADMIN_GMAIL; protected readonly DASHBOARD_ADMIN_EXECUTION = DASHBOARD_ADMIN_EXECUTION; protected readonly DASHBOARD_ADMIN_SETTINGS = DASHBOARD_ADMIN_SETTINGS; constructor( private userService: UserService, private router: Router, private flarumService: FlarumService, private ngZone: NgZone, private socialAuthService: SocialAuthService, private route: ActivatedRoute, private adminSettingsService: AdminSettingsService, protected config: GuiConfigService ) {} ngOnInit(): void { this.isCollapsed = false; this.router.events.pipe(untilDestroyed(this)).subscribe(() => { this.checkRoute(); }); this.router.events.pipe(untilDestroyed(this)).subscribe(event => { if (event instanceof NavigationEnd) { this.checkRoute(); this.showLinks = event.url.includes("about"); } }); this.userService .userChanged() .pipe(untilDestroyed(this)) .subscribe(user => { this.ngZone.run(() => { this.isLogin = this.userService.isLogin(); this.isAdmin = this.userService.isAdmin(); this.forumLogin(); }); }); this.socialAuthService.authState.pipe(untilDestroyed(this)).subscribe(user => { this.userService .googleLogin(user.idToken) .pipe(untilDestroyed(this)) .subscribe(() => { this.ngZone.run(() => { this.router.navigateByUrl(this.route.snapshot.queryParams["returnUrl"] || DASHBOARD_USER_WORKFLOW); }); }); }); this.loadLogos(); this.loadTabs(); } loadLogos(): void { this.adminSettingsService .getSetting("logo") .pipe(untilDestroyed(this)) .subscribe(dataUri => { this.logo = dataUri; }); this.adminSettingsService .getSetting("mini_logo") .pipe(untilDestroyed(this)) .subscribe(dataUri => { this.miniLogo = dataUri; }); this.adminSettingsService .getSetting("favicon") .pipe(untilDestroyed(this)) .subscribe(dataUri => { document.querySelectorAll("link[rel*='icon']").forEach(el => ((el as HTMLLinkElement).href = dataUri)); }); } loadTabs(): void { (Object.keys(this.sidebarTabs) as (keyof SidebarTabs)[]).forEach(tab => { this.adminSettingsService .getSetting(tab) .pipe(untilDestroyed(this)) .subscribe(value => { this.sidebarTabs[tab] = value === "true"; }); }); } forumLogin() { if (!document.cookie.includes("flarum_remember") && this.isLogin) { this.flarumService .auth() .pipe(untilDestroyed(this)) .subscribe({ next: (response: any) => { document.cookie = `flarum_remember=${response.token};path=/`; }, error: (err: unknown) => { if ([404, 500].includes((err as HttpErrorResponse).status)) { this.displayForum = false; } else { this.flarumService .register() .pipe(untilDestroyed(this)) .subscribe(() => this.forumLogin()); } }, }); } } checkRoute() { const currentRoute = this.router.url; this.displayNavbar = this.isNavbarEnabled(currentRoute); } isNavbarEnabled(currentRoute: string) { // Hide navbar for workflow workspace pages (with numeric ID) if (currentRoute.match(/\/dashboard\/user\/workflow\/\d+/)) { return false; } return true; } handleCollapseChange(collapsed: boolean) { this.isCollapsed = collapsed; const resizeEvent = new Event("resize"); const editor = document.getElementById("workflow-editor"); if (editor) { setTimeout(() => { window.dispatchEvent(resizeEvent); }, 175); } } protected readonly DASHBOARD_ABOUT = DASHBOARD_ABOUT; protected readonly String = String; } ================================================ FILE: frontend/src/app/dashboard/component/section-style.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * grid of the second row which contains the search bar **/ .section-search-bar { grid-row-start: 2; grid-row-end: 3; margin-left: 10px; gap: 10px; display: flex; padding: 4px 24px; align-items: center; .search-input-box { width: 100%; } } /** * Create a 3*1 grid area * The first row contains add and sort button. * The second row contains the search bar. * The third row contains the saved workflow/files list. * * This layout is shared between workflows and files section. * * ****************** * * * * ****************** * * * * ****************** * * * * * * * * * * ****************** **/ $section-header-height: 124px; $section-search-bar-height: 60px; $dashboard-navigation-height: 76px; .section-container { display: grid; grid-template-rows: $section-header-height $section-search-bar-height calc((100% - #{$section-header-height} - #{$section-search-bar-height})); } /** * grid of the first row which contains title and utility buttons **/ .section-title { grid-row-start: 1; grid-row-end: 2; .page-title { font-family: -apple-system, BlinkMacSystemFont, sans-serif; } .management-panel { display: flex; align-items: center; justify-content: space-between; } // .utility-button-group { // float: left; // } .go-back-button { position: absolute; left: 20px; top: 20px; } } /** * the third grid of the workflow/files list. * including css shared by workflow/file item **/ .section-list-container { grid-row-start: 3; grid-row-end: 4; overflow: auto; min-height: 100%; height: 100%; // always show the scroll bar for the virtual scroll container ::-webkit-scrollbar { -webkit-appearance: none; width: 7px; } ::-webkit-scrollbar-thumb { border-radius: 4px; background-color: rgba(0, 0, 0, 0.5); -webkit-box-shadow: 0 0 1px rgba(255, 255, 255, 0.5); } .virtual-scroll-container { height: 100%; border: 1px solid #e8e8e8; border-radius: 4px; } .metadata-container { span { margin: 0 1rem 0 0; } } } /** * css specifically for each workflow item in the list (not shared by file items) **/ .workflow-list-item { margin-bottom: 10px; min-height: 70px; padding: 5px 0 5px 0; .workflow-item-checkbox { margin: 8px; } .workflow-item-meta-title { display: flex; align-items: center; .workflow-name { font-size: 20px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; text-align: center; margin-bottom: 0; color: inherit; text-decoration: none; } .workflow-name:hover { cursor: pointer; } i { position: relative; font-size: 17px; } i.workflow-is-owner-icon { margin-left: 7px; } } .workflow-item-meta-description { display: flex; align-items: center; padding: 2px 8px 2px 10px; margin-bottom: 5px; .workflow-description { font-size: 13px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; display: inline-block; min-width: 17ch; border: 0 none; outline: none; &:hover { cursor: pointer; box-shadow: 0 0 0 1px rgb(202, 202, 202); } } .workflow-editable-description { margin-bottom: 5px; display: inline-block; min-width: 17ch; border: 0 none; outline: none; box-shadow: 0 0 0 2px #007bff; } } } .subsection-grid-container { min-width: 100%; width: 100%; min-height: 100%; height: 100%; } .ant-btn-icon-only { margin-left: 5px; margin-right: 5px; } .metadata-container { span { margin: 0 1rem 0 0; // add space to the right } } ================================================ FILE: frontend/src/app/dashboard/component/user/files-uploader/conflicting-file-modal-content/conflicting-file-modal-content.component.html ================================================ Title
    File: {{ data.fileName }}
    Path: {{ data.path }}
    Size: {{ data.size }}
    An upload session already exists for this path.
    ================================================ FILE: frontend/src/app/dashboard/component/user/files-uploader/conflicting-file-modal-content/conflicting-file-modal-content.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .hint { margin-top: 8px; } ================================================ FILE: frontend/src/app/dashboard/component/user/files-uploader/conflicting-file-modal-content/conflicting-file-modal-content.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ChangeDetectionStrategy, Component, inject } from "@angular/core"; import { NZ_MODAL_DATA } from "ng-zorro-antd/modal"; export interface ConflictingFileModalData { fileName: string; path: string; size: string; } @Component({ selector: "texera-conflicting-file-modal-content", templateUrl: "./conflicting-file-modal-content.component.html", styleUrls: ["./conflicting-file-modal-content.component.scss"], changeDetection: ChangeDetectionStrategy.OnPush, }) export class ConflictingFileModalContentComponent { readonly data: ConflictingFileModalData = inject(NZ_MODAL_DATA); } ================================================ FILE: frontend/src/app/dashboard/component/user/files-uploader/files-uploader.component.html ================================================

    Drag & drop file/folder to upload

    or

    ================================================ FILE: frontend/src/app/dashboard/component/user/files-uploader/files-uploader.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .file-uploader { padding: 20px; transition: border-color 0.3s ease-in-out; margin: auto; /* Center the uploader if width is less than 100% */ } .ngx-drop-box { width: 100%; height: 90%; } .file-uploader:hover { border-color: #007bff; } .file-drop-description p { text-align: center; font-size: 15px; margin: 10px 0; } .uploaded-files-list { font-size: 15px; margin-top: 5px; height: 25%; width: 90%; margin-left: 5%; margin-right: 5%; overflow-y: auto; overflow-x: auto; } .upload-file-button { padding: 5px 20px; /* Smaller padding to make the button thinner */ font-size: 0.7em; /* Optionally reduce font size */ height: 30px; /* Or set a specific height */ width: 60%; margin-left: 20%; display: inline-flex; /* Use flexbox to center content */ align-items: center; /* Align items vertically */ justify-content: center; margin-bottom: 15px; } .file-uploader-banner { } ================================================ FILE: frontend/src/app/dashboard/component/user/files-uploader/files-uploader.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, EventEmitter, Input, Output } from "@angular/core"; import { firstValueFrom } from "rxjs"; import { NgxFileDropEntry, NgxFileDropModule } from "ngx-file-drop"; import { NzModalRef, NzModalService } from "ng-zorro-antd/modal"; import { FileUploadItem } from "../../../type/dashboard-file.interface"; import { DatasetFileNode } from "../../../../common/type/datasetVersionFileTree"; import { NotificationService } from "../../../../common/service/notification/notification.service"; import { AdminSettingsService } from "../../../service/admin/settings/admin-settings.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { DatasetService } from "../../../service/user/dataset/dataset.service"; import { formatSize } from "../../../../common/util/size-formatter.util"; import { ConflictingFileModalContentComponent, ConflictingFileModalData, } from "./conflicting-file-modal-content/conflicting-file-modal-content.component"; import { NgIf } from "@angular/common"; import { NzAlertComponent } from "ng-zorro-antd/alert"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; @UntilDestroy() @Component({ selector: "texera-user-files-uploader", templateUrl: "./files-uploader.component.html", styleUrls: ["./files-uploader.component.scss"], imports: [ NgIf, NzAlertComponent, NgxFileDropModule, NzSpaceCompactItemDirective, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, ], }) export class FilesUploaderComponent { @Input() showUploadAlert: boolean = false; /** * Optional context fields supplied by the embedding component. When the * uploader is used inside `DatasetDetailComponent`, the parent passes * `ownerEmail` and `datasetName` so the uploader can address staged files * under the right owner/dataset path. When used standalone (e.g. dataset * creation flow), they default to empty. */ @Input() ownerEmail: string = ""; @Input() datasetName: string = ""; @Output() uploadedFiles = new EventEmitter(); newUploadFileTreeNodes: DatasetFileNode[] = []; fileUploadingFinished: boolean = false; fileUploadBannerType: "error" | "success" | "info" | "warning" = "success"; fileUploadBannerMessage: string = ""; singleFileUploadMaxSizeMiB: number = 20; constructor( private notificationService: NotificationService, private adminSettingsService: AdminSettingsService, private datasetService: DatasetService, private modal: NzModalService ) { this.adminSettingsService .getSetting("single_file_upload_max_size_mib") .pipe(untilDestroyed(this)) .subscribe(value => (this.singleFileUploadMaxSizeMiB = parseInt(value))); } private markForceRestart(item: FileUploadItem): void { // uploader should call backend init with type=forceRestart when this is set item.restart = true; } private askResumeOrSkip( item: FileUploadItem, showForAll: boolean ): Promise<"resume" | "resumeAll" | "restart" | "restartAll"> { return new Promise(resolve => { const fileName = item.name.split("/").pop() || item.name; const sizeStr = formatSize(item.file.size); const ref: NzModalRef = this.modal.create({ nzTitle: "Conflicting File", nzMaskClosable: false, nzClosable: false, nzContent: ConflictingFileModalContentComponent, nzData: { fileName, path: item.name, size: sizeStr, }, nzFooter: [ ...(showForAll ? [ { label: "Restart For All", onClick: () => { resolve("restartAll"); ref.destroy(); }, }, { label: "Resume For All", onClick: () => { resolve("resumeAll"); ref.destroy(); }, }, ] : []), { label: "Restart", onClick: () => { resolve("restart"); ref.destroy(); }, }, { label: "Resume", type: "primary", onClick: () => { resolve("resume"); ref.destroy(); }, }, ], }); }); } private async resolveConflicts(items: FileUploadItem[], activePaths: string[]): Promise { const active = new Set(activePaths ?? []); const isConflict = (p: string) => active.has(p) || active.has(encodeURIComponent(p)); const showForAll = items.length > 1; let mode: "ask" | "resumeAll" | "restartAll" = "ask"; const out: FileUploadItem[] = []; await items.reduce>(async (chain, item) => { await chain; if (!isConflict(item.name)) { out.push(item); return; } if (mode === "resumeAll") { out.push(item); return; } if (mode === "restartAll") { this.markForceRestart(item); out.push(item); return; } const choice = await this.askResumeOrSkip(item, showForAll); if (choice === "resume") out.push(item); if (choice === "resumeAll") { mode = "resumeAll"; out.push(item); } if (choice === "restart") { this.markForceRestart(item); out.push(item); } if (choice === "restartAll") { mode = "restartAll"; this.markForceRestart(item); out.push(item); } }, Promise.resolve()); return out; } hideBanner(): void { this.fileUploadingFinished = false; } showFileUploadBanner(bannerType: "error" | "success" | "info" | "warning", bannerMessage: string): void { this.fileUploadingFinished = true; this.fileUploadBannerType = bannerType; this.fileUploadBannerMessage = bannerMessage; } private getOwnerAndName(): { ownerEmail: string; datasetName: string } { return { ownerEmail: this.ownerEmail, datasetName: this.datasetName, }; } public fileDropped(files: NgxFileDropEntry[]): void { const filePromises = files.map(droppedFile => { return new Promise((resolve, reject) => { if (droppedFile.fileEntry.isFile) { const fileEntry = droppedFile.fileEntry as FileSystemFileEntry; fileEntry.file( file => { if (file.size > this.singleFileUploadMaxSizeMiB * 1024 * 1024) { this.notificationService.error( `File ${file.name}'s size exceeds the maximum limit of ${this.singleFileUploadMaxSizeMiB}MiB.` ); reject(null); return; } resolve({ file, name: droppedFile.relativePath, description: "", uploadProgress: 0, isUploadingFlag: false, restart: false, }); }, err => reject(err) ); } else { resolve(null); } }); }); Promise.allSettled(filePromises) .then(async results => { const { ownerEmail, datasetName } = this.getOwnerAndName(); const activePathsPromise = ownerEmail && datasetName ? firstValueFrom(this.datasetService.listMultipartUploads(ownerEmail, datasetName)).catch(() => []) : []; const activePaths = await activePathsPromise; const successfulUploads = results .filter((r): r is PromiseFulfilledResult => r.status === "fulfilled") .map(r_1 => r_1.value) .filter((item): item is FileUploadItem => item !== null); const filteredUploads = await this.resolveConflicts(successfulUploads, activePaths); if (filteredUploads.length > 0) { const msg = `${filteredUploads.length} file${filteredUploads.length > 1 ? "s" : ""} selected successfully!`; this.showFileUploadBanner("success", msg); } const failedCount = results.length - successfulUploads.length; if (failedCount > 0) { const errorMessage = `${failedCount} file${failedCount > 1 ? "s" : ""} failed to be selected.`; this.showFileUploadBanner("error", errorMessage); } this.uploadedFiles.emit(filteredUploads); }) .catch(error => { this.showFileUploadBanner("error", `Unexpected error: ${error?.message ?? error}`); }); } } ================================================ FILE: frontend/src/app/dashboard/component/user/filters/filters.component.html ================================================
    ================================================ FILE: frontend/src/app/dashboard/component/user/filters/filters.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ @import "../user-workflow/user-workflow.component.scss"; ================================================ FILE: frontend/src/app/dashboard/component/user/filters/filters.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { NO_ERRORS_SCHEMA } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { FiltersComponent } from "./filters.component"; import { StubOperatorMetadataService } from "src/app/workspace/service/operator-metadata/stub-operator-metadata.service"; import { OperatorMetadataService } from "src/app/workspace/service/operator-metadata/operator-metadata.service"; import { WorkflowPersistService } from "src/app/common/service/workflow-persist/workflow-persist.service"; import { StubWorkflowPersistService } from "src/app/common/service/workflow-persist/stub-workflow-persist.service"; import { testWorkflowEntries } from "../../user-dashboard-test-fixtures"; import { NzDropDownModule } from "ng-zorro-antd/dropdown"; import { JWT_OPTIONS, JwtHelperService } from "@auth0/angular-jwt"; import { FormsModule } from "@angular/forms"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { commonTestProviders } from "src/app/common/testing/test-utils"; import { NzModalModule } from "ng-zorro-antd/modal"; import { en_US, provideNzI18n } from "ng-zorro-antd/i18n"; describe("FiltersComponent", () => { let component: FiltersComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ providers: [ JwtHelperService, { provide: JWT_OPTIONS, useValue: {} }, { provide: WorkflowPersistService, useValue: new StubWorkflowPersistService(testWorkflowEntries) }, { provide: OperatorMetadataService, useClass: StubOperatorMetadataService }, provideNzI18n(en_US), ...commonTestProviders, ], imports: [FiltersComponent, NzModalModule, NzDropDownModule, FormsModule, HttpClientTestingModule], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(FiltersComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); it("parses manually entered mtime", () => { component.masterFilterList = ["mtime: 2022-01-22 ~ 2022-04-21"]; expect(component.selectedMtime).toEqual([new Date(2022, 0, 22), new Date(2022, 3, 21)]); }); it("parses manually entered ctime", () => { component.masterFilterList = ["ctime: 2022-01-22 ~ 2022-04-21"]; expect(component.selectedCtime).toEqual([new Date(2022, 0, 22), new Date(2022, 3, 21)]); }); it("preserves ordering when parsing drop down", () => { component.masterFilterList = ["keyword", "ctime: 2022-01-22 ~ 2022-04-21", "keyword 2"]; component.selectedCtime = [new Date(2022, 2, 22), new Date(2022, 4, 21)]; component.buildMasterFilterList(); expect(component.masterFilterList).toEqual(["keyword", "ctime: 2022-03-22 ~ 2022-05-21", "keyword 2"]); component.masterFilterList = [...component.masterFilterList, "another keyword"]; expect(component.masterFilterList).toEqual([ "keyword", "ctime: 2022-03-22 ~ 2022-05-21", "keyword 2", "another keyword", ]); }); }); ================================================ FILE: frontend/src/app/dashboard/component/user/filters/filters.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { OperatorMetadataService } from "src/app/workspace/service/operator-metadata/operator-metadata.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { Observable, of } from "rxjs"; import { DashboardProject } from "../../../type/dashboard-project.interface"; import { NotificationService } from "src/app/common/service/notification/notification.service"; import { UserProjectService } from "../../../service/user/project/user-project.service"; import { WorkflowPersistService } from "src/app/common/service/workflow-persist/workflow-persist.service"; import { SearchFilterParameters } from "../../../type/search-filter-parameters"; import { UserService } from "../../../../common/service/user/user.service"; import { switchMap } from "rxjs/operators"; import { NzDropdownADirective, NzDropdownDirective, NzDropdownMenuComponent } from "ng-zorro-antd/dropdown"; import { NzSpaceCompactItemDirective, NzSpaceCompactComponent } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzDatePickerComponent, NzRangePickerComponent } from "ng-zorro-antd/date-picker"; import { FormsModule } from "@angular/forms"; import { NgFor, NgIf } from "@angular/common"; import { NzMenuDirective, NzMenuItemComponent, NzSubMenuComponent } from "ng-zorro-antd/menu"; import { NzCheckboxComponent } from "ng-zorro-antd/checkbox"; @UntilDestroy() @Component({ selector: "texera-filters", templateUrl: "./filters.component.html", styleUrls: ["./filters.component.scss"], imports: [ NzDropdownADirective, NzDropdownDirective, NzSpaceCompactItemDirective, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, NzIconDirective, NzDropdownMenuComponent, NzDatePickerComponent, NzRangePickerComponent, FormsModule, NgFor, NzMenuDirective, NzMenuItemComponent, NzCheckboxComponent, NgIf, NzSubMenuComponent, NzSpaceCompactComponent, ], }) export class FiltersComponent implements OnInit { public isLogin = this.userService.isLogin(); private _masterFilterList: ReadonlyArray = []; // receive input from parent components (UserProjectSection), if any @Input() public pid?: number = undefined; @Output() public masterFilterListChange = new EventEmitter(); public get masterFilterList(): ReadonlyArray { return this._masterFilterList; } public set masterFilterList(value: ReadonlyArray) { this.setMasterFilterList(value, true); } private setMasterFilterList(value: ReadonlyArray, updateDropdown: boolean) { if ( !this._masterFilterList || !value || this._masterFilterList.length !== value.length || this._masterFilterList.some((v, i) => v !== value[i]) ) { // Only update when there is a change to prevent unnecessary calls to search. this._masterFilterList = value; if (updateDropdown) { this.updateDropdownMenus(value); } this.masterFilterListChange.emit(value); } } public owners: { userName: string; checked: boolean }[] = []; public wids: { id: string; checked: boolean }[] = []; public operatorGroups: string[] = []; public operators: Map< string, { userFriendlyName: string; operatorType: string; operatorGroup: string; checked: boolean }[] > = new Map(); public selectedCtime: Date[] = []; public selectedMtime: Date[] = []; public selectedOwners: string[] = []; public selectedIDs: string[] = []; public selectedOperators: { userFriendlyName: string; operatorType: string; operatorGroup: string }[] = []; public selectedProjects: { name: string; pid: number }[] = []; /* variables for filtering workflows by projects */ public userProjectsList!: Observable; // list of projects accessible by user public userProjectsDropdown: { pid: number; name: string; checked: boolean }[] = []; /* variables for project color tags */ public userProjectsMap: ReadonlyMap = new Map(); // maps pid to its corresponding DashboardProjectInterface public userProjectsLoaded: boolean = false; // tracks whether all DashboardProjectInterface information has been loaded (ready to render project colors) public searchCriteria: string[] = ["owner", "id", "ctime", "mtime", "operator", "project"]; constructor( private userService: UserService, private operatorMetadataService: OperatorMetadataService, private notificationService: NotificationService, private userProjectService: UserProjectService, private workflowPersistService: WorkflowPersistService, private cdr: ChangeDetectorRef ) {} ngOnInit(): void { this.setupUserProject(); this.searchParameterBackendSetup(); } private setupUserProject(): void { this.userService .userChanged() .pipe( switchMap(() => { this.isLogin = this.userService.isLogin(); this.cdr.detectChanges(); if (this.isLogin) { return this.userProjectService.getProjectList() as Observable; } else { return of([] as DashboardProject[]); } }), untilDestroyed(this) ) .subscribe((userProjectsList: DashboardProject[]) => { if (userProjectsList && userProjectsList.length > 0) { this.userProjectsMap = new Map(userProjectsList.map(userProject => [userProject.pid, userProject])); this.userProjectsDropdown = userProjectsList.map(proj => ({ pid: proj.pid, name: proj.name, checked: false, })); this.userProjectsLoaded = true; } }); } /** * Backend calls for Workflow IDs, Owners, and Operators in saved workflow component */ private searchParameterBackendSetup() { this.operatorMetadataService .getOperatorMetadata() .pipe(untilDestroyed(this)) .subscribe(opdata => { opdata.groups.forEach(group => { this.operators.set( group.groupName, opdata.operators .filter(operator => operator.additionalMetadata.operatorGroupName === group.groupName) .map(operator => { return { userFriendlyName: operator.additionalMetadata.userFriendlyName, operatorType: operator.operatorType, operatorGroup: operator.additionalMetadata.operatorGroupName, checked: false, }; }) ); }); this.operatorGroups = opdata.groups.map(group => group.groupName); }); if (this.isLogin) { this.workflowPersistService .retrieveOwners() .pipe(untilDestroyed(this)) .subscribe(list_of_owners => { this.owners = list_of_owners.map(i => ({ userName: i, checked: false })); }); this.workflowPersistService .retrieveWorkflowIDs() .pipe(untilDestroyed(this)) .subscribe(wids => { this.wids = wids.map(wid => { return { id: wid.toString(), checked: false, }; }); }); } } /** * updates selectedOwners array to match owners checked in dropdown menu */ public updateSelectedOwners(): void { this.selectedOwners = this.owners.filter(owner => owner.checked).map(owner => owner.userName); this.buildMasterFilterList(); } /** * updates selectedIDs array to match worfklow ids checked in dropdown menu */ public updateSelectedIDs(): void { this.selectedIDs = this.wids.filter(wid => wid.checked).map(wid => wid.id); this.buildMasterFilterList(); } /** * updates selectedOperators array to match operators checked in dropdown menu */ public updateSelectedOperators(): void { const filteredOperators: { userFriendlyName: string; operatorType: string; operatorGroup: string }[] = []; Array.from(this.operators.values()) .flat() .forEach(operator => { if (operator.checked) { filteredOperators.push({ userFriendlyName: operator.userFriendlyName, operatorType: operator.operatorType, operatorGroup: operator.operatorGroup, }); } }); this.selectedOperators = filteredOperators; this.buildMasterFilterList(); } /** * updates selectedProjects array to match projects checked in dropdown menu */ public updateSelectedProjects(): void { this.selectedProjects = this.userProjectsDropdown .filter(proj => proj.checked) .map(proj => { return { name: proj.name, pid: proj.pid }; }); this.buildMasterFilterList(); } /** * updates dropdown menus when nz-select bar is changed */ public updateDropdownMenus(tagListString: ReadonlyArray): void { //operators array is not cleared, so that operator object properties can be used for reconstruction of the array //operators map is too expensive/difficult to search for operator object properties this.selectedIDs = []; this.selectedOwners = []; this.selectedProjects = []; let newSelectedOperators: { userFriendlyName: string; operatorType: string; operatorGroup: string }[] = []; this.selectedCtime = []; this.selectedMtime = []; this.setDropdownSelectionsToUnchecked(); tagListString.forEach(tag => { if (tag.includes(":")) { const searchArray = tag.split(":"); const searchField = searchArray[0]; const searchValue = searchArray[1].trim(); const date_regex = /^(\d{4})[-](0[1-9]|1[0-2])[-](0[1-9]|[12][0-9]|3[01]) ~ (\d{4})[-](0[1-9]|1[0-2])[-](0[1-9]|[12][0-9]|3[01])$/; const searchDate: RegExpMatchArray | null = searchValue.match(date_regex); switch (searchField) { case "owner": const selectedOwnerIndex = this.owners.findIndex(owner => owner.userName === searchValue); if (selectedOwnerIndex === -1) { this.removeInvalidFilterTag(tag); this.notificationService.error("Invalid owner name"); break; } this.owners[selectedOwnerIndex].checked = true; this.selectedOwners.push(searchValue); break; case "id": const selectedIDIndex = this.wids.findIndex(wid => wid.id === searchValue); if (selectedIDIndex === -1) { this.removeInvalidFilterTag(tag); this.notificationService.error("Invalid workflow id"); break; } this.wids[selectedIDIndex].checked = true; this.selectedIDs.push(searchValue); break; case "operator": const selectedOperator = this.selectedOperators.find(operator => operator.userFriendlyName === searchValue); if (!selectedOperator) { this.removeInvalidFilterTag(tag); this.notificationService.error("Invalid operator name"); break; } newSelectedOperators.push(selectedOperator); const operatorSublist = this.operators.get(selectedOperator.operatorGroup); if (operatorSublist) { for (let operator of operatorSublist) { if (operator.userFriendlyName === searchValue) { operator.checked = true; break; } } } break; case "project": const selectedProjectIndex = this.userProjectsDropdown.findIndex(proj => proj.name === searchValue); if (selectedProjectIndex === -1) { this.removeInvalidFilterTag(tag); this.notificationService.error("Invalid project name"); break; } this.userProjectsDropdown[selectedProjectIndex].checked = true; const selectedProject = this.userProjectsDropdown[selectedProjectIndex]; this.selectedProjects.push({ name: selectedProject.name, pid: selectedProject.pid }); break; case "ctime": //should only run at most once if (this.selectedCtime.length > 0) { // if there is already an selected date, ignore the subsequent ctime tags this.notificationService.error("Multiple search dates is not allowed"); break; } if (!searchDate) { this.notificationService.error("Date format is incorrect"); break; } this.selectedCtime[0] = new Date( parseInt(searchDate[1]), parseInt(searchDate[2]) - 1, parseInt(searchDate[3]) ); this.selectedCtime[1] = new Date( parseInt(searchDate[4]), parseInt(searchDate[5]) - 1, parseInt(searchDate[6]) ); break; case "mtime": //should only run at most once if (this.selectedMtime.length > 0) { // if there is already an selected date, ignore the subsequent ctime tags this.notificationService.error("Multiple search dates is not allowed"); break; } if (!searchDate) { this.notificationService.error("Date format is incorrect"); break; } this.selectedMtime[0] = new Date( parseInt(searchDate[1]), parseInt(searchDate[2]) - 1, parseInt(searchDate[3]) ); this.selectedMtime[1] = new Date( parseInt(searchDate[4]), parseInt(searchDate[5]) - 1, parseInt(searchDate[6]) ); break; } } }); this.selectedOperators = newSelectedOperators; this.buildMasterFilterList(); } private removeInvalidFilterTag(tag: string): void { this.setMasterFilterList( this.masterFilterList.filter(filterTag => filterTag !== tag), false ); } /** * sets all dropdown menu options to unchecked */ private setDropdownSelectionsToUnchecked(): void { this.owners.forEach(owner => { owner.checked = false; }); this.wids.forEach(wid => { wid.checked = false; }); for (let operatorList of this.operators.values()) { operatorList.forEach(operator => (operator.checked = false)); } this.userProjectsDropdown.forEach(proj => { proj.checked = false; }); } /** * checks if a tag string is a workflow name or dropdown menu search parameter */ private checkIfWorkflowName(tag: string) { const stringChecked: string[] = tag.split(":"); return !(stringChecked.length === 2 && this.searchCriteria.includes(stringChecked[0])); } /** * builds the tags to be displayd in the nz-select search bar * - Workflow names with ":" are not allowed due to conflict with other search parameters' format */ public buildMasterFilterList(): void { let newFilterList: string[] = this.masterFilterList.filter(tag => this.checkIfWorkflowName(tag)); newFilterList = newFilterList.concat(this.selectedOwners.map(owner => "owner: " + owner)); newFilterList = newFilterList.concat(this.selectedIDs.map(id => "id: " + id)); newFilterList = newFilterList.concat( this.selectedOperators.map(operator => "operator: " + operator.userFriendlyName) ); newFilterList = newFilterList.concat(this.selectedProjects.map(proj => "project: " + proj.name)); if (this.selectedCtime.length != 0) { newFilterList.push( "ctime: " + this.getFormattedDateString(this.selectedCtime[0]) + " ~ " + this.getFormattedDateString(this.selectedCtime[1]) ); } if (this.selectedMtime.length != 0) { newFilterList.push( "mtime: " + this.getFormattedDateString(this.selectedMtime[0]) + " ~ " + this.getFormattedDateString(this.selectedMtime[1]) ); } this.setMasterFilterList(this.updateMasterFilterList(this.masterFilterList, newFilterList), false); } private updateMasterFilterList(masterFilterList: ReadonlyArray, items: string[]): string[] { const list = [...masterFilterList]; // The purpose of this function is to preserve order. // Add the item if it doesn't exist. for (const item of items) { const ctime = item.startsWith("ctime: "); const mtime = item.startsWith("mtime: "); if (ctime || mtime) { const index = list.findIndex(i => i.startsWith(ctime ? "ctime: " : "mtime: ")); if (index !== -1) { list[index] = item; } else { list.push(item); } } else { const index = list.indexOf(item); if (index === -1) { list.push(item); } } } // Remove ones that doesn't exist in the new list. return list.filter(i => items.indexOf(i) !== -1); } /** * returns a formatted string representing a Date object */ private getFormattedDateString(date: Date): string { let dateMonth: number = date.getMonth() + 1; let dateDay: number = date.getDate(); return `${date.getFullYear()}-${(dateMonth < 10 ? "0" : "") + dateMonth}-${(dateDay < 10 ? "0" : "") + dateDay}`; } public getSearchFilterParameters(): SearchFilterParameters { return { createDateStart: this.selectedCtime.length > 0 ? this.selectedCtime[0] : null, createDateEnd: this.selectedCtime.length > 0 ? this.selectedCtime[1] : null, modifiedDateStart: this.selectedMtime.length > 0 ? this.selectedMtime[0] : null, modifiedDateEnd: this.selectedMtime.length > 0 ? this.selectedMtime[1] : null, owners: this.selectedOwners, ids: this.selectedIDs, operators: this.selectedOperators.map(o => o.operatorType), projectIds: this.selectedProjects.map(p => p.pid), }; } public getSearchKeywords(): string[] { return this.masterFilterList.filter(tag => this.checkIfWorkflowName(tag)); } } ================================================ FILE: frontend/src/app/dashboard/component/user/filters-instructions/filters-instructions.component.html ================================================ We support the following search criteria:
    • Search by Workflow Name: workflowName
    • Search by Workflow Creation Time: ctime: yyyy-MM-dd
    • Search by Workflow Modification Time: mtime: yyyy-MM-dd
    • Search by Workflow Owner: owner: John
    • Search by Workflow Id: id: workflowId
    • Search by Workflows' Operators: operator: operatorName
    • Search by User Projects: project: projectName
    You can change search parameters by:
    • selecting/unselecting dropdown menu options
    • manually typing parameters into search bar
    • clicking the X on a tag or the search bar, to clear one or all tags

    Example: "Untitled Workflow" id:1 owner:John
    Meaning: Search for the workflow with name Untitled Workflow, id 1, and the owner called John.
    ================================================ FILE: frontend/src/app/dashboard/component/user/filters-instructions/filters-instructions.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { FiltersInstructionsComponent } from "./filters-instructions.component"; import { NzPopoverModule } from "ng-zorro-antd/popover"; describe("FiltersInstructionsComponent", () => { let component: FiltersInstructionsComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [FiltersInstructionsComponent, NzPopoverModule], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(FiltersInstructionsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/dashboard/component/user/filters-instructions/filters-instructions.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component } from "@angular/core"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzPopoverDirective } from "ng-zorro-antd/popover"; @Component({ selector: "texera-filters-instructions", templateUrl: "./filters-instructions.component.html", imports: [ɵNzTransitionPatchDirective, NzIconDirective, NzPopoverDirective], }) export class FiltersInstructionsComponent {} ================================================ FILE: frontend/src/app/dashboard/component/user/flarum/flarum.component.html ================================================ ================================================ FILE: frontend/src/app/dashboard/component/user/flarum/flarum.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component } from "@angular/core"; import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser"; @Component({ templateUrl: "./flarum.component.html" }) export class FlarumComponent { flarumUrl: SafeResourceUrl = this.sanitizer.bypassSecurityTrustResourceUrl("forum"); constructor(private sanitizer: DomSanitizer) {} } ================================================ FILE: frontend/src/app/dashboard/component/user/list-item/list-item.component.html ================================================
    #{{ entry.id }}
    {{ entry.name }}
    {{ renderedDescription ? renderedDescription.slice(0, 200) : (hovering && editable) ? 'Write a description...' : '' }}
    Views:
    {{ formatCount(this.viewCount) }}
    Size:
    {{ formatSize(size) }}
    Created:
    {{ formatTime(entry.creationTime) }}
    Edited:
    {{ formatTime(entry.lastModifiedTime) }}
    ================================================ FILE: frontend/src/app/dashboard/component/user/list-item/list-item.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .list-item-card { padding: 3px; width: 100%; background-color: white; position: relative; min-height: 65px; height: auto; &.editing-description { z-index: 10; } &:hover { background-color: #f0f0f0; .edit-button { display: flex; } &.has-button-group:hover .resource-info { opacity: 0.3; } } &.selected { border-color: #1e90ff; background-color: #e6f7ff; .button-group, .button-group button { background-color: #e6f7ff; } .button-group button:hover, .edit-button button:hover { background-color: #cceeff; } } } .resource-name-group { min-width: 0; } .truncate-single-line { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; max-width: 80vh; } .resource-name { font-size: 17px; font-weight: 600; } .resource-description { font-size: 12px; font-weight: 300; color: grey; } .resource-info { font-size: 13px; color: grey; } .large-checkbox { display: none; width: 16px; height: 16px; } .list-item-card:hover .large-checkbox, .showc { display: block; z-index: 10; } .button-group { display: none; position: absolute; height: 70px; min-width: 150px; right: 0; bottom: 0; justify-content: right; align-items: center; transition: none; button { margin-right: 32px; transition: none; background-color: #e0e0e0; border: 1px solid #d0d0d0; border-radius: 8px; } button:hover { background-color: #c7c7c7; } } .list-item-card:hover .button-group { display: flex; background-color: transparent; } .resource-name-edit-input { font-size: 17px; font-weight: 600; padding: 0; margin: 4px 0; outline: none; height: 23px; border: 1px solid #d9d9d9; } .resource-description { font-size: 14px; font-weight: 400; color: #333333; transition: all 0.2s ease; } .list-item-card:hover .resource-description { font-size: 15px; font-weight: 500; color: #1e90ff; } .resource-description-edit-textarea { font-size: 12px; font-weight: 300; padding: 0; border: 1px solid #d9d9d9; outline: none; resize: none; line-height: 1.5; overflow: hidden; white-space: pre-wrap; width: 100%; min-height: 18px; height: auto; } .edit-button { display: none; &:hover { display: block; background-color: #d9d9d9; } button { height: 25px; } } .type-icon { font-size: 30px; } .workflow-id { padding: 6px; } .like-button { width: 50px; border-radius: 5px; padding: 3px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } .liked { color: red; } :host ::ng-deep nz-card.list-item-card.ant-card:hover .ant-card-body { cursor: pointer; } ================================================ FILE: frontend/src/app/dashboard/component/user/list-item/list-item.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ListItemComponent } from "./list-item.component"; import { WorkflowPersistService } from "src/app/common/service/workflow-persist/workflow-persist.service"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { NzModalService } from "ng-zorro-antd/modal"; import { of, throwError } from "rxjs"; import { NO_ERRORS_SCHEMA } from "@angular/core"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { RouterTestingModule } from "@angular/router/testing"; import { StubUserService } from "../../../../common/service/user/stub-user.service"; import { UserService } from "../../../../common/service/user/user.service"; import { commonTestProviders } from "../../../../common/testing/test-utils"; import type { Mocked } from "vitest"; import { DashboardEntry } from "src/app/dashboard/type/dashboard-entry"; describe("ListItemComponent", () => { let component: ListItemComponent; let fixture: ComponentFixture; let workflowPersistService: Mocked; beforeEach(async () => { const workflowPersistServiceSpy = { updateWorkflowName: vi.fn(), updateWorkflowDescription: vi.fn() }; await TestBed.configureTestingModule({ imports: [ListItemComponent, HttpClientTestingModule, BrowserAnimationsModule, RouterTestingModule], providers: [ { provide: WorkflowPersistService, useValue: workflowPersistServiceSpy }, { provide: UserService, useClass: StubUserService }, NzModalService, ...commonTestProviders, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(ListItemComponent); component = fixture.componentInstance; workflowPersistService = TestBed.inject(WorkflowPersistService) as unknown as Mocked; }); it("should update workflow name successfully", () => { const newName = "New Workflow Name"; component.entry = { id: 1, name: "Old Name", type: "workflow" } as unknown as DashboardEntry; workflowPersistService.updateWorkflowName.mockReturnValue(of({} as Response)); component.confirmUpdateCustomName(newName); expect(workflowPersistService.updateWorkflowName).toHaveBeenCalledWith(1, newName); expect(component.entry.name).toBe(newName); expect(component.editingName).toBe(false); }); it("should handle error when updating workflow name", () => { const newName = "New Workflow Name"; component.entry = { id: 1, name: "Old Name", type: "workflow" } as unknown as DashboardEntry; component.originalName = "Old Name"; workflowPersistService.updateWorkflowName.mockReturnValue(throwError(() => new Error("Error"))); component.confirmUpdateCustomName(newName); expect(workflowPersistService.updateWorkflowName).toHaveBeenCalledWith(1, newName); expect(component.entry.name).toBe("Old Name"); expect(component.editingName).toBe(false); }); it("should update workflow description successfully", () => { const newDescription = "New Description"; component.entry = { id: 1, description: "Old Description", type: "workflow" } as unknown as DashboardEntry; workflowPersistService.updateWorkflowDescription.mockReturnValue(of({} as Response)); component.confirmUpdateCustomDescription(newDescription); expect(workflowPersistService.updateWorkflowDescription).toHaveBeenCalledWith(1, newDescription); expect(component.entry.description).toBe(newDescription); expect(component.editingDescription).toBe(false); }); it("should handle error when updating workflow description", () => { const newDescription = "New Description"; component.entry = { id: 1, description: "Old Description", type: "workflow" } as unknown as DashboardEntry; component.originalDescription = "Old Description"; workflowPersistService.updateWorkflowDescription.mockReturnValue(throwError(() => new Error("Error"))); component.confirmUpdateCustomDescription(newDescription); expect(workflowPersistService.updateWorkflowDescription).toHaveBeenCalledWith(1, newDescription); expect(component.entry.description).toBe("Old Description"); expect(component.editingDescription).toBe(false); }); }); ================================================ FILE: frontend/src/app/dashboard/component/user/list-item/list-item.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ChangeDetectorRef, ElementRef, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewChild, } from "@angular/core"; import { Component } from "@angular/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { NzModalRef, NzModalService } from "ng-zorro-antd/modal"; import { DashboardEntry } from "src/app/dashboard/type/dashboard-entry"; import { MarkdownDescriptionComponent } from "../markdown-description/markdown-description.component"; import { ShareAccessComponent } from "../share-access/share-access.component"; import { DEFAULT_WORKFLOW_NAME, WorkflowPersistService, } from "src/app/common/service/workflow-persist/workflow-persist.service"; import { firstValueFrom } from "rxjs"; import { HubWorkflowDetailComponent } from "../../../../hub/component/workflow/detail/hub-workflow-detail.component"; import { ActionType, HubService } from "../../../../hub/service/hub.service"; import { DownloadService } from "src/app/dashboard/service/user/download/download.service"; import { formatSize } from "src/app/common/util/size-formatter.util"; import { DatasetService, DEFAULT_DATASET_NAME } from "../../../service/user/dataset/dataset.service"; import { NotificationService } from "../../../../common/service/notification/notification.service"; import { DASHBOARD_HUB_DATASET_RESULT_DETAIL, DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL, DASHBOARD_USER_DATASET, DASHBOARD_USER_PROJECT, DASHBOARD_USER_WORKSPACE, } from "../../../../app-routing.constant"; import { isDefined } from "../../../../common/util/predicate"; import { NzCardComponent } from "ng-zorro-antd/card"; import { NzRowDirective, NzColDirective } from "ng-zorro-antd/grid"; import { RouterLink } from "@angular/router"; import { NgIf, NgClass } from "@angular/common"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { FormsModule } from "@angular/forms"; import { UserAvatarComponent } from "../user-avatar/user-avatar.component"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { NzPopconfirmDirective } from "ng-zorro-antd/popconfirm"; @UntilDestroy() @Component({ selector: "texera-list-item", templateUrl: "./list-item.component.html", styleUrls: ["./list-item.component.scss"], imports: [ NzCardComponent, NzRowDirective, RouterLink, NzColDirective, NgIf, NgClass, ɵNzTransitionPatchDirective, NzIconDirective, NzSpaceCompactItemDirective, NzButtonComponent, FormsModule, UserAvatarComponent, NzWaveDirective, NzPopconfirmDirective, ], }) export class ListItemComponent implements OnChanges { private owners: number[] = []; public originalName: string = ""; public originalDescription: string | undefined = undefined; public disableDelete: boolean = false; @Input() currentUid: number | undefined; @ViewChild("nameInput") nameInput!: ElementRef; @ViewChild("descriptionInput") descriptionInput!: ElementRef; editingName = false; editingDescription = false; renderedDescription = ""; likeCount: number = 0; viewCount = 0; entryLink: string[] = []; size: number | undefined = 0; public iconType: string = ""; isLiked: boolean = false; @Input() isPrivateSearch = false; @Input() editable = false; private _entry?: DashboardEntry; hovering: boolean = false; @Input() get entry(): DashboardEntry { if (!this._entry) { throw new Error("entry property must be provided."); } return this._entry; } set entry(value: DashboardEntry) { this._entry = value; } @Output() checkboxChanged = new EventEmitter(); @Output() deleted = new EventEmitter(); @Output() duplicated = new EventEmitter(); @Output() refresh = new EventEmitter(); constructor( private modalService: NzModalService, private workflowPersistService: WorkflowPersistService, private datasetService: DatasetService, private modal: NzModalService, private hubService: HubService, private downloadService: DownloadService, private cdr: ChangeDetectorRef, private notificationService: NotificationService ) {} initializeEntry() { if (this.entry.type === "workflow") { if (typeof this.entry.id === "number") { this.disableDelete = !this.entry.workflow.isOwner; this.owners = this.entry.accessibleUserIds; if (this.currentUid !== undefined && this.owners.includes(this.currentUid)) { this.entryLink = [DASHBOARD_USER_WORKSPACE, String(this.entry.id)]; } else { this.entryLink = [DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL, String(this.entry.id)]; } this.size = this.entry.size; } this.iconType = "project"; } else if (this.entry.type === "project") { this.entryLink = [DASHBOARD_USER_PROJECT, String(this.entry.id)]; this.iconType = "container"; } else if (this.entry.type === "dataset") { if (typeof this.entry.id === "number") { this.disableDelete = !this.entry.dataset.isOwner; this.owners = this.entry.accessibleUserIds; if (this.currentUid !== undefined && this.owners.includes(this.currentUid)) { this.entryLink = [DASHBOARD_USER_DATASET, String(this.entry.id)]; } else { this.entryLink = [DASHBOARD_HUB_DATASET_RESULT_DETAIL, String(this.entry.id)]; } this.iconType = "database"; this.size = this.entry.size; } } else if (this.entry.type === "file") { // not sure where to redirect this.iconType = "folder-open"; } else { throw new Error("Unexpected type in DashboardEntry."); } this.likeCount = this.entry.likeCount; this.viewCount = this.entry.viewCount; this.isLiked = this.entry.isLiked; } private renderMarkdownPreview(text: string | undefined): void { const trimmed = (text ?? "").trim(); if (!trimmed) { this.renderedDescription = ""; return; } this.renderedDescription = trimmed .replace(/[#*_~`>|]/g, "") .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") // [text](url) → text .replace(/\s+/g, " ") .trim(); } ngOnChanges(changes: SimpleChanges): void { if (changes["entry"]) { this.initializeEntry(); this.renderMarkdownPreview(this.entry.description); } } onCheckboxChange(entry: DashboardEntry): void { entry.checked = !entry.checked; this.cdr.markForCheck(); this.checkboxChanged.emit(); } public async onClickOpenShareAccess(): Promise { let modal: NzModalRef | undefined; if (this.entry.type === "workflow") { modal = this.modalService.create({ nzContent: ShareAccessComponent, nzData: { writeAccess: this.entry.workflow.accessLevel === "WRITE", type: this.entry.type, id: this.entry.id, allOwners: await firstValueFrom(this.workflowPersistService.retrieveOwners()), inWorkspace: false, }, nzFooter: null, nzTitle: "Share this workflow with others", nzCentered: true, nzWidth: "700px", }); } else if (this.entry.type === "dataset") { modal = this.modalService.create({ nzContent: ShareAccessComponent, nzData: { writeAccess: this.entry.accessLevel === "WRITE", type: "dataset", id: this.entry.id, allOwners: await firstValueFrom(this.datasetService.retrieveOwners()), }, nzFooter: null, nzTitle: "Share this dataset with others", nzCentered: true, nzWidth: "700px", }); } if (modal) { modal.componentInstance?.refresh.pipe(untilDestroyed(this)).subscribe(() => { this.refresh.emit(); }); } } public onClickDownload = (): void => { if (!this.entry.id) return; if (this.entry.type === "workflow") { this.downloadService .downloadWorkflow(this.entry.id, this.entry.workflow.workflow.name) .pipe(untilDestroyed(this)) .subscribe(); } else if (this.entry.type === "dataset") { this.downloadService.downloadDataset(this.entry.id, this.entry.name).pipe(untilDestroyed(this)).subscribe(); } }; onEditName(): void { this.originalName = this.entry.name; this.editingName = true; setTimeout(() => { if (this.nameInput) { const inputElement = this.nameInput.nativeElement; const valueLength = inputElement.value.length; inputElement.focus(); inputElement.setSelectionRange(valueLength, valueLength); } }, 0); } onEditDescription(): void { if (!this.editable) return; this.originalDescription = this.entry.description; const modalRef = this.modalService.create({ nzTitle: "Edit Description", nzContent: MarkdownDescriptionComponent, nzData: { description: this.entry.description ?? "", }, nzFooter: null, nzWidth: "800px", }); modalRef.componentInstance?.descriptionChange.pipe(untilDestroyed(this)).subscribe(desc => { this.confirmUpdateCustomDescription(desc); modalRef.destroy(); }); } private updateProperty( updateMethod: (id: number, value: string) => any, propertyName: "name" | "description", newValue: string, originalValue: string | undefined ): void { if (!this.entry.id) { this.notificationService.error("Id is missing"); return; } updateMethod(this.entry.id, newValue) .pipe(untilDestroyed(this)) .subscribe({ next: () => { this.entry[propertyName] = newValue; // Dynamic property assignment if (propertyName === "description") { this.renderMarkdownPreview(newValue); } }, error: () => { this.notificationService.error("Update failed"); (this.entry as any)[propertyName] = originalValue ?? ""; // Fallback to original value if (propertyName === "description") { this.renderMarkdownPreview(originalValue); } this.setEditingState(propertyName, false); }, complete: () => { this.setEditingState(propertyName, false); }, }); } private setEditingState(propertyName: "name" | "description", state: boolean): void { if (propertyName === "name") { this.editingName = state; } else if (propertyName === "description") { this.editingDescription = state; } } public confirmUpdateCustomName(name: string): void { const newName = this.entry.type === "workflow" ? name || DEFAULT_WORKFLOW_NAME : name || DEFAULT_DATASET_NAME; if (this.entry.type === "workflow") { this.updateProperty( this.workflowPersistService.updateWorkflowName.bind(this.workflowPersistService), "name", newName, this.originalName ); } else if (this.entry.type === "dataset") { this.updateProperty( this.datasetService.updateDatasetName.bind(this.datasetService), "name", newName, this.originalName ); } } public confirmUpdateCustomDescription(description: string | undefined): void { const updatedDescription = description ?? ""; if (this.entry.type === "workflow") { this.updateProperty( this.workflowPersistService.updateWorkflowDescription.bind(this.workflowPersistService), "description", updatedDescription, this.originalDescription ); } else if (this.entry.type === "dataset") { this.updateProperty( this.datasetService.updateDatasetDescription.bind(this.datasetService), "description", updatedDescription, this.originalDescription ); } } formatTime(timestamp: number | undefined): string { if (timestamp === undefined) { return "Unknown"; // Return "Unknown" if the timestamp is undefined } const currentTime = new Date().getTime(); const timeDifference = currentTime - timestamp; const minutesAgo = Math.floor(timeDifference / (1000 * 60)); const hoursAgo = Math.floor(timeDifference / (1000 * 60 * 60)); const daysAgo = Math.floor(timeDifference / (1000 * 60 * 60 * 24)); const weeksAgo = Math.floor(daysAgo / 7); if (minutesAgo < 60) { return `${minutesAgo} minutes ago`; } else if (hoursAgo < 24) { return `${hoursAgo} hours ago`; } else if (daysAgo < 7) { return `${daysAgo} days ago`; } else if (weeksAgo < 4) { return `${weeksAgo} weeks ago`; } else { return new Date(timestamp).toLocaleDateString(); } } openDetailModal(wid: number | undefined): void { const modalRef = this.modal.create({ nzTitle: "Workflow Detail", nzContent: HubWorkflowDetailComponent, nzData: { wid: wid ?? 0, }, nzFooter: null, nzStyle: { width: "60%" }, nzBodyStyle: { maxHeight: "70vh", overflow: "auto" }, }); const instance = modalRef.componentInstance; if (instance) { if (wid !== undefined) { this.hubService .getCounts([this.entry.type], [wid], [ActionType.View]) .pipe(untilDestroyed(this)) .subscribe(counts => { const count = counts[0]; this.viewCount = (count?.counts.view ?? 0) + 1; // hacky fix to display view correctly }); } } } toggleLike(): void { const userId = this.currentUid; if (!isDefined(userId) || !isDefined(this.entry.id)) { return; } const entryId = this.entry.id!; if (this.isLiked) { this.hubService .postUnlike(entryId, this.entry.type) .pipe(untilDestroyed(this)) .subscribe((success: boolean) => { if (success) { this.isLiked = false; this.hubService .getCounts([this.entry.type], [entryId], [ActionType.Like]) .pipe(untilDestroyed(this)) .subscribe(counts => { this.likeCount = counts[0].counts.like ?? 0; }); } }); } else { this.hubService .postLike(entryId, this.entry.type) .pipe(untilDestroyed(this)) .subscribe((success: boolean) => { if (success) { this.isLiked = true; this.hubService .getCounts([this.entry.type], [entryId], [ActionType.Like]) .pipe(untilDestroyed(this)) .subscribe(counts => { this.likeCount = counts[0].counts.like ?? 0; }); } }); } } formatCount(count: number): string { if (count >= 1000) { return (count / 1000).toFixed(1) + "k"; } return count.toString(); } // alias for formatSize formatSize = formatSize; } ================================================ FILE: frontend/src/app/dashboard/component/user/markdown-description/markdown-description.component.html ================================================

    No description provided.

    ================================================ FILE: frontend/src/app/dashboard/component/user/markdown-description/markdown-description.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .preview-box { position: relative; margin-top: 5px; &.collapsed { overflow-y: hidden; } } .view-more-btn { margin-top: 6px; color: #0c7ec8; } .md-actions { display: flex; justify-content: flex-end; gap: 10px; } .md-split { display: flex; margin-top: 10px; gap: 16px; margin-bottom: 10px; max-height: 50vh; } .md-left { flex: 1; display: flex; flex-direction: column; } .md-toolbar { gap: 2px; padding: 4px 8px; background: #fafafa; border: 1px solid #d9d9d9; border-radius: 4px 4px 0 0; } .md-textarea { flex: 1; min-height: 300px; border-radius: 0 0 4px 4px; } .md-right { flex: 1; overflow-y: auto; padding: 10px; border: 1px solid #e8e8e8; border-radius: 4px; } .md-rendered { ::ng-deep { table { margin: 12px 0; th, td { border: 1px solid #d9d9d9; padding: 8px 12px; } th { background: #fafafa; } } blockquote { border-left: 4px solid #1890ff; padding-left: 12px; color: #595959; } code { background: #f5f5f5; padding: 2px 4px; border-radius: 3px; } } } ================================================ FILE: frontend/src/app/dashboard/component/user/markdown-description/markdown-description.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges, ViewChild, ElementRef, inject, AfterViewInit, OnDestroy, } from "@angular/core"; import { NZ_MODAL_DATA } from "ng-zorro-antd/modal"; import { MarkdownService } from "ngx-markdown"; import { NgIf, NgFor } from "@angular/common"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzInputDirective } from "ng-zorro-antd/input"; import { FormsModule } from "@angular/forms"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; const COLLAPSED_HEIGHT_PX = 320; const TOOLBAR = [ { icon: "bold", tip: "Bold", prefix: "**", suffix: "**", default: "bold" }, { icon: "italic", tip: "Italic", prefix: "_", suffix: "_", default: "italic" }, { icon: "strikethrough", tip: "Strikethrough", prefix: "~~", suffix: "~~", default: "text" }, { icon: "font-size", tip: "Heading", prefix: "### ", suffix: "", default: "Heading" }, { icon: "code", tip: "Code", prefix: "`", suffix: "`", default: "code" }, { icon: "block", tip: "Code Block", prefix: "\n```\n", suffix: "\n```\n", default: "code" }, { icon: "minus", tip: "Quote", prefix: "> ", suffix: "", default: "quote" }, { icon: "unordered-list", tip: "Bullet List", prefix: "- ", suffix: "", default: "item" }, { icon: "ordered-list", tip: "Numbered List", prefix: "1. ", suffix: "", default: "item" }, { icon: "link", tip: "Link", prefix: "[", suffix: "](url)", default: "text" }, { icon: "picture", tip: "Image", prefix: "![", suffix: "](url)", default: "alt text" }, { icon: "table", tip: "Table", prefix: "\n| Col 1 | Col 2 | Col 3 |\n| --- | --- | --- |\n| ", suffix: " | | |\n", default: "", }, { icon: "line", tip: "Divider", prefix: "\n---\n", suffix: "", default: "" }, ] as const; @Component({ selector: "texera-markdown-description", templateUrl: "./markdown-description.component.html", styleUrls: ["./markdown-description.component.scss"], imports: [ NgIf, NzSpaceCompactItemDirective, NzButtonComponent, ɵNzTransitionPatchDirective, NzIconDirective, NgFor, NzTooltipDirective, NzInputDirective, FormsModule, NzWaveDirective, ], }) export class MarkdownDescriptionComponent implements OnInit, OnChanges, AfterViewInit, OnDestroy { private modalData = inject(NZ_MODAL_DATA, { optional: true }); private markdownRenderSequence = 0; private resizeObserver?: ResizeObserver; @Input() description = ""; @Input() editable = false; @Input() enableViewMore = false; @Output() descriptionChange = new EventEmitter(); @ViewChild("textarea") textareaRef!: ElementRef; @ViewChild("previewBox") previewBoxRef?: ElementRef; currentMode: "preview" | "edit" = "preview"; editingContent = ""; renderedDescription = ""; isExpanded = false; hasOverflow = false; readonly toolbar = TOOLBAR; readonly COLLAPSED_HEIGHT_PX = COLLAPSED_HEIGHT_PX; constructor(private markdownService: MarkdownService) {} ngOnInit(): void { if (this.modalData) { this.description = this.modalData.description ?? ""; this.editable = true; } this.currentMode = this.modalData ? "edit" : "preview"; this.editingContent = this.description; this.renderMarkdown(this.description); } ngOnChanges(changes: SimpleChanges): void { if (changes["description"] && !changes["description"].firstChange) { if (this.currentMode === "edit") { return; } this.editingContent = this.description; this.renderMarkdown(this.description); } } enterEditMode(): void { if (!this.editable) { return; } this.editingContent = this.description; this.renderMarkdown(this.description); this.currentMode = "edit"; } save(): void { this.description = this.editingContent; this.descriptionChange.emit(this.description); this.renderMarkdown(this.description); this.currentMode = this.modalData ? "edit" : "preview"; } cancel(): void { this.editingContent = this.description; this.renderMarkdown(this.description); this.currentMode = "preview"; } insert(action: { prefix: string; suffix: string; default: string }): void { const textarea = this.textareaRef.nativeElement; const selectionStart = textarea.selectionStart; const selectionEnd = textarea.selectionEnd; const selectedText = this.editingContent.substring(selectionStart, selectionEnd) || action.default; const textBefore = this.editingContent.substring(0, selectionStart); const textAfter = this.editingContent.substring(selectionEnd); this.editingContent = textBefore + action.prefix + selectedText + action.suffix + textAfter; this.renderMarkdown(this.editingContent); requestAnimationFrame(() => textarea.focus()); } renderMarkdown(text: string): void { const currentRenderSequence = ++this.markdownRenderSequence; if (!text?.trim()) { this.renderedDescription = ""; this.scheduleOverflowCheck(); return; } Promise.resolve(this.markdownService.parse(text)).then(renderedDescription => { if (currentRenderSequence !== this.markdownRenderSequence) { return; } this.renderedDescription = renderedDescription; this.scheduleOverflowCheck(); }); } toggleViewMore(): void { this.isExpanded = !this.isExpanded; } ngAfterViewInit(): void { if (this.enableViewMore && this.previewBoxRef) { this.resizeObserver = new ResizeObserver(() => this.scheduleOverflowCheck()); this.resizeObserver.observe(this.previewBoxRef.nativeElement); } } ngOnDestroy(): void { this.resizeObserver?.disconnect(); } private scheduleOverflowCheck(): void { if (!this.enableViewMore) { this.hasOverflow = false; this.isExpanded = false; return; } requestAnimationFrame(() => { if (!this.previewBoxRef) return; this.hasOverflow = this.previewBoxRef.nativeElement.scrollHeight > COLLAPSED_HEIGHT_PX; }); } } ================================================ FILE: frontend/src/app/dashboard/component/user/search/search.component.html ================================================
    ================================================ FILE: frontend/src/app/dashboard/component/user/search/search.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .search-container { width: 100%; height: 100%; display: flex; flex-direction: column; .search-section { width: 100%; height: 76px; display: flex; align-items: center; gap: 10px; padding: 0 24px; .filters { flex-grow: 1; display: flex; align-items: center; justify-content: space-between; .left-controls { display: flex; align-items: center; gap: 10px; button { height: 32px; padding: 0 12px; font-size: 14px; line-height: 1.5; display: flex; align-items: center; justify-content: center; border-radius: 4px; } } } } .search-result { width: 100%; flex-grow: 1; } .selected { background-color: #1890ff; color: #fff; border-color: #1890ff; } } ================================================ FILE: frontend/src/app/dashboard/component/user/search/search.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { AfterViewInit, ChangeDetectorRef, Component, ViewChild } from "@angular/core"; import { SearchService } from "../../../service/user/search.service"; import { FiltersComponent } from "../filters/filters.component"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { SearchResultsComponent } from "../search-results/search-results.component"; import { SortMethod } from "../../../type/sort-method"; import { Location, NgClass } from "@angular/common"; import { ActivatedRoute } from "@angular/router"; import { UserService } from "../../../../common/service/user/user.service"; import { firstValueFrom } from "rxjs"; import { map } from "rxjs/operators"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { SortButtonComponent } from "../sort-button/sort-button.component"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; @UntilDestroy() @Component({ selector: "texera-search", templateUrl: "./search.component.html", styleUrls: ["./search.component.scss"], imports: [ NzSpaceCompactItemDirective, NzButtonComponent, ɵNzTransitionPatchDirective, NzIconDirective, SortButtonComponent, NzWaveDirective, NgClass, FiltersComponent, SearchResultsComponent, ], }) export class SearchComponent implements AfterViewInit { public searchParam: string = ""; sortMethod = SortMethod.EditTimeDesc; lastSortMethod: SortMethod | null = null; private isLogin = this.userService.isLogin(); private includePublic = true; currentUid = this.userService.getCurrentUser()?.uid; searchKeywords: string[] = []; selectedType: "project" | "workflow" | "dataset" | null = null; lastSelectedType: "project" | "workflow" | "dataset" | null = null; public masterFilterList: ReadonlyArray = []; @ViewChild(SearchResultsComponent) searchResultsComponent?: SearchResultsComponent; private _filters?: FiltersComponent; @ViewChild(FiltersComponent) get filters(): FiltersComponent { if (this._filters) { return this._filters; } throw new Error("Property cannot be accessed before it is initialized."); } set filters(value: FiltersComponent) { value.masterFilterListChange.pipe(untilDestroyed(this)).subscribe({ next: () => this.search() }); this._filters = value; } constructor( private location: Location, private searchService: SearchService, private userService: UserService, private activatedRoute: ActivatedRoute, private cdr: ChangeDetectorRef ) { this.userService .userChanged() .pipe(untilDestroyed(this)) .subscribe(() => { this.isLogin = this.userService.isLogin(); this.currentUid = this.userService.getCurrentUser()?.uid; }); } ngAfterViewInit() { this.activatedRoute.queryParams.pipe(untilDestroyed(this)).subscribe(params => { const keyword = params["q"]; if (keyword) { this.searchParam = keyword; this.updateMasterFilterList(); } this.searchKeywords = this.filters.getSearchKeywords(); this.cdr.detectChanges(); }); } async search(): Promise { const sameList = this.filters.masterFilterList.length === this.masterFilterList.length && this.filters.masterFilterList.every((v, i) => v === this.masterFilterList[i]); if (sameList && this.sortMethod === this.lastSortMethod && this.selectedType === this.lastSelectedType) { // If the filter lists are the same, do no make the same request again. return; } this.masterFilterList = this.filters.masterFilterList; this.lastSortMethod = this.sortMethod; this.lastSelectedType = this.selectedType; if (!this.searchResultsComponent) { throw new Error("searchResultsComponent is undefined."); } this.searchResultsComponent.reset((start, count) => { return firstValueFrom( this.searchService .executeSearch( this.filters.getSearchKeywords(), this.filters.getSearchFilterParameters(), start, count, this.selectedType, this.sortMethod, this.isLogin, this.includePublic ) .pipe(map(({ entries, more }) => ({ entries, more }))) ); }); await this.searchResultsComponent.loadMore(); } filterByType(type: "project" | "workflow" | "dataset" | null): void { this.selectedType = type; this.search(); } goBack(): void { this.location.back(); } updateMasterFilterList() { this.filters.masterFilterList = this.searchParam.split(/\s+/); } } ================================================ FILE: frontend/src/app/dashboard/component/user/search-bar/search-bar.component.html ================================================ ================================================ FILE: frontend/src/app/dashboard/component/user/search-bar/search-bar.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .nz-input-group { display: flex; align-items: center; margin: 0; } input[nz-input] { border: none; outline: none; font-size: 17px; padding: 0 5px; } .nz-prefix-icon { color: #888; } ================================================ FILE: frontend/src/app/dashboard/component/user/search-bar/search-bar.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component } from "@angular/core"; import { Router } from "@angular/router"; import { SearchService } from "../../../service/user/search.service"; import { SearchFilterParameters } from "../../../type/search-filter-parameters"; import { SortMethod } from "../../../type/sort-method"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { SearchResult, SearchResultItem } from "../../../type/search-result"; import { DashboardEntry } from "../../../type/dashboard-entry"; import { Observable, of, Subject } from "rxjs"; import { debounceTime, switchMap } from "rxjs/operators"; import { UserService } from "../../../../common/service/user/user.service"; import { DASHBOARD_SEARCH } from "../../../../app-routing.constant"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzInputGroupComponent, NzInputDirective } from "ng-zorro-antd/input"; import { FormsModule } from "@angular/forms"; import { NzAutocompleteTriggerDirective, NzAutocompleteComponent } from "ng-zorro-antd/auto-complete"; @UntilDestroy() @Component({ selector: "texera-search-bar", templateUrl: "./search-bar.component.html", styleUrls: ["./search-bar.component.scss"], imports: [ ɵNzTransitionPatchDirective, NzSpaceCompactItemDirective, NzInputGroupComponent, NzInputDirective, FormsModule, NzAutocompleteTriggerDirective, NzAutocompleteComponent, ], }) export class SearchBarComponent { private includePublic = true; public searchParam: string = ""; public listOfResult: string[] = []; private searchSubject = new Subject(); isLogin = this.userService.isLogin(); private params: SearchFilterParameters = { createDateStart: null, createDateEnd: null, modifiedDateStart: null, modifiedDateEnd: null, owners: [], ids: [], operators: [], projectIds: [], }; private searchCache = new Map(); private queryOrder: string[] = []; constructor( private router: Router, private searchService: SearchService, private userService: UserService ) { this.userService .userChanged() .pipe(untilDestroyed(this)) .subscribe(() => { this.isLogin = this.userService.isLogin(); }); this.searchSubject .pipe( debounceTime(200), switchMap(query => this.getSearchResults(query)), untilDestroyed(this) ) .subscribe((results: string[]) => { this.listOfResult = results; }); } // Method to get search results with caching and limit cache size private getSearchResults(query: string): Observable { if (this.searchCache.has(query)) { return of(this.searchCache.get(query)!); } else { const searchObservable = this.searchService.search( [query], this.params, 0, 5, null, SortMethod.NameAsc, this.isLogin, this.includePublic ); return searchObservable.pipe( switchMap((result: SearchResult) => { const uniqueResults = Array.from(new Set(result.results.map(item => this.convertToName(item)))); this.addToCache(query, uniqueResults); return of(uniqueResults); }) ); } } private addToCache(query: string, results: string[]): void { if (this.queryOrder.length >= 20) { const oldestQuery = this.queryOrder.shift(); this.searchCache.delete(oldestQuery!); } this.queryOrder.push(query); this.searchCache.set(query, results); } onSearchInputChange(query: string): void { if (query) { this.searchSubject.next(query); } else { this.listOfResult = []; } } performSearch(keyword: string) { this.router.navigate([DASHBOARD_SEARCH], { queryParams: { q: keyword } }); } convertToName(resultItem: SearchResultItem): string { if (resultItem.workflow) { return new DashboardEntry(resultItem.workflow).name; } else if (resultItem.project) { return new DashboardEntry(resultItem.project).name; } else if (resultItem.file) { return new DashboardEntry(resultItem.file).name; } else if (resultItem.dataset) { return new DashboardEntry(resultItem.dataset).name; } else { throw new Error("Unexpected type in SearchResult."); } } } ================================================ FILE: frontend/src/app/dashboard/component/user/search-results/search-results.component.html ================================================
    ================================================ FILE: frontend/src/app/dashboard/component/user/search-results/search-results.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ @import "../../section-style"; .load-more { text-align: center; margin-bottom: 12px; margin-top: 12px; height: 32px; line-height: 32px; } nz-sider { background: rgb(255, 255, 255); } nz-content { background: rgb(255, 255, 255); } .resource-type-icon { font-size: 30px; margin-top: 10px; margin-left: 5px; } .dataset-search-bar { margin-left: 10px; } .dataset-list-item { margin-bottom: 10px; min-height: 70px; padding: 5px 0 5px 0; .dataset-item-checkbox { margin: 8px; } .dataset-item-meta-title { display: flex; align-items: center; .dataset-name { font-size: 20px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; text-align: center; margin-bottom: 0; color: inherit; text-decoration: none; } .dataset-name:hover { cursor: pointer; } i { position: relative; font-size: 17px; } i.dataset-is-owner-icon { margin-left: 7px; } } .dataset-item-meta-description { display: flex; align-items: center; padding: 2px 8px 2px 10px; margin-bottom: 5px; .dataset-description { font-size: 13px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; display: inline-block; min-width: 17ch; border: 0 none; outline: none; &:hover { cursor: pointer; box-shadow: 0 0 0 1px rgb(202, 202, 202); } } .dataset-editable-description { margin-bottom: 5px; display: inline-block; min-width: 17ch; border: 0 none; outline: none; box-shadow: 0 0 0 2px #007bff; } } } .subsection-grid-container { min-width: 100%; width: 100%; min-height: 100%; height: 100%; } .ant-btn-icon-only { margin-left: 5px; margin-right: 5px; } .metadata-container { span { margin: 0 1rem 0 0; // add space to the right } } ================================================ FILE: frontend/src/app/dashboard/component/user/search-results/search-results.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, EventEmitter, Input, Output } from "@angular/core"; import { DashboardEntry } from "../../../type/dashboard-entry"; import { UserService } from "../../../../common/service/user/user.service"; import { NzCardComponent } from "ng-zorro-antd/card"; import { ɵɵCdkVirtualScrollViewport, ɵɵCdkFixedSizeVirtualScroll } from "@angular/cdk/overlay"; import { NzListComponent } from "ng-zorro-antd/list"; import { NgFor, NgIf } from "@angular/common"; import { ListItemComponent } from "../list-item/list-item.component"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; export type LoadMoreFunction = (start: number, count: number) => Promise<{ entries: DashboardEntry[]; more: boolean }>; @Component({ selector: "texera-search-results", templateUrl: "./search-results.component.html", styleUrls: ["./search-results.component.scss"], imports: [ NzCardComponent, ɵɵCdkVirtualScrollViewport, ɵɵCdkFixedSizeVirtualScroll, NzListComponent, NgFor, ListItemComponent, NgIf, NzSpaceCompactItemDirective, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, ], }) export class SearchResultsComponent { loadMoreFunction: LoadMoreFunction | null = null; loading = false; more = false; entries: ReadonlyArray = []; private resetCounter = 0; @Input() isPrivateSearch = false; @Input() showResourceTypes = false; @Input() public pid: number = 0; @Input() editable = false; @Input() searchKeywords: string[] = []; @Input() currentUid: number | undefined; @Output() deleted = new EventEmitter(); @Output() duplicated = new EventEmitter(); @Output() modified = new EventEmitter(); @Output() notifyWorkflow = new EventEmitter(); @Output() refresh = new EventEmitter(); constructor(private userService: UserService) {} getUid(): number | undefined { return this.userService.getCurrentUser()?.uid; } reset(loadMoreFunction: LoadMoreFunction): void { this.entries = []; this.loadMoreFunction = loadMoreFunction; this.resetCounter++; } async loadMore(): Promise { if (!this.loadMoreFunction) { throw new Error("This is an empty list and cannot load more entries."); } this.loading = true; try { const originalResetCounter = this.resetCounter; const results = await this.loadMoreFunction(this.entries.length, 20); if (this.resetCounter !== originalResetCounter) { return; } this.entries = [...this.entries, ...results.entries]; this.more = results.more; } finally { this.loading = false; } } onEntryCheckboxChange(): void { const allSelected = this.entries.every(entry => entry.checked); if (allSelected) { this.notifyWorkflow.emit(); } } selectAll(): void { this.entries.forEach(entry => (entry.checked = true)); } clearAllSelections() { this.entries.forEach(entry => (entry.checked = false)); } } ================================================ FILE: frontend/src/app/dashboard/component/user/share-access/share-access.component.html ================================================
    E-mails
    {{ email }}

    Access Level:

    • OWNER {{ owner }}
    • {{ entry.email }} ({{ entry.name }})
    ================================================ FILE: frontend/src/app/dashboard/component/user/share-access/share-access.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .input-group { display: flex; align-items: center; } .input-group input[nz-input] { flex: 1; margin-right: 8px; } .add-button { margin-left: 8px; white-space: nowrap; padding: 8px 18px; height: 25px; font-size: 14px; line-height: 1; } .access-button-group { display: flex; flex-direction: row; justify-content: space-evenly; align-items: center; } .access-button { display: flex; flex-direction: column; align-items: center; justify-content: space-evenly; height: 150px; width: 300px; padding: 20px; margin-bottom: 24px; border-radius: 10px; } .button-icon { font-size: 40px; } .button-text > p { font-size: 12px; font-weight: lighter; display: flex; flex-direction: column; align-items: center; justify-content: space-between; } .button-text-header { font-size: 25px; font-weight: bold; } ================================================ FILE: frontend/src/app/dashboard/component/user/share-access/share-access.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, EventEmitter, inject, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder, FormControl, FormGroup, Validators, FormsModule, ReactiveFormsModule } from "@angular/forms"; import { ShareAccessService } from "../../../service/user/share-access/share-access.service"; import { Privilege, ShareAccess } from "../../../type/share-access.interface"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { UserService } from "../../../../common/service/user/user.service"; import { GmailService } from "../../../../common/service/gmail/gmail.service"; import { NZ_MODAL_DATA, NzModalRef, NzModalService } from "ng-zorro-antd/modal"; import { NotificationService } from "../../../../common/service/notification/notification.service"; import { HttpErrorResponse } from "@angular/common/http"; import { NzMessageService } from "ng-zorro-antd/message"; import { DatasetService } from "../../../service/user/dataset/dataset.service"; import { WorkflowPersistService } from "src/app/common/service/workflow-persist/workflow-persist.service"; import { WorkflowActionService } from "src/app/workspace/service/workflow-graph/model/workflow-action.service"; import { NgIf, NgFor } from "@angular/common"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzCardComponent } from "ng-zorro-antd/card"; import { NzRowDirective, NzColDirective } from "ng-zorro-antd/grid"; import { NzFormItemComponent, NzFormLabelComponent, NzFormControlComponent } from "ng-zorro-antd/form"; import { NzInputDirective } from "ng-zorro-antd/input"; import { NzAutocompleteTriggerDirective, NzAutocompleteComponent } from "ng-zorro-antd/auto-complete"; import { NzTagComponent } from "ng-zorro-antd/tag"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; @UntilDestroy() @Component({ selector: "texera-share-access", templateUrl: "share-access.component.html", styleUrls: ["./share-access.component.scss"], imports: [ NgIf, NzSpaceCompactItemDirective, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, NzIconDirective, FormsModule, ReactiveFormsModule, NzCardComponent, NzRowDirective, NzFormItemComponent, NzColDirective, NzFormLabelComponent, NzFormControlComponent, NzInputDirective, NzAutocompleteTriggerDirective, NzAutocompleteComponent, NgFor, NzTagComponent, NzTooltipDirective, ], }) export class ShareAccessComponent implements OnInit, OnDestroy { readonly nzModalData = inject(NZ_MODAL_DATA); readonly type: string = this.nzModalData.type; readonly id: number = this.nzModalData.id; readonly allOwners: string[] = this.nzModalData.allOwners; readonly inWorkspace: boolean = this.nzModalData.inWorkspace; public validateForm: FormGroup; public accessList: ReadonlyArray = []; public owner: string = ""; public filteredOwners: Array = []; public ownerSearchValue?: string; public emailTags: string[] = []; currentEmail: string | undefined = ""; isPublic: boolean | null = null; private shouldRefresh = false; @Output() refresh = new EventEmitter(); constructor( private accessService: ShareAccessService, private formBuilder: FormBuilder, private userService: UserService, private gmailService: GmailService, private notificationService: NotificationService, private message: NzMessageService, private modalService: NzModalService, private workflowPersistService: WorkflowPersistService, private datasetService: DatasetService, private workflowActionService: WorkflowActionService, private modalRef: NzModalRef ) { this.validateForm = this.formBuilder.group({ email: [null, Validators.email], accessLevel: ["WRITE"], }); this.currentEmail = this.userService.getCurrentUser()?.email; } get hasWriteAccess(): boolean { if (!this.currentEmail) { return false; } if (this.currentEmail === this.owner) { return true; } const currentUserAccess = this.accessList.find(entry => entry.email === this.currentEmail); return currentUserAccess?.privilege === Privilege.WRITE; } ngOnInit(): void { this.accessService .getAccessList(this.type, this.id) .pipe(untilDestroyed(this)) .subscribe(access => (this.accessList = access)); this.accessService .getOwner(this.type, this.id) .pipe(untilDestroyed(this)) .subscribe(name => { this.owner = name; }); if (this.type === "workflow") { this.workflowPersistService .getWorkflowIsPublished(this.id) .pipe(untilDestroyed(this)) .subscribe(dashboardWorkflow => { this.isPublic = dashboardWorkflow === "Public"; }); } else if (this.type === "dataset") { this.datasetService .getDataset(this.id) .pipe(untilDestroyed(this)) .subscribe(dashboardDataset => { this.isPublic = dashboardDataset.dataset.isPublic; }); } } ngOnDestroy(): void { if (this.shouldRefresh) { this.refresh.emit(); } } public handleInputConfirm(event?: Event): void { if (event) { event.preventDefault(); } const emailInput = this.validateForm.get("email")?.value; if (emailInput) { const emailArray: string[] = emailInput.split(/[\s,;]+/); emailArray.forEach(email => { if (email) { const emailControl = new FormControl(email, Validators.email); if (!emailControl.errors && !this.emailTags.includes(email)) { this.emailTags.push(email); } else if (this.emailTags.includes(email)) { this.message.error(`${email} is already in the tags`); } else { this.message.error(`${email} is not a valid email`); } } }); } this.validateForm.get("email")?.reset(); } public removeEmailTag(email: string): void { this.emailTags = this.emailTags.filter(tag => tag !== email); } public grantAccess(): void { this.handleInputConfirm(); if (this.emailTags.length > 0) { this.emailTags.forEach(email => { let message = `${this.userService.getCurrentUser()?.email} shared a ${this.type} with you`; if (this.type !== "computing-unit") message += `, access the ${this.type} at ${location.origin}/dashboard/user/workflow/${this.id}`; this.accessService .grantAccess(this.type, this.id, email, this.validateForm.value.accessLevel) .pipe(untilDestroyed(this)) .subscribe({ next: () => { this.notificationService.success(this.type + " shared with " + email + " successfully."); this.gmailService.sendEmail( "Texera: " + this.userService.getCurrentUser()?.email + " shared a " + this.type + " with you", message, email ); this.ngOnInit(); }, error: (error: unknown) => { if (error instanceof HttpErrorResponse) { this.notificationService.error(error.error.message); } }, }); }); this.emailTags = []; } } public onPaste(event: ClipboardEvent): void { event.preventDefault(); const pasteData = event.clipboardData?.getData("text"); if (pasteData) { const currentEmailValue = this.validateForm.get("email")?.value || ""; // concaste new emails and old emails const newValue = currentEmailValue + pasteData; this.validateForm.get("email")?.setValue(newValue); this.handleInputConfirm(); } } public onChange(value: string): void { if (value === null || value === undefined) { this.filteredOwners = []; } else { this.filteredOwners = this.allOwners.filter(owner => owner.toLowerCase().indexOf(value.toLowerCase()) !== -1); } } public verifyRevokeAccess(userToRemove: string): void { const isRevokingOwnAccess = userToRemove === this.userService.getCurrentUser()?.email; const modalTitle = isRevokingOwnAccess ? "Revoke Your Access" : "Revoke Access"; const modalContent = isRevokingOwnAccess ? `Are you sure you want to revoke your own access to this ${this.type}? You will no longer be able to view or edit it.` : `Are you sure you want to revoke ${userToRemove}'s access to this ${this.type}?`; const modal: NzModalRef = this.modalService.create({ nzTitle: modalTitle, nzContent: modalContent, nzFooter: [ { label: "Cancel", onClick: () => modal.close(), }, { label: "Revoke", type: "primary", danger: true, onClick: () => { this.revokeAccess(userToRemove); modal.close(); }, }, ], }); } private revokeAccess(userToRemove: string): void { this.accessService .revokeAccess(this.type, this.id, userToRemove) .pipe(untilDestroyed(this)) .subscribe({ next: () => { if (userToRemove == this.userService.getCurrentUser()?.email) { this.shouldRefresh = true; this.modalRef.close({ userRevokedOwnAccess: true }); } this.ngOnInit(); }, error: (error: unknown) => { if (error instanceof HttpErrorResponse) { this.notificationService.error(error.error.message); } }, }); } public changeAccessLevel(email: string, newPrivilege: string): void { const isOwnAccess = email === this.currentEmail; const currentUserAccess = this.accessList.find(entry => entry.email === email); const isDowngrade = currentUserAccess?.privilege === Privilege.WRITE && newPrivilege === "READ"; if (isOwnAccess && isDowngrade) { const modal: NzModalRef = this.modalService.create({ nzTitle: "Downgrade Your Access", nzContent: `Are you sure you want to change your own access to READ? You will no longer be able to edit this ${this.type} or manage access.`, nzFooter: [ { label: "Cancel", onClick: () => { modal.close(); this.ngOnInit(); }, }, { label: "Confirm", type: "primary", danger: true, onClick: () => { this.applyAccessLevelChange(email, newPrivilege); modal.close(); }, }, ], }); } else { this.applyAccessLevelChange(email, newPrivilege); } } private applyAccessLevelChange(email: string, newPrivilege: string): void { this.accessService .grantAccess(this.type, this.id, email, newPrivilege) .pipe(untilDestroyed(this)) .subscribe({ next: () => { this.notificationService.success(`Access level for ${email} changed to ${newPrivilege}.`); this.ngOnInit(); }, error: (error: unknown) => { if (error instanceof HttpErrorResponse) { this.notificationService.error(error.error.message); } this.ngOnInit(); }, }); } public verifyPublish(): void { if (!this.isPublic) { const modal: NzModalRef = this.modalService.create({ nzTitle: "Notice", nzContent: `Publishing your ${this.type} would grant all Texera users read access to your ${this.type} along with the right to clone your work.`, nzFooter: [ { label: "Cancel", onClick: () => modal.close(), }, { label: "Publish", type: "primary", onClick: () => { if (this.type === "workflow") { this.publishWorkflow(); if (this.inWorkspace) { this.workflowActionService.setWorkflowIsPublished(1); } } else if (this.type === "dataset") { this.publishDataset(); } modal.close(); }, }, ], }); } } public verifyUnpublish(): void { if (this.isPublic) { const modal: NzModalRef = this.modalService.create({ nzTitle: "Notice", nzContent: `All other users would lose access to your ${this.type} if you unpublish it.`, nzFooter: [ { label: "Cancel", onClick: () => modal.close(), }, { label: "Unpublish", type: "primary", onClick: () => { if (this.type === "workflow") { this.unpublishWorkflow(); if (this.inWorkspace) { this.workflowActionService.setWorkflowIsPublished(0); } } else if (this.type === "dataset") { this.unpublishDataset(); } modal.close(); }, }, ], }); } } public publishWorkflow(): void { if (!this.isPublic) { this.workflowPersistService .updateWorkflowIsPublished(this.id, true) .pipe(untilDestroyed(this)) .subscribe({ next: () => { this.isPublic = true; this.notificationService.success("Workflow published successfully"); }, error: (error: unknown) => { if (error instanceof HttpErrorResponse) { this.notificationService.error(error.error.message); } }, }); } } public unpublishWorkflow(): void { if (this.isPublic) { this.workflowPersistService .updateWorkflowIsPublished(this.id, false) .pipe(untilDestroyed(this)) .subscribe({ next: () => { this.isPublic = false; this.notificationService.success("Workflow unpublished successfully"); }, error: (error: unknown) => { if (error instanceof HttpErrorResponse) { this.notificationService.error(error.error.message); } }, }); } } public publishDataset(): void { if (!this.isPublic) { this.datasetService .updateDatasetPublicity(this.id) .pipe(untilDestroyed(this)) .subscribe({ next: (res: Response) => { this.isPublic = true; this.notificationService.success("Dataset published successfully"); }, error: (error: unknown) => { if (error instanceof HttpErrorResponse) { this.notificationService.error(error.error.message); } }, }); } } public unpublishDataset(): void { if (this.isPublic) { this.datasetService .updateDatasetPublicity(this.id) .pipe(untilDestroyed(this)) .subscribe({ next: (res: Response) => { this.isPublic = false; this.notificationService.success("Dataset unpublished successfully"); }, error: (error: unknown) => { if (error instanceof HttpErrorResponse) { this.notificationService.error(error.error.message); } }, }); } } } ================================================ FILE: frontend/src/app/dashboard/component/user/sort-button/sort-button.component.html ================================================
    ================================================ FILE: frontend/src/app/dashboard/component/user/sort-button/sort-button.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ @import "../user-workflow/user-workflow.component.scss"; ================================================ FILE: frontend/src/app/dashboard/component/user/sort-button/sort-button.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, EventEmitter, Output } from "@angular/core"; import { SortMethod } from "../../../type/sort-method"; import { NzDropdownADirective, NzDropdownDirective, NzDropdownMenuComponent } from "ng-zorro-antd/dropdown"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzMenuDirective, NzMenuItemComponent } from "ng-zorro-antd/menu"; @Component({ selector: "texera-sort-button", templateUrl: "./sort-button.component.html", styleUrls: ["./sort-button.component.scss"], imports: [ NzDropdownADirective, NzDropdownDirective, NzSpaceCompactItemDirective, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, NzIconDirective, NzDropdownMenuComponent, NzMenuDirective, NzMenuItemComponent, ], }) export class SortButtonComponent { @Output() public sortMethodChange = new EventEmitter(); public sortMethod = SortMethod.EditTimeDesc; public lastSort(): void { this.sortMethod = SortMethod.EditTimeDesc; this.sortMethodChange.emit(this.sortMethod); } public dateSort(): void { this.sortMethod = SortMethod.CreateTimeDesc; this.sortMethodChange.emit(this.sortMethod); } public ascSort(): void { this.sortMethod = SortMethod.NameAsc; this.sortMethodChange.emit(this.sortMethod); } public dscSort(): void { this.sortMethod = SortMethod.NameDesc; this.sortMethodChange.emit(this.sortMethod); } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-avatar/user-avatar.component.html ================================================ ================================================ FILE: frontend/src/app/dashboard/component/user/user-avatar/user-avatar.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .owner-badge { vertical-align: top; } ================================================ FILE: frontend/src/app/dashboard/component/user/user-avatar/user-avatar.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { HttpClientModule } from "@angular/common/http"; import { UserAvatarComponent } from "./user-avatar.component"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { NzAvatarModule } from "ng-zorro-antd/avatar"; import { UserService } from "../../../../common/service/user/user.service"; import { StubUserService } from "../../../../common/service/user/stub-user.service"; import { commonTestProviders } from "../../../../common/testing/test-utils"; describe("UserAvatarComponent", () => { let component: UserAvatarComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [UserAvatarComponent, HttpClientModule, HttpClientTestingModule, NzAvatarModule], providers: [{ provide: UserService, useClass: StubUserService }, ...commonTestProviders], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(UserAvatarComponent); component = fixture.componentInstance; component.userName = "fake Texera user"; fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/dashboard/component/user/user-avatar/user-avatar.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, Input, OnChanges } from "@angular/core"; import { UserService } from "../../../../common/service/user/user.service"; import { Observable, of } from "rxjs"; import { NgIf, AsyncPipe } from "@angular/common"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzAvatarComponent } from "ng-zorro-antd/avatar"; @Component({ selector: "texera-user-avatar", templateUrl: "./user-avatar.component.html", styleUrls: ["./user-avatar.component.scss"], imports: [NgIf, ɵNzTransitionPatchDirective, NzIconDirective, NzAvatarComponent, AsyncPipe], }) /** * UserAvatarComponent is used to show the avatar of a user * The avatar of a Google user will be its Google profile picture * The avatar of a normal user will be a default one with the initial */ export class UserAvatarComponent implements OnChanges { @Input() googleAvatar?: string; @Input() userName?: string; @Input() userColor?: string; @Input() isOwner: Boolean = false; avatarUrl$: Observable = of(undefined); constructor(private userService: UserService) {} ngOnChanges(): void { if (this.googleAvatar) { this.avatarUrl$ = this.userService.getAvatar(this.googleAvatar); } else { this.avatarUrl$ = of(undefined); } } /** * abbreviates the name under 5 chars * @param userName */ public abbreviate(userName: string): string { if (userName.length <= 5) { return userName; } else { return userName.slice(0, 5); } } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.html ================================================
    #{{ unit.cuid }}
    {{ unit.name }}
    CPU
    Memory
    Created:
    {{ formatTime(unit.creationTime) }}
    CPU Limit:
    {{ unit.resource.cpuLimit }}
    Memory Limit:
    {{ unit.resource.memoryLimit }}

    CPU

    {{getCpuValue() | number:'1.4-4'}} / {{getCpuLimit()}} {{getCpuLimitUnit()}} ({{getCpuPercentage() | number:'1.1-1'}}%)

    RAM

    {{getMemoryValue() | number:'1.4-4'}} / {{getMemoryLimit()}} {{getMemoryLimitUnit()}} ({{getMemoryPercentage() | number:'1.1-1'}}%)

    GPU

    {{getGpuLimit()}} GPU(s)

    JVM Memory Size

    {{getJvmMemorySize()}}

    Shared Memory Size

    {{getSharedMemorySize()}}

    ================================================ FILE: frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ @import "../../../section-style"; @import "../../../dashboard.component.scss"; .computing-unit-list-item-card { padding: 3px; width: 100%; background-color: white; position: relative; min-height: 65px; height: auto; &:hover { background-color: #f0f0f0; .edit-button { display: flex; } &.resource-info { opacity: 0.3; } } } .computing-unit-list-item-card:hover .button-group { display: flex; background-color: transparent; } .edit-button { display: none; &:hover { display: block; background-color: #d9d9d9; } button { height: 25px; } } .type-icon { font-size: 30px; } .unit-id { padding: 6px; } .resource-name-group { min-width: 0; } .resource-name { font-size: 17px; font-weight: 600; cursor: pointer; text-decoration: none; } .resource-name:hover { text-decoration: underline; } .resource-info { font-size: 13px; color: grey; } .truncate-single-line { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; } .unit-name-edit-input { width: 100%; max-width: 200px; font-size: inherit; border: 1px solid #d9d9d9; border-radius: 2px; padding: 2px 6px; background: white; } .resource-metrics { width: 250px; display: grid; grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(1, 1fr); justify-content: start; align-items: center; gap: 5px; } .general-metric { display: flex; flex-direction: column; width: 100%; background-color: #f9fafb; border-radius: 3px; padding: 10px; gap: 3px; } .metrics-container { margin-right: 20px; } .metric-unit { color: #888; font-size: 0.9em; margin-left: 4px; } .metric-percentage { color: #555; font-size: 0.9em; margin-left: 6px; font-weight: 500; } .metric-name { font-size: 10px; margin: 0; } .metric-value { margin: 0; } .metric-item { display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 6px; height: 32px; width: 120px; min-width: 120px; padding: 0; border: none; flex-shrink: 0; /* Prevent shrinking */ } .metric-label { font-size: 10px; width: 45px; flex-shrink: 0; line-height: 1; text-align: center; } .metric-bar-wrapper { flex-grow: 1; width: 90px; min-width: 60px; display: flex; align-items: center; padding: 0; height: 8px; } .cpu-progress-bar, .memory-progress-bar { width: 100%; margin: 0 !important; padding: 0 !important; vertical-align: middle; } .button-group { display: none; position: absolute; height: 70px; min-width: 150px; right: 0; bottom: 0; justify-content: right; align-items: center; transition: none; z-index: 10; button { margin-right: 32px; transition: none; background-color: #e0e0e0; border: 1px solid #d0d0d0; border-radius: 8px; } button:hover { background-color: #c7c7c7; } } :host ::ng-deep .ant-progress { width: 100%; } :host ::ng-deep .ant-progress-inner { width: 100% !important; background-color: #cacaca !important; } :host ::ng-deep .ant-badge-status-dot { position: relative; top: -1px; vertical-align: middle; } ================================================ FILE: frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit-list-item/user-computing-unit-list-item.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild, ElementRef, } from "@angular/core"; import { ComputingUnitStatusService } from "../../../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; import { extractErrorMessage } from "../../../../../common/util/error"; import { NotificationService } from "../../../../../common/service/notification/notification.service"; import { NzModalService } from "ng-zorro-antd/modal"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { DashboardWorkflowComputingUnit, WorkflowComputingUnit, } from "../../../../../common/type/workflow-computing-unit"; import { WorkflowComputingUnitManagingService } from "../../../../../common/service/computing-unit/workflow-computing-unit/workflow-computing-unit-managing.service"; import { ComputingUnitMetadataComponent, parseResourceUnit, parseResourceNumber, cpuResourceConversion, memoryResourceConversion, cpuPercentage, memoryPercentage, validateName, getComputingUnitBadgeColor, getComputingUnitStatusTooltip, getComputingUnitCpuStatus, getComputingUnitMemoryStatus, getComputingUnitCpuLimitUnit, } from "../../../../../common/util/computing-unit.util"; import { GuiConfigService } from "../../../../../common/service/gui-config.service"; import { ComputingUnitActionsService } from "../../../../../common/service/computing-unit/computing-unit-actions/computing-unit-actions.service"; import { NzCardComponent } from "ng-zorro-antd/card"; import { NzRowDirective, NzColDirective } from "ng-zorro-antd/grid"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NgIf, DecimalPipe } from "@angular/common"; import { NzBadgeComponent } from "ng-zorro-antd/badge"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzInputDirective } from "ng-zorro-antd/input"; import { NzPopoverDirective } from "ng-zorro-antd/popover"; import { NzProgressComponent } from "ng-zorro-antd/progress"; @UntilDestroy() @Component({ selector: "texera-user-computing-unit-list-item", templateUrl: "./user-computing-unit-list-item.component.html", styleUrls: ["./user-computing-unit-list-item.component.scss"], imports: [ NzCardComponent, NzRowDirective, NzColDirective, ɵNzTransitionPatchDirective, NzIconDirective, NzSpaceCompactItemDirective, NzButtonComponent, NgIf, NzBadgeComponent, NzTooltipDirective, NzInputDirective, NzPopoverDirective, NzProgressComponent, DecimalPipe, ], }) export class UserComputingUnitListItemComponent implements OnInit { private _entry?: DashboardWorkflowComputingUnit; editingNameOfUnit: number | null = null; editingUnitName: string = ""; gpuOptions: string[] = []; @Output() deleted = new EventEmitter(); @Input() get entry(): DashboardWorkflowComputingUnit { if (!this._entry) { throw new Error("entry property must be provided to UserComputingUnitListItemComponent."); } return this._entry; } set entry(value: DashboardWorkflowComputingUnit) { this._entry = value; } get unit(): WorkflowComputingUnit { if (!this.entry.computingUnit) { throw new Error( "Incorrect type of DashboardEntry provided to UserComputingUnitListItemComponent. Entry must be computing unit." ); } return this.entry.computingUnit; } constructor( private cdr: ChangeDetectorRef, private modalService: NzModalService, private notificationService: NotificationService, private computingUnitService: WorkflowComputingUnitManagingService, private computingUnitStatusService: ComputingUnitStatusService, private computingUnitActionsService: ComputingUnitActionsService, protected config: GuiConfigService ) {} ngOnInit(): void { this.computingUnitService .getComputingUnitLimitOptions() .pipe(untilDestroyed(this)) .subscribe({ next: ({ gpuLimitOptions }) => { this.gpuOptions = gpuLimitOptions ?? []; }, error: (err: unknown) => this.notificationService.error(`Failed to fetch resource options: ${extractErrorMessage(err)}`), }); } @ViewChild("unitNameInput") unitNameInputRef?: ElementRef; startEditingUnitName(entry: DashboardWorkflowComputingUnit): void { if (!entry.isOwner) { this.notificationService.error("Only owners can rename computing units"); return; } this.editingNameOfUnit = entry.computingUnit.cuid; this.editingUnitName = entry.computingUnit.name; // Force change detection and focus the input this.cdr.detectChanges(); setTimeout(() => { const input = document.querySelector(".unit-name-edit-input") as HTMLInputElement; if (input) { this.unitNameInputRef?.nativeElement.focus(); this.unitNameInputRef?.nativeElement.select(); } }, 0); } confirmUpdateUnitName(cuid: number, newName: string): void { const trimmedName = newName.trim(); const validationError = validateName(trimmedName); if (validationError) { this.notificationService.error(validationError); this.cancelEditingUnitName(); return; } this.computingUnitService .renameComputingUnit(cuid, trimmedName) .pipe(untilDestroyed(this)) .subscribe({ next: () => { this.notificationService.success("Successfully renamed computing unit"); if (this.entry.computingUnit.cuid === cuid) { this.entry.computingUnit.name = trimmedName; } // Refresh the computing units list this.computingUnitStatusService.refreshComputingUnitList(); }, error: (err: unknown) => { this.notificationService.error(`Failed to rename computing unit: ${extractErrorMessage(err)}`); }, }) .add(() => { this.editingNameOfUnit = null; this.editingUnitName = ""; }); } cancelEditingUnitName(): void { this.editingNameOfUnit = null; this.editingUnitName = ""; } openComputingUnitMetadataModal(entry: DashboardWorkflowComputingUnit) { this.modalService.create({ nzTitle: "Computing Unit Information", nzContent: ComputingUnitMetadataComponent, nzData: entry, nzFooter: null, nzMaskClosable: true, nzWidth: "600px", }); } getBadgeColor(status: string): string { return getComputingUnitBadgeColor(status); } getUnitStatusTooltip(entry: DashboardWorkflowComputingUnit): string { return getComputingUnitStatusTooltip(entry); } getCpuPercentage(): number { return cpuPercentage(this.getCurrentComputingUnitCpuUsage(), this.getCurrentComputingUnitCpuLimit()); } getMemoryPercentage(): number { return memoryPercentage(this.getCurrentComputingUnitMemoryUsage(), this.getCurrentComputingUnitMemoryLimit()); } getCpuStatus(): "success" | "exception" | "active" | "normal" { return getComputingUnitCpuStatus(this.getCpuPercentage()); } getMemoryStatus(): "success" | "exception" | "active" | "normal" { return getComputingUnitMemoryStatus(this.getMemoryPercentage()); } getCurrentComputingUnitCpuUsage(): string { return this.entry?.metrics?.cpuUsage ?? "N/A"; } getCurrentComputingUnitMemoryUsage(): string { return this.entry?.metrics?.memoryUsage ?? "N/A"; } getCurrentComputingUnitCpuLimit(): string { return this.unit?.resource?.cpuLimit ?? "N/A"; } getCurrentComputingUnitMemoryLimit(): string { return this.unit?.resource?.memoryLimit ?? "N/A"; } getCurrentComputingUnitGpuLimit(): string { return this.unit?.resource?.gpuLimit ?? "N/A"; } getCurrentComputingUnitJvmMemorySize(): string { return this.unit?.resource?.jvmMemorySize ?? "N/A"; } getCurrentSharedMemorySize(): string { return this.unit?.resource?.shmSize ?? "N/A"; } getCpuLimit(): number { return parseResourceNumber(this.getCurrentComputingUnitCpuLimit()); } getGpuLimit(): string { return this.getCurrentComputingUnitGpuLimit(); } getJvmMemorySize(): string { return this.getCurrentComputingUnitJvmMemorySize(); } getSharedMemorySize(): string { return this.getCurrentSharedMemorySize(); } getCpuLimitUnit(): string { return getComputingUnitCpuLimitUnit(parseResourceUnit(this.getCurrentComputingUnitCpuLimit())); } getMemoryLimit(): number { return parseResourceNumber(this.getCurrentComputingUnitMemoryLimit()); } getMemoryLimitUnit(): string { return parseResourceUnit(this.getCurrentComputingUnitMemoryLimit()); } getCpuValue(): number { const usage = this.getCurrentComputingUnitCpuUsage(); const limit = this.getCurrentComputingUnitCpuLimit(); if (usage === "N/A" || limit === "N/A") return 0; const displayUnit = this.getCpuLimitUnit() === "CPU" ? "" : this.getCpuLimitUnit(); const usageValue = cpuResourceConversion(usage, displayUnit); return parseFloat(usageValue); } getMemoryValue(): number { const usage = this.getCurrentComputingUnitMemoryUsage(); const limit = this.getCurrentComputingUnitMemoryLimit(); if (usage === "N/A" || limit === "N/A") return 0; const displayUnit = this.getMemoryLimitUnit(); const usageValue = memoryResourceConversion(usage, displayUnit); return parseFloat(usageValue); } showGpuSelection(): boolean { return this.gpuOptions.length > 1 || (this.gpuOptions.length === 1 && this.gpuOptions[0] !== "0"); } formatTime(timestamp: number | undefined): string { if (timestamp === undefined) { return "Unknown"; // Return "Unknown" if the timestamp is undefined } const currentTime = new Date().getTime(); const timeDifference = currentTime - timestamp; const minutesAgo = Math.floor(timeDifference / (1000 * 60)); const hoursAgo = Math.floor(timeDifference / (1000 * 60 * 60)); const daysAgo = Math.floor(timeDifference / (1000 * 60 * 60 * 24)); const weeksAgo = Math.floor(daysAgo / 7); if (minutesAgo < 60) { return `${minutesAgo} minutes ago`; } else if (hoursAgo < 24) { return `${hoursAgo} hours ago`; } else if (daysAgo < 7) { return `${daysAgo} days ago`; } else if (weeksAgo < 4) { return `${weeksAgo} weeks ago`; } else { return new Date(timestamp).toLocaleDateString(); } } public async onClickOpenShareAccess(cuid: number): Promise { this.computingUnitActionsService.openShareAccessModal(cuid, false); } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.html ================================================

    Computing Units

    Create Computing Unit
    Computing Unit Type
    Computing Unit Name
    Select RAM Size
    Select #CPU Core(s)
    Select #GPU(s)
    Adjust the Shared Memory Size
    Shared memory cannot be greater than total memory.
    JVM Memory Size: {{selectedJvmMemorySize}}
    Computing Unit Name
    Computing Unit URI
    ================================================ FILE: frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ @import "../../dashboard.component.scss"; @import "../../section-style"; @import "../../button-style"; .subsection-grid-container { min-width: 100%; width: 100%; min-height: 100%; height: 100%; } .memory-selection, .cpu-selection, .gpu-selection { width: 100%; } .jvm-memory-slider { width: 100%; margin: 10px 0; } .memory-warning { margin-top: 10px; font-size: 0.9em; } .create-compute-unit-container { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; justify-content: start; align-items: center; } .select-unit { display: flex; flex-direction: column; gap: 10px; justify-content: center; align-items: flex-start; } .select-unit.name-field { grid-column: span 2; } .unit-name-input { width: 100%; } .shared-memory-group { width: 100%; .shm-input-row { display: flex; gap: 8px; align-items: center; width: 100%; } .shm-size-input { width: 60px; min-width: 50px; flex-shrink: 0; } .shm-unit-select { width: 80px; min-width: 70px; flex-shrink: 0; } .shm-warning { margin-top: 4px; font-size: 12px; color: #faad14; white-space: nowrap; } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { NO_ERRORS_SCHEMA } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { UserComputingUnitComponent } from "./user-computing-unit.component"; import { NzCardModule } from "ng-zorro-antd/card"; import { NzIconModule } from "ng-zorro-antd/icon"; import { NzModalService } from "ng-zorro-antd/modal"; import { FileAddOutline } from "@ant-design/icons-angular/icons"; import { HttpClient } from "@angular/common/http"; import { UserService } from "../../../../common/service/user/user.service"; import { StubUserService } from "../../../../common/service/user/stub-user.service"; import { commonTestProviders } from "../../../../common/testing/test-utils"; import { WorkflowComputingUnitManagingService } from "../../../../common/service/computing-unit/workflow-computing-unit/workflow-computing-unit-managing.service"; import { ComputingUnitStatusService } from "../../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; import { MockComputingUnitStatusService } from "../../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service"; import { of } from "rxjs"; import type { Mocked } from "vitest"; describe("UserComputingUnitComponent", () => { let component: UserComputingUnitComponent; let fixture: ComponentFixture; let mockComputingUnitService: Mocked; beforeEach(async () => { mockComputingUnitService = { getComputingUnitTypes: vi.fn(), getComputingUnitLimitOptions: vi.fn(), createKubernetesBasedComputingUnit: vi.fn(), createLocalComputingUnit: vi.fn(), } as unknown as Mocked; mockComputingUnitService.getComputingUnitTypes.mockReturnValue(of({ typeOptions: [] })); mockComputingUnitService.getComputingUnitLimitOptions.mockReturnValue( of({ cpuLimitOptions: [], memoryLimitOptions: [], gpuLimitOptions: [] }) ); await TestBed.configureTestingModule({ providers: [ NzModalService, HttpClient, { provide: UserService, useClass: StubUserService }, { provide: WorkflowComputingUnitManagingService, useValue: mockComputingUnitService }, { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService }, ...commonTestProviders, ], imports: [UserComputingUnitComponent, NzCardModule, NzIconModule.forChild([FileAddOutline])], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(UserComputingUnitComponent); component = fixture.componentInstance; }); it("should create", () => { fixture.detectChanges(); expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/dashboard/component/user/user-computing-unit/user-computing-unit.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, Input, OnInit } from "@angular/core"; import { ComputingUnitStatusService } from "../../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; import { DashboardEntry } from "../../../type/dashboard-entry"; import { DashboardWorkflowComputingUnit, WorkflowComputingUnitType, } from "../../../../common/type/workflow-computing-unit"; import { extractErrorMessage } from "../../../../common/util/error"; import { NotificationService } from "../../../../common/service/notification/notification.service"; import { NzModalService, NzModalComponent } from "ng-zorro-antd/modal"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { UserService } from "../../../../common/service/user/user.service"; import { WorkflowComputingUnitManagingService } from "../../../../common/service/computing-unit/workflow-computing-unit/workflow-computing-unit-managing.service"; import { parseResourceUnit, parseResourceNumber, findNearestValidStep, unitTypeMessageTemplate, isComputingUnitShmTooLarge, getJvmMemorySliderConfig, } from "../../../../common/util/computing-unit.util"; import { ComputingUnitActionsService } from "../../../../common/service/computing-unit/computing-unit-actions/computing-unit-actions.service"; import { NzCardComponent } from "ng-zorro-antd/card"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { ɵɵCdkVirtualScrollViewport, ɵɵCdkFixedSizeVirtualScroll, ɵɵCdkVirtualForOf } from "@angular/cdk/overlay"; import { NzListComponent } from "ng-zorro-antd/list"; import { UserComputingUnitListItemComponent } from "./user-computing-unit-list-item/user-computing-unit-list-item.component"; import { NzSelectComponent, NzOptionComponent } from "ng-zorro-antd/select"; import { FormsModule } from "@angular/forms"; import { NgFor, NgIf, TitleCasePipe } from "@angular/common"; import { NzInputDirective } from "ng-zorro-antd/input"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzSliderComponent } from "ng-zorro-antd/slider"; import { NzAlertComponent } from "ng-zorro-antd/alert"; @UntilDestroy() @Component({ selector: "texera-computing-unit-section", templateUrl: "user-computing-unit.component.html", styleUrls: ["user-computing-unit.component.scss"], imports: [ NzCardComponent, NzSpaceCompactItemDirective, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, NzIconDirective, ɵɵCdkVirtualScrollViewport, ɵɵCdkFixedSizeVirtualScroll, NzListComponent, ɵɵCdkVirtualForOf, UserComputingUnitListItemComponent, NzModalComponent, NzSelectComponent, FormsModule, NgFor, NzOptionComponent, NgIf, NzInputDirective, NzTooltipDirective, NzSliderComponent, NzAlertComponent, TitleCasePipe, ], }) export class UserComputingUnitComponent implements OnInit { public entries: DashboardEntry[] = []; public isLogin = this.userService.isLogin(); public currentUid = this.userService.getCurrentUser()?.uid; allComputingUnits: DashboardWorkflowComputingUnit[] = []; // variables for creating a computing unit addComputeUnitModalVisible = false; newComputingUnitName: string = ""; selectedMemory: string = ""; selectedCpu: string = ""; selectedGpu: string = "0"; // Default to no GPU selectedJvmMemorySize: string = "1G"; // Initial JVM memory size selectedComputingUnitType?: WorkflowComputingUnitType; // Selected computing unit type selectedShmSize: string = "64Mi"; // Shared memory size shmSizeValue: number = 64; // default to 64 shmSizeUnit: "Mi" | "Gi" = "Mi"; // default unit availableComputingUnitTypes: WorkflowComputingUnitType[] = []; localComputingUnitUri: string = ""; // URI for local computing unit // JVM memory slider configuration jvmMemorySliderValue: number = 1; // Initial value in GB jvmMemoryMarks: { [key: number]: string } = { 1: "1G" }; jvmMemoryMax: number = 1; jvmMemorySteps: number[] = [1]; // Available steps in binary progression (1,2,4,8...) showJvmMemorySlider: boolean = false; // Whether to show the slider // cpu&memory limit options from backend cpuOptions: string[] = []; memoryOptions: string[] = []; gpuOptions: string[] = []; // Add GPU options array constructor( private notificationService: NotificationService, private modalService: NzModalService, private userService: UserService, private computingUnitService: WorkflowComputingUnitManagingService, private computingUnitStatusService: ComputingUnitStatusService, private computingUnitActionsService: ComputingUnitActionsService ) { this.userService .userChanged() .pipe(untilDestroyed(this)) .subscribe(() => { this.isLogin = this.userService.isLogin(); this.currentUid = this.userService.getCurrentUser()?.uid; }); } ngOnInit() { this.localComputingUnitUri = `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ""}/wsapi`; this.newComputingUnitName = "My Computing Unit"; this.computingUnitService .getComputingUnitTypes() .pipe(untilDestroyed(this)) .subscribe({ next: ({ typeOptions }) => { this.availableComputingUnitTypes = typeOptions; // Set default selected type if available if (typeOptions.includes("kubernetes")) { this.selectedComputingUnitType = "kubernetes"; } else if (typeOptions.length > 0) { this.selectedComputingUnitType = typeOptions[0]; } }, error: (err: unknown) => this.notificationService.error(`Failed to fetch computing unit types: ${extractErrorMessage(err)}`), }); this.computingUnitService .getComputingUnitLimitOptions() .pipe(untilDestroyed(this)) .subscribe({ next: ({ cpuLimitOptions, memoryLimitOptions, gpuLimitOptions }) => { this.cpuOptions = cpuLimitOptions; this.memoryOptions = memoryLimitOptions; this.gpuOptions = gpuLimitOptions; // fallback defaults this.selectedCpu = this.cpuOptions[0] ?? "1"; this.selectedMemory = this.memoryOptions[0] ?? "1Gi"; this.selectedGpu = this.gpuOptions[0] ?? "0"; // Initialize JVM memory slider based on selected memory this.updateJvmMemorySlider(); }, error: (err: unknown) => this.notificationService.error(`Failed to fetch resource options: ${extractErrorMessage(err)}`), }); this.computingUnitStatusService .getAllComputingUnits() .pipe(untilDestroyed(this)) .subscribe(units => { this.allComputingUnits = units; this.entries = units.map(u => new DashboardEntry(u)); }); } terminateComputingUnit(cuid: number): void { const unit = this.allComputingUnits.find(u => u.computingUnit.cuid === cuid); if (!unit) { this.notificationService.error("Invalid computing unit."); return; } this.computingUnitActionsService.confirmAndTerminate(cuid, unit); } startComputingUnit(): void { if (this.selectedComputingUnitType === "kubernetes" && this.newComputingUnitName.trim() === "") { this.notificationService.error("Name of the computing unit cannot be empty"); return; } if (this.selectedComputingUnitType === "local" && this.localComputingUnitUri.trim() === "") { this.notificationService.error("URI for local computing unit cannot be empty"); return; } if (!this.selectedComputingUnitType) { this.notificationService.error("Please select a valid computing unit type"); return; } const request = { type: this.selectedComputingUnitType, name: this.newComputingUnitName, cpu: this.selectedCpu, memory: this.selectedMemory, gpu: this.selectedGpu, jvmMemorySize: this.selectedJvmMemorySize, shmSize: `${this.shmSizeValue}${this.shmSizeUnit}`, localUri: this.localComputingUnitUri, }; this.computingUnitActionsService .create(request) .pipe(untilDestroyed(this)) .subscribe({ next: () => { this.notificationService.success("Successfully created the new compute unit"); this.computingUnitStatusService.refreshComputingUnitList(); }, error: (err: unknown) => this.notificationService.error(`Failed to start computing unit: ${extractErrorMessage(err)}`), }); } showGpuSelection(): boolean { // Don't show GPU selection if there are no options or only "0" option return this.gpuOptions.length > 1 || (this.gpuOptions.length === 1 && this.gpuOptions[0] !== "0"); } showAddComputeUnitModalVisible(): void { this.addComputeUnitModalVisible = true; } handleAddComputeUnitModalOk(): void { this.startComputingUnit(); this.addComputeUnitModalVisible = false; } handleAddComputeUnitModalCancel(): void { this.addComputeUnitModalVisible = false; } isShmTooLarge(): boolean { return isComputingUnitShmTooLarge(this.selectedMemory, this.shmSizeValue, this.shmSizeUnit); } updateJvmMemorySlider(): void { this.resetJvmMemorySlider(); } onJvmMemorySliderChange(value: number): void { // Ensure the value is one of the valid steps const validStep = findNearestValidStep(value, this.jvmMemorySteps); this.jvmMemorySliderValue = validStep; this.selectedJvmMemorySize = `${validStep}G`; } isMaxJvmMemorySelected(): boolean { // Only show warning for larger memory sizes (>=4GB) where the slider is shown // AND when the maximum value is selected return this.showJvmMemorySlider && this.jvmMemorySliderValue === this.jvmMemoryMax && this.jvmMemoryMax >= 4; } // Completely reset the JVM memory slider based on the selected CU memory resetJvmMemorySlider(): void { const config = getJvmMemorySliderConfig(this.selectedMemory); this.jvmMemoryMax = config.jvmMemoryMax; this.showJvmMemorySlider = config.showJvmMemorySlider; this.jvmMemorySteps = config.jvmMemorySteps; this.jvmMemoryMarks = config.jvmMemoryMarks; this.jvmMemorySliderValue = config.jvmMemorySliderValue; this.selectedJvmMemorySize = config.selectedJvmMemorySize; } onMemorySelectionChange(): void { // Store current JVM memory value for potential reuse const previousJvmMemory = this.jvmMemorySliderValue; // Reset slider configuration based on the new memory selection this.resetJvmMemorySlider(); // For CU memory > 3GB, preserve previous value if valid and >= 2GB // Get the current memory in GB const memoryValue = parseResourceNumber(this.selectedMemory); const memoryUnit = parseResourceUnit(this.selectedMemory); let cuMemoryInGb = memoryUnit === "Gi" ? memoryValue : memoryUnit === "Mi" ? Math.floor(memoryValue / 1024) : 1; // Only try to preserve previous value for larger memory sizes where slider is shown if ( cuMemoryInGb > 3 && previousJvmMemory >= 2 && previousJvmMemory <= this.jvmMemoryMax && this.jvmMemorySteps.includes(previousJvmMemory) ) { this.jvmMemorySliderValue = previousJvmMemory; this.selectedJvmMemorySize = `${previousJvmMemory}G`; } } getCreateModalTitle(): string { if (!this.selectedComputingUnitType) return "Create Computing Unit"; return unitTypeMessageTemplate[this.selectedComputingUnitType].createTitle; } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html ================================================

    Dataset: {{datasetName}}

    Created at: {{datasetCreationTime}}
    {{ datasetIsPublic ? "Public" : "Private" }} {{ datasetIsDownloadable ? "Downloadable" : "Download restricted" }} {{ formatCount(viewCount) }}
    Quick Settings
    Visibility:
    Downloadable:

    {{ currentDisplayedFileName }} {{ formatSize(currentFileSize) }}

    Choose a Version:
    Version Size: {{ formatSize(currentDatasetVersionSize) }}
    Created at: {{ selectedVersionCreationTime }}
    {{ fileName }}
    {{ task.status }}: {{ task.filePath }}
    {{ formatSpeed(task.uploadSpeed) }} - {{ formatTime(task.totalTime ?? 0) }} elapsed, {{ formatTime(task.estimatedTimeRemaining ?? 0) }} left Upload time: {{ formatTime(task.totalTime ?? 0) }}
    ================================================ FILE: frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .create-dataset-version-button { display: flex; /* Use flexbox for centering */ align-items: center; /* Center vertically */ justify-content: center; /* Center horizontally */ color: white; border: none; padding: 12px 40px; /* Increase padding for a wider button */ border-radius: 25px; cursor: pointer; transition: background-color 0.3s; margin: 18px auto 0 auto; /* Auto margins for horizontal centering */ width: 200px; /* Adjust width as needed */ font-size: 18px; /* Make text slightly bigger */ font-weight: bold; /* Optional: Make text bold */ } .version-storage { padding: 0 15px; margin-bottom: 25px; } .version-storage nz-select { width: 100%; margin: 0; } nz-layout { height: 100%; } html, body { height: 100%; margin: 0; padding: 0; } .right-sider { height: 100%; overflow-y: auto; position: relative; z-index: 0; } .sider-resize-line { height: 100%; width: 5px; border-right: 1px solid #e8e8e8; } .sider-resize-handle { background: #fff; border: 1px solid #ddd; text-align: center; font-size: 12px; height: 20px; line-height: 20px; margin-top: 400px; } .file-renderer { width: 95%; height: 80%; margin: auto; } .grayed-out { filter: grayscale(1) opacity(0.7); background-color: #f0f0f0; /* Adjust the color as needed */ } .disabled-click { pointer-events: none; } .select-and-button-container { display: flex; align-items: stretch; gap: 10px; } .spaced-button { flex-shrink: 0; width: 30px; height: 24px; } nz-select { flex-grow: 1; } .file-title { display: flex; align-items: center; gap: 10px; } .file-size, .version-size, .version-date { font-size: 12px; color: #8c8c8c; display: inline-flex; align-items: center; gap: 4px; } .version-date { display: flex; } .version-size { margin-top: 8px; } .icon-database, .icon-file { font-size: 14px; } .empty-version-indicator { margin-top: 15%; } .upload-progress-wrapper, .upload-progress-wrapper-pending { max-height: 25vh; overflow-y: auto; padding-right: 4px; } .upload-progress-wrapper-pending { display: flex; flex-direction: column; max-height: 15vh; } .version-creator { margin-top: 20px; padding: 20px; } .version-input-container label { font-size: 15px; } .version-input-container { display: flex; align-items: center; gap: 10px; } .version-input { padding: 6px; } .upload-stats { font-size: 13px; margin-bottom: 20px; nz-progress { width: 97%; display: inline-block; } } :host ::ng-deep .upload-stats .ant-tag { border: none; } .fixed-width-speed { display: inline-block; min-width: 5ch; text-align: right; } .fixed-width-time { display: inline-block; min-width: 2ch; text-align: right; } .section-divider { margin: 8px 0; } .status-tag-row { margin-top: 16px; display: flex; align-items: center; gap: 5px; } .status-tag { padding: 3px 10px 3px 3px; font-size: 13px; display: inline-flex; align-items: center; gap: 5px; background: #fff; border-radius: 5px; i { width: 22px; height: 22px; display: inline-flex; align-items: center; justify-content: center; border-radius: 4px; background: #f0f0f0; } } .tag-public i { color: #3b82f6; background: #eff6ff; } .tag-downloadable i { color: #22c55e; background: #f0fdf4; } .like-tag { cursor: pointer; } .like-tag.liked { i { color: #ef4444; background: #fef2f2; } } .like-tag.disabled { cursor: not-allowed; opacity: 0.55; } .tag-settings-btn { width: 32px; height: 32px; border-radius: 50%; border: 1px solid #d9d9d9; } .tag-settings-title { font-weight: 600; } .tag-settings-row { display: flex; align-items: center; gap: 10px; padding: 5px 0; } .tag-settings-row span { min-width: 110px; } .tag-settings-row ::ng-deep .ant-switch { min-width: 80px; } .description-section { margin-top: 20px; padding-top: 10px; border-top: 1px solid #eee; } .file-title { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-bottom: 0; } .file-title-main { display: inline-flex; align-items: center; gap: 6px; min-width: 0; } .copy-path-btn { padding: 0 4px; height: auto; line-height: 1; display: inline-flex; align-items: center; justify-content: center; } .file-size { display: inline-flex; align-items: center; gap: 4px; } ================================================ FILE: frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, EventEmitter, OnInit, Output } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { DatasetService, MultipartUploadProgress } from "../../../../service/user/dataset/dataset.service"; import { NzResizeEvent, NzResizableDirective, NzResizeHandleComponent } from "ng-zorro-antd/resizable"; import { DatasetFileNode, getFullPathFromDatasetFileNode, getRelativePathFromDatasetFileNode, } from "../../../../../common/type/datasetVersionFileTree"; import { DatasetVersion } from "../../../../../common/type/dataset"; import { switchMap, throttleTime } from "rxjs/operators"; import { NotificationService } from "../../../../../common/service/notification/notification.service"; import { DownloadService } from "../../../../service/user/download/download.service"; import { formatSize } from "src/app/common/util/size-formatter.util"; import { UserService } from "../../../../../common/service/user/user.service"; import { isDefined } from "../../../../../common/util/predicate"; import { ActionType, EntityType, HubService, LikedStatus } from "../../../../../hub/service/hub.service"; import { FileUploadItem } from "../../../../type/dashboard-file.interface"; import { DatasetStagedObject } from "../../../../../common/type/dataset-staged-object"; import { NzModalService } from "ng-zorro-antd/modal"; import { AdminSettingsService } from "../../../../service/admin/settings/admin-settings.service"; import { HttpErrorResponse, HttpStatusCode } from "@angular/common/http"; import { Subscription } from "rxjs"; import { formatSpeed, formatTime } from "src/app/common/util/format.util"; import { format } from "date-fns"; import { NgIf, NgClass, NgFor } from "@angular/common"; import { NzCardComponent, NzCardMetaComponent } from "ng-zorro-antd/card"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzTagComponent } from "ng-zorro-antd/tag"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzPopoverDirective } from "ng-zorro-antd/popover"; import { NzSwitchComponent } from "ng-zorro-antd/switch"; import { FormsModule } from "@angular/forms"; import { MarkdownDescriptionComponent } from "../../markdown-description/markdown-description.component"; import { NzLayoutComponent, NzContentComponent, NzSiderComponent } from "ng-zorro-antd/layout"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { NzEmptyComponent } from "ng-zorro-antd/empty"; import { UserDatasetFileRendererComponent } from "./user-dataset-file-renderer/user-dataset-file-renderer.component"; import { NzCollapseComponent, NzCollapsePanelComponent } from "ng-zorro-antd/collapse"; import { NzSelectComponent, NzOptionComponent } from "ng-zorro-antd/select"; import { UserDatasetVersionFiletreeComponent } from "./user-dataset-version-filetree/user-dataset-version-filetree.component"; import { NzDividerComponent } from "ng-zorro-antd/divider"; import { FilesUploaderComponent } from "../../files-uploader/files-uploader.component"; import { NzProgressComponent } from "ng-zorro-antd/progress"; import { UserDatasetStagedObjectsListComponent } from "./user-dataset-staged-objects-list/user-dataset-staged-objects-list.component"; import { NzInputDirective } from "ng-zorro-antd/input"; export const THROTTLE_TIME_MS = 1000; export const ABORT_RETRY_MAX_ATTEMPTS = 10; export const ABORT_RETRY_BACKOFF_BASE_MS = 100; @UntilDestroy() @Component({ templateUrl: "./dataset-detail.component.html", styleUrls: ["./dataset-detail.component.scss"], imports: [ NgIf, NzCardComponent, NzCardMetaComponent, NzTooltipDirective, NzTagComponent, NgClass, ɵNzTransitionPatchDirective, NzIconDirective, NzSpaceCompactItemDirective, NzButtonComponent, NzPopoverDirective, NzSwitchComponent, FormsModule, MarkdownDescriptionComponent, NzLayoutComponent, NzContentComponent, NzWaveDirective, NzEmptyComponent, UserDatasetFileRendererComponent, NzSiderComponent, NzResizableDirective, NzResizeHandleComponent, NzCollapseComponent, NzCollapsePanelComponent, NzSelectComponent, NgFor, NzOptionComponent, UserDatasetVersionFiletreeComponent, NzDividerComponent, FilesUploaderComponent, NzProgressComponent, UserDatasetStagedObjectsListComponent, NzInputDirective, ], }) export class DatasetDetailComponent implements OnInit { public did: number | undefined; public datasetName: string = ""; public datasetDescription: string = ""; public datasetCreationTime: string = ""; public datasetCreationTimeTooltip: string = ""; public datasetIsPublic: boolean = false; public datasetIsDownloadable: boolean = true; public userDatasetAccessLevel: "READ" | "WRITE" | "NONE" = "NONE"; public ownerEmail: string = ""; public isOwner: boolean = false; public currentDisplayedFileName: string = ""; public currentFileSize: number | undefined; public currentDatasetVersionSize: number | undefined; public isRightBarCollapsed = false; public isMaximized = false; public versions: ReadonlyArray = []; public selectedVersion: DatasetVersion | undefined; public fileTreeNodeList: DatasetFileNode[] = []; public selectedVersionCreationTime: string = ""; public versionCreatorBaseVersion: DatasetVersion | undefined; public isLogin: boolean = this.userService.isLogin(); public isLiked: boolean = false; public likeCount: number = 0; public currentUid: number | undefined; public viewCount: number = 0; public displayPreciseViewCount = false; userHasPendingChanges: boolean = false; pendingChangesCount: number = 0; // Uploading setting chunkSizeMiB: number = 50; maxConcurrentChunks: number = 10; private uploadSubscriptions = new Map(); uploadTimeMap = new Map(); // Cap number of concurrent files uploads maxConcurrentFiles: number = 3; private activeUploads: number = 0; private pendingQueue: Array<{ fileName: string; startUpload: () => void }> = []; versionName: string = ""; isCreatingVersion: boolean = false; public activeMultipartFilePaths: string[] = []; // List of upload tasks – each task tracked by its filePath public uploadTasks: Array< MultipartUploadProgress & { filePath: string; } > = []; @Output() userMakeChanges = new EventEmitter(); constructor( private route: ActivatedRoute, private modalService: NzModalService, private datasetService: DatasetService, private notificationService: NotificationService, private downloadService: DownloadService, private userService: UserService, private hubService: HubService, private adminSettingsService: AdminSettingsService ) { this.userService .userChanged() .pipe(untilDestroyed(this)) .subscribe(() => { this.currentUid = this.userService.getCurrentUser()?.uid; this.isLogin = this.userService.isLogin(); }); } // item for control the resizeable sider MAX_SIDER_WIDTH = 600; MIN_SIDER_WIDTH = 150; siderWidth = 400; id = -1; onSideResize({ width }: NzResizeEvent): void { cancelAnimationFrame(this.id); this.id = requestAnimationFrame(() => { this.siderWidth = width!; }); } ngOnInit(): void { this.route.params .pipe( switchMap(params => { this.did = params["did"]; this.retrieveDatasetInfo(); this.retrieveDatasetVersionList(); return this.route.data; // or some other observable }), untilDestroyed(this) ) .subscribe(); if (!isDefined(this.did)) { return; } this.hubService .getCounts([EntityType.Dataset], [this.did], [ActionType.Like]) .pipe(untilDestroyed(this)) .subscribe(counts => { this.likeCount = counts[0].counts.like ?? 0; }); this.hubService .postView(this.did, this.currentUid ? this.currentUid : 0, EntityType.Dataset) .pipe(throttleTime(THROTTLE_TIME_MS)) .pipe(untilDestroyed(this)) .subscribe(count => { this.viewCount = count; }); if (!isDefined(this.currentUid)) { return; } this.hubService .isLiked([this.did], [EntityType.Dataset]) .pipe(untilDestroyed(this)) .subscribe((isLiked: LikedStatus[]) => { this.isLiked = isLiked.length > 0 ? isLiked[0].isLiked : false; }); this.loadUploadSettings(); } public onClickOpenVersionCreator() { if (this.did && !this.isCreatingVersion) { this.isCreatingVersion = true; this.datasetService .createDatasetVersion(this.did, this.versionName?.trim() || "") .pipe(untilDestroyed(this)) .subscribe({ next: res => { this.notificationService.success("Version Created"); this.isCreatingVersion = false; this.versionName = ""; this.retrieveDatasetVersionList(); this.userMakeChanges.emit(); }, error: (res: unknown) => { const err = res as HttpErrorResponse; this.notificationService.error(`Version creation failed: ${err.error.message}`); this.isCreatingVersion = false; }, }); } } public onClickDownloadVersionAsZip() { if (this.did && this.selectedVersion && this.selectedVersion.dvid) { this.downloadService .downloadDatasetVersion(this.did, this.selectedVersion.dvid, this.datasetName, this.selectedVersion.name) .pipe(untilDestroyed(this)) .subscribe(); } } onPublicStatusChange(checked: boolean): void { // Handle the change in dataset public status if (this.did) { this.datasetService .updateDatasetPublicity(this.did) .pipe(untilDestroyed(this)) .subscribe({ next: (res: Response) => { this.datasetIsPublic = checked; let state = "public"; if (!this.datasetIsPublic) { state = "private"; } this.notificationService.success(`Dataset ${this.datasetName} is now ${state}`); }, error: (err: unknown) => { this.notificationService.error("Fail to change the dataset publicity"); }, }); } } onDownloadableStatusChange(checked: boolean): void { // Handle the change in dataset downloadable status if (this.did) { this.datasetService .updateDatasetDownloadable(this.did) .pipe(untilDestroyed(this)) .subscribe({ next: (res: Response) => { this.datasetIsDownloadable = checked; let state = "allowed"; if (!this.datasetIsDownloadable) { state = "not allowed"; } this.notificationService.success(`Dataset downloads are now ${state}`); }, error: (err: unknown) => { this.notificationService.error("Failed to change the dataset download permission"); }, }); } } retrieveDatasetInfo() { if (this.did) { this.datasetService .getDataset(this.did, this.isLogin) .pipe(untilDestroyed(this)) .subscribe(dashboardDataset => { const dataset = dashboardDataset.dataset; this.datasetName = dataset.name; this.datasetDescription = dataset.description; this.userDatasetAccessLevel = dashboardDataset.accessPrivilege; this.datasetIsPublic = dataset.isPublic; this.datasetIsDownloadable = dataset.isDownloadable; this.ownerEmail = dashboardDataset.ownerEmail; this.isOwner = dashboardDataset.isOwner; if (typeof dataset.creationTime === "number") { const date = new Date(dataset.creationTime); this.datasetCreationTime = format(date, "MM/dd/yyyy HH:mm:ss"); const timeZoneName = new Intl.DateTimeFormat("en-US", { timeZoneName: "long", }) .format(date) .split(", ") .pop() || ""; this.datasetCreationTimeTooltip = `${format(date, "zzzz")} (${timeZoneName})`; } }); } } retrieveDatasetVersionList() { if (this.did) { this.datasetService .retrieveDatasetVersionList(this.did, this.isLogin) .pipe(untilDestroyed(this)) .subscribe(versionNames => { this.versions = versionNames; // by default, the selected version is the 1st element in the retrieved list // which is guaranteed(by the backend) to be the latest created version. if (this.versions.length > 0) { this.selectedVersion = this.versions[0]; this.onVersionSelected(this.selectedVersion); } }); } } loadFileContent(node: DatasetFileNode) { this.currentDisplayedFileName = getFullPathFromDatasetFileNode(node); this.currentFileSize = node.size; } onClickDownloadCurrentFile = (): void => { if (!this.did || !this.selectedVersion?.dvid) return; // For public datasets accessed by non-owners, use public endpoint const shouldUsePublicEndpoint = this.datasetIsPublic && !this.isOwner; this.downloadService .downloadSingleFile(this.currentDisplayedFileName, !shouldUsePublicEndpoint) .pipe(untilDestroyed(this)) .subscribe(); }; onClickScaleTheView() { this.isMaximized = !this.isMaximized; } onClickHideRightBar() { this.isRightBarCollapsed = !this.isRightBarCollapsed; } onStagedObjectsUpdated(stagedObjects: DatasetStagedObject[]) { this.userHasPendingChanges = stagedObjects.length > 0; this.pendingChangesCount = stagedObjects.length; } onVersionSelected(version: DatasetVersion): void { this.selectedVersion = version; if (this.did && this.selectedVersion.dvid) this.datasetService .retrieveDatasetVersionFileTree(this.did, this.selectedVersion.dvid, this.isLogin) .pipe(untilDestroyed(this)) .subscribe(data => { this.fileTreeNodeList = data.fileNodes; this.currentDatasetVersionSize = data.size; if (typeof version.creationTime === "number") { const date = new Date(version.creationTime); this.selectedVersionCreationTime = format(date, "MM/dd/yyyy HH:mm:ss"); } let currentNode = this.fileTreeNodeList[0]; while (currentNode.type === "directory" && currentNode.children) { currentNode = currentNode.children[0]; } this.loadFileContent(currentNode); }); } onVersionFileTreeNodeSelected(node: DatasetFileNode) { this.loadFileContent(node); } userHasWriteAccess(): boolean { return this.userDatasetAccessLevel == "WRITE"; } isDownloadAllowed(): boolean { // Owners can always download if (this.isOwner) { return true; } // Non-owners can download if dataset is downloadable and they have access // For public datasets, users have access even if userDatasetAccessLevel is 'NONE' // For private datasets, users need explicit access (userDatasetAccessLevel !== 'NONE') return this.datasetIsDownloadable && (this.datasetIsPublic || this.userDatasetAccessLevel !== "NONE"); } // Track multiple file by unique key trackByTask(_: number, task: MultipartUploadProgress & { filePath: string }): string { return task.filePath; } private loadUploadSettings(): void { this.adminSettingsService .getSetting("multipart_upload_chunk_size_mib") .pipe(untilDestroyed(this)) .subscribe(value => (this.chunkSizeMiB = parseInt(value))); this.adminSettingsService .getSetting("max_number_of_concurrent_uploading_file_chunks") .pipe(untilDestroyed(this)) .subscribe(value => (this.maxConcurrentChunks = parseInt(value))); this.adminSettingsService .getSetting("max_number_of_concurrent_uploading_file") .pipe(untilDestroyed(this)) .subscribe(value => { this.maxConcurrentFiles = parseInt(value); }); } onNewUploadFilesChanged(files: FileUploadItem[]) { if (this.did) { files.forEach(file => { // Check if currently uploading const continueWithUpload = () => { // Create upload function const startUpload = () => { this.pendingQueue = this.pendingQueue.filter(item => item.fileName !== file.name); // Add an initializing task placeholder to uploadTasks this.uploadTasks.unshift({ filePath: file.name, percentage: 0, status: "initializing", }); // Start multipart upload const subscription = this.datasetService .multipartUpload( this.ownerEmail, this.datasetName, file.name, file.file, this.chunkSizeMiB * 1024 * 1024, this.maxConcurrentChunks, file.restart ) .pipe(untilDestroyed(this)) .subscribe({ next: progress => { // Find the task const taskIndex = this.uploadTasks.findIndex(t => t.filePath === file.name); if (taskIndex !== -1) { // Update the task with new progress info this.uploadTasks[taskIndex] = { ...this.uploadTasks[taskIndex], ...progress, percentage: progress.percentage ?? this.uploadTasks[taskIndex].percentage ?? 0, }; // Auto-hide when upload is truly finished if (progress.status === "finished" && progress.totalTime) { const filename = file.name.split("/").pop() || file.name; this.uploadTimeMap.set(filename, progress.totalTime); this.userMakeChanges.emit(); this.scheduleHide(taskIndex); this.onUploadComplete(); } } }, error: (res: unknown) => { const err = res as HttpErrorResponse; if (err?.status === HttpStatusCode.Conflict) { this.notificationService.error( "Upload blocked (409). Another upload is likely in progress for this file (another tab/browser), or the server is finalizing a previous upload. Please retry in a moment." ); } else { this.notificationService.error("Upload failed. Please retry."); } // Handle upload error const taskIndex = this.uploadTasks.findIndex(t => t.filePath === file.name); if (taskIndex !== -1) { this.uploadTasks[taskIndex] = { ...this.uploadTasks[taskIndex], percentage: this.uploadTasks[taskIndex].percentage ?? 0, // was 100 status: "failed", }; this.scheduleHide(taskIndex); } this.onUploadComplete(); }, complete: () => { const taskIndex = this.uploadTasks.findIndex(t => t.filePath === file.name); if (taskIndex !== -1 && this.uploadTasks[taskIndex].status !== "finished") { this.uploadTasks[taskIndex].status = "finished"; this.userMakeChanges.emit(); this.scheduleHide(taskIndex); this.onUploadComplete(); } }, }); // Store the subscription for later cleanup this.uploadSubscriptions.set(file.name, subscription); }; // Queue management if (this.activeUploads < this.maxConcurrentFiles) { this.activeUploads++; startUpload(); } else { this.pendingQueue.push({ fileName: file.name, startUpload }); } }; // Check if currently uploading this.cancelExistingUpload(file.name, continueWithUpload); }); } } cancelExistingUpload(fileName: string, onCanceled?: () => void): void { const task = this.uploadTasks.find(t => t.filePath === fileName); if (task) { if (task.status === "uploading" || task.status === "initializing") { this.onClickAbortUploadProgress(task, onCanceled); return; } } // Remove from pending queue if present this.pendingQueue = this.pendingQueue.filter(item => item.fileName !== fileName); if (onCanceled) { onCanceled(); } } private processNextQueuedUpload(): void { if (this.pendingQueue.length > 0 && this.activeUploads < this.maxConcurrentFiles) { const next = this.pendingQueue.shift(); if (next) { this.activeUploads++; next.startUpload(); } } } private onUploadComplete(): void { this.activeUploads--; this.processNextQueuedUpload(); } get queuedFileNames(): string[] { return this.pendingQueue.map(item => item.fileName); } get queuedCount(): number { return this.pendingQueue.length; } get activeCount(): number { return this.activeUploads; } get hasAnyActivity(): boolean { return this.pendingChangesCount > 0 || this.activeCount > 0 || this.queuedCount > 0; } // Hide a task row after 5s private scheduleHide(idx: number) { if (idx === -1) { return; } const key = this.uploadTasks[idx].filePath; this.uploadSubscriptions.delete(key); setTimeout(() => { this.uploadTasks = this.uploadTasks.filter(t => t.filePath !== key); }, 5000); } onClickAbortUploadProgress(task: MultipartUploadProgress & { filePath: string }, onAborted?: () => void) { const subscription = this.uploadSubscriptions.get(task.filePath); if (subscription) { subscription.unsubscribe(); this.uploadSubscriptions.delete(task.filePath); } if (task.status === "uploading" || task.status === "initializing") { this.onUploadComplete(); } let doneCalled = false; const done = () => { if (doneCalled) { return; } doneCalled = true; if (onAborted) { onAborted(); } }; const abortWithRetry = (attempt: number) => { this.datasetService .finalizeMultipartUpload( this.ownerEmail, this.datasetName, task.filePath, true // abort flag ) .pipe(untilDestroyed(this)) .subscribe({ next: () => { this.notificationService.info(`${task.filePath} uploading has been terminated`); done(); }, error: (res: unknown) => { const err = res as HttpErrorResponse; // Already gone, treat as done if (err.status === 404) { done(); return; } // Backend is still finalizing/aborting; retry with a tiny backoff if (err.status === HttpStatusCode.Conflict && attempt < ABORT_RETRY_MAX_ATTEMPTS) { setTimeout(() => abortWithRetry(attempt + 1), ABORT_RETRY_BACKOFF_BASE_MS * (attempt + 1)); return; } // Keep current UX: still consider it "aborted" client-side done(); }, }); }; abortWithRetry(0); const idx = this.uploadTasks.findIndex(t => t.filePath === task.filePath); if (idx !== -1) { this.uploadTasks[idx] = { ...this.uploadTasks[idx], status: "aborted" }; this.scheduleHide(idx); } } getUploadStatus(status: MultipartUploadProgress["status"]): "active" | "exception" | "success" { return status === "uploading" || status === "initializing" ? "active" : status === "aborted" || status === "failed" ? "exception" : "success"; } onPreviouslyUploadedFileDeleted(node: DatasetFileNode) { if (this.did) { this.datasetService .deleteDatasetFile(this.did, getRelativePathFromDatasetFileNode(node)) .pipe(untilDestroyed(this)) .subscribe({ next: (res: Response) => { this.notificationService.success( `File ${node.name} is successfully deleted. You may finalize it or revert it at the "Create Version" panel` ); this.userMakeChanges.emit(); }, error: (err: unknown) => { this.notificationService.error("Failed to delete the file"); }, }); } } // alias for formatSize formatSize = formatSize; formatCount(count: number): string { if (count >= 1000) { return (count / 1000).toFixed(1) + "k"; } return count.toString(); } formatTime = formatTime; formatSpeed = formatSpeed; toggleLike(): void { const userId = this.currentUid; if (!isDefined(userId) || !isDefined(this.did)) { return; } if (this.isLiked) { this.hubService .postUnlike(this.did, EntityType.Dataset) .pipe(untilDestroyed(this)) .subscribe((success: boolean) => { if (success) { this.isLiked = false; this.hubService .getCounts([EntityType.Dataset], [this.did!], [ActionType.Like]) .pipe(untilDestroyed(this)) .subscribe(counts => { this.likeCount = counts[0].counts.like ?? 0; }); } }); } else { this.hubService .postLike(this.did, EntityType.Dataset) .pipe(untilDestroyed(this)) .subscribe((success: boolean) => { if (success) { this.isLiked = true; this.hubService .getCounts([EntityType.Dataset], [this.did!], [ActionType.Like]) .pipe(untilDestroyed(this)) .subscribe(counts => { this.likeCount = counts[0].counts.like ?? 0; }); } }); } } changeViewDisplayStyle() { this.displayPreciseViewCount = !this.displayPreciseViewCount; } onSetCoverImage(filePath: string): void { if (!this.did || !this.selectedVersion) { return; } this.datasetService .updateDatasetCoverImage(this.did, `${this.selectedVersion.name}/${filePath}`) .pipe(untilDestroyed(this)) .subscribe({ next: () => { this.notificationService.success("Cover image set successfully"); }, error: (err: unknown) => { this.notificationService.error( err instanceof HttpErrorResponse ? err.error?.message || "Failed to set cover image" : "Failed to set cover image" ); }, }); } onDatasetDescriptionChange(description: string): void { const updatedDescription = description ?? ""; const previousDescription = this.datasetDescription; if (!this.did || this.datasetDescription === updatedDescription) { return; } this.datasetDescription = updatedDescription; this.datasetService .updateDatasetDescription(this.did, updatedDescription) .pipe(untilDestroyed(this)) .subscribe({ error: () => { this.datasetDescription = previousDescription; this.notificationService.error("Failed to update dataset description"); }, }); } async copyCurrentFilePath(): Promise { if (!this.currentDisplayedFileName) { return; } try { await navigator.clipboard.writeText(this.currentDisplayedFileName); this.notificationService.success("File path copied to clipboard"); } catch (error) { this.notificationService.error("Failed to copy file path"); } } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.html ================================================
    {{ column }} {{ cell }}
    {{filePath}}
    {{filePath}}
    {{ textContent }}
    ================================================ FILE: frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .file-display-area { width: 100%; height: 100%; } /* CSS styles */ .image-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.8); display: flex; justify-content: center; align-items: center; z-index: 1000; } .full-size-image { max-width: 90%; max-height: 90%; } ================================================ FILE: frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { TestBed } from "@angular/core/testing"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { UserDatasetFileRendererComponent } from "./user-dataset-file-renderer.component"; import { DatasetService } from "../../../../../service/user/dataset/dataset.service"; import { NotificationService } from "../../../../../../common/service/notification/notification.service"; import { DomSanitizer } from "@angular/platform-browser"; import { commonTestProviders } from "../../../../../../common/testing/test-utils"; describe("UserDatasetFileRendererComponent", () => { let component: UserDatasetFileRendererComponent; beforeEach(() => { TestBed.configureTestingModule({ imports: [UserDatasetFileRendererComponent, HttpClientTestingModule], providers: [ DatasetService, NotificationService, { provide: DomSanitizer, useValue: { bypassSecurityTrustUrl: vi.fn() } }, ...commonTestProviders, ], }); const fixture = TestBed.createComponent(UserDatasetFileRendererComponent); component = fixture.componentInstance; }); it("should return true for supported MIME type", () => { const supportedMimeType = "image/jpeg"; // Example of a supported MIME type const result = component.isPreviewSupported(supportedMimeType); expect(result).toBe(true); }); it("should return false for unsupported MIME type", () => { const unsupportedMimeType = "application/unknown"; // Example of an unsupported MIME type const result = component.isPreviewSupported(unsupportedMimeType); expect(result).toBe(false); }); }); ================================================ FILE: frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from "@angular/core"; import { DatasetService } from "../../../../../service/user/dataset/dataset.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import * as Papa from "papaparse"; import { ParseResult } from "papaparse"; import { DomSanitizer, SafeUrl } from "@angular/platform-browser"; import readXlsxFile from "read-excel-file"; import { NotificationService } from "../../../../../../common/service/notification/notification.service"; import { NgStyle, NgIf, NgFor } from "@angular/common"; import { NzSpinComponent } from "ng-zorro-antd/spin"; import { NzAlertComponent } from "ng-zorro-antd/alert"; import { NzTableComponent, NzTheadComponent, NzTrDirective, NzTableCellDirective, NzThMeasureDirective, NzTbodyComponent, } from "ng-zorro-antd/table"; import { MarkdownComponent } from "ngx-markdown"; import { NgxJsonViewerModule } from "ngx-json-viewer"; export const MIME_TYPES = { JPEG: "image/jpeg", JPG: "image/jpeg", PNG: "image/png", WEBP: "image/webp", GIF: "image/gif", CSV: "text/csv", TXT: "text/plain", MD: "text/markdown", HTML: "text/html", JSON: "application/json", PDF: "application/pdf", MSWORD: "application/msword", MSEXCEL: "application/vnd.ms-excel", MSPOWERPOINT: "application/vnd.ms-powerpoint", MP4: "video/mp4", MP3: "audio/mpeg", OCTET_STREAM: "application/octet-stream", // Default binary format }; export function getMimeType(filename: string): string { const extension = filename.split(".").pop()?.toUpperCase(); return extension && MIME_TYPES[extension as keyof typeof MIME_TYPES] ? MIME_TYPES[extension as keyof typeof MIME_TYPES] : MIME_TYPES.OCTET_STREAM; } // the size limits for all preview-supported types export const MIME_TYPE_SIZE_LIMITS_MB = { [MIME_TYPES.JPEG]: 5 * 1024 * 1024, // 5 MB [MIME_TYPES.PNG]: 5 * 1024 * 1024, // 5 MB [MIME_TYPES.WEBP]: 5 * 1024 * 1024, // 5 MB [MIME_TYPES.GIF]: 10 * 1024 * 1024, // 10 MB [MIME_TYPES.CSV]: 2 * 1024 * 1024, // 2 MB for text-based data files [MIME_TYPES.TXT]: 1 * 1024 * 1024, // 1 MB for plain text files [MIME_TYPES.MD]: 1 * 1024 * 1024, // 1 MB for MD files [MIME_TYPES.JSON]: 1 * 1024 * 1024, // 1 MB for JSON files [MIME_TYPES.MSEXCEL]: 10 * 1024 * 1024, // 10 MB for Excel spreadsheets [MIME_TYPES.MP4]: 50 * 1024 * 1024, // 50 MB for MP4 videos [MIME_TYPES.MP3]: 10 * 1024 * 1024, // 10 MB for MP3 audio files [MIME_TYPES.OCTET_STREAM]: 5 * 1024 * 1024, // Default size for other binary formats }; @UntilDestroy() @Component({ selector: "texera-user-dataset-file-renderer", templateUrl: "./user-dataset-file-renderer.component.html", styleUrls: ["./user-dataset-file-renderer.component.scss"], imports: [ NgStyle, NgIf, NzSpinComponent, NzAlertComponent, NzTableComponent, NzTheadComponent, NzTrDirective, NgFor, NzTableCellDirective, NzThMeasureDirective, NzTbodyComponent, MarkdownComponent, NgxJsonViewerModule, ], }) export class UserDatasetFileRendererComponent implements OnInit, OnChanges, OnDestroy { private DEFAULT_MAX_SIZE = 5 * 1024 * 1024; // 5 MB public fileURL: string | undefined; // safe url is used to display some formats including image public safeFileURL: SafeUrl | undefined; // table related control public displayCSV: boolean = false; public displayXlsx: boolean = false; public tableDataHeader: any[] = []; public tableContent: any[][] = []; // image related control public displayImage: boolean = false; // markdown control public displayMarkdown: boolean = false; // json control public displayJson: boolean = false; // video public displayMP4: boolean = false; // audio public displayMP3: boolean = false; // plain text & octet stream related control public displayPlainText: boolean = false; public textContent: string = ""; // control flags public isLoading: boolean = false; public isFileSizeUnloadable = false; public isFileLoadingError: boolean = false; public isFileTypePreviewUnsupported: boolean = false; public currentFile: File | undefined = undefined; @Input() isMaximized: boolean = false; @Input() did: number | undefined; @Input() dvid: number | undefined; @Input() filePath: string = ""; @Input() fileSize?: number; @Input() isLogin: boolean = false; @Output() loadFile = new EventEmitter<{ file: string; prefix: string }>(); constructor( private datasetService: DatasetService, private sanitizer: DomSanitizer, private notificationService: NotificationService ) {} ngOnInit(): void { this.reloadFileContent(); } ngOnChanges(changes: SimpleChanges): void { if ((changes.did && changes.dvid) || changes.filePath) { this.reloadFileContent(); } } ngOnDestroy(): void { if (this.fileURL) { URL.revokeObjectURL(this.fileURL); } } showImageModal = false; toggleImageModal() { this.showImageModal = !this.showImageModal; } reloadFileContent() { this.turnOffAllDisplay(); // Pre-check - file size const mimeType = getMimeType(this.filePath); if (!this.isPreviewSupported(mimeType)) { this.onFileTypePreviewUnsupported(); return; } const limit = MIME_TYPE_SIZE_LIMITS_MB[mimeType] ?? this.DEFAULT_MAX_SIZE; if (this.fileSize != null && this.fileSize > limit) { this.onFileSizeNotLoadable(); return; } // Load file this.isLoading = true; if (this.did && this.dvid && this.filePath != "") { this.datasetService .retrieveDatasetVersionSingleFile(this.filePath, this.isLogin) .pipe(untilDestroyed(this)) .subscribe({ next: blob => { this.isLoading = false; const blobMimeType = getMimeType(this.filePath); if (!this.isPreviewSupported(blobMimeType)) { this.onFileTypePreviewUnsupported(); return; } const MaxSize = MIME_TYPE_SIZE_LIMITS_MB[blobMimeType] || this.DEFAULT_MAX_SIZE; const fileSize = blob.size; if (fileSize > MaxSize) { this.onFileSizeNotLoadable(); this.notificationService.warning(`File ${this.filePath} is too large to be previewed`); return; } this.currentFile = new File([blob], this.filePath, { type: blob.type }); // Handle different file types switch (blobMimeType) { case MIME_TYPES.PNG: case MIME_TYPES.JPEG: case MIME_TYPES.WEBP: case MIME_TYPES.GIF: this.displayImage = true; this.loadSafeURL(blob); break; case MIME_TYPES.MP4: this.displayMP4 = true; this.loadSafeURL(blob); break; case MIME_TYPES.MP3: this.displayMP3 = true; this.loadSafeURL(blob); break; case MIME_TYPES.MSEXCEL: readXlsxFile(blob).then(rows => { let parsedData: string[][] = []; rows.forEach(row => { // Convert each cell in the row to a string let stringRow = row.map(cell => (cell ? cell.toString() : "")); // Add the string array to the main array parsedData.push(stringRow); }); if (parsedData.length > 0) { this.loadTabularFile(parsedData); this.displayXlsx = true; } }); break; case MIME_TYPES.CSV: this.displayCSV = true; // Handle CSV display Papa.parse(this.currentFile, { complete: (results: ParseResult) => { if (results.data.length > 0) { this.loadTabularFile(results.data); } }, error: error => { console.error("Error parsing file:", error); this.onFileLoadingError(); }, }); break; case MIME_TYPES.MD: this.displayMarkdown = true; this.readFileAsText(blob); break; case MIME_TYPES.JSON: this.displayJson = true; this.readFileAsText(blob); break; case MIME_TYPES.TXT: default: this.displayPlainText = true; this.readFileAsText(blob); break; } }, }); } } turnOffAllDisplay() { this.displayCSV = false; this.displayXlsx = false; this.displayImage = false; this.displayPlainText = false; this.displayMarkdown = false; this.displayJson = false; this.displayMP4 = false; this.displayMP3 = false; this.isLoading = false; this.isFileLoadingError = false; this.isFileSizeUnloadable = false; this.isFileTypePreviewUnsupported = false; // garbage collection if (this.fileURL) { URL.revokeObjectURL(this.fileURL); } if (this.safeFileURL) { URL.revokeObjectURL(this.safeFileURL.toString()); } } onFileLoadingError() { this.turnOffAllDisplay(); this.isFileLoadingError = true; } onFileSizeNotLoadable() { this.turnOffAllDisplay(); this.isFileSizeUnloadable = true; } onFileTypePreviewUnsupported() { this.turnOffAllDisplay(); this.isFileTypePreviewUnsupported = true; } isPreviewSupported(mimeType: string) { return mimeType !== MIME_TYPES.OCTET_STREAM && Object.hasOwnProperty.call(MIME_TYPE_SIZE_LIMITS_MB, mimeType); } private readFileAsText(blob: Blob) { const txtReader = new FileReader(); txtReader.onload = (event: any) => { this.textContent = event.target.result; }; txtReader.readAsText(blob); } private loadSafeURL(blob: Blob) { this.fileURL = URL.createObjectURL(blob); this.safeFileURL = this.sanitizer.bypassSecurityTrustUrl(this.fileURL); } private loadTabularFile(data: any[][]) { if (data.length > 0) { // Extract the header (first row) this.tableDataHeader = data[0]; // Process the rest of the rows this.tableContent = data .slice(1) .map(row => { // Normalize the row length to match the header length while (row.length < this.tableDataHeader.length) { row.push(""); } return row; }) .filter(row => { // filter out all empty row let areCellAllEmpty = true; for (const cell in row) { if (cell != "") { areCellAllEmpty = false; break; } } return !areCellAllEmpty; }); } } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-staged-objects-list/user-dataset-staged-objects-list.component.html ================================================
    {{ obj.diffType }} {{ obj.path }}
    {{ obj.path }}
    Upload time: {{ formatTime(uploadTime) }}
    ================================================ FILE: frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-staged-objects-list/user-dataset-staged-objects-list.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /* Styles for the file tree container */ .staged-object-list-container { max-height: 200px; /* Adjust the max-height as needed */ overflow-y: auto; /* Enables vertical scrolling when content exceeds max-height */ overflow-x: auto; /* Prevents horizontal scrolling */ } .custom-border-list { border-bottom: 1px solid #f0f0f0; } .truncate-file-path { display: inline-block; max-width: 250px; /* Adjust width as needed */ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .delete-button { width: 24px; /* Minimum width for button */ height: 24px; /* Keep the button small */ padding: 0; /* Remove extra padding */ display: flex; align-items: center; justify-content: center; min-width: unset; /* Prevents unnecessary expansion */ } ================================================ FILE: frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-staged-objects-list/user-dataset-staged-objects-list.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { DatasetStagedObject } from "../../../../../../common/type/dataset-staged-object"; import { DatasetService } from "../../../../../service/user/dataset/dataset.service"; import { NotificationService } from "../../../../../../common/service/notification/notification.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { formatTime } from "src/app/common/util/format.util"; import { NgIf, NgFor } from "@angular/common"; import { NzListComponent, NzListItemComponent } from "ng-zorro-antd/list"; import { NzTagComponent } from "ng-zorro-antd/tag"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzEmptyComponent } from "ng-zorro-antd/empty"; @UntilDestroy() @Component({ selector: "texera-dataset-staged-objects-list", templateUrl: "./user-dataset-staged-objects-list.component.html", styleUrls: ["./user-dataset-staged-objects-list.component.scss"], imports: [ NgIf, NzListComponent, NgFor, NzListItemComponent, NzTagComponent, NzTooltipDirective, NzSpaceCompactItemDirective, NzButtonComponent, ɵNzTransitionPatchDirective, NzIconDirective, NzEmptyComponent, ], }) export class UserDatasetStagedObjectsListComponent implements OnInit { @Input() did?: number; // Dataset ID @Input() set userMakeChangesEvent(event: EventEmitter) { if (event) { event.pipe(untilDestroyed(this)).subscribe(() => { this.fetchDatasetStagedObjects(); }); } } @Input() uploadTimeMap?: Map; @Output() stagedObjectsChanged = new EventEmitter(); // Emits staged objects list datasetStagedObjects: DatasetStagedObject[] = []; formatTime = formatTime; constructor( private datasetService: DatasetService, private notificationService: NotificationService ) {} ngOnInit(): void { this.fetchDatasetStagedObjects(); } private fetchDatasetStagedObjects(): void { if (this.did != undefined) { this.datasetService .getDatasetDiff(this.did) .pipe(untilDestroyed(this)) .subscribe(diffs => { this.datasetStagedObjects = diffs; // Emit the updated staged objects list this.stagedObjectsChanged.emit(this.datasetStagedObjects); }); } } onObjectReverted(objDiff: DatasetStagedObject) { if (this.did) { this.datasetService .resetDatasetFileDiff(this.did, objDiff.path) .pipe(untilDestroyed(this)) .subscribe({ next: (res: Response) => { this.notificationService.success(`"${objDiff.diffType} ${objDiff.path}" is successfully reverted`); this.fetchDatasetStagedObjects(); }, error: (err: unknown) => { this.notificationService.error("Failed to delete the file"); }, }); } } getFileUploadTime(filePath: string): number | null { if (!this.uploadTimeMap) return null; const filename = filePath.split("/").pop() || filePath; return this.uploadTimeMap.get(filename) || null; } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.html ================================================
    ================================================ FILE: frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .texera-user-dataset-creator-form { width: 80%; margin: auto; } .create-btn { background-color: #ffffff; color: #007bff; border: 1px solid lightgray; padding: 5px 20px; cursor: pointer; margin-left: 15%; margin-right: 5%; width: 30%; text-align: center; &:hover { background-color: darken(#007bff, 10%); color: #ffffff; } } .cancel-btn { background-color: #ffffff; color: #ff0000; border: 1px solid lightgray; padding: 5px 20px; cursor: pointer; margin-right: 15%; margin-left: 5%; margin-top: 10%; width: 30%; text-align: center; &:hover { background-color: darken(#ff0000, 10%); color: #ffffff; } } .file-uploader { height: 40%; width: 90%; margin: auto; } .dataset-toggles { margin-top: 16px; margin-bottom: 16px; } .toggle-item { display: flex; align-items: center; margin-bottom: 12px; label { margin-right: 12px; min-width: 80px; font-weight: 500; } .help-text { margin-left: 8px; font-size: 12px; color: #999; font-style: italic; } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, inject, OnInit } from "@angular/core"; import { FormBuilder, FormGroup, FormsModule } from "@angular/forms"; import { FormlyFieldConfig, FormlyModule } from "@ngx-formly/core"; import { DatasetService } from "../../../../../service/user/dataset/dataset.service"; import { Dataset } from "../../../../../../common/type/dataset"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { NotificationService } from "../../../../../../common/service/notification/notification.service"; import { HttpErrorResponse } from "@angular/common/http"; import { NZ_MODAL_DATA, NzModalRef } from "ng-zorro-antd/modal"; import { NzSpinComponent } from "ng-zorro-antd/spin"; import { NgClass, NgIf } from "@angular/common"; import { NzSwitchComponent } from "ng-zorro-antd/switch"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; @UntilDestroy() @Component({ selector: "texera-user-dataset-version-creator", templateUrl: "./user-dataset-version-creator.component.html", styleUrls: ["./user-dataset-version-creator.component.scss"], imports: [ NzSpinComponent, NgClass, FormlyModule, NgIf, NzSwitchComponent, FormsModule, NzSpaceCompactItemDirective, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, ], }) export class UserDatasetVersionCreatorComponent implements OnInit { readonly isCreatingVersion: boolean = inject(NZ_MODAL_DATA).isCreatingVersion; readonly did: number = inject(NZ_MODAL_DATA)?.did ?? undefined; isCreateButtonDisabled: boolean = false; public form: FormGroup = new FormGroup({}); model: any = {}; fields: FormlyFieldConfig[] = []; isDatasetPublic: boolean = false; isDatasetDownloadable: boolean = false; // used when creating the dataset isDatasetNameSanitized: boolean = false; // boolean to control if is uploading isCreating: boolean = false; constructor( private modalRef: NzModalRef, private datasetService: DatasetService, private notificationService: NotificationService, private formBuilder: FormBuilder ) {} ngOnInit() { this.setFormFields(); this.isDatasetNameSanitized = false; } private setFormFields() { this.fields = this.isCreatingVersion ? [ // Fields when isCreatingVersion is true { key: "versionDescription", type: "input", defaultValue: "", templateOptions: { label: "Describe the new version", required: false, }, }, ] : [ // Fields when isCreatingVersion is false { key: "name", type: "input", templateOptions: { label: "Name", required: true, }, }, { key: "description", type: "input", defaultValue: "", templateOptions: { label: "Description", }, }, ]; } get formControlNames(): string[] { return Object.keys(this.form.controls); } datasetNameSanitization(datasetName: string): string { // Remove leading spaces let sanitizedDatasetName = datasetName.trimStart(); // Replace all characters that are not letters (a-z, A-Z), numbers (0-9) with a short dash "-" sanitizedDatasetName = sanitizedDatasetName.replace(/[^a-zA-Z0-9]+/g, "-"); // Lower-case everything sanitizedDatasetName = sanitizedDatasetName.toLowerCase(); // Track whether user’s input be changed if (sanitizedDatasetName !== datasetName) { this.isDatasetNameSanitized = true; } return sanitizedDatasetName; } private triggerValidation() { Object.keys(this.form.controls).forEach(field => { const control = this.form.get(field); control?.markAsTouched({ onlySelf: true }); }); } onClickCancel() { this.modalRef.close(null); } onClickCreate() { // check if the form is valid this.triggerValidation(); if (!this.form.valid) { return; // Stop further execution if the form is not valid } this.isCreating = true; if (this.isCreatingVersion && this.did) { const versionName = this.form.get("versionDescription")?.value; this.datasetService .createDatasetVersion(this.did, versionName) .pipe(untilDestroyed(this)) .subscribe({ next: res => { this.notificationService.success("Version Created"); this.isCreating = false; // creation succeed, emit created version this.modalRef.close(res); }, error: (res: unknown) => { const err = res as HttpErrorResponse; this.notificationService.error(`Version creation failed: ${err.error.message}`); this.isCreating = false; // creation failed, emit null value this.modalRef.close(null); }, }); } else { // capture original and sanitized names const originalName = this.form.get("name")?.value as string; const sanitizedName = this.datasetNameSanitization(originalName); const ds: Dataset = { name: sanitizedName, description: this.form.get("description")?.value, isPublic: this.isDatasetPublic, isDownloadable: this.isDatasetDownloadable, did: undefined, ownerUid: undefined, storagePath: undefined, creationTime: undefined, coverImage: undefined, }; this.datasetService .createDataset(ds) .pipe(untilDestroyed(this)) .subscribe({ next: res => { const msg = this.isDatasetNameSanitized ? `Dataset '${originalName}' was sanitized to '${sanitizedName}' and created successfully.` : `Dataset '${sanitizedName}' created successfully.`; this.notificationService.success(msg); this.isCreating = false; // if creation succeed, emit the created dashboard dataset this.modalRef.close(res); }, error: (res: unknown) => { const err = res as HttpErrorResponse; this.notificationService.error(`Dataset ${ds.name} creation failed: ${err.error.message}`); this.isCreating = false; // if creation failed, emit null value this.modalRef.close(null); }, }); } } onPublicStatusChange(newValue: boolean): void { // Handle the change in dataset public status this.isDatasetPublic = newValue; } onDownloadableStatusChange(newValue: boolean): void { // Handle the change in dataset downloadable status this.isDatasetDownloadable = newValue; } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.html ================================================
    {{ node.data.name }}
    ================================================ FILE: frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .file-display-padding { width: 20px; } /* Styles for the delete button */ .icon-button { width: 15px; margin-left: 5px; } /* Allow text selection so users can copy file/folder names */ :host ::ng-deep tree-node-content span { user-select: text; } /* Styles for the file tree container */ .file-tree-container { max-height: 200px; /* Adjust the max-height as needed */ overflow-y: auto; /* Enables vertical scrolling when content exceeds max-height */ overflow-x: auto; /* Prevents horizontal scrolling */ } //tree-root .fa-file { // //padding-left: 4%; /* Adjust the value as needed */ //} // //tree-root span { // display: inline-block; // max-width: 100%; /* Adjust the width as needed */ // text-overflow: ellipsis; // white-space: nowrap; // overflow: hidden; //} ================================================ FILE: frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { UntilDestroy } from "@ngneat/until-destroy"; import { AfterViewInit, Component, EventEmitter, Input, Output, ViewChild } from "@angular/core"; import { DatasetFileNode, getRelativePathFromDatasetFileNode, } from "../../../../../../common/type/datasetVersionFileTree"; import { ITreeOptions, TREE_ACTIONS, TreeModule } from "@ali-hm/angular-tree-component"; import { NgIf } from "@angular/common"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; const IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif", ".webp"] as const; @UntilDestroy() @Component({ selector: "texera-user-dataset-version-filetree", templateUrl: "./user-dataset-version-filetree.component.html", styleUrls: ["./user-dataset-version-filetree.component.scss"], imports: [ TreeModule, NgIf, ɵNzTransitionPatchDirective, NzIconDirective, NzSpaceCompactItemDirective, NzButtonComponent, NzTooltipDirective, ], }) export class UserDatasetVersionFiletreeComponent implements AfterViewInit { @Input() public isTreeNodeDeletable: boolean = false; @Input() public fileTreeNodes: DatasetFileNode[] = []; @Input() public isExpandAllAfterViewInit = false; @ViewChild("tree") tree: any; @Output() setCoverImage = new EventEmitter(); public fileTreeDisplayOptions: ITreeOptions = { displayField: "name", hasChildrenField: "children", actionMapping: { mouse: { click: (tree: any, node: any, $event: any) => { if (node.hasChildren) { TREE_ACTIONS.TOGGLE_EXPANDED(tree, node, $event); } else { this.selectedTreeNode.emit(node.data); } }, }, }, }; @Output() public selectedTreeNode = new EventEmitter(); @Output() public deletedTreeNode = new EventEmitter(); constructor() {} onNodeDeleted(node: DatasetFileNode): void { // look up for the DatasetVersionFileTreeNode this.deletedTreeNode.emit(node); } ngAfterViewInit(): void { if (this.isExpandAllAfterViewInit) { this.tree.treeModel.expandAll(); } } isImageFile(fileName: string): boolean { return IMAGE_EXTENSIONS.some(ext => fileName.toLowerCase().endsWith(ext)); } onSetCover(nodeData: DatasetFileNode): void { this.setCoverImage.emit(getRelativePathFromDatasetFileNode(nodeData)); } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.html ================================================
    ================================================ FILE: frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ @import "../../../section-style"; @import "../../../dashboard.component.scss"; /** * css for project label, shared by workflow section and file section **/ .dataset-list-item { margin-bottom: 10px; min-height: 70px; padding: 5px 0 5px 0; .dataset-item-meta { margin: 0 20px; } .dataset-item-meta-title { display: flex; align-items: center; .dataset-name { color: black; font-size: 20px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; text-align: center; } i { position: relative; font-size: 17px; margin-bottom: 6px; } i.dataset-is-owner-icon { margin-left: 7px; } } .dataset-item-meta-description { display: flex; align-items: center; padding: 2px 8px 2px 10px; margin-bottom: 8px; .dataset-description-label { font-size: 13px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; display: inline-block; min-width: 17ch; border: 0 none; outline: none; &:hover { cursor: pointer; box-shadow: 0 0 0 1px rgb(202, 202, 202); } } .dataset-editable-description-input { margin-bottom: 5px; display: inline-block; min-width: 17ch; border: 0 none; outline: none; box-shadow: 0 0 0 2px #007bff; } } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { Component, EventEmitter, Input, Output } from "@angular/core"; import { Dataset } from "../../../../../common/type/dataset"; import { DatasetService } from "../../../../service/user/dataset/dataset.service"; import { ShareAccessComponent } from "../../share-access/share-access.component"; import { NotificationService } from "../../../../../common/service/notification/notification.service"; import { NzModalService } from "ng-zorro-antd/modal"; import { DashboardDataset } from "../../../../type/dashboard-dataset.interface"; import { DASHBOARD_USER_DATASET } from "../../../../../app-routing.constant"; import { NzListItemComponent, NzListItemMetaComponent, NzListItemMetaAvatarComponent, NzListItemMetaTitleComponent, NzListItemMetaDescriptionComponent, NzListItemActionsComponent, NzListItemActionComponent, } from "ng-zorro-antd/list"; import { NgStyle, NgIf } from "@angular/common"; import { NzAvatarComponent } from "ng-zorro-antd/avatar"; import { RouterLink } from "@angular/router"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { NzPopconfirmDirective } from "ng-zorro-antd/popconfirm"; @UntilDestroy() @Component({ selector: "texera-user-dataset-list-item", templateUrl: "./user-dataset-list-item.component.html", styleUrls: ["./user-dataset-list-item.component.scss"], imports: [ NzListItemComponent, NzListItemMetaComponent, NzListItemMetaAvatarComponent, NgStyle, NzAvatarComponent, NzListItemMetaTitleComponent, NgIf, RouterLink, NzSpaceCompactItemDirective, NzButtonComponent, ɵNzTransitionPatchDirective, NzTooltipDirective, NzIconDirective, NzListItemMetaDescriptionComponent, NzListItemActionsComponent, NzListItemActionComponent, NzWaveDirective, NzPopconfirmDirective, ], }) export class UserDatasetListItemComponent { protected readonly DASHBOARD_USER_DATASET = DASHBOARD_USER_DATASET; private _entry?: DashboardDataset; @Output() refresh = new EventEmitter(); @Input() get entry(): DashboardDataset { if (!this._entry) { throw new Error("entry property must be provided to UserDatasetListItemComponent."); } return this._entry; } set entry(value: DashboardDataset) { this._entry = value; } get dataset(): Dataset { if (!this.entry.dataset) { throw new Error( "Incorrect type of DashboardEntry provided to UserDatasetListItemComponent. Entry must be dataset." ); } return this.entry.dataset; } @Input() editable = false; @Output() deleted = new EventEmitter(); @Output() duplicated = new EventEmitter(); editingName = false; editingDescription = false; constructor( private modalService: NzModalService, private datasetService: DatasetService, private notificationService: NotificationService ) {} public confirmUpdateDatasetCustomName(name: string) { if (this.entry.dataset.name === name) { return; } if (this.entry.dataset.did) this.datasetService .updateDatasetName(this.entry.dataset.did, name) .pipe(untilDestroyed(this)) .subscribe({ next: () => { this.entry.dataset.name = name; this.editingName = false; }, error: () => { this.notificationService.error("Update dataset name failed"); this.editingName = false; }, }); } public confirmUpdateDatasetCustomDescription(description: string) { if (this.entry.dataset.description === description) { return; } if (this.entry.dataset.did) this.datasetService .updateDatasetDescription(this.entry.dataset.did, description) .pipe(untilDestroyed(this)) .subscribe({ next: () => { this.entry.dataset.description = description; this.editingDescription = false; }, error: () => { this.notificationService.error("Update dataset description failed"); this.editingDescription = false; }, }); } public onClickOpenShareAccess() { this.modalService.create({ nzContent: ShareAccessComponent, nzData: { writeAccess: this.entry.accessPrivilege === "WRITE", type: "dataset", id: this.dataset.did, }, nzFooter: null, nzTitle: "Share this dataset with others", nzCentered: true, }); } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.html ================================================

    Datasets

    ================================================ FILE: frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ @import "../../dashboard.component"; @import "../../section-style"; @import "../../button-style"; .dataset-menu-bar { height: 55px; overflow-y: hidden; } ================================================ FILE: frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { AfterViewInit, Component, ViewChild } from "@angular/core"; import { UserService } from "../../../../common/service/user/user.service"; import { Router } from "@angular/router"; import { SearchService } from "../../../service/user/search.service"; import { DatasetService } from "../../../service/user/dataset/dataset.service"; import { SortMethod } from "../../../type/sort-method"; import { DashboardEntry } from "../../../type/dashboard-entry"; import { SearchResultsComponent } from "../search-results/search-results.component"; import { FiltersComponent } from "../filters/filters.component"; import { firstValueFrom } from "rxjs"; import { DASHBOARD_USER_DATASET } from "../../../../app-routing.constant"; import { NzModalService } from "ng-zorro-antd/modal"; import { UserDatasetVersionCreatorComponent } from "./user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component"; import { DashboardDataset } from "../../../type/dashboard-dataset.interface"; import { NzMessageService } from "ng-zorro-antd/message"; import { map, tap } from "rxjs/operators"; import { NzCardComponent } from "ng-zorro-antd/card"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { FiltersInstructionsComponent } from "../filters-instructions/filters-instructions.component"; import { NzSelectComponent } from "ng-zorro-antd/select"; import { FormsModule } from "@angular/forms"; @UntilDestroy() @Component({ selector: "texera-dataset-section", templateUrl: "user-dataset.component.html", styleUrls: ["user-dataset.component.scss"], imports: [ NzCardComponent, NzSpaceCompactItemDirective, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, NzIconDirective, FiltersComponent, FiltersInstructionsComponent, NzSelectComponent, FormsModule, SearchResultsComponent, ], }) export class UserDatasetComponent implements AfterViewInit { public sortMethod = SortMethod.EditTimeDesc; lastSortMethod: SortMethod | null = null; public isLogin = this.userService.isLogin(); public currentUid = this.userService.getCurrentUser()?.uid; public hasMismatch = false; // Display warning when there are mismatched datasets private _searchResultsComponent?: SearchResultsComponent; @ViewChild(SearchResultsComponent) get searchResultsComponent(): SearchResultsComponent { if (this._searchResultsComponent) { return this._searchResultsComponent; } throw new Error("Property cannot be accessed before it is initialized."); } set searchResultsComponent(value: SearchResultsComponent) { this._searchResultsComponent = value; } private _filters?: FiltersComponent; @ViewChild(FiltersComponent) get filters(): FiltersComponent { if (this._filters) { return this._filters; } throw new Error("Property cannot be accessed before it is initialized."); } set filters(value: FiltersComponent) { value.masterFilterListChange.pipe(untilDestroyed(this)).subscribe({ next: () => this.search() }); this._filters = value; } private masterFilterList: ReadonlyArray | null = null; constructor( private modalService: NzModalService, private userService: UserService, private router: Router, private searchService: SearchService, private datasetService: DatasetService, private message: NzMessageService ) { this.userService .userChanged() .pipe(untilDestroyed(this)) .subscribe(() => { this.isLogin = this.userService.isLogin(); this.currentUid = this.userService.getCurrentUser()?.uid; }); } ngAfterViewInit() { this.userService .userChanged() .pipe(untilDestroyed(this)) .subscribe(() => this.search()); } /* * Executes a dataset search with filtering, sorting. * * Parameters: * - filterScope = "all" | "public" | "private" - Determines visibility scope for search: * - "all": includes all datasets, public and private * - "public": limits the search to public datasets * - "private": limits the search to dataset where the user has direct access rights. */ async search(forced: Boolean = false, filterScope: "all" | "public" | "private" = "private"): Promise { const sameList = this.masterFilterList !== null && this.filters.masterFilterList.length === this.masterFilterList.length && this.filters.masterFilterList.every((v, i) => v === this.masterFilterList![i]); if (!forced && sameList && this.sortMethod === this.lastSortMethod) { // If the filter lists are the same, do no make the same request again. return; } this.lastSortMethod = this.sortMethod; this.masterFilterList = this.filters.masterFilterList; if (!this.searchResultsComponent) { throw new Error("searchResultsComponent is undefined."); } let filterParams = this.filters.getSearchFilterParameters(); // if the filter requires only public datasets, the public search should be invoked, and the search method should // set the isLogin parameter to false in this case const isLogin = filterScope === "public" ? false : this.isLogin; const includePublic = filterScope === "all" || filterScope === "public"; this.searchResultsComponent.reset((start, count) => { return firstValueFrom( this.searchService .executeSearch( this.filters.getSearchKeywords(), filterParams, start, count, "dataset", this.sortMethod, isLogin, includePublic ) .pipe( tap(({ hasMismatch }) => { this.hasMismatch = hasMismatch ?? false; if (this.hasMismatch) { this.message.warning( "There is a mismatch between some datasets in the database and LakeFS. Only matched datasets are displayed.", { nzDuration: 4000 } ); } }), map(({ entries, more }) => ({ entries, more })) ) ); }); await this.searchResultsComponent.loadMore(); } public onClickOpenDatasetAddComponent(): void { const modal = this.modalService.create({ nzTitle: "Create New Dataset", nzContent: UserDatasetVersionCreatorComponent, nzFooter: null, nzData: { isCreatingVersion: false, }, nzBodyStyle: { resize: "both", overflow: "auto", minHeight: "200px", minWidth: "550px", maxWidth: "90vw", maxHeight: "80vh", }, nzWidth: "fit-content", }); // Handle the selection from the modal modal.afterClose.pipe(untilDestroyed(this)).subscribe(result => { if (result != null) { const dashboardDataset: DashboardDataset = result as DashboardDataset; this.router.navigate([`${DASHBOARD_USER_DATASET}/${dashboardDataset.dataset.did}`]); } }); } public deleteDataset(entry: DashboardEntry): void { if (entry.dataset.dataset.did == undefined) { return; } this.datasetService .deleteDatasets(entry.dataset.dataset.did) .pipe(untilDestroyed(this)) .subscribe(_ => { this.searchResultsComponent.entries = this.searchResultsComponent.entries.filter( datasetEntry => datasetEntry.dataset.dataset.did !== entry.dataset.dataset.did ); }); } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-icon/user-icon.component.html ================================================
    • Sign Out
    ================================================ FILE: frontend/src/app/dashboard/component/user/user-icon/user-icon.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ ================================================ FILE: frontend/src/app/dashboard/component/user/user-icon/user-icon.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { NO_ERRORS_SCHEMA } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { UserIconComponent } from "./user-icon.component"; import { UserService } from "../../../../common/service/user/user.service"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { StubUserService } from "../../../../common/service/user/stub-user.service"; import { NzDropDownModule } from "ng-zorro-antd/dropdown"; import { RouterTestingModule } from "@angular/router/testing"; import { AboutComponent } from "../../../../hub/component/about/about.component"; import { commonTestProviders } from "../../../../common/testing/test-utils"; describe("UserIconComponent", () => { let component: UserIconComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ providers: [{ provide: UserService, useClass: StubUserService }, ...commonTestProviders], imports: [ UserIconComponent, RouterTestingModule.withRoutes([{ path: "home", component: AboutComponent }]), HttpClientTestingModule, NzDropDownModule, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(UserIconComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/dashboard/component/user/user-icon/user-icon.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component } from "@angular/core"; import { UserService } from "../../../../common/service/user/user.service"; import { User } from "../../../../common/type/user"; import { UntilDestroy } from "@ngneat/until-destroy"; import { Router } from "@angular/router"; import { DASHBOARD_ABOUT } from "../../../../app-routing.constant"; import { UserAvatarComponent } from "../user-avatar/user-avatar.component"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzDropdownDirective, NzDropdownMenuComponent } from "ng-zorro-antd/dropdown"; import { NzMenuDirective, NzMenuItemComponent } from "ng-zorro-antd/menu"; /** * UserIconComponent is used to control user system on the top right corner * It includes the button for login/registration/logout * It also includes what is shown on the top right */ @UntilDestroy() @Component({ selector: "texera-user-icon", templateUrl: "./user-icon.component.html", styleUrls: ["./user-icon.component.scss"], imports: [ UserAvatarComponent, ɵNzTransitionPatchDirective, NzDropdownDirective, NzDropdownMenuComponent, NzMenuDirective, NzMenuItemComponent, ], }) export class UserIconComponent { public user: User | undefined; constructor( private userService: UserService, private router: Router ) { this.user = this.userService.getCurrentUser(); } /** * handle the event when user click on the logout button */ public onClickLogout(): void { this.userService.logout(); document.cookie = "flarum_remember=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; this.router.navigate([DASHBOARD_ABOUT]); } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-project/public-project/public-project.component.html ================================================ ID Name Owner Creation Time {{ data.pid }} {{ data.name }} {{ data.owner }} {{ data.creationTime | date: "yyyy-MM-dd HH:mm" }} Selected {{ checkedList.size }} items ================================================ FILE: frontend/src/app/dashboard/component/user/user-project/public-project/public-project.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, inject, OnInit } from "@angular/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { PublicProjectService } from "../../../../service/user/public-project/public-project.service"; import { PublicProject } from "../../../../type/dashboard-project.interface"; import { NZ_MODAL_DATA, NzModalRef } from "ng-zorro-antd/modal"; import { NzTableComponent, NzTheadComponent, NzTrDirective, NzTableCellDirective, NzThMeasureDirective, NzThSelectionComponent, NzTbodyComponent, NzTdAddOnComponent, } from "ng-zorro-antd/table"; import { NgFor, DatePipe } from "@angular/common"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; @UntilDestroy() @Component({ templateUrl: "public-project.component.html", imports: [ NzTableComponent, NzTheadComponent, NzTrDirective, NzTableCellDirective, NzThMeasureDirective, NzThSelectionComponent, NzTbodyComponent, NgFor, NzTdAddOnComponent, NzSpaceCompactItemDirective, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, DatePipe, ], }) export class PublicProjectComponent implements OnInit { readonly modal = inject(NzModalRef); readonly disabledList: Set = inject(NZ_MODAL_DATA).disabledList; publicProjectEntries: PublicProject[] = []; checked = false; indeterminate = false; checkedList = new Set(); constructor(private publicProjectService: PublicProjectService) {} ngOnInit(): void { this.publicProjectService .getPublicProjects() .pipe(untilDestroyed(this)) .subscribe(publicProjects => (this.publicProjectEntries = publicProjects)); } updateCheckedSet(id: number, checked: boolean): void { if (checked) { this.checkedList.add(id); } else { this.checkedList.delete(id); } } onItemChecked(id: number, checked: boolean): void { this.updateCheckedSet(id, checked); this.refreshCheckedStatus(); } onAllChecked(value: boolean): void { this.publicProjectEntries.forEach(item => this.updateCheckedSet(item.pid, value)); this.refreshCheckedStatus(); } refreshCheckedStatus(): void { this.checked = this.publicProjectEntries.every(item => this.checkedList.has(item.pid)); this.indeterminate = this.publicProjectEntries.some(item => this.checkedList.has(item.pid)) && !this.checked; } addPublicProjects(): void { this.publicProjectService .addPublicProjects(Array.from(this.checkedList)) .pipe(untilDestroyed(this)) .subscribe(() => this.modal.destroy()); } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.html ================================================
    {{descriptionBox.value.length}}/{{MAX_PROJECT_DESCRIPTION_CHAR_COUNT}}

    Created: {{entry.creationTime | date: "yyyy-MM-dd HH:mm"}}

    ================================================ FILE: frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ @import "../../../dashboard.component.scss"; .project-name { font-size: 20px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; text-align: center; } .project-avatar-container { text-align: center; } .edit-name-icon:hover, .edit-description-icon:hover { color: skyblue; } .description-container { border: 1px solid gainsboro; border-radius: 10px; padding: 2.5%; } .character-count { text-align: right; } ================================================ FILE: frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, ViewChild } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { UserProjectListItemComponent } from "./user-project-list-item.component"; import { NotificationService } from "src/app/common/service/notification/notification.service"; import { UserProjectService } from "../../../../service/user/project/user-project.service"; import { DashboardProject } from "../../../../type/dashboard-project.interface"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { NzListComponent } from "ng-zorro-antd/list"; import { NzModalService } from "ng-zorro-antd/modal"; import { provideRouter } from "@angular/router"; import { StubUserService } from "../../../../../common/service/user/stub-user.service"; import { UserService } from "../../../../../common/service/user/user.service"; import { commonTestProviders } from "../../../../../common/testing/test-utils"; // UserProjectListItemComponent is rooted at ; instantiating it // outside an host throws "No provider found for NzListComponent". @Component({ standalone: true, imports: [NzListComponent, UserProjectListItemComponent], template: ` `, }) class TestHostComponent { entry!: DashboardProject; editable = true; @ViewChild(UserProjectListItemComponent, { static: true }) inner!: UserProjectListItemComponent; } describe("UserProjectListItemComponent", () => { let component: UserProjectListItemComponent; let hostFixture: ComponentFixture; const januaryFirst1970 = 28800000; // 1970-01-01 in PST const testProject: DashboardProject = { color: null, creationTime: januaryFirst1970, description: "description", name: "project1", ownerId: 1, pid: 1, accessLevel: "WRITE", }; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [TestHostComponent, HttpClientTestingModule], providers: [ NotificationService, UserProjectService, NzModalService, { provide: UserService, useClass: StubUserService }, provideRouter([]), ...commonTestProviders, ], }).compileComponents(); }); beforeEach(() => { hostFixture = TestBed.createComponent(TestHostComponent); hostFixture.componentInstance.entry = testProject; hostFixture.componentInstance.editable = true; hostFixture.detectChanges(); component = hostFixture.componentInstance.inner; }); it("should create", () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { DashboardProject } from "../../../../type/dashboard-project.interface"; import { UserProjectService } from "../../../../service/user/project/user-project.service"; import { NotificationService } from "src/app/common/service/notification/notification.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { ShareAccessComponent } from "../../share-access/share-access.component"; import { NzModalService } from "ng-zorro-antd/modal"; import { UserService } from "../../../../../common/service/user/user.service"; import { DASHBOARD_USER_PROJECT } from "../../../../../app-routing.constant"; import { NzListItemComponent, NzListItemMetaComponent, NzListItemMetaAvatarComponent, NzListItemMetaTitleComponent, NzListItemMetaDescriptionComponent, NzListItemActionsComponent, NzListItemActionComponent, } from "ng-zorro-antd/list"; import { NgStyle, NgIf, DatePipe } from "@angular/common"; import { NzAvatarComponent } from "ng-zorro-antd/avatar"; import { ColorPickerModule } from "ngx-color-picker"; import { RouterLink } from "@angular/router"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { MarkdownComponent } from "ngx-markdown"; import { NzInputGroupComponent, NzInputGroupWhitSuffixOrPrefixDirective, NzInputDirective, NzAutosizeDirective, } from "ng-zorro-antd/input"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { NzPopconfirmDirective } from "ng-zorro-antd/popconfirm"; import { HighlightSearchTermsPipe } from "../../user-workflow/user-workflow-list-item/highlight-search-terms.pipe"; @UntilDestroy() @Component({ selector: "texera-user-project-list-item", templateUrl: "./user-project-list-item.component.html", styleUrls: ["./user-project-list-item.component.scss"], imports: [ NzListItemComponent, NzListItemMetaComponent, NzListItemMetaAvatarComponent, NgStyle, NzAvatarComponent, ColorPickerModule, NzListItemMetaTitleComponent, NgIf, RouterLink, NzSpaceCompactItemDirective, NzButtonComponent, ɵNzTransitionPatchDirective, NzTooltipDirective, NzIconDirective, NzListItemMetaDescriptionComponent, MarkdownComponent, NzInputGroupComponent, NzInputGroupWhitSuffixOrPrefixDirective, NzInputDirective, NzAutosizeDirective, NzListItemActionsComponent, NzListItemActionComponent, NzWaveDirective, NzPopconfirmDirective, DatePipe, HighlightSearchTermsPipe, ], }) export class UserProjectListItemComponent implements OnInit { public readonly ROUTER_USER_PROJECT_BASE_URL = DASHBOARD_USER_PROJECT; public readonly MAX_PROJECT_DESCRIPTION_CHAR_COUNT = 10000; private _entry?: DashboardProject; @Input() public keywords: string[] = []; @Input() get entry(): DashboardProject { if (!this._entry) { throw new Error("entry property must be provided to UserProjectListItemComponent."); } return this._entry; } set entry(value: DashboardProject) { this._entry = value; } @Output() deleted = new EventEmitter(); @Output() refresh = new EventEmitter(); @Input() editable = false; @Input() uid: number | undefined; editingColor = false; editingName = false; editingDescription = false; descriptionCollapsed = true; color = "#ffffff"; isAdmin: boolean = false; /** To make sure info remains visible against white background */ get lightColor() { return UserProjectService.isLightColor(this.color); } constructor( private userProjectService: UserProjectService, private notificationService: NotificationService, private modalService: NzModalService, private userService: UserService ) { this.isAdmin = this.userService.isAdmin(); } ngOnInit(): void { if (this.entry.color) { this.color = this.entry.color; } } updateProjectColor(): void { if (!this.editable) { return; } const color = this.color.substring(1); this.editingColor = false; // validate that color is in proper HEX format if (UserProjectService.isInvalidColorFormat(color)) { this.notificationService.error( `Cannot update color for project: "${this.entry.name}". It must be a valid HEX color format` ); return; } if (color === this.entry.color) { return; } this.userProjectService .updateProjectColor(this.entry.pid, color) .pipe(untilDestroyed(this)) .subscribe(() => { this.color = color; this.entry = { ...this.entry, color: color }; }); } removeProjectColor(): void { this.editingColor = false; this.userProjectService .deleteProjectColor(this.entry.pid) .pipe(untilDestroyed(this)) .subscribe(() => { this.color = "#ffffff"; // reset color wheel this.entry = { ...this.entry, color: null }; }); } saveProjectName(name: string): void { // nothing happens if name is the same if (this.entry.name === name) { this.editingName = false; } else { this.userProjectService .updateProjectName(this.entry.pid, name) .pipe(untilDestroyed(this)) .subscribe(() => { if (!this.entry) { throw new Error("entry property must be provided to UserProjectListItemComponent."); } this.editingName = false; this.entry.name = name; }); } } saveProjectDescription(description: string): void { // nothing happens if description is the same if (this.entry.description === description) { this.editingDescription = false; return; } // update the project's description this.userProjectService .updateProjectDescription(this.entry.pid, description) .pipe(untilDestroyed(this)) .subscribe(() => { this.entry.description = description; this.notificationService.success(`Saved description for project: "${this.entry.name}".`); this.editingDescription = false; }); } public onClickOpenShareAccess(): void { const modalRef = this.modalService.create({ nzContent: ShareAccessComponent, nzData: { writeAccess: this.entry.accessLevel === "WRITE", type: "project", id: this.entry.pid, }, nzFooter: null, nzTitle: "Share this project with others", nzCentered: true, }); modalRef.afterClose.pipe(untilDestroyed(this)).subscribe(() => this.refresh.emit()); } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-project/user-project-section/ngbd-modal-add-project-workflow/ngbd-modal-add-project-workflow.component.html ================================================
    ID # Name Creation Time Last Modified Time
    {{workflowEntry.workflow.wid}} {{workflowEntry.workflow.name}} {{workflowEntry.workflow.creationTime | date: "yyyy-MM-dd HH:mm"}} {{workflowEntry.workflow.lastModifiedTime | date: "yyyy-MM-dd HH:mm"}}
    ================================================ FILE: frontend/src/app/dashboard/component/user/user-project/user-project-section/ngbd-modal-add-project-workflow/ngbd-modal-add-project-workflow.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .modal-body { min-height: 20vh; max-height: 60vh; overflow: auto; table { table-layout: fixed; width: 100%; #checkbox-col { width: 10%; } #id-col { width: 10%; } #name-col { width: 40%; } td { word-wrap: break-word; white-space: normal; } } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-project/user-project-section/ngbd-modal-add-project-workflow/ngbd-modal-add-project-workflow.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, inject, OnInit } from "@angular/core"; import { forkJoin, Observable } from "rxjs"; import { concatMap } from "rxjs/operators"; import { WorkflowPersistService } from "../../../../../../common/service/workflow-persist/workflow-persist.service"; import { DashboardWorkflow } from "../../../../../type/dashboard-workflow.interface"; import { UserProjectService } from "../../../../../service/user/project/user-project.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { NZ_MODAL_DATA } from "ng-zorro-antd/modal"; import { NzTheadComponent, NzTrDirective, NzTableCellDirective, NzThMeasureDirective, NzTbodyComponent, } from "ng-zorro-antd/table"; import { NgFor, DatePipe } from "@angular/common"; import { FormsModule } from "@angular/forms"; @UntilDestroy() @Component({ selector: "texera-add-project-workflow-modal", templateUrl: "./ngbd-modal-add-project-workflow.component.html", styleUrls: ["./ngbd-modal-add-project-workflow.component.scss"], imports: [ NzTheadComponent, NzTrDirective, NzTableCellDirective, NzThMeasureDirective, NzTbodyComponent, NgFor, FormsModule, DatePipe, ], }) export class NgbdModalAddProjectWorkflowComponent implements OnInit { readonly projectId: number = inject(NZ_MODAL_DATA).projectId; public unaddedWorkflows: DashboardWorkflow[] = []; // tracks which workflows to display, the ones that have not yet been added to the project public checkedWorkflows: boolean[] = []; // used to implement check boxes private addedWorkflowKeys: Set = new Set(); // tracks which workflows to NOT display, the workflow IDs of the workflows (if any) already inside the project private addedWorkflows: DashboardWorkflow[] = []; // for passing back to update the frontend cache, stores the new workflow list including newly added workflows constructor( private workflowPersistService: WorkflowPersistService, private userProjectService: UserProjectService ) {} ngOnInit(): void { this.refreshProjectWorkflowEntries(); } public submitForm() { // data structure to track group of updates to make to backend let observables: Observable[] = []; // process any selected workflows, updating backend then frontend cache for (let index = 0; index < this.checkedWorkflows.length; ++index) { if (this.checkedWorkflows[index]) { // if workflow is checked observables.push( this.userProjectService.addWorkflowToProject(this.projectId, this.unaddedWorkflows[index].workflow.wid!) ); this.addedWorkflows.push(this.unaddedWorkflows[index]); // for updating frontend cache } } // pass back data to update local cache after all changes propagated to backend forkJoin(observables).pipe(untilDestroyed(this)).subscribe(); } public changeAll() { if (this.isAllChecked()) { this.checkedWorkflows.fill(false); } else { this.checkedWorkflows.fill(true); } } public isAllChecked() { return this.checkedWorkflows.length > 0 && this.checkedWorkflows.every(isChecked => isChecked); } private refreshProjectWorkflowEntries(): void { this.userProjectService .retrieveWorkflowsOfProject(this.projectId) .pipe( concatMap((dashboardWorkflowEntries: DashboardWorkflow[]) => { this.addedWorkflows = dashboardWorkflowEntries; dashboardWorkflowEntries.forEach(workflowEntry => this.addedWorkflowKeys.add(workflowEntry.workflow.wid!)); return this.workflowPersistService.retrieveWorkflowsBySessionUser(); }), untilDestroyed(this) ) .subscribe(dashboardWorkflowEntries => { this.unaddedWorkflows = dashboardWorkflowEntries.filter( workflowEntry => workflowEntry.workflow.wid !== undefined && !this.addedWorkflowKeys.has(workflowEntry.workflow.wid!) ); this.checkedWorkflows = new Array(this.unaddedWorkflows.length).fill(false); }); } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-project/user-project-section/ngbd-modal-remove-project-workflow/ngbd-modal-remove-project-workflow.component.html ================================================
    ID # Name Creation Time Last Modified Time
    {{workflowEntry.workflow.wid}} {{workflowEntry.workflow.name}} {{workflowEntry.workflow.creationTime | date: "yyyy-MM-dd HH:mm"}} {{workflowEntry.workflow.lastModifiedTime | date: "yyyy-MM-dd HH:mm"}}
    ================================================ FILE: frontend/src/app/dashboard/component/user/user-project/user-project-section/ngbd-modal-remove-project-workflow/ngbd-modal-remove-project-workflow.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .modal-body { min-height: 20vh; max-height: 60vh; overflow: auto; table { table-layout: fixed; width: 100%; #checkbox-col { width: 10%; } #id-col { width: 10%; } #name-col { width: 40%; } td { word-wrap: break-word; white-space: normal; } } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-project/user-project-section/ngbd-modal-remove-project-workflow/ngbd-modal-remove-project-workflow.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, inject, OnInit } from "@angular/core"; import { forkJoin, Observable } from "rxjs"; import { UserProjectService } from "../../../../../service/user/project/user-project.service"; import { DashboardWorkflow } from "../../../../../type/dashboard-workflow.interface"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { NZ_MODAL_DATA } from "ng-zorro-antd/modal"; import { NzTheadComponent, NzTrDirective, NzTableCellDirective, NzThMeasureDirective, NzTbodyComponent, } from "ng-zorro-antd/table"; import { NgFor, DatePipe } from "@angular/common"; import { FormsModule } from "@angular/forms"; @UntilDestroy() @Component({ selector: "texera-remove-project-workflow-modal", templateUrl: "./ngbd-modal-remove-project-workflow.component.html", styleUrls: ["./ngbd-modal-remove-project-workflow.component.scss"], imports: [ NzTheadComponent, NzTrDirective, NzTableCellDirective, NzThMeasureDirective, NzTbodyComponent, NgFor, FormsModule, DatePipe, ], }) export class NgbdModalRemoveProjectWorkflowComponent implements OnInit { readonly projectId: number = inject(NZ_MODAL_DATA).projectId; public checkedWorkflows: boolean[] = []; // used to implement check boxes public addedWorkflows: DashboardWorkflow[] = []; // for passing back to update the frontend cache, stores the new workflow list with selected ones removed constructor(private userProjectService: UserProjectService) {} ngOnInit(): void { this.refreshProjectWorkflowEntries(); } public submitForm() { let observables: Observable[] = []; for (let index = this.checkedWorkflows.length - 1; index >= 0; --index) { if (this.checkedWorkflows[index]) { observables.push( this.userProjectService.removeWorkflowFromProject(this.projectId, this.addedWorkflows[index].workflow.wid!) ); this.addedWorkflows.splice(index, 1); // for updating frontend cache } } forkJoin(observables).pipe(untilDestroyed(this)).subscribe(); } public isAllChecked() { return this.checkedWorkflows.length > 0 && this.checkedWorkflows.every(isChecked => isChecked); } public changeAll() { if (this.isAllChecked()) { this.checkedWorkflows.fill(false); } else { this.checkedWorkflows.fill(true); } } private refreshProjectWorkflowEntries(): void { this.userProjectService .retrieveWorkflowsOfProject(this.projectId) .pipe(untilDestroyed(this)) .subscribe(dashboardWorkflowEntries => { this.addedWorkflows = dashboardWorkflowEntries; this.checkedWorkflows = new Array(this.addedWorkflows.length).fill(false); }); } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-project/user-project-section/user-project-section.component.html ================================================

    Project : {{name}}

    color
    x
    ================================================ FILE: frontend/src/app/dashboard/component/user/user-project/user-project-section/user-project-section.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .user-project-container { display: flex; flex-direction: column; height: 100%; } /* project info */ .description-container { padding: 2.5%; } /* for workflow items */ /* .saved-workflow-container { display: grid; grid-template-rows: 120px calc((100% - 120px)); } */ .ant-btn-icon-only { margin-right: 10px; } .subsection-grid-container { min-width: 100%; width: 100%; min-height: 100%; height: 100%; } .workflow-list-container { grid-row-start: 2; grid-row-end: 3; overflow: auto; } .workflow-list-item { margin-bottom: 1.5%; height: 70px; padding: 5px 0 5px 0; } .time-space { margin-left: 3%; } .workflow-name { font-size: 20px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; text-align: center; } .workflow-dashboard-entry { display: flex; align-items: center; } .workflow-dashboard-entry > i { position: relative; font-size: 17px; margin-bottom: 6px; } .workflow-name:hover { cursor: pointer; } .workflow-time { font-size: 12px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; color: gray; } button { border-radius: 5px; border: none; } .color-tag { display: inline-block; cursor: pointer; } #left-div { padding: 1.5px 7px; border-top-left-radius: 2px; border-bottom-left-radius: 2px; } #right-div { padding: 1.5px 7px; border-top-right-radius: 2px; border-bottom-right-radius: 2px; margin-right: 10px; } .light-color { border: 1px solid gainsboro; } .light-color:hover { background-image: linear-gradient(rgba(0, 0, 0, 0.4) 0 0); } .dark-color:hover { background-image: linear-gradient(rgba(255, 255, 255, 0.345) 0 0); } // temporarily here for the file section, TODO : will be removed once file service PR approved .file-project-label-container { width: 40%; display: flex; flex-direction: row-reverse; overflow-x: auto; .file-project-label { display: inline; white-space: nowrap; } } texera-saved-workflow-section { flex: 1; } ================================================ FILE: frontend/src/app/dashboard/component/user/user-project/user-project-section/user-project-section.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, OnInit } from "@angular/core"; import { UserProjectService } from "../../../../service/user/project/user-project.service"; import { ActivatedRoute } from "@angular/router"; import { DashboardFile } from "../../../../type/dashboard-file.interface"; import { NotificationService } from "../../../../../common/service/notification/notification.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { isDefined } from "../../../../../common/util/predicate"; import { NzCardComponent, NzCardMetaComponent } from "ng-zorro-antd/card"; import { NgIf, NgClass, NgStyle, DatePipe } from "@angular/common"; import { MarkdownComponent } from "ngx-markdown"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { ColorPickerModule } from "ngx-color-picker"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { UserWorkflowComponent } from "../../user-workflow/user-workflow.component"; @UntilDestroy() @Component({ selector: "texera-user-project-section", templateUrl: "./user-project-section.component.html", styleUrls: ["./user-project-section.component.scss"], imports: [ NzCardComponent, NgIf, MarkdownComponent, NzCardMetaComponent, NgClass, NgStyle, NzTooltipDirective, NzSpaceCompactItemDirective, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, ColorPickerModule, NzIconDirective, UserWorkflowComponent, DatePipe, ], }) export class UserProjectSectionComponent implements OnInit { // information from the database about this project public pid?: number = undefined; public name: string = ""; public description: string = ""; public ownerID: number = 0; public creationTime: number = 0; public accessLevel: string = "READ"; public color: string | null = null; // information for modifying project color public inputColor: string = "#ffffff"; // needs to have a '#' in front, as it is used by ngx-color-picker public colorIsBright: boolean = false; public projectDataIsLoaded: boolean = false; public colorPickerIsSelected: boolean = false; public updateProjectStatus = ""; // track any updates to user project for child components to rerender constructor( private userProjectService: UserProjectService, private activatedRoute: ActivatedRoute, private notificationService: NotificationService ) {} ngOnInit(): void { // extract passed PID from parameter and re-render page if necessary this.activatedRoute.url.pipe(untilDestroyed(this)).subscribe(url => { if (url.length == 2 && url[1].path) { this.pid = parseInt(url[1].path); this.getUserProjectMetadata(); this.userProjectService.refreshFilesOfProject(this.pid); // TODO : remove after refactoring file section } }); // otherwise no project ID, no project to load } public getUserProjectFilesArray(): ReadonlyArray { const fileArray = this.userProjectService.getProjectFiles(); if (!fileArray) { return []; } return fileArray; } public updateProjectColor(color: string) { color = color.substring(1); this.colorPickerIsSelected = false; if (UserProjectService.isInvalidColorFormat(color)) { this.notificationService.error("Cannot update project color. Color must be in valid HEX format"); return; } if (this.color === color) { return; } if (!isDefined(this.pid)) { return; } this.userProjectService .updateProjectColor(this.pid, color) .pipe(untilDestroyed(this)) .subscribe({ next: () => { this.color = color; this.colorIsBright = UserProjectService.isLightColor(this.color); this.updateProjectStatus = "updated project color"; // cause workflow / file components to update project filtering list }, error: (e: unknown) => this.notificationService.error((e as Error).message), }); } public removeProjectColor() { this.colorPickerIsSelected = false; if (this.color == null) { this.notificationService.error("There is no color to delete for this project"); return; } this.userProjectService .deleteProjectColor(this.pid!) .pipe(untilDestroyed(this)) .subscribe(_ => { this.color = null; this.inputColor = "#ffffff"; this.updateProjectStatus = "removed project color"; // cause workflow / file components to update project filtering list }); } private getUserProjectMetadata() { if (!isDefined(this.pid)) { return; } this.userProjectService .retrieveProject(this.pid) .pipe(untilDestroyed(this)) .subscribe(project => { this.name = project.name; this.ownerID = project.ownerId; this.creationTime = project.creationTime; if (project.color != null) { this.color = project.color; this.inputColor = "#" + project.color; this.colorIsBright = UserProjectService.isLightColor(project.color); } this.projectDataIsLoaded = true; }); this.userProjectService .getProjectList() .pipe(untilDestroyed(this)) .subscribe(userProjectList => { if (userProjectList != null && userProjectList.length > 0) { // calculate whether project colors are light or dark const projectColorBrightnessMap: Map = new Map(); userProjectList.forEach(userProject => { if (userProject.color != null) { projectColorBrightnessMap.set(userProject.pid, UserProjectService.isLightColor(userProject.color)); } // get single project information if (userProject.pid === this.pid) { this.name = userProject.name; this.description = userProject.description; this.ownerID = userProject.ownerId; this.creationTime = userProject.creationTime; this.accessLevel = userProject.accessLevel; if (userProject.color != null) { this.color = userProject.color; this.inputColor = "#" + userProject.color; this.colorIsBright = UserProjectService.isLightColor(userProject.color); } } }); this.projectDataIsLoaded = true; } }); } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-project/user-project.component.html ================================================

    Projects

    • By Time Created
    • A -> Z
    • Z -> A
    ================================================ FILE: frontend/src/app/dashboard/component/user/user-project/user-project.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ @import "../../section-style"; button { border-radius: 5px; border: none; } ================================================ FILE: frontend/src/app/dashboard/component/user/user-project/user-project.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, OnInit } from "@angular/core"; import { UserProjectService } from "../../../service/user/project/user-project.service"; import { DashboardProject } from "../../../type/dashboard-project.interface"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { NotificationService } from "../../../../common/service/notification/notification.service"; import { UserService } from "../../../../common/service/user/user.service"; import { NzModalService } from "ng-zorro-antd/modal"; import { PublicProjectComponent } from "./public-project/public-project.component"; import { NzCardComponent } from "ng-zorro-antd/card"; import { NzDropdownADirective, NzDropdownDirective, NzDropdownMenuComponent } from "ng-zorro-antd/dropdown"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzMenuDirective, NzMenuItemComponent } from "ng-zorro-antd/menu"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NgIf, NgFor } from "@angular/common"; import { NzInputDirective } from "ng-zorro-antd/input"; import { FormsModule } from "@angular/forms"; import { NzListComponent } from "ng-zorro-antd/list"; import { UserProjectListItemComponent } from "./user-project-list-item/user-project-list-item.component"; @UntilDestroy() @Component({ selector: "texera-user-project-list", templateUrl: "./user-project.component.html", styleUrls: ["./user-project.component.scss"], imports: [ NzCardComponent, NzDropdownADirective, NzDropdownDirective, NzSpaceCompactItemDirective, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, NzIconDirective, NzDropdownMenuComponent, NzMenuDirective, NzMenuItemComponent, NzTooltipDirective, NgIf, NzInputDirective, FormsModule, NzListComponent, NgFor, UserProjectListItemComponent, ], }) export class UserProjectComponent implements OnInit { // store list of projects / variables to create and edit projects public userProjectEntries: DashboardProject[] = []; public createButtonIsClicked: boolean = false; public createProjectName: string = ""; public uid: number | undefined; constructor( private userProjectService: UserProjectService, private notificationService: NotificationService, private userService: UserService, private modalService: NzModalService ) { this.uid = this.userService.getCurrentUser()?.uid; } ngOnInit(): void { this.userProjectService .getProjectList() .pipe(untilDestroyed(this)) .subscribe(projectEntries => { this.userProjectEntries = projectEntries; }); } public deleteProject(pid: number): void { this.userProjectService .deleteProject(pid) .pipe(untilDestroyed(this)) .subscribe(() => this.ngOnInit()); } public clickCreateButton(): void { this.createButtonIsClicked = true; } public unclickCreateButton(): void { this.createButtonIsClicked = false; this.createProjectName = ""; } public createNewProject(): void { if (this.isValidNewProjectName(this.createProjectName)) { this.userProjectService .createProject(this.createProjectName) .pipe(untilDestroyed(this)) .subscribe(() => this.ngOnInit()); } else { this.notificationService.error( `Cannot create project named: "${this.createProjectName}". It must be a non-empty, unique name` ); } } public sortByCreationTime(): void { this.userProjectEntries.sort((p1, p2) => p1.creationTime !== undefined && p2.creationTime !== undefined ? p1.creationTime - p2.creationTime : 0 ); } public sortByNameAsc(): void { this.userProjectEntries.sort((p1, p2) => p1.name.toLowerCase().localeCompare(p2.name.toLowerCase())); } public sortByNameDesc(): void { this.userProjectEntries.sort((p1, p2) => p2.name.toLowerCase().localeCompare(p1.name.toLowerCase())); } private isValidNewProjectName(newName: string, oldProject?: DashboardProject): boolean { if (typeof oldProject === "undefined") { return newName.length != 0 && this.userProjectEntries.filter(project => project.name === newName).length === 0; } else { return ( newName.length != 0 && this.userProjectEntries.filter(project => project.pid !== oldProject.pid && project.name === newName).length === 0 ); } } public openPublicProject(): void { const modalRef = this.modalService.create({ nzContent: PublicProjectComponent, nzData: { disabledList: new Set(this.userProjectEntries.map(project => project.pid)) }, nzFooter: null, nzTitle: "Add Public Projects", nzCentered: true, }); modalRef.afterClose.pipe(untilDestroyed(this)).subscribe(() => this.ngOnInit()); } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-quota/user-quota.component.html ================================================

    Quota

    Files Uploaded

    {{ this.createdFiles.length }}

    Datasets Uploaded

    {{ this.totalUploadedDatasetCount }}

    Total Size of Datasets

    {{ formatSize(this.totalUploadedDatasetSize) }}

    Workflows Created

    {{ this.createdWorkflows.length }}

    Files with Access

    {{ this.accessFiles.length }}

    Workflows with Access

    {{ this.accessWorkflows.length }}

    Total Size of the Files

    {{ formatSize(this.totalFileSize) }}

    Total Result Cache Size

    {{ formatSize(this.totalQuotaSize) }}

    Collection Name Execution ID Cache Size Action {{ execution.workflowName }} {{ execution.eid }} {{ formatSize(execution.resultBytes + execution.logBytes + execution.runTimeStatsBytes) }}
    ================================================ FILE: frontend/src/app/dashboard/component/user/user-quota/user-quota.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .info-container { display: flex; flex-wrap: wrap; justify-content: center; } .info-box { background-color: #f4f4f4; padding: 40px; margin: 10px; width: 375px; height: 135px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); font-family: Arial, sans-serif; text-align: center; } .info-heading { font-weight: bold; font-size: 18px; } .info-content { font-size: 24px; margin-top: 10px; } .charts-grid { display: flex; flex-wrap: wrap; justify-content: center; height: 70vh; overflow-y: auto; > div { display: flex; margin: 20px; justify-content: center; align-items: center; background: #fff; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); padding: 10px; &:hover { box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); } } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-quota/user-quota.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { NO_ERRORS_SCHEMA } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { UserQuotaComponent } from "./user-quota.component"; import { UserQuotaService } from "../../../service/user/quota/user-quota.service"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { commonTestProviders } from "../../../../common/testing/test-utils"; import { of } from "rxjs"; import type { Mocked } from "vitest"; describe("UserQuotaComponent", () => { let component: UserQuotaComponent; let fixture: ComponentFixture; let mockUserQuotaService: Mocked; beforeEach(() => { mockUserQuotaService = { getCreatedDatasets: vi.fn(), getCreatedWorkflows: vi.fn(), getAccessWorkflows: vi.fn(), getExecutionQuota: vi.fn(), deleteExecutionCollection: vi.fn(), } as unknown as Mocked; mockUserQuotaService.getCreatedDatasets.mockReturnValue(of([])); mockUserQuotaService.getCreatedWorkflows.mockReturnValue(of([])); mockUserQuotaService.getAccessWorkflows.mockReturnValue(of([])); mockUserQuotaService.getExecutionQuota.mockReturnValue(of([])); TestBed.configureTestingModule({ providers: [{ provide: UserQuotaService, useValue: mockUserQuotaService }, ...commonTestProviders], imports: [UserQuotaComponent, HttpClientTestingModule], schemas: [NO_ERRORS_SCHEMA], }); fixture = TestBed.createComponent(UserQuotaComponent); component = fixture.componentInstance; }); it("should create", () => { fixture.detectChanges(); expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/dashboard/component/user/user-quota/user-quota.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, inject, OnInit } from "@angular/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { ExecutionQuota, File, Workflow, WorkflowQuota } from "../../../../common/type/user"; import { DatasetQuota } from "src/app/dashboard/type/quota-statistic.interface"; import { NzTableSortFn, NzTableComponent, NzTheadComponent, NzTrDirective, NzTableCellDirective, NzThMeasureDirective, NzThAddOnComponent, NzTbodyComponent, } from "ng-zorro-antd/table"; import { UserQuotaService } from "src/app/dashboard/service/user/quota/user-quota.service"; import { AdminUserService } from "src/app/dashboard/service/admin/user/admin-user.service"; import { NZ_MODAL_DATA } from "ng-zorro-antd/modal"; import * as Plotly from "plotly.js-basic-dist-min"; import { formatSize } from "src/app/common/util/size-formatter.util"; import { NzCardComponent } from "ng-zorro-antd/card"; import { NzTabsComponent, NzTabComponent } from "ng-zorro-antd/tabs"; import { NzCollapseComponent, NzCollapsePanelComponent } from "ng-zorro-antd/collapse"; import { NgFor } from "@angular/common"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { NzPopconfirmDirective } from "ng-zorro-antd/popconfirm"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzIconDirective } from "ng-zorro-antd/icon"; type UserServiceType = AdminUserService | UserQuotaService; @UntilDestroy() @Component({ templateUrl: "./user-quota.component.html", styleUrls: ["./user-quota.component.scss"], imports: [ NzCardComponent, NzTabsComponent, NzTabComponent, NzCollapseComponent, NgFor, NzCollapsePanelComponent, NzTableComponent, NzTheadComponent, NzTrDirective, NzTableCellDirective, NzThMeasureDirective, NzThAddOnComponent, NzTbodyComponent, NzSpaceCompactItemDirective, NzButtonComponent, NzWaveDirective, NzPopconfirmDirective, ɵNzTransitionPatchDirective, NzTooltipDirective, NzIconDirective, ], }) export class UserQuotaComponent implements OnInit { readonly userId: number; backgroundColor: String = "white"; textColor: String = "Black"; dynamicHeight: string = "700px"; totalFileSize: number = 0; totalQuotaSize: number = 0; totalUploadedDatasetSize: number = 0; totalUploadedDatasetCount: number = 0; createdFiles: ReadonlyArray = []; createdWorkflows: ReadonlyArray = []; accessFiles: ReadonlyArray = []; accessWorkflows: ReadonlyArray = []; executionCollections: ReadonlyArray = []; datasetList: ReadonlyArray = []; workflows: Array = []; UserService: UserServiceType; DEFAULT_PIE_CHART_WIDTH = 480; DEFAULT_PIE_CHART_HEIGHT = 340; DEFAULT_LINE_CHART_WIDTH = 480; DEFAULT_LINE_CHART_HEIGHT = 340; constructor( private adminUserService: AdminUserService, private regularUserService: UserQuotaService ) { this.UserService = adminUserService; if (inject(NZ_MODAL_DATA, { optional: true })) { this.userId = inject(NZ_MODAL_DATA).uid; this.UserService = this.adminUserService; this.backgroundColor = "lightcoral"; this.textColor = "white"; } else { this.userId = -1; this.UserService = this.regularUserService; this.dynamicHeight = ""; } } ngOnInit(): void { this.refreshData(); } /* takes in an array of tuple ('label', 'value') and generates the corresponding pie chart */ generatePieChart(dataToDisplay: Array<[string, ...number[]]>, title: string, chart: string) { var data = [ { values: dataToDisplay.map(d => d[1]), labels: dataToDisplay.map(d => d[0]), type: "pie" as const, }, ]; var layout = { height: this.DEFAULT_PIE_CHART_HEIGHT, width: this.DEFAULT_PIE_CHART_WIDTH, title: { text: title, }, }; Plotly.newPlot(chart, data, layout); } filterOutdatedData(data: Array<[string, number]>): Array<[string, number]> { const oneYearAgo = new Date(); oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1); return data.filter(([date]) => new Date(date) >= oneYearAgo); } aggregateByMonth(data: Array<[string, number]>): Array<[string, number]> { const monthMap = new Map(); data.forEach(([date, value]) => { const month = date.substring(0, 7); // 'YYYY-MM' if (monthMap.has(month)) { monthMap.set(month, monthMap.get(month)! + value); } else { monthMap.set(month, value); } }); return Array.from(monthMap, ([date, value]) => [date, value]); } aggregateData(data: Array<[string, number]>, numGroup: number) { data = this.filterOutdatedData(data); if (data.length < 8) { return data; } const uniqueMonths = new Set(data.map(([date]) => date.substring(0, 7))); if (uniqueMonths.size >= 3) { return this.aggregateByMonth(data); } const startDate = new Date(data[0][0]); const endDate = new Date(data[data.length - 1][0]); const newOfDays = (endDate.getTime() - startDate.getTime()) / (1000 * 3600 * 24); const daysPerGroup = Math.ceil(newOfDays / numGroup); let aggData: Array<[string, number]> = []; let currentGroupStartDate = startDate; let sum = 0; let nextDate = new Date(currentGroupStartDate); nextDate.setDate(currentGroupStartDate.getDate() + daysPerGroup); data.forEach(([date, value]) => { const currentDate = new Date(date); if (currentDate < nextDate) { sum += value; } else { aggData.push([currentGroupStartDate.toISOString().split("T")[0], sum]); currentGroupStartDate = new Date(nextDate); nextDate.setDate(currentGroupStartDate.getDate() + daysPerGroup); sum = value; } }); aggData.push([currentGroupStartDate.toISOString().split("T")[0], sum]); return aggData; } generateLineChart( dataToDisplay: Array<[string, number]>, x_label: string, y_label: string, title: string, chart: string ) { var data = [ { x: dataToDisplay.map(d => d[0]), y: dataToDisplay.map(d => d[1]), type: "scatter" as const, }, ]; const yValues = dataToDisplay.map(d => d[1]); const maxY = Math.max(...yValues); const minY = Math.min(...yValues); const yRange = maxY - minY; var layout = { height: this.DEFAULT_LINE_CHART_HEIGHT, width: this.DEFAULT_LINE_CHART_WIDTH, title: { text: title, }, xaxis: { title: { text: x_label, }, }, yaxis: { title: { text: y_label, }, rangemode: "tozero" as const, zeroline: true, zerolinewidth: 2, zerolinecolor: "#000", tickmode: yRange <= 5 ? ("linear" as const) : undefined, dtick: yRange <= 5 ? 1 : undefined, }, }; Plotly.newPlot(chart, data, layout); } refreshData() { this.UserService.getCreatedDatasets(this.userId) .pipe(untilDestroyed(this)) .subscribe(datasetList => { this.datasetList = datasetList; let totalDatasetSize = 0; this.totalUploadedDatasetCount = datasetList.length; let pieChartData: Array<[string, ...number[]]> = []; let lineChartData: Map = new Map(); this.datasetList.forEach(dataset => { totalDatasetSize += dataset.size; pieChartData.push([dataset.name, dataset.size]); const date = new Date(dataset.creationTime).toLocaleDateString(); if (lineChartData.has(date)) { lineChartData.set(date, lineChartData.get(date)! + 1); } else { lineChartData.set(date, 1); } }); this.generatePieChart(pieChartData, "Dataset Size Distribution", "sizePieChart"); let lineChartDataArray: Array<[string, number]> = []; lineChartData.forEach((count, date) => { lineChartDataArray.push([date, count]); }); lineChartDataArray = this.aggregateData(lineChartDataArray, 5); this.generateLineChart(lineChartDataArray, "Date", "Count", "Dataset Upload Overview", "datasetLineChart"); this.totalUploadedDatasetSize = totalDatasetSize; }); this.UserService.getCreatedWorkflows(this.userId) .pipe(untilDestroyed(this)) .subscribe(workflowList => { let lineChartData: Map = new Map(); this.createdWorkflows = workflowList; this.createdWorkflows.forEach(workflow => { const date = new Date(workflow.creationTime).toLocaleDateString(); if (lineChartData.has(date)) { lineChartData.set(date, lineChartData.get(date)! + 1); } else { lineChartData.set(date, 1); } }); let lineChartDataArray: Array<[string, number]> = []; lineChartData.forEach((count, date) => { lineChartDataArray.push([date, count]); }); lineChartDataArray = this.aggregateData(lineChartDataArray, 5); this.generateLineChart(lineChartDataArray, "Date", "Count", "Workflow Upload Overview", "workflowLineChart"); }); this.UserService.getAccessWorkflows(this.userId) .pipe(untilDestroyed(this)) .subscribe(accessWorkflows => { this.accessWorkflows = accessWorkflows; }); this.UserService.getExecutionQuota(this.userId) .pipe(untilDestroyed(this)) .subscribe(executionList => { this.totalQuotaSize = 0; this.executionCollections = executionList; this.workflows = []; this.executionCollections.forEach(execution => { this.totalQuotaSize += execution.resultBytes + execution.runTimeStatsBytes + execution.logBytes; let workflow = this.workflows.find( w => w.executions.length > 0 && w.executions[0].workflowId === execution.workflowId ); if (!workflow) { workflow = { workflowId: execution.workflowId, workflowName: execution.workflowName, executions: [], }; this.workflows.push(workflow); } workflow.executions.push(execution); }); }); } deleteCollection(eid: number) { this.UserService.deleteExecutionCollection(eid) .pipe(untilDestroyed(this)) .subscribe(() => { this.workflows.forEach((workflow, index, array) => { const executionToDelete = workflow.executions.find(execution => execution.eid === eid); if (executionToDelete) { this.totalQuotaSize -= executionToDelete.resultBytes + executionToDelete.logBytes + executionToDelete.runTimeStatsBytes; workflow.executions = workflow.executions.filter(execution => execution.eid !== eid); } }); this.workflows = this.workflows.filter(workflow => workflow.executions.length > 0); }); } // alias for formatSize formatSize = formatSize; public sortBySize: NzTableSortFn = (a: ExecutionQuota, b: ExecutionQuota) => b.resultBytes + b.logBytes + b.runTimeStatsBytes - a.resultBytes - a.logBytes - a.runTimeStatsBytes; } ================================================ FILE: frontend/src/app/dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-execution-history.component.html ================================================
    {{column}} {{ row.cuId }} {{row.startingTime | date:'MM/dd/yyyy HH:mm:ss'}} {{row.completionTime | date:'MM/dd/yyyy HH:mm:ss'}}
    ================================================ FILE: frontend/src/app/dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-execution-history.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ #search-box input { width: 100%; font-size: 11px; padding: 0px; margin: 0; } .ant-table-thead > tr > th { padding: 0px; } nz-table th { text-align: center; } .version-sample { flex-grow: 2; text-align: center; } .ant-btn { border: 0; background-color: transparent; } .workflow-snapshot { float: left; display: inline; border: 1px solid #343a40; width: 170px; height: 95px; } #exection-history-table { width: 100%; height: 100%; font-size: 11px; padding: 0px; margin: 0; } .bookmark-icon:hover { cursor: pointer; } .rename-icon:hover { color: #007bff; } .ant-table-cell { font-size: 11px; padding: 0px; min-width: 50px; box-sizing: border-box; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 0; } .ant-table { width: 100%; table-layout: fixed; } ================================================ FILE: frontend/src/app/dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-execution-history.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { AfterViewInit, Component, Inject, OnInit, Optional } from "@angular/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { EXECUTION_STATUS_CODE, WorkflowExecutionsEntry } from "../../../../type/workflow-executions-entry"; import { WorkflowExecutionsService } from "../../../../service/user/workflow-executions/workflow-executions.service"; import { ExecutionState } from "../../../../../workspace/types/execute-workflow.interface"; import { NotificationService } from "../../../../../common/service/notification/notification.service"; import { WorkflowActionService } from "../../../../../workspace/service/workflow-graph/model/workflow-action.service"; import Fuse from "fuse.js"; import { ceil } from "lodash"; import { NZ_MODAL_DATA, NzModalRef, NzModalService } from "ng-zorro-antd/modal"; import { WorkflowRuntimeStatisticsComponent } from "./workflow-runtime-statistics/workflow-runtime-statistics.component"; import * as Plotly from "plotly.js-basic-dist-min"; import { ActivatedRoute } from "@angular/router"; import { NzCardComponent } from "ng-zorro-antd/card"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzInputGroupComponent, NzInputGroupWhitSuffixOrPrefixDirective, NzInputDirective } from "ng-zorro-antd/input"; import { FormsModule } from "@angular/forms"; import { NzAutocompleteTriggerDirective, NzAutocompleteComponent } from "ng-zorro-antd/auto-complete"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzPopoverDirective } from "ng-zorro-antd/popover"; import { NgIf, NgStyle, NgFor, DatePipe } from "@angular/common"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzPopconfirmDirective } from "ng-zorro-antd/popconfirm"; import { NzTabsComponent, NzTabComponent } from "ng-zorro-antd/tabs"; import { NzTableComponent, NzTheadComponent, NzTrDirective, NzTableCellDirective, NzThMeasureDirective, NzThSelectionComponent, NzTbodyComponent, NzCellEllipsisDirective, NzTdAddOnComponent, } from "ng-zorro-antd/table"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { UserAvatarComponent } from "../../user-avatar/user-avatar.component"; import { NzRowDirective, NzColDirective } from "ng-zorro-antd/grid"; const MAX_TEXT_SIZE = 20; const MAX_RGB = 255; const MAX_USERNAME_SIZE = 5; @UntilDestroy() @Component({ selector: "texera-ngbd-modal-workflow-executions", templateUrl: "./workflow-execution-history.component.html", styleUrls: ["./workflow-execution-history.component.scss"], imports: [ NzCardComponent, ɵNzTransitionPatchDirective, NzSpaceCompactItemDirective, NzInputGroupComponent, NzInputGroupWhitSuffixOrPrefixDirective, NzInputDirective, FormsModule, NzAutocompleteTriggerDirective, NzIconDirective, NzPopoverDirective, NzAutocompleteComponent, NgIf, NzTooltipDirective, NzPopconfirmDirective, NzTabsComponent, NzTabComponent, NzTableComponent, NzTheadComponent, NzTrDirective, NzTableCellDirective, NzThMeasureDirective, NzThSelectionComponent, NgStyle, NgFor, NzButtonComponent, NzWaveDirective, NzTbodyComponent, NzCellEllipsisDirective, NzTdAddOnComponent, UserAvatarComponent, NzRowDirective, NzColDirective, DatePipe, ], }) export class WorkflowExecutionHistoryComponent implements OnInit, AfterViewInit { wid: number = 0; public static readonly USERNAME_PIE_CHART_ID = "#execution-userName-pie-chart"; public static readonly STATUS_PIE_CHART_ID = "#execution-status-pie-chart"; public static readonly PROCESS_TIME_BAR_CHART = "#execution-average-process-time-bar-chart"; public static readonly WIDTH = 450; public static readonly HEIGHT = 450; public static readonly BARCHARTSIZE = 600; // Instance properties referencing the static ones public usernamePieChartId = WorkflowExecutionHistoryComponent.USERNAME_PIE_CHART_ID; public statusPieChartId = WorkflowExecutionHistoryComponent.STATUS_PIE_CHART_ID; public processTimeBarChart = WorkflowExecutionHistoryComponent.PROCESS_TIME_BAR_CHART; public workflowExecutionsDisplayedList: WorkflowExecutionsEntry[] | undefined; public workflowExecutionsIsEditingName: number[] = []; public currentlyHoveredExecution: WorkflowExecutionsEntry | undefined; public executionsTableHeaders: string[] = [ "", "Avatar", "Name (ID)", "Computing Unit ID", "Execution Start Time", "Execution Completion Time", "Status", "Runtime Statistics", "", ]; /*Tooltip for each header in execution table*/ public executionTooltip: Record = { "Name (ID)": "Execution Name", "Computing Unit ID": "ID of the Computing Unit that ran the Workflow", Username: "The User Who Ran This Execution", "Execution Start Time": "Start Time of Workflow Execution", "Execution Completion Time": "Latest Status Updated Time of Workflow Execution", Status: "Current Status of Workflow Execution", "Runtime Statistics": "Runtime Statistics of Workflow Execution", "Group Bookmarking": "Mark or Unmark the Selected Entries", "Group Deletion": "Delete the Selected Entries", }; /*custom column width*/ public customColumnWidth: Record = { "": "0%", "Name (ID)": "7%", "Computing Unit ID": "7%", "Workflow Version Sample": "10%", Avatar: "5.5%", "Execution Start Time": "9%", "Execution Completion Time": "10.5%", Status: "2.5%", "Runtime Statistics": "6%", }; /** variables related to executions filtering */ public allExecutionEntries: WorkflowExecutionsEntry[] = []; public filteredExecutionInfo: Array = []; public executionSearchValue: string = ""; public searchCriteria: string[] = ["user", "status"]; public fuse = new Fuse([] as ReadonlyArray, { shouldSort: true, threshold: 0.2, location: 0, distance: 100, minMatchCharLength: 1, keys: ["name", "userName", "status"], }); // Pagination attributes public currentPageIndex: number = 1; public pageSize: number = 10; public pageSizeOptions: number[] = [5, 10, 20, 30, 40]; public paginatedExecutionEntries: WorkflowExecutionsEntry[] = []; public searchCriteriaPathMapping: Map = new Map([ ["executionName", ["name"]], ["user", ["userName"]], ["status", ["status"]], ]); public statusMapping: Map = new Map([ ["initializing", 0], ["running", 1], ["paused", 2], ["completed", 3], ["failed", 4], ["killed", 5], ]); public showORhide: boolean[] = [false, false, false, false, true]; public avatarColors: { [key: string]: string } = {}; public checked: boolean = false; public setOfEid = new Set(); public setOfExecution = new Set(); public averageProcessingTimeDivider: number = 10; modalRef?: NzModalRef; constructor( private workflowExecutionsService: WorkflowExecutionsService, private notificationService: NotificationService, private runtimeStatisticsModal: NzModalService, private workflowActionService: WorkflowActionService, private route: ActivatedRoute, @Optional() @Inject(NZ_MODAL_DATA) private modalData: any ) {} ngOnInit(): void { this.wid = this.modalData?.wid || this.route.snapshot.params["id"] || 0; // gets the workflow executions and display the runs in the table on the form this.displayWorkflowExecutions(); } ngAfterViewInit() { this.workflowExecutionsService .retrieveWorkflowExecutions(this.wid) .pipe(untilDestroyed(this)) .subscribe(workflowExecutions => { // generate charts data let userNameData: { [key: string]: [string, number] } = {}; let statusData: { [key: string]: [string, number] } = {}; workflowExecutions.forEach(execution => { if (userNameData[execution.userName] === undefined) { userNameData[execution.userName] = [execution.userName, 0]; } userNameData[execution.userName][1] += 1; if (statusData[EXECUTION_STATUS_CODE[execution.status]] === undefined) { statusData[EXECUTION_STATUS_CODE[execution.status]] = [EXECUTION_STATUS_CODE[execution.status], 0]; } statusData[EXECUTION_STATUS_CODE[execution.status]][1] += 1; }); this.generatePieChart( Object.values(userNameData), "Users who ran the execution", WorkflowExecutionHistoryComponent.USERNAME_PIE_CHART_ID ); this.generatePieChart( Object.values(statusData), "Executions status", WorkflowExecutionHistoryComponent.STATUS_PIE_CHART_ID ); // generate an average processing time bar chart const processTimeData: Array<[string, ...number[]]> = [["processing time"]]; const processTimeCategory: string[] = []; Object.entries(this.getBarChartProcessTimeData(workflowExecutions)).forEach(([eId, processTime]) => { processTimeData[0].push(processTime); processTimeCategory.push(eId); }); this.generateBarChart( processTimeData, processTimeCategory, "Execution Numbers", "Average Processing Time (m)", "Execution performance", WorkflowExecutionHistoryComponent.PROCESS_TIME_BAR_CHART ); }); } generatePieChart(dataToDisplay: Array<[string, ...number[]]>, title: string, chart: string) { var data = [ { values: dataToDisplay.map(d => d[1]), labels: dataToDisplay.map(d => d[0]), type: "pie" as const, }, ]; var layout = { height: WorkflowExecutionHistoryComponent.HEIGHT, width: WorkflowExecutionHistoryComponent.WIDTH, title: { text: title, }, }; Plotly.newPlot(chart, data, layout); } generateBarChart( dataToDisplay: Array<[string, ...number[]]>, category: string[], x_label: string, y_label: string, title: string, chart: string ) { var data = [ { x: category.map(c => `${c}`), y: dataToDisplay[0].slice(1), type: "bar" as const, }, ]; var layout = { title: { text: title, }, xaxis: { title: { text: x_label, }, }, yaxis: { title: { text: y_label, }, }, autosize: false, width: WorkflowExecutionHistoryComponent.BARCHARTSIZE, height: WorkflowExecutionHistoryComponent.BARCHARTSIZE, }; Plotly.newPlot(chart, data, layout); } /** * calls the service to display the workflow executions on the table */ displayWorkflowExecutions(): void { this.workflowExecutionsService .retrieveWorkflowExecutions(this.wid) .pipe(untilDestroyed(this)) .subscribe(workflowExecutions => { this.allExecutionEntries = workflowExecutions; this.dscSort("Execution Start Time"); this.updatePaginatedExecutions(); }); } /** * display icons corresponding to workflow execution status * * NOTES: Colors match with gui/src/app/workspace/service/joint-ui/joint-ui.service.ts line 347 * TODO: Move colors to a config file for changing them once for many files */ getExecutionStatus(statusCode: number): string[] { switch (statusCode) { case 0: return [ExecutionState.Initializing.toString(), "sync", "#a6bd37"]; case 1: return [ExecutionState.Running.toString(), "play-circle", "orange"]; case 2: return [ExecutionState.Paused.toString(), "pause-circle", "magenta"]; case 3: return [ExecutionState.Completed.toString(), "check-circle", "green"]; case 4: return [ExecutionState.Failed.toString(), "exclamation-circle", "gray"]; case 5: return [ExecutionState.Killed.toString(), "minus-circle", "red"]; } return ["", "question-circle", "gray"]; } onBookmarkToggle(row: WorkflowExecutionsEntry) { const wasPreviouslyBookmarked = row.bookmarked; // Update bookmark state locally. row.bookmarked = !wasPreviouslyBookmarked; // Update on the server. this.workflowExecutionsService .groupSetIsBookmarked(this.wid, [row.eId], wasPreviouslyBookmarked) .pipe(untilDestroyed(this)) .subscribe({ error: (_: unknown) => (row.bookmarked = wasPreviouslyBookmarked), }); } setBookmarked(): void { if (this.setOfExecution !== undefined) { // isBookmarked: true if all the execution are bookmarked, false if there is one that is unbookmarked const isBookmarked = !Array.from(this.setOfExecution).some(execution => { return execution.bookmarked === null || !execution.bookmarked; }); // update the bookmark locally this.setOfExecution.forEach(execution => { execution.bookmarked = !isBookmarked; }); this.workflowExecutionsService .groupSetIsBookmarked(this.wid, Array.from(this.setOfEid), isBookmarked) .pipe(untilDestroyed(this)) .subscribe({}); } } /* delete a single execution */ onDelete(row: WorkflowExecutionsEntry) { this.workflowExecutionsService .groupDeleteWorkflowExecutions(this.wid, [row.eId]) .pipe(untilDestroyed(this)) .subscribe({ complete: () => { this.allExecutionEntries?.splice(this.allExecutionEntries.indexOf(row), 1); this.handlePaginationAfterDeletingExecutions(); }, }); } onGroupDelete() { this.workflowExecutionsService .groupDeleteWorkflowExecutions(this.wid, Array.from(this.setOfEid)) .pipe(untilDestroyed(this)) .subscribe({ complete: () => { this.allExecutionEntries = this.allExecutionEntries?.filter( execution => !Array.from(this.setOfExecution).includes(execution) ); this.handlePaginationAfterDeletingExecutions(); this.setOfEid.clear(); this.setOfExecution.clear(); }, }); } /* rename a single execution */ confirmUpdateWorkflowExecutionsCustomName(row: WorkflowExecutionsEntry, name: string, index: number): void { // if name doesn't change, no need to call API if (name === row.name) { this.workflowExecutionsIsEditingName = this.workflowExecutionsIsEditingName.filter( entryIsEditingIndex => entryIsEditingIndex != index ); return; } this.workflowExecutionsService .updateWorkflowExecutionsName(this.wid, row.eId, name) .pipe(untilDestroyed(this)) .subscribe(() => { if (this.workflowExecutionsDisplayedList === undefined) { return; } // change the execution name globally this.allExecutionEntries[this.allExecutionEntries.indexOf(this.workflowExecutionsDisplayedList[index])].name = name; this.paginatedExecutionEntries[ this.paginatedExecutionEntries.indexOf(this.workflowExecutionsDisplayedList[index]) ].name = name; this.workflowExecutionsDisplayedList[index].name = name; this.fuse.setCollection(this.paginatedExecutionEntries); }) .add(() => { this.workflowExecutionsIsEditingName = this.workflowExecutionsIsEditingName.filter( entryIsEditingIndex => entryIsEditingIndex != index ); }); } /* sort executions by name/username/start time/update time based in ascending alphabetical order */ ascSort(type: string): void { if (type === "Name (ID)") { this.workflowExecutionsDisplayedList = this.workflowExecutionsDisplayedList ?.slice() .sort((exe1, exe2) => exe1.name.toLowerCase().localeCompare(exe2.name.toLowerCase())); } else if (type === "Username") { this.workflowExecutionsDisplayedList = this.workflowExecutionsDisplayedList ?.slice() .sort((exe1, exe2) => exe1.userName.toLowerCase().localeCompare(exe2.userName.toLowerCase())); } else if (type === "Execution Start Time") { this.workflowExecutionsDisplayedList = this.workflowExecutionsDisplayedList ?.slice() .sort((exe1, exe2) => exe1.startingTime > exe2.startingTime ? 1 : exe2.startingTime > exe1.startingTime ? -1 : 0 ); } else if (type == "Execution Completion Time") { this.workflowExecutionsDisplayedList = this.workflowExecutionsDisplayedList ?.slice() .sort((exe1, exe2) => exe1.completionTime > exe2.completionTime ? 1 : exe2.completionTime > exe1.completionTime ? -1 : 0 ); } else if (type === "Computing Unit ID") { this.workflowExecutionsDisplayedList = this.workflowExecutionsDisplayedList ?.slice() .sort((a, b) => a.cuId - b.cuId); } } /* sort executions by name/username/start time/update time based in descending alphabetical order */ dscSort(type: string): void { if (type === "Name (ID)") { this.workflowExecutionsDisplayedList = this.workflowExecutionsDisplayedList ?.slice() .sort((exe1, exe2) => exe2.name.toLowerCase().localeCompare(exe1.name.toLowerCase())); } else if (type === "Username") { this.workflowExecutionsDisplayedList = this.workflowExecutionsDisplayedList ?.slice() .sort((exe1, exe2) => exe2.userName.toLowerCase().localeCompare(exe1.userName.toLowerCase())); } else if (type === "Execution Start Time") { this.workflowExecutionsDisplayedList = this.workflowExecutionsDisplayedList ?.slice() .sort((exe1, exe2) => exe1.startingTime < exe2.startingTime ? 1 : exe2.startingTime < exe1.startingTime ? -1 : 0 ); } else if (type == "Execution Completion Time") { this.workflowExecutionsDisplayedList = this.workflowExecutionsDisplayedList ?.slice() .sort((exe1, exe2) => exe1.completionTime < exe2.completionTime ? 1 : exe2.completionTime < exe1.completionTime ? -1 : 0 ); } else if (type === "Computing Unit ID") { this.workflowExecutionsDisplayedList = this.workflowExecutionsDisplayedList ?.slice() .sort((a, b) => b.cuId - a.cuId); } } /** * * @param name * @param nameFlag true for execution name and false for username */ abbreviate(name: string, nameFlag: boolean): string { let maxLength = nameFlag ? MAX_TEXT_SIZE : MAX_USERNAME_SIZE; if (name.length <= maxLength) { return name; } else { return name.slice(0, maxLength); } } onHit(column: string, index: number): void { if (this.showORhide[index]) { this.ascSort(column); } else { this.dscSort(column); } this.showORhide[index] = !this.showORhide[index]; } setAvatarColor(userName: string): string { if (userName in this.avatarColors) { return this.avatarColors[userName]; } else { this.avatarColors[userName] = this.getRandomColor(); return this.avatarColors[userName]; } } getRandomColor(): string { const r = Math.floor(Math.random() * MAX_RGB); const g = Math.floor(Math.random() * MAX_RGB); const b = Math.floor(Math.random() * MAX_RGB); return "rgba(" + r + "," + g + "," + b + ",0.8)"; } /** * Update the eId set to keep track of the status of the checkbox * @param eId * @param checked true if checked false if unchecked */ updateEidSet(eId: number, checked: boolean): void { if (checked) { this.setOfEid.add(eId); } else { this.setOfEid.delete(eId); } } /** * Update the row set to keep track of the status of the checkbox * @param row * @param checked true if checked false if unchecked */ updateRowSet(row: WorkflowExecutionsEntry, checked: boolean): void { if (checked) { this.setOfExecution.add(row); } else { this.setOfExecution.delete(row); } } /** * Mark all the checkboxes checked and check the status of the all check * @param value true if we need to check all false if we need to uncheck all */ onAllChecked(value: boolean): void { if (this.paginatedExecutionEntries !== undefined) { for (let execution of this.paginatedExecutionEntries) { this.updateEidSet(execution.eId, value); this.updateRowSet(execution, value); } } this.refreshCheckedStatus(); } /** * Update the eId and row set, and check the status of the all check * @param row * @param checked true if checked false if unchecked */ onItemChecked(row: WorkflowExecutionsEntry, checked: boolean) { this.updateEidSet(row.eId, checked); this.updateRowSet(row, checked); this.refreshCheckedStatus(); } /** * Check the status of the all check */ refreshCheckedStatus(): void { if (this.paginatedExecutionEntries !== undefined) { this.checked = this.paginatedExecutionEntries.length === this.setOfEid.size; } } public searchInputOnChange(value: string): void { const searchConditionsSet = [...new Set(value.trim().split(/ +(?=(?:(?:[^"]*"){2})*[^"]*$)/g))]; searchConditionsSet.forEach((condition, index) => { const preCondition = searchConditionsSet.slice(0, index); var executionSearchField = ""; var executionSearchValue = ""; if (condition.includes(":")) { const conditionArray = condition.split(":"); executionSearchField = conditionArray[0]; executionSearchValue = conditionArray[1]; } else { executionSearchField = "executionName"; executionSearchValue = preCondition ? value.slice(preCondition.map(c => c.length).reduce((a, b) => a + b, 0) + preCondition.length) : value; } const filteredExecutionInfo: string[] = []; this.paginatedExecutionEntries.forEach(executionEntry => { const searchField = this.searchCriteriaPathMapping.get(executionSearchField); var executionInfo = ""; if (searchField === undefined) { return; } else { executionInfo = searchField[0] === "status" ? [...this.statusMapping.entries()] .filter(({ 1: val }) => val === executionEntry.status) .map(([key]) => key)[0] : Object.values(executionEntry)[Object.keys(executionEntry).indexOf(searchField[0])]; } if (executionInfo.toLowerCase().indexOf(executionSearchValue.toLowerCase()) !== -1) { let filterQuery: string; if (preCondition.length !== 0) { filterQuery = executionSearchField === "executionName" ? preCondition.join(" ") + " " + executionInfo : preCondition.join(" ") + " " + executionSearchField + ":" + executionInfo; } else { filterQuery = executionSearchField === "executionName" ? executionInfo : executionSearchField + ":" + executionInfo; } filteredExecutionInfo.push(filterQuery); } }); this.filteredExecutionInfo = [...new Set(filteredExecutionInfo)]; }); } // check https://fusejs.io/api/query.html#logical-query-operators for logical query operators rule public buildAndPathQuery( executionSearchField: string, executionSearchValue: string ): { $path: ReadonlyArray; $val: string; } { return { $path: this.searchCriteriaPathMapping.get(executionSearchField) as ReadonlyArray, $val: executionSearchValue, }; } /** * Search executions by execution name, user name, or status * Use fuse.js https://fusejs.io/ as the tool for searching */ public searchExecution(): void { // empty search value, return all execution entries if (this.executionSearchValue.trim() === "") { this.workflowExecutionsDisplayedList = this.paginatedExecutionEntries; return; } let andPathQuery: Object[] = []; const searchConditionsSet = new Set(this.executionSearchValue.trim().split(/ +(?=(?:(?:[^"]*"){2})*[^"]*$)/g)); searchConditionsSet.forEach(condition => { // field search if (condition.includes(":")) { const conditionArray = condition.split(":"); if (conditionArray.length !== 2) { this.notificationService.error("Please check the format of the search query"); return; } const executionSearchField = conditionArray[0]; const executionSearchValue = conditionArray[1].toLowerCase(); if (!this.searchCriteria.includes(executionSearchField)) { this.notificationService.error("Cannot search by " + executionSearchField); return; } if (executionSearchField === "status") { var statusSearchValue = this.statusMapping.get(executionSearchValue)?.toString(); // check if user type correct status if (statusSearchValue === undefined) { this.notificationService.error("Status " + executionSearchValue + " is not available to execution"); return; } andPathQuery.push(this.buildAndPathQuery(executionSearchField, statusSearchValue)); } else { // handle all other searches andPathQuery.push(this.buildAndPathQuery(executionSearchField, executionSearchValue)); } } else { //search by execution name andPathQuery.push(this.buildAndPathQuery("executionName", condition)); } }); this.workflowExecutionsDisplayedList = this.fuse.search({ $and: andPathQuery }).map(res => res.item); } /* Pagination handler */ /* Assign new page index and change current list */ onPageIndexChange(pageIndex: number): void { this.currentPageIndex = pageIndex; this.updatePaginatedExecutions(); } /* Assign new page size and change current list */ onPageSizeChange(pageSize: number): void { this.pageSize = pageSize; this.updatePaginatedExecutions(); } /** * Change current page list everytime the page change */ changePaginatedExecutions(): WorkflowExecutionsEntry[] { this.executionSearchValue = ""; return this.allExecutionEntries?.slice( (this.currentPageIndex - 1) * this.pageSize, this.currentPageIndex * this.pageSize ); } getBarChartProcessTimeData(rows: WorkflowExecutionsEntry[]) { let processTimeData: { [key: string]: number } = {}; let divider: number = ceil(rows.length / this.averageProcessingTimeDivider); let tracker = 0; let totProcessTime = 0; let category = ""; let eIdToNumber = 1; rows.forEach(execution => { tracker++; let processTime = execution.completionTime - execution.startingTime; processTime = processTime / 60000; totProcessTime += processTime; if (tracker === 1) { category += String(eIdToNumber); } if (tracker === divider) { category += "~" + String(eIdToNumber); processTimeData[category] = totProcessTime / divider; tracker = 0; totProcessTime = 0; category = ""; } eIdToNumber++; }); return processTimeData; } showRuntimeStatistics(eId: number, cuid: number): void { this.workflowExecutionsService .retrieveWorkflowRuntimeStatistics(this.wid, eId, cuid) .pipe(untilDestroyed(this)) .subscribe(workflowRuntimeStatistics => { this.modalRef = this.runtimeStatisticsModal.create({ nzTitle: "Runtime Statistics", nzStyle: { top: "5px", width: "98vw", height: "92vh" }, nzFooter: null, // null indicates that the footer of the window would be hidden nzBodyStyle: { width: "98vw", height: "92vh" }, nzContent: WorkflowRuntimeStatisticsComponent, nzData: { workflowRuntimeStatistics: workflowRuntimeStatistics }, }); }); } private updatePaginatedExecutions(): void { this.paginatedExecutionEntries = this.changePaginatedExecutions(); this.workflowExecutionsDisplayedList = this.paginatedExecutionEntries; this.fuse.setCollection(this.paginatedExecutionEntries); } private handlePaginationAfterDeletingExecutions(): void { this.updatePaginatedExecutions(); /* If a current page index has 0 number of execution entries after deletion (e.g., deleting all the executions in the last page), * the following code will decrement the current page index by 1. */ if (this.currentPageIndex > 1 && this.paginatedExecutionEntries.length === 0) { this.onPageIndexChange(this.currentPageIndex - 1); } } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-runtime-statistics/workflow-runtime-statistics.component.html ================================================
    ================================================ FILE: frontend/src/app/dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-runtime-statistics/workflow-runtime-statistics.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .chart-property { display: flex; justify-content: center; align-items: center; width: 95vw; height: 86vh; } ================================================ FILE: frontend/src/app/dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-runtime-statistics/workflow-runtime-statistics.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, inject, OnInit } from "@angular/core"; import { UntilDestroy } from "@ngneat/until-destroy"; import { WorkflowRuntimeStatistics } from "../../../../../type/workflow-runtime-statistics"; import * as Plotly from "plotly.js-basic-dist-min"; import { NZ_MODAL_DATA } from "ng-zorro-antd/modal"; import { NzTabsComponent, NzTabComponent } from "ng-zorro-antd/tabs"; import { NgFor } from "@angular/common"; const NANOSECONDS_TO_SECONDS = 1_000_000_000; interface ChartData { x: number[]; y: number[]; mode: string; name: string; } @UntilDestroy() @Component({ selector: "texera-workflow-runtime-statistics", templateUrl: "./workflow-runtime-statistics.component.html", styleUrls: ["./workflow-runtime-statistics.component.scss"], imports: [NzTabsComponent, NgFor, NzTabComponent], }) export class WorkflowRuntimeStatisticsComponent implements OnInit { readonly workflowRuntimeStatistics: WorkflowRuntimeStatistics[] = inject(NZ_MODAL_DATA).workflowRuntimeStatistics; private groupedStatistics?: Record; public metrics: string[] = [ "Input Tuple Count", "Input Tuple Size (bytes)", "Output Tuple Count", "Output Tuple Size (bytes)", "Total Data Processing Time (s)", "Total Control Processing Time (s)", "Total Idle Time (s)", "Number of Workers", ]; ngOnInit(): void { if (!this.workflowRuntimeStatistics) { console.warn("No workflow runtime statistics available."); return; } this.groupedStatistics = this.groupStatisticsByOperatorId(); this.createChart(0); } /** * Create a new line chart corresponding to the change of a tab */ onTabChanged(index: number): void { this.createChart(index); } /** * Groups statistics by operator ID, converting times from nanoseconds to seconds, * and adjusts timestamps relative to the initial timestamp. */ private groupStatisticsByOperatorId(): Record { if (!this.workflowRuntimeStatistics || this.workflowRuntimeStatistics.length === 0) { console.warn("No workflow runtime statistics available."); return {}; } const initialTimestamp = this.workflowRuntimeStatistics[0].timestamp; return this.workflowRuntimeStatistics.reduce( (accumulator, stat) => { if (!stat.operatorId) { console.warn("Missing operatorId in statistic:", stat); return accumulator; } const statsArray = accumulator[stat.operatorId] ?? []; const processedStat = { ...stat, dataProcessingTime: stat.totalDataProcessingTime / NANOSECONDS_TO_SECONDS, controlProcessingTime: stat.totalControlProcessingTime / NANOSECONDS_TO_SECONDS, idleTime: stat.totalIdleTime / NANOSECONDS_TO_SECONDS, numberOfWorkers: stat.numberOfWorkers, timestamp: stat.timestamp - initialTimestamp, }; statsArray.push(processedStat); accumulator[stat.operatorId] = statsArray; return accumulator; }, {} as Record ); } /** * Preprocess the dataset which will be used as an input for a line chart * 1. Shorten the operator ID * 2. Remove sink operator * 3. Contain only a certain metric given a metric idx * @param metricIndex */ private createDataset(metricIndex: number): ChartData[] { if (!this.groupedStatistics) { return []; } const metricKeys = [ "inputTupleCount", "inputTupleSize", "outputTupleCount", "outputTupleSize", "dataProcessingTime", "controlProcessingTime", "idleTime", "numberOfWorkers", ]; const yValuesKey = metricKeys[metricIndex] || "numberOfWorkers"; return Object.entries(this.groupedStatistics) .map(([operatorId, stats]) => { const [operatorName] = operatorId.split("-"); const uuidLast6Digits = operatorId.slice(-6); if (operatorName.startsWith("sink")) { return null; } return { x: stats.map(stat => stat.timestamp / 1000), y: stats.map(stat => stat[yValuesKey]), mode: "lines", name: `${operatorName}-${uuidLast6Digits}`, }; }) .filter((data): data is ChartData => data !== null); } /** * Create a line chart using plotly * @param metricIndex */ private createChart(metricIndex: number): void { const dataset = this.createDataset(metricIndex); if (!dataset || dataset.length === 0) { console.warn("No data available for the chart."); return; } const layout = { title: { text: this.metrics[metricIndex], }, xaxis: { title: { text: "Time (s)", }, }, yaxis: { title: { text: this.metrics[metricIndex], }, }, }; Plotly.newPlot("chart", dataset, layout); } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-workflow/user-workflow-list-item/highlight-search-terms.pipe.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Pipe, PipeTransform } from "@angular/core"; import { DomSanitizer, SafeHtml } from "@angular/platform-browser"; @Pipe({ name: "highlightSearchTerms" }) export class HighlightSearchTermsPipe implements PipeTransform { constructor(private sanitizer: DomSanitizer) {} transform(value: string | undefined, terms: string[]): SafeHtml { if (!terms || !terms.length || !value) { // Return the original value if there's nothing to highlight or if the value is undefined return this.sanitizer.bypassSecurityTrustHtml(value || ""); } // Escape the terms to be used in a RegExp const regex = new RegExp(`(${terms.join("|")})`, "gi"); const highlightedString = value.replace(regex, '$1'); // Use the sanitizer to avoid security risks return this.sanitizer.bypassSecurityTrustHtml(highlightedString); } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-workflow/user-workflow-list-item/user-workflow-list-item.component.html ================================================
    ================================================ FILE: frontend/src/app/dashboard/component/user/user-workflow/user-workflow-list-item/user-workflow-list-item.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ @import "../../../section-style"; @import "../../../dashboard.component.scss"; /** * css for project label, shared by workflow section and file section **/ .project-label-container { min-width: 20px; display: flex; flex-direction: row-reverse; overflow-x: auto; .project-label { display: inline; white-space: nowrap; .color-tag { display: inline-block; cursor: pointer; } .project-label-name { padding: 1.5px 7px; border-top-left-radius: 2px; border-bottom-left-radius: 2px; color: inherit; text-decoration: none; } .project-label-remove { padding: 1.5px 7px; border-top-right-radius: 2px; border-bottom-right-radius: 2px; margin-right: 10px; } .light-color { border: 1px solid gainsboro; } .light-color:hover { background-image: linear-gradient(rgba(0, 0, 0, 0.4) 0 0); } .dark-color:hover { background-image: linear-gradient(rgba(255, 255, 255, 0.345) 0 0); } } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-workflow/user-workflow-list-item/user-workflow-list-item.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, ViewChild } from "@angular/core"; import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; import { UserWorkflowListItemComponent } from "./user-workflow-list-item.component"; import { FileSaverService } from "../../../../service/user/file/file-saver.service"; import { testWorkflowEntries } from "../../../user-dashboard-test-fixtures"; import { By } from "@angular/platform-browser"; import { StubWorkflowPersistService } from "../../../../../common/service/workflow-persist/stub-workflow-persist.service"; import { WorkflowPersistService } from "../../../../../common/service/workflow-persist/workflow-persist.service"; import { UserProjectService } from "../../../../service/user/project/user-project.service"; import { StubUserProjectService } from "../../../../service/user/project/stub-user-project.service"; import { NzListComponent } from "ng-zorro-antd/list"; import { NzModalModule } from "ng-zorro-antd/modal"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { provideRouter } from "@angular/router"; import { DashboardEntry } from "../../../../type/dashboard-entry"; import { NzTooltipModule } from "ng-zorro-antd/tooltip"; import { commonTestProviders } from "../../../../../common/testing/test-utils"; import type { Mocked } from "vitest"; // UserWorkflowListItemComponent is rooted at ; instantiating it // outside an host throws "No provider found for NzListComponent". @Component({ standalone: true, imports: [NzListComponent, UserWorkflowListItemComponent], template: ` `, }) class TestHostComponent { entry!: DashboardEntry; editable = true; @ViewChild(UserWorkflowListItemComponent, { static: true }) inner!: UserWorkflowListItemComponent; } describe("UserWorkflowListItemComponent", () => { let component: UserWorkflowListItemComponent; let fixture: ComponentFixture; const fileSaverServiceSpy = { saveAs: vi.fn() } as unknown as Mocked; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [TestHostComponent, NzModalModule, HttpClientTestingModule, NzTooltipModule], providers: [ { provide: WorkflowPersistService, useValue: new StubWorkflowPersistService(testWorkflowEntries) }, { provide: UserProjectService, useValue: new StubUserProjectService() }, { provide: FileSaverService, useValue: fileSaverServiceSpy }, provideRouter([]), ...commonTestProviders, ], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(TestHostComponent); fixture.componentInstance.entry = testWorkflowEntries[0]; fixture.componentInstance.editable = true; fixture.detectChanges(); component = fixture.componentInstance.inner; }); it("should create", () => { expect(component).toBeTruthy(); }); it("sends http request to backend to retrieve export json", () => { // Test the workflow download button. component.onClickDownloadWorkfllow(); expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledExactlyOnceWith( new Blob([JSON.stringify(testWorkflowEntries[0].workflow.workflow.content)], { type: "text/plain;charset=utf-8", }), "workflow 1.json" ); }); it("adding a workflow description adds a description to the workflow", waitForAsync(() => { fixture.whenStable().then(() => { let addWorkflowDescriptionBtn = fixture.debugElement.query(By.css(".add-description-btn")); expect(addWorkflowDescriptionBtn).toBeTruthy(); addWorkflowDescriptionBtn.triggerEventHandler("click", null); fixture.detectChanges(); let editableDescriptionInput = fixture.debugElement.nativeElement.querySelector(".workflow-editable-description"); expect(editableDescriptionInput).toBeTruthy(); vi.spyOn(component, "confirmUpdateWorkflowCustomDescription"); sendInput(editableDescriptionInput, "dummy description added by focusing out the input element.").then(() => { fixture.detectChanges(); editableDescriptionInput.dispatchEvent(new Event("focusout")); fixture.detectChanges(); expect(component.confirmUpdateWorkflowCustomDescription).toHaveBeenCalledTimes(1); }); }); })); it("Editing a workflow description edits a description to the workflow", waitForAsync(() => { fixture.whenStable().then(() => { const workflowDescriptionLabel = fixture.debugElement.query(By.css(".workflow-description")); expect(workflowDescriptionLabel).toBeTruthy(); workflowDescriptionLabel.triggerEventHandler("click", null); fixture.detectChanges(); let editableDescriptionInput1 = fixture.debugElement.nativeElement.querySelector( ".workflow-editable-description" ); expect(editableDescriptionInput1).toBeTruthy(); vi.spyOn(component, "confirmUpdateWorkflowCustomDescription"); sendInput(editableDescriptionInput1, "dummy description added by focusing out the input element.").then(() => { fixture.detectChanges(); editableDescriptionInput1.dispatchEvent(new Event("focusout")); fixture.detectChanges(); expect(component.confirmUpdateWorkflowCustomDescription).toHaveBeenCalledTimes(1); }); }); })); function sendInput(editableDescriptionInput: HTMLInputElement, text: string) { // Helper function to change the workflow description textbox. editableDescriptionInput.value = text; editableDescriptionInput.dispatchEvent(new Event("input")); fixture.detectChanges(); return fixture.whenStable(); } }); ================================================ FILE: frontend/src/app/dashboard/component/user/user-workflow/user-workflow-list-item/user-workflow-list-item.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, EventEmitter, Input, Output } from "@angular/core"; import { NzModalService } from "ng-zorro-antd/modal"; import { WorkflowExecutionHistoryComponent } from "../ngbd-modal-workflow-executions/workflow-execution-history.component"; import { DEFAULT_WORKFLOW_NAME, WorkflowPersistService, } from "../../../../../common/service/workflow-persist/workflow-persist.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { ShareAccessComponent } from "../../share-access/share-access.component"; import { Workflow } from "../../../../../common/type/workflow"; import { DashboardProject } from "../../../../type/dashboard-project.interface"; import { UserProjectService } from "../../../../service/user/project/user-project.service"; import { DashboardEntry } from "../../../../type/dashboard-entry"; import { firstValueFrom } from "rxjs"; import { DownloadService } from "src/app/dashboard/service/user/download/download.service"; import { DASHBOARD_USER_PROJECT, DASHBOARD_USER_WORKSPACE } from "../../../../../app-routing.constant"; import { GuiConfigService } from "../../../../../common/service/gui-config.service"; import { NzListItemComponent, NzListItemMetaComponent, NzListItemMetaAvatarComponent, NzListItemMetaTitleComponent, NzListItemMetaDescriptionComponent, NzListItemActionsComponent, NzListItemActionComponent, } from "ng-zorro-antd/list"; import { NgStyle, NgIf, NgFor, NgClass, DatePipe } from "@angular/common"; import { FormsModule } from "@angular/forms"; import { NzAvatarComponent } from "ng-zorro-antd/avatar"; import { RouterLink } from "@angular/router"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { NzPopconfirmDirective } from "ng-zorro-antd/popconfirm"; import { HighlightSearchTermsPipe } from "./highlight-search-terms.pipe"; @UntilDestroy() @Component({ selector: "texera-user-workflow-list-item", templateUrl: "./user-workflow-list-item.component.html", styleUrls: ["./user-workflow-list-item.component.scss"], imports: [ NzListItemComponent, NzListItemMetaComponent, NzListItemMetaAvatarComponent, NgStyle, NgIf, FormsModule, NzAvatarComponent, NzListItemMetaTitleComponent, RouterLink, NzSpaceCompactItemDirective, NzButtonComponent, ɵNzTransitionPatchDirective, NzTooltipDirective, NzIconDirective, NzListItemMetaDescriptionComponent, NgFor, NgClass, NzListItemActionsComponent, NzListItemActionComponent, NzWaveDirective, NzPopconfirmDirective, DatePipe, HighlightSearchTermsPipe, ], }) export class UserWorkflowListItemComponent { protected readonly DASHBOARD_USER_WORKSPACE = DASHBOARD_USER_WORKSPACE; protected readonly DASHBOARD_USER_PROJECT = DASHBOARD_USER_PROJECT; private _entry?: DashboardEntry; @Input() public keywords: string[] = []; @Input() get entry(): DashboardEntry { if (!this._entry) { throw new Error("entry property must be provided to UserWorkflowListItemComponent."); } return this._entry; } set entry(value: DashboardEntry) { this._entry = value; } get workflow(): Workflow { if (!this.entry.workflow) { throw new Error( "Incorrect type of DashboardEntry provided to UserWorkflowListItemComponent. Entry must be workflow." ); } return this.entry.workflow.workflow; } @Input() editable = false; @Input() public pid: number = 0; userProjectsMap: ReadonlyMap = new Map(); @Output() deleted = new EventEmitter(); @Output() duplicated = new EventEmitter(); editingName = false; editingDescription = false; constructor( private workflowPersistService: WorkflowPersistService, private modalService: NzModalService, protected config: GuiConfigService, private userProjectService: UserProjectService, private downloadService: DownloadService ) { this.userProjectService .getProjectList() .pipe(untilDestroyed(this)) .subscribe(userProjectsList => { this.userProjectsMap = new Map(userProjectsList.map(userProject => [userProject.pid, userProject])); }); } getProjectIds() { return new Set(this.entry.workflow.projectIDs); } /** * open the workflow executions page */ public onClickGetWorkflowExecutions(): void { this.modalService.create({ nzContent: WorkflowExecutionHistoryComponent, nzData: { wid: this.workflow.wid }, nzTitle: "Execution results of Workflow: " + this.workflow.name, nzFooter: null, nzWidth: "80%", nzCentered: true, }); } public confirmUpdateWorkflowCustomName(name: string): void { if (this.workflow.wid === undefined) { return; } this.workflowPersistService .updateWorkflowName(this.workflow.wid, name || DEFAULT_WORKFLOW_NAME) .pipe(untilDestroyed(this)) .subscribe(() => { this.workflow.name = name || DEFAULT_WORKFLOW_NAME; }) .add(() => { this.editingName = false; }); } public confirmUpdateWorkflowCustomDescription(description: string): void { if (this.workflow.wid === undefined) { return; } this.workflowPersistService .updateWorkflowDescription(this.workflow.wid, description) .pipe(untilDestroyed(this)) .subscribe(() => { this.workflow.description = description; }) .add(() => { this.editingDescription = false; }); } /** * open the Modal based on the workflow clicked on */ public async onClickOpenShareAccess(): Promise { this.modalService.create({ nzContent: ShareAccessComponent, nzData: { writeAccess: this.entry.workflow.accessLevel === "WRITE", type: "workflow", id: this.workflow.wid, allOwners: await firstValueFrom(this.workflowPersistService.retrieveOwners()), }, nzFooter: null, nzTitle: "Share this workflow with others", nzCentered: true, }); } /** * Download the workflow as a json file */ public onClickDownloadWorkfllow(): void { if (this.workflow.wid) { this.downloadService .downloadWorkflow(this.workflow.wid, this.workflow.name) .pipe(untilDestroyed(this)) .subscribe(); } } public isLightColor(color: string): boolean { return UserProjectService.isLightColor(color); } /** * For color tags, enable clicking 'x' to remove a workflow from a project */ public removeWorkflowFromProject(pid: number): void { this.userProjectService .removeWorkflowFromProject(pid, this.workflow.wid!) .pipe(untilDestroyed(this)) .subscribe(() => { this.entry.workflow.projectIDs = this.entry.workflow.projectIDs.filter(projectID => projectID != pid); }); } } ================================================ FILE: frontend/src/app/dashboard/component/user/user-workflow/user-workflow.component.html ================================================

    Workflows

    ================================================ FILE: frontend/src/app/dashboard/component/user/user-workflow/user-workflow.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ @import "../../dashboard.component.scss"; @import "../../section-style"; @import "../../button-style"; ::ng-deep .ant-badge-dot { right: 8px; top: 5px; } ================================================ FILE: frontend/src/app/dashboard/component/user/user-workflow/user-workflow.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { RouterTestingModule } from "@angular/router/testing"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { UserWorkflowComponent } from "./user-workflow.component"; import { WorkflowPersistService } from "../../../../common/service/workflow-persist/workflow-persist.service"; import { StubWorkflowPersistService } from "../../../../common/service/workflow-persist/stub-workflow-persist.service"; import { ShareAccessComponent } from "../share-access/share-access.component"; import { HttpClient } from "@angular/common/http"; import { ShareAccessService } from "../../../service/user/share-access/share-access.service"; import { UserService } from "../../../../common/service/user/user.service"; import { StubUserService } from "../../../../common/service/user/stub-user.service"; import { NzDropDownModule } from "ng-zorro-antd/dropdown"; import { NzCardModule } from "ng-zorro-antd/card"; import { NzListModule } from "ng-zorro-antd/list"; import { NzCalendarModule } from "ng-zorro-antd/calendar"; import { NzSelectModule } from "ng-zorro-antd/select"; import { NzPopoverModule } from "ng-zorro-antd/popover"; import { NzDatePickerModule } from "ng-zorro-antd/date-picker"; import { en_US, NZ_I18N } from "ng-zorro-antd/i18n"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; import { OperatorMetadataService } from "../../../../workspace/service/operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../../../../workspace/service/operator-metadata/stub-operator-metadata.service"; import { NzUploadModule } from "ng-zorro-antd/upload"; import { ScrollingModule } from "@angular/cdk/scrolling"; import { NzAvatarModule } from "ng-zorro-antd/avatar"; import { NzTooltipModule } from "ng-zorro-antd/tooltip"; import { mockUserInfo, testWorkflowEntries, testWorkflowFileNameConflictEntries, } from "../../user-dashboard-test-fixtures"; import { FiltersComponent } from "../filters/filters.component"; import { UserWorkflowListItemComponent } from "./user-workflow-list-item/user-workflow-list-item.component"; import { UserProjectService } from "../../../service/user/project/user-project.service"; import { StubUserProjectService } from "../../../service/user/project/stub-user-project.service"; import { SearchService } from "../../../service/user/search.service"; import { StubSearchService } from "../../../service/user/stub-search.service"; import { SearchResultsComponent } from "../search-results/search-results.component"; import { delay, of } from "rxjs"; import { NzModalService } from "ng-zorro-antd/modal"; import { NzButtonModule } from "ng-zorro-antd/button"; import { DownloadService } from "../../../service/user/download/download.service"; import { commonTestProviders } from "../../../../common/testing/test-utils"; import type { Mocked } from "vitest"; describe("SavedWorkflowSectionComponent", () => { let component: UserWorkflowComponent; let fixture: ComponentFixture; let downloadServiceSpy: Mocked; beforeEach(async () => { downloadServiceSpy = { downloadWorkflowsAsZip: vi.fn() } as unknown as Mocked; await TestBed.configureTestingModule({ providers: [ NzModalService, { provide: WorkflowPersistService, useValue: new StubWorkflowPersistService(testWorkflowEntries) }, { provide: UserProjectService, useValue: new StubUserProjectService() }, HttpClient, ShareAccessService, { provide: OperatorMetadataService, useClass: StubOperatorMetadataService }, { provide: NZ_I18N, useValue: en_US }, { provide: UserService, useClass: StubUserService }, { provide: SearchService, useValue: new StubSearchService(testWorkflowEntries, mockUserInfo), }, { provide: DownloadService, useValue: downloadServiceSpy }, ...commonTestProviders, ], imports: [ UserWorkflowComponent, ShareAccessComponent, FiltersComponent, UserWorkflowListItemComponent, SearchResultsComponent, FormsModule, RouterTestingModule, HttpClientTestingModule, ReactiveFormsModule, NzDropDownModule, NzCardModule, NzListModule, NzCalendarModule, NzDatePickerModule, NzSelectModule, NzPopoverModule, NzAvatarModule, NzTooltipModule, NzUploadModule, ScrollingModule, NoopAnimationsModule, NzButtonModule, ], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(UserWorkflowComponent); component = fixture.componentInstance; component.filters = TestBed.createComponent(FiltersComponent).componentInstance; component.filters.masterFilterList = []; component.filters.selectedMtime = []; component.filters.selectedMtime = []; component.searchResultsComponent = TestBed.createComponent(SearchResultsComponent).componentInstance; fixture.detectChanges(); }); // TODO: add this test case back and figure out why it failed // it.skip("Modal Opened, then Closed", () => { // const modalRef: NgbModalRef = modalService.open(NgbdModalWorkflowShareAccessComponent); // vi.spyOn(modalService, "open").mockReturnValue(modalRef); // component.onClickOpenShareAccess(testWorkflowEntries[0]); // expect(modalService.open).toHaveBeenCalled(); // fixture.detectChanges(); // modalRef.dismiss(); // }); const waitForLoading = async () => { while (component.searchResultsComponent.loading) { await delay(10); } }; it("searchNoInput", async () => { // When no search input is provided, it should show all workflows. await component.search(); expect(component.searchResultsComponent.loading).toBe(false); const SortedCase = component.searchResultsComponent.entries.map(workflow => workflow.name); expect(SortedCase).toEqual(["workflow 1", "workflow 2", "workflow 3", "workflow 4", "workflow 5"]); console.log("Master Filter List:", component.filters.masterFilterList); expect(component.filters.masterFilterList).toEqual([]); }); it("searchByWorkflowName", async () => { // If the name "workflow 5" is entered as a single phrase, only workflow 5 should be returned, rather // than all containing the keyword "workflow". component.filters.masterFilterList = ["workflow 5"]; await waitForLoading(); expect(component.searchResultsComponent.loading).toBe(false); const SortedCase = component.searchResultsComponent.entries.map(workflow => workflow.name); expect(SortedCase).toEqual(["workflow 5"]); expect(component.filters.masterFilterList).toEqual(["workflow 5"]); }); it("searchByOwners", async () => { // If the owner filter is applied, only those workflow ownered by that user should be returned. component.filters.owners[0].checked = true; component.filters.updateSelectedOwners(); await waitForLoading(); expect(component.searchResultsComponent.loading).toBe(false); const SortedCase = component.searchResultsComponent.entries.map(workflow => workflow.name); expect(SortedCase).toEqual(["workflow 1", "workflow 2"]); expect(component.filters.masterFilterList).toEqual(["owner: Texera"]); }); it("searchByIDs", async () => { // If the ID filter is applied, only those workflows should be returned. component.filters.wids[0].checked = true; component.filters.wids[1].checked = true; component.filters.wids[2].checked = true; component.filters.updateSelectedIDs(); await waitForLoading(); expect(component.searchResultsComponent.loading).toBe(false); const SortedCase = component.searchResultsComponent.entries.map(workflow => workflow.name); expect(SortedCase).toEqual(["workflow 1", "workflow 2", "workflow 3"]); expect(component.filters.masterFilterList).toEqual(["id: 1", "id: 2", "id: 3"]); }); it("searchByProjects", async () => { component.filters.userProjectsDropdown = [ { pid: 1, name: "Project1", checked: false }, { pid: 2, name: "Project2", checked: false }, { pid: 3, name: "Project3", checked: false }, ]; // If the project filter is applied, only those workflows belonging to those projects should be returned. component.filters.userProjectsDropdown[0].checked = true; component.filters.updateSelectedProjects(); await waitForLoading(); expect(component.searchResultsComponent.loading).toBe(false); const SortedCase = component.searchResultsComponent.entries.map(workflow => workflow.name); expect(SortedCase).toEqual(["workflow 1", "workflow 2", "workflow 3"]); expect(component.filters.masterFilterList).toEqual(["project: Project1"]); }); it("searchByCreationTime", async () => { // If the creation time filter is applied, only those workflows matching the date range should be returned. component.filters.selectedCtime = [new Date(1970, 0, 3), new Date(1981, 2, 13)]; component.filters.buildMasterFilterList(); await waitForLoading(); expect(component.searchResultsComponent.loading).toBe(false); const SortedCase = component.searchResultsComponent.entries.map(workflow => workflow.name); expect(SortedCase).toEqual(["workflow 4", "workflow 5"]); expect(component.filters.masterFilterList).toEqual(["ctime: 1970-01-03 ~ 1981-03-13"]); }); it("searchByModifyTime", async () => { // If the modified time filter is applied, only those workflows matching the date range should be returned. component.filters.selectedMtime = [new Date(1970, 0, 3), new Date(1981, 2, 13)]; component.filters.buildMasterFilterList(); await waitForLoading(); expect(component.searchResultsComponent.loading).toBe(false); const SortedCase = component.searchResultsComponent.entries.map(workflow => workflow.name); expect(SortedCase).toEqual(["workflow 4", "workflow 5"]); expect(component.filters.masterFilterList).toEqual(["mtime: 1970-01-03 ~ 1981-03-13"]); }); /* * To add operators to this test: * 1. Check if the operator's group is true * 2. Mark the selected operator "checked" as true * 3. Push the operator's operatorType to operatorSelectionList * 4. Update masterFilterList to have the correct tags * * - Recommendation: print out the component.operators after the operatorDropdownRequest is made * * - See searchByManyOperators test */ it("searchByOperators", async () => { // If a single operator filter is provided, only the workflows containing that operator should be returned. const operatorGroup = component.filters.operators.get("Analysis"); if (operatorGroup) { operatorGroup[2].checked = true; // sentiment analysis component.filters.updateSelectedOperators(); } await waitForLoading(); const SortedCase = component.searchResultsComponent.entries.map(workflow => workflow.name); expect(SortedCase).toEqual(["workflow 1", "workflow 2", "workflow 3"]); expect(component.filters.masterFilterList).toEqual(["operator: Sentiment Analysis"]); // userFriendlyName }); it("searchByManyOperators", async () => { // If a multiple operator filters are provided, workflows containing any of the provided operators should be returned. const operatorGroup = component.filters.operators.get("Analysis"); const operatorGroup2 = component.filters.operators.get("View Results"); if (operatorGroup && operatorGroup2) { operatorGroup[2].checked = true; // sentiment analysis operatorGroup2[0].checked = true; component.filters.updateSelectedOperators(); } await waitForLoading(); const SortedCase = component.searchResultsComponent.entries.map(workflow => workflow.name); expect(SortedCase).toEqual(["workflow 1", "workflow 2", "workflow 3"]); expect(component.filters.masterFilterList).toEqual(["operator: Sentiment Analysis", "operator: View Results"]); // userFriendlyName }); it("searchByManyParameters", async () => { // Apply the project, ID, owner, and operator filter all at once. component.filters.masterFilterList = ["1"]; const operatorGroup = component.filters.operators.get("Analysis"); if (operatorGroup) { operatorGroup[3].checked = true; // Aggregation operator component.filters.updateSelectedOperators(); component.filters.userProjectsDropdown = [ { pid: 1, name: "Project1", checked: false }, { pid: 2, name: "Project2", checked: false }, { pid: 3, name: "Project3", checked: false }, ]; component.filters.owners[0].checked = true; //Texera component.filters.owners[1].checked = true; //Angular component.filters.wids[0].checked = true; component.filters.wids[1].checked = true; component.filters.wids[2].checked = true; //id 1,2,3 component.filters.userProjectsDropdown[0].checked = true; //Project 1 component.filters.selectedCtime = [new Date(1970, 0, 1), new Date(1973, 2, 11)]; component.filters.selectedMtime = [new Date(1970, 0, 1), new Date(1982, 3, 14)]; //add/select new search parameter here component.filters.updateSelectedProjects(); component.filters.updateSelectedIDs(); component.filters.updateSelectedOwners(); } await waitForLoading(); await component.search(); const SortedCase = component.searchResultsComponent.entries.map(workflow => workflow.name); expect(SortedCase).toEqual(["workflow 1"]); expect(component.filters.masterFilterList).toEqual( expect.arrayContaining([ "1", "owner: Texera", "owner: Angular", "id: 1", "id: 2", "id: 3", "operator: Aggregation", "project: Project1", "ctime: 1970-01-01 ~ 1973-03-11", "mtime: 1970-01-01 ~ 1982-04-14", ]) ); }); it("downloads checked files", async () => { // If multiple workflows in a single batch download have name conflicts, rename them as workflow-1, workflow-2, etc. component.searchResultsComponent.entries = component.searchResultsComponent.entries.concat( testWorkflowFileNameConflictEntries ); testWorkflowFileNameConflictEntries[0].checked = true; testWorkflowFileNameConflictEntries[2].checked = true; downloadServiceSpy.downloadWorkflowsAsZip.mockReturnValue(of(new Blob())); await component.onClickOpenDownloadZip(); expect(downloadServiceSpy.downloadWorkflowsAsZip).toHaveBeenCalledTimes(1); expect(downloadServiceSpy.downloadWorkflowsAsZip).toHaveBeenCalledWith([ { id: testWorkflowFileNameConflictEntries[0].workflow.workflow.wid!, name: testWorkflowFileNameConflictEntries[0].workflow.workflow.name, }, { id: testWorkflowFileNameConflictEntries[2].workflow.workflow.wid!, name: testWorkflowFileNameConflictEntries[2].workflow.workflow.name, }, ]); // Check that the checked entries are unchecked after download expect(testWorkflowFileNameConflictEntries[0].checked).toBe(true); expect(testWorkflowFileNameConflictEntries[2].checked).toBe(true); }); }); ================================================ FILE: frontend/src/app/dashboard/component/user/user-workflow/user-workflow.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { AfterViewInit, Component, Input, ViewChild } from "@angular/core"; import { Router } from "@angular/router"; import { NzModalService } from "ng-zorro-antd/modal"; import { firstValueFrom, from, lastValueFrom, Observable, of } from "rxjs"; import { DEFAULT_WORKFLOW_NAME, WorkflowPersistService, } from "../../../../common/service/workflow-persist/workflow-persist.service"; import { NgbdModalAddProjectWorkflowComponent } from "../user-project/user-project-section/ngbd-modal-add-project-workflow/ngbd-modal-add-project-workflow.component"; import { NgbdModalRemoveProjectWorkflowComponent } from "../user-project/user-project-section/ngbd-modal-remove-project-workflow/ngbd-modal-remove-project-workflow.component"; import { DashboardEntry, UserInfo } from "../../../type/dashboard-entry"; import { UserService } from "../../../../common/service/user/user.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { NotificationService } from "../../../../common/service/notification/notification.service"; import { ExecutionMode, WorkflowContent } from "../../../../common/type/workflow"; import { NzUploadFile, NzUploadComponent } from "ng-zorro-antd/upload"; import * as JSZip from "jszip"; import { FiltersComponent } from "../filters/filters.component"; import { SearchResultsComponent } from "../search-results/search-results.component"; import { SearchService } from "../../../service/user/search.service"; import { SortMethod } from "../../../type/sort-method"; import { isDefined } from "../../../../common/util/predicate"; import { UserProjectService } from "../../../service/user/project/user-project.service"; import { map, mergeMap, switchMap, tap } from "rxjs/operators"; import { DashboardWorkflow } from "../../../type/dashboard-workflow.interface"; import { DownloadService } from "../../../service/user/download/download.service"; import { DASHBOARD_USER_WORKSPACE } from "../../../../app-routing.constant"; import { GuiConfigService } from "../../../../common/service/gui-config.service"; import { NzCardComponent } from "ng-zorro-antd/card"; import { NzSpaceCompactItemDirective, NzSpaceCompactComponent } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { SortButtonComponent } from "../sort-button/sort-button.component"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NgIf } from "@angular/common"; import { NzPopconfirmDirective } from "ng-zorro-antd/popconfirm"; import { FiltersInstructionsComponent } from "../filters-instructions/filters-instructions.component"; import { NzSelectComponent } from "ng-zorro-antd/select"; import { FormsModule } from "@angular/forms"; /** * Saved-workflow-section component contains information and functionality * of the saved workflows section and is re-used in the user projects section when a project is clicked * * This component: * - displays the workflows the user has access to * - allows easy searching for workflows by name or other parameters using Fuse.js * - sorting options * - creation of a new workflow * * Steps to add new search parameter: * 1. Add a newly formatted dropdown menu in the html and css files, and a backend call to retrieve any necessary data * 2. Create an array of objects to hold data for the search parameter and a boolean "checked" variable * 3. Write a callback function that triggers when new dropdown menu changes and updates a "filtered" array of the selected options * 4. Add call to searchWorkflows() in this function * 5. Add parameter to buildMasterFilterList() * 6. Update synchronousSearch() to search based on the new parameter (either through filter iteration or fuse) * - If it uses Fuse.js, create OrPathQuery object for multiple of the same new parameter and push it to the AndPathQuery array * - Do this in asyncSearch(if it requires a backend call) * 7. Add parameter as key to searchCriteria * 8. If it uses Fuse.js, update fuse keys and searchCriteriaPathMapping * 9. Add parameter to updateDropdownMenus() and setDropdownSelectionsToUnchecked() * * * */ @UntilDestroy() @Component({ selector: "texera-saved-workflow-section", templateUrl: "user-workflow.component.html", styleUrls: ["user-workflow.component.scss"], imports: [ NzCardComponent, NzSpaceCompactItemDirective, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, NzIconDirective, SortButtonComponent, NzUploadComponent, NzTooltipDirective, NgIf, NzPopconfirmDirective, FiltersComponent, FiltersInstructionsComponent, NzSelectComponent, FormsModule, SearchResultsComponent, NzSpaceCompactComponent, ], }) export class UserWorkflowComponent implements AfterViewInit { private _searchResultsComponent?: SearchResultsComponent; public isLogin = this.userService.isLogin(); private includePublic = false; public currentUid = this.userService.getCurrentUser()?.uid; @ViewChild(SearchResultsComponent) get searchResultsComponent(): SearchResultsComponent { if (this._searchResultsComponent) { return this._searchResultsComponent; } throw new Error("Property cannot be accessed before it is initialized."); } set searchResultsComponent(value: SearchResultsComponent) { this._searchResultsComponent = value; } private _filters?: FiltersComponent; @ViewChild(FiltersComponent) get filters(): FiltersComponent { if (this._filters) { return this._filters; } throw new Error("Property cannot be accessed before it is initialized."); } set filters(value: FiltersComponent) { value.masterFilterListChange.pipe(untilDestroyed(this)).subscribe({ next: () => this.search() }); this._filters = value; } private masterFilterList: ReadonlyArray | null = null; // receive input from parent components (UserProjectSection), if any @Input() public pid?: number = undefined; @Input() public accessLevel?: string = undefined; public sortMethod = SortMethod.EditTimeDesc; lastSortMethod: SortMethod | null = null; constructor( private userService: UserService, private workflowPersistService: WorkflowPersistService, private userProjectService: UserProjectService, private notificationService: NotificationService, private modalService: NzModalService, private router: Router, private downloadService: DownloadService, private searchService: SearchService, private config: GuiConfigService ) { this.userService .userChanged() .pipe(untilDestroyed(this)) .subscribe(() => { this.isLogin = this.userService.isLogin(); this.currentUid = this.userService.getCurrentUser()?.uid; }); } public multiWorkflowsOperationButtonEnabled(): boolean { if (this._searchResultsComponent) { return this.searchResultsComponent?.entries.filter(i => i.checked).length > 0; } else { return false; } } public selectionTooltip: string = "Select all"; public updateTooltip(): void { const entries = this.searchResultsComponent.entries; const allSelected = entries.every(entry => entry.checked); this.selectionTooltip = allSelected ? "Unselect all" : "Select all"; } ngAfterViewInit() { this.userService .userChanged() .pipe(untilDestroyed(this)) .subscribe(() => this.search()); } /** * open the Modal to add workflow(s) to project */ public onClickOpenAddWorkflow() { const modalRef = this.modalService.create({ nzContent: NgbdModalAddProjectWorkflowComponent, nzData: { projectId: this.pid }, nzFooter: null, nzTitle: "Add Workflows To Project", nzCentered: true, }); modalRef.afterClose.pipe(untilDestroyed(this)).subscribe(() => this.search(true)); } /** * open the Modal to remove workflow(s) from project */ public onClickOpenRemoveWorkflow() { const modalRef = this.modalService.create({ nzContent: NgbdModalRemoveProjectWorkflowComponent, nzData: { projectId: this.pid }, nzFooter: null, nzTitle: "Remove Workflows From Project", nzCentered: true, }); modalRef.afterClose.pipe(untilDestroyed(this)).subscribe(() => this.search(true)); } /** * Searches workflows with keywords and filters given in the masterFilterList. * @returns */ async search(forced: Boolean = false): Promise { const sameList = this.masterFilterList !== null && this.filters.masterFilterList.length === this.masterFilterList.length && this.filters.masterFilterList.every((v, i) => v === this.masterFilterList![i]); if (!forced && sameList && this.sortMethod === this.lastSortMethod) { // If the filter lists are the same, do no make the same request again. return; } this.lastSortMethod = this.sortMethod; this.masterFilterList = this.filters.masterFilterList; let filterParams = this.filters.getSearchFilterParameters(); if (isDefined(this.pid)) { // force the project id in the search query to be the current pid. filterParams.projectIds = [this.pid]; } this.searchResultsComponent.reset((start, count) => { return firstValueFrom( this.searchService .executeSearch( this.filters.getSearchKeywords(), filterParams, start, count, "workflow", this.sortMethod, this.isLogin, this.includePublic ) .pipe(map(({ entries, more }) => ({ entries, more }))) ); }); await this.searchResultsComponent.loadMore(); } /** * create a new workflow. will redirect to a pre-emptied workspace */ public onClickCreateNewWorkflowFromDashboard(): void { const emptyWorkflowContent: WorkflowContent = { operators: [], commentBoxes: [], links: [], operatorPositions: {}, settings: { dataTransferBatchSize: this.config.env.defaultDataTransferBatchSize, executionMode: this.config.env.defaultExecutionMode, }, }; let localPid = this.pid; this.workflowPersistService .createWorkflow(emptyWorkflowContent, DEFAULT_WORKFLOW_NAME) .pipe( tap(createdWorkflow => { if (!createdWorkflow.workflow.wid) { throw new Error("Workflow creation failed."); } }), mergeMap(createdWorkflow => { // Check if localPid is defined; if so, add the workflow to the project if (localPid) { return this.userProjectService.addWorkflowToProject(localPid, createdWorkflow.workflow.wid!).pipe( // Regardless of the project addition outcome, pass the wid downstream map(() => createdWorkflow.workflow.wid) ); } else { // If there's no localPid, skip adding to the project and directly pass the wid downstream return of(createdWorkflow.workflow.wid); } }), untilDestroyed(this) ) .subscribe({ next: (wid: number | undefined) => { // Use the wid here for navigation this.router.navigate([DASHBOARD_USER_WORKSPACE, wid]).then(null); }, error: (err: unknown) => this.notificationService.error("Workflow creation failed"), }); } /** * duplicate the current workflow. A new record will appear in frontend * workflow list and backend database. * * for workflow components inside a project-section, it will also add * the workflow to the project */ public async onClickDuplicateWorkflow(entry: DashboardEntry): Promise { if (entry.workflow.workflow.wid) { try { let duplicatedWorkflowsInfo: DashboardWorkflow[] = []; if (!isDefined(this.pid)) { duplicatedWorkflowsInfo = await firstValueFrom( this.workflowPersistService.duplicateWorkflow([entry.workflow.workflow.wid]) ); } else { const localPid = this.pid; duplicatedWorkflowsInfo = await firstValueFrom( this.workflowPersistService.duplicateWorkflow([entry.workflow.workflow.wid], localPid) ); } const userIds = new Set(); duplicatedWorkflowsInfo.forEach(workflow => { if (workflow.ownerId) { userIds.add(workflow.ownerId); } }); let userIdToInfoMap: { [key: number]: UserInfo } = {}; if (userIds.size > 0) { userIdToInfoMap = await firstValueFrom(this.searchService.getUserInfo(Array.from(userIds))); } const newEntries = duplicatedWorkflowsInfo.map(duplicatedWorkflowInfo => { const entry = new DashboardEntry(duplicatedWorkflowInfo); const userInfo = userIdToInfoMap[duplicatedWorkflowInfo.ownerId]; if (userInfo) { entry.setOwnerName(userInfo.userName); entry.setOwnerGoogleAvatar(userInfo.googleAvatar ?? ""); } if (this.currentUid !== undefined) { entry.setAccessUsers([this.currentUid]); } return entry; }); this.searchResultsComponent.entries = [...newEntries, ...this.searchResultsComponent.entries]; } catch (err: unknown) { console.log("Error duplicating workflow:", err); // @ts-ignore // TODO: fix this with notification component alert((err as any).error); } } } /** * deleteWorkflow trigger the delete workflow * component. If user confirms the deletion, the method sends * message to frontend and delete the workflow on frontend. It * calls the deleteWorkflow method in service which implements backend API. */ public deleteWorkflow(entry: DashboardEntry): void { if (entry.workflow.workflow.wid == undefined) { return; } this.workflowPersistService .deleteWorkflow([entry.workflow.workflow.wid]) .pipe(untilDestroyed(this)) .subscribe(_ => { this.searchResultsComponent.entries = this.searchResultsComponent.entries.filter( workflowEntry => workflowEntry.workflow.workflow.wid !== entry.workflow.workflow.wid ); }); } /** * Verify Uploaded file name and upload the file */ public onClickUploadExistingWorkflowFromLocal = (file: NzUploadFile): Observable => { const fileExtensionIndex = file.name.lastIndexOf("."); let upload$: Observable; if (file.name.substring(fileExtensionIndex) === ".zip") { upload$ = this.handleZipUploads(file as unknown as Blob); } else { upload$ = this.handleFileUploads(file as unknown as Blob, file.name); } return upload$.pipe( switchMap(() => from(this.search(true))), tap(() => this.notificationService.success("Upload Successful")), switchMap(() => of(false)) ); }; /** * process .zip file uploads */ private handleZipUploads(zipFile: Blob): Observable { let zip = new JSZip(); return from(zip.loadAsync(zipFile)).pipe( switchMap(zip => from( Promise.all( Object.keys(zip.files).map(relativePath => zip.files[relativePath] .async("blob") .then(content => lastValueFrom(this.handleFileUploads(content, relativePath))) ) ) ) ), map(() => undefined) ); } /** * Process .json file uploads */ private handleFileUploads(file: Blob, name: string): Observable { return new Observable(observer => { let reader = new FileReader(); reader.readAsText(file); reader.onload = () => { try { const result = reader.result; if (typeof result !== "string") { throw new Error("Incorrect format: file is not a string"); } const workflowContent = JSON.parse(result) as WorkflowContent; const fileExtensionIndex = name.lastIndexOf("."); let workflowName = fileExtensionIndex === -1 ? name : name.substring(0, fileExtensionIndex); if (workflowName.trim() === "") { workflowName = DEFAULT_WORKFLOW_NAME; } this.workflowPersistService .createWorkflow(workflowContent, workflowName) .pipe(untilDestroyed(this)) .subscribe({ next: uploadedWorkflow => { this.searchResultsComponent.entries = [ ...this.searchResultsComponent.entries, new DashboardEntry(uploadedWorkflow), ]; observer.next(); observer.complete(); }, error: (err: unknown) => { observer.error(err); }, }); } catch (error) { this.notificationService.error( "An error occurred when importing the workflow. Please import a workflow json file." ); observer.error(error); } }; }); } /** * Download selected workflow as zip file */ public onClickOpenDownloadZip(): void { const checkedEntries = this.searchResultsComponent.entries.filter(i => i.checked); if (checkedEntries.length === 0) { return; } const workflowEntries = checkedEntries.map(entry => ({ id: entry.workflow.workflow.wid!, name: entry.workflow.workflow.name, })); this.downloadService .downloadWorkflowsAsZip(workflowEntries) .pipe(untilDestroyed(this)) .subscribe({ next: () => { // this.searchResultsComponent.clearAllSelections(); }, error: (err: unknown) => console.error("Error downloading workflows:", err), }); } public onClickDuplicateSelectedWorkflows(): void { const checkedEntries = this.searchResultsComponent.entries.filter(i => i.checked); let targetWids: number[] = []; for (const entry of checkedEntries) { const wid = entry.workflow.workflow.wid; if (wid) { targetWids.push(wid); } else { return; } } if (targetWids.length > 0) { if (!isDefined(this.pid)) { this.workflowPersistService .duplicateWorkflow(targetWids) .pipe(untilDestroyed(this)) .subscribe({ next: duplicatedWorkflowsInfo => { this.searchResultsComponent.entries = [ ...duplicatedWorkflowsInfo.map(duplicatedWorkflowInfo => new DashboardEntry(duplicatedWorkflowInfo)), ...this.searchResultsComponent.entries, ]; // this.searchResultsComponent.clearAllSelections(); }, // TODO: fix this with notification component error: (err: unknown) => alert(err), }); } else { const localPid = this.pid; this.workflowPersistService .duplicateWorkflow(targetWids, localPid) .pipe(untilDestroyed(this)) .subscribe({ next: duplicatedWorkflowsInfo => { this.searchResultsComponent.entries = [ ...duplicatedWorkflowsInfo.map(duplicatedWorkflowInfo => new DashboardEntry(duplicatedWorkflowInfo)), ...this.searchResultsComponent.entries, ]; // this.searchResultsComponent.clearAllSelections(); }, // TODO: fix this with notification component error: (err: unknown) => alert(err), }); } } } public handleConfirmDeleteSelectedWorkflows(): void { const checkedEntries = this.searchResultsComponent.entries.filter(i => i.checked); let targetWids: number[] = []; for (const entry of checkedEntries) { const wid = entry.workflow.workflow.wid; if (wid) { targetWids.push(wid); } else { return; } } if (targetWids.length > 0) { this.workflowPersistService .deleteWorkflow(targetWids) .pipe(untilDestroyed(this)) .subscribe({ next: _ => { this.searchResultsComponent.entries = this.searchResultsComponent.entries.filter(workflowEntry => { let entryWid = workflowEntry.workflow.workflow.wid; // Check if wid is defined and if it's not included in targetWids return entryWid === undefined || !targetWids.includes(entryWid); }); }, // TODO: fix this with notification component error: (err: unknown) => alert(err), }); } } /** * Resolve name conflict */ private nameWorkflow(name: string, zip: JSZip) { let count = 0; let copyName = name; while (true) { if (!zip.files[copyName + ".json"]) { return copyName; } else { copyName = name + "-" + ++count; } } } public toggleSelection(): void { const allSelected = this.searchResultsComponent.entries.every(entry => entry.checked); if (allSelected) { this.searchResultsComponent.clearAllSelections(); this.updateTooltip(); } else { this.searchResultsComponent.selectAll(); this.updateTooltip(); } } public refreshSearchResult() { void this.search(true); } } ================================================ FILE: frontend/src/app/dashboard/component/user-dashboard-test-fixtures.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ //All times in test Workflows are in PST because our local machine's timezone is PST import { ExecutionMode, Workflow, WorkflowContent } from "../../common/type/workflow"; import { DashboardEntry } from "../type/dashboard-entry"; import { DashboardProject } from "../type/dashboard-project.interface"; //the Date class creates unix timestamp based on local timezone, therefore test workflow time needs to be in local timezone const oneDay = 86400000; const januaryFirst1970 = 28800000; // 1970-01-01 in PST export const testWorkflowContent = (operatorTypes: string[]): WorkflowContent => ({ operators: operatorTypes.map(t => ({ operatorType: t, operatorID: t, operatorVersion: "1", operatorProperties: {}, inputPorts: [], outputPorts: [], showAdvanced: false, })), commentBoxes: [], links: [], operatorPositions: {}, settings: { dataTransferBatchSize: 400, executionMode: ExecutionMode.PIPELINED }, }); export const testWorkflow1: Workflow = { wid: 1, name: "workflow 1", description: "dummy description", content: testWorkflowContent(["Aggregation", "NlpSentiment", "SimpleSink"]), creationTime: januaryFirst1970, lastModifiedTime: januaryFirst1970 + 2, isPublished: 0, readonly: false, }; export const testWorkflow2: Workflow = { wid: 2, name: "workflow 2", description: "dummy description", content: testWorkflowContent(["Aggregation", "NlpSentiment", "SimpleSink"]), creationTime: januaryFirst1970 + (oneDay + 3), lastModifiedTime: januaryFirst1970 + (oneDay + 3), isPublished: 0, readonly: false, }; export const testWorkflow3: Workflow = { wid: 3, name: "workflow 3", description: "dummy description", content: testWorkflowContent(["Aggregation", "NlpSentiment"]), creationTime: januaryFirst1970 + oneDay, lastModifiedTime: januaryFirst1970 + (oneDay + 4), isPublished: 0, readonly: false, }; export const testWorkflow4: Workflow = { wid: 4, name: "workflow 4", description: "dummy description", content: testWorkflowContent([]), creationTime: januaryFirst1970 + (oneDay + 3) * 2, lastModifiedTime: januaryFirst1970 + oneDay * 2 + 6, isPublished: 0, readonly: false, }; export const testWorkflow5: Workflow = { wid: 5, name: "workflow 5", description: "dummy description", content: testWorkflowContent([]), creationTime: januaryFirst1970 + oneDay * 2, lastModifiedTime: januaryFirst1970 + oneDay * 2 + 8, isPublished: 0, readonly: false, }; export const testDownloadWorkflow1: Workflow = { wid: 6, name: "workflow", description: "dummy description", content: testWorkflowContent([]), creationTime: januaryFirst1970, //januaryFirst1970 is 1970-01-01 in PST lastModifiedTime: januaryFirst1970 + 2, isPublished: 0, readonly: false, }; export const testDownloadWorkflow2: Workflow = { wid: 7, name: "workflow", description: "dummy description", content: testWorkflowContent([]), creationTime: januaryFirst1970 + (oneDay + 3), // oneDay is the number of milliseconds in a day lastModifiedTime: januaryFirst1970 + (oneDay + 3), isPublished: 0, readonly: false, }; export const testDownloadWorkflow3: Workflow = { wid: 8, name: "workflow", description: "dummy description", content: testWorkflowContent([]), creationTime: januaryFirst1970 + oneDay, lastModifiedTime: januaryFirst1970 + (oneDay + 4), isPublished: 0, readonly: false, }; export const testWorkflowFileNameConflictEntries: DashboardEntry[] = [ new DashboardEntry({ workflow: testDownloadWorkflow1, isOwner: true, ownerName: "Texera", accessLevel: "Write", projectIDs: [1], ownerId: 1, }), new DashboardEntry({ workflow: testDownloadWorkflow2, isOwner: true, ownerName: "Texera", accessLevel: "Write", projectIDs: [1, 2], ownerId: 1, }), new DashboardEntry({ workflow: testDownloadWorkflow3, isOwner: true, ownerName: "Angular", accessLevel: "Write", projectIDs: [1], ownerId: 2, }), ]; export const testWorkflowEntries: DashboardEntry[] = [ new DashboardEntry({ workflow: testWorkflow1, isOwner: true, ownerName: "Texera", accessLevel: "Write", projectIDs: [1], ownerId: 1, }), new DashboardEntry({ workflow: testWorkflow2, isOwner: true, ownerName: "Texera", accessLevel: "Write", projectIDs: [1, 2], ownerId: 1, }), new DashboardEntry({ workflow: testWorkflow3, isOwner: true, ownerName: "Angular", accessLevel: "Write", projectIDs: [1], ownerId: 2, }), new DashboardEntry({ workflow: testWorkflow4, isOwner: true, ownerName: "Angular", accessLevel: "Write", projectIDs: [3], ownerId: 2, }), new DashboardEntry({ workflow: testWorkflow5, isOwner: true, ownerName: "UCI", accessLevel: "Write", projectIDs: [3], ownerId: 3, }), ]; export const testUserProjects: DashboardProject[] = [ { pid: 1, name: "Project1", description: "p1", ownerId: 1, color: "#ffffff", creationTime: 0, accessLevel: "WRITE" }, { pid: 2, name: "Project2", description: "p1", ownerId: 1, color: "#ffffff", creationTime: 0, accessLevel: "WRITE" }, { pid: 3, name: "Project3", description: "p1", ownerId: 1, color: "#ffffff", creationTime: 0, accessLevel: "WRITE" }, ]; export const mockUserInfo = { 1: { userName: "Texera", googleAvatar: "avatar_url_1" }, 2: { userName: "Angular", googleAvatar: "avatar_url_2" }, 3: { userName: "UCI", googleAvatar: "avatar_url_3" }, }; ================================================ FILE: frontend/src/app/dashboard/service/admin/execution/admin-execution.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { HttpClient, HttpParams } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable } from "rxjs"; import { AppSettings } from "../../../../common/app-setting"; import { Execution } from "../../../../common/type/execution"; export const WORKFLOW_BASE_URL = `${AppSettings.getApiEndpoint()}/admin/execution`; @Injectable({ providedIn: "root", }) export class AdminExecutionService { constructor(private http: HttpClient) {} public getExecutionList( pageSize: number, pageIndex: number, sortField: string, sortDirection: string, filter: string[] ): Observable> { const params = new HttpParams().set("filter", filter.join(",")); return this.http.get>( `${WORKFLOW_BASE_URL}/executionList/${pageSize}/${pageIndex}/${sortField}/${sortDirection}`, { params } ); } public getTotalWorkflows(): Observable { return this.http.get(`${WORKFLOW_BASE_URL}/totalWorkflow`); } } ================================================ FILE: frontend/src/app/dashboard/service/admin/guard/admin-guard.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { CanActivate, Router } from "@angular/router"; import { UserService } from "../../../../common/service/user/user.service"; import { DASHBOARD_USER_WORKFLOW } from "../../../../app-routing.constant"; /** * AuthGuardService is a service can tell the router whether * it should allow navigation to a requested route. */ @Injectable() export class AdminGuardService implements CanActivate { constructor( private userService: UserService, private router: Router ) {} canActivate(): boolean { if (this.userService.isAdmin()) { return true; } else { this.router.navigate([DASHBOARD_USER_WORKFLOW]); return false; } } } ================================================ FILE: frontend/src/app/dashboard/service/admin/settings/admin-settings.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { Observable } from "rxjs"; import { map } from "rxjs/operators"; /** * Service for managing site-wide settings (key-value pairs) via REST API. * All values are stored and retrieved as plain strings. */ @Injectable({ providedIn: "root", }) export class AdminSettingsService { private readonly BASE_URL = "/api/admin/settings"; constructor(private http: HttpClient) {} getSetting(key: string): Observable { return this.http .get<{ key: string; value: string }>(`${this.BASE_URL}/${key}`) .pipe(map(resp => resp?.value ?? null)); } updateSetting(key: string, value: string): Observable { return this.http.put(`${this.BASE_URL}/${key}`, { value }, { withCredentials: true }); } resetSetting(key: string): Observable { return this.http.post(`${this.BASE_URL}/reset/${key}`, {}); } } ================================================ FILE: frontend/src/app/dashboard/service/admin/user/admin-user.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { HttpClient, HttpParams } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable } from "rxjs"; import { AppSettings } from "../../../../common/app-setting"; import { ExecutionQuota, File, Role, User, Workflow } from "../../../../common/type/user"; import { DatasetQuota } from "src/app/dashboard/type/quota-statistic.interface"; export const USER_BASE_URL = `${AppSettings.getApiEndpoint()}/admin/user`; export const USER_LIST_URL = `${USER_BASE_URL}/list`; export const USER_UPDATE_URL = `${USER_BASE_URL}/update`; export const USER_ADD_URL = `${USER_BASE_URL}/add`; export const USER_CREATED_FILES = `${USER_BASE_URL}/uploaded_files`; export const USER_UPLOADED_DATASE_SIZE = `${USER_BASE_URL}/dataset_size`; export const USER_UPLOADED_DATASET_COUNT = `${USER_BASE_URL}/uploaded_dataset`; export const USER_CREATED_DATASETS = `${USER_BASE_URL}/created_datasets`; export const USER_CREATED_WORKFLOWS = `${USER_BASE_URL}/created_workflows`; export const USER_ACCESS_WORKFLOWS = `${USER_BASE_URL}/access_workflows`; export const USER_ACCESS_FILES = `${USER_BASE_URL}/access_files`; export const USER_QUOTA_SIZE = `${USER_BASE_URL}/user_quota_size`; export const USER_DELETE_EXECUTION_COLLECTION = `${USER_BASE_URL}/deleteCollection`; @Injectable({ providedIn: "root", }) export class AdminUserService { constructor(private http: HttpClient) {} public getUserList(): Observable> { return this.http.get>(`${USER_LIST_URL}`); } public updateUser(uid: number, name: string, email: string, role: Role, comment: string): Observable { return this.http.put(`${USER_UPDATE_URL}`, { uid: uid, name: name, email: email, role: role, comment: comment, }); } public addUser(): Observable { return this.http.post(`${USER_ADD_URL}/`, {}); } public getUploadedFiles(uid: number): Observable> { let params = new HttpParams().set("user_id", uid.toString()); return this.http.get>(`${USER_CREATED_FILES}`, { params: params }); } public getCreatedDatasets(uid: number): Observable> { return this.http.get>(`${USER_CREATED_DATASETS}`); } public getCreatedWorkflows(uid: number): Observable> { let params = new HttpParams().set("user_id", uid.toString()); return this.http.get>(`${USER_CREATED_WORKFLOWS}`, { params: params }); } public getAccessFiles(uid: number): Observable> { let params = new HttpParams().set("user_id", uid.toString()); return this.http.get>(`${USER_ACCESS_FILES}`, { params: params }); } public getAccessWorkflows(uid: number): Observable> { let params = new HttpParams().set("user_id", uid.toString()); return this.http.get>(`${USER_ACCESS_WORKFLOWS}`, { params: params }); } public getExecutionQuota(uid: number): Observable> { let params = new HttpParams().set("user_id", uid.toString()); return this.http.get>(`${USER_QUOTA_SIZE}`, { params: params }); } public deleteExecutionCollection(eid: number): Observable { return this.http.delete(`${USER_DELETE_EXECUTION_COLLECTION}/${eid.toString()}`); } } ================================================ FILE: frontend/src/app/dashboard/service/user/dataset/dataset.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { HttpClient, HttpErrorResponse, HttpParams } from "@angular/common/http"; import { catchError, map, mergeMap, switchMap, tap, toArray } from "rxjs/operators"; import { Dataset, DatasetVersion } from "../../../../common/type/dataset"; import { AppSettings } from "../../../../common/app-setting"; import { EMPTY, from, Observable, throwError } from "rxjs"; import { DashboardDataset } from "../../../type/dashboard-dataset.interface"; import { DatasetFileNode } from "../../../../common/type/datasetVersionFileTree"; import { DatasetStagedObject } from "../../../../common/type/dataset-staged-object"; import { GuiConfigService } from "../../../../common/service/gui-config.service"; import { AuthService } from "src/app/common/service/user/auth.service"; export const DATASET_BASE_URL = "dataset"; export const DATASET_CREATE_URL = DATASET_BASE_URL + "/create"; export const DATASET_UPDATE_BASE_URL = DATASET_BASE_URL + "/update"; export const DATASET_UPDATE_NAME_URL = DATASET_UPDATE_BASE_URL + "/name"; export const DATASET_UPDATE_DESCRIPTION_URL = DATASET_UPDATE_BASE_URL + "/description"; export const DATASET_UPDATE_PUBLICITY_URL = "update/publicity"; export const DATASET_UPDATE_DOWNLOADABLE_URL = "update/downloadable"; export const DATASET_LIST_URL = DATASET_BASE_URL + "/list"; export const DATASET_SEARCH_URL = DATASET_BASE_URL + "/search"; export const DATASET_DELETE_URL = DATASET_BASE_URL + "/delete"; export const DATASET_VERSION_BASE_URL = "version"; export const DATASET_VERSION_RETRIEVE_LIST_URL = DATASET_VERSION_BASE_URL + "/list"; export const DATASET_VERSION_LATEST_URL = DATASET_VERSION_BASE_URL + "/latest"; export const DEFAULT_DATASET_NAME = "Untitled dataset"; export const DATASET_PUBLIC_VERSION_BASE_URL = "publicVersion"; export const DATASET_PUBLIC_VERSION_RETRIEVE_LIST_URL = DATASET_PUBLIC_VERSION_BASE_URL + "/list"; export const DATASET_GET_OWNERS_URL = DATASET_BASE_URL + "/user-dataset-owners"; export interface MultipartUploadProgress { filePath: string; percentage: number; status: "initializing" | "uploading" | "finished" | "aborted" | "failed"; uploadSpeed?: number; // bytes per second estimatedTimeRemaining?: number; // seconds totalTime?: number; // total seconds taken } @Injectable({ providedIn: "root", }) export class DatasetService { constructor( private http: HttpClient, private config: GuiConfigService ) {} public createDataset(dataset: Dataset): Observable { return this.http.post(`${AppSettings.getApiEndpoint()}/${DATASET_CREATE_URL}`, { datasetName: dataset.name, datasetDescription: dataset.description, isDatasetPublic: dataset.isPublic, isDatasetDownloadable: dataset.isDownloadable, }); } public getDataset(did: number, isLogin: boolean = true): Observable { const apiUrl = isLogin ? `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}` : `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/public/${did}`; return this.http.get(apiUrl); } /** * Retrieves a single file from a dataset version using a pre-signed URL. * @param filePath Relative file path within the dataset. * @param isLogin Determine whether a user is currently logged in * @returns Observable */ public retrieveDatasetVersionSingleFile(filePath: string, isLogin: boolean = true): Observable { const endpointSegment = isLogin ? "presign-download" : "public-presign-download"; const endpoint = `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${endpointSegment}?filePath=${encodeURIComponent(filePath)}`; return this.http .get<{ presignedUrl: string }>(endpoint) .pipe(switchMap(({ presignedUrl }) => this.http.get(presignedUrl, { responseType: "blob" }))); } /** * Retrieves a zip file of a dataset version. * @param did Dataset ID * @param dvid (Optional) Dataset version ID. If omitted, the latest version is downloaded. * @returns An Observable that emits a Blob containing the zip file. */ public retrieveDatasetVersionZip(did: number, dvid?: number): Observable { let params = new HttpParams(); if (dvid !== undefined && dvid !== null) { params = params.set("dvid", dvid.toString()); } else { params = params.set("latest", "true"); } return this.http.get(`${AppSettings.getApiEndpoint()}/dataset/${did}/versionZip`, { params, responseType: "blob", }); } public retrieveAccessibleDatasets(): Observable { return this.http.get(`${AppSettings.getApiEndpoint()}/${DATASET_LIST_URL}`); } public createDatasetVersion(did: number, newVersion: string): Observable { return this.http .post<{ datasetVersion: DatasetVersion; fileNodes: DatasetFileNode[]; }>(`${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/version/create`, newVersion, { headers: { "Content-Type": "text/plain" }, }) .pipe( map(response => { response.datasetVersion.fileNodes = response.fileNodes; return response.datasetVersion; }) ); } /** * Handles multipart upload for large files using RxJS, * with a concurrency limit on how many parts we process in parallel. * * Backend flow: * POST /dataset/multipart-upload?type=init&ownerEmail=...&datasetName=...&filePath=...&numParts=N * POST /dataset/multipart-upload/part?ownerEmail=...&datasetName=...&filePath=...&partNumber= (body: raw chunk) * POST /dataset/multipart-upload?type=finish&ownerEmail=...&datasetName=...&filePath=... * POST /dataset/multipart-upload?type=abort&ownerEmail=...&datasetName=...&filePath=... */ public multipartUpload( ownerEmail: string, datasetName: string, filePath: string, file: File, partSize: number, concurrencyLimit: number, restart: boolean ): Observable { const partCount = Math.ceil(file.size / partSize); return new Observable(observer => { // Track upload progress (bytes) for each part independently const partProgress = new Map(); let baselineUploaded = 0; // Progress tracking state let startTime: number | null = null; const speedSamples: number[] = []; let lastETA = 0; let lastUpdateTime = 0; const lastStats = { uploadSpeed: 0, estimatedTimeRemaining: 0, totalTime: 0, }; const getTotalTime = () => (startTime ? (Date.now() - startTime) / 1000 : 0); // Calculate stats with smoothing and simple throttling (~1s) const calculateStats = (totalUploaded: number) => { if (startTime === null) { startTime = Date.now(); } const now = Date.now(); const elapsed = getTotalTime(); const shouldUpdate = now - lastUpdateTime >= 1000; if (!shouldUpdate) { // keep totalTime fresh even when throttled lastStats.totalTime = elapsed; return lastStats; } lastUpdateTime = now; const sessionUploaded = Math.max(0, totalUploaded - baselineUploaded); const currentSpeed = elapsed > 0 ? sessionUploaded / elapsed : 0; speedSamples.push(currentSpeed); if (speedSamples.length > 5) { speedSamples.shift(); } const avgSpeed = speedSamples.length > 0 ? speedSamples.reduce((a, b) => a + b, 0) / speedSamples.length : 0; const remaining = file.size - totalUploaded; let eta = avgSpeed > 0 ? remaining / avgSpeed : 0; eta = Math.min(eta, 24 * 60 * 60); // cap ETA at 24h if (lastETA > 0 && eta > 0) { const maxChange = lastETA * 0.3; const diff = Math.abs(eta - lastETA); if (diff > maxChange) { eta = lastETA + (eta > lastETA ? maxChange : -maxChange); } } lastETA = eta; const percentComplete = (totalUploaded / file.size) * 100; if (percentComplete > 95) { eta = Math.min(eta, 10); } lastStats.uploadSpeed = avgSpeed; lastStats.estimatedTimeRemaining = Math.max(0, Math.round(eta)); lastStats.totalTime = elapsed; return lastStats; }; // 1. INIT: ask backend to create a LakeFS multipart upload session const initParams = new HttpParams() .set("type", "init") .set("ownerEmail", ownerEmail) .set("datasetName", datasetName) .set("filePath", encodeURIComponent(filePath)) .set("fileSizeBytes", file.size.toString()) .set("partSizeBytes", partSize.toString()) .set("restart", restart); const init$ = this.http.post<{ missingParts: number[]; completedPartsCount: number }>( `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/multipart-upload`, {}, { params: initParams } ); const subscription = init$ .pipe( switchMap(initResp => { const missingParts = (initResp?.missingParts ?? []).slice(); const completedPartsCount = initResp?.completedPartsCount ?? 0; const missingBytes = missingParts.reduce((sum, partNumber) => { const start = (partNumber - 1) * partSize; const end = Math.min(start + partSize, file.size); return sum + (end - start); }, 0); baselineUploaded = file.size - missingBytes; const baselinePct = partCount > 0 ? Math.round((completedPartsCount / partCount) * 100) : 0; observer.next({ filePath, percentage: baselinePct, status: "initializing", uploadSpeed: 0, estimatedTimeRemaining: 0, totalTime: 0, }); // 2. Upload each part to /multipart-upload/part using XMLHttpRequest return from(missingParts).pipe( mergeMap(partNumber => { const start = (partNumber - 1) * partSize; const end = Math.min(start + partSize, file.size); const chunk = file.slice(start, end); return new Observable(partObserver => { const xhr = new XMLHttpRequest(); xhr.upload.addEventListener("progress", event => { if (event.lengthComputable) { partProgress.set(partNumber, event.loaded); let totalUploaded = baselineUploaded; // CHANGED partProgress.forEach(bytes => { totalUploaded += bytes; }); const percentage = Math.round((totalUploaded / file.size) * 100); const stats = calculateStats(totalUploaded); observer.next({ filePath, percentage: Math.min(percentage, 99), status: "uploading", ...stats, }); } }); xhr.addEventListener("load", () => { if (xhr.status === 200 || xhr.status === 204) { // Mark part as fully uploaded partProgress.set(partNumber, chunk.size); let totalUploaded = baselineUploaded; partProgress.forEach(bytes => { totalUploaded += bytes; }); // Force stats recompute on completion lastUpdateTime = 0; const percentage = Math.round((totalUploaded / file.size) * 100); const stats = calculateStats(totalUploaded); observer.next({ filePath, percentage: Math.min(percentage, 99), status: "uploading", ...stats, }); partObserver.complete(); } else { partObserver.error(new Error(`Failed to upload part ${partNumber} (HTTP ${xhr.status})`)); } }); xhr.addEventListener("error", () => { // Remove failed part from progress partProgress.delete(partNumber); partObserver.error(new Error(`Failed to upload part ${partNumber}`)); }); const partUrl = `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/multipart-upload/part` + `?ownerEmail=${encodeURIComponent(ownerEmail)}` + `&datasetName=${encodeURIComponent(datasetName)}` + `&filePath=${encodeURIComponent(filePath)}` + `&partNumber=${partNumber}`; xhr.open("POST", partUrl); xhr.setRequestHeader("Content-Type", "application/octet-stream"); const token = AuthService.getAccessToken(); if (token) { xhr.setRequestHeader("Authorization", `Bearer ${token}`); } xhr.send(chunk); return () => { try { xhr.abort(); } catch {} }; }); }, concurrencyLimit), toArray(), // wait for all parts // 3. FINISH: notify backend that all parts are done switchMap(() => { const finishParams = new HttpParams() .set("type", "finish") .set("ownerEmail", ownerEmail) .set("datasetName", datasetName) .set("filePath", encodeURIComponent(filePath)); return this.http.post( `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/multipart-upload`, {}, { params: finishParams } ); }), tap(() => { const totalTime = getTotalTime(); observer.next({ filePath, percentage: 100, status: "finished", uploadSpeed: 0, estimatedTimeRemaining: 0, totalTime, }); observer.complete(); }), catchError((error: unknown) => { // On error, compute best-effort percentage from bytes we've seen let totalUploaded = baselineUploaded; partProgress.forEach(bytes => { totalUploaded += bytes; }); const percentage = file.size > 0 ? Math.round((totalUploaded / file.size) * 100) : 0; observer.next({ filePath, percentage, status: "failed", uploadSpeed: 0, estimatedTimeRemaining: 0, totalTime: getTotalTime(), }); return throwError(() => error); }) ); }) ) .subscribe({ error: (err: unknown) => observer.error(err), }); return () => subscription.unsubscribe(); }); } public listMultipartUploads(ownerEmail: string, datasetName: string): Observable { const params = new HttpParams().set("type", "list").set("ownerEmail", ownerEmail).set("datasetName", datasetName); return this.http .post<{ filePaths: string[]; }>(`${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/multipart-upload`, {}, { params }) .pipe(map(res => res?.filePaths ?? [])); } public finalizeMultipartUpload( ownerEmail: string, datasetName: string, filePath: string, isAbort: boolean ): Observable { const params = new HttpParams() .set("type", isAbort ? "abort" : "finish") .set("ownerEmail", ownerEmail) .set("datasetName", datasetName) .set("filePath", encodeURIComponent(filePath)); return this.http.post( `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/multipart-upload`, {}, { params } ); } /** * Resets a dataset file difference in LakeFS. * @param did Dataset ID * @param filePath File path to reset */ public resetDatasetFileDiff(did: number, filePath: string): Observable { const params = new HttpParams().set("filePath", encodeURIComponent(filePath)); return this.http.put(`${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/diff`, {}, { params }); } /** * Deletes a dataset file from LakeFS. * @param did Dataset ID * @param filePath File path to delete */ public deleteDatasetFile(did: number, filePath: string): Observable { const params = new HttpParams().set("filePath", encodeURIComponent(filePath)); return this.http.delete(`${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/file`, { params }); } /** * Retrieves the list of uncommitted dataset changes (diffs). * @param did Dataset ID */ public getDatasetDiff(did: number): Observable { return this.http.get(`${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/diff`); } /** * retrieve a list of versions of a dataset. The list is sorted so that the latest versions are at front. * @param did * @param isLogin */ public retrieveDatasetVersionList(did: number, isLogin: boolean = true): Observable { const apiEndPont = isLogin ? `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/${DATASET_VERSION_RETRIEVE_LIST_URL}` : `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/${DATASET_PUBLIC_VERSION_RETRIEVE_LIST_URL}`; return this.http.get(apiEndPont); } /** * retrieve the latest version of a dataset. * @param did */ public retrieveDatasetLatestVersion(did: number): Observable { return this.http .get<{ datasetVersion: DatasetVersion; fileNodes: DatasetFileNode[]; }>(`${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/${DATASET_VERSION_LATEST_URL}`) .pipe( map(response => { response.datasetVersion.fileNodes = response.fileNodes; return response.datasetVersion; }) ); } /** * retrieve a list of nodes that represent the files in the version * @param did * @param dvid * @param isLogin */ public retrieveDatasetVersionFileTree( did: number, dvid: number, isLogin: boolean = true ): Observable<{ fileNodes: DatasetFileNode[]; size: number }> { const apiUrl = isLogin ? `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/${DATASET_VERSION_BASE_URL}/${dvid}/rootFileNodes` : `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/${DATASET_PUBLIC_VERSION_BASE_URL}/${dvid}/rootFileNodes`; return this.http.get<{ fileNodes: DatasetFileNode[]; size: number }>(apiUrl); } public deleteDatasets(did: number): Observable { return this.http.delete(`${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}`); } public updateDatasetName(did: number, name: string): Observable { return this.http.post(`${AppSettings.getApiEndpoint()}/${DATASET_UPDATE_NAME_URL}`, { did: did, name: name, }); } public updateDatasetDescription(did: number, description: string): Observable { return this.http.post(`${AppSettings.getApiEndpoint()}/${DATASET_UPDATE_DESCRIPTION_URL}`, { did: did, description: description, }); } public updateDatasetPublicity(did: number): Observable { return this.http.post( `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/${DATASET_UPDATE_PUBLICITY_URL}`, {} ); } public updateDatasetDownloadable(did: number): Observable { return this.http.post( `${AppSettings.getApiEndpoint()}/${DATASET_BASE_URL}/${did}/${DATASET_UPDATE_DOWNLOADABLE_URL}`, {} ); } public retrieveOwners(): Observable { return this.http.get(`${AppSettings.getApiEndpoint()}/${DATASET_GET_OWNERS_URL}`); } public updateDatasetCoverImage(did: number, coverImage: string): Observable { return this.http.post(`${AppSettings.getApiEndpoint()}/dataset/${did}/update/cover`, { coverImage: coverImage, }); } } ================================================ FILE: frontend/src/app/dashboard/service/user/download/download.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { TestBed } from "@angular/core/testing"; import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing"; import { DownloadService } from "./download.service"; import { DatasetService } from "../dataset/dataset.service"; import { FileSaverService } from "../file/file-saver.service"; import { NotificationService } from "../../../../common/service/notification/notification.service"; import { WorkflowPersistService } from "../../../../common/service/workflow-persist/workflow-persist.service"; import { firstValueFrom, lastValueFrom, of, throwError } from "rxjs"; import { commonTestProviders } from "../../../../common/testing/test-utils"; import type { Mocked } from "vitest"; describe("DownloadService", () => { let downloadService: DownloadService; let datasetServiceSpy: Mocked; let fileSaverServiceSpy: Mocked; let notificationServiceSpy: Mocked; let workflowPersistServiceSpy: Mocked; let httpMock: HttpTestingController; beforeEach(() => { const datasetSpy = { retrieveDatasetVersionSingleFile: vi.fn(), retrieveDatasetVersionZip: vi.fn() }; const fileSaverSpy = { saveAs: vi.fn() }; const notificationSpy = { info: vi.fn(), success: vi.fn(), error: vi.fn() }; const workflowPersistSpy = { retrieveWorkflow: vi.fn() }; TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ DownloadService, { provide: DatasetService, useValue: datasetSpy }, { provide: FileSaverService, useValue: fileSaverSpy }, { provide: NotificationService, useValue: notificationSpy }, { provide: WorkflowPersistService, useValue: workflowPersistSpy }, ...commonTestProviders, ], }); downloadService = TestBed.inject(DownloadService); datasetServiceSpy = TestBed.inject(DatasetService) as unknown as Mocked; fileSaverServiceSpy = TestBed.inject(FileSaverService) as unknown as Mocked; notificationServiceSpy = TestBed.inject(NotificationService) as unknown as Mocked; workflowPersistServiceSpy = TestBed.inject(WorkflowPersistService) as unknown as Mocked; httpMock = TestBed.inject(HttpTestingController); }); afterEach(() => { // Catch any test that fires an HTTP request without flushing it; keeps // the suite safe as more specs start using HttpTestingController. httpMock.verify(); }); // ─── downloadSingleFile ─────────────────────────────────────────────────── it("downloads a single file and saves it under the basename of the path", async () => { const mockBlob = new Blob(["test content"], { type: "text/plain" }); datasetServiceSpy.retrieveDatasetVersionSingleFile.mockReturnValue(of(mockBlob)); const result = await firstValueFrom(downloadService.downloadSingleFile("test/file.txt", true)); expect(result).toBe(mockBlob); expect(notificationServiceSpy.info).toHaveBeenCalledWith("Starting to download file test/file.txt"); expect(datasetServiceSpy.retrieveDatasetVersionSingleFile).toHaveBeenCalledWith("test/file.txt", true); expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(mockBlob, "file.txt"); expect(notificationServiceSpy.success).toHaveBeenCalledWith("File test/file.txt has been downloaded"); }); it("falls back to a default filename when the path has no basename segment", async () => { const mockBlob = new Blob(["x"], { type: "text/plain" }); datasetServiceSpy.retrieveDatasetVersionSingleFile.mockReturnValue(of(mockBlob)); await firstValueFrom(downloadService.downloadSingleFile("", true)); // path.split("/").pop() returns "" for "", which falls through to the default name expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(mockBlob, "download"); }); it("propagates errors from downloadSingleFile and emits the error notification", async () => { datasetServiceSpy.retrieveDatasetVersionSingleFile.mockReturnValue(throwError(() => new Error("boom"))); await expect(firstValueFrom(downloadService.downloadSingleFile("test/file.txt", true))).rejects.toThrow("boom"); expect(notificationServiceSpy.info).toHaveBeenCalledWith("Starting to download file test/file.txt"); expect(fileSaverServiceSpy.saveAs).not.toHaveBeenCalled(); expect(notificationServiceSpy.error).toHaveBeenCalledWith("Error downloading file 'test/file.txt'"); }); it("passes isLogin=false through to retrieveDatasetVersionSingleFile", async () => { const mockBlob = new Blob(["x"], { type: "text/plain" }); datasetServiceSpy.retrieveDatasetVersionSingleFile.mockReturnValue(of(mockBlob)); await firstValueFrom(downloadService.downloadSingleFile("public/sample.csv", false)); expect(datasetServiceSpy.retrieveDatasetVersionSingleFile).toHaveBeenCalledWith("public/sample.csv", false); }); // ─── downloadDataset ────────────────────────────────────────────────────── it("downloads the latest dataset version as a zip named after the dataset", async () => { const mockBlob = new Blob(["dataset content"], { type: "application/zip" }); datasetServiceSpy.retrieveDatasetVersionZip.mockReturnValue(of(mockBlob)); const result = await firstValueFrom(downloadService.downloadDataset(1, "TestDataset")); expect(result).toBe(mockBlob); expect(notificationServiceSpy.info).toHaveBeenCalledWith( "Starting to download the latest version of the dataset as ZIP" ); expect(datasetServiceSpy.retrieveDatasetVersionZip).toHaveBeenCalledWith(1); expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(mockBlob, "TestDataset.zip"); expect(notificationServiceSpy.success).toHaveBeenCalledWith( "The latest version of the dataset has been downloaded as ZIP" ); }); it("emits the dataset error notification and rethrows on retrieve failure", async () => { datasetServiceSpy.retrieveDatasetVersionZip.mockReturnValue(throwError(() => new Error("fail"))); await expect(firstValueFrom(downloadService.downloadDataset(1, "TestDataset"))).rejects.toThrow("fail"); expect(fileSaverServiceSpy.saveAs).not.toHaveBeenCalled(); expect(notificationServiceSpy.error).toHaveBeenCalledWith( "Error downloading the latest version of the dataset as ZIP" ); }); // ─── downloadDatasetVersion ─────────────────────────────────────────────── it("downloads a specific dataset version with composite zip name", async () => { const mockBlob = new Blob(["v1"], { type: "application/zip" }); datasetServiceSpy.retrieveDatasetVersionZip.mockReturnValue(of(mockBlob)); const result = await firstValueFrom(downloadService.downloadDatasetVersion(1, 2, "TestDataset", "v1.0")); expect(result).toBe(mockBlob); expect(notificationServiceSpy.info).toHaveBeenCalledWith("Starting to download version v1.0 as ZIP"); expect(datasetServiceSpy.retrieveDatasetVersionZip).toHaveBeenCalledWith(1, 2); expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(mockBlob, "TestDataset-v1.0.zip"); expect(notificationServiceSpy.success).toHaveBeenCalledWith("Version v1.0 has been downloaded as ZIP"); }); it("emits the version-specific error notification on retrieve failure", async () => { datasetServiceSpy.retrieveDatasetVersionZip.mockReturnValue(throwError(() => new Error("nope"))); await expect(firstValueFrom(downloadService.downloadDatasetVersion(1, 2, "TestDataset", "v1.0"))).rejects.toThrow( "nope" ); expect(notificationServiceSpy.error).toHaveBeenCalledWith("Error downloading version 'v1.0' as ZIP"); }); // ─── downloadWorkflow ───────────────────────────────────────────────────── it("downloads a workflow as a JSON blob named after the workflow", async () => { const workflowContent = { hello: "world", operators: [] }; workflowPersistServiceSpy.retrieveWorkflow.mockReturnValue(of({ content: workflowContent } as any)); const result = await firstValueFrom(downloadService.downloadWorkflow(42, "MyWorkflow")); expect(result.fileName).toBe("MyWorkflow.json"); expect(result.blob).toBeInstanceOf(Blob); expect(result.blob.type).toBe("text/plain;charset=utf-8"); expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(result.blob, "MyWorkflow.json"); // Blob.text() isn't shipped by jsdom, so we don't pin the body content // here; the saveAs assertion above already verifies the path that // produced it. }); // ─── downloadWorkflowsAsZip ─────────────────────────────────────────────── it("downloads the workflow ZIP and routes through createWorkflowsZip", async () => { const mockBlob = new Blob(["zip"], { type: "application/zip" }); const entries = [ { id: 1, name: "Workflow1" }, { id: 2, name: "Workflow2" }, ]; vi.spyOn(downloadService as any, "createWorkflowsZip").mockReturnValue(of(mockBlob)); const result = await firstValueFrom(downloadService.downloadWorkflowsAsZip(entries)); expect(result).toBe(mockBlob); expect(notificationServiceSpy.info).toHaveBeenCalledWith("Starting to download workflows as ZIP"); expect((downloadService as any).createWorkflowsZip).toHaveBeenCalledWith(entries); expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith( mockBlob, expect.stringMatching(/^workflowExports-.*\.zip$/) ); expect(notificationServiceSpy.success).toHaveBeenCalledWith("Workflows have been downloaded as ZIP"); }); it("propagates errors from createWorkflowsZip with the expected error notification", async () => { vi.spyOn(downloadService as any, "createWorkflowsZip").mockReturnValue(throwError(() => new Error("zip fail"))); await expect(firstValueFrom(downloadService.downloadWorkflowsAsZip([{ id: 1, name: "W" }]))).rejects.toThrow( "zip fail" ); expect(fileSaverServiceSpy.saveAs).not.toHaveBeenCalled(); expect(notificationServiceSpy.error).toHaveBeenCalledWith("Error downloading workflows as ZIP"); }); // ─── downloadOperatorsResult ────────────────────────────────────────────── it("downloads a single operator file directly when there's exactly one file", async () => { const fileBlob = new Blob(["hello"], { type: "text/plain" }); const result = await firstValueFrom( downloadService.downloadOperatorsResult([of([{ filename: "out.csv", blob: fileBlob }])], { wid: 1, name: "W", } as any) ); expect(result).toBe(fileBlob); expect(notificationServiceSpy.info).toHaveBeenCalledWith("Starting to download operator result"); expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(fileBlob, "out.csv"); expect(notificationServiceSpy.success).toHaveBeenCalledWith("Operator result has been downloaded"); }); // The multi-file zip path goes through `new JSZip()` against the // `import * as JSZip from "jszip"` namespace, which the build flags as // `Constructing "JSZip" will crash at run-time because it's an import // namespace object`. Vitest reproduces the failure (`__vite_ssr_import_* // is not a constructor`). Tracked as a separate cleanup in the codebase; // the test is here as a placeholder so we re-enable it once the import // is normalised to a default import. it.skip("zips multiple operator files into a workflow-named archive", async () => { const a = new Blob(["a"], { type: "text/plain" }); const b = new Blob(["b"], { type: "text/plain" }); const result = await firstValueFrom( downloadService.downloadOperatorsResult( [ of([ { filename: "a.csv", blob: a }, { filename: "b.csv", blob: b }, ]), ], { wid: 7, name: "TwoFile" } as any ) ); expect(result).toBeInstanceOf(Blob); expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(expect.any(Blob), "results_7_TwoFile.zip"); expect(notificationServiceSpy.success).toHaveBeenCalledWith("Operator results have been downloaded as ZIP"); }); it("errors out cleanly when no operator result files are provided", async () => { await expect( firstValueFrom(downloadService.downloadOperatorsResult([of([])], { wid: 1, name: "Empty" } as any)) ).rejects.toThrow("No files to download"); }); // ─── getWorkflowResultDownloadability ───────────────────────────────────── it("hits the downloadability endpoint and returns the operator → labels map", async () => { const promise = lastValueFrom(downloadService.getWorkflowResultDownloadability(99)); const req = httpMock.expectOne(r => r.url.includes("/99/result/downloadability")); expect(req.request.method).toBe("GET"); req.flush({ "op-1": ["my-dataset"], "op-2": [] }); const map = await promise; expect(map).toEqual({ "op-1": ["my-dataset"], "op-2": [] }); }); }); ================================================ FILE: frontend/src/app/dashboard/service/user/download/download.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { forkJoin, from, Observable, of, throwError } from "rxjs"; import { catchError, map, switchMap, tap } from "rxjs/operators"; import { FileSaverService } from "../file/file-saver.service"; import { NotificationService } from "../../../../common/service/notification/notification.service"; import { DatasetService } from "../dataset/dataset.service"; import { WorkflowPersistService } from "src/app/common/service/workflow-persist/workflow-persist.service"; import * as JSZip from "jszip"; import { Workflow } from "../../../../common/type/workflow"; import { HttpClient, HttpResponse } from "@angular/common/http"; import { WORKFLOW_EXECUTIONS_API_BASE_URL } from "../workflow-executions/workflow-executions.service"; import { DashboardWorkflowComputingUnit } from "../../../../common/type/workflow-computing-unit"; import { TOKEN_KEY } from "../../../../common/service/user/auth.service"; export const EXPORT_BASE_URL = "result/export"; const IFRAME_TIMEOUT_MS = 10000; export const DOWNLOADABILITY_BASE_URL = "result/downloadability"; interface DownloadableItem { blob: Blob; fileName: string; } export interface ExportWorkflowJsonResponse { status: string; message: string; } export interface WorkflowResultDownloadabilityResponse { [operatorId: string]: string[]; // operatorId -> array of dataset labels blocking export } @Injectable({ providedIn: "root", }) export class DownloadService { constructor( private fileSaverService: FileSaverService, private notificationService: NotificationService, private datasetService: DatasetService, private workflowPersistService: WorkflowPersistService, private http: HttpClient ) {} downloadWorkflow(id: number, name: string): Observable { return this.workflowPersistService.retrieveWorkflow(id).pipe( map(({ content }) => { const workflowJson = JSON.stringify(content, null, 2); const fileName = `${name}.json`; const blob = new Blob([workflowJson], { type: "text/plain;charset=utf-8" }); return { blob, fileName }; }), tap(this.saveFile.bind(this)) ); } downloadDataset(id: number, name: string): Observable { return this.downloadWithNotification( () => this.datasetService.retrieveDatasetVersionZip(id), `${name}.zip`, "Starting to download the latest version of the dataset as ZIP", "The latest version of the dataset has been downloaded as ZIP", "Error downloading the latest version of the dataset as ZIP" ); } downloadDatasetVersion( datasetId: number, datasetVersionId: number, datasetName: string, versionName: string ): Observable { return this.downloadWithNotification( () => this.datasetService.retrieveDatasetVersionZip(datasetId, datasetVersionId), `${datasetName}-${versionName}.zip`, `Starting to download version ${versionName} as ZIP`, `Version ${versionName} has been downloaded as ZIP`, `Error downloading version '${versionName}' as ZIP` ); } downloadSingleFile(filePath: string, isLogin: boolean = true): Observable { const DEFAULT_FILE_NAME = "download"; const fileName = filePath.split("/").pop() || DEFAULT_FILE_NAME; return this.downloadWithNotification( () => this.datasetService.retrieveDatasetVersionSingleFile(filePath, isLogin), fileName, `Starting to download file ${filePath}`, `File ${filePath} has been downloaded`, `Error downloading file '${filePath}'` ); } downloadWorkflowsAsZip(workflowEntries: Array<{ id: number; name: string }>): Observable { return this.downloadWithNotification( () => this.createWorkflowsZip(workflowEntries), `workflowExports-${new Date().toISOString()}.zip`, "Starting to download workflows as ZIP", "Workflows have been downloaded as ZIP", "Error downloading workflows as ZIP" ); } /** * Retrieves workflow result downloadability information from the backend. * Returns a map of operator IDs to arrays of dataset labels that block their export. * * @param workflowId The workflow ID to check * @returns Observable of downloadability information */ public getWorkflowResultDownloadability(workflowId: number): Observable { const urlPath = `${WORKFLOW_EXECUTIONS_API_BASE_URL}/${workflowId}/${DOWNLOADABILITY_BASE_URL}`; return this.http.get(urlPath); } /** * Export the workflow result to specified dataset(s). */ public exportWorkflowResultToDataset( exportType: string, workflowId: number, workflowName: string, operators: { id: string; outputType: string; }[], datasetIds: number[], rowIndex: number, columnIndex: number, filename: string, unit: DashboardWorkflowComputingUnit // computing unit for cluster setting ): Observable | HttpResponse> { const computingUnitId = unit.computingUnit.cuid; const requestBody = { exportType, workflowId, workflowName, operators, datasetIds, rowIndex, columnIndex, filename, computingUnitId, }; const urlPath = unit && unit.computingUnit.type == "kubernetes" && unit.computingUnit?.cuid ? `${WORKFLOW_EXECUTIONS_API_BASE_URL}/${EXPORT_BASE_URL}/dataset?cuid=${unit.computingUnit.cuid}` : `${WORKFLOW_EXECUTIONS_API_BASE_URL}/${EXPORT_BASE_URL}/dataset`; return this.http.post(urlPath, requestBody, { responseType: "json", observe: "response", headers: { "Content-Type": "application/json", Accept: "application/json", }, }); } /** * Export the workflow result to local filesystem. The export is handled by the browser. */ public exportWorkflowResultToLocal( exportType: string, workflowId: number, workflowName: string, operators: { id: string; outputType: string; }[], rowIndex: number, columnIndex: number, filename: string, unit: DashboardWorkflowComputingUnit // computing unit for cluster setting ): void { const computingUnitId = unit.computingUnit.cuid; const datasetIds: number[] = []; const requestBody = { exportType, workflowId, workflowName, operators, datasetIds, rowIndex, columnIndex, filename, computingUnitId, }; const token = localStorage.getItem(TOKEN_KEY) ?? ""; const urlPath = unit && unit.computingUnit.type == "kubernetes" && unit.computingUnit?.cuid ? `${WORKFLOW_EXECUTIONS_API_BASE_URL}/${EXPORT_BASE_URL}/local?cuid=${unit.computingUnit.cuid}` : `${WORKFLOW_EXECUTIONS_API_BASE_URL}/${EXPORT_BASE_URL}/local`; const iframe = document.createElement("iframe"); iframe.name = "download-iframe"; iframe.style.display = "none"; document.body.appendChild(iframe); const form = document.createElement("form"); form.method = "POST"; form.action = urlPath; form.target = "download-iframe"; form.enctype = "application/x-www-form-urlencoded"; form.style.display = "none"; const requestInput = document.createElement("input"); requestInput.type = "hidden"; requestInput.name = "request"; requestInput.value = JSON.stringify(requestBody); form.appendChild(requestInput); const tokenInput = document.createElement("input"); tokenInput.type = "hidden"; tokenInput.name = "token"; tokenInput.value = token; form.appendChild(tokenInput); document.body.appendChild(form); form.submit(); setTimeout(() => { document.body.removeChild(form); document.body.removeChild(iframe); }, IFRAME_TIMEOUT_MS); } downloadOperatorsResult( resultObservables: Observable<{ filename: string; blob: Blob }[]>[], workflow: Workflow ): Observable { return forkJoin(resultObservables).pipe( map(filesArray => filesArray.flat()), switchMap(files => { if (files.length === 0) { return throwError(() => new Error("No files to download")); } else if (files.length === 1) { // Single file, download directly return this.downloadWithNotification( () => of(files[0].blob), files[0].filename, "Starting to download operator result", "Operator result has been downloaded", "Error downloading operator result" ); } else { // Multiple files, create a zip return this.downloadWithNotification( () => this.createZip(files), `results_${workflow.wid}_${workflow.name}.zip`, "Starting to download operator results as ZIP", "Operator results have been downloaded as ZIP", "Error downloading operator results as ZIP" ); } }) ); } private createWorkflowsZip(workflowEntries: Array<{ id: number; name: string }>): Observable { const zip = new JSZip(); const downloadObservables = workflowEntries.map(entry => this.downloadWorkflow(entry.id, entry.name).pipe( tap(({ blob, fileName }) => { zip.file(this.nameWorkflow(fileName, zip), blob); }) ) ); return forkJoin(downloadObservables).pipe(switchMap(() => zip.generateAsync({ type: "blob" }))); } private nameWorkflow(name: string, zip: JSZip): string { let count = 0; let copyName = name; while (zip.file(copyName)) { copyName = `${name.replace(".json", "")}-${++count}.json`; } return copyName; } private downloadWithNotification( retrieveFunction: () => Observable, fileName: string, startMessage: string, successMessage: string, errorMessage: string ): Observable { this.notificationService.info(startMessage); return retrieveFunction().pipe( tap(blob => { this.saveFile({ blob, fileName }); this.notificationService.success(successMessage); }), catchError((error: unknown) => { this.notificationService.error(errorMessage); return throwError(() => error); }) ); } private saveFile({ blob, fileName }: DownloadableItem): void { this.fileSaverService.saveAs(blob, fileName); } private createZip(files: { filename: string; blob: Blob }[]): Observable { const zip = new JSZip(); files.forEach(file => { zip.file(file.filename, file.blob); }); return from(zip.generateAsync({ type: "blob" })); } } ================================================ FILE: frontend/src/app/dashboard/service/user/file/file-saver.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import * as FileSaver from "file-saver"; @Injectable({ providedIn: "root", }) export class FileSaverService { saveAs(data: Blob | string, filename?: string, options?: FileSaver.FileSaverOptions): void { FileSaver.saveAs(data, filename, options); } } ================================================ FILE: frontend/src/app/dashboard/service/user/flarum/flarum.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { UserService } from "../../../../common/service/user/user.service"; @Injectable({ providedIn: "root", }) export class FlarumService { constructor( private http: HttpClient, private userService: UserService ) {} register() { const user = this.userService.getCurrentUser(); return this.http.post( "forum/api/users", { data: { attributes: { username: user!.email.split("@")[0] + user!.uid, email: user!.email, password: user!.googleId }, }, }, { headers: { Authorization: "Token hdebsyxiigyklxgsqivyswwiisohzlnezzzzzzzz;userId=1" } } ); } auth() { const user = this.userService.getCurrentUser(); return this.http.post("forum/api/token", { identification: user!.email, password: user!.googleId, remember: "1" }); } } ================================================ FILE: frontend/src/app/dashboard/service/user/project/stub-user-project.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Observable } from "rxjs"; import { DashboardProject } from "../../../type/dashboard-project.interface"; import { DashboardWorkflow } from "../../../type/dashboard-workflow.interface"; import { DashboardFile } from "../../../type/dashboard-file.interface"; import { UserProjectService } from "./user-project.service"; import { testUserProjects } from "../../../component/user-dashboard-test-fixtures"; export class StubUserProjectService { public getProjectList(): Observable { return new Observable(observer => observer.next(testUserProjects.slice())); } public retrieveWorkflowsOfProject(pid: number): Observable { throw new Error("Not implemented."); } public retrieveFilesOfProject(pid: number): Observable { throw new Error("Not implemented."); } public getProjectFiles(): ReadonlyArray { throw new Error("Not implemented."); } public refreshFilesOfProject(pid: number): void { throw new Error("Not implemented."); } public retrieveProject(pid: number): Observable { throw new Error("Not implemented."); } public updateProjectName(pid: number, name: string): Observable { throw new Error("Not implemented."); } public updateProjectDescription(pid: number, description: string): Observable { throw new Error("Not implemented."); } public deleteProject(pid: number): Observable { throw new Error("Not implemented."); } public createProject(name: string): Observable { throw new Error("Not implemented."); } public addWorkflowToProject(pid: number, wid: number): Observable { throw new Error("Not implemented."); } public removeWorkflowFromProject(pid: number, wid: number): Observable { throw new Error("Not implemented."); } public addFileToProject(pid: number, fid: number): Observable { throw new Error("Not implemented."); } public updateProjectColor(pid: number, colorHex: string): Observable { throw new Error("Not implemented."); } public deleteProjectColor(pid: number): Observable { throw new Error("Not implemented."); } public removeFileFromProject(pid: number, fid: number): Observable { throw new Error("Not implemented."); } /** * same as UserFileService"s deleteDashboardUserFileEntry method, except * it is modified to refresh the project"s list of files */ public deleteDashboardUserFileEntry(pid: number, targetUserFileEntry: DashboardFile): void { throw new Error("Not implemented."); } /** * Helper function to determine if a project color is light * or dark, which can be helpful for styling decisions * * @param color (HEX formatted color string) * @returns boolean indicating whether color is "light" or "dark" */ public isLightColor(color: string): boolean { return UserProjectService.isLightColor(color); } /** * Helper function to validate if a project color is in HEX format * * @param color * @returns boolean indicating whether color is in valid HEX format */ public isInvalidColorFormat(color: string): boolean { return UserProjectService.isInvalidColorFormat(color); } } ================================================ FILE: frontend/src/app/dashboard/service/user/project/user-project.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable } from "rxjs"; import { AppSettings } from "../../../../common/app-setting"; import { DashboardWorkflow } from "../../../type/dashboard-workflow.interface"; import { DashboardFile } from "../../../type/dashboard-file.interface"; import { DashboardProject } from "../../../type/dashboard-project.interface"; import { NotificationService } from "../../../../common/service/notification/notification.service"; export const USER_PROJECT_BASE_URL = `${AppSettings.getApiEndpoint()}/project`; export const USER_PROJECT_LIST_URL = `${USER_PROJECT_BASE_URL}/list`; export const DELETE_PROJECT_URL = `${USER_PROJECT_BASE_URL}/delete`; export const CREATE_PROJECT_URL = `${USER_PROJECT_BASE_URL}/create`; export const USER_FILE_BASE_URL = `${AppSettings.getApiEndpoint()}/user/file`; export const USER_FILE_DELETE_URL = `${USER_FILE_BASE_URL}/delete`; @Injectable({ providedIn: "root", }) export class UserProjectService { private files: ReadonlyArray = []; constructor( private http: HttpClient, private notificationService: NotificationService ) {} public getProjectList(): Observable { return this.http.get(`${USER_PROJECT_LIST_URL}`); } public retrieveWorkflowsOfProject(pid: number): Observable { return this.http.get(`${USER_PROJECT_BASE_URL}/${pid}/workflows`); } public retrieveFilesOfProject(pid: number): Observable { return this.http.get(`${USER_PROJECT_BASE_URL}/${pid}/files`); } public getProjectFiles(): ReadonlyArray { return this.files; } public refreshFilesOfProject(pid: number): void { this.retrieveFilesOfProject(pid).subscribe(files => { this.files = files; }); } public retrieveProject(pid: number): Observable { return this.http.get(`${USER_PROJECT_BASE_URL}/${pid}`); } public updateProjectName(pid: number, name: string): Observable { return this.http.post(`${USER_PROJECT_BASE_URL}/${pid}/rename/${name}`, {}); } public updateProjectDescription(pid: number, description: string): Observable { return this.http.post(`${USER_PROJECT_BASE_URL}/${pid}/update/description`, `${description}`); } public deleteProject(pid: number): Observable { return this.http.delete(`${DELETE_PROJECT_URL}/` + pid); } public createProject(name: string): Observable { return this.http.post(`${CREATE_PROJECT_URL}/` + name, {}); } public addWorkflowToProject(pid: number, wid: number): Observable { return this.http.post(`${USER_PROJECT_BASE_URL}/${pid}/workflow/${wid}/add`, {}); } public removeWorkflowFromProject(pid: number, wid: number): Observable { return this.http.delete(`${USER_PROJECT_BASE_URL}/${pid}/workflow/${wid}/delete`, {}); } public addFileToProject(pid: number, fid: number): Observable { return this.http.post(`${USER_PROJECT_BASE_URL}/${pid}/user-file/${fid}/add`, {}); } public updateProjectColor(pid: number, colorHex: string): Observable { return this.http.post(`${USER_PROJECT_BASE_URL}/${pid}/color/${colorHex}/add`, {}); } public deleteProjectColor(pid: number): Observable { return this.http.post(`${USER_PROJECT_BASE_URL}/${pid}/color/delete`, {}); } public removeFileFromProject(pid: number, fid: number): Observable { return this.http.delete(`${USER_PROJECT_BASE_URL}/${pid}/user-file/${fid}/delete`, {}); } /** * same as UserFileService"s deleteDashboardUserFileEntry method, except * it is modified to refresh the project"s list of files */ public deleteDashboardUserFileEntry(pid: number, targetUserFileEntry: DashboardFile): void { this.http .delete(`${USER_FILE_DELETE_URL}/${targetUserFileEntry.file.name}/${targetUserFileEntry.ownerEmail}`) .subscribe({ next: () => { this.refreshFilesOfProject(pid); // refresh files within project }, // @ts-ignore // TODO: fix this with notification component error: (err: unknown) => alert("Cannot delete the file entry: " + err.error), }); } /** * Helper function to determine if a project color is light * or dark, which can be helpful for styling decisions * * @param color (HEX formatted color string) * @returns boolean indicating whether color is "light" or "dark" */ public static isLightColor(color: string): boolean { if (this.isInvalidColorFormat(color)) { return false; // default color is dark } // ensure format is in 6 digit HEX if (color.length == 3) { color = color .split("") .map(s => s + s) .join(""); } // convert to RGB form let colorRGB: number = +("0x" + color); let r: number = colorRGB >> 16; let g: number = (colorRGB >> 8) & 255; let b: number = colorRGB & 255; // estimate HSV value let hsv: number = Math.sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b)); return hsv > 200; } /** * Helper function to validate if a project color is in HEX format * * @param color * @returns boolean indicating whether color is in valid HEX format */ public static isInvalidColorFormat(color: string) { return color == null || (color.length != 6 && color.length != 3) || !/^([0-9A-Fa-f]{3}){1,2}$/.test(color); } } ================================================ FILE: frontend/src/app/dashboard/service/user/public-project/public-project.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable } from "rxjs"; import { AppSettings } from "../../../../common/app-setting"; import { PublicProject } from "../../../type/dashboard-project.interface"; export const USER_BASE_URL = `${AppSettings.getApiEndpoint()}/public/project`; @Injectable({ providedIn: "root", }) export class PublicProjectService { constructor(private http: HttpClient) {} public getType(pid: number): Observable { return this.http.get(`${USER_BASE_URL}/type/${pid}`, { responseType: "text" }); } public makePublic(pid: number): Observable { return this.http.put(`${USER_BASE_URL}/public/${pid}`, null); } public makePrivate(pid: number): Observable { return this.http.put(`${USER_BASE_URL}/private/${pid}`, null); } public getPublicProjects(): Observable { return this.http.get(`${USER_BASE_URL}/list`); } public addPublicProjects(CheckedId: number[]): Observable { return this.http.put(`${USER_BASE_URL}/add`, CheckedId); } } ================================================ FILE: frontend/src/app/dashboard/service/user/quota/user-quota.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable } from "rxjs"; import { AppSettings } from "../../../../common/app-setting"; import { ExecutionQuota, Workflow } from "../../../../common/type/user"; import { DatasetQuota } from "src/app/dashboard/type/quota-statistic.interface"; export const USER_BASE_URL = `${AppSettings.getApiEndpoint()}/quota`; export const USER_CREATED_DATASETS = `${USER_BASE_URL}/created_datasets`; export const USER_CREATED_WORKFLOWS = `${USER_BASE_URL}/created_workflows`; export const USER_ACCESS_WORKFLOWS = `${USER_BASE_URL}/access_workflows`; export const USER_QUOTA_SIZE = `${USER_BASE_URL}/user_quota_size`; export const USER_DELETE_EXECUTION_COLLECTION = `${USER_BASE_URL}/deleteCollection`; @Injectable({ providedIn: "root", }) export class UserQuotaService { constructor(private http: HttpClient) {} public getCreatedDatasets(uid: number): Observable> { return this.http.get>(`${USER_CREATED_DATASETS}`); } public getCreatedWorkflows(uid: number): Observable> { return this.http.get>(`${USER_CREATED_WORKFLOWS}`); } public getAccessWorkflows(uid: number): Observable> { return this.http.get>(`${USER_ACCESS_WORKFLOWS}`); } public getExecutionQuota(uid: number): Observable> { return this.http.get>(`${USER_QUOTA_SIZE}`); } public deleteExecutionCollection(eid: number): Observable { return this.http.delete(`${USER_DELETE_EXECUTION_COLLECTION}/${eid.toString()}`); } } ================================================ FILE: frontend/src/app/dashboard/service/user/search.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { forkJoin, Observable, of } from "rxjs"; import { SearchResult, SearchResultBatch, SearchResultItem } from "../../type/search-result"; import { AppSettings } from "../../../common/app-setting"; import { SearchFilterParameters, toQueryStrings } from "../../type/search-filter-parameters"; import { SortMethod } from "../../type/sort-method"; import { DashboardEntry, UserInfo } from "../../type/dashboard-entry"; import { AccessResponse, ActionType, CountResponse, EntityType, HubService, LikedStatus, } from "../../../hub/service/hub.service"; import { map, switchMap } from "rxjs/operators"; import { WorkflowPersistService } from "../../../common/service/workflow-persist/workflow-persist.service"; const DASHBOARD_SEARCH_URL = "dashboard/search"; const DASHBOARD_PUBLIC_SEARCH_URL = "dashboard/publicSearch"; const DASHBOARD_USER_INFO_URL = "dashboard/resultsOwnersInfo"; export type EnrichActivity = "counts" | "liked" | "access" | "size"; @Injectable({ providedIn: "root", }) export class SearchService { constructor( private http: HttpClient, private hubService: HubService, private workflowPersistService: WorkflowPersistService ) {} /** * Retrieves a workflow or other resource from the backend database given the specified search parameters. * The user in the session must have access to the workflow or resource unless the search is public. * * @param keywords - Array of search keywords. * @param params - Additional search filter parameters. * @param start - The starting index for paginated results. * @param count - The number of results to retrieve. * @param type - The type of resource to search for ("workflow", "project", "dataset", "file", or null (all resource type)). * @param orderBy - Specifies the sorting method. * @param isLogin - Indicates if the user is logged in. * - `isLogin = true`: Use the authenticated search endpoint, retrieving both user-accessible and public resources based on `includePublic`. * - `isLogin = false`: Use the public search endpoint, limited to public resources only. * @param includePublic - Specifies whether to include public resources in the search results. * - If `isLogin` is `true`, `includePublic` controls whether public resources are included alongside user-accessible ones. * - If `isLogin` is `false`, this parameter defaults to `true` to ensure only public resources are fetched. */ public search( keywords: string[], params: SearchFilterParameters, start: number, count: number, type: "workflow" | "project" | "file" | "dataset" | null, orderBy: SortMethod, isLogin: boolean, includePublic: boolean = false ): Observable { const url = isLogin ? `${AppSettings.getApiEndpoint()}/${DASHBOARD_SEARCH_URL}` : `${AppSettings.getApiEndpoint()}/${DASHBOARD_PUBLIC_SEARCH_URL}`; const finalIncludePublic = isLogin ? includePublic : true; return this.http.get( `${url}?${toQueryStrings(keywords, params, start, count, type, orderBy)}&includePublic=${finalIncludePublic}` ); } public getUserInfo(userIds: number[]): Observable<{ [key: number]: UserInfo }> { const queryString = userIds.map(id => `userIds=${encodeURIComponent(id)}`).join("&"); return this.http.get<{ [key: number]: UserInfo }>( `${AppSettings.getApiEndpoint()}/${DASHBOARD_USER_INFO_URL}?${queryString}` ); } /** * Executes a search query and returns an observable stream of enriched dashboard entries. * * This method: * - Dispatches a paginated search request (authenticated or public) via `this.search(...)`. * - Filters out null or mismatched datasets when `type === 'dataset'` and sets `hasMismatch`. * - Fetches owner information (name, Google avatar) in batch for workflows, projects, and datasets. * - Aggregates view/clone/like counts via the batch counts API. * - Constructs `DashboardEntry` instances and attaches owner info and counts. * * @param keywords Array of search keywords. * @param params Additional search filter parameters. * @param start The starting index for paginated results. * @param count The number of results to retrieve. * @param type The type of resource to search for ("workflow", "project", "dataset", "file", or null (all resource type)). * @param orderBy Specifies the sorting method. * @param isLogin Indicates if the user is logged in. * @param includePublic Specifies whether to include public resources in the search results. * * @returns An `Observable` that emits exactly one value containing: * - `entries`: the array of fully populated `DashboardEntry` objects, * - `more`: whether additional pages are available, * - `hasMismatch` (for datasets): true if any dataset entries were dropped due to mismatch. */ public executeSearch( keywords: string[], params: SearchFilterParameters, start: number, count: number, type: "workflow" | "project" | "dataset" | "file" | null, orderBy: SortMethod, isLogin: boolean, includePublic: boolean ): Observable { return this.search(keywords, params, start, count, type, orderBy, isLogin, includePublic).pipe( switchMap(results => { const hasMismatch = type === "dataset" ? results.hasMismatch ?? false : undefined; const filteredResults = type === "dataset" ? results.results.filter(i => i !== null && i.dataset != null) : results.results; return this.extendSearchResultsWithHubActivityInfo(filteredResults, isLogin).pipe( map(entries => ({ entries, more: results.more, hasMismatch, })) ); }) ); } /** * Enriches an array of SearchResultItem into DashboardEntry instances. * * @param items The SearchResultItem[] to enrich. * @param isLogin Whether the current user is authenticated. * @param activities Which activities to perform: 'counts', 'liked', 'access'. * Defaults to all three if omitted or empty. * @returns Observable that emits the fully populated DashboardEntry[]. */ public extendSearchResultsWithHubActivityInfo( items: SearchResultItem[], isLogin: boolean, activities: EnrichActivity[] = [] ): Observable { const acts = activities.length > 0 ? activities : (["counts", "liked", "access", "size"] as EnrichActivity[]); const doCounts = acts.includes("counts"); const doLiked = acts.includes("liked") && isLogin; const doAccess = acts.includes("access"); const doSize = acts.includes("size"); const userIds = new Set(); items.forEach(i => { if (i.project) userIds.add(i.project.ownerId); else if (i.workflow) userIds.add(i.workflow.ownerId); else if (i.dataset?.dataset?.ownerUid != null) userIds.add(i.dataset.dataset.ownerUid); }); const userInfo$ = userIds.size ? this.getUserInfo(Array.from(userIds)) : of({} as Record); const entityTypes: EntityType[] = []; const entityIds: number[] = []; items.forEach(i => { if (i.workflow?.workflow?.wid != null) { entityTypes.push(EntityType.Workflow); entityIds.push(i.workflow.workflow.wid); } else if (i.project) { entityTypes.push(EntityType.Project); entityIds.push(i.project.pid); } else if (i.dataset?.dataset?.did != null) { entityTypes.push(EntityType.Dataset); entityIds.push(i.dataset.dataset.did); } }); const counts$ = doCounts && entityTypes.length > 0 ? this.hubService.getCounts(entityTypes, entityIds) : of([] as CountResponse[]); const liked$ = doLiked && entityTypes.length > 0 ? this.hubService.isLiked(entityIds, entityTypes) : of([] as LikedStatus[]); const access$ = doAccess && entityTypes.length > 0 ? this.hubService.getUserAccess(entityTypes, entityIds) : of([] as AccessResponse[]); const workflowIds = items.map(i => i.workflow?.workflow?.wid).filter((wid): wid is number => wid != null); const sizes$ = doSize && workflowIds.length > 0 ? this.workflowPersistService.getSizes(workflowIds) : of({} as Record); return forkJoin([userInfo$, counts$, liked$, access$, sizes$]).pipe( map(([userMap, counts, liked, access, sizesMap]) => { const countsMap: Record>> = {}; counts.forEach(r => (countsMap[`${r.entityType}:${r.entityId}`] = r.counts)); const likedMap: Record = {}; liked.forEach(r => (likedMap[`${r.entityType}:${r.entityId}`] = r.isLiked)); const accessMap: Record = {}; access.forEach(r => (accessMap[`${r.entityType}:${r.entityId}`] = r.userIds)); return items.map(i => { const entry = i.workflow ? new DashboardEntry(i.workflow) : i.project ? new DashboardEntry(i.project) : new DashboardEntry(i.dataset!); const key = `${entry.type}:${entry.id}`; const ownerId = i.workflow ? i.workflow.ownerId : i.project ? i.project.ownerId : i.dataset!.dataset!.ownerUid!; const ui = (userMap as any)[ownerId]; if (ui) { entry.setOwnerName(ui.userName); entry.setOwnerGoogleAvatar(ui.googleAvatar ?? ""); } if (doCounts) { const c = countsMap[key] ?? {}; entry.setCount(c.view ?? 0, c.clone ?? 0, c.like ?? 0); } if (doLiked) { entry.setIsLiked(likedMap[key] ?? false); } if (doAccess) { entry.setAccessUsers(accessMap[key] ?? []); } if (doSize && entry.type === EntityType.Workflow && entry.id != null) { entry.setSize(sizesMap[entry.id] ?? 0); } return entry; }); }) ); } } ================================================ FILE: frontend/src/app/dashboard/service/user/share-access/share-access.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable } from "rxjs"; import { AppSettings } from "../../../../common/app-setting"; import { ShareAccess } from "../../../type/share-access.interface"; export const BASE = `${AppSettings.getApiEndpoint()}/access`; @Injectable({ providedIn: "root", }) export class ShareAccessService { constructor(private http: HttpClient) {} public grantAccess(type: string, id: number, email: string, privilege: string): Observable { return this.http.put(`${BASE}/${type}/grant/${id}/${email}/${privilege}`, null); } public revokeAccess(type: string, id: number, username: string): Observable { return this.http.delete(`${BASE}/${type}/revoke/${id}/${username}`); } public getOwner(type: string, id: number): Observable { return this.http.get(`${BASE}/${type}/owner/${id}`, { responseType: "text" }); } public getAccessList(type: string, id: number | undefined): Observable> { return this.http.get>(`${BASE}/${type}/list/${id}`); } } ================================================ FILE: frontend/src/app/dashboard/service/user/stub-search.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { Observable, of } from "rxjs"; import { SearchResult } from "../../type/search-result"; import { SearchFilterParameters, searchTestEntries } from "../../type/search-filter-parameters"; import { DashboardEntry, UserInfo } from "../../type/dashboard-entry"; import { SortMethod } from "../../type/sort-method"; import { map } from "rxjs/operators"; @Injectable({ providedIn: "root", }) export class StubSearchService { constructor( private testEntries: DashboardEntry[], private mockUserInfo: { [key: number]: UserInfo } ) {} /** * retrieves a workflow from backend database given its id. The user in the session must have access to the workflow. * @param wid, the workflow id. */ public search( keywords: string[], params: SearchFilterParameters, start: number, count: number, type: "workflow" | "project" | "file" | "dataset" | null, orderBy: SortMethod, isLogin: boolean = true, includePublic: boolean = false ): Observable { // Igoring start count and orderBy as they are not tested in the unit tests. return new Observable(observer => { observer.next({ results: searchTestEntries(keywords, params, this.testEntries, type).map(i => ({ resourceType: i.type, workflow: i.type === "workflow" ? i.workflow : undefined, project: i.type === "project" ? i.project : undefined, })), more: false, }); }); } public getUserInfo(userIds: number[]): Observable<{ [key: number]: UserInfo }> { const result = userIds.reduce( (acc, id) => { if (this.mockUserInfo[id]) { acc[id] = this.mockUserInfo[id]; } return acc; }, {} as { [key: number]: UserInfo } ); return of(result); } public executeSearch( keywords: string[], params: SearchFilterParameters, start: number, count: number, type: "workflow" | "project" | "dataset" | "file" | null, orderBy: SortMethod, isLogin: boolean, includePublic: boolean ): Observable<{ entries: DashboardEntry[]; more: boolean; hasMismatch?: boolean }> { return this.search(keywords, params, start, count, type, orderBy, isLogin, includePublic).pipe( map(result => { const hasMismatch = type === "dataset" ? result.hasMismatch ?? false : undefined; const filteredResults = type === "dataset" ? result.results.filter(i => i.dataset != null) : result.results; const entries: DashboardEntry[] = filteredResults.map( i => this.testEntries.find(e => { if (i.workflow && e.type === "workflow" && e.workflow === i.workflow) return true; if (i.project && e.type === "project" && e.project === i.project) return true; if (i.dataset && e.type === "dataset" && e.dataset === i.dataset) return true; return false; })! ); entries.forEach(entry => { if (!entry.ownerId) { return; } const info = this.mockUserInfo[entry.ownerId]; if (info) { entry.setOwnerName(info.userName); entry.setOwnerGoogleAvatar(info.googleAvatar ?? ""); } }); return { entries, more: false, hasMismatch }; }) ); } } ================================================ FILE: frontend/src/app/dashboard/service/user/workflow-executions/workflow-executions.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ describe("WorkflowExecutionsService", () => { // This spec was created without test bodies. The placeholder below keeps // Vitest's discovery happy so the file compiles cleanly; real tests for // WorkflowExecutionsService are tracked in #4861. it.todo("add unit tests for WorkflowExecutionsService"); }); ================================================ FILE: frontend/src/app/dashboard/service/user/workflow-executions/workflow-executions.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { Observable } from "rxjs"; import { AppSettings } from "../../../../common/app-setting"; import { HttpClient, HttpParams } from "@angular/common/http"; import { WorkflowExecutionsEntry } from "../../../type/workflow-executions-entry"; import { WorkflowRuntimeStatistics } from "../../../type/workflow-runtime-statistics"; import { ExecutionState } from "../../../../workspace/types/execute-workflow.interface"; export const WORKFLOW_EXECUTIONS_API_BASE_URL = `${AppSettings.getApiEndpoint()}/executions`; @Injectable({ providedIn: "root", }) export class WorkflowExecutionsService { constructor(private http: HttpClient) {} /** * Retrieves the latest execution entry (latest VID, latest start-time) * for the given workflow ID. */ retrieveLatestWorkflowExecution(wid: number): Observable { return this.http.get(`${WORKFLOW_EXECUTIONS_API_BASE_URL}/${wid}/latest`); } /** * retrieves a list of executions for a particular workflow from the back-end * database. * * @param wid workflow ID * @param statuses optional list of status strings * (e.g. ["running", "completed"]). If the array is empty or * omitted, no status filter is applied. */ retrieveWorkflowExecutions(wid: number, statuses?: ExecutionState[]): Observable { /* -------------------------------------------------------------------- */ /* build query-string ?status=running,completed … */ /* -------------------------------------------------------------------- */ let params = new HttpParams(); if (statuses && statuses.length > 0) { params = params.set("status", statuses.join(",")); } return this.http.get(`${WORKFLOW_EXECUTIONS_API_BASE_URL}/${wid}`, { params }); } groupSetIsBookmarked(wid: number, eIds: number[], isBookmarked: boolean): Observable { return this.http.put(`${WORKFLOW_EXECUTIONS_API_BASE_URL}/set_execution_bookmarks`, { wid, eIds, isBookmarked, }); } groupDeleteWorkflowExecutions(wid: number, eIds: number[]): Observable { return this.http.put(`${WORKFLOW_EXECUTIONS_API_BASE_URL}/delete_executions`, { wid, eIds, }); } updateWorkflowExecutionsName(wid: number | undefined, eId: number, executionName: string): Observable { return this.http.post(`${WORKFLOW_EXECUTIONS_API_BASE_URL}/update_execution_name`, { wid, eId, executionName, }); } retrieveWorkflowRuntimeStatistics(wid: number, eId: number, cuid: number): Observable { const params = new HttpParams().set("cuid", cuid.toString()); return this.http.get(`${WORKFLOW_EXECUTIONS_API_BASE_URL}/${wid}/stats/${eId}`, { params, }); } } ================================================ FILE: frontend/src/app/dashboard/service/user/workflow-snapshot/workflow-snapshot.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { Observable } from "rxjs"; import { AppSettings } from "../../../../common/app-setting"; import { HttpClient } from "@angular/common/http"; import html2canvas from "html2canvas"; import { WorkflowSnapshotEntry } from "../../../type/workflow-snapshot-entry"; export const WORKFLOW_SNAPSHOT_API_BASE_URL = `${AppSettings.getApiEndpoint()}/snapshot`; export const WORKFLOW_SNAPSHOT_UPLOAD_URL = `${WORKFLOW_SNAPSHOT_API_BASE_URL}/upload`; @Injectable({ providedIn: "root", }) export class WorkflowSnapshotService { constructor(private http: HttpClient) {} /** * create canvas for snapshot */ public createSnapShotCanvas( heightRatio: number, yRatio: number, widthRatio: number, xRatio: number ): Promise { let doc = document.getElementById("texera-workflow-editor") || document.body; const { height, width } = doc.getBoundingClientRect(); return html2canvas(doc, { allowTaint: true, useCORS: true, backgroundColor: "transparent", height: height * heightRatio, y: height * yRatio, width: width * widthRatio, x: width * xRatio, }); } /** * store snapshot into sql */ public uploadWorkflowSnapshot(snapshotBlob: Blob, wid: number | undefined): Observable { const formData: FormData = new FormData(); formData.append("wid", wid?.toString() || ""); formData.append("SnapshotBlob", snapshotBlob); return this.http.put(`${WORKFLOW_SNAPSHOT_UPLOAD_URL}`, formData); } /** * retrieve the snapshot */ public retrieveWorkflowSnapshot(sid: number): Observable { return this.http.get(`${WORKFLOW_SNAPSHOT_API_BASE_URL}/${sid}`); } } ================================================ FILE: frontend/src/app/dashboard/service/user/workflow-version/workflow-version.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { TestBed } from "@angular/core/testing"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { WorkflowVersionService } from "./workflow-version.service"; import { WorkflowActionService } from "src/app/workspace/service/workflow-graph/model/workflow-action.service"; import { OperatorMetadataService } from "src/app/workspace/service/operator-metadata/operator-metadata.service"; import { commonTestProviders } from "../../../../common/testing/test-utils"; describe("WorkflowVersionService", () => { let service: WorkflowVersionService; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [WorkflowVersionService, WorkflowActionService, OperatorMetadataService, ...commonTestProviders], }); service = TestBed.inject(WorkflowVersionService); }); describe("canRestoreVersion", () => { it("should return true when modificationEnabledBeforeTempWorkflow is true", () => { // Arrange service["modificationEnabledBeforeTempWorkflow"] = true; // Act const result = service.canRestoreVersion; // Assert expect(result).toBe(true); }); it("should return false when modificationEnabledBeforeTempWorkflow is undefined", () => { // Arrange service["modificationEnabledBeforeTempWorkflow"] = undefined; // Act const result = service.canRestoreVersion; // Assert expect(result).toBe(false); }); }); }); ================================================ FILE: frontend/src/app/dashboard/service/user/workflow-version/workflow-version.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { BehaviorSubject, Observable } from "rxjs"; import { WorkflowActionService } from "../../../../workspace/service/workflow-graph/model/workflow-action.service"; import { Workflow, WorkflowContent } from "../../../../common/type/workflow"; import { WorkflowPersistService } from "../../../../common/service/workflow-persist/workflow-persist.service"; import { UndoRedoService } from "../../../../workspace/service/undo-redo/undo-redo.service"; import { isEqual } from "lodash"; import { OperatorLink, OperatorPredicate, Point } from "../../../../workspace/types/workflow-common.interface"; import { WorkflowVersionEntry } from "../../../type/workflow-version-entry"; import { AppSettings } from "../../../../common/app-setting"; import { filter, map } from "rxjs/operators"; import { WorkflowUtilService } from "../../../../workspace/service/workflow-graph/util/workflow-util.service"; import { HttpClient } from "@angular/common/http"; export const WORKFLOW_VERSIONS_API_BASE_URL = "version"; type WorkflowContentKeys = keyof WorkflowContent; type Element = OperatorLink | OperatorPredicate | Point; type DifferentOpIDsList = { [key in "modified" | "added" | "deleted"]: string[]; }; // only element types specified in this list are consider when calculating the difference between workflows const ELEMENT_TYPES_IN_WORKFLOW_DIFF_CALC: Partial[] = ["operators"]; // it maps a name of the element type to its ID field name used in WorkflowContent const ID_FILED_FOR_ELEMENTS_CONFIG: { [key: string]: string } = { operators: "operatorID", commentBoxes: "commentBoxID", }; @Injectable({ providedIn: "root", }) export class WorkflowVersionService { private modificationEnabledBeforeTempWorkflow: boolean | undefined; public operatorPropertyDiff: { [key: string]: Map } = {}; private displayParticularWorkflowVersion = new BehaviorSubject(false); private differentOpIDsList: DifferentOpIDsList = { modified: [], added: [], deleted: [] }; public selectedVersionId = new BehaviorSubject(null); public selectedDisplayedVersionId = new BehaviorSubject(null); constructor( private workflowActionService: WorkflowActionService, private workflowPersistService: WorkflowPersistService, private undoRedoService: UndoRedoService, private http: HttpClient ) {} public displayWorkflowVersions(): void { // unhighlight all the current highlighted operators and links const elements = this.workflowActionService.getJointGraphWrapper().getCurrentHighlights(); this.workflowActionService.getJointGraphWrapper().unhighlightElements(elements); } public setDisplayParticularVersion(flag: boolean, versionId?: number, displayedVersionId?: number): void { if (flag) { if (versionId != undefined) { this.selectedVersionId.next(versionId); } if (displayedVersionId !== undefined) { this.selectedDisplayedVersionId.next(displayedVersionId); } } else { this.selectedVersionId.next(null); this.selectedDisplayedVersionId.next(null); } this.displayParticularWorkflowVersion.next(flag); } public getDisplayParticularVersionStream(): Observable { return this.displayParticularWorkflowVersion.asObservable(); } public get canRestoreVersion(): boolean { return this.modificationEnabledBeforeTempWorkflow !== undefined && this.modificationEnabledBeforeTempWorkflow; } public displayReadonlyWorkflow(workflow: Workflow) { this.saveModificationState(); // we need to display the version on the paper but keep the original workflow in the background this.workflowActionService.setTempWorkflow(this.workflowActionService.getWorkflow()); // disable persist to DB because it is read only this.workflowPersistService.setWorkflowPersistFlag(false); // disable the undoredo service because reloading the workflow is considered an action this.undoRedoService.disableWorkFlowModification(); // reload the read only workflow version on the paper this.workflowActionService.reloadWorkflow(workflow); // disable modifications because it is read only this.workflowActionService.disableWorkflowModification(); } public displayParticularVersion(workflow: Workflow, vid: number, displayedVId: number) { // get the list of IDs of different elements when comparing displaying to the editing version this.differentOpIDsList = this.getWorkflowsDifference( this.workflowActionService.getWorkflowContent(), workflow.content ); this.displayReadonlyWorkflow(workflow); this.setDisplayParticularVersion(true, vid, displayedVId); // highlight the different elements by changing the color of boundary of the operator // needs a list of ids of elements to be highlighted this.highlightOpVersionDiff(this.differentOpIDsList); } public highlightOpVersionDiff(differentOpIDsList: DifferentOpIDsList) { differentOpIDsList.modified.map(id => this.highlightOpBoundary(id, "255,118,20,0.5")); differentOpIDsList.added.map(id => this.highlightOpBoundary(id, "0,255,0,0.5")); if (differentOpIDsList.deleted.length > 0) { const tempWorkflow = this.workflowActionService.getTempWorkflow(); if (tempWorkflow != undefined) { for (const link of tempWorkflow.content.links) { if (differentOpIDsList.deleted.includes(link.source.operatorID) && link.target.operatorID != undefined) { this.highlightOpBracket(link.target.operatorID, "255,0,0,0.5", "left-"); } if (differentOpIDsList.deleted.includes(link.target.operatorID) && link.source.operatorID != undefined) { this.highlightOpBracket(link.source.operatorID, "255,0,0,0.5", "right-"); } } } } } public highlightOpBoundary(id: string, color: string) { this.workflowActionService .getJointGraphWrapper() .getMainJointPaper() ?.getModelById(id) .attr("rect.boundary/fill", "rgba(" + color + ")"); } public highlightOpBracket(id: string, color: string, position: string) { this.workflowActionService .getJointGraphWrapper() .getMainJointPaper() ?.getModelById(id) .attr("path." + position + "boundary/stroke", "rgba(" + color + ")"); } // TODO: the logic of the function will be refined later public getWorkflowsDifference(workflowContent1: WorkflowContent, workflowContent2: WorkflowContent) { let eleType; this.operatorPropertyDiff = {}; const difference: DifferentOpIDsList = { added: [], modified: [], deleted: [] }; // get a list of element types that are changed between versions const eleTypeWithDiffList: WorkflowContentKeys[] = []; for (eleType of ELEMENT_TYPES_IN_WORKFLOW_DIFF_CALC) { if (!isEqual(workflowContent1[eleType], workflowContent2[eleType])) { eleTypeWithDiffList.push(eleType); } } for (eleType of eleTypeWithDiffList) { if (ELEMENT_TYPES_IN_WORKFLOW_DIFF_CALC.includes(eleType)) { let eleID; // return an object with key: ID of the element, value: detailed content of the element let getEleIDMap = function (workflowContent: WorkflowContent, eleType: WorkflowContentKeys) { const elements = workflowContent[eleType] as Element[]; const eleIDtoContentMap: { [key: string]: Element } = {}; for (const element of elements) { eleIDtoContentMap[element[ID_FILED_FOR_ELEMENTS_CONFIG[eleType] as keyof Element]] = element; } return eleIDtoContentMap; }; const eleIDtoContentMap1 = getEleIDMap(workflowContent1, eleType); const eleIDtoContentMap2 = getEleIDMap(workflowContent2, eleType); for (eleID of Object.keys(eleIDtoContentMap2)) { if (!Object.keys(eleIDtoContentMap1).includes(eleID)) { // there is an addition if the element ID exist in historical but not current workflow version difference.added.push(eleID); } else { // there might be a modification if the element ID exist in both historical and current workflow versions if (!isEqual(eleIDtoContentMap1[eleID], eleIDtoContentMap2[eleID])) { // if the contents in two workflow versions are different for the same element ID difference.modified.push(eleID); if (eleType == "operators") { this.operatorPropertyDiff[eleID] = this.getOperatorsDifference( eleIDtoContentMap1[eleID] as OperatorPredicate, eleIDtoContentMap2[eleID] as OperatorPredicate ); } } } } for (eleID of Object.keys(eleIDtoContentMap1)) { if (!Object.keys(eleIDtoContentMap2).includes(eleID)) { // there is a deletion if the element ID exist in current but not historical workflow version difference.deleted.push(eleID); } } } } return difference; } public getOperatorsDifference(operator1: OperatorPredicate, operator2: OperatorPredicate) { const difference: Map = new Map(); for (const property of Object.keys(operator1.operatorProperties)) { if (operator1.operatorProperties[property] != operator2.operatorProperties[property]) { difference.set(property, "outline: 3px solid rgb(255, 118, 20); transition: 0.3s ease-in-out outline;"); } } if (operator1.operatorVersion != operator2.operatorVersion) { difference.set("operatorVersion", "outline: 3px solid rgb(255, 118, 20); transition: 0.3s ease-in-out outline;"); } return difference; } public revertToVersion() { // set all elements to transparent boundary this.unhighlightOpVersionDiff(this.differentOpIDsList); // we need to clear the undo and redo stack because it is a new version from a previous workflow on paper this.undoRedoService.clearRedoStack(); this.undoRedoService.clearUndoStack(); // we need to enable workflow modifications which also automatically enables undoredo service this.workflowActionService.enableWorkflowModification(); // clear the temp workflow this.workflowActionService.resetTempWorkflow(); this.workflowPersistService.setWorkflowPersistFlag(true); this.setDisplayParticularVersion(false); this.restoreModificationState(); } public closeReadonlyWorkflowDisplay() { // should enable modifications first to be able to make action of reloading old version on paper this.workflowActionService.enableWorkflowModification(); // but still disable redo and undo service to not capture swapping the workflows, because enabling modifications // automatically enables undo and redo this.undoRedoService.disableWorkFlowModification(); // reload the old workflow don't persist anything this.workflowActionService.reloadWorkflow(this.workflowActionService.getTempWorkflow()); // clear the temp workflow this.workflowActionService.resetTempWorkflow(); // after reloading the workflow, we can enable the undoredo service this.undoRedoService.enableWorkFlowModification(); this.workflowPersistService.setWorkflowPersistFlag(true); this.restoreModificationState(); } public closeParticularVersionDisplay() { // set all elements to transparent boundary this.unhighlightOpVersionDiff(this.differentOpIDsList); this.closeReadonlyWorkflowDisplay(); this.setDisplayParticularVersion(false); } public unhighlightOpVersionDiff(differentOpIDsList: DifferentOpIDsList) { for (const id of differentOpIDsList.added.concat(differentOpIDsList.modified)) { this.highlightOpBoundary(id, "0,0,0,0"); } this.operatorPropertyDiff = {}; } /** * retrieves a list of versions for a particular workflow from backend database */ retrieveVersionsOfWorkflow(wid: number): Observable { return this.http.get( `${AppSettings.getApiEndpoint()}/${WORKFLOW_VERSIONS_API_BASE_URL}/${wid}` ); } /** * retrieves a version of the workflow from backend database */ retrieveWorkflowByVersion(wid: number, vid: number): Observable { return this.http .get(`${AppSettings.getApiEndpoint()}/${WORKFLOW_VERSIONS_API_BASE_URL}/${wid}/${vid}`) .pipe( filter((updatedWorkflow: Workflow) => updatedWorkflow != null), map(WorkflowUtilService.parseWorkflowInfo) ); } private saveModificationState(): void { if (this.modificationEnabledBeforeTempWorkflow === undefined) { this.modificationEnabledBeforeTempWorkflow = this.workflowActionService.checkWorkflowModificationEnabled(); } } private restoreModificationState(): void { if (this.modificationEnabledBeforeTempWorkflow !== undefined) { if (!this.modificationEnabledBeforeTempWorkflow) { this.workflowActionService.disableWorkflowModification(); } this.modificationEnabledBeforeTempWorkflow = undefined; } } public cloneWorkflowVersion(): Observable { const vid = this.selectedVersionId.getValue(); const displayedVersionId = this.selectedDisplayedVersionId.getValue(); return this.http.post(`${AppSettings.getApiEndpoint()}/${WORKFLOW_VERSIONS_API_BASE_URL}/clone/${vid}`, { displayedVersionId, }); } } ================================================ FILE: frontend/src/app/dashboard/type/dashboard-dataset.interface.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Dataset } from "../../common/type/dataset"; export interface DashboardDataset { isOwner: boolean; ownerEmail: string; dataset: Dataset; accessPrivilege: "READ" | "WRITE" | "NONE"; size: number; } ================================================ FILE: frontend/src/app/dashboard/type/dashboard-entry.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { DashboardFile } from "./dashboard-file.interface"; import { DashboardWorkflow } from "./dashboard-workflow.interface"; import { DashboardProject } from "./dashboard-project.interface"; import { DashboardDataset } from "./dashboard-dataset.interface"; import { DashboardWorkflowComputingUnit } from "../../common/type/workflow-computing-unit"; import { isDashboardDataset, isDashboardFile, isDashboardProject, isDashboardWorkflow, isDashboardWorkflowComputingUnit, } from "./type-predicates"; import { EntityType } from "../../hub/service/hub.service"; export interface UserInfo { userName: string; googleAvatar?: string; } export class DashboardEntry { checked = false; type: EntityType; name: string; creationTime: number | undefined; lastModifiedTime: number | undefined; id: number | undefined; description: string | undefined; accessLevel: string | undefined; ownerName: string | undefined; ownerEmail: string | undefined; ownerGoogleAvatar: string | undefined; ownerId: number | undefined; size: number | undefined; viewCount: number; cloneCount: number; likeCount: number; isLiked: boolean; accessibleUserIds: number[]; coverImageUrl?: string; constructor( public value: | DashboardWorkflow | DashboardProject | DashboardFile | DashboardDataset | DashboardWorkflowComputingUnit ) { if (isDashboardWorkflow(value)) { this.type = EntityType.Workflow; this.id = value.workflow.wid; this.name = value.workflow.name; this.description = value.workflow.description; this.creationTime = value.workflow.creationTime; this.lastModifiedTime = value.workflow.lastModifiedTime; this.accessLevel = value.accessLevel; this.ownerName = value.ownerName; this.ownerEmail = ""; this.ownerGoogleAvatar = ""; this.ownerId = value.ownerId; this.size = 0; this.viewCount = 0; this.cloneCount = 0; this.likeCount = 0; this.isLiked = false; this.accessibleUserIds = []; } else if (isDashboardProject(value)) { this.type = EntityType.Project; this.id = value.pid; this.name = value.name; this.description = ""; this.creationTime = value.creationTime; this.lastModifiedTime = value.creationTime; this.accessLevel = value.accessLevel; this.ownerName = ""; this.ownerEmail = ""; this.ownerGoogleAvatar = ""; this.ownerId = value.ownerId; this.size = 0; this.viewCount = 0; this.cloneCount = 0; this.likeCount = 0; this.isLiked = false; this.accessibleUserIds = []; } else if (isDashboardFile(value)) { this.type = EntityType.File; this.id = value.file.fid; this.name = value.file.name; this.description = value.file.description; this.creationTime = value.file.uploadTime; this.lastModifiedTime = value.file.uploadTime; this.accessLevel = value.accessLevel; this.ownerName = ""; this.ownerEmail = value.ownerEmail; this.ownerGoogleAvatar = ""; this.ownerId = value.file.ownerUid; this.size = value.file.size; this.viewCount = 0; this.cloneCount = 0; this.likeCount = 0; this.isLiked = false; this.accessibleUserIds = []; } else if (isDashboardDataset(value)) { this.type = EntityType.Dataset; this.id = value.dataset.did; this.name = value.dataset.name; this.description = value.dataset.description; this.creationTime = value.dataset.creationTime; this.lastModifiedTime = value.dataset.creationTime; this.accessLevel = value.accessPrivilege; this.ownerName = ""; this.ownerEmail = value.ownerEmail; this.ownerGoogleAvatar = ""; this.ownerId = value.dataset.ownerUid; this.size = value.size; this.viewCount = 0; this.cloneCount = 0; this.likeCount = 0; this.isLiked = false; this.accessibleUserIds = []; this.coverImageUrl = value.dataset.coverImage; } else if (isDashboardWorkflowComputingUnit(value)) { this.type = EntityType.ComputingUnit; this.id = value.computingUnit.cuid; this.name = value.computingUnit.name; this.creationTime = value.computingUnit.creationTime; this.accessLevel = value.accessPrivilege; this.ownerName = ""; this.ownerGoogleAvatar = ""; this.ownerId = value.computingUnit.uid; this.viewCount = 0; this.cloneCount = 0; this.likeCount = 0; this.isLiked = false; this.accessibleUserIds = []; } else { throw new Error("Unexpected type in DashboardEntry."); } } setOwnerName(ownerName: string): void { this.ownerName = ownerName; } setOwnerGoogleAvatar(ownerGoogleAvatar: string): void { this.ownerGoogleAvatar = ownerGoogleAvatar; } setCount(viewCount: number, cloneCount: number, likeCount: number): void { this.viewCount = viewCount; this.cloneCount = cloneCount; this.likeCount = likeCount; } setIsLiked(isLiked: boolean): void { this.isLiked = isLiked; } setAccessUsers(accessUsers: number[]): void { this.accessibleUserIds = accessUsers; } setSize(size: number): void { this.size = size; } get project(): DashboardProject { if (!isDashboardProject(this.value)) { throw new Error("Value is not of type DashboardProject."); } return this.value; } get workflow(): DashboardWorkflow { if (!isDashboardWorkflow(this.value)) { throw new Error("Value is not of type DashboardWorkflow."); } return this.value; } get file(): DashboardFile { if (!isDashboardFile(this.value)) { throw new Error("Value is not of type DashboardFile."); } return this.value; } get dataset(): DashboardDataset { if (!isDashboardDataset(this.value)) { throw new Error("Value is not of type DashboardDataset"); } return this.value; } get computingUnit(): DashboardWorkflowComputingUnit { if (!isDashboardWorkflowComputingUnit(this.value)) { throw new Error("Value is not of type DashboardWorkflowComputingUnit"); } return this.value; } } ================================================ FILE: frontend/src/app/dashboard/type/dashboard-file.interface.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ export interface DashboardFile extends Readonly<{ ownerEmail: string; accessLevel: string; file: UserFile; }> {} /** * This interface stores the information about the users' files. * These information is used to locate the file for the operators. * Corresponds to `src/main/scala/org/apache/texera/web/resource/dashboard/file/UserFileResource.scala` (backend); * and `sql/texera_ddl.sql`, table `file` (database). */ export interface UserFile { ownerUid: number; fid: number; size: number; name: string; path: string; description: string; uploadTime: number; } /** * This interface stores the information about the users' files when uploading. * These information is used to upload the file to the backend. */ export interface FileUploadItem { file: File; name: string; description: string; uploadProgress: number; isUploadingFlag: boolean; restart: boolean; } /** * This enum type helps indicate the method in which DashboardUserFileEntry[] is sorted */ export enum SortMethod { NameAsc, NameDesc, SizeDesc, UploadTimeAsc, UploadTimeDesc, } ================================================ FILE: frontend/src/app/dashboard/type/dashboard-project.interface.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ export interface DashboardProject { pid: number; name: string; description: string; ownerId: number; creationTime: number; color: string | null; accessLevel: string; } export interface PublicProject { pid: number; name: string; owner: string; creationTime: number; } ================================================ FILE: frontend/src/app/dashboard/type/dashboard-workflow.interface.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Workflow } from "../../common/type/workflow"; export interface DashboardWorkflow { isOwner: boolean; ownerName: string | undefined; workflow: Workflow; projectIDs: number[]; accessLevel: string; ownerId: number; } ================================================ FILE: frontend/src/app/dashboard/type/google-api-response.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ export interface Source extends Readonly<{ type: string; id: string; }> {} export interface Metadata extends Readonly<{ primary: boolean; source: Source; }> {} export interface Photo extends Readonly<{ metadata: Metadata; url: string; }> {} export interface GooglePeopleApiResponse extends Readonly<{ resourceName: string; etag: string; photos: Photo[]; }> {} ================================================ FILE: frontend/src/app/dashboard/type/quota-statistic.interface.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ export interface DatasetQuota extends Readonly<{ did: number; name: string; creationTime: number; size: number; }> {} ================================================ FILE: frontend/src/app/dashboard/type/search-filter-parameters.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { DashboardEntry } from "./dashboard-entry"; import { SortMethod } from "./sort-method"; export interface SearchFilterParameters { createDateStart: Date | null; createDateEnd: Date | null; modifiedDateStart: Date | null; modifiedDateEnd: Date | null; owners: string[]; ids: string[]; operators: string[]; projectIds: number[]; } export const toQueryStrings = ( keywords: string[], params: SearchFilterParameters, start?: number, count?: number, type?: "workflow" | "project" | "file" | "dataset" | null, orderBy?: SortMethod ): string => { function* getQueryParameters(): Iterable<[name: string, value: string]> { if (keywords) { for (const keyword of keywords) { yield ["query", keyword.trim()]; } } const createDateStart = params.createDateStart; const modifiedDateStart = params.modifiedDateStart; const createDateEnd = params.createDateEnd; const modifiedDateEnd = params.modifiedDateEnd; if (createDateStart) yield ["createDateStart", createDateStart.toISOString().split("T")[0]]; if (createDateEnd) yield ["createDateEnd", createDateEnd.toISOString().split("T")[0]]; if (modifiedDateStart) yield ["modifiedDateStart", modifiedDateStart.toISOString().split("T")[0]]; if (modifiedDateEnd) yield ["modifiedDateEnd", modifiedDateEnd.toISOString().split("T")[0]]; for (const owner of params.owners) { yield ["owner", owner]; } for (const id of params.ids) { yield ["id", id]; } for (const operator of params.operators) { yield ["operator", operator]; } for (const id of params.projectIds) { yield ["projectId", id.toString()]; } } const concatenateQueryStrings = (queryStrings: ReturnType): string => [ ...queryStrings, ...(start ? [["start", start.toString()]] : []), ...(count ? [["count", count.toString()]] : []), ["resourceType", type ? type.toString() : ""], ...(orderBy != null ? [["orderBy", SortMethod[orderBy]]] : []), ] .filter(q => q[1]) .map(([name, value]) => name + "=" + encodeURIComponent(value)) .join("&"); return concatenateQueryStrings(getQueryParameters()); }; /** JavaScript-based search function used for only unit tests. Actual search is done on the server. */ export const searchTestEntries = ( keywords: string[], params: SearchFilterParameters, testEntries: DashboardEntry[], type: "workflow" | "project" | "file" | "dataset" | null ): DashboardEntry[] => { const endOfDay = (date: Date) => { date.setHours(23); date.setMinutes(59); date.setSeconds(59); date.setMilliseconds(999); return date.getTime(); }; const createDateStart = params.createDateStart; const modifiedDateStart = params.modifiedDateStart; const createDateEnd = params.createDateEnd; const modifiedDateEnd = params.modifiedDateEnd; if (keywords.length > 0) { testEntries = testEntries.filter(e => keywords.some(k => e.name.indexOf(k) !== -1)); } if (createDateStart) { testEntries = testEntries.filter(e => e.creationTime && e.creationTime >= createDateStart.getTime()); } if (createDateEnd) { testEntries = testEntries.filter(e => e.creationTime && e.creationTime <= endOfDay(createDateEnd)); } if (modifiedDateStart) { testEntries = testEntries.filter(e => e.lastModifiedTime && e.lastModifiedTime >= modifiedDateStart.getTime()); } if (modifiedDateEnd) { testEntries = testEntries.filter(e => e.lastModifiedTime && e.lastModifiedTime <= endOfDay(modifiedDateEnd)); } if (params.owners.length > 0) { testEntries = testEntries.filter(e => params.owners.some(o => e.type === "workflow" && e.workflow.ownerName === o)); } if (params.ids.length > 0) { testEntries = testEntries.filter(e => params.ids.some(i => e.type === "workflow" && e.workflow.workflow.wid && e.workflow.workflow.wid.toString() === i) ); } if (params.operators.length > 0) { testEntries = testEntries.filter( e => e.type === "workflow" && e.workflow.workflow.content.operators.some(operator => params.operators.some(operatorTypeFilterBy => operatorTypeFilterBy === operator.operatorType) ) ); } if (params.projectIds.length > 0) { testEntries = testEntries.filter( e => e.type === "workflow" && e.workflow.projectIDs.some(id => params.projectIds.some(projectIdToFilterBy => projectIdToFilterBy == id)) ); } if (type) { testEntries = testEntries.filter(e => e.type === type); } return testEntries; }; ================================================ FILE: frontend/src/app/dashboard/type/search-result.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { DashboardFile } from "./dashboard-file.interface"; import { DashboardWorkflow } from "./dashboard-workflow.interface"; import { DashboardProject } from "./dashboard-project.interface"; import { DashboardDataset } from "./dashboard-dataset.interface"; import { DashboardEntry } from "./dashboard-entry"; export interface SearchResultItem { resourceType: "workflow" | "project" | "file" | "dataset" | "computing-unit"; workflow?: DashboardWorkflow; project?: DashboardProject; file?: DashboardFile; dataset?: DashboardDataset; } export interface SearchResult { results: SearchResultItem[]; more: boolean; hasMismatch?: boolean; // Indicates whether there are mismatched datasets (added for dashboard notification) } export interface SearchResultBatch { entries: DashboardEntry[]; more: boolean; hasMismatch?: boolean; } ================================================ FILE: frontend/src/app/dashboard/type/share-access.interface.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ export enum Privilege { NONE = "NONE", READ = "READ", WRITE = "WRITE", } export interface ShareAccess extends Readonly<{ email: string; name: string; privilege: Privilege; }> {} ================================================ FILE: frontend/src/app/dashboard/type/sort-method.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ export enum SortMethod { NameAsc, NameDesc, CreateTimeDesc, EditTimeDesc, } ================================================ FILE: frontend/src/app/dashboard/type/type-predicates.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { DashboardWorkflow } from "./dashboard-workflow.interface"; import { DashboardProject } from "./dashboard-project.interface"; import { DashboardFile } from "./dashboard-file.interface"; import { DashboardDataset } from "./dashboard-dataset.interface"; import { DashboardWorkflowComputingUnit } from "../../common/type/workflow-computing-unit"; export function isDashboardWorkflow(value: any): value is DashboardWorkflow { return value && typeof value.workflow === "object"; } export function isDashboardProject(value: any): value is DashboardProject { return value && typeof value.name === "string" && !value.workflow; } export function isDashboardFile(value: any): value is DashboardFile { return value && typeof value.ownerEmail === "string" && typeof value.file === "object"; } export function isDashboardDataset(value: any): value is DashboardDataset { return value && typeof value.dataset === "object"; } export function isDashboardWorkflowComputingUnit(value: any): value is DashboardWorkflowComputingUnit { return value && typeof value.computingUnit === "object"; } ================================================ FILE: frontend/src/app/dashboard/type/workflow-executions-entry.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ExecutionState } from "../../workspace/types/execute-workflow.interface"; export interface WorkflowExecutionsEntry { eId: number; vId: number; cuId: number; sId: number; userName: string; googleAvatar: string; name: string; startingTime: number; completionTime: number; status: number; result: string; bookmarked: boolean; logLocation: string; } export const EXECUTION_STATUS_CODE: Record = { 0: ExecutionState.Initializing, 1: ExecutionState.Running, 2: ExecutionState.Paused, 3: ExecutionState.Completed, 4: ExecutionState.Failed, 5: ExecutionState.Killed, }; ================================================ FILE: frontend/src/app/dashboard/type/workflow-metadata.interface.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ export interface WorkflowMetadata { name: string; description: string | undefined; wid: number | undefined; creationTime: number | undefined; lastModifiedTime: number | undefined; isPublished: number; readonly: boolean; } ================================================ FILE: frontend/src/app/dashboard/type/workflow-runtime-statistics.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ export interface WorkflowRuntimeStatistics { [key: string]: any; operatorId: string; timestamp: number; inputTupleCount: number; inputTupleSize: number; outputTupleCount: number; outputTupleSize: number; totalDataProcessingTime: number; totalControlProcessingTime: number; totalIdleTime: number; numberOfWorkers: number; status: number; // Operator status (e.g., RUNNING, COMPLETED, FAILED, etc.) } ================================================ FILE: frontend/src/app/dashboard/type/workflow-snapshot-entry.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ export interface WorkflowSnapshotEntry { sId: number; snapshot: Blob; } ================================================ FILE: frontend/src/app/dashboard/type/workflow-version-entry.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ export interface WorkflowVersionEntry extends Readonly<{ vId: number; creationTime: number; content: string; importance: boolean; }> {} export interface WorkflowVersionCollapsableEntry extends WorkflowVersionEntry { expand: boolean; // for double binding with nzExpand } ================================================ FILE: frontend/src/app/hub/component/about/about.component.html ================================================

    About Texera

    Apache Texera (Incubating) is an open-source platform for human-AI collaborative data science using visual workflows. It enables analysts to construct, execute, and refine data analysis tasks through an intuitive GUI, assisted by AI agents that understand natural-language instructions. Texera is well-suited for a wide range of applications, including “AI for Science,” by making advanced AI and data science capabilities accessible to a broader community. It can run on a laptop for local use or be deployed in the cloud to support scalable processing of large datasets.

    The platform has the following key features:

    • Natural-language data science through AI agents
    • Intuitive GUI-based workflows for data science
    • Real-time collaboration for workflow editing and execution
    • Parallel backend engine for scalable big-data processing
    • Language-agnostic workflow runtime, native support for Python and Java
    • Separation of compute and storage for flexible cloud deployment
    • Runtime debugging and interactive workflow execution
    workflow gui screenshot

    Learn More

    To learn more, please visit https://texera.apache.org/. The source code of the platform is available on GitHub.

    ================================================ FILE: frontend/src/app/hub/component/about/about.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .about-page-container { overflow-y: auto; height: 100%; padding: 0 133px; } .content { margin-top: 40px; max-width: 1600px; width: 100%; text-align: left; } .content > h1 { font-size: 40px; font-weight: bold; margin: 0; } .content > h2 { font-size: 28px; font-weight: bold; margin-top: 10px; } .content > p { font-size: 16px; font-weight: 400; line-height: 1.8; margin-bottom: 10px; } #logo { height: 70px; padding: 0; margin-right: 14px; } .workflow-gui-screenshot { width: 100%; height: auto; } .login-container { position: absolute; top: 130px; right: 0; max-width: 350px; width: 100%; background: white; } .text-content { width: 70%; font-size: 16px; line-height: 1.8; font-weight: 400; } .learn-more-section { margin-top: 10px; text-align: left; p { font-size: 16px; line-height: 1.8; margin-bottom: 10px; a { color: #007acc; text-decoration: none; } a:hover { text-decoration: underline; } img { margin-left: 5px; vertical-align: middle; } } } ================================================ FILE: frontend/src/app/hub/component/about/about.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { RouterTestingModule } from "@angular/router/testing"; import { NzIconModule } from "ng-zorro-antd/icon"; import { UserOutline, LockOutline } from "@ant-design/icons-angular/icons"; import { vi } from "vitest"; import { AboutComponent } from "./about.component"; import { UserService } from "../../../common/service/user/user.service"; import { StubUserService } from "../../../common/service/user/stub-user.service"; import { GuiConfigService } from "../../../common/service/gui-config.service"; import { MockGuiConfigService } from "../../../common/service/gui-config.service.mock"; import { NotificationService } from "../../../common/service/notification/notification.service"; import { commonTestProviders } from "../../../common/testing/test-utils"; describe("AboutComponent", () => { let component: AboutComponent; let fixture: ComponentFixture; let userService: StubUserService; let configService: MockGuiConfigService; function build() { fixture = TestBed.createComponent(AboutComponent); component = fixture.componentInstance; fixture.detectChanges(); } beforeEach(() => { const notificationSpy = { info: vi.fn(), success: vi.fn(), error: vi.fn() }; TestBed.configureTestingModule({ imports: [ AboutComponent, RouterTestingModule.withRoutes([]), // Register the icons used by 's nzPrefixIcon // bindings. jsdom can't fetch icon SVGs over HTTP, so without this // the icon registry emits unhandled errors that fail the run in CI. NzIconModule.forChild([UserOutline, LockOutline]), ], providers: [ { provide: UserService, useClass: StubUserService }, { provide: NotificationService, useValue: notificationSpy }, ...commonTestProviders, ], }); userService = TestBed.inject(UserService) as unknown as StubUserService; configService = TestBed.inject(GuiConfigService) as unknown as MockGuiConfigService; }); it("should create", () => { build(); expect(component).toBeTruthy(); }); it("hides the local login form when the user is already logged in", () => { // StubUserService starts with MOCK_USER, so isLogin() === true. build(); expect(fixture.nativeElement.querySelector("texera-local-login")).toBeNull(); }); it("shows the local login form when logged out and localLogin is enabled", () => { userService.user = undefined; build(); expect(fixture.nativeElement.querySelector("texera-local-login")).not.toBeNull(); }); it("hides the local login form when localLogin is disabled in config", () => { userService.user = undefined; configService.setConfig({ localLogin: false }); build(); expect(fixture.nativeElement.querySelector("texera-local-login")).toBeNull(); }); }); ================================================ FILE: frontend/src/app/hub/component/about/about.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, OnInit } from "@angular/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { UserService } from "src/app/common/service/user/user.service"; import { BehaviorSubject } from "rxjs"; import { GuiConfigService } from "../../../common/service/gui-config.service"; import { NzRowDirective, NzColDirective } from "ng-zorro-antd/grid"; import { NgIf, AsyncPipe } from "@angular/common"; import { LocalLoginComponent } from "./local-login/local-login.component"; @UntilDestroy() @Component({ selector: "texera-about", templateUrl: "./about.component.html", styleUrls: ["./about.component.scss"], imports: [NzRowDirective, NzColDirective, NgIf, LocalLoginComponent, AsyncPipe], }) export class AboutComponent implements OnInit { isLogin$ = new BehaviorSubject(false); // control the visibility of the local login component constructor( private userService: UserService, protected config: GuiConfigService ) {} ngOnInit() { this.isLogin$.next(this.userService.isLogin()); // Subscribe to user changes this.userService .userChanged() .pipe(untilDestroyed(this)) .subscribe(user => { this.isLogin$.next(user !== undefined); }); } } ================================================ FILE: frontend/src/app/hub/component/about/local-login/local-login.component.html ================================================
    Please input your password. Minimal password length is 6. Please confirm your password. Two passwords are inconsistent.
    ================================================ FILE: frontend/src/app/hub/component/about/local-login/local-login.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .login-form-button { width: 100%; } .form { border: 2px solid black; border-radius: 5px; margin: 16px; padding: 20px; ::ng-deep nz-form-item { margin-bottom: 12px; } } ================================================ FILE: frontend/src/app/hub/component/about/local-login/local-login.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, OnInit } from "@angular/core"; import { FormBuilder, FormControl, FormGroup, Validators, FormsModule, ReactiveFormsModule } from "@angular/forms"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { ActivatedRoute, Router } from "@angular/router"; import { UserService } from "../../../../common/service/user/user.service"; import { NotificationService } from "../../../../common/service/notification/notification.service"; import { catchError } from "rxjs/operators"; import { throwError } from "rxjs"; import { DASHBOARD_USER_WORKFLOW } from "../../../../app-routing.constant"; import { GuiConfigService } from "../../../../common/service/gui-config.service"; import { NzTabsComponent, NzTabComponent } from "ng-zorro-antd/tabs"; import { NgIf } from "@angular/common"; import { NzFormDirective, NzFormItemComponent, NzFormControlComponent } from "ng-zorro-antd/form"; import { NzRowDirective, NzColDirective } from "ng-zorro-antd/grid"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzInputGroupComponent, NzInputDirective } from "ng-zorro-antd/input"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; @UntilDestroy() @Component({ selector: "texera-local-login", templateUrl: "./local-login.component.html", styleUrls: ["./local-login.component.scss"], imports: [ NzTabsComponent, NgIf, NzTabComponent, FormsModule, NzFormDirective, ReactiveFormsModule, NzRowDirective, NzFormItemComponent, NzColDirective, NzFormControlComponent, ɵNzTransitionPatchDirective, NzSpaceCompactItemDirective, NzInputGroupComponent, NzInputDirective, NzButtonComponent, NzWaveDirective, ], }) export class LocalLoginComponent implements OnInit { public loginErrorMessage: string | undefined; public registerErrorMessage: string | undefined; public allForms: FormGroup; constructor( private formBuilder: FormBuilder, private userService: UserService, private route: ActivatedRoute, private notificationService: NotificationService, private router: Router, private config: GuiConfigService ) { this.allForms = this.formBuilder.group({ loginUsername: new FormControl("", [Validators.required]), registerUsername: new FormControl("", [Validators.required]), loginPassword: new FormControl("", [Validators.required, Validators.minLength(6)]), registerPassword: new FormControl("", [Validators.required, Validators.minLength(6)]), registerConfirmationPassword: new FormControl("", [Validators.required, this.confirmationValidator]), }); } ngOnInit() { if (this.config.env.defaultLocalUser && Object.keys(this.config.env.defaultLocalUser).length > 0) { this.allForms.patchValue({ loginUsername: this.config.env.defaultLocalUser.username, loginPassword: this.config.env.defaultLocalUser.password, }); } } public updateConfirmValidator(): void { // immediately update validator (asynchronously to wait for value to refresh) setTimeout(() => this.allForms.controls.registerConfirmationPassword.updateValueAndValidity(), 0); } // validator for confirm password in sign up page public confirmationValidator = (control: FormControl): { [s: string]: boolean } => { if (this.allForms && control.value !== this.allForms.controls.registerPassword.value) { return { confirm: true }; } return {}; }; /** * This method responds to the sign-in button * It will send data inside the text entry to the user service to login */ public login(): void { // validate the credentials format this.loginErrorMessage = undefined; const validation = UserService.validateUsername(this.allForms.get("loginUsername")?.value); if (!validation.result) { this.loginErrorMessage = validation.message; return; } const username = this.allForms.get("loginUsername")?.value.trim(); const password = this.allForms.get("loginPassword")?.value; this.userService .login(username, password) .pipe( catchError((e: unknown) => { const errorMessage = (e as Error)?.message || "Incorrect username or password"; this.notificationService.error(errorMessage); return throwError(() => e); }), untilDestroyed(this) ) .subscribe(() => this.router.navigateByUrl(this.route.snapshot.queryParams["returnUrl"] || DASHBOARD_USER_WORKFLOW) ); } /** * This method responds to the sign-up button * It will send data inside the text entry to the user service to register */ public register(): void { // validate the credentials format this.registerErrorMessage = undefined; const registerPassword = this.allForms.get("registerPassword")?.value; const registerConfirmationPassword = this.allForms.get("registerConfirmationPassword")?.value; const registerUsername = this.allForms.get("registerUsername")?.value.trim(); const validation = UserService.validateUsername(registerUsername); if (registerPassword.length < 6) { this.registerErrorMessage = "Password length should be greater than 5"; return; } if (registerPassword !== registerConfirmationPassword) { this.registerErrorMessage = "Passwords do not match"; return; } if (!validation.result) { this.registerErrorMessage = validation.message; return; } // register the credentials with backend this.userService .register(registerUsername, registerPassword) .pipe( catchError((e: unknown) => { const errorMessage = (e as Error)?.message || "Registration failed"; this.notificationService.error(errorMessage); return throwError(() => e); }), untilDestroyed(this) ) .subscribe(() => this.notificationService.success( "Your account has been created. Please contact the Texera administrator to activate your account." ) ); } } ================================================ FILE: frontend/src/app/hub/component/browse-section/browse-section.component.html ================================================

    {{ sectionTitle }}

    {{ entity.name }}

    {{ entity.description || 'No description available' }}

    example
    ================================================ FILE: frontend/src/app/hub/component/browse-section/browse-section.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .results-container { display: flex; flex-direction: column; align-items: flex-start; width: 100%; padding: 20px; .results-title { font-size: 24px; margin-bottom: 20px; text-align: left; width: 100%; padding-left: 10px; } .entity-cards { display: grid; grid-template-columns: repeat(4, 1fr); grid-gap: 10px; width: 100%; max-width: 1350px; .entity-card { margin: 10px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); border-radius: 8px; overflow: hidden; display: flex; flex-direction: column; transition: transform 0.3s ease, box-shadow 0.3s ease; &:hover { transform: translateY(-10px); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); } .card-title { font-size: 18px; margin: 10px 0; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .card-description { padding: 0 15px; font-size: 14px; color: #666; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; text-overflow: ellipsis; } .footer { margin-top: auto; display: flex; justify-content: space-between; padding: 10px 15px; border-top: 1px solid #eee; .footer-text { font-size: 12px; color: #999; } } .cover-container { position: relative; width: 100%; height: 150px; overflow: hidden; display: flex; justify-content: center; align-items: center; img.card-cover-image { width: 100%; height: 100%; object-fit: cover; } .entity-avatar { position: absolute; bottom: 10px; left: 10px; background-color: grey; } } } } } ================================================ FILE: frontend/src/app/hub/component/browse-section/browse-section.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { BrowseSectionComponent } from "./browse-section.component"; import { WorkflowPersistService } from "../../../common/service/workflow-persist/workflow-persist.service"; import { DatasetService } from "../../../dashboard/service/user/dataset/dataset.service"; import { ChangeDetectorRef } from "@angular/core"; import { commonTestProviders } from "../../../common/testing/test-utils"; describe("BrowseSectionComponent", () => { let component: BrowseSectionComponent; let fixture: ComponentFixture; beforeEach(() => { TestBed.configureTestingModule({ imports: [BrowseSectionComponent], providers: [ { provide: WorkflowPersistService, useValue: {} }, { provide: DatasetService, useValue: {} }, { provide: ChangeDetectorRef, useValue: {} }, ...commonTestProviders, ], }); fixture = TestBed.createComponent(BrowseSectionComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/hub/component/browse-section/browse-section.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from "@angular/core"; import { DashboardEntry } from "../../../dashboard/type/dashboard-entry"; import { WorkflowPersistService } from "../../../common/service/workflow-persist/workflow-persist.service"; import { DatasetService } from "../../../dashboard/service/user/dataset/dataset.service"; import { UntilDestroy } from "@ngneat/until-destroy"; import { DASHBOARD_HUB_DATASET_RESULT_DETAIL, DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL, DASHBOARD_USER_DATASET, DASHBOARD_USER_WORKSPACE, } from "../../../app-routing.constant"; import { AppSettings } from "../../../common/app-setting"; import { NgIf, NgFor, NgStyle, DatePipe } from "@angular/common"; import { NzCardComponent } from "ng-zorro-antd/card"; import { RouterLink } from "@angular/router"; import { UserAvatarComponent } from "../../../dashboard/component/user/user-avatar/user-avatar.component"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzAvatarComponent } from "ng-zorro-antd/avatar"; @UntilDestroy() @Component({ selector: "texera-browse-section", templateUrl: "./browse-section.component.html", styleUrls: ["./browse-section.component.scss"], imports: [ NgIf, NgFor, NzCardComponent, RouterLink, UserAvatarComponent, ɵNzTransitionPatchDirective, NzAvatarComponent, NgStyle, DatePipe, ], }) export class BrowseSectionComponent implements OnInit, OnChanges { @Input() entities: DashboardEntry[] = []; @Input() sectionTitle: string = ""; @Input() currentUid: number | undefined; defaultBackground: string = "../../../../../assets/card_background.jpg"; protected readonly DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL = DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL; protected readonly DASHBOARD_USER_WORKSPACE = DASHBOARD_USER_WORKSPACE; protected readonly DASHBOARD_HUB_DATASET_RESULT_DETAIL = DASHBOARD_HUB_DATASET_RESULT_DETAIL; protected readonly DASHBOARD_USER_DATASET = DASHBOARD_USER_DATASET; entityRoutes: { [key: number]: string[] } = {}; private coverImageUrls = new Map(); constructor( private workflowPersistService: WorkflowPersistService, private datasetService: DatasetService, private cdr: ChangeDetectorRef ) {} ngOnInit(): void { this.entities.forEach(entity => { this.initializeEntry(entity); }); this.loadCoverImages(); } ngOnChanges(changes: SimpleChanges): void { this.entities.forEach(entity => { this.initializeEntry(entity); }); this.loadCoverImages(); } private initializeEntry(entity: DashboardEntry): void { if (typeof entity.id !== "number") { return; } const entityId = entity.id; const owners = entity.accessibleUserIds; if (entity.type === "workflow") { if (this.currentUid !== undefined && owners.includes(this.currentUid)) { this.entityRoutes[entityId] = [this.DASHBOARD_USER_WORKSPACE, String(entityId)]; } else { this.entityRoutes[entityId] = [this.DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL, String(entityId)]; } } else if (entity.type === "dataset") { if (this.currentUid !== undefined && owners.includes(this.currentUid)) { this.entityRoutes[entityId] = [this.DASHBOARD_USER_DATASET, String(entityId)]; } else { this.entityRoutes[entityId] = [this.DASHBOARD_HUB_DATASET_RESULT_DETAIL, String(entityId)]; } } else { throw new Error("Unexpected type in DashboardEntry."); } } private loadCoverImages(): void { if (!this.entities) return; this.entities .filter( (entity): entity is DashboardEntry & { id: number } => entity.type === "dataset" && entity.coverImageUrl !== undefined && entity.id !== undefined && !this.coverImageUrls.has(entity.id) ) .forEach(entity => { const coverUrl = `${AppSettings.getApiEndpoint()}/dataset/${entity.id}/cover`; this.coverImageUrls.set(entity.id, coverUrl); }); } getCoverImage(entity: DashboardEntry): string { return this.coverImageUrls.get(entity.id!) || this.defaultBackground; } } ================================================ FILE: frontend/src/app/hub/component/hub-search-result/hub-search-result.component.html ================================================
    ================================================ FILE: frontend/src/app/hub/component/hub-search-result/hub-search-result.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ @import "../../../dashboard/component/user/search/search.component"; ================================================ FILE: frontend/src/app/hub/component/hub-search-result/hub-search-result.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { AfterViewInit, Component, Input, OnInit, ViewChild } from "@angular/core"; import { Router } from "@angular/router"; import { SearchResultsComponent } from "../../../dashboard/component/user/search-results/search-results.component"; import { FiltersComponent } from "../../../dashboard/component/user/filters/filters.component"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { SortMethod } from "../../../dashboard/type/sort-method"; import { UserService } from "../../../common/service/user/user.service"; import { SearchService } from "../../../dashboard/service/user/search.service"; import { isDefined } from "../../../common/util/predicate"; import { firstValueFrom } from "rxjs"; import { map } from "rxjs/operators"; import { SortButtonComponent } from "../../../dashboard/component/user/sort-button/sort-button.component"; @UntilDestroy() @Component({ selector: "texera-hub-search", templateUrl: "./hub-search-result.component.html", styleUrls: ["./hub-search-result.component.scss"], imports: [SortButtonComponent, FiltersComponent, SearchResultsComponent], }) export class HubSearchResultComponent implements OnInit, AfterViewInit { public searchType: "dataset" | "workflow" = "workflow"; public searchKeywords: string[] = []; currentUid = this.userService.getCurrentUser()?.uid; private isLogin = false; private includePublic = true; private _searchResultsComponent?: SearchResultsComponent; @ViewChild(SearchResultsComponent) get searchResultsComponent(): SearchResultsComponent | undefined { return this._searchResultsComponent; } set searchResultsComponent(value: SearchResultsComponent) { this._searchResultsComponent = value; } private _filters?: FiltersComponent; @ViewChild(FiltersComponent) get filters(): FiltersComponent | undefined { return this._filters; } set filters(value: FiltersComponent) { value.masterFilterListChange.pipe(untilDestroyed(this)).subscribe({ next: () => this.search() }); this._filters = value; } private masterFilterList: ReadonlyArray | null = null; @Input() public pid?: number = undefined; @Input() public accessLevel?: string = undefined; public sortMethod = SortMethod.EditTimeDesc; lastSortMethod: SortMethod | null = null; constructor( private userService: UserService, private searchService: SearchService, private router: Router ) { this.userService .userChanged() .pipe(untilDestroyed(this)) .subscribe(() => { this.currentUid = this.userService.getCurrentUser()?.uid; }); } ngOnInit() { const url = this.router.url; if (url.includes("dataset")) { this.searchType = "dataset"; } else if (url.includes("workflow")) { this.searchType = "workflow"; } } ngAfterViewInit() { this.userService .userChanged() .pipe(untilDestroyed(this)) .subscribe(() => this.search()); } /** * Searches dataset or workflow based on the `searchType` determined from the full URL. * @returns * * todo: Integrate the search functions from different interfaces into a single method. */ async search(forced: boolean = false): Promise { if (!this.filters || !this.searchResultsComponent) { return; } const sameList = this.masterFilterList !== null && this.filters.masterFilterList.length === this.masterFilterList.length && this.filters.masterFilterList.every((v, i) => v === this.masterFilterList![i]); if (!forced && sameList && this.sortMethod === this.lastSortMethod) { // If the filter lists are the same, do no make the same request again. return; } this.lastSortMethod = this.sortMethod; this.masterFilterList = this.filters.masterFilterList; this.searchKeywords = this.filters.getSearchKeywords(); let filterParams = this.filters.getSearchFilterParameters(); if (isDefined(this.pid)) { // force the project id in the search query to be the current pid. filterParams.projectIds = [this.pid]; } this.searchResultsComponent.reset((start, count) => { return firstValueFrom( this.searchService .executeSearch( [""], filterParams, start, count, this.searchType, this.sortMethod, this.isLogin, this.includePublic ) .pipe(map(({ entries, more }) => ({ entries, more }))) ); }); await this.searchResultsComponent.loadMore(); } } ================================================ FILE: frontend/src/app/hub/component/hub.component.html ================================================
    • Home
    • Workflows
    • Datasets
    ================================================ FILE: frontend/src/app/hub/component/hub.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ ================================================ FILE: frontend/src/app/hub/component/hub.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, Input } from "@angular/core"; import { DASHBOARD_HOME, DASHBOARD_HUB_DATASET_RESULT, DASHBOARD_HUB_WORKFLOW_RESULT, } from "../../app-routing.constant"; import { GuiConfigService } from "../../common/service/gui-config.service"; import { SidebarTabs } from "../../common/type/gui-config"; import { NgIf } from "@angular/common"; import { NzMenuItemComponent } from "ng-zorro-antd/menu"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { RouterLink } from "@angular/router"; import { NzIconDirective } from "ng-zorro-antd/icon"; @Component({ selector: "texera-hub", templateUrl: "hub.component.html", styleUrls: ["hub.component.scss"], imports: [NgIf, NzMenuItemComponent, ɵNzTransitionPatchDirective, NzTooltipDirective, RouterLink, NzIconDirective], }) export class HubComponent { @Input() isLogin: boolean = false; @Input() sidebarTabs: SidebarTabs = {} as SidebarTabs; protected readonly DASHBOARD_HOME = DASHBOARD_HOME; protected readonly DASHBOARD_HUB_WORKFLOW_RESULT = DASHBOARD_HUB_WORKFLOW_RESULT; protected readonly DASHBOARD_HUB_DATASET_RESULT = DASHBOARD_HUB_DATASET_RESULT; constructor(protected config: GuiConfigService) {} } ================================================ FILE: frontend/src/app/hub/component/landing-page/landing-page.component.html ================================================

    Texera Hub

    Join our community to explore public workflows, collaborate with others, and enhance your data analytics capabilities. Access {{workflowCount}} workflows and {{datasetCount}} datasets provided by Texera and our community.

    ================================================ FILE: frontend/src/app/hub/component/landing-page/landing-page.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .landing-page-container { display: flex; flex-direction: column; padding: 0 133px; align-items: center; height: 100%; width: 100%; overflow-y: auto; } .search-bar { max-width: 1400px; width: 100%; } .section-intro { max-width: 1400px; margin-top: 20px; padding: 20px 0; width: 100%; display: flex; justify-content: space-between; align-items: center; } .section-intro-text { max-width: 500px; } .section-intro-text > h1 { font-size: 40px; font-weight: bold; margin: 0; } .section-intro-text > p { color: grey; font-size: 13px; font-weight: 300; margin-bottom: 20px; } .section-intro-img { max-height: 176px; } .browse-section { max-width: 1200px; width: 100%; padding-bottom: 40px; } ================================================ FILE: frontend/src/app/hub/component/landing-page/landing-page.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, OnInit } from "@angular/core"; import { firstValueFrom } from "rxjs"; import { ActionType, EntityType, HubService } from "../../service/hub.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { Router } from "@angular/router"; import { SearchService } from "../../../dashboard/service/user/search.service"; import { DashboardEntry } from "../../../dashboard/type/dashboard-entry"; import { DASHBOARD_HOME, DASHBOARD_HUB_DATASET_RESULT, DASHBOARD_HUB_WORKFLOW_RESULT, } from "../../../app-routing.constant"; import { UserService } from "../../../common/service/user/user.service"; import { BrowseSectionComponent } from "../browse-section/browse-section.component"; @UntilDestroy() @Component({ selector: "texera-landing-page", templateUrl: "./landing-page.component.html", styleUrls: ["./landing-page.component.scss"], imports: [BrowseSectionComponent], }) export class LandingPageComponent implements OnInit { public isLogin = this.userService.isLogin(); public currentUid = this.userService.getCurrentUser()?.uid; public workflowCount: number = 0; public datasetCount: number = 0; public topLovedWorkflows: DashboardEntry[] = []; public topClonedWorkflows: DashboardEntry[] = []; public topLovedDatasets: DashboardEntry[] = []; constructor( private hubService: HubService, private router: Router, private searchService: SearchService, private userService: UserService ) { this.userService .userChanged() .pipe(untilDestroyed(this)) .subscribe(() => { this.isLogin = this.userService.isLogin(); this.currentUid = this.userService.getCurrentUser()?.uid; }); } ngOnInit(): void { this.getWorkflowCount(); this.loadTops(); } async loadTops() { try { const [workflowEntries, datasetEntries] = await Promise.all([ this.getTopLovedEntries(EntityType.Workflow, [ActionType.Like, ActionType.Clone]), this.getTopLovedEntries(EntityType.Dataset, [ActionType.Like]), ]); this.topLovedWorkflows = workflowEntries["like"] || []; this.topClonedWorkflows = workflowEntries["clone"] || []; this.topLovedDatasets = datasetEntries["like"] || []; } catch (error) { console.error("Failed to load top entries:", error); } } getWorkflowCount(): void { this.hubService .getCount(EntityType.Workflow) .pipe(untilDestroyed(this)) .subscribe((count: number) => { this.workflowCount = count; }); this.hubService .getCount(EntityType.Dataset) .pipe(untilDestroyed(this)) .subscribe((count: number) => { this.datasetCount = count; }); } public async getTopLovedEntries( entityType: EntityType, actionTypes: ActionType[] ): Promise<{ [actionType: string]: DashboardEntry[] }> { const topsMap = await firstValueFrom(this.hubService.getTops(entityType, actionTypes, this.currentUid)); const result: { [key: string]: DashboardEntry[] } = {}; for (const act of actionTypes) { const items = topsMap[act] || []; result[act] = await firstValueFrom( this.searchService.extendSearchResultsWithHubActivityInfo(items, true, ["access"]) ); } return result; } navigateToSearch(type: string): void { let path: string; switch (type) { case "workflow": path = DASHBOARD_HUB_WORKFLOW_RESULT; break; case "dataset": path = DASHBOARD_HUB_DATASET_RESULT; break; default: path = DASHBOARD_HOME; } this.router.navigate([path]); } } ================================================ FILE: frontend/src/app/hub/component/type/hub-workflow.interface.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ export interface HubWorkflow { name: string; description: string; wid: number; content: string; creationTime: number; lastModifiedTime: number; } ================================================ FILE: frontend/src/app/hub/component/workflow/detail/hub-workflow-detail.component.html ================================================

    Workflow Detail Page

    ID

    {{ wid }}

    Workflow Name

    {{ workflowName }}

    Created By

    {{ ownerName }}

    Description

    Preview

    ================================================ FILE: frontend/src/app/hub/component/workflow/detail/hub-workflow-detail.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f4f4f4; } .container { width: 80%; margin: 30px auto 0; padding: 20px; background-color: #fff; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); } h1 { color: #333; } .workflow-info { display: flex; justify-content: space-between; margin-bottom: 20px; } .workflow-info div { flex: 1; padding: 10px; background-color: #e9e9e9; margin-right: 10px; } .workflow-info div:last-child { margin-right: 0; } .workflow-steps { margin-top: 20px; } .workflow-step { background-color: #f9f9f9; padding: 15px; margin-bottom: 10px; border-left: 5px solid #007bff; } .workflow-step h3 { margin: 0; color: #007bff; } .workflow-step p { margin: 5px 0; } .preview-containers { position: relative; height: 600px; } texera-mini-map { position: absolute; bottom: 0; right: 0; } .workflow-description { padding: 10px; background-color: #e9e9e9; margin-top: 20px; } .go-back-button { position: absolute; left: 20px; top: 20px; } .header-info { display: flex; align-items: center; justify-content: space-between; padding: 10px 0; } .workflow-panel { display: flex; align-items: center; gap: 10px; } .clone-button, .like-button { width: 70px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .liked { color: red; } ================================================ FILE: frontend/src/app/hub/component/workflow/detail/hub-workflow-detail.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { AfterViewInit, Component, HostListener, Inject, OnDestroy, OnInit, Optional } from "@angular/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { ActivatedRoute, Router } from "@angular/router"; import { UserService } from "../../../../common/service/user/user.service"; import { WorkflowActionService } from "../../../../workspace/service/workflow-graph/model/workflow-action.service"; import { throttleTime } from "rxjs/operators"; import { Workflow } from "../../../../common/type/workflow"; import { isDefined } from "../../../../common/util/predicate"; import { ActionType, EntityType, HubService, LikedStatus } from "../../../service/hub.service"; import { Role, User } from "src/app/common/type/user"; import { NotificationService } from "../../../../common/service/notification/notification.service"; import { WorkflowPersistService } from "../../../../common/service/workflow-persist/workflow-persist.service"; import { NZ_MODAL_DATA } from "ng-zorro-antd/modal"; import { DASHBOARD_HUB_WORKFLOW_RESULT, DASHBOARD_USER_WORKSPACE } from "../../../../app-routing.constant"; import { NgIf, NgClass } from "@angular/common"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { MarkdownDescriptionComponent } from "../../../../dashboard/component/user/markdown-description/markdown-description.component"; import { WorkflowEditorComponent } from "../../../../workspace/component/workflow-editor/workflow-editor.component"; import { MiniMapComponent } from "../../../../workspace/component/workflow-editor/mini-map/mini-map.component"; import { FormlyRepeatDndComponent } from "../../../../common/formly/repeat-dnd/repeat-dnd.component"; export const THROTTLE_TIME_MS = 1000; @UntilDestroy() @Component({ selector: "texera-hub-workflow-detail", templateUrl: "hub-workflow-detail.component.html", styleUrls: ["hub-workflow-detail.component.scss"], imports: [ NgIf, NzSpaceCompactItemDirective, NzButtonComponent, ɵNzTransitionPatchDirective, NzIconDirective, NzWaveDirective, NgClass, MarkdownDescriptionComponent, WorkflowEditorComponent, MiniMapComponent, FormlyRepeatDndComponent, ], }) export class HubWorkflowDetailComponent implements AfterViewInit, OnDestroy, OnInit { isHub: boolean = false; workflowName: string = ""; ownerName: string = ""; workflowDescription: string = ""; isLogin = this.userService.isLogin(); isActivatedUser: boolean = false; isLiked: boolean = false; likeCount: number = 0; cloneCount: number = 0; displayPreciseViewCount = false; viewCount: number = 0; wid: number | undefined; protected readonly currentUser?: User; constructor( private userService: UserService, private workflowActionService: WorkflowActionService, private route: ActivatedRoute, private router: Router, private notificationService: NotificationService, private hubService: HubService, private workflowPersistService: WorkflowPersistService, @Optional() @Inject(NZ_MODAL_DATA) public input: { wid: number } | undefined ) { this.wid = input?.wid; //Accessing from the pop up. getting wid from the @Input if (!isDefined(this.wid)) { // otherwise getting wid from the route this.wid = this.route.snapshot.params.id; this.isHub = true; } this.currentUser = this.userService.getCurrentUser(); if (this.currentUser?.role === Role.ADMIN || this.currentUser?.role === Role.REGULAR) { this.isActivatedUser = true; } this.workflowActionService.disableWorkflowModification(); } ngOnInit() { if (!isDefined(this.wid)) { return; } this.hubService .getCounts([EntityType.Workflow], [this.wid], [ActionType.Like, ActionType.Clone]) .pipe(untilDestroyed(this)) .subscribe(counts => { this.likeCount = counts[0].counts.like ?? 0; this.cloneCount = counts[0].counts.clone ?? 0; }); this.hubService .postView(this.wid, this.currentUser?.uid ?? 0, EntityType.Workflow) .pipe(throttleTime(THROTTLE_TIME_MS)) .pipe(untilDestroyed(this)) .subscribe(count => { this.viewCount = count; }); this.workflowPersistService .getOwnerName(this.wid) .pipe(untilDestroyed(this)) .subscribe(ownerName => { this.ownerName = ownerName; }); this.workflowPersistService .getWorkflowName(this.wid) .pipe(untilDestroyed(this)) .subscribe(workflowName => { this.workflowName = workflowName; }); this.workflowPersistService .getWorkflowDescription(this.wid) .pipe(untilDestroyed(this)) .subscribe(workflowDescription => { this.workflowDescription = workflowDescription || "No description available"; }); // if there is a user, check if the user liked the workflow if (!isDefined(this.currentUser)) { return; } this.hubService .isLiked([this.wid], [EntityType.Workflow]) .pipe(untilDestroyed(this)) .subscribe((isLiked: LikedStatus[]) => { this.isLiked = isLiked.length > 0 ? isLiked[0].isLiked : false; }); } ngAfterViewInit(): void { if (!this.wid) { return; } this.loadWorkflowWithId(this.wid); } @HostListener("window:beforeunload") ngOnDestroy() { this.workflowActionService.clearWorkflow(); } /** * Load the workflow with the given id. * If accessing through the hub, load the public workflow. * If accessing through the workspace, load the private workflow. * @param wid */ loadWorkflowWithId(wid: number): void { if (!this.isHub) { this.workflowPersistService .retrieveWorkflow(wid) .pipe(untilDestroyed(this)) .subscribe({ next: (workflow: Workflow) => { // load the fetched workflow this.workflowActionService.reloadWorkflow(workflow); this.workflowActionService.getTexeraGraph().triggerCenterEvent(); }, error: () => { throw new Error(`Failed to load workflow with id ${wid}`); }, }); } else { this.workflowPersistService .retrievePublicWorkflow(wid) .pipe(untilDestroyed(this)) .subscribe({ next: (workflow: Workflow) => { // load the fetched workflow this.workflowActionService.reloadWorkflow(workflow); this.workflowActionService.getTexeraGraph().triggerCenterEvent(); }, error: () => { throw new Error(`Failed to load workflow with id ${wid}`); }, }); } } goBack(): void { this.router.navigateByUrl(DASHBOARD_HUB_WORKFLOW_RESULT).catch(() => { this.notificationService.error("Go back failed. Please try again."); }); } cloneWorkflow(): void { if (!isDefined(this.wid)) { return; } this.hubService .cloneWorkflow(this.wid) .pipe(untilDestroyed(this)) .subscribe(newWid => { this.router.navigate([`${DASHBOARD_USER_WORKSPACE}/${newWid}`]).then(() => { this.notificationService.success("Clone Successful"); }); }); } toggleLike(): void { const userId = this.currentUser?.uid; if (!isDefined(userId) || !isDefined(this.wid)) { return; } if (this.isLiked) { this.hubService .postUnlike(this.wid, EntityType.Workflow) .pipe(untilDestroyed(this)) .subscribe((success: boolean) => { if (success) { this.isLiked = false; if (!isDefined(this.wid)) { return; } this.hubService .getCounts([EntityType.Workflow], [this.wid], [ActionType.Like]) .pipe(untilDestroyed(this)) .subscribe(counts => { this.likeCount = counts[0].counts.like ?? 0; }); } }); } else { this.hubService .postLike(this.wid, EntityType.Workflow) .pipe(untilDestroyed(this)) .subscribe((success: boolean) => { if (success) { this.isLiked = true; if (!isDefined(this.wid)) { return; } this.hubService .getCounts([EntityType.Workflow], [this.wid], [ActionType.Like]) .pipe(untilDestroyed(this)) .subscribe(counts => { this.likeCount = counts[0].counts.like ?? 0; }); } }); } } formatCount(count: number): string { if (count >= 1000) { return (count / 1000).toFixed(1) + "k"; } return count.toString(); } changeViewDisplayStyle() { this.displayPreciseViewCount = !this.displayPreciseViewCount; } } ================================================ FILE: frontend/src/app/hub/service/hub.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { HttpClient, HttpHeaders, HttpParams } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable } from "rxjs"; import { AppSettings } from "../../common/app-setting"; import { SearchResultItem } from "../../dashboard/type/search-result"; export const WORKFLOW_BASE_URL = `${AppSettings.getApiEndpoint()}/workflow`; export enum EntityType { Workflow = "workflow", Dataset = "dataset", Project = "project", File = "file", ComputingUnit = "computing-unit", } export enum ActionType { View = "view", Like = "like", Clone = "clone", Unlike = "unlike", } export type LikedStatus = { entityId: number; entityType: EntityType; isLiked: boolean; }; export interface CountResponse { entityId: number; entityType: EntityType; counts: Partial>; } export interface AccessResponse { entityType: EntityType; entityId: number; userIds: number[]; } @Injectable({ providedIn: "root", }) export class HubService { readonly BASE_URL: string = `${AppSettings.getApiEndpoint()}/hub`; constructor(private http: HttpClient) {} public getCount(entityType: EntityType): Observable { return this.http.get(`${this.BASE_URL}/count`, { params: { entityType: entityType }, }); } public cloneWorkflow(wid: number): Observable { return this.http.post(`${WORKFLOW_BASE_URL}/clone/${wid}`, null); } public isLiked(entityIds: number[], entityTypes: EntityType[]): Observable { let params = new HttpParams(); entityIds.forEach(id => { params = params.append("entityId", id.toString()); }); entityTypes.forEach(type => { params = params.append("entityType", type); }); return this.http.get(`${this.BASE_URL}/isLiked`, { params }); } public postLike(entityId: number, entityType: EntityType): Observable { const body = { entityId, entityType }; return this.http.post(`${this.BASE_URL}/like`, body, { headers: new HttpHeaders({ "Content-Type": "application/json" }), }); } public postUnlike(entityId: number, entityType: EntityType): Observable { const body = { entityId, entityType }; return this.http.post(`${this.BASE_URL}/unlike`, body, { headers: new HttpHeaders({ "Content-Type": "application/json" }), }); } public postView(entityId: number, userId: number, entityType: EntityType): Observable { const body = { entityId, userId, entityType }; return this.http.post(`${this.BASE_URL}/view`, body, { headers: new HttpHeaders({ "Content-Type": "application/json" }), }); } /** * Fetches the top entities for the given action types in one request. * * @param entityType The type of entity to query (e.g. EntityType.Workflow or EntityType.Dataset). * @param actionTypes An array of action types to retrieve (only ActionType.Like and ActionType.Clone). * @param currentUid Optional user ID context (will be sent as -1 if undefined). * @param limit Optional maximum number of top items per action; must be >0 (default: 8). * @returns An Observable resolving to a map where each key is one of ActionType.Like | ActionType.Clone * and the value is the corresponding list of SearchResultItem. */ public getTops( entityType: EntityType, actionTypes: ActionType[], currentUid?: number, limit?: number ): Observable> { let params = new HttpParams() .set("entityType", entityType) .set("uid", (currentUid !== undefined ? currentUid : -1).toString()); if (limit != null && limit > 0) { params = params.set("limit", limit.toString()); } actionTypes.forEach(act => { params = params.append("actionTypes", act); }); return this.http.get>(`${this.BASE_URL}/getTops`, { params }); } /** * Fetches count metrics for multiple entities in a single request. * * @param entityTypes Array of entity types (e.g., [EntityType.Workflow, EntityType.Dataset]). * @param entityIds Corresponding array of entity IDs (e.g., [123, 456]). Must match length of entityTypes. * @param actionTypes Optional array of action types to retrieve counts for (members of ActionType enum). * Supported values: ActionType.View, ActionType.Like, ActionType.Clone. * If omitted or empty, all three will be returned. * @returns An Observable that emits an array of CountResponse objects, each containing: * - entityId: the ID of the entity * - entityType: the type of the entity * - counts: a map from ActionType to number */ public getCounts( entityTypes: EntityType[], entityIds: number[], actionTypes: ActionType[] = [] ): Observable { let params = new HttpParams(); entityTypes.forEach(type => { params = params.append("entityType", type); }); entityIds.forEach(id => { params = params.append("entityId", id.toString()); }); actionTypes.forEach(a => { params = params.append("actionType", a); }); return this.http.get(`${this.BASE_URL}/counts`, { params }); } public getUserAccess(entityTypes: EntityType[], entityIds: number[]): Observable { let params = new HttpParams(); entityTypes.forEach(t => (params = params.append("entityType", t))); entityIds.forEach(i => (params = params.append("entityId", i.toString()))); return this.http.get(`${this.BASE_URL}/user-access`, { params }); } } ================================================ FILE: frontend/src/app/workspace/component/agent/agent-interaction/agent-interaction.component.html ================================================
    {{ getColumnDisplayName(col) }}
    ...
    {{ row.record?.[col] }}
    Column Statistics
    {{ col.column }} {{ col.dataType }}
    {{ s.key }}: {{ s.value }}
    {{ agent.name }} {{ selected.nzLabel }}
    Ctrl+Enter to send
    Select a connected agent to chat
    ================================================ FILE: frontend/src/app/workspace/component/agent/agent-interaction/agent-interaction.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Main container - always compact .agent-interaction { display: flex; flex-direction: column; background: #fafafa; padding: 8px 12px; &.compact { padding: 6px 8px; } } // Visualization rendering .visualization-section { margin-bottom: 10px; } .visualization-iframe { width: 100%; height: 450px; border: 1px solid #e8e8e8; border-radius: 4px; background: #fff; } // Sample Records Table .sample-records-section { margin-bottom: 10px; } .sample-records-table-wrapper { max-height: 200px; overflow: auto; border: 1px solid #e8e8e8; border-radius: 4px; // Custom scrollbar &::-webkit-scrollbar { width: 6px; height: 6px; } &::-webkit-scrollbar-track { background: #f0f0f0; } &::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; &:hover { background: #999; } } } .sample-records-table { width: 100%; border-collapse: collapse; font-size: 11px; th, td { padding: 4px 8px; text-align: left; white-space: nowrap; border-bottom: 1px solid #f0f0f0; } th { background: #fafafa; font-weight: 600; color: #555; position: sticky; top: 0; z-index: 1; border-bottom: 1px solid #e8e8e8; } td { color: #333; max-width: 150px; overflow: hidden; text-overflow: ellipsis; } tbody tr:hover { background: #f5f5f5; } .ellipsis-row td { text-align: center; color: #999; font-weight: 500; padding: 2px 8px; background: #fafafa; } } // Column Statistics .column-stats-section { margin-bottom: 10px; border: 1px solid #e8e8e8; border-radius: 4px; background: #fff; } .column-stats-header { display: flex; align-items: center; gap: 6px; padding: 6px 10px; background: #fafafa; border-bottom: 1px solid #f0f0f0; font-size: 11px; font-weight: 600; color: #555; i { font-size: 12px; color: #1890ff; } } .column-stats-list { max-height: 180px; overflow: auto; scrollbar-width: thin; &::-webkit-scrollbar { width: 4px; height: 4px; } &::-webkit-scrollbar-track { background: #f0f0f0; } &::-webkit-scrollbar-thumb { background: #ccc; border-radius: 2px; } } .column-stat-item { padding: 4px 10px; border-bottom: 1px solid #f5f5f5; &:last-child { border-bottom: none; } } .column-stat-name { display: flex; align-items: center; gap: 6px; margin-bottom: 2px; .col-name { font-size: 11px; font-weight: 600; color: #333; font-family: "SFMono-Regular", Consolas, monospace; } .col-type { font-size: 10px; color: #8c8c8c; background: #f0f0f0; padding: 0 4px; border-radius: 2px; } } .column-stat-details { display: flex; flex-wrap: wrap; gap: 4px 10px; .stat-entry { font-size: 10px; color: #595959; } .stat-key { color: #8c8c8c; } } .header-row { display: flex; align-items: center; gap: 8px; } .agent-select { flex: 1; min-width: 100px; max-width: 180px; } .agent-status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background-color: #d9d9d9; margin-right: 6px; vertical-align: middle; &.connected { background-color: #52c41a; } } .input-section { display: flex; flex-direction: column; gap: 8px; border-top: 1px solid #f0f0f0; padding-top: 12px; } .feedback-input { width: 100%; font-size: 13px; resize: none; } .controls-row { display: flex; gap: 8px; align-items: center; justify-content: flex-end; } .hint-text { font-size: 11px; color: #999; margin-right: auto; } .send-btn { display: flex; align-items: center; gap: 4px; i { font-size: 12px; } } .not-connected-message { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 20px; color: #999; font-size: 13px; background: #f5f5f5; border-radius: 6px; margin-top: 8px; i { font-size: 18px; } } ================================================ FILE: frontend/src/app/workspace/component/agent/agent-interaction/agent-interaction.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from "@angular/core"; import { DomSanitizer, SafeHtml } from "@angular/platform-browser"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { AgentService } from "../../../service/agent/agent.service"; import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; import { NotificationService } from "../../../../common/service/notification/notification.service"; import { NgIf, NgFor } from "@angular/common"; import { NzTheadComponent, NzTrDirective, NzTableCellDirective, NzThMeasureDirective, NzTbodyComponent, } from "ng-zorro-antd/table"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzSelectComponent, NzOptionComponent } from "ng-zorro-antd/select"; import { FormsModule } from "@angular/forms"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzInputDirective, NzAutosizeDirective } from "ng-zorro-antd/input"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; /** * AgentInteractionComponent provides a compact interface for users to send feedback * or messages to agents regarding a specific operator. * It consists of an agent dropdown and a text input area. */ @UntilDestroy() @Component({ selector: "texera-agent-interaction", templateUrl: "./agent-interaction.component.html", styleUrls: ["./agent-interaction.component.scss"], imports: [ NgIf, NzTheadComponent, NzTrDirective, NgFor, NzTableCellDirective, NzThMeasureDirective, NzTbodyComponent, ɵNzTransitionPatchDirective, NzIconDirective, NzSpaceCompactItemDirective, NzSelectComponent, FormsModule, NzOptionComponent, NzTooltipDirective, NzInputDirective, NzAutosizeDirective, NzButtonComponent, NzWaveDirective, ], }) export class AgentInteractionComponent implements OnInit, OnChanges { @Input() operatorId!: string; @Input() operatorDisplayName?: string; @Input() sampleRecords?: Record[]; @Input() resultStatistics?: Record; public availableAgents: Array<{ id: string; name: string; isConnected: boolean }> = []; public selectedAgentId: string | null = null; public feedbackMessage: string = ""; // Cached visualization HTML to avoid iframe re-render on every WS update private cachedVisualizationHtml: SafeHtml | null = null; private cachedVisualizationRawHtml: string | null = null; constructor( private agentService: AgentService, private workflowActionService: WorkflowActionService, private notificationService: NotificationService, private changeDetectorRef: ChangeDetectorRef, private sanitizer: DomSanitizer ) {} ngOnInit(): void { this.loadAvailableAgents(); this.agentService.agentChange$.pipe(untilDestroyed(this)).subscribe(() => { this.loadAvailableAgents(); }); } ngOnChanges(changes: SimpleChanges): void { if (changes["sampleRecords"]) { // Only update cached visualization HTML when the actual content changes const newRecords = changes["sampleRecords"].currentValue as Record[] | undefined; const newHtml = newRecords?.[0]?.["html-content"] || null; if (newHtml !== this.cachedVisualizationRawHtml) { this.cachedVisualizationRawHtml = newHtml; this.cachedVisualizationHtml = newHtml ? this.sanitizer.bypassSecurityTrustHtml(newHtml) : null; } } } private loadAvailableAgents(): void { this.agentService .getAllAgents() .pipe(untilDestroyed(this)) .subscribe(agents => { const connectedAgentIds = new Set(this.agentService.getActivelyConnectedAgentIds()); this.availableAgents = agents.map(agent => ({ id: agent.id, name: agent.name, isConnected: connectedAgentIds.has(agent.id), })); // Auto-select: prefer connected agent, then first agent if only one const connectedAgent = this.availableAgents.find(a => a.isConnected); if (connectedAgent) { this.selectedAgentId = connectedAgent.id; } else if (this.availableAgents.length === 1) { this.selectedAgentId = this.availableAgents[0].id; } this.changeDetectorRef.detectChanges(); }); } public isSelectedAgentConnected(): boolean { if (!this.selectedAgentId) return false; return this.agentService.isAgentActivelyConnected(this.selectedAgentId); } public sendFeedbackToAgent(): void { if (!this.selectedAgentId || !this.feedbackMessage.trim() || !this.operatorId) { return; } if (!this.isSelectedAgentConnected()) { this.notificationService.error("Agent is not connected. Please open the agent chat panel first."); return; } const agentId = this.selectedAgentId; const operatorName = this.operatorDisplayName || this.getOperatorName() || "this operator"; const contextMessage = `Regarding operator "${operatorName}" (ID: ${this.operatorId}): ${this.feedbackMessage.trim()}`; this.agentService.sendMessage(agentId, contextMessage, "feedback"); this.notificationService.success("Message sent to agent successfully"); this.feedbackMessage = ""; this.changeDetectorRef.detectChanges(); } private getOperatorName(): string | undefined { try { const operator = this.workflowActionService.getTexeraGraph().getOperator(this.operatorId); return operator?.customDisplayName || undefined; } catch { return undefined; } } public canSend(): boolean { return !!this.selectedAgentId && !!this.feedbackMessage.trim(); } /** * Check if sample records represent a visualization (has __is_visualization__ flag). */ public isVisualization(): boolean { if (!this.sampleRecords || this.sampleRecords.length === 0) return false; return this.sampleRecords[0]["__is_visualization__"] === true; } /** * Get the cached sanitized HTML content from a visualization record for iframe srcdoc. */ public getVisualizationHtml(): SafeHtml { return this.cachedVisualizationHtml || this.sanitizer.bypassSecurityTrustHtml(""); } /** * Get column names from sample records, placing __row_index__ first (displayed as "Row"). */ public getSampleColumns(): string[] { if (!this.sampleRecords || this.sampleRecords.length === 0) return []; const allKeys = Object.keys(this.sampleRecords[0]); const rowIndexKey = allKeys.find(k => k.startsWith("_") && k.includes("row_index")); const otherKeys = allKeys.filter(k => k !== rowIndexKey); return rowIndexKey ? [rowIndexKey, ...otherKeys] : otherKeys; } /** * Get display name for a column header. */ public getColumnDisplayName(col: string): string { if (col.startsWith("_") && col.includes("row_index")) return "Row"; return col; } /** * Parse resultStatistics into displayable column stats. * Each entry in resultStatistics is a JSON string with { data_type, statistics: { ... } }. */ public getParsedColumnStats(): Array<{ column: string; dataType: string; stats: Array<{ key: string; value: string }>; }> { if (!this.resultStatistics) return []; const sampleCols = this.getSampleColumns().filter(c => !c.startsWith("_") || !c.includes("row_index")); const columns = sampleCols.length > 0 ? sampleCols : Object.keys(this.resultStatistics); const result: Array<{ column: string; dataType: string; stats: Array<{ key: string; value: string }> }> = []; const excludedKeys = new Set(["count", "std", "p25", "median", "p75"]); for (const colName of columns) { const statsJson = this.resultStatistics[colName]; if (!statsJson) continue; try { const parsed = JSON.parse(statsJson); const dataType: string = parsed.data_type ?? "unknown"; const statistics: Record = parsed.statistics ?? {}; const statEntries: Array<{ key: string; value: string }> = []; for (const [key, value] of Object.entries(statistics)) { if (value === undefined || excludedKeys.has(key)) continue; if (key === "top_10" && typeof value === "object") { const topEntries = Object.entries(value as Record) .slice(0, 5) .map(([k, v]) => `${k}: ${v}`) .join(", "); statEntries.push({ key: "top values", value: topEntries }); } else if (value === null || String(value) === "null") { statEntries.push({ key, value: "NaN" }); } else if (typeof value !== "object") { const formatted = typeof value === "number" && !Number.isInteger(value) ? Number(value.toPrecision(4)).toString() : String(value); statEntries.push({ key, value: formatted }); } } result.push({ column: colName, dataType, stats: statEntries }); } catch { // skip unparseable } } return result; } public hasColumnStats(): boolean { return this.getParsedColumnStats().length > 0; } public getDisplayRows(): Array<{ record?: Record; isEllipsis: boolean }> { if (!this.sampleRecords || this.sampleRecords.length === 0) return []; const rowIndexKey = Object.keys(this.sampleRecords[0]).find(k => k.startsWith("_") && k.includes("row_index")); if (!rowIndexKey) { return this.sampleRecords.map(r => ({ record: r, isEllipsis: false })); } const rows: Array<{ record?: Record; isEllipsis: boolean }> = []; for (let i = 0; i < this.sampleRecords.length; i++) { if (i > 0) { const prevIdx = this.sampleRecords[i - 1][rowIndexKey]; const currIdx = this.sampleRecords[i][rowIndexKey]; if (typeof prevIdx === "number" && typeof currIdx === "number" && currIdx - prevIdx > 1) { rows.push({ isEllipsis: true }); } } rows.push({ record: this.sampleRecords[i], isEllipsis: false }); } return rows; } } ================================================ FILE: frontend/src/app/workspace/component/agent/agent-panel/agent-chat/agent-chat.component.html ================================================
    Model: {{ agentInfo.modelType }}
    {{ response.role === 'user' ? 'You' : agentInfo.name }}
    Execute {{ response.toolCalls.length }} tool{{ response.toolCalls.length > 1 ? 's' : '' }}
    {{ agentInfo.name }}
    Thinking...
    {{ agentInfo.name }}
    Stopping...
    Agent is disconnected. Please check your connection.
    {{ tool.name }} {{ tool.description }}
    Max characters for operator results (uses symmetric truncation)
    Max characters per cell - truncates large cell values
    Max time for individual tool execution (1-600 seconds)
    Max time for workflow execution (1-60 minutes)
    Maximum reasoning/tool steps per message (1-50)
    Operators {{ settingsAllowedOperatorTypes.length }}/{{ allAvailableOperatorTypes.length }}
    Toggle which operators are included in the agent's system prompt (general mode). When none are selected, all operators are included.
    {{ op.type }} {{ op.description }}
    No operators match "{{ operatorTypeSearchQuery }}"
    ================================================ FILE: frontend/src/app/workspace/component/agent/agent-panel/agent-chat/agent-chat.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .agent-chat-container { display: flex; flex-direction: column; height: 100%; width: 100%; background: white; } .chat-toolbar { display: flex; justify-content: flex-end; align-items: center; padding: 8px 12px; border-bottom: 1px solid #f0f0f0; background: white; } .chat-toolbar-controls { display: flex; width: 100%; align-items: center; justify-content: space-between; gap: 24px; } .toolbar-item { display: flex; align-items: center; flex-shrink: 0; } .chat-content-wrapper { flex: 1; overflow: hidden; display: flex; flex-direction: row; } // Main chat column (messages + input) .chat-main-column { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; } .messages-container { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 8px; } .message { display: flex; flex-direction: column; gap: 2px; max-width: 85%; &.user-message { align-self: flex-end; .message-content { background: #1890ff; color: white; } } &.ai-message { align-self: flex-start; .message-content { background: #f5f5f5; color: #262626; } } &.loading-message .message-content { background: #e6f7ff; border: 1px solid #91d5ff; } } .message-header { display: flex; align-items: center; gap: 6px; padding: 0 8px; font-size: 12px; color: #8c8c8c; i { font-size: 14px; } } .message-content { padding: 8px 12px; border-radius: 6px; line-height: 1.5; word-wrap: break-word; font-size: 14px; user-select: text; ::ng-deep markdown { display: block; p { margin: 0 0 8px 0; &:last-child { margin-bottom: 0; } } code { background: rgba(0, 0, 0, 0.06); padding: 2px 6px; border-radius: 3px; font-size: 13px; } pre { background: rgba(0, 0, 0, 0.06); padding: 12px; border-radius: 4px; overflow-x: auto; margin: 8px 0; code { background: transparent; padding: 0; } } ul, ol { margin: 8px 0; padding-left: 24px; } li { margin: 4px 0; } blockquote { border-left: 3px solid rgba(0, 0, 0, 0.1); padding-left: 12px; margin: 8px 0; color: rgba(0, 0, 0, 0.65); } a { color: #1890ff; text-decoration: none; &:hover { text-decoration: underline; } } } } .user-message .message-content ::ng-deep markdown { code { background: rgba(255, 255, 255, 0.2); color: white; } pre { background: rgba(255, 255, 255, 0.15); code { color: white; } } blockquote { border-left-color: rgba(255, 255, 255, 0.3); color: rgba(255, 255, 255, 0.9); } a { color: #e6f7ff; &:hover { color: white; } } } .input-area { display: flex; gap: 8px; padding: 8px 12px; border-top: 1px solid #f0f0f0; background: #ffffff; align-items: center; textarea { flex: 1; } button { align-self: flex-end; } } .connection-warning { display: flex; align-items: center; gap: 8px; padding: 8px 16px; background: #fff7e6; border-top: 1px solid #ffd666; color: #d46b08; font-size: 12px; i { color: #faad14; } } // Agent Settings Modal Styles .settings-section { padding: 12px 0; } .settings-prompt-display { background: #fafafa; padding: 12px; border-radius: 4px; border: 1px solid #d9d9d9; max-height: 500px; overflow-y: auto; } .settings-tools-list { max-height: 400px; overflow-y: auto; } .settings-tool-item { display: flex; align-items: flex-start; gap: 12px; padding: 8px 0; border-bottom: 1px solid #f5f5f5; &:last-child { border-bottom: none; } .settings-tool-info { flex: 1; min-width: 0; .settings-tool-name { display: block; font-weight: 500; color: #262626; font-size: 13px; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; } .settings-tool-description { display: block; color: #8c8c8c; font-size: 12px; line-height: 1.4; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } } } .settings-param-item { margin-bottom: 16px; .settings-param-label { display: block; font-weight: 500; color: #262626; margin-bottom: 8px; } .settings-param-input { display: flex; align-items: center; gap: 12px; .settings-param-hint { color: #8c8c8c; font-size: 12px; } } } ================================================ FILE: frontend/src/app/workspace/component/agent/agent-panel/agent-chat/agent-chat.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, ViewChild, ElementRef, Input, OnInit, AfterViewChecked, ChangeDetectorRef, OnDestroy, OnChanges, SimpleChanges, } from "@angular/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { Subject } from "rxjs"; import { distinctUntilChanged, filter, pairwise, startWith, takeUntil } from "rxjs/operators"; import { AgentState, ReActStep } from "../../../../service/agent/agent-types"; import { AgentInfo, AgentService } from "../../../../service/agent/agent.service"; import { WorkflowActionService } from "../../../../service/workflow-graph/model/workflow-action.service"; import { NotificationService } from "../../../../../common/service/notification/notification.service"; import { WorkflowPersistService } from "../../../../../common/service/workflow-persist/workflow-persist.service"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NgIf, NgFor } from "@angular/common"; import { MarkdownComponent } from "ngx-markdown"; import { NzSpinComponent } from "ng-zorro-antd/spin"; import { NzInputDirective, NzAutosizeDirective, NzInputGroupComponent, NzInputGroupWhitSuffixOrPrefixDirective, } from "ng-zorro-antd/input"; import { FormsModule } from "@angular/forms"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ReActStepDetailModalComponent } from "../react-step-detail-modal/react-step-detail-modal.component"; import { NzModalComponent, NzModalContentDirective } from "ng-zorro-antd/modal"; import { NzTabsComponent, NzTabComponent } from "ng-zorro-antd/tabs"; import { NzInputNumberComponent } from "ng-zorro-antd/input-number"; import { NzTagComponent } from "ng-zorro-antd/tag"; import { NzSwitchComponent } from "ng-zorro-antd/switch"; @UntilDestroy() @Component({ selector: "texera-agent-chat", templateUrl: "agent-chat.component.html", styleUrls: ["agent-chat.component.scss"], imports: [ ɵNzTransitionPatchDirective, NzIconDirective, NzTooltipDirective, NzSpaceCompactItemDirective, NzButtonComponent, NgIf, NgFor, MarkdownComponent, NzSpinComponent, NzInputDirective, FormsModule, NzAutosizeDirective, NzWaveDirective, ReActStepDetailModalComponent, NzModalComponent, NzModalContentDirective, NzTabsComponent, NzTabComponent, NzInputNumberComponent, NzTagComponent, NzInputGroupComponent, NzInputGroupWhitSuffixOrPrefixDirective, NzSwitchComponent, ], }) export class AgentChatComponent implements OnInit, AfterViewChecked, OnDestroy, OnChanges { @Input() agentInfo!: AgentInfo; @Input() isActive: boolean = false; @ViewChild("messageContainer", { static: false }) messageContainer?: ElementRef; @ViewChild("messageInput", { static: false }) messageInput?: ElementRef; /** All steps (for timeline rendering) */ public agentResponses: ReActStep[] = []; /** Steps on the HEAD path only (for chat rendering) */ public visibleSteps: ReActStep[] = []; public currentMessage = ""; private shouldScrollToBottom = false; public isDetailsModalVisible = false; public selectedResponse: ReActStep | null = null; public hoveredMessageIndex: number | null = null; public isSystemInfoModalVisible = false; public systemPrompt: string = ""; public availableTools: Array<{ name: string; description: string; inputSchema: any }> = []; public agentState: AgentState = AgentState.UNAVAILABLE; // Current HEAD step ID in the version tree public currentHeadId: string | null = null; // System info modal state public settingsMaxCharLimit = 20000; // Default max characters for operator results public settingsMaxCellCharLimit = 4000; // Default max characters per cell public settingsToolTimeoutSeconds = 120; // 2 minutes default public settingsExecutionTimeoutMinutes = 10; // 10 minutes default public settingsMaxSteps = 10; // Default max steps per message public settingsAllowedOperatorTypes: string[] = []; // Allowed operator types for general mode public allAvailableOperatorTypes: Array<{ type: string; description: string }> = []; // All operator types from backend public operatorTypeSearchQuery = ""; // Search filter for operator types // Track if we disabled auto-persist so we can re-enable it on destroy private disabledAutoPersist = false; // Subject to control workflow subscription lifecycle private stopWorkflowSubscription$ = new Subject(); constructor( private agentService: AgentService, private workflowActionService: WorkflowActionService, private notificationService: NotificationService, private cdr: ChangeDetectorRef, private workflowPersistService: WorkflowPersistService ) {} ngOnInit(): void { if (!this.agentInfo) { return; } // Ensure workflow polling is started if we have a workflowId // This handles agents created via API that weren't created through the UI const workflowId = this.agentInfo.delegate?.workflowId; if (workflowId) { this.agentService.ensureWorkflowPolling(this.agentInfo.id, workflowId); } // Get the current state from manager service this.agentService .getAgentState(this.agentInfo.id) .pipe(untilDestroyed(this)) .subscribe(state => { this.agentState = state; // Immediately trigger change detection to show the current state this.cdr.detectChanges(); }); // Then subscribe to agent state changes (BehaviorSubject will immediately emit current value) this.agentService .getAgentStateObservable(this.agentInfo.id) .pipe(untilDestroyed(this)) .subscribe(state => { this.agentState = state; // Force immediate change detection this.cdr.detectChanges(); }); // Subscribe to ReActSteps this.agentService .getReActStepsObservable(this.agentInfo.id) .pipe(untilDestroyed(this)) .subscribe(steps => { const previousLength = this.visibleSteps.length; this.agentResponses = steps; this.updateVisibleSteps(); this.shouldScrollToBottom = true; // Automatically highlight the latest visible step if (this.visibleSteps.length > 0) { const latestIndex = this.visibleSteps.length - 1; const previousLatestIndex = previousLength - 1; if ( this.hoveredMessageIndex === null || this.hoveredMessageIndex === previousLatestIndex || this.hoveredMessageIndex >= this.visibleSteps.length ) { this.setHoveredMessage(latestIndex); } } // Trigger change detection this.cdr.detectChanges(); }); // Subscribe to HEAD changes this.agentService .getHeadIdObservable(this.agentInfo.id) .pipe(untilDestroyed(this)) .subscribe(headId => { this.currentHeadId = headId; this.updateVisibleSteps(); this.cdr.detectChanges(); }); // Subscribe to agent state changes to manage auto-persist // Disable auto-persist when agent is GENERATING, re-enable when AVAILABLE this.agentService .getAgentStateObservable(this.agentInfo.id) .pipe(startWith(AgentState.UNAVAILABLE), pairwise(), untilDestroyed(this)) .subscribe(([previousState, currentState]) => { // When agent starts generating, disable auto-persist if (currentState === AgentState.GENERATING && previousState !== AgentState.GENERATING) { this.workflowPersistService.setWorkflowPersistFlag(false); this.disabledAutoPersist = true; } // When agent finishes (becomes AVAILABLE from GENERATING/STOPPING), re-enable auto-persist if ( currentState === AgentState.AVAILABLE && (previousState === AgentState.GENERATING || previousState === AgentState.STOPPING) ) { this.workflowPersistService.setWorkflowPersistFlag(true); this.disabledAutoPersist = false; } }); // Note: Workflow subscription is started/stopped via ngOnChanges based on isActive // This prevents automatic workflow switching when multiple agents are running // Start workflow subscription if already active if (this.isActive) { this.startWorkflowSubscription(); } // Subscribe to scroll-to-step requests this.agentService.scrollToStep$.pipe(untilDestroyed(this)).subscribe(({ agentId, messageId, stepId }) => { if (agentId === this.agentInfo.id) { this.scrollToStep(messageId, stepId); } }); } ngOnChanges(changes: SimpleChanges): void { if (changes["isActive"]) { if (this.isActive) { this.startWorkflowSubscription(); } else { this.stopWorkflowSubscription(); } } } /** * Start subscribing to workflow changes from the agent. * Only called when this agent tab is active. */ private startWorkflowSubscription(): void { if (!this.agentInfo) { return; } // Stop any existing subscription first this.stopWorkflowSubscription$.next(); this.agentService .getWorkflowObservable(this.agentInfo.id) .pipe( filter(workflow => workflow !== null), distinctUntilChanged((prev, curr) => { // Compare workflow content to avoid unnecessary reloads if (!prev || !curr) return false; return JSON.stringify(prev.content) === JSON.stringify(curr.content); }), takeUntil(this.stopWorkflowSubscription$), untilDestroyed(this) ) .subscribe(workflow => { if (workflow) { this.workflowActionService.reloadWorkflow(workflow, false, false); } }); } /** * Stop subscribing to workflow changes. * Called when switching away from this agent tab. */ private stopWorkflowSubscription(): void { this.stopWorkflowSubscription$.next(); } ngOnDestroy(): void { // Stop workflow subscription this.stopWorkflowSubscription$.next(); this.stopWorkflowSubscription$.complete(); // Re-enable auto-persist if we disabled it if (this.disabledAutoPersist) { this.workflowPersistService.setWorkflowPersistFlag(true); } } ngAfterViewChecked(): void { if (this.shouldScrollToBottom) { this.scrollToBottom(); this.shouldScrollToBottom = false; } } public setHoveredMessage(index: number | null): void { // When unhovered (null), automatically revert to latest step if (index === null && this.visibleSteps.length > 0) { index = this.visibleSteps.length - 1; } this.hoveredMessageIndex = index; const hoveredStep = index !== null && index >= 0 ? this.visibleSteps[index] : null; this.agentService.setHoveredMessage(this.agentInfo.id, hoveredStep); } public showResponseDetails(response: ReActStep): void { this.selectedResponse = response; this.isDetailsModalVisible = true; } public closeDetailsModal(): void { this.isDetailsModalVisible = false; this.selectedResponse = null; } public showSystemInfo(): void { this.refreshSystemInfo(); this.isSystemInfoModalVisible = true; } /** * Refresh system info from the agent. */ private refreshSystemInfo(): void { this.agentService .getSystemInfo(this.agentInfo.id) .pipe(untilDestroyed(this)) .subscribe(systemInfo => { this.systemPrompt = systemInfo.systemPrompt; this.availableTools = systemInfo.tools; }); // Fetch settings from server this.agentService .getAgentSettings(this.agentInfo.id) .pipe(untilDestroyed(this)) .subscribe(settings => { this.settingsMaxCharLimit = settings.maxOperatorResultCharLimit ?? 20000; this.settingsMaxCellCharLimit = settings.maxOperatorResultCellCharLimit ?? 4000; this.settingsToolTimeoutSeconds = settings.toolTimeoutSeconds ?? 120; this.settingsExecutionTimeoutMinutes = settings.executionTimeoutMinutes ?? 10; this.settingsMaxSteps = settings.maxSteps ?? 10; this.settingsAllowedOperatorTypes = settings.allowedOperatorTypes ?? []; }); // Fetch all available operator types this.agentService .getAvailableOperatorTypes(this.agentInfo.id) .pipe(untilDestroyed(this)) .subscribe(types => { this.allAvailableOperatorTypes = types.sort((a, b) => a.type.localeCompare(b.type)); }); } public closeSystemInfoModal(): void { this.isSystemInfoModalVisible = false; } public getToolResult(response: ReActStep, toolCallIndex: number): any { if (!response.toolResults || toolCallIndex >= response.toolResults.length) { return null; } const toolResult = response.toolResults[toolCallIndex]; return toolResult.output || toolResult.result || toolResult; } public getToolOperatorAccess( response: ReActStep, toolCallIndex: number ): { viewedOperatorIds: string[]; modifiedOperatorIds: string[] } | null { if (!response.operatorAccess) { return null; } return response.operatorAccess.get(toolCallIndex) || null; } public hasOperatorAccess(response: ReActStep): boolean { return !!response.operatorAccess && response.operatorAccess.size > 0; } public sendMessage(): void { if (!this.currentMessage.trim() || !this.canSendMessage()) { return; } const userMessage = this.currentMessage.trim(); this.currentMessage = ""; // Fire-and-forget; responses stream in via the WebSocket subscription. this.agentService.sendMessage(this.agentInfo.id, userMessage); } /** * Check if messages can be sent (only when agent is available). */ public canSendMessage(): boolean { return this.agentState === AgentState.AVAILABLE; } /** * Get the NG-ZORRO icon type based on current agent state. */ public getStateIcon(): string { switch (this.agentState) { case AgentState.AVAILABLE: return "check-circle"; case AgentState.GENERATING: case AgentState.STOPPING: return "sync"; case AgentState.UNAVAILABLE: default: return "close-circle"; } } /** * Get the icon color based on current agent state. */ public getStateIconColor(): string { switch (this.agentState) { case AgentState.AVAILABLE: return "#52c41a"; case AgentState.GENERATING: case AgentState.STOPPING: return "#1890ff"; case AgentState.UNAVAILABLE: default: return "#ff4d4f"; } } /** * Get the tooltip text for the state icon. */ public getStateTooltip(): string { switch (this.agentState) { case AgentState.AVAILABLE: return "Agent is ready"; case AgentState.GENERATING: return "Agent is generating response..."; case AgentState.STOPPING: return "Agent is stopping..."; case AgentState.UNAVAILABLE: return "Agent is unavailable"; default: return "Agent status unknown"; } } public onEnterPress(event: KeyboardEvent): void { if (!event.shiftKey) { event.preventDefault(); this.sendMessage(); } } private scrollToBottom(): void { if (this.messageContainer) { const element = this.messageContainer.nativeElement; element.scrollTop = element.scrollHeight; } } public stopGeneration(): void { this.agentService.stopGeneration(this.agentInfo.id); } public clearMessages(): void { this.agentService.clearMessages(this.agentInfo.id); } /** * Export the ReAct steps as a JSON file. * Fetches steps from the backend to get clean JSON (without Map objects). */ public exportReActSteps(): void { if (this.visibleSteps.length === 0) { this.notificationService.warning("No ReAct steps to export"); return; } this.agentService .getReActSteps(this.agentInfo.id) .pipe(untilDestroyed(this)) .subscribe({ next: (steps: ReActStep[]) => { // Convert steps to plain objects (handle Map -> object for operatorAccess) const exportSteps = steps.map(step => { const plain: any = { ...step }; if (step.operatorAccess) { const accessObj: Record = {}; step.operatorAccess.forEach((value, key) => { accessObj[key] = value; }); plain.operatorAccess = accessObj; } return plain; }); const exportData = { agentId: this.agentInfo.id, agentName: this.agentInfo.name, modelType: this.agentInfo.modelType, exportedAt: new Date().toISOString(), stepCount: exportSteps.length, steps: exportSteps, }; const jsonString = JSON.stringify(exportData, null, 2); const blob = new Blob([jsonString], { type: "application/json" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `${this.agentInfo.name}-react-steps-${new Date().toISOString().slice(0, 19).replace(/:/g, "-")}.json`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); this.notificationService.success(`Exported ${exportSteps.length} ReAct steps`); }, error: (err: unknown) => { console.error("Failed to export ReAct steps:", err); this.notificationService.error("Failed to export ReAct steps"); }, }); } public isGenerating(): boolean { return this.agentState === AgentState.GENERATING; } public isAvailable(): boolean { return this.agentState === AgentState.AVAILABLE; } public isConnected(): boolean { return this.agentState !== AgentState.UNAVAILABLE; } public isStopping(): boolean { return this.agentState === AgentState.STOPPING; } /** * Recompute visibleSteps: only steps on the ancestor path from root to HEAD. */ private updateVisibleSteps(): void { if (!this.currentHeadId || this.agentResponses.length === 0) { this.visibleSteps = this.agentResponses; return; } const stepMap = new Map(this.agentResponses.map(s => [s.id, s])); const ancestorIds = new Set(); let current: string | undefined = this.currentHeadId; while (current) { ancestorIds.add(current); current = stepMap.get(current)?.parentId; } this.visibleSteps = this.agentResponses.filter(s => ancestorIds.has(s.id)); } /** * Scroll chat messages to a specific step index. */ private scrollToMessage(stepIndex: number): void { if (!this.messageContainer) { return; } const container = this.messageContainer.nativeElement; const messages = container.querySelectorAll(".message"); if (stepIndex >= 0 && stepIndex < messages.length) { messages[stepIndex].scrollIntoView({ behavior: "smooth", block: "center" }); } } /** * Save the max character limit. */ public saveMaxCharLimit(): void { this.agentService .updateAgentSettings(this.agentInfo.id, { maxOperatorResultCharLimit: this.settingsMaxCharLimit, }) .pipe(untilDestroyed(this)) .subscribe({ next: () => this.notificationService.success("Max character limit saved"), error: () => {}, // Error already handled by service }); } /** * Save the max cell character limit. */ public saveMaxCellCharLimit(): void { this.agentService .updateAgentSettings(this.agentInfo.id, { maxOperatorResultCellCharLimit: this.settingsMaxCellCharLimit, }) .pipe(untilDestroyed(this)) .subscribe({ next: () => this.notificationService.success("Max cell character limit saved"), error: () => {}, // Error already handled by service }); } /** * Save the tool execution timeout. */ public saveToolTimeout(): void { this.agentService .updateAgentSettings(this.agentInfo.id, { toolTimeoutSeconds: this.settingsToolTimeoutSeconds, }) .pipe(untilDestroyed(this)) .subscribe({ next: () => this.notificationService.success("Tool timeout saved"), error: () => {}, // Error already handled by service }); } /** * Save the workflow execution timeout. */ public saveExecutionTimeout(): void { this.agentService .updateAgentSettings(this.agentInfo.id, { executionTimeoutMinutes: this.settingsExecutionTimeoutMinutes, }) .pipe(untilDestroyed(this)) .subscribe({ next: () => this.notificationService.success("Execution timeout saved"), error: () => {}, // Error already handled by service }); } /** * Save the max steps per message setting. */ public saveMaxSteps(): void { this.agentService .updateAgentSettings(this.agentInfo.id, { maxSteps: this.settingsMaxSteps, }) .pipe(untilDestroyed(this)) .subscribe({ next: () => this.notificationService.success("Max steps saved"), error: () => {}, // Error already handled by service }); } /** * Toggle an operator type in the allowed list and save. */ public toggleOperatorType(operatorType: string, enabled: boolean): void { if (enabled) { if (!this.settingsAllowedOperatorTypes.includes(operatorType)) { this.settingsAllowedOperatorTypes = [...this.settingsAllowedOperatorTypes, operatorType]; } } else { this.settingsAllowedOperatorTypes = this.settingsAllowedOperatorTypes.filter(t => t !== operatorType); } this.saveAllowedOperatorTypes(); } /** * Check if an operator type is enabled (in allowed list). */ public isOperatorTypeEnabled(operatorType: string): boolean { return this.settingsAllowedOperatorTypes.includes(operatorType); } /** * Enable all operator types. */ public enableAllOperatorTypes(): void { this.settingsAllowedOperatorTypes = this.allAvailableOperatorTypes.map(op => op.type); this.saveAllowedOperatorTypes(); } /** * Deselect all operator types. */ public deselectAllOperatorTypes(): void { this.settingsAllowedOperatorTypes = []; this.saveAllowedOperatorTypes(); } /** * Get filtered operator types based on search query. */ public getFilteredOperatorTypes(): Array<{ type: string; description: string }> { if (!this.operatorTypeSearchQuery) { return this.allAvailableOperatorTypes; } const query = this.operatorTypeSearchQuery.toLowerCase(); return this.allAvailableOperatorTypes.filter( op => op.type.toLowerCase().includes(query) || op.description.toLowerCase().includes(query) ); } /** * Save allowed operator types to backend. */ private saveAllowedOperatorTypes(): void { this.agentService .updateAgentSettings(this.agentInfo.id, { allowedOperatorTypes: this.settingsAllowedOperatorTypes, }) .pipe(untilDestroyed(this)) .subscribe({ next: () => { const count = this.settingsAllowedOperatorTypes.length; this.notificationService.success(count === 0 ? "All operators enabled" : `${count} operators enabled`); }, error: () => {}, }); } /** * Scroll to a specific step in the chat by messageId and stepId. */ private scrollToStep(messageId: string, stepId: number): void { // Find the step index in visibleSteps const stepIndex = this.visibleSteps.findIndex(step => step.messageId === messageId && step.stepId === stepId); if (stepIndex >= 0) { this.scrollToMessage(stepIndex); // Highlight the message briefly this.setHoveredMessage(stepIndex); } } } ================================================ FILE: frontend/src/app/workspace/component/agent/agent-panel/agent-panel.component.html ================================================

    AI Agents

    {{ agents.length }} agent(s)
    {{ agent.name }}
    ================================================ FILE: frontend/src/app/workspace/component/agent/agent-panel/agent-panel.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ :host { display: block; width: 100%; height: 100%; position: fixed; z-index: 3; } #agent-container { position: absolute; top: calc(-100% + 80px); right: 0; z-index: 3; background: white; } #title { padding: 5px 9px; border-bottom: 1px solid #e0e0e0; position: absolute; top: 0; background: white; width: 100%; z-index: 2; } #agent-docked-button { position: fixed; bottom: 40px; // Position above the mini-map button right: 10px; z-index: 4; box-shadow: 0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f; } #return-button { position: absolute; top: 0; right: 0; z-index: 3; display: flex; } #content { width: 100%; height: 100%; padding-top: 32px; display: inline-block; overflow-y: auto; } .shadow { border-radius: 5px; box-shadow: 0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f; } .ant-menu-item { margin: 0 !important; height: 32px; line-height: 32px; padding: 0 9px; } .agent-tabs { height: calc(100% - 32px); // Account for the title bar display: flex; flex-direction: column; overflow: hidden; ::ng-deep { .ant-tabs { height: 100%; display: flex; flex-direction: column; } .ant-tabs-nav { margin-bottom: 0; } .ant-tabs-content-holder { flex: 1; overflow: hidden; } .ant-tabs-content { height: 100%; } .ant-tabs-tabpane { height: 100%; overflow: hidden; padding: 0; } } } .agent-tab-title { display: flex; align-items: center; gap: 8px; .agent-tab-name { flex: 1; } // Visual feedback for agents on different workflows &.workflow-mismatch { opacity: 0.6; .agent-tab-name { color: #8c8c8c; } } .workflow-lock-icon { color: #faad14; font-size: 12px; margin-left: -4px; } } .agent-tab-close { padding: 0 !important; width: 20px !important; height: 20px !important; min-width: 20px !important; display: flex; align-items: center; justify-content: center; opacity: 0.6; margin-left: 4px; &:hover { opacity: 1; color: #ff4d4f !important; background: rgba(255, 77, 79, 0.1) !important; } i { font-size: 12px; } } .tab-bar-extra { padding-right: 8px; } .agent-count { font-size: 12px; color: #8c8c8c; padding: 4px 8px; background: #f0f0f0; border-radius: 4px; } ================================================ FILE: frontend/src/app/workspace/component/agent/agent-panel/agent-panel.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, HostListener, Input, OnDestroy, OnInit, OnChanges, SimpleChanges } from "@angular/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { NzResizeEvent, NzResizableDirective, NzResizeHandlesComponent } from "ng-zorro-antd/resizable"; import { AgentService, AgentInfo } from "../../../service/agent/agent.service"; import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; import { NotificationService } from "../../../../common/service/notification/notification.service"; import { calculateTotalTranslate3d } from "../../../../common/util/panel-dock"; import { NgIf, NgClass, NgFor } from "@angular/common"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { CdkDrag, CdkDragHandle } from "@angular/cdk/drag-drop"; import { NzMenuDirective, NzMenuItemComponent } from "ng-zorro-antd/menu"; import { NzTabsComponent, NzTabBarExtraContentDirective, NzTabComponent, NzTabDirective } from "ng-zorro-antd/tabs"; import { AgentRegistrationComponent } from "./agent-registration/agent-registration.component"; import { AgentChatComponent } from "./agent-chat/agent-chat.component"; import { FormlyRepeatDndComponent } from "../../../../common/formly/repeat-dnd/repeat-dnd.component"; @UntilDestroy() @Component({ selector: "texera-agent-panel", templateUrl: "agent-panel.component.html", styleUrls: ["agent-panel.component.scss"], imports: [ NgIf, NzSpaceCompactItemDirective, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, NzTooltipDirective, NzIconDirective, CdkDrag, NzResizableDirective, NzMenuDirective, NgClass, NzMenuItemComponent, CdkDragHandle, NzTabsComponent, NzTabBarExtraContentDirective, NzTabComponent, NzTabDirective, AgentRegistrationComponent, NgFor, AgentChatComponent, NzResizeHandlesComponent, FormlyRepeatDndComponent, ], }) export class AgentPanelComponent implements OnInit, OnDestroy, OnChanges { protected readonly window = window; private static readonly MIN_PANEL_WIDTH = 400; private static readonly MIN_PANEL_HEIGHT = 450; /** * Optional agent ID to activate when the panel loads. * When provided (from agent dashboard), the panel will open * and switch to this agent's tab automatically. */ @Input() agentIdToActivate?: string; // Panel dimensions and position width: number = 0; // Start with 0 to show docked button height = Math.max(AgentPanelComponent.MIN_PANEL_HEIGHT, window.innerHeight * 0.7); id = -1; dragPosition = { x: 0, y: 0 }; returnPosition = { x: 0, y: 0 }; isDocked = true; // Tab management selectedTabIndex: number = 0; // 0 = registration tab, 1+ = agent tabs agents: AgentInfo[] = []; // Active agent tracking - only one agent can be connected at a time activeAgentId: string | null = null; constructor( private agentService: AgentService, private workflowActionService: WorkflowActionService, private notificationService: NotificationService ) {} ngOnInit(): void { this.loadPanelSettings(); // Subscribe to agent changes this.agentService.agentChange$.pipe(untilDestroyed(this)).subscribe(() => { this.agentService .getAllAgents() .pipe(untilDestroyed(this)) .subscribe(agents => { this.agents = agents; // Try to activate the agent if agentIdToActivate is set this.tryActivateAgentFromInput(); }); }); // Load initial agents this.agentService .getAllAgents() .pipe(untilDestroyed(this)) .subscribe(agents => { this.agents = agents; // Try to activate the agent if agentIdToActivate is set this.tryActivateAgentFromInput(); }); } ngOnChanges(changes: SimpleChanges): void { if (changes["agentIdToActivate"] && this.agentIdToActivate) { this.tryActivateAgentFromInput(); } } /** * Try to activate the agent specified by agentIdToActivate input. * Opens the panel and switches to the agent's tab. */ private tryActivateAgentFromInput(): void { if (!this.agentIdToActivate || this.agents.length === 0) { return; } const agentIndex = this.agents.findIndex(agent => agent.id === this.agentIdToActivate); if (agentIndex === -1) { return; } // Open the panel if it's closed if (this.width === 0) { this.width = AgentPanelComponent.MIN_PANEL_WIDTH; } // Switch to the agent's tab and activate it const agent = this.agents[agentIndex]; // Deactivate previous agent if any if (this.activeAgentId) { this.agentService.deactivateAgent(this.activeAgentId); } // Activate the specified agent this.activeAgentId = agent.id; this.agentService.activateAgent(agent.id); this.selectedTabIndex = agentIndex + 1; // +1 because tab 0 is registration // Clear the input so we don't re-activate on every change this.agentIdToActivate = undefined; } @HostListener("window:beforeunload") ngOnDestroy(): void { // Deactivate any active agent before destroying this.deactivateCurrentAgent(); this.savePanelSettings(); } /** * Open the panel from docked state */ public openPanel(): void { if (this.width === 0) { // Open panel this.width = AgentPanelComponent.MIN_PANEL_WIDTH; } else { // Close panel (dock it) this.width = 0; this.isDocked = true; } } /** * Handle agent creation - activates and switches to the new agent */ public onAgentCreated(agentId: string): void { // Deactivate previous agent if any if (this.activeAgentId) { this.agentService.deactivateAgent(this.activeAgentId); } // Set the new agent as active immediately this.activeAgentId = agentId; this.agentService.activateAgent(agentId); // Fetch the latest agent list and switch to the new agent's tab this.agentService .getAllAgents() .pipe(untilDestroyed(this)) .subscribe(agents => { this.agents = agents; const agentIndex = agents.findIndex(agent => agent.id === agentId); if (agentIndex !== -1) { this.selectedTabIndex = agentIndex + 1; // +1 because tab 0 is registration } }); } /** * Handle tab selection change - validates workflow compatibility before switching */ public onTabSelectChange(index: number): void { // Tab 0 is registration - always allow if (index === 0) { this.deactivateCurrentAgent(); this.selectedTabIndex = 0; return; } // Get the agent for this tab (index - 1 because tab 0 is registration) const agentIndex = index - 1; if (agentIndex < 0 || agentIndex >= this.agents.length) { return; } const agent = this.agents[agentIndex]; const agentWorkflowId = agent.delegate?.workflowId; const currentWorkflowId = this.workflowActionService.getWorkflowMetadata().wid; // If agent has a workflow ID, check if it matches the current workflow if (agentWorkflowId !== undefined && agentWorkflowId !== 0) { if (currentWorkflowId !== agentWorkflowId) { // Block switching - workflow mismatch this.notificationService.warning( `Cannot switch to agent "${agent.name}": It's working on a different workflow. ` + `Open workflow #${agentWorkflowId} to interact with this agent.` ); return; } } // Workflow matches or agent has no workflow - allow switch this.switchToAgent(agent.id, index); } /** * Switch to a specific agent tab */ private switchToAgent(agentId: string, tabIndex: number): void { // Skip if already on this agent and tab if (this.activeAgentId === agentId && this.selectedTabIndex === tabIndex) { return; } // Deactivate previous agent only if switching to a different agent if (this.activeAgentId !== agentId) { this.deactivateCurrentAgent(); } // Activate new agent this.activeAgentId = agentId; this.agentService.activateAgent(agentId); this.selectedTabIndex = tabIndex; } /** * Deactivate the currently active agent */ private deactivateCurrentAgent(): void { if (this.activeAgentId) { this.agentService.deactivateAgent(this.activeAgentId); this.activeAgentId = null; } } /** * Check if an agent's workflow matches the current workspace workflow */ public canSwitchToAgent(agent: AgentInfo): boolean { const agentWorkflowId = agent.delegate?.workflowId; if (agentWorkflowId === undefined || agentWorkflowId === 0) { return true; // Agent has no workflow - always allow } const currentWorkflowId = this.workflowActionService.getWorkflowMetadata().wid; return currentWorkflowId === agentWorkflowId; } /** * Delete an agent */ public deleteAgent(agentId: string, event: Event): void { event.stopPropagation(); // Prevent tab switch if (confirm("Are you sure you want to delete this agent?")) { const agentIndex = this.agents.findIndex(agent => agent.id === agentId); // Deactivate if this is the active agent if (this.activeAgentId === agentId) { this.deactivateCurrentAgent(); } // Must subscribe to the observable for it to execute this.agentService .deleteAgent(agentId) .pipe(untilDestroyed(this)) .subscribe({ next: () => { // If we're on the deleted agent's tab, switch to registration if (agentIndex !== -1 && this.selectedTabIndex === agentIndex + 1) { this.selectedTabIndex = 0; } else if (this.selectedTabIndex > agentIndex + 1) { // Adjust selected index if we deleted a tab before the current one this.selectedTabIndex--; } }, error: (error: unknown) => { console.error("Failed to delete agent:", error); }, }); } } /** * Handle panel resize */ onResize({ width, height }: NzResizeEvent): void { cancelAnimationFrame(this.id); this.id = requestAnimationFrame(() => { this.width = width!; this.height = height!; }); } /** * Handle drag start */ handleDragStart(): void { this.isDocked = false; } /** * Load panel settings from localStorage */ private loadPanelSettings(): void { const savedWidth = localStorage.getItem("agent-panel-width"); const savedHeight = localStorage.getItem("agent-panel-height"); const savedStyle = localStorage.getItem("agent-panel-style"); const savedDocked = localStorage.getItem("agent-panel-docked"); // Only restore width if the panel was not docked if (savedDocked === "false" && savedWidth) { const parsedWidth = Number(savedWidth); if (!isNaN(parsedWidth) && parsedWidth >= AgentPanelComponent.MIN_PANEL_WIDTH) { this.width = parsedWidth; } } if (savedHeight) { const parsedHeight = Number(savedHeight); if (!isNaN(parsedHeight) && parsedHeight >= AgentPanelComponent.MIN_PANEL_HEIGHT) { this.height = parsedHeight; } } if (savedStyle) { const container = document.getElementById("agent-container"); if (container) { container.style.cssText = savedStyle; const translates = container.style.transform; const [xOffset, yOffset] = calculateTotalTranslate3d(translates); this.returnPosition = { x: -xOffset, y: -yOffset }; this.isDocked = this.dragPosition.x === this.returnPosition.x && this.dragPosition.y === this.returnPosition.y; } } } /** * Save panel settings to localStorage */ private savePanelSettings(): void { localStorage.setItem("agent-panel-width", String(this.width)); localStorage.setItem("agent-panel-height", String(this.height)); localStorage.setItem("agent-panel-docked", String(this.width === 0)); const container = document.getElementById("agent-container"); if (container) { localStorage.setItem("agent-panel-style", container.style.cssText); } } } ================================================ FILE: frontend/src/app/workspace/component/agent/agent-panel/agent-registration/agent-registration.component.html ================================================

    Welcome to Texera Agent!

    Select a model type and create an AI agent to assist with your data science tasks

    Select Model Type

    Loading available models...

    No models available

    {{ modelType.name }}

    {{ modelType.description }}

    Agent Name (Optional)

    ================================================ FILE: frontend/src/app/workspace/component/agent/agent-panel/agent-registration/agent-registration.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .agent-registration-container { display: flex; flex-direction: column; gap: 24px; padding: 20px; height: 100%; overflow-y: auto; } .registration-header { text-align: center; h3 { margin: 0 0 8px 0; font-size: 20px; font-weight: 600; color: #262626; } p { margin: 0; font-size: 14px; color: #8c8c8c; } } .model-type-selection { h4 { margin: 0 0 12px 0; font-size: 16px; font-weight: 500; color: #262626; } } .model-cards { display: flex; flex-direction: column; gap: 12px; } .model-card { display: flex; align-items: center; gap: 16px; padding: 16px; border: 2px solid #d9d9d9; border-radius: 8px; cursor: pointer; transition: all 0.3s ease; position: relative; &:hover { border-color: #1890ff; box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2); } &.selected { border-color: #1890ff; background: #e6f7ff; box-shadow: 0 2px 8px rgba(24, 144, 255, 0.3); } } .model-icon { display: flex; align-items: center; justify-content: center; width: 48px; height: 48px; background: #f0f0f0; border-radius: 8px; flex-shrink: 0; i { font-size: 28px; color: #1890ff; } } .model-info { flex: 1; h5 { margin: 0 0 4px 0; font-size: 15px; font-weight: 600; color: #262626; } p { margin: 0; font-size: 13px; color: #8c8c8c; line-height: 1.4; } } .selected-indicator { flex-shrink: 0; i { font-size: 24px; color: #1890ff; } } .agent-name-input { h4 { margin: 0 0 8px 0; font-size: 16px; font-weight: 500; color: #262626; } input { width: 100%; } } .action-buttons { display: flex; justify-content: center; button { min-width: 200px; } } ================================================ FILE: frontend/src/app/workspace/component/agent/agent-panel/agent-registration/agent-registration.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, EventEmitter, OnDestroy, OnInit, Output } from "@angular/core"; import { AgentService, ModelType } from "../../../../service/agent/agent.service"; import { NotificationService } from "../../../../../common/service/notification/notification.service"; import { WorkflowActionService } from "../../../../service/workflow-graph/model/workflow-action.service"; import { ComputingUnitStatusService } from "../../../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; import { ComputingUnitState } from "../../../../../common/type/computing-unit-connection.interface"; import { Subject, takeUntil } from "rxjs"; import { NgIf, NgFor } from "@angular/common"; import { NzSpinComponent } from "ng-zorro-antd/spin"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzInputDirective } from "ng-zorro-antd/input"; import { FormsModule } from "@angular/forms"; import { NzAlertComponent } from "ng-zorro-antd/alert"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; @Component({ selector: "texera-agent-registration", templateUrl: "agent-registration.component.html", styleUrls: ["agent-registration.component.scss"], imports: [ NgIf, NzSpinComponent, ɵNzTransitionPatchDirective, NzIconDirective, NgFor, NzSpaceCompactItemDirective, NzInputDirective, FormsModule, NzAlertComponent, NzButtonComponent, NzWaveDirective, NzTooltipDirective, ], }) export class AgentRegistrationComponent implements OnInit, OnDestroy { @Output() agentCreated = new EventEmitter(); public modelTypes: ModelType[] = []; public selectedModelType: string | null = null; public customAgentName: string = "Texera Agent"; public isLoadingModels: boolean = false; public hasLoadingError: boolean = false; public computingUnitConnected: boolean = false; public isCreating: boolean = false; private destroy$ = new Subject(); constructor( private agentService: AgentService, private notificationService: NotificationService, private workflowActionService: WorkflowActionService, private computingUnitStatusService: ComputingUnitStatusService ) {} ngOnInit(): void { this.isLoadingModels = true; this.hasLoadingError = false; this.computingUnitStatusService .getStatus() .pipe(takeUntil(this.destroy$)) .subscribe(status => { this.computingUnitConnected = status === ComputingUnitState.Running; }); this.agentService .fetchModelTypes() .pipe(takeUntil(this.destroy$)) .subscribe({ next: models => { this.modelTypes = models; this.isLoadingModels = false; if (models.length === 0) { this.hasLoadingError = true; this.notificationService.error("No models available. Please check the LiteLLM configuration."); } }, error: (error: unknown) => { this.isLoadingModels = false; this.hasLoadingError = true; const errorMessage = error instanceof Error ? error.message : String(error); this.notificationService.error(`Failed to fetch models: ${errorMessage}`); }, }); } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } public selectModelType(modelTypeId: string): void { this.selectedModelType = modelTypeId; } public createAgent(): void { if (!this.selectedModelType || this.isCreating) { return; } this.isCreating = true; const workflowMetadata = this.workflowActionService.getWorkflowMetadata(); const workflowId = workflowMetadata?.wid; this.agentService .createAgent(this.selectedModelType!, this.customAgentName || undefined, workflowId) .pipe(takeUntil(this.destroy$)) .subscribe({ next: agentInfo => { this.agentCreated.emit(agentInfo.id); this.resetForm(); }, error: (error: unknown) => { this.notificationService.error(`Failed to create agent: ${error}`); this.isCreating = false; }, }); } private resetForm(): void { this.selectedModelType = null; this.customAgentName = ""; this.isCreating = false; } public canCreate(): boolean { return this.selectedModelType !== null && !this.isCreating && this.computingUnitConnected; } } ================================================ FILE: frontend/src/app/workspace/component/agent/agent-panel/react-step-detail-modal/react-step-detail-modal.component.html ================================================

    Step Identification

    {{ step.messageId }} {{ step.stepId }} {{ step.timestamp | date: 'yyyy-MM-dd HH:mm:ss.SSS' }}

    Content

    {{ step.content }}

    Token Usage

    {{ step.usage.inputTokens || 0 }} {{ step.usage.outputTokens || 0 }} {{ step.usage.totalTokens || 0 }} {{ step.usage.cachedInputTokens || 0 }}

    Input Messages ({{ step.inputMessages.length }})

    user {{ getTextFromMessage(msg).length > 120 ? (getTextFromMessage(msg) | slice: 0:120) + '...' : getTextFromMessage(msg) }} assistant {{ getTextFromMessage(msg).length > 80 ? (getTextFromMessage(msg) | slice: 0:80) + '...' : getTextFromMessage(msg) }} {{ call.toolName }} ({{ call.operatorId }}) tool-result {{ item.toolName }} ~{{ item.tokenCount }} tokens TRIMMED
    {{ getTextFromMessage(msg) }}
    Text:
    {{ getTextFromMessage(msg) }}
    {{ call.toolName }} arguments:
    {{ formatJson(call.fullArgs) }}
    {{ item.toolName }} result: ~{{ item.tokenCount }} tokens TRIMMED
    {{ item.resultContent }}

    Tool Calls ({{ step.toolCalls.length }})

    Arguments:
    {{ formatJson(call.input || call.args) }}
    Result:
    {{ formatResult(getToolResult(step, idx)) }}
    Operator Access:
    VIEWED: {{ opId }}
    ADDED: {{ opId }}
    MODIFIED: {{ opId }}
    No additional details available for this step.
    ================================================ FILE: frontend/src/app/workspace/component/agent/agent-panel/react-step-detail-modal/react-step-detail-modal.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Styles for ReActStep detail modal ================================================ FILE: frontend/src/app/workspace/component/agent/agent-panel/react-step-detail-modal/react-step-detail-modal.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, Input, Output, EventEmitter } from "@angular/core"; import { ReActStep } from "../../../../service/agent/agent-types"; import { NzModalComponent, NzModalContentDirective } from "ng-zorro-antd/modal"; import { NgIf, NgFor, SlicePipe, DatePipe } from "@angular/common"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzDescriptionsComponent, NzDescriptionsItemComponent } from "ng-zorro-antd/descriptions"; import { NzTagComponent } from "ng-zorro-antd/tag"; import { NzCollapseComponent, NzCollapsePanelComponent } from "ng-zorro-antd/collapse"; /** * Reusable modal component for displaying ReActStep details. * Shows step identification, token usage, and tool calls. */ @Component({ selector: "texera-react-step-detail-modal", templateUrl: "./react-step-detail-modal.component.html", styleUrls: ["./react-step-detail-modal.component.scss"], imports: [ NzModalComponent, NzModalContentDirective, NgIf, ɵNzTransitionPatchDirective, NzIconDirective, NzDescriptionsComponent, NzDescriptionsItemComponent, NzTagComponent, NzCollapseComponent, NgFor, NzCollapsePanelComponent, SlicePipe, DatePipe, ], }) export class ReActStepDetailModalComponent { @Input() visible: boolean = false; @Input() step: ReActStep | null = null; @Input() agentId: string | null = null; @Output() visibleChange = new EventEmitter(); public closeModal(): void { this.visible = false; this.visibleChange.emit(false); } /** * Format data for display. * If the data is a string, return it as-is (with newlines preserved). * If it's an object, JSON.stringify it with formatting. */ public formatResult(data: any): string { if (typeof data === "string") { return data; } return JSON.stringify(data, null, 2); } public formatJson(data: any): string { return JSON.stringify(data, null, 2); } public getToolResult(step: ReActStep, toolCallIndex: number): any { if (!step.toolResults || toolCallIndex >= step.toolResults.length) { return null; } const toolResult = step.toolResults[toolCallIndex]; return toolResult.output || toolResult.result || toolResult; } public getToolOperatorAccess( step: ReActStep, toolCallIndex: number ): { viewedOperatorIds: string[]; addedOperatorIds: string[]; modifiedOperatorIds: string[] } | null { if (!step.operatorAccess) { return null; } return step.operatorAccess.get(toolCallIndex) || null; } public hasOperatorAccess(step: ReActStep): boolean { return !!step.operatorAccess && step.operatorAccess.size > 0; } /** * Get tag color for a message role. */ public getMessageRoleColor(role: string): string { switch (role) { case "user": return "blue"; case "assistant": return "orange"; case "tool": return "green"; default: return "default"; } } // ============================================================================ // Input Messages helpers // ============================================================================ /** * Get text content from a message (user or assistant text parts). */ public getTextFromMessage(msg: any): string { if (!msg?.content) return ""; if (typeof msg.content === "string") return msg.content; if (Array.isArray(msg.content)) { return msg.content .filter((p: any) => p.type === "text") .map((p: any) => p.text || "") .join("\n"); } return ""; } /** * Get tool call summaries from an assistant message. * Returns array of { toolName, operatorId, fullArgs } for display. */ public getToolCallSummaries(msg: any): { toolName: string; operatorId: string; fullArgs: any }[] { if (!msg?.content || !Array.isArray(msg.content)) return []; return msg.content .filter((p: any) => p.type === "tool-call") .map((p: any) => { const args = p.args || p.input || {}; return { toolName: p.toolName, operatorId: args.operatorId || "", fullArgs: args, }; }); } /** * Get tool calls from an assistant message, formatted as function-call strings. */ public getToolCallStrings(msg: any): string[] { if (!msg?.content || !Array.isArray(msg.content)) return []; return msg.content.filter((p: any) => p.type === "tool-call").map((p: any) => this.formatAsFunction(p)); } /** * Format a tool-call part as function-call notation: toolName(key=val, key=val) */ private formatAsFunction(part: any): string { const args = part.args || part.input || {}; const params = Object.entries(args) .map(([k, v]) => { let val: string; if (typeof v === "string") { val = v.length > 60 ? `"${v.substring(0, 60)}..."` : `"${v}"`; } else { const s = JSON.stringify(v); val = s.length > 60 ? s.substring(0, 60) + "..." : s; } return `${k}=${val}`; }) .join(", "); return `${part.toolName}(${params})`; } /** * Build a toolCallId → toolName map from all input messages. */ private buildToolCallNameMap(messages: any[]): Map { const map = new Map(); for (const msg of messages) { if (msg.role === "assistant" && Array.isArray(msg.content)) { for (const part of msg.content) { if (part.type === "tool-call") { map.set(part.toolCallId, part.toolName); } } } } return map; } /** * Get full tool result content items for expanded view. * Each item includes: toolName, resultContent string, approximate token count, and whether it was trimmed. */ public getToolResultFullItems( msg: any ): { toolName: string; resultContent: string; tokenCount: number; isTrimmed: boolean }[] { if (!msg?.content || !Array.isArray(msg.content)) return []; const nameMap = this.step?.inputMessages ? this.buildToolCallNameMap(this.step.inputMessages) : new Map(); return msg.content .filter((p: any) => p.type === "tool-result") .map((p: any) => { const raw = p.result ?? p.output ?? p.content ?? ""; const resultStr = typeof raw === "string" ? raw : JSON.stringify(raw, null, 2); return { toolName: nameMap.get(p.toolCallId) || p.toolCallId, resultContent: resultStr, tokenCount: Math.ceil(resultStr.length / 4), isTrimmed: resultStr.includes("context compaction"), }; }); } /** * Get structured tool results from a tool message. * Each result includes: toolName, approximate token count, and whether it was trimmed. */ public getToolResultItems(msg: any): { toolName: string; tokenCount: number; isTrimmed: boolean }[] { if (!msg?.content || !Array.isArray(msg.content)) return []; const nameMap = this.step?.inputMessages ? this.buildToolCallNameMap(this.step.inputMessages) : new Map(); return msg.content .filter((p: any) => p.type === "tool-result") .map((p: any) => { const raw = p.result ?? p.output ?? p.content ?? ""; const resultStr = typeof raw === "string" ? raw : JSON.stringify(raw); return { toolName: nameMap.get(p.toolCallId) || p.toolCallId, tokenCount: Math.ceil(resultStr.length / 4), isTrimmed: resultStr.includes("context compaction"), }; }); } } ================================================ FILE: frontend/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.html ================================================

    Do you agree with the type annotation suggestion?

    Adding annotation for code: {{ code }}

    Given suggestion: {{ suggestion }}

    ================================================ FILE: frontend/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .annotation-suggestion { position: absolute; background: #222; color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5); z-index: 1000; } .annotation-suggestion button { margin-right: 10px; } .annotation-suggestion button.accept-button { background-color: #28a745; color: #000; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; } .annotation-suggestion button.accept-button:hover { background-color: #218838; } .annotation-suggestion button.decline-button { background-color: #dc3545; color: #000; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; } .annotation-suggestion button.decline-button:hover { background-color: #c82333; } ================================================ FILE: frontend/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { AnnotationSuggestionComponent } from "./annotation-suggestion.component"; describe("AnnotationSuggestionComponent", () => { let component: AnnotationSuggestionComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [AnnotationSuggestionComponent], }).compileComponents(); fixture = TestBed.createComponent(AnnotationSuggestionComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it("creates", () => { expect(component).toBeTruthy(); }); it("defaults its inputs to empty / zero", () => { expect(component.code).toBe(""); expect(component.suggestion).toBe(""); expect(component.top).toBe(0); expect(component.left).toBe(0); }); it("emits accept when onAccept is called", () => { const spy = vi.fn(); component.accept.subscribe(spy); component.onAccept(); expect(spy).toHaveBeenCalledTimes(1); }); it("emits decline when onDecline is called", () => { const spy = vi.fn(); component.decline.subscribe(spy); component.onDecline(); expect(spy).toHaveBeenCalledTimes(1); }); it("emits accept and decline independently — onAccept does not fire decline, and vice versa", () => { const acceptSpy = vi.fn(); const declineSpy = vi.fn(); component.accept.subscribe(acceptSpy); component.decline.subscribe(declineSpy); component.onAccept(); expect(acceptSpy).toHaveBeenCalledTimes(1); expect(declineSpy).not.toHaveBeenCalled(); component.onDecline(); expect(acceptSpy).toHaveBeenCalledTimes(1); expect(declineSpy).toHaveBeenCalledTimes(1); }); it("respects the latest values bound to its @Input fields", () => { component.code = "x = 1"; component.suggestion = ": int"; component.top = 50; component.left = 75; fixture.detectChanges(); expect(component.code).toBe("x = 1"); expect(component.suggestion).toBe(": int"); expect(component.top).toBe(50); expect(component.left).toBe(75); }); }); ================================================ FILE: frontend/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, EventEmitter, Input, Output } from "@angular/core"; @Component({ selector: "texera-annotation-suggestion", templateUrl: "./annotation-suggestion.component.html", styleUrls: ["./annotation-suggestion.component.scss"], }) export class AnnotationSuggestionComponent { @Input() code: string = ""; @Input() suggestion: string = ""; @Input() top: number = 0; @Input() left: number = 0; @Output() accept = new EventEmitter(); @Output() decline = new EventEmitter(); onAccept() { this.accept.emit(); } onDecline() { this.decline.emit(); } } ================================================ FILE: frontend/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.html ================================================
    Condition on line {{ lineNum }}:
    {{ conditionTextarea.focus() }}
    ================================================ FILE: frontend/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .condition-input-popup { position: absolute; background: #333; color: #fff; padding: 5px; /* Larger padding for better UX */ border-radius: 3px; /* Smoother rounded corners */ z-index: 1000; pointer-events: auto; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); display: flex; flex-direction: column; align-items: flex-start; width: 240px; opacity: 1; transition: opacity 1s ease-in-out; } .condition-input-popup .tooltip-header { font-weight: bold; margin-bottom: 5px; white-space: nowrap; flex-shrink: 0; } .condition-input-popup .condition-textarea { width: 100%; height: 50px; resize: vertical; background-color: transparent; color: white; border: 1px solid #ccc; padding: 5px; } .condition-input-popup.fade-out { opacity: 0; pointer-events: none; } ::ng-deep .cgmr.codicon.monaco-conditional-breakpoint { width: 10px !important; height: 10px !important; border-radius: 100%; background-color: #d47d78; margin: 4px 0 0 8px; cursor: pointer; color: #000; text-align: center; font-size: 8px; font-weight: bold; } ::ng-deep .cgmr.codicon.monaco-conditional-breakpoint::before { content: "?"; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } ================================================ FILE: frontend/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { CommonModule } from "@angular/common"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { FormsModule } from "@angular/forms"; import { BreakpointConditionInputComponent } from "./breakpoint-condition-input.component"; import { UdfDebugService } from "../../../service/operator-debug/udf-debug.service"; import { SimpleChanges } from "@angular/core"; import { commonTestProviders } from "../../../../common/testing/test-utils"; import type { Mocked } from "vitest"; import type { editor } from "monaco-editor"; describe("BreakpointConditionInputComponent", () => { let component: BreakpointConditionInputComponent; let fixture: ComponentFixture; let mockUdfDebugService: Mocked; beforeEach(async () => { // Create a mock UdfDebugService mockUdfDebugService = { getCondition: vi.fn(), doUpdateBreakpointCondition: vi.fn(), } as unknown as Mocked; await TestBed.configureTestingModule({ imports: [BreakpointConditionInputComponent, CommonModule, FormsModule], providers: [{ provide: UdfDebugService, useValue: mockUdfDebugService }, ...commonTestProviders], }).compileComponents(); fixture = TestBed.createComponent(BreakpointConditionInputComponent); component = fixture.componentInstance; component.monacoEditor = { getLayoutInfo: () => ({ glyphMarginLeft: 10 }), getDomNode: () => ({ getBoundingClientRect: () => ({ top: 20, left: 30 }), }) as HTMLDivElement, getBottomForLineNumber: () => 40, getScrollTop: () => 5, getScrollLeft: () => 0, dispose: vi.fn(), } as unknown as editor.IStandaloneCodeEditor; // Set required inputs component.operatorId = "test-operator"; component.lineNum = 1; fixture.detectChanges(); // Trigger Angular's change detection }); afterEach(() => { // Clean up the editor and DOM element after each test component.monacoEditor.dispose(); component.closeEmitter.emit(); }); it("should create the component", () => { expect(component).toBeTruthy(); }); it("should update the condition when lineNum changes", () => { mockUdfDebugService.getCondition.mockReturnValue("existing condition"); const changes: SimpleChanges = { lineNum: { currentValue: 2, previousValue: 1, firstChange: false, isFirstChange: () => false, }, }; component.ngOnChanges(changes); expect(component.condition).toBe("existing condition"); }); it("should handle Enter key event and save the condition", () => { const emitSpy = vi.spyOn(component.closeEmitter, "emit"); const event = new KeyboardEvent("keydown", { key: "Enter" }); component.condition = " new condition "; component.handleEvent(event); expect(mockUdfDebugService.doUpdateBreakpointCondition).toHaveBeenCalledWith("test-operator", 1, "new condition"); expect(emitSpy).toHaveBeenCalled(); }); it("should not handle Enter key event if shift key is pressed", () => { const emitSpy = vi.spyOn(component.closeEmitter, "emit"); const event = new KeyboardEvent("keydown", { key: "Enter", shiftKey: true }); component.handleEvent(event); expect(mockUdfDebugService.doUpdateBreakpointCondition).not.toHaveBeenCalled(); expect(emitSpy).not.toHaveBeenCalled(); }); it("should emit close event on focusout", () => { const emitSpy = vi.spyOn(component.closeEmitter, "emit"); component.handleEvent(); // Simulate focusout expect(emitSpy).toHaveBeenCalled(); }); }); ================================================ FILE: frontend/src/app/workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, EventEmitter, HostListener, Input, OnChanges, Output, SimpleChanges } from "@angular/core"; import { editor } from "monaco-editor"; import { UdfDebugService } from "../../../service/operator-debug/udf-debug.service"; import { isDefined } from "../../../../common/util/predicate"; import { NgIf, NgStyle } from "@angular/common"; import { FormsModule } from "@angular/forms"; import { FormlyRepeatDndComponent } from "../../../../common/formly/repeat-dnd/repeat-dnd.component"; type MonacoEditor = editor.IStandaloneCodeEditor; /** * This component is a dialog that allows users to input a condition for a breakpoint. */ @Component({ selector: "texera-breakpoint-condition-input", templateUrl: "./breakpoint-condition-input.component.html", styleUrls: ["./breakpoint-condition-input.component.scss"], imports: [NgIf, NgStyle, FormsModule, FormlyRepeatDndComponent], }) export class BreakpointConditionInputComponent implements OnChanges { constructor(private udfDebugService: UdfDebugService) {} @Input() operatorId = ""; @Input() lineNum?: number; @Input() monacoEditor!: MonacoEditor; @Output() closeEmitter = new EventEmitter(); public condition = ""; public topPosition: string = "0px"; public leftPosition: string = "0px"; ngOnChanges(changes: SimpleChanges): void { if (!isDefined(changes["lineNum"]?.currentValue)) { return; } // when the line number changes, update the condition this.condition = this.udfDebugService.getCondition(this.operatorId, this.lineNum!) ?? ""; // update position const layoutInfo = this.monacoEditor.getLayoutInfo(); const editorRect = this.monacoEditor.getDomNode()?.getBoundingClientRect(); const topValue = (editorRect?.top || 0) + this.monacoEditor.getBottomForLineNumber(this.lineNum!) - this.monacoEditor.getScrollTop(); const leftValue = (editorRect?.left || 0) + (layoutInfo?.glyphMarginLeft || 0) - 160; this.topPosition = `${topValue}px`; this.leftPosition = `${leftValue}px`; } public left(): number { if (!isDefined(this.monacoEditor)) { return 0; } // Calculate the left position of the input popup based on the editor layout const { glyphMarginLeft } = this.monacoEditor.getLayoutInfo()!; const { left } = this.monacoEditor.getDomNode()!.getBoundingClientRect(); return left + glyphMarginLeft - this.monacoEditor.getScrollLeft() - 160; } public top(): number { if (!(isDefined(this.monacoEditor) && isDefined(this.lineNum))) { return 0; } // Calculate the top position of the input popup based on the editor layout const topPixel = this.monacoEditor.getBottomForLineNumber(this.lineNum); const editorRect = this.monacoEditor.getDomNode()?.getBoundingClientRect(); return (editorRect?.top || 0) + topPixel - this.monacoEditor.getScrollTop(); } get isVisible(): boolean { return isDefined(this.lineNum); } /** * Update the condition and close the dialog when the user presses Enter or focus out. * @param event the keyboard event, or undefined if the event is focus out. */ @HostListener("window:keydown", ["$event"]) @HostListener("focusout") handleEvent(event?: KeyboardEvent): void { if (!this.lineNum || (event && !(event.key === "Enter" && !event.shiftKey))) { // perform no changes if no line number or the key is not Enter return; } // prevent the default behavior of the Enter key event?.preventDefault(); // save the updated condition this.udfDebugService.doUpdateBreakpointCondition(this.operatorId, this.lineNum, this.condition.trim()); // close the dialog this.closeEmitter.emit(); } } ================================================ FILE: frontend/src/app/workspace/component/code-editor-dialog/code-debugger.component.html ================================================ ================================================ FILE: frontend/src/app/workspace/component/code-editor-dialog/code-debugger.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; import { CodeDebuggerComponent } from "./code-debugger.component"; import { WorkflowStatusService } from "../../service/workflow-status/workflow-status.service"; import { UdfDebugService } from "../../service/operator-debug/udf-debug.service"; import { Subject } from "rxjs"; import * as Y from "yjs"; import { BreakpointInfo } from "../../types/workflow-common.interface"; import { OperatorState, OperatorStatistics } from "../../types/execute-workflow.interface"; import { commonTestProviders } from "../../../common/testing/test-utils"; import type { Mocked } from "vitest"; import type { MonacoBreakpoint } from "monaco-breakpoints"; import type * as monaco from "monaco-editor"; describe("CodeDebuggerComponent", () => { let component: CodeDebuggerComponent; let fixture: ComponentFixture; let mockWorkflowStatusService: Mocked; let mockUdfDebugService: Mocked; let statusUpdateStream: Subject>; let debugState: Y.Map; const operatorId = "test-operator-id"; beforeEach(async () => { // Initialize streams and spy objects statusUpdateStream = new Subject>(); debugState = new Y.Map(); mockWorkflowStatusService = { getStatusUpdateStream: vi.fn() } as unknown as Mocked; mockWorkflowStatusService.getStatusUpdateStream.mockReturnValue(statusUpdateStream.asObservable()); mockUdfDebugService = { getDebugState: vi.fn(), doModifyBreakpoint: vi.fn(), } as unknown as Mocked; mockUdfDebugService.getDebugState.mockReturnValue(debugState); await TestBed.configureTestingModule({ imports: [CodeDebuggerComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], providers: [ { provide: WorkflowStatusService, useValue: mockWorkflowStatusService }, { provide: UdfDebugService, useValue: mockUdfDebugService }, ...commonTestProviders, ], }).compileComponents(); fixture = TestBed.createComponent(CodeDebuggerComponent); component = fixture.componentInstance; // Set required input properties component.currentOperatorId = operatorId; component.monacoEditor = { dispose: vi.fn() } as unknown as monaco.editor.IStandaloneCodeEditor; // Trigger change detection to ensure view updates fixture.detectChanges(); }); afterEach(() => { // Clean up streams to prevent memory leaks statusUpdateStream.complete(); component.monacoEditor?.dispose(); }); it("should create the component", () => { expect(component).toBeTruthy(); }); it("should setup monaco breakpoint methods when state is Running", fakeAsync(() => { // Stub the real implementations: setupMonacoBreakpointMethods constructs // a `MonacoBreakpoint` over a real monaco editor instance, which calls // editor.onMouseMove / onMouseDown — APIs the test's minimal // `monacoEditor` mock doesn't expose. The behavior under test is the // state-machine wiring, not the breakpoint plumbing itself. const setupSpy = vi.spyOn(component, "setupMonacoBreakpointMethods").mockImplementation(() => {}); const rerenderSpy = vi.spyOn(component, "rerenderExistingBreakpoints").mockImplementation(() => {}); // Emit a Running state event statusUpdateStream.next({ [operatorId]: { operatorState: OperatorState.Running, aggregatedOutputRowCount: 0, aggregatedInputRowCount: 0, inputPortMetrics: {}, outputPortMetrics: {}, }, }); tick(); fixture.detectChanges(); // Trigger change detection expect(setupSpy).toHaveBeenCalled(); expect(rerenderSpy).toHaveBeenCalled(); // Emit the same state again (should not trigger setup again) statusUpdateStream.next({ [operatorId]: { operatorState: OperatorState.Running, aggregatedOutputRowCount: 0, aggregatedInputRowCount: 0, inputPortMetrics: {}, outputPortMetrics: {}, }, }); tick(); fixture.detectChanges(); // Trigger change detection expect(setupSpy).toHaveBeenCalledTimes(1); // No additional call expect(rerenderSpy).toHaveBeenCalledTimes(1); // No additional call // Emit the paused state (should not trigger setup) statusUpdateStream.next({ [operatorId]: { operatorState: OperatorState.Paused, aggregatedOutputRowCount: 0, aggregatedInputRowCount: 0, inputPortMetrics: {}, outputPortMetrics: {}, }, }); tick(); fixture.detectChanges(); // Trigger change detection expect(setupSpy).toHaveBeenCalledTimes(1); // No additional call expect(rerenderSpy).toHaveBeenCalledTimes(1); // No additional call // Emit the running state once more (should not trigger setup) statusUpdateStream.next({ [operatorId]: { operatorState: OperatorState.Paused, aggregatedOutputRowCount: 0, aggregatedInputRowCount: 0, inputPortMetrics: {}, outputPortMetrics: {}, }, }); tick(); fixture.detectChanges(); // Trigger change detection expect(setupSpy).toHaveBeenCalledTimes(1); // No additional call expect(rerenderSpy).toHaveBeenCalledTimes(1); // No additional call })); it("should remove monaco breakpoint methods when state changes to Uninitialized", () => { const removeSpy = vi.spyOn(component, "removeMonacoBreakpointMethods"); // Emit an Uninitialized state event statusUpdateStream.next({ [operatorId]: { operatorState: OperatorState.Uninitialized, aggregatedOutputRowCount: 0, aggregatedInputRowCount: 0, inputPortMetrics: {}, outputPortMetrics: {}, }, }); fixture.detectChanges(); // Trigger change detection expect(removeSpy).toHaveBeenCalled(); // Emit the same state again (should not trigger removal again) statusUpdateStream.next({ [operatorId]: { operatorState: OperatorState.Uninitialized, aggregatedOutputRowCount: 0, aggregatedInputRowCount: 0, inputPortMetrics: {}, outputPortMetrics: {}, }, }); expect(removeSpy).toHaveBeenCalledTimes(1); // No additional call }); it("should call doModifyBreakpoint on left click", () => { // Simulate a left click on line 1 component["onMouseLeftClick"](1); // Verify that the mock service was called with the correct arguments expect(mockUdfDebugService.doModifyBreakpoint).toHaveBeenCalledWith(operatorId, 1); }); it("should set breakpoint condition input on right click", () => { // Mock a valid decoration map component.monacoBreakpoint = { lineNumberAndDecorationIdMap: new Map([ [1, "breakpoint1"], [2, "breakpoint2"], ]), } as unknown as MonacoBreakpoint; // Simulate a right click on line 1, it should switch to 1 component["onMouseRightClick"](1); expect(component.breakpointConditionLine).toBe(1); // Simulate a right click on line 3, which does not have a breakpoint. no changes should occur component["onMouseRightClick"](3); expect(component.breakpointConditionLine).toBe(1); // Simulate a right click on line 2, it should switch to 2 component["onMouseRightClick"](2); expect(component.breakpointConditionLine).toBe(2); // Simulate a right click on line 1, it should switch to 1 component["onMouseRightClick"](1); expect(component.breakpointConditionLine).toBe(1); }); it("should reset the breakpoint condition input when closed", () => { // Set a condition line and close it component.breakpointConditionLine = 1; component.closeBreakpointConditionInput(); expect(component.breakpointConditionLine).toBeUndefined(); }); }); ================================================ FILE: frontend/src/app/workspace/component/code-editor-dialog/code-debugger.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { AfterViewInit, Component, Input, ViewChild } from "@angular/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { SafeStyle } from "@angular/platform-browser"; import "@codingame/monaco-vscode-python-default-extension"; import "@codingame/monaco-vscode-r-default-extension"; import "@codingame/monaco-vscode-java-default-extension"; import { isDefined } from "../../../common/util/predicate"; import * as monaco from "monaco-editor"; import { MonacoBreakpoint } from "monaco-breakpoints"; import { UdfDebugService } from "../../service/operator-debug/udf-debug.service"; import { BreakpointConditionInputComponent } from "./breakpoint-condition-input/breakpoint-condition-input.component"; import { WorkflowStatusService } from "../../service/workflow-status/workflow-status.service"; import { distinctUntilChanged, map } from "rxjs/operators"; import { OperatorState } from "../../types/execute-workflow.interface"; type MonacoEditor = monaco.editor.IStandaloneCodeEditor; type EditorMouseEvent = monaco.editor.IEditorMouseEvent; type EditorMouseTarget = monaco.editor.IMouseTargetMargin; type ModelDecorationOptions = monaco.editor.IModelDecorationOptions; type Range = monaco.IRange; const MouseTargetType = monaco.editor.MouseTargetType; /** * This component is the main component for the code debugger. */ @UntilDestroy() @Component({ selector: "texera-code-debugger", templateUrl: "code-debugger.component.html", imports: [BreakpointConditionInputComponent], }) export class CodeDebuggerComponent implements AfterViewInit, SafeStyle { @Input() monacoEditor!: MonacoEditor; @Input() currentOperatorId!: string; @ViewChild(BreakpointConditionInputComponent) breakpointConditionInput!: BreakpointConditionInputComponent; public monacoBreakpoint: MonacoBreakpoint | undefined = undefined; public breakpointConditionLine: number | undefined = undefined; constructor( private udfDebugService: UdfDebugService, private workflowStatusService: WorkflowStatusService ) {} ngAfterViewInit() { this.registerStatusChangeHandler(); this.registerBreakpointRenderingHandler(); } setupMonacoBreakpointMethods(editor: MonacoEditor) { // mimic the enum in monaco-breakpoints enum BreakpointEnum { Exist, } this.monacoBreakpoint = new MonacoBreakpoint({ editor, hoverMessage: { added: { value: "Click to remove the breakpoint.", }, unAdded: { value: "Click to add a breakpoint at this line.", }, }, }); // override the default createBreakpointDecoration so that it considers // 1) hovering breakpoints; // 2) exist breakpoints; // 3) conditional breakpoints. (conditional breakpoints are also exist breakpoints) this.monacoBreakpoint["createBreakpointDecoration"] = ( range: Range, breakpointEnum: BreakpointEnum ): { range: Range; options: ModelDecorationOptions } => { const condition = this.udfDebugService.getCondition(this.currentOperatorId, range.startLineNumber); const isConditional = Boolean(condition?.trim()); const exists = breakpointEnum === BreakpointEnum.Exist; const glyphMarginClassName = exists ? isConditional ? "monaco-conditional-breakpoint" : "monaco-breakpoint" : "monaco-hover-breakpoint"; return { range, options: { glyphMarginClassName } }; }; // override the default mouseDownDisposable to handle // 1) left click to add/remove breakpoints; // 2) right click to open breakpoint condition input. this.monacoBreakpoint["mouseDownDisposable"]?.dispose(); this.monacoBreakpoint["mouseDownDisposable"] = editor.onMouseDown((evt: EditorMouseEvent) => { const { type, detail, position } = { ...(evt.target as EditorMouseTarget) }; const model = editor.getModel()!; if (model && type === MouseTargetType.GUTTER_GLYPH_MARGIN) { if (detail.isAfterLines) { return; } if (evt.event.leftButton) { this.onMouseLeftClick(position.lineNumber); } else { this.onMouseRightClick(position.lineNumber); } } }); } removeMonacoBreakpointMethods() { if (!isDefined(this.monacoBreakpoint)) { return; } this.monacoBreakpoint["mouseDownDisposable"]?.dispose(); this.monacoBreakpoint.dispose(); } /** * This function is called when the user left clicks on the gutter of the editor. * It adds or removes a breakpoint on the line number that the user clicked on. * @param lineNum the line number that the user clicked on * @private */ private onMouseLeftClick(lineNum: number) { // This indicates that the current position of the mouse is over the total number of lines in the editor this.udfDebugService.doModifyBreakpoint(this.currentOperatorId, lineNum); } /** * This function is called when the user right clicks on the gutter of the editor. * It opens the breakpoint condition input for the line number that the user clicked on. * @param lineNum the line number that the user clicked on * @private */ private onMouseRightClick(lineNum: number) { if (!this.monacoBreakpoint!["lineNumberAndDecorationIdMap"].has(lineNum)) { return; } this.breakpointConditionLine = lineNum; } closeBreakpointConditionInput() { this.breakpointConditionLine = undefined; } /** * This function registers a handler that listens to the changes in the lineNumToBreakpointMapping. * @private */ private registerBreakpointRenderingHandler() { this.udfDebugService.getDebugState(this.currentOperatorId).observe(evt => { evt.changes.keys.forEach((change, lineNum) => { switch (change.action) { case "add": const addedValue = evt.target.get(lineNum)!; if (isDefined(addedValue.breakpointId)) { this.createBreakpointDecoration(Number(lineNum)); } break; case "delete": const deletedValue = change.oldValue; if (isDefined(deletedValue.breakpointId)) { this.removeBreakpointDecoration(Number(lineNum)); } break; case "update": const oldValue = change.oldValue; const newValue = evt.target.get(lineNum)!; if (newValue.hit) { this.monacoBreakpoint?.setLineHighlight(Number(lineNum)); } else { this.monacoBreakpoint?.removeHighlight(); } if (oldValue.condition !== newValue.condition) { // recreate the decoration with condition this.removeBreakpointDecoration(Number(lineNum)); this.createBreakpointDecoration(Number(lineNum)); } break; } }); }); } private createBreakpointDecoration(lineNum: number) { this.monacoBreakpoint!["createSpecifyDecoration"]({ startLineNumber: Number(lineNum), startColumn: 1, endLineNumber: Number(lineNum), endColumn: 1, }); } private removeBreakpointDecoration(lineNum: number) { const decorationId = this.monacoBreakpoint!["lineNumberAndDecorationIdMap"].get(lineNum); this.monacoBreakpoint!["removeSpecifyDecoration"](decorationId, lineNum); } rerenderExistingBreakpoints() { this.udfDebugService.getDebugState(this.currentOperatorId).forEach(({ breakpointId }, lineNumStr) => { if (!isDefined(breakpointId)) { return; } this.createBreakpointDecoration(Number(lineNumStr)); }); } private registerStatusChangeHandler() { this.workflowStatusService .getStatusUpdateStream() .pipe( map( event => event[this.currentOperatorId]?.operatorState === OperatorState.Running || event[this.currentOperatorId]?.operatorState === OperatorState.Paused ), distinctUntilChanged(), untilDestroyed(this) ) .subscribe(enable => { console.log("enable", enable); // Only enable the breakpoint methods if the operator is running or paused if (enable) { this.setupMonacoBreakpointMethods(this.monacoEditor); this.rerenderExistingBreakpoints(); } else { // for other states, remove the breakpoint methods this.removeMonacoBreakpointMethods(); } }); } } ================================================ FILE: frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.html ================================================
    {{ languageTitle }} : {{ title }}
    ================================================ FILE: frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ #code-editor-container { position: fixed; left: 25vw; top: random(100) + px; z-index: 5; resize: both; overflow: hidden; width: 45vw; height: 45vh; min-width: 320px; min-height: 240px; border-radius: 3px; } #title { background: #333333; color: #fff; text-align: center; margin: 0; padding-bottom: 10px; line-height: 30px; } #close-button { line-height: 0; color: white; float: right; } #code-editor { width: 100%; height: 100%; } ::ng-deep .yRemoteSelectionHead { position: absolute; border-left: solid 2px; border-top: solid 2px; border-bottom: solid 2px; height: 100%; box-sizing: border-box; } ::ng-deep .yRemoteSelectionHead::after { position: absolute; content: " "; border: 3px solid; border-radius: 4px; left: -4px; top: -5px; } ================================================ FILE: frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { CodeEditorComponent } from "./code-editor.component"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; import { mockJavaUDFPredicate, mockPoint } from "../../service/workflow-graph/model/mock-workflow-data"; import { OperatorMetadataService } from "../../service/operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../../service/operator-metadata/stub-operator-metadata.service"; import { mockOperatorMetaData } from "../../service/operator-metadata/mock-operator-metadata.data"; import { commonTestProviders } from "../../../common/testing/test-utils"; import { OperatorPredicate } from "../../types/workflow-common.interface"; import { OperatorSchema } from "../../types/operator-schema.interface"; import { of } from "rxjs"; // Operator types that the constructor's language-detection branch must map // to a specific language. `RUDFSource` / `RUDF` -> `r`; the three V2 Python // types -> `python`; everything else -> `java`. Local to this spec so we // don't perturb the shared mock-workflow-data fixtures. const R_OPERATOR_TYPES = ["RUDFSource", "RUDF"]; const PYTHON_OPERATOR_TYPES = ["PythonUDFV2", "PythonUDFSourceV2", "DualInputPortsPythonUDFV2"]; // Augment `mockOperatorMetaData` with synthetic schemas for the V2 operator // types and one unknown type so `addOperator` and `JointUIService` accept // them. Cloning the existing `PythonUDF` schema and renaming the // `operatorType` is the cheapest way to satisfy both `operatorTypeExists` // and the schema-driven joint element creation. const baseSchema = mockOperatorMetaData.operators.find(op => op.operatorType === "PythonUDF"); if (!baseSchema) { throw new Error( "CodeEditorComponent spec setup expected a PythonUDF schema in mockOperatorMetaData — fixture has drifted." ); } const synthesizeSchema = (operatorType: string): OperatorSchema => ({ ...baseSchema, operatorType }); const augmentedSchemas: OperatorSchema[] = [ ...mockOperatorMetaData.operators, ...PYTHON_OPERATOR_TYPES.map(synthesizeSchema), ...R_OPERATOR_TYPES.map(synthesizeSchema), synthesizeSchema("SomeUnknownType"), ]; class AugmentedStubMetadataService extends StubOperatorMetadataService { // JointUIService snapshots `operatorSchemas` from this stream once on // construction, so we have to feed it the augmented list (overriding only // `getOperatorSchema`/`operatorTypeExists` is not enough). private readonly augmentedMetadata = of({ ...mockOperatorMetaData, operators: augmentedSchemas, }); override getOperatorMetadata(): typeof this.augmentedMetadata { return this.augmentedMetadata; } override getOperatorSchema(operatorType: string): OperatorSchema { const schema = augmentedSchemas.find(op => op.operatorType === operatorType); if (!schema) throw new Error(`unknown operatorType ${operatorType}`); return schema; } override operatorTypeExists(operatorType: string): boolean { return augmentedSchemas.some(op => op.operatorType === operatorType); } } const buildPredicate = (operatorID: string, operatorType: string): OperatorPredicate => ({ operatorID, operatorType, operatorVersion: "p1", operatorProperties: {}, inputPorts: [{ portID: "input-0" }], outputPorts: [{ portID: "output-0" }], showAdvanced: false, isDisabled: false, }); describe("CodeEditorComponent", () => { let workflowActionService: WorkflowActionService; beforeEach(async () => { await TestBed.configureTestingModule({ providers: [ WorkflowActionService, { provide: OperatorMetadataService, useClass: AugmentedStubMetadataService }, ...commonTestProviders, ], imports: [CodeEditorComponent, HttpClientTestingModule], }).compileComponents(); workflowActionService = TestBed.inject(WorkflowActionService); }); function makeFixture(predicate: OperatorPredicate): ComponentFixture { workflowActionService.addOperator(predicate, mockPoint); workflowActionService.getJointGraphWrapper().highlightOperators(predicate.operatorID); const fixture = TestBed.createComponent(CodeEditorComponent); fixture.detectChanges(); return fixture; } it("creates with the highlighted operator", () => { const fixture = makeFixture(mockJavaUDFPredicate); expect(fixture.componentInstance).toBeTruthy(); expect(fixture.componentInstance.currentOperatorId).toBe(mockJavaUDFPredicate.operatorID); }); // Language detection — the constructor maps `RUDFSource` / `RUDF` to `r`, // the three V2-era Python operator types to `python`, and anything else // to `java`. The exact branch lives in the constructor; the public // `language` field is what the rest of the editor (LSP wiring, file- // suffix selection) keys off. R_OPERATOR_TYPES.forEach((operatorType, index) => { it(`picks language="r" for operatorType=${operatorType}`, () => { const fixture = makeFixture(buildPredicate(`r-${index}`, operatorType)); expect(fixture.componentInstance.language).toBe("r"); expect(fixture.componentInstance.languageTitle).toBe("R UDF"); }); }); PYTHON_OPERATOR_TYPES.forEach((operatorType, index) => { it(`picks language="python" for operatorType=${operatorType}`, () => { const fixture = makeFixture(buildPredicate(`p-${index}`, operatorType)); expect(fixture.componentInstance.language).toBe("python"); expect(fixture.componentInstance.languageTitle).toBe("Python UDF"); }); }); it('picks language="java" for plain JavaUDF', () => { const fixture = makeFixture(mockJavaUDFPredicate); expect(fixture.componentInstance.language).toBe("java"); expect(fixture.componentInstance.languageTitle).toBe("Java UDF"); }); it('picks language="java" for unknown operator types', () => { const fixture = makeFixture(buildPredicate("u-0", "SomeUnknownType")); expect(fixture.componentInstance.language).toBe("java"); expect(fixture.componentInstance.languageTitle).toBe("Java UDF"); }); it("derives languageTitle as Capitalized(language) + ' UDF'", () => { const fixture = makeFixture(buildPredicate("p-x", "PythonUDFV2")); const c = fixture.componentInstance; // Independent re-derivation matches whatever the component computed. const expected = `${c.language[0].toUpperCase()}${c.language.slice(1)} UDF`; expect(c.languageTitle).toBe(expected); }); // Coeditor cursor styles — getCoeditorCursorStyles takes the awareness- // sourced clientId + colour and wraps a `"; return this.sanitizer.bypassSecurityTrustHtml(textCSS); } private getFileSuffixByLanguage(language: string): string { switch (language.toLowerCase()) { case "python": return ".py"; case "r": return ".r"; case "javascript": return ".js"; case "java": return ".java"; default: return ".py"; } } /** * Create a Monaco editor and connect it to MonacoBinding. * @private */ private initializeMonacoEditor() { const fileSuffix = this.getFileSuffixByLanguage(this.language); const userConfig: UserConfig = { wrapperConfig: { editorAppConfig: { $type: "extended", codeResources: { main: { text: this.code?.toString() ?? "", uri: `in-memory-${this.currentOperatorId}.${fileSuffix}`, }, }, userConfiguration: { json: JSON.stringify({ "workbench.colorTheme": "Default Dark Modern", }), }, }, }, }; // optionally, configure python language client. // it may fail if no valid connection is established, yet the failure would be ignored. const languageServerWebsocketUrl = getWebsocketUrl( "/python-language-server", this.config.env.pythonLanguageServerPort ); if (this.language === "python") { userConfig.languageClientConfig = { languageId: this.language, options: { $type: "WebSocketUrl", url: languageServerWebsocketUrl, }, }; } // init monaco editor, optionally with attempt on language client. from(this.editorWrapper.initAndStart(userConfig, this.editorElement.nativeElement)) .pipe( timeout(LANGUAGE_SERVER_CONNECTION_TIMEOUT_MS), switchMap(() => of(this.editorWrapper.getEditor())), catchError(() => of(this.editorWrapper.getEditor())), filter(isDefined), untilDestroyed(this) ) .subscribe((editor: MonacoEditor) => { editor.updateOptions({ readOnly: this.formControl.disabled }); if (!this.code) { return; } if (this.monacoBinding) { this.monacoBinding.destroy(); } this.monacoBinding = new MonacoBinding( this.code, editor.getModel()!, new Set([editor]), this.workflowActionService.getTexeraGraph().getSharedModelAwareness() ); this.setupAIAssistantActions(editor); this.initCodeDebuggerComponent(editor); }); } private initializeDiffEditor(): void { const fileSuffix = this.getFileSuffixByLanguage(this.language); const latestVersionOperator = this.workflowActionService .getTempWorkflow() ?.content.operators?.find(({ operatorID }) => operatorID === this.currentOperatorId); const latestVersionCode: string = latestVersionOperator?.operatorProperties?.code ?? ""; const oldVersionCode: string = this.code?.toString() ?? ""; const userConfig: UserConfig = { wrapperConfig: { editorAppConfig: { $type: "extended", codeResources: { main: { text: latestVersionCode, uri: `in-memory-${this.currentOperatorId}.${fileSuffix}`, }, original: { text: oldVersionCode, uri: `in-memory-${this.currentOperatorId}-version.${fileSuffix}`, }, }, useDiffEditor: true, diffEditorOptions: { readOnly: true, }, userConfiguration: { json: JSON.stringify({ "workbench.colorTheme": "Default Dark Modern", }), }, }, }, }; this.editorWrapper.initAndStart(userConfig, this.editorElement.nativeElement); } private initCodeDebuggerComponent(editor: MonacoEditor) { this.codeDebuggerComponent = CodeDebuggerComponent; this.editorToPass = editor; } private setupAIAssistantActions(editor: MonacoEditor) { // Check if the AI provider is "openai" this.aiAssistantService .checkAIAssistantEnabled() .pipe(untilDestroyed(this)) .subscribe({ next: (isEnabled: string) => { if (isEnabled === "OpenAI") { // "Add Type Annotation" Button editor.addAction({ id: "type-annotation-action", label: "Add Type Annotation", contextMenuGroupId: "1_modification", contextMenuOrder: 1.0, run: (editor: MonacoEditor) => { // User selected code (including range and content) const selection = editor.getSelection(); const model = editor.getModel(); if (!model || !selection) { return; } // All the code in Python UDF const allCode = model.getValue(); // Content of user selected code const userSelectedCode = model.getValueInRange(selection); // Start line of the selected code const lineNumber = selection.startLineNumber; this.handleTypeAnnotation(userSelectedCode, selection, editor, lineNumber, allCode); }, }); } // "Add All Type Annotation" Button editor.addAction({ id: "all-type-annotation-action", label: "Add All Type Annotations", contextMenuGroupId: "1_modification", contextMenuOrder: 1.1, run: (editor: MonacoEditor) => { const selection = editor.getSelection(); const model = editor.getModel(); if (!model || !selection) { return; } const selectedCode = model.getValueInRange(selection); const allCode = model.getValue(); this.aiAssistantService .locateUnannotated(selectedCode, selection.startLineNumber) .pipe(untilDestroyed(this)) .subscribe(variablesWithoutAnnotations => { // If no unannotated variable, then do nothing. if (variablesWithoutAnnotations.length == 0) { return; } let offset = 0; let lastLine: number | undefined; this.isMultipleVariables = true; this.userResponseSubject = new Subject(); const processNextVariable = (index: number) => { if (index >= variablesWithoutAnnotations.length) { this.isMultipleVariables = false; this.userResponseSubject = undefined; return; } const currVariable = variablesWithoutAnnotations[index]; const variableCode = currVariable.name; const variableLineNumber = currVariable.startLine; // Update range if (lastLine !== undefined && lastLine === variableLineNumber) { offset += this.currentSuggestion.length; } else { offset = 0; } const variableRange = new monaco.Range( currVariable.startLine, currVariable.startColumn + offset, currVariable.endLine, currVariable.endColumn + offset ); const highlight = editor.createDecorationsCollection([ { range: variableRange, options: { hoverMessage: { value: "Argument without Annotation" }, isWholeLine: false, className: "annotation-highlight", }, }, ]); this.handleTypeAnnotation(variableCode, variableRange, editor, variableLineNumber, allCode); lastLine = variableLineNumber; // Make sure the currVariable will not go to the next one until the user click the accept/decline button if (isDefined(this.userResponseSubject)) { this.userResponseSubject .pipe(take(1)) // Only take one response (accept/decline) .pipe(untilDestroyed(this)) .subscribe(() => { highlight.clear(); processNextVariable(index + 1); }); } }; processNextVariable(0); }); }, }); }, }); } private handleTypeAnnotation( code: string, range: monaco.Range, editor: MonacoEditor, lineNumber: number, allCode: string ): void { this.aiAssistantService .getTypeAnnotations(code, lineNumber, allCode) .pipe(untilDestroyed(this)) .subscribe((response: TypeAnnotationResponse) => { const choices = response.choices || []; if (!(choices.length > 0 && choices[0].message && choices[0].message.content)) { throw Error("Error: OpenAI response does not contain valid message content " + response); } this.currentSuggestion = choices[0].message.content.trim(); this.currentCode = code; this.currentRange = range; const position = editor.getScrolledVisiblePosition(range.getStartPosition()); if (position) { this.suggestionTop = position.top + 100; this.suggestionLeft = position.left + 100; } this.showAnnotationSuggestion = true; if (!this.annotationSuggestion) { return; } this.annotationSuggestion.code = this.currentCode; this.annotationSuggestion.suggestion = this.currentSuggestion; this.annotationSuggestion.top = this.suggestionTop; this.annotationSuggestion.left = this.suggestionLeft; }); } // Called when the user clicks the "accept" button public acceptCurrentAnnotation(): void { // Avoid accidental calls if (!this.showAnnotationSuggestion || !this.currentRange || !this.currentSuggestion) { return; } if (this.currentRange && this.currentSuggestion) { const selection = new monaco.Selection( this.currentRange.startLineNumber, this.currentRange.startColumn, this.currentRange.endLineNumber, this.currentRange.endColumn ); this.insertTypeAnnotations(this.editorWrapper.getEditor()!, selection, this.currentSuggestion); // Only for "Add All Type Annotation" if (this.isMultipleVariables && this.userResponseSubject) { this.userResponseSubject.next(); } } // close the UI after adding the annotation this.showAnnotationSuggestion = false; } // Called when the user clicks the "decline" button public rejectCurrentAnnotation(): void { // Do nothing except for closing the UI this.showAnnotationSuggestion = false; this.currentCode = ""; this.currentSuggestion = ""; // Only for "Add All Type Annotation" if (this.isMultipleVariables && this.userResponseSubject) { this.userResponseSubject.next(); } } private insertTypeAnnotations(editor: MonacoEditor, selection: monaco.Selection, annotations: string) { const endLineNumber = selection.endLineNumber; const endColumn = selection.endColumn; const insertPosition = new monaco.Position(endLineNumber, endColumn); const insertOffset = editor.getModel()?.getOffsetAt(insertPosition) || 0; this.code?.insert(insertOffset, annotations); } @HostListener("window:resize") onWindowResize() { this.adjustEditorSize(); } private adjustEditorSize(): void { const container = this.containerElement.nativeElement; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const rect = container.getBoundingClientRect(); if (rect.right > viewportWidth) { container.style.width = `${viewportWidth - rect.left}px`; } if (rect.bottom > viewportHeight) { container.style.height = `${viewportHeight - rect.top}px`; } this.editorWrapper.getEditor()?.layout(); } onFocus() { this.workflowActionService.getJointGraphWrapper().highlightOperators(this.currentOperatorId); } } ================================================ FILE: frontend/src/app/workspace/component/codearea-custom-template/codearea-custom-template.component.html ================================================
    ================================================ FILE: frontend/src/app/workspace/component/codearea-custom-template/codearea-custom-template.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ #editor-button { text-align: center; padding: 10px; } .readonly { color: black; background-color: blanchedalmond; border-color: blanchedalmond; } ================================================ FILE: frontend/src/app/workspace/component/codearea-custom-template/codearea-custom-template.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { CodeareaCustomTemplateComponent } from "./codearea-custom-template.component"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; import { OperatorMetadataService } from "../../service/operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../../service/operator-metadata/stub-operator-metadata.service"; import { FormControl } from "@angular/forms"; import { commonTestProviders } from "../../../common/testing/test-utils"; describe("CodeareaCustomTemplateComponent", () => { let component: CodeareaCustomTemplateComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [CodeareaCustomTemplateComponent, HttpClientTestingModule], providers: [ WorkflowActionService, { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, ...commonTestProviders, ], }).compileComponents(); fixture = TestBed.createComponent(CodeareaCustomTemplateComponent); component = fixture.componentInstance; component.field = { props: {}, formControl: new FormControl() }; fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/workspace/component/codearea-custom-template/codearea-custom-template.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ChangeDetectorRef, Component, ComponentRef, OnDestroy, OnInit } from "@angular/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { FieldType, FieldTypeConfig } from "@ngx-formly/core"; import { CodeEditorComponent } from "../code-editor-dialog/code-editor.component"; import { CoeditorPresenceService } from "../../service/workflow-graph/model/coeditor-presence.service"; import { CodeEditorService } from "../../service/code-editor/code-editor.service"; import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NgClass, NgIf } from "@angular/common"; /** * CodeareaCustomTemplateComponent is the custom template for 'codearea' type of formly field. * * When the formly field type is 'codearea', it overrides the default one line string input template * with this component. */ @UntilDestroy() @Component({ selector: "texera-codearea-custom-template", templateUrl: "codearea-custom-template.component.html", styleUrls: ["codearea-custom-template.component.scss"], imports: [ NzSpaceCompactItemDirective, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, NgClass, NgIf, ], }) export class CodeareaCustomTemplateComponent extends FieldType implements OnInit, OnDestroy { componentRef: ComponentRef | undefined; public isEditorOpen: boolean = false; private operatorID: string = ""; constructor( private coeditorPresenceService: CoeditorPresenceService, private codeEditorService: CodeEditorService, private changeDetectorRef: ChangeDetectorRef, private workflowActionService: WorkflowActionService ) { super(); this.coeditorPresenceService .getCoeditorOpenedCodeEditorSubject() .pipe(untilDestroyed(this)) .subscribe(_ => this.openEditor()); this.coeditorPresenceService .getCoeditorClosedCodeEditorSubject() .pipe(untilDestroyed(this)) .subscribe(_ => this.componentRef?.destroy()); } ngOnInit() { this.operatorID = this.getOperatorID(); this.codeEditorService .getEditorState(this.operatorID) .pipe(untilDestroyed(this)) .subscribe(isOpen => { this.isEditorOpen = isOpen; this.changeDetectorRef.detectChanges(); }); } openEditor(): void { this.componentRef = this.codeEditorService.vc.createComponent(CodeEditorComponent); this.componentRef.instance.componentRef = this.componentRef; this.componentRef.instance.formControl = this.field.formControl; this.isEditorOpen = true; this.codeEditorService.setEditorState(this.operatorID, true); this.componentRef.onDestroy(() => { this.isEditorOpen = false; this.codeEditorService.setEditorState(this.operatorID, false); this.changeDetectorRef.detectChanges(); }); } ngOnDestroy() { this.codeEditorService.setEditorState(this.operatorID, this.isEditorOpen); } private getOperatorID(): string { return this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs()[0]; } } ================================================ FILE: frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html ================================================ ================================================ FILE: frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component } from "@angular/core"; import { FieldType, FieldTypeConfig } from "@ngx-formly/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; import { NzModalService } from "ng-zorro-antd/modal"; import { DatasetSelectionModalComponent } from "../dataset-selection-modal/dataset-selection-modal.component"; import { GuiConfigService } from "../../../common/service/gui-config.service"; import { NgIf } from "@angular/common"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzInputDirective } from "ng-zorro-antd/input"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; @UntilDestroy() @Component({ templateUrl: "dataset-file-selector.component.html", imports: [ NgIf, NzSpaceCompactItemDirective, NzInputDirective, FormsModule, ReactiveFormsModule, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, ], }) export class DatasetFileSelectorComponent extends FieldType { constructor( private modalService: NzModalService, public workflowActionService: WorkflowActionService, private config: GuiConfigService ) { super(); } onClickOpenFileSelectionModal(): void { const modal = this.modalService.create({ nzContent: DatasetSelectionModalComponent, nzFooter: null, nzData: { fileMode: true, selectedPath: this.formControl.getRawValue(), }, nzBodyStyle: { resize: "both", overflow: "auto", minHeight: "200px", minWidth: "550px", maxWidth: "90vw", maxHeight: "80vh", }, nzWidth: "fit-content", }); modal.afterClose.pipe(untilDestroyed(this)).subscribe(selectedPath => { if (selectedPath) { this.formControl.setValue(selectedPath); } }); } get isFileSelectionEnabled(): boolean { return this.config.env.selectingFilesFromDatasetsEnabled; } } ================================================ FILE: frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html ================================================
    #{{ dataset.dataset.did }} {{ dataset.dataset.name }} {{ dataset.isOwner ? 'OWNER' : dataset.accessPrivilege }}


    ================================================ FILE: frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .dataset-row { display: flex; justify-content: space-between; } .dataset-name { overflow: hidden; text-overflow: ellipsis; } .access-level { color: grey; } ================================================ FILE: frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, inject, OnInit } from "@angular/core"; import { NZ_MODAL_DATA, NzModalRef } from "ng-zorro-antd/modal"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { DatasetFileNode, getFullPathFromDatasetFileNode } from "../../../common/type/datasetVersionFileTree"; import { DatasetVersion } from "../../../common/type/dataset"; import { DashboardDataset } from "../../../dashboard/type/dashboard-dataset.interface"; import { DatasetService } from "../../../dashboard/service/user/dataset/dataset.service"; import { NzRowDirective, NzColDirective } from "ng-zorro-antd/grid"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzSelectComponent, NzOptionComponent } from "ng-zorro-antd/select"; import { FormsModule } from "@angular/forms"; import { NgFor } from "@angular/common"; import { UserDatasetVersionFiletreeComponent } from "../../../dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; @UntilDestroy() @Component({ templateUrl: "dataset-selection-modal.component.html", styleUrls: ["dataset-selection-modal.component.scss"], imports: [ NzRowDirective, NzSpaceCompactItemDirective, NzSelectComponent, NzColDirective, FormsModule, NgFor, NzOptionComponent, UserDatasetVersionFiletreeComponent, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, ], }) export class DatasetSelectionModalComponent implements OnInit { private readonly data = inject(NZ_MODAL_DATA) as { fileMode: boolean; selectedPath?: string | null; }; datasets: ReadonlyArray = []; datasetVersions: ReadonlyArray = []; fileTree: DatasetFileNode[] = []; selectedDataset?: DashboardDataset; selectedVersion?: DatasetVersion; selectedPath?: string; constructor( private modalRef: NzModalRef, private datasetService: DatasetService ) {} ngOnInit() { this.datasetService .retrieveAccessibleDatasets() .pipe(untilDestroyed(this)) .subscribe(datasets => { this.datasets = datasets; const selectedPath = this.data.selectedPath; if (selectedPath) { const [ownerEmail, datasetName, versionName] = selectedPath.split("/").filter(part => part.length > 0); this.selectedDataset = this.datasets.find( dataset => dataset.ownerEmail === ownerEmail && dataset.dataset.name === datasetName ); this.onDatasetChange(versionName); } }); } onDatasetChange(versionName?: string) { this.fileTree = []; if (this.selectedDataset?.dataset.did !== undefined) { this.datasetService .retrieveDatasetVersionList(this.selectedDataset.dataset.did) .pipe(untilDestroyed(this)) .subscribe(versions => { this.datasetVersions = versions; if (this.data.fileMode) { this.selectedVersion = versions.find(version => version.name === versionName) ?? versions[0]; this.onVersionChange(); } }); } } onVersionChange() { if (this.selectedDataset?.dataset.did !== undefined && this.selectedVersion?.dvid !== undefined) { this.selectedPath = undefined; this.datasetService .retrieveDatasetVersionFileTree(this.selectedDataset.dataset.did, this.selectedVersion.dvid) .pipe(untilDestroyed(this)) .subscribe(data => { this.fileTree = data.fileNodes; }); if (!this.data.fileMode) { this.selectedPath = `/${this.selectedDataset.ownerEmail}/${this.selectedDataset.dataset.name}/${this.selectedVersion.name}`; } } } onFileSelected(node: DatasetFileNode) { if (this.data.fileMode) { this.selectedPath = getFullPathFromDatasetFileNode(node); } } onConfirmSelection() { this.modalRef.close(this.selectedPath); } } ================================================ FILE: frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html ================================================ ================================================ FILE: frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component } from "@angular/core"; import { FieldType, FieldTypeConfig } from "@ngx-formly/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { NzModalService } from "ng-zorro-antd/modal"; import { DatasetSelectionModalComponent } from "../dataset-selection-modal/dataset-selection-modal.component"; import { NgIf } from "@angular/common"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzInputDirective } from "ng-zorro-antd/input"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; @UntilDestroy() @Component({ templateUrl: "dataset-version-selector.component.html", imports: [ NgIf, NzSpaceCompactItemDirective, NzInputDirective, FormsModule, ReactiveFormsModule, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, ], }) export class DatasetVersionSelectorComponent extends FieldType { constructor(private modalService: NzModalService) { super(); } onClickOpenDatasetSelectionModal(): void { const modal = this.modalService.create({ nzContent: DatasetSelectionModalComponent, nzFooter: null, nzData: { fileMode: false, selectedPath: this.formControl.getRawValue(), }, nzBodyStyle: { resize: "both", overflow: "auto", minHeight: "200px", minWidth: "550px", maxWidth: "90vw", maxHeight: "80vh", }, nzWidth: "fit-content", }); modal.afterClose.pipe(untilDestroyed(this)).subscribe(selectedPath => { if (selectedPath) { this.formControl.setValue(selectedPath); } }); } } ================================================ FILE: frontend/src/app/workspace/component/left-panel/environment/environment.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ ================================================ FILE: frontend/src/app/workspace/component/left-panel/left-panel.component.html ================================================

    {{title}}

    ================================================ FILE: frontend/src/app/workspace/component/left-panel/left-panel.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ :host { display: block; width: 100%; height: 100%; position: fixed; z-index: 3; } #left-container { position: absolute; top: calc(-100% + 80px); left: 0; z-index: 3; background: white; } #title { padding: 5px 9px; border-bottom: 1px solid #e0e0e0; position: absolute; top: 0; background: white; width: calc(100% - 33px); z-index: 2; } #dock { display: inline-block; vertical-align: top; padding-bottom: 10px; border: none; } #docked-buttons { position: fixed; top: 80px; z-index: 4; } #return-button { position: absolute; top: 0; right: 0; z-index: 3; display: flex; } #content { width: calc(100% - 33px); height: 100%; padding-top: 32px; display: inline-block; overflow-y: auto; border-left: 1px solid #f0f0f0; } .divider { margin: 0; } .ant-menu-item { margin: 0 !important; height: 32px; line-height: 32px; padding: 0 9px; } .shadow { border-radius: 5px; box-shadow: 0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f; } ================================================ FILE: frontend/src/app/workspace/component/left-panel/left-panel.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { NO_ERRORS_SCHEMA } from "@angular/core"; import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; import { LeftPanelComponent } from "./left-panel.component"; import { mockPoint, mockScanPredicate } from "../../service/workflow-graph/model/mock-workflow-data"; import { VersionsListComponent } from "./versions-list/versions-list.component"; import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { OperatorMetadataService } from "../../service/operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../../service/operator-metadata/stub-operator-metadata.service"; import { RouterTestingModule } from "@angular/router/testing"; import { commonTestProviders } from "../../../common/testing/test-utils"; describe("LeftPanelComponent", () => { let component: LeftPanelComponent; let workflowActionService: WorkflowActionService; let fixture: ComponentFixture; beforeEach(async () => { TestBed.overrideComponent(LeftPanelComponent, { set: { template: '
    ', }, }); await TestBed.configureTestingModule({ imports: [LeftPanelComponent, HttpClientTestingModule, RouterTestingModule.withRoutes([])], providers: [ { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, ...commonTestProviders, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(LeftPanelComponent); component = fixture.componentInstance; workflowActionService = TestBed.inject(WorkflowActionService); fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); it("should switch to versions frame component when get all versions is clicked", fakeAsync(() => { const jointGraphWrapper = workflowActionService.getJointGraphWrapper(); // add one operator workflowActionService.addOperator(mockScanPredicate, mockPoint); // highlight the first operator jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID); //the operator shall be highlighted expect(workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs().length).toBe(1); // click on versions display component.openFrame(2); new VersionsListComponent(workflowActionService, {} as never, { snapshot: { params: {} } } as never).ngOnInit(); // all the elements shall be un-highlighted expect(workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs().length).toBe(0); expect(workflowActionService.getJointGraphWrapper().getCurrentHighlightedLinkIDs().length).toBe(0); // the component should switch to versions display expect(component.currentComponent).toBe(VersionsListComponent); })); }); ================================================ FILE: frontend/src/app/workspace/component/left-panel/left-panel.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { AfterViewInit, Component, ElementRef, HostListener, OnDestroy, OnInit, Type, ViewChild } from "@angular/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { NzResizeEvent, NzResizableDirective, NzResizeHandlesComponent } from "ng-zorro-antd/resizable"; import { CdkDragDrop, moveItemInArray, CdkDropList, CdkDrag, CdkDragHandle } from "@angular/cdk/drag-drop"; import { OperatorMenuComponent } from "./operator-menu/operator-menu.component"; import { VersionsListComponent } from "./versions-list/versions-list.component"; import { WorkflowExecutionHistoryComponent } from "../../../dashboard/component/user/user-workflow/ngbd-modal-workflow-executions/workflow-execution-history.component"; import { TimeTravelComponent } from "./time-travel/time-travel.component"; import { SettingsComponent } from "./settings/settings.component"; import { calculateTotalTranslate3d } from "../../../common/util/panel-dock"; import { PanelService } from "../../service/panel/panel.service"; import { GuiConfigService } from "../../../common/service/gui-config.service"; import { NzMenuDirective, NzMenuItemComponent, NzMenuDividerDirective } from "ng-zorro-antd/menu"; import { NgClass, NgIf, NgFor, NgComponentOutlet } from "@angular/common"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { FormlyRepeatDndComponent } from "../../../common/formly/repeat-dnd/repeat-dnd.component"; @UntilDestroy() @Component({ selector: "texera-left-panel", templateUrl: "left-panel.component.html", styleUrls: ["left-panel.component.scss"], imports: [ NzMenuDirective, CdkDropList, NgClass, NgIf, NzMenuItemComponent, ɵNzTransitionPatchDirective, NzIconDirective, NzMenuDividerDirective, NgFor, CdkDrag, NzTooltipDirective, NzResizableDirective, NzSpaceCompactItemDirective, NzButtonComponent, CdkDragHandle, NgComponentOutlet, NzResizeHandlesComponent, FormlyRepeatDndComponent, ], }) export class LeftPanelComponent implements OnDestroy, OnInit, AfterViewInit { @ViewChild("content") content!: ElementRef; protected readonly window = window; private static readonly MIN_PANEL_WIDTH = 230; currentComponent: Type | null = null; title = "Operators"; width = LeftPanelComponent.MIN_PANEL_WIDTH; minPanelHeight = 410; height = Math.max(this.minPanelHeight, window.innerHeight * 0.6); id = -1; currentIndex = 0; items = [ { component: null, title: "", icon: "", enabled: true }, { component: OperatorMenuComponent, title: "Operators", icon: "appstore", enabled: true }, { component: VersionsListComponent, title: "Versions", icon: "schedule", enabled: true }, { component: SettingsComponent, title: "Settings", icon: "setting", enabled: true, }, { component: WorkflowExecutionHistoryComponent, title: "Execution History", icon: "history", enabled: false, }, { component: TimeTravelComponent, title: "Time Travel", icon: "clock-circle", enabled: false, }, ]; order = Array.from({ length: this.items.length - 1 }, (_, index) => index + 1); dragPosition = { x: 0, y: 0 }; returnPosition = { x: 0, y: 0 }; isDocked = true; constructor( private panelService: PanelService, private config: GuiConfigService ) { // Initialize items array with config values this.updateItemsWithConfig(); const savedOrder = localStorage.getItem("left-panel-order")?.split(",").map(Number); this.order = savedOrder && new Set(savedOrder).size === new Set(this.order).size ? savedOrder : this.order; const savedIndex = Number(localStorage.getItem("left-panel-index")); this.openFrame(savedIndex < this.items.length && this.items[savedIndex].enabled ? savedIndex : 1); this.width = Number(localStorage.getItem("left-panel-width")) || this.width; this.height = Number(localStorage.getItem("left-panel-height")) || this.height; } private updateItemsWithConfig(): void { this.items[4].enabled = this.config.env.workflowExecutionsTrackingEnabled; // Execution History this.items[5].enabled = this.config.env.timetravelEnabled; // Time Travel } ngOnInit(): void { const style = localStorage.getItem("left-panel-style"); if (style) document.getElementById("left-container")!.style.cssText = style; const translates = document.getElementById("left-container")!.style.transform; const [xOffset, yOffset, _] = calculateTotalTranslate3d(translates); this.returnPosition = { x: -xOffset, y: -yOffset }; this.isDocked = this.dragPosition.x === this.returnPosition.x && this.dragPosition.y === this.returnPosition.y; this.panelService.closePanelStream.pipe(untilDestroyed(this)).subscribe(() => this.openFrame(0)); this.panelService.resetPanelStream.pipe(untilDestroyed(this)).subscribe(() => { this.resetPanelPosition(); this.openFrame(1); }); } // Calculates the sum of level one operator tabs, and sets minPanelHeight to this value ngAfterViewInit(): void { setTimeout(() => { const topLevelCategories = this.content.nativeElement.querySelectorAll( 'nz-collapse-panel.operator-group[data-depth="0"]' ); if (topLevelCategories.length > 0) { let totalCategoriesHeight = 0; topLevelCategories.forEach(element => { totalCategoriesHeight += element.clientHeight; }); let padding = 90; this.minPanelHeight = totalCategoriesHeight + padding; // Add padding for search bar and other UI elements this.height = this.minPanelHeight; } }, 0); // Wait for collapsible panels to render } @HostListener("window:beforeunload") ngOnDestroy(): void { localStorage.setItem("left-panel-width", String(this.width)); localStorage.setItem("left-panel-height", String(this.height)); localStorage.setItem("left-panel-order", String(this.order)); localStorage.setItem("left-panel-index", String(this.currentIndex)); const leftContainer = document.getElementById("left-container"); if (leftContainer) { localStorage.setItem("left-panel-style", leftContainer.style.cssText); } } openFrame(i: number) { if (!i) { this.width = 0; this.height = 65; } else if (!this.width) { this.width = LeftPanelComponent.MIN_PANEL_WIDTH; this.height = this.minPanelHeight; } this.title = this.items[i].title; this.currentComponent = this.items[i].component; this.currentIndex = i; } onDrop(event: CdkDragDrop) { moveItemInArray(this.order, event.previousIndex, event.currentIndex); } onResize({ width, height }: NzResizeEvent) { cancelAnimationFrame(this.id); this.id = requestAnimationFrame(() => { this.width = width!; this.height = height!; }); } resetPanelPosition() { this.dragPosition = { x: this.returnPosition.x, y: this.returnPosition.y }; this.isDocked = true; } handleDragStart() { this.isDocked = false; } } ================================================ FILE: frontend/src/app/workspace/component/left-panel/operator-menu/operator-label/operator-label.component.html ================================================
    {{ operator?.additionalMetadata?.userFriendlyName }}
    ================================================ FILE: frontend/src/app/workspace/component/left-panel/operator-menu/operator-label/operator-label.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .disable-drag-drop { color: grey; } .operator-label { white-space: nowrap; height: 32px; line-height: 30px; border-radius: 3px; border: 1px solid rgba(0, 0, 0, 0); transition: border-color 0.3s, color 0.3s; overflow-x: hidden; &:hover { border-color: rgba(64, 169, 255, 1); > .text { transform: translateX(10px); } } > .text { transition: transform 0.5s 0.1s; } } ================================================ FILE: frontend/src/app/workspace/component/left-panel/operator-menu/operator-label/operator-label.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { WorkflowUtilService } from "../../../../service/workflow-graph/util/workflow-util.service"; import { JointUIService } from "../../../../service/joint-ui/joint-ui.service"; import { DragDropService } from "../../../../service/drag-drop/drag-drop.service"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { OperatorLabelComponent } from "./operator-label.component"; import { OperatorMetadataService } from "../../../../service/operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../../../../service/operator-metadata/stub-operator-metadata.service"; import { mockScanSourceSchema } from "../../../../service/operator-metadata/mock-operator-metadata.data"; import { By } from "@angular/platform-browser"; import { WorkflowActionService } from "../../../../service/workflow-graph/model/workflow-action.service"; import { UndoRedoService } from "../../../../service/undo-redo/undo-redo.service"; import { RouterTestingModule } from "@angular/router/testing"; import { commonTestProviders } from "../../../../../common/testing/test-utils"; describe("OperatorLabelComponent", () => { const mockOperatorData = mockScanSourceSchema; let component: OperatorLabelComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [OperatorLabelComponent, RouterTestingModule.withRoutes([])], providers: [ DragDropService, JointUIService, WorkflowUtilService, WorkflowActionService, UndoRedoService, { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, ...commonTestProviders, ], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(OperatorLabelComponent); component = fixture.componentInstance; // use one mock operator schema as input to construct the operator label component.operator = mockOperatorData; fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); it("should display operator user friendly name on the UI", () => { const element = fixture.debugElement.query(By.css(".text")).nativeElement; expect(element.textContent?.trim()).toEqual(mockOperatorData.additionalMetadata.userFriendlyName); }); }); ================================================ FILE: frontend/src/app/workspace/component/left-panel/operator-menu/operator-label/operator-label.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { DragDropService } from "../../../../service/drag-drop/drag-drop.service"; import { WorkflowActionService } from "../../../../service/workflow-graph/model/workflow-action.service"; import { AfterContentInit, Component, Input } from "@angular/core"; import { OperatorSchema } from "../../../../types/operator-schema.interface"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { Point } from "../../../../types/workflow-common.interface"; import { CdkDropList, CdkDrag, CdkDragPreview } from "@angular/cdk/drag-drop"; import { NgClass } from "@angular/common"; @UntilDestroy() @Component({ selector: "texera-operator-label", templateUrl: "operator-label.component.html", styleUrls: ["operator-label.component.scss"], imports: [CdkDropList, CdkDrag, NgClass, CdkDragPreview], }) export class OperatorLabelComponent implements AfterContentInit { @Input() operator?: OperatorSchema; public draggable = true; constructor( private dragDropService: DragDropService, private workflowActionService: WorkflowActionService ) {} ngAfterContentInit(): void { this.workflowActionService .getWorkflowModificationEnabledStream() .pipe(untilDestroyed(this)) .subscribe(canModify => { this.draggable = canModify; }); } dragStarted() { if (this.draggable) { this.dragDropService.dragStarted(this.operator!.operatorType); } } dragDropped(dropPoint: Point) { this.dragDropService.dragDropped(dropPoint); } } ================================================ FILE: frontend/src/app/workspace/component/left-panel/operator-menu/operator-menu.component.html ================================================
    ================================================ FILE: frontend/src/app/workspace/component/left-panel/operator-menu/operator-menu.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ #search-box { border-bottom: 1px solid #e0e0e0; position: absolute; top: 33px; z-index: 1; background: white; width: calc(100% - 33px); input { border: none; padding: 4px 9px; } } .operator-group { background-color: #fff; } .operator-group:last-child { border-bottom: 1px solid #e0e0e0; } .operator-group .operator-group { border: none; } .indent { padding-left: 10px; } .operator-label { padding-left: 7px; padding-right: 0; margin: 0 !important; height: 32px; } #spacer { margin-top: 32px; } ================================================ FILE: frontend/src/app/workspace/component/left-panel/operator-menu/operator-menu.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { mockScanSourceSchema } from "../../../service/operator-metadata/mock-operator-metadata.data"; import { UndoRedoService } from "../../../service/undo-redo/undo-redo.service"; import { DragDropService } from "../../../service/drag-drop/drag-drop.service"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { OperatorMenuComponent } from "./operator-menu.component"; import { OperatorLabelComponent } from "./operator-label/operator-label.component"; import { OperatorMetadataService } from "../../../service/operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../../../service/operator-metadata/stub-operator-metadata.service"; import { RouterTestingModule } from "@angular/router/testing"; import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; import { JointUIService } from "../../../service/joint-ui/joint-ui.service"; import { WorkflowUtilService } from "../../../service/workflow-graph/util/workflow-util.service"; import { NzDropDownModule } from "ng-zorro-antd/dropdown"; import { NzCollapseModule } from "ng-zorro-antd/collapse"; import { commonTestProviders } from "../../../../common/testing/test-utils"; import { NO_ERRORS_SCHEMA } from "@angular/core"; describe("OperatorPanelComponent", () => { let component: OperatorMenuComponent; let fixture: ComponentFixture; beforeEach(async () => { TestBed.overrideComponent(OperatorMenuComponent, { set: { template: "", }, }); await TestBed.configureTestingModule({ providers: [ { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, DragDropService, WorkflowActionService, UndoRedoService, WorkflowUtilService, JointUIService, ...commonTestProviders, ], imports: [ OperatorMenuComponent, OperatorLabelComponent, NzDropDownModule, NzCollapseModule, BrowserAnimationsModule, RouterTestingModule.withRoutes([]), ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(OperatorMenuComponent); fixture.detectChanges(); component = fixture.componentInstance; }); it("should create", () => { expect(component).toBeTruthy(); }); it("should search an operator by its user friendly name", () => { component.searchInputValue = "Source: Scan"; component.onInput({ target: { value: "Source: Scan" } } as unknown as Event); expect(component.autocompleteOptions.length).toBe(1); expect(component.autocompleteOptions[0]).toBe(mockScanSourceSchema); }); it("should support fuzzy search on operator user friendly name", () => { component.searchInputValue = "scan"; component.onInput({ target: { value: "scan" } } as unknown as Event); expect(component.autocompleteOptions.length).toBe(1); expect(component.autocompleteOptions[0]).toBe(mockScanSourceSchema); }); it("should clear the search box when an operator from search box is dropped", () => { component.searchInputValue = "scan"; component.onInput({ target: { value: "scan" } } as unknown as Event); const dragDropService = TestBed.inject(DragDropService); (dragDropService as any).operatorDroppedSubject.next(); expect(component.searchInputValue).toBeFalsy(); }); }); ================================================ FILE: frontend/src/app/workspace/component/left-panel/operator-menu/operator-menu.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component } from "@angular/core"; import Fuse from "fuse.js"; import { OperatorMetadataService } from "../../../service/operator-metadata/operator-metadata.service"; import { GroupInfo, OperatorSchema } from "../../../types/operator-schema.interface"; import { DragDropService } from "../../../service/drag-drop/drag-drop.service"; import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; import { WorkflowUtilService } from "../../../service/workflow-graph/util/workflow-util.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { NzAutocompleteOptionComponent, NzAutocompleteTriggerDirective, NzAutocompleteComponent, } from "ng-zorro-antd/auto-complete"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzInputDirective } from "ng-zorro-antd/input"; import { FormsModule } from "@angular/forms"; import { NgFor, NgTemplateOutlet } from "@angular/common"; import { OperatorLabelComponent } from "./operator-label/operator-label.component"; import { NzCollapseComponent, NzCollapsePanelComponent } from "ng-zorro-antd/collapse"; @UntilDestroy() @Component({ selector: "texera-operator-menu", templateUrl: "operator-menu.component.html", styleUrls: ["operator-menu.component.scss"], imports: [ NzSpaceCompactItemDirective, NzInputDirective, FormsModule, NzAutocompleteTriggerDirective, NzAutocompleteComponent, NgFor, NzAutocompleteOptionComponent, OperatorLabelComponent, NgTemplateOutlet, NzCollapseComponent, NzCollapsePanelComponent, ], }) export class OperatorMenuComponent { public opList = new Map>(); public groupNames: ReadonlyArray = []; // input value of the search input box public searchInputValue: string = ""; // search autocomplete suggestion list public autocompleteOptions: OperatorSchema[] = []; public canModify = true; // fuzzy search using fuse.js. See parameters in options at https://fusejs.io/ public fuse = new Fuse([] as ReadonlyArray, { shouldSort: true, threshold: 0.3, location: 0, distance: 100, minMatchCharLength: 1, keys: ["additionalMetadata.userFriendlyName"], }); constructor( private operatorMetadataService: OperatorMetadataService, private workflowActionService: WorkflowActionService, private workflowUtilService: WorkflowUtilService, private dragDropService: DragDropService ) { // clear the search box if an operator is dropped from operator search box this.dragDropService.operatorDropStream.pipe(untilDestroyed(this)).subscribe(() => { this.searchInputValue = ""; this.autocompleteOptions = []; }); this.workflowActionService .getWorkflowModificationEnabledStream() .pipe(untilDestroyed(this)) .subscribe(canModify => (this.canModify = canModify)); this.operatorMetadataService .getOperatorMetadata() .pipe(untilDestroyed(this)) .subscribe(operatorMetadata => { const ops = operatorMetadata.operators.filter( operatorSchema => operatorSchema.operatorType !== "PythonUDF" && operatorSchema.operatorType !== "Dummy" ); this.groupNames = operatorMetadata.groups; ops.forEach(x => { if (x.operatorType !== "Sleep") { const group = x.additionalMetadata.operatorGroupName; const list = this.opList.get(group) || []; list.push(x); this.opList.set(group, list); } }); this.opList.forEach(value => { value.sort((a, b) => a.operatorType.localeCompare(b.operatorType)); }); this.fuse.setCollection(ops); }); } /** * create the search results observable * whenever the search box text is changed, perform the search using fuse.js */ onInput(e: Event): void { const v = (e.target as HTMLInputElement).value; if (v === null || v.trim().length === 0) { this.autocompleteOptions = []; } this.autocompleteOptions = this.fuse.search(v).map(item => { return item.item; }); } /** * handles the event when an operator search option is selected. * adds the operator to the canvas and clears the text in the search box */ onSelectionChange(e: NzAutocompleteOptionComponent): void { const selectSchema = e.nzValue as OperatorSchema; // add the operator to the graph on select (position relative to the current viewpoint) const origin = this.workflowActionService.getJointGraphWrapper().getMainJointPaper()?.translate(); const point = { x: 400 - (origin?.tx ?? 0), y: 200 - (origin?.ty ?? 0) }; this.workflowActionService.addOperator( this.workflowUtilService.getNewOperatorPredicate(selectSchema.operatorType), point ); // asynchronously immediately clear the search input and suggestions // because ng-zorro shows the selected value if it's synchronously setTimeout(() => { this.searchInputValue = ""; this.autocompleteOptions = []; }, 0); } } ================================================ FILE: frontend/src/app/workspace/component/left-panel/settings/settings.component.html ================================================
    Execution Mode:

    Data Transfer Batch Size size must be at least 1.
    ================================================ FILE: frontend/src/app/workspace/component/left-panel/settings/settings.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .settings-container { padding: 10px; } .form-inline { display: flex; flex-direction: column; } .form-group { display: flex; flex-direction: column; width: 100%; } label { margin-bottom: 5px; font-weight: bold; } input.form-control { width: 100%; height: 30px; padding: 5px; font-size: 14px; border-radius: 4px; border: 1px solid #ccc; box-sizing: border-box; margin-bottom: 5px; } .error-message { color: red; font-size: 0.8em; margin: 0; } ================================================ FILE: frontend/src/app/workspace/component/left-panel/settings/settings.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { Observable, Subject, of, throwError } from "rxjs"; import { SettingsComponent } from "./settings.component"; import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; import { WorkflowPersistService } from "../../../../common/service/workflow-persist/workflow-persist.service"; import { UserService } from "../../../../common/service/user/user.service"; import { StubUserService } from "../../../../common/service/user/stub-user.service"; import { NotificationService } from "../../../../common/service/notification/notification.service"; import { ExecutionMode, Workflow, WorkflowContent, WorkflowSettings } from "../../../../common/type/workflow"; import { commonTestProviders } from "../../../../common/testing/test-utils"; /** * Minimal stand-in for WorkflowActionService covering only the surface * SettingsComponent uses. Avoids constructing the real service (and its * transitive OperatorMetadataService HTTP request) for these unit tests. */ class StubWorkflowActionService { private settings: WorkflowSettings = { dataTransferBatchSize: 100, executionMode: ExecutionMode.PIPELINED, }; private workflowChangedSubject = new Subject(); getWorkflowSettings(): WorkflowSettings { return this.settings; } getWorkflowContent(): WorkflowContent { return { operators: [], operatorPositions: {}, links: [], commentBoxes: [], settings: this.settings }; } getWorkflow(): Workflow { return { content: this.getWorkflowContent() } as Workflow; } setWorkflowDataTransferBatchSize(size: number): void { if (size > 0 && size != null) { this.settings = { ...this.settings, dataTransferBatchSize: size }; } } updateExecutionMode(mode: ExecutionMode): void { this.settings = { ...this.settings, executionMode: mode }; } workflowChanged(): Observable { return this.workflowChangedSubject.asObservable(); } } describe("SettingsComponent", () => { let component: SettingsComponent; let fixture: ComponentFixture; let workflowActionService: StubWorkflowActionService; let userService: StubUserService; let workflowPersistSpy: { persistWorkflow: ReturnType }; let notificationSpy: { error: ReturnType }; beforeEach(async () => { workflowPersistSpy = { persistWorkflow: vi.fn().mockReturnValue(of({})) }; notificationSpy = { error: vi.fn() }; await TestBed.configureTestingModule({ providers: [ { provide: WorkflowActionService, useClass: StubWorkflowActionService }, { provide: UserService, useClass: StubUserService }, { provide: WorkflowPersistService, useValue: workflowPersistSpy }, { provide: NotificationService, useValue: notificationSpy }, ...commonTestProviders, ], imports: [SettingsComponent, BrowserAnimationsModule, FormsModule, ReactiveFormsModule], }).compileComponents(); workflowActionService = TestBed.inject(WorkflowActionService) as unknown as StubWorkflowActionService; userService = TestBed.inject(UserService) as unknown as StubUserService; fixture = TestBed.createComponent(SettingsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); it("should initialize the form from current workflow settings", () => { const settings = workflowActionService.getWorkflowContent().settings; expect(component.settingsForm.get("dataTransferBatchSize")!.value).toBe(settings.dataTransferBatchSize); expect(component.settingsForm.get("executionMode")!.value).toBe(settings.executionMode); expect(component.settingsForm.valid).toBe(true); }); it("should mark dataTransferBatchSize invalid when below the minimum", () => { const control = component.settingsForm.get("dataTransferBatchSize")!; control.setValue(0); expect(control.valid).toBe(false); expect(control.hasError("min")).toBe(true); control.setValue(null); expect(control.valid).toBe(false); expect(control.hasError("required")).toBe(true); }); it("should push dataTransferBatchSize updates to the workflow service and persist when logged in", () => { const setBatchSizeSpy = vi.spyOn(workflowActionService, "setWorkflowDataTransferBatchSize"); component.confirmUpdateDataTransferBatchSize(42); expect(setBatchSizeSpy).toHaveBeenCalledWith(42); expect(workflowActionService.getWorkflowSettings().dataTransferBatchSize).toBe(42); expect(workflowPersistSpy.persistWorkflow).toHaveBeenCalledTimes(1); }); it("should not update or persist a non-positive batch size", () => { const setBatchSizeSpy = vi.spyOn(workflowActionService, "setWorkflowDataTransferBatchSize"); component.confirmUpdateDataTransferBatchSize(0); expect(setBatchSizeSpy).not.toHaveBeenCalled(); expect(workflowPersistSpy.persistWorkflow).not.toHaveBeenCalled(); }); it("should skip persistWorkflow when the user is not logged in", () => { userService.user = undefined; const setBatchSizeSpy = vi.spyOn(workflowActionService, "setWorkflowDataTransferBatchSize"); component.confirmUpdateDataTransferBatchSize(7); expect(setBatchSizeSpy).toHaveBeenCalledWith(7); expect(workflowPersistSpy.persistWorkflow).not.toHaveBeenCalled(); }); it("should update the execution mode on the workflow service and persist", () => { const updateModeSpy = vi.spyOn(workflowActionService, "updateExecutionMode"); component.updateExecutionMode(ExecutionMode.MATERIALIZED); expect(updateModeSpy).toHaveBeenCalledWith(ExecutionMode.MATERIALIZED); expect(workflowActionService.getWorkflowSettings().executionMode).toBe(ExecutionMode.MATERIALIZED); expect(workflowPersistSpy.persistWorkflow).toHaveBeenCalledTimes(1); }); it("should surface a notification error when persistWorkflow fails", () => { workflowPersistSpy.persistWorkflow.mockReturnValueOnce(throwError(() => new Error("network down"))); component.persistWorkflow(); expect(notificationSpy.error).toHaveBeenCalledWith("network down"); }); it("should propagate form value changes through to the workflow service", () => { const setBatchSizeSpy = vi.spyOn(workflowActionService, "setWorkflowDataTransferBatchSize"); const updateModeSpy = vi.spyOn(workflowActionService, "updateExecutionMode"); component.settingsForm.get("dataTransferBatchSize")!.setValue(256); component.settingsForm.get("executionMode")!.setValue(ExecutionMode.MATERIALIZED); expect(setBatchSizeSpy).toHaveBeenCalledWith(256); expect(updateModeSpy).toHaveBeenCalledWith(ExecutionMode.MATERIALIZED); }); it("should ignore form value changes that fail validation", () => { const setBatchSizeSpy = vi.spyOn(workflowActionService, "setWorkflowDataTransferBatchSize"); component.settingsForm.get("dataTransferBatchSize")!.setValue(-5); expect(setBatchSizeSpy).not.toHaveBeenCalled(); }); }); ================================================ FILE: frontend/src/app/workspace/component/left-panel/settings/settings.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, OnInit } from "@angular/core"; import { FormBuilder, FormGroup, Validators, FormsModule, ReactiveFormsModule } from "@angular/forms"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; import { WorkflowPersistService } from "src/app/common/service/workflow-persist/workflow-persist.service"; import { UserService } from "../../../../common/service/user/user.service"; import { NotificationService } from "src/app/common/service/notification/notification.service"; import { ExecutionMode } from "../../../../common/type/workflow"; import { NzRadioGroupComponent, NzRadioComponent } from "ng-zorro-antd/radio"; import { NgClass, NgIf } from "@angular/common"; @UntilDestroy() @Component({ selector: "texera-settings", templateUrl: "./settings.component.html", styleUrls: ["./settings.component.scss"], imports: [FormsModule, ReactiveFormsModule, NzRadioGroupComponent, NzRadioComponent, NgClass, NgIf], }) export class SettingsComponent implements OnInit { settingsForm: FormGroup; constructor( private fb: FormBuilder, private workflowActionService: WorkflowActionService, private workflowPersistService: WorkflowPersistService, private userService: UserService, private notificationService: NotificationService ) { this.settingsForm = this.fb.group({ dataTransferBatchSize: [ this.workflowActionService.getWorkflowContent().settings.dataTransferBatchSize, [Validators.required, Validators.min(1)], ], executionMode: [this.workflowActionService.getWorkflowContent().settings.executionMode], }); } ngOnInit(): void { this.settingsForm .get("dataTransferBatchSize")! .valueChanges.pipe(untilDestroyed(this)) .subscribe((batchSize: number) => { if (this.settingsForm.get("dataTransferBatchSize")!.valid) { this.confirmUpdateDataTransferBatchSize(batchSize); } }); this.settingsForm .get("executionMode")! .valueChanges.pipe(untilDestroyed(this)) .subscribe((mode: ExecutionMode) => { this.updateExecutionMode(mode); }); this.workflowActionService .workflowChanged() .pipe(untilDestroyed(this)) .subscribe(() => { this.settingsForm.patchValue( { dataTransferBatchSize: this.workflowActionService.getWorkflowContent().settings.dataTransferBatchSize, executionMode: this.workflowActionService.getWorkflowContent().settings.executionMode, }, { emitEvent: false } ); }); } public confirmUpdateDataTransferBatchSize(dataTransferBatchSize: number): void { if (dataTransferBatchSize > 0) { this.workflowActionService.setWorkflowDataTransferBatchSize(dataTransferBatchSize); if (this.userService.isLogin()) { this.persistWorkflow(); } } } public persistWorkflow(): void { this.workflowPersistService .persistWorkflow(this.workflowActionService.getWorkflow()) .pipe(untilDestroyed(this)) .subscribe({ error: (e: unknown) => this.notificationService.error((e as Error).message), }); } public updateExecutionMode(mode: ExecutionMode) { this.workflowActionService.updateExecutionMode(mode); this.persistWorkflow(); } protected readonly ExecutionMode = ExecutionMode; } ================================================ FILE: frontend/src/app/workspace/component/left-panel/time-travel/time-travel.component.html ================================================ EID Starting Time {{ execution.eId }} {{ execution.startingTime | date:'short' }}
    ================================================ FILE: frontend/src/app/workspace/component/left-panel/time-travel/time-travel.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .interaction-container { display: grid; } .interaction-item[disabled] { pointer-events: none; color: #163aec; font-weight: bold; } .interaction-item { margin: 5px 0; padding: 10px; border: 1px solid #ccc; border-radius: 5px; background-color: #f9f9f9; text-align: left; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; } .interaction-item:hover { background-color: #e6e6e6; cursor: pointer; } #execution-list { min-width: auto; } ================================================ FILE: frontend/src/app/workspace/component/left-panel/time-travel/time-travel.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { FormlyModule } from "@ngx-formly/core"; import { TEXERA_FORMLY_CONFIG } from "../../../../common/formly/formly-config"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { TimeTravelComponent } from "./time-travel.component"; import { ComputingUnitStatusService } from "../../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; import { MockComputingUnitStatusService } from "../../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service"; import { commonTestProviders } from "../../../../common/testing/test-utils"; describe("TimeTravelComponent", () => { let component: TimeTravelComponent; let fixture: ComponentFixture; let workflowActionService: WorkflowActionService; beforeEach(async () => { await TestBed.configureTestingModule({ providers: [ WorkflowActionService, { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService }, ...commonTestProviders, ], imports: [ TimeTravelComponent, BrowserAnimationsModule, FormsModule, FormlyModule.forRoot(TEXERA_FORMLY_CONFIG), ReactiveFormsModule, HttpClientTestingModule, ], }).compileComponents(); fixture = TestBed.createComponent(TimeTravelComponent); component = fixture.componentInstance; workflowActionService = TestBed.inject(WorkflowActionService); fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/workspace/component/left-panel/time-travel/time-travel.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, OnDestroy, OnInit } from "@angular/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; import { WorkflowExecutionsEntry } from "../../../../dashboard/type/workflow-executions-entry"; import { ExecuteWorkflowService } from "../../../service/execute-workflow/execute-workflow.service"; import { WorkflowVersionService } from "../../../../dashboard/service/user/workflow-version/workflow-version.service"; import { WORKFLOW_EXECUTIONS_API_BASE_URL, WorkflowExecutionsService, } from "../../../../dashboard/service/user/workflow-executions/workflow-executions.service"; import { HttpClient } from "@angular/common/http"; import { Observable, timer } from "rxjs"; import { map } from "rxjs/operators"; import { ReplayExecutionInfo } from "../../../types/workflow-websocket.interface"; import { NotificationService } from "../../../../common/service/notification/notification.service"; import { NgIf, NgFor, DatePipe } from "@angular/common"; import { NzTableComponent, NzTheadComponent, NzTrDirective, NzTableCellDirective, NzThMeasureDirective, NzCellAlignDirective, NzTbodyComponent, } from "ng-zorro-antd/table"; @UntilDestroy() @Component({ selector: "texera-time-travel", templateUrl: "time-travel.component.html", styleUrls: ["time-travel.component.scss"], imports: [ NgIf, NzTableComponent, NzTheadComponent, NzTrDirective, NzTableCellDirective, NzThMeasureDirective, NzCellAlignDirective, NzTbodyComponent, NgFor, DatePipe, ], }) export class TimeTravelComponent implements OnInit, OnDestroy { interactionHistories: { [eid: number]: string[] } = {}; public executionList: WorkflowExecutionsEntry[] = []; expandedRows = new Set(); // Tracks expanded rows by execution ID public revertedToInteraction: ReplayExecutionInfo | undefined = undefined; constructor( private workflowActionService: WorkflowActionService, public executeWorkflowService: ExecuteWorkflowService, private workflowVersionService: WorkflowVersionService, private workflowExecutionsService: WorkflowExecutionsService, private notificationService: NotificationService, private http: HttpClient ) {} ngOnInit(): void { // gets the versions result and updates the workflow versions table displayed on the form timer(0, 5000) // trigger per 5 secs .pipe(untilDestroyed(this)) .subscribe(e => { let wid = this.getWid(); if (wid === undefined) { return; } this.displayExecutionWithLogs(wid); }); } ngOnDestroy() { if (this.revertedToInteraction !== undefined) { this.workflowVersionService.closeReadonlyWorkflowDisplay(); try { this.executeWorkflowService.killWorkflow(); } catch (e) { // ignore exception. } } } public getWid(): number | undefined { return this.workflowActionService.getWorkflowMetadata()?.wid; } toggleRow(eId: number): void { if (this.expandedRows.has(eId)) { this.expandedRows.delete(eId); } else { this.expandedRows.add(eId); this.getInteractionHistory(eId); // Call only if needed } } retrieveInteractionHistory(wid: number, eid: number): Observable { return this.http.get(`${WORKFLOW_EXECUTIONS_API_BASE_URL}/${wid}/interactions/${eid}`); } public retrieveLoggedExecutions(wid: number): Observable { return this.workflowExecutionsService.retrieveWorkflowExecutions(wid).pipe( map(executionList => executionList.filter(execution => { return execution.logLocation ? execution.logLocation.length > 0 : false; }) ) ); } getInteractionHistory(eid: number): void { let wid = this.getWid(); if (wid === undefined) { return; } this.retrieveInteractionHistory(wid, eid) .pipe(untilDestroyed(this)) .subscribe(data => { this.interactionHistories[eid] = data; // TODO:add FULL_REPLAY here to support fault tolerance. }); } /** * calls the http get request service to display the versions result in the table */ displayExecutionWithLogs(wid: number): void { this.retrieveLoggedExecutions(wid) .pipe(untilDestroyed(this)) .subscribe(executions => { this.executionList = executions; this.expandedRows.forEach(row => this.getInteractionHistory(row)); }); } onInteractionClick(vid: number, eid: number, interaction: string) { let wid = this.getWid(); if (wid === undefined) { return; } this.workflowVersionService .retrieveWorkflowByVersion(wid, vid) .pipe(untilDestroyed(this)) .subscribe(workflow => { this.workflowVersionService.displayReadonlyWorkflow(workflow); let replayExecutionInfo = { eid: eid, interaction: interaction }; this.revertedToInteraction = replayExecutionInfo; this.notificationService.info(`start replay to interaction ${interaction} at execution ${eid}`); this.executeWorkflowService.executeWorkflowWithReplay(replayExecutionInfo); }); } protected readonly requestIdleCallback = requestIdleCallback; } ================================================ FILE: frontend/src/app/workspace/component/left-panel/versions-list/versions-list.component.html ================================================ {{column}} {{ getDisplayedVersionId(i, l) }} ================================================ FILE: frontend/src/app/workspace/component/left-panel/versions-list/versions-list.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ #versions-list ::ng-deep { .ant-table-content { font-size: 11px; } .ant-table-cell { padding-right: 0; } .ant-table-tbody > tr:hover > td { background-color: #e6f7ff; } .selected-row > td { background-color: #bae7ff; } } .version-link { color: black; font-size: 11px; padding: 0; } ================================================ FILE: frontend/src/app/workspace/component/left-panel/versions-list/versions-list.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { FormlyModule } from "@ngx-formly/core"; import { TEXERA_FORMLY_CONFIG } from "../../../../common/formly/formly-config"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { VersionsListComponent } from "./versions-list.component"; import { RouterTestingModule } from "@angular/router/testing"; import { commonTestProviders } from "../../../../common/testing/test-utils"; describe("VersionsListComponent", () => { let component: VersionsListComponent; let fixture: ComponentFixture; let workflowActionService: WorkflowActionService; beforeEach(async () => { await TestBed.configureTestingModule({ providers: [WorkflowActionService, ...commonTestProviders], imports: [ VersionsListComponent, BrowserAnimationsModule, FormsModule, FormlyModule.forRoot(TEXERA_FORMLY_CONFIG), ReactiveFormsModule, HttpClientTestingModule, RouterTestingModule.withRoutes([]), ], }).compileComponents(); fixture = TestBed.createComponent(VersionsListComponent); component = fixture.componentInstance; workflowActionService = TestBed.inject(WorkflowActionService); fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/workspace/component/left-panel/versions-list/versions-list.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, OnInit } from "@angular/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; import { WorkflowVersionService } from "../../../../dashboard/service/user/workflow-version/workflow-version.service"; import { WorkflowVersionCollapsableEntry } from "../../../../dashboard/type/workflow-version-entry"; import { ActivatedRoute } from "@angular/router"; import { NgIf, NgFor, NgClass, DatePipe } from "@angular/common"; import { NzTableComponent, NzTheadComponent, NzTrDirective, NzTableCellDirective, NzThMeasureDirective, NzCellAlignDirective, NzTbodyComponent, NzTdAddOnComponent, } from "ng-zorro-antd/table"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; @UntilDestroy() @Component({ selector: "texera-version-list", templateUrl: "versions-list.component.html", styleUrls: ["versions-list.component.scss"], imports: [ NgIf, NzTableComponent, NzTheadComponent, NzTrDirective, NgFor, NzTableCellDirective, NzThMeasureDirective, NzCellAlignDirective, NzTbodyComponent, NgClass, NzTdAddOnComponent, NzSpaceCompactItemDirective, NzButtonComponent, ɵNzTransitionPatchDirective, DatePipe, ], }) export class VersionsListComponent implements OnInit { public versionsList: WorkflowVersionCollapsableEntry[] | undefined; public versionTableHeaders: string[] = ["Version#", "Timestamp"]; public selectedRowIndex: number | null = null; constructor( private workflowActionService: WorkflowActionService, public workflowVersionService: WorkflowVersionService, public route: ActivatedRoute ) {} public getDisplayedVersionId(index: number, count: number) { return count - index; } collapse(index: number, $event: boolean): void { if (this.versionsList == undefined) { return; } if (!$event) { while (++index < this.versionsList.length && !this.versionsList[index].importance) { this.versionsList[index].expand = false; } } else { while (++index < this.versionsList.length && !this.versionsList[index].importance) { this.versionsList[index].expand = true; } } } ngOnInit(): void { // unhighlight all the current highlighted operators/groups/links const elements = this.workflowActionService.getJointGraphWrapper().getCurrentHighlights(); this.workflowActionService.getJointGraphWrapper().unhighlightElements(elements); // gets the versions result and updates the workflow versions table displayed on the form const wid = this.route.snapshot.params.id; if (wid === undefined) { return; } this.workflowVersionService .retrieveVersionsOfWorkflow(wid) .pipe(untilDestroyed(this)) .subscribe(versionsList => { this.versionsList = versionsList.map(version => ({ vId: version.vId, creationTime: version.creationTime, content: version.content, importance: version.importance, expand: false, })); }); } getVersion(vid: number, displayedVersionId: number, index: number) { this.selectedRowIndex = index; this.workflowVersionService .retrieveWorkflowByVersion(this.workflowActionService.getWorkflowMetadata()?.wid, vid) .pipe(untilDestroyed(this)) .subscribe(workflow => { this.workflowVersionService.displayParticularVersion(workflow, vid, displayedVersionId); }); } } ================================================ FILE: frontend/src/app/workspace/component/menu/coeditor-user-icon/coeditor-user-icon.component.css ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ ================================================ FILE: frontend/src/app/workspace/component/menu/coeditor-user-icon/coeditor-user-icon.component.html ================================================
    • Start "shadowing":   {{coeditor.name}} - {{coeditor.clientId}}
    • Stop Shadowing
    ================================================ FILE: frontend/src/app/workspace/component/menu/coeditor-user-icon/coeditor-user-icon.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { CoeditorUserIconComponent } from "./coeditor-user-icon.component"; import { CoeditorPresenceService } from "../../../service/workflow-graph/model/coeditor-presence.service"; import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; import { HttpClient } from "@angular/common/http"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { NzDropdownMenuComponent, NzDropDownModule } from "ng-zorro-antd/dropdown"; import { StubUserService } from "../../../../common/service/user/stub-user.service"; import { UserService } from "../../../../common/service/user/user.service"; import { commonTestProviders } from "../../../../common/testing/test-utils"; describe("CoeditorUserIconComponent", () => { let component: CoeditorUserIconComponent; let fixture: ComponentFixture; let coeditorPresenceService: CoeditorPresenceService; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [CoeditorUserIconComponent, HttpClientTestingModule, NzDropDownModule], providers: [ WorkflowActionService, CoeditorPresenceService, HttpClient, NzDropdownMenuComponent, { provide: UserService, useClass: StubUserService }, ...commonTestProviders, ], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(CoeditorUserIconComponent); component = fixture.componentInstance; coeditorPresenceService = TestBed.inject(CoeditorPresenceService); fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/workspace/component/menu/coeditor-user-icon/coeditor-user-icon.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, Input } from "@angular/core"; import { Coeditor, Role } from "../../../../common/type/user"; import { CoeditorPresenceService } from "../../../service/workflow-graph/model/coeditor-presence.service"; import { UserAvatarComponent } from "../../../../dashboard/component/user/user-avatar/user-avatar.component"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzDropdownDirective, NzDropdownMenuComponent } from "ng-zorro-antd/dropdown"; import { NzMenuDirective, NzMenuItemComponent } from "ng-zorro-antd/menu"; import { NgIf } from "@angular/common"; /** * CoeditorUserIconComponent is the user icon of a co-editor. * * It is also the entry for shadowing mode. */ @Component({ selector: "texera-coeditor-user-icon", templateUrl: "coeditor-user-icon.component.html", styleUrls: ["coeditor-user-icon.component.css"], imports: [ UserAvatarComponent, ɵNzTransitionPatchDirective, NzDropdownDirective, NzDropdownMenuComponent, NzMenuDirective, NgIf, NzMenuItemComponent, ], }) export class CoeditorUserIconComponent { @Input() coeditor: Coeditor = { name: "", email: "", uid: -1, role: Role.REGULAR, comment: "", clientId: "0", joiningReason: "", }; constructor(public coeditorPresenceService: CoeditorPresenceService) {} public shadowCoeditor() { this.coeditorPresenceService.shadowCoeditor(this.coeditor); } stopShadowing() { this.coeditorPresenceService.stopShadowing(); } } ================================================ FILE: frontend/src/app/workspace/component/menu/menu.component.html ================================================ ================================================ FILE: frontend/src/app/workspace/component/menu/menu.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ #menu-container { box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); width: 100%; } #logo { display: inline-block; vertical-align: top; } .disabled-menu-item { pointer-events: none; opacity: 0.5; } #menu-content { display: inline-block; width: 100%; } .workflow-name { min-width: 100px; max-width: 800px; font-size: 18px; padding: 2px 8px; border: none; box-sizing: content-box; &:hover { box-shadow: 0 0 0 1px rgb(202, 202, 202); } &:focus { box-shadow: 0 0 0 2px #007bff; } } #menu-user { height: 36px; margin: 1px; border-bottom: 1px solid #cacaca; display: flex; } #metadata { width: 100%; } #user-icon { width: 120px; } #button-groups { display: flex; align-items: center; justify-content: space-between; min-width: 0; gap: 16px; margin: 3px 5px; } #user-buttons, #execution-buttons, nz-button-group, nz-upload, texera-computing-unit-selection { display: inline-flex; align-items: center; flex: 0 0 auto; } #user-buttons, #execution-buttons, nz-button-group { flex-wrap: nowrap; } #share-button { display: flex; align-items: center; justify-content: center; width: 80px; font-weight: 400; } i { display: inline-flex; align-content: center; justify-content: center; } [nz-button] { width: 32px; padding: 0; } #save-state { font-size: 10px; color: royalblue; font-style: italic; } #run-button { width: 140px; padding-left: 10px; text-align: left; &[disabled].computing-unit-status { color: rgba(0, 0, 0, 0.5); border-color: #d9d9d9; background-color: #f5f5f5; text-shadow: none; box-shadow: none; } .anticon-loading { animation: loading 1.4s infinite linear; } // Special icon styles .anticon-cloud-server { color: #1890ff; } .anticon-disconnect { color: #ff4d4f; } .anticon-exclamation-circle { color: #faad14; } } @keyframes loading { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } #expanded-utilities { display: flex; flex: 1 1 auto; justify-content: center; min-width: 0; overflow: hidden; @media screen and (max-width: 1250px) { display: none; } } #utilities-dropdown-button { flex: 0 0 auto; @media screen and (min-width: 1251px) { display: none; } } texera-user-icon { margin-left: 10px; } texera-coeditor-user-icon { margin-left: -10px; } ::ng-deep .layers-dropdown { user-select: none; } ================================================ FILE: frontend/src/app/workspace/component/menu/menu.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { DatePipe, Location } from "@angular/common"; import { NO_ERRORS_SCHEMA } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { RouterTestingModule } from "@angular/router/testing"; import { NzModalService, NzModalModule, NzModalRef } from "ng-zorro-antd/modal"; import { BehaviorSubject, of, throwError } from "rxjs"; import { MenuComponent } from "./menu.component"; import { OperatorMetadataService } from "../../service/operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../../service/operator-metadata/stub-operator-metadata.service"; import { ComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; import { UserService } from "../../../common/service/user/user.service"; import { StubUserService } from "../../../common/service/user/stub-user.service"; import { commonTestProviders } from "../../../common/testing/test-utils"; import { ExecuteWorkflowService } from "../../service/execute-workflow/execute-workflow.service"; import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; import { ValidationWorkflowService, ValidationOutput } from "../../service/validation/validation-workflow.service"; import { PanelService } from "../../service/panel/panel.service"; import { WorkflowVersionService } from "../../../dashboard/service/user/workflow-version/workflow-version.service"; import { WorkflowPersistService } from "../../../common/service/workflow-persist/workflow-persist.service"; import { NotificationService } from "../../../common/service/notification/notification.service"; import { ExecutionState } from "../../types/execute-workflow.interface"; import { ComputingUnitState } from "../../../common/type/computing-unit-connection.interface"; import { mockPoint, mockScanPredicate } from "../../service/workflow-graph/model/mock-workflow-data"; import { saveAs } from "file-saver"; import type { ModalOptions } from "ng-zorro-antd/modal"; import { ComputingUnitSelectionComponent } from "../power-button/computing-unit-selection.component"; import { WorkflowContent } from "../../../common/type/workflow"; import type { Mocked } from "vitest"; vi.mock("file-saver", () => ({ saveAs: vi.fn() })); describe("MenuComponent", () => { let component: MenuComponent; let fixture: ComponentFixture; let workflowActionService: WorkflowActionService; let executeWorkflowService: ExecuteWorkflowService; let validationWorkflowService: ValidationWorkflowService; let panelService: PanelService; let workflowVersionService: WorkflowVersionService; let workflowPersistService: WorkflowPersistService; let modalService: NzModalService; let notificationService: NotificationService; let location: Location; let validationStream$: BehaviorSubject; beforeEach(async () => { TestBed.overrideComponent(MenuComponent, { set: { template: "" }, }); await TestBed.configureTestingModule({ imports: [MenuComponent, HttpClientTestingModule, RouterTestingModule.withRoutes([]), NzModalModule], providers: [ DatePipe, { provide: OperatorMetadataService, useClass: StubOperatorMetadataService }, { provide: ComputingUnitStatusService, useValue: { getSelectedComputingUnit: () => of(null), getStatus: () => of(ComputingUnitState.NoComputingUnit), }, }, { provide: UserService, useClass: StubUserService }, ...commonTestProviders, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); workflowActionService = TestBed.inject(WorkflowActionService); executeWorkflowService = TestBed.inject(ExecuteWorkflowService); validationWorkflowService = TestBed.inject(ValidationWorkflowService); panelService = TestBed.inject(PanelService); workflowVersionService = TestBed.inject(WorkflowVersionService); workflowPersistService = TestBed.inject(WorkflowPersistService); modalService = TestBed.inject(NzModalService); notificationService = TestBed.inject(NotificationService); location = TestBed.inject(Location); validationStream$ = new BehaviorSubject({ errors: {}, workflowEmpty: false }); vi.spyOn(validationWorkflowService, "getWorkflowValidationErrorStream").mockReturnValue( validationStream$.asObservable() ); fixture = TestBed.createComponent(MenuComponent); component = fixture.componentInstance; fixture.detectChanges(); vi.mocked(saveAs).mockClear(); }); it("should create", () => { expect(component).toBeTruthy(); }); describe("getRunButtonBehavior", () => { it("returns 'Invalid Workflow' when the workflow is invalid", () => { component.isWorkflowValid = false; component.isWorkflowEmpty = false; const behavior = component.getRunButtonBehavior(); expect(behavior.text).toBe("Invalid Workflow"); expect(behavior.icon).toBe("warning"); expect(behavior.disable).toBe(true); }); it("returns 'Empty Workflow' when the workflow has no operators", () => { component.isWorkflowValid = true; component.isWorkflowEmpty = true; const behavior = component.getRunButtonBehavior(); expect(behavior.text).toBe("Empty Workflow"); expect(behavior.icon).toBe("info-circle"); expect(behavior.disable).toBe(true); }); it("returns 'Connect' when no computing unit is attached", () => { component.isWorkflowValid = true; component.isWorkflowEmpty = false; component.computingUnitStatus = ComputingUnitState.NoComputingUnit; const behavior = component.getRunButtonBehavior(); expect(behavior.text).toBe("Connect"); expect(behavior.icon).toBe("plus-circle"); expect(behavior.disable).toBe(false); }); it("returns 'Run' when connected and execution is uninitialized", () => { component.isWorkflowValid = true; component.isWorkflowEmpty = false; component.computingUnitStatus = ComputingUnitState.Running; Object.defineProperty(component.workflowWebsocketService, "isConnected", { get: () => true, configurable: true }); component.executionState = ExecutionState.Uninitialized; const behavior = component.getRunButtonBehavior(); expect(behavior.text).toBe("Run"); expect(behavior.icon).toBe("play-circle"); expect(behavior.disable).toBe(false); }); it("returns 'Pause' while a workflow is running", () => { component.isWorkflowValid = true; component.isWorkflowEmpty = false; component.computingUnitStatus = ComputingUnitState.Running; Object.defineProperty(component.workflowWebsocketService, "isConnected", { get: () => true, configurable: true }); component.executionState = ExecutionState.Running; const pauseSpy = vi.spyOn(executeWorkflowService, "pauseWorkflow").mockImplementation(() => {}); const behavior = component.getRunButtonBehavior(); behavior.onClick(); expect(behavior.text).toBe("Pause"); expect(behavior.disable).toBe(false); expect(pauseSpy).toHaveBeenCalled(); }); it("returns 'Resume' when execution is paused", () => { component.isWorkflowValid = true; component.isWorkflowEmpty = false; component.computingUnitStatus = ComputingUnitState.Running; Object.defineProperty(component.workflowWebsocketService, "isConnected", { get: () => true, configurable: true }); component.executionState = ExecutionState.Paused; const resumeSpy = vi.spyOn(executeWorkflowService, "resumeWorkflow").mockImplementation(() => {}); const behavior = component.getRunButtonBehavior(); behavior.onClick(); expect(behavior.text).toBe("Resume"); expect(resumeSpy).toHaveBeenCalled(); }); it("returns 'Connecting' when a unit exists but the websocket is not connected", () => { component.isWorkflowValid = true; component.isWorkflowEmpty = false; component.computingUnitStatus = ComputingUnitState.Running; Object.defineProperty(component.workflowWebsocketService, "isConnected", { get: () => false, configurable: true, }); const behavior = component.getRunButtonBehavior(); expect(behavior.text).toBe("Connecting"); expect(behavior.disable).toBe(true); }); }); it("applyRunButtonBehavior copies the behavior onto the bound fields", () => { const handler = () => {}; component.applyRunButtonBehavior({ text: "Custom", icon: "custom-icon", disable: true, onClick: handler, }); expect(component.runButtonText).toBe("Custom"); expect(component.runIcon).toBe("custom-icon"); expect(component.runDisable).toBe(true); expect(component.onClickRunHandler).toBe(handler); }); it("re-applies run button behavior when the validation stream reports an empty workflow", () => { validationStream$.next({ errors: {}, workflowEmpty: true }); expect(component.isWorkflowEmpty).toBe(true); expect(component.runButtonText).toBe("Empty Workflow"); expect(component.runDisable).toBe(true); }); describe("hasOperators", () => { it("returns false on an empty graph", () => { expect(component.hasOperators()).toBe(false); }); it("returns true once an operator is added", () => { workflowActionService.addOperator(mockScanPredicate, mockPoint); expect(component.hasOperators()).toBe(true); }); }); it("onClickAddCommentBox delegates to the workflow action service", () => { const addCommentBoxSpy = vi.spyOn(workflowActionService, "addCommentBox"); component.onClickAddCommentBox(); expect(addCommentBoxSpy).toHaveBeenCalledTimes(1); }); it("onClickDeleteAllOperators removes every operator from the graph", () => { workflowActionService.addOperator(mockScanPredicate, mockPoint); expect(workflowActionService.getTexeraGraph().getAllOperators().length).toBe(1); component.onClickDeleteAllOperators(); expect(workflowActionService.getTexeraGraph().getAllOperators().length).toBe(0); }); it("onClickAutoLayout is a no-op when there are no operators", () => { const autoLayoutSpy = vi.spyOn(workflowActionService, "autoLayoutWorkflow"); component.onClickAutoLayout(); expect(autoLayoutSpy).not.toHaveBeenCalled(); }); it("onClickAutoLayout invokes auto layout when operators are present", () => { workflowActionService.addOperator(mockScanPredicate, mockPoint); const autoLayoutSpy = vi.spyOn(workflowActionService, "autoLayoutWorkflow").mockImplementation(() => {}); component.onClickAutoLayout(); expect(autoLayoutSpy).toHaveBeenCalledTimes(1); }); it("handleKill delegates to executeWorkflowService.killWorkflow", () => { const killSpy = vi.spyOn(executeWorkflowService, "killWorkflow").mockImplementation(() => {}); component.handleKill(); expect(killSpy).toHaveBeenCalledTimes(1); }); it("handleCheckpoint delegates to executeWorkflowService.takeGlobalCheckpoint", () => { const checkpointSpy = vi.spyOn(executeWorkflowService, "takeGlobalCheckpoint").mockImplementation(() => {}); component.handleCheckpoint(); expect(checkpointSpy).toHaveBeenCalledTimes(1); }); it("onClickClosePanels and onClickResetPanels delegate to PanelService", () => { const closeSpy = vi.spyOn(panelService, "closePanels").mockImplementation(() => {}); const resetSpy = vi.spyOn(panelService, "resetPanels").mockImplementation(() => {}); component.onClickClosePanels(); component.onClickResetPanels(); expect(closeSpy).toHaveBeenCalledTimes(1); expect(resetSpy).toHaveBeenCalledTimes(1); }); describe("runWorkflow", () => { beforeEach(() => { component.computingUnitSelectionComponent = { newComputingUnitName: "", showAddComputeUnitModalVisible: vi.fn(), } as unknown as Mocked; }); it("does nothing when the workflow is invalid", () => { component.isWorkflowValid = false; component.isWorkflowEmpty = false; const executeSpy = vi.spyOn(executeWorkflowService, "executeWorkflowWithEmailNotification"); component.runWorkflow(); expect(executeSpy).not.toHaveBeenCalled(); expect(component.computingUnitSelectionComponent.showAddComputeUnitModalVisible).not.toHaveBeenCalled(); }); it("does nothing when the workflow is empty", () => { component.isWorkflowValid = true; component.isWorkflowEmpty = true; const executeSpy = vi.spyOn(executeWorkflowService, "executeWorkflowWithEmailNotification"); component.runWorkflow(); expect(executeSpy).not.toHaveBeenCalled(); }); it("opens the add-computing-unit modal when no unit is connected", () => { component.isWorkflowValid = true; component.isWorkflowEmpty = false; component.computingUnitStatus = ComputingUnitState.NoComputingUnit; component.currentWorkflowName = "wf"; const executeSpy = vi.spyOn(executeWorkflowService, "executeWorkflowWithEmailNotification"); component.runWorkflow(); expect(component.computingUnitSelectionComponent.newComputingUnitName).toBe("wf's Computing Unit"); expect(component.computingUnitSelectionComponent.showAddComputeUnitModalVisible).toHaveBeenCalledTimes(1); expect(executeSpy).not.toHaveBeenCalled(); }); it("submits the execution when connected", () => { component.isWorkflowValid = true; component.isWorkflowEmpty = false; component.computingUnitStatus = ComputingUnitState.Running; component.currentExecutionName = "exec-1"; const executeSpy = vi .spyOn(executeWorkflowService, "executeWorkflowWithEmailNotification") .mockImplementation(() => {}); component.runWorkflow(); expect(executeSpy).toHaveBeenCalledWith("exec-1", expect.any(Boolean)); }); }); it("onWorkflowNameChange forwards the new name to the workflow action service", () => { const setNameSpy = vi.spyOn(workflowActionService, "setWorkflowName"); component.currentWorkflowName = "renamed"; component.onWorkflowNameChange(); expect(setNameSpy).toHaveBeenCalledWith("renamed"); }); describe("onClickExportWorkflow (save)", () => { it("serializes the workflow content as JSON and downloads it under the workflow name", () => { const fakeContent = { operators: [{ operatorID: "op1" }], links: [], commentBoxes: [], settings: {}, } as unknown as WorkflowContent; vi.spyOn(workflowActionService, "getWorkflowContent").mockReturnValue(fakeContent); component.currentWorkflowName = "my-workflow"; component.onClickExportWorkflow(); expect(saveAs).toHaveBeenCalledTimes(1); const [blobArg, fileNameArg] = vi.mocked(saveAs).mock.calls[0] as [Blob, string]; expect(fileNameArg).toBe("my-workflow.json"); expect(blobArg).toBeInstanceOf(Blob); expect(blobArg.type).toBe("text/plain;charset=utf-8"); }); }); describe("version history", () => { it("onClickGetAllVersions delegates to workflowVersionService.displayWorkflowVersions", () => { const displaySpy = vi.spyOn(workflowVersionService, "displayWorkflowVersions").mockImplementation(() => {}); component.onClickGetAllVersions(); expect(displaySpy).toHaveBeenCalledTimes(1); }); it("closeParticularVersionDisplay delegates to workflowVersionService", () => { const closeSpy = vi.spyOn(workflowVersionService, "closeParticularVersionDisplay").mockImplementation(() => {}); component.closeParticularVersionDisplay(); expect(closeSpy).toHaveBeenCalledTimes(1); }); it("revertToVersion reverts and then persists the workflow", () => { const revertSpy = vi.spyOn(workflowVersionService, "revertToVersion").mockImplementation(() => {}); const persistSpy = vi .spyOn(workflowPersistService, "persistWorkflow") .mockReturnValue(of(workflowActionService.getWorkflow())); component.revertToVersion(); expect(revertSpy).toHaveBeenCalledTimes(1); expect(persistSpy).toHaveBeenCalledTimes(1); }); it("cloneVersion notifies success and closes the version panel when cloning succeeds", () => { vi.spyOn(workflowVersionService, "cloneWorkflowVersion").mockReturnValue(of(42)); const successSpy = vi.spyOn(notificationService, "success").mockImplementation(() => {}); const closeSpy = vi.spyOn(workflowVersionService, "closeParticularVersionDisplay").mockImplementation(() => {}); component.cloneVersion(); expect(successSpy).toHaveBeenCalledTimes(1); expect(successSpy.mock.calls[0][0]).toContain("42"); expect(closeSpy).toHaveBeenCalledTimes(1); }); it("cloneVersion shows an error notification and does not close the panel when cloning fails", () => { vi.spyOn(workflowVersionService, "cloneWorkflowVersion").mockReturnValue(throwError(() => new Error("boom"))); const errorSpy = vi.spyOn(notificationService, "error").mockImplementation(() => {}); const successSpy = vi.spyOn(notificationService, "success").mockImplementation(() => {}); const closeSpy = vi.spyOn(workflowVersionService, "closeParticularVersionDisplay").mockImplementation(() => {}); component.cloneVersion(); expect(errorSpy).toHaveBeenCalledTimes(1); expect(successSpy).not.toHaveBeenCalled(); expect(closeSpy).not.toHaveBeenCalled(); }); }); describe("onClickOpenShareAccess (share)", () => { it("looks up workflow owners and opens the share-access modal", async () => { vi.spyOn(workflowPersistService, "retrieveOwners").mockReturnValue(of(["alice@example.com"])); const fakeModalRef = { afterClose: of(undefined) } as unknown as NzModalRef; const createSpy = vi.spyOn(modalService, "create").mockReturnValue(fakeModalRef); component.workflowId = 7; component.writeAccess = true; await component.onClickOpenShareAccess(); expect(createSpy).toHaveBeenCalledTimes(1); const config = createSpy.mock.calls[0][0] as ModalOptions; expect(config.nzTitle).toBe("Share this workflow with others"); expect(config.nzData).toEqual( expect.objectContaining({ writeAccess: true, type: "workflow", id: 7, allOwners: ["alice@example.com"], inWorkspace: true, }) ); }); }); it("onClickCreateNewWorkflow resets the graph and navigates back to root", () => { const resetSpy = vi.spyOn(workflowActionService, "resetAsNewWorkflow").mockImplementation(() => {}); const goSpy = vi.spyOn(location, "go").mockImplementation(() => {}); component.onClickCreateNewWorkflow(); expect(resetSpy).toHaveBeenCalledTimes(1); expect(goSpy).toHaveBeenCalledWith("/"); }); it("onClickRestoreZoomOffsetDefault delegates to the joint graph wrapper", () => { const restoreSpy = vi .spyOn(workflowActionService.getJointGraphWrapper(), "restoreDefaultZoomAndOffset") .mockImplementation(() => {}); component.onClickRestoreZoomOffsetDefault(); expect(restoreSpy).toHaveBeenCalledTimes(1); }); it("onClickEditDescription opens the markdown description modal seeded with the current description", () => { vi.spyOn(workflowActionService, "getWorkflow").mockReturnValue({ content: { operators: [], links: [], commentBoxes: [], settings: {} } as unknown as WorkflowContent, name: "wf", description: "hello world", wid: 1, creationTime: undefined, lastModifiedTime: undefined, readonly: false, isPublished: 0, }); const fakeModalRef = { afterClose: of(undefined), getContentComponent: () => ({ descriptionChange: of() }), close: vi.fn(), } as unknown as NzModalRef; const createSpy = vi.spyOn(modalService, "create").mockReturnValue(fakeModalRef); component.onClickEditDescription(); expect(createSpy).toHaveBeenCalledTimes(1); const config = createSpy.mock.calls[0][0] as ModalOptions; expect(config.nzTitle).toBe("Edit Workflow Description"); expect(config.nzData).toEqual({ description: "hello world" }); }); it("onClickExportExecutionResult opens the result-exportation modal with the current workflow name", () => { const fakeModalRef = { afterClose: of(undefined) } as unknown as NzModalRef; const createSpy = vi.spyOn(modalService, "create").mockReturnValue(fakeModalRef); component.currentWorkflowName = "report-wf"; component.onClickExportExecutionResult(); expect(createSpy).toHaveBeenCalledTimes(1); const config = createSpy.mock.calls[0][0] as ModalOptions; expect(config.nzTitle).toBe("Export All Operators Result"); expect(config.nzData).toEqual(expect.objectContaining({ workflowName: "report-wf", sourceTriggered: "menu" })); }); }); ================================================ FILE: frontend/src/app/workspace/component/menu/menu.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { DatePipe, Location, NgIf, NgFor, NgTemplateOutlet } from "@angular/common"; import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { Router, RouterLink } from "@angular/router"; import { UserService } from "../../../common/service/user/user.service"; import { DEFAULT_WORKFLOW_NAME, WorkflowPersistService, } from "../../../common/service/workflow-persist/workflow-persist.service"; import { Workflow, WorkflowContent } from "../../../common/type/workflow"; import { ExecuteWorkflowService } from "../../service/execute-workflow/execute-workflow.service"; import { UndoRedoService } from "../../service/undo-redo/undo-redo.service"; import { ValidationWorkflowService } from "../../service/validation/validation-workflow.service"; import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; import { ExecutionState } from "../../types/execute-workflow.interface"; import { WorkflowWebsocketService } from "../../service/workflow-websocket/workflow-websocket.service"; import { WorkflowResultExportService } from "../../service/workflow-result-export/workflow-result-export.service"; import { catchError, debounceTime, filter, mergeMap, tap } from "rxjs/operators"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { WorkflowUtilService } from "../../service/workflow-graph/util/workflow-util.service"; import { WorkflowVersionService } from "../../../dashboard/service/user/workflow-version/workflow-version.service"; import { UserProjectService } from "../../../dashboard/service/user/project/user-project.service"; import { NzUploadFile, NzUploadComponent } from "ng-zorro-antd/upload"; import { saveAs } from "file-saver"; import { NotificationService } from "src/app/common/service/notification/notification.service"; import { OperatorMenuService } from "../../service/operator-menu/operator-menu.service"; import { CoeditorPresenceService } from "../../service/workflow-graph/model/coeditor-presence.service"; import { firstValueFrom, of, Subscription, timer } from "rxjs"; import { isDefined } from "../../../common/util/predicate"; import { NzModalService } from "ng-zorro-antd/modal"; import { ResultExportationComponent } from "../result-exportation/result-exportation.component"; import { ReportGenerationService } from "../../service/report-generation/report-generation.service"; import { ShareAccessComponent } from "src/app/dashboard/component/user/share-access/share-access.component"; import { PanelService } from "../../service/panel/panel.service"; import { DASHBOARD_USER_WORKFLOW } from "../../../app-routing.constant"; import { ComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; import { ComputingUnitState } from "../../../common/type/computing-unit-connection.interface"; import { ComputingUnitSelectionComponent } from "../power-button/computing-unit-selection.component"; import { GuiConfigService } from "../../../common/service/gui-config.service"; import { DashboardWorkflowComputingUnit } from "../../../common/type/workflow-computing-unit"; import { Privilege } from "../../../dashboard/type/share-access.interface"; import { MarkdownDescriptionComponent } from "../../../dashboard/component/user/markdown-description/markdown-description.component"; import { NzSpaceCompactItemDirective, NzSpaceCompactComponent } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzAvatarComponent } from "ng-zorro-antd/avatar"; import { FormsModule } from "@angular/forms"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { CoeditorUserIconComponent } from "./coeditor-user-icon/coeditor-user-icon.component"; import { UserIconComponent } from "../../../dashboard/component/user/user-icon/user-icon.component"; import { NzDropdownDirective, NzDropdownMenuComponent } from "ng-zorro-antd/dropdown"; import { NzMenuDirective, NzMenuItemComponent } from "ng-zorro-antd/menu"; import { NzCheckboxComponent } from "ng-zorro-antd/checkbox"; import { NzPopoverDirective } from "ng-zorro-antd/popover"; import { NzSwitchComponent } from "ng-zorro-antd/switch"; import { NzBadgeComponent } from "ng-zorro-antd/badge"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; /** * MenuComponent is the top level menu bar that shows * the Texera title and workflow execution button * * This Component will be the only Component capable of executing * the workflow in the WorkflowEditor Component. * * Clicking the run button on the top-right hand corner will begin * the execution. During execution, the run button will be replaced * with a pause/resume button to show that graph is under execution. * * @author Zuozhi Wang * @author Henry Chen * */ @UntilDestroy() @Component({ selector: "texera-menu", templateUrl: "menu.component.html", styleUrls: ["menu.component.scss"], imports: [ NgIf, NzSpaceCompactItemDirective, NzButtonComponent, ɵNzTransitionPatchDirective, NzIconDirective, NzAvatarComponent, FormsModule, NzWaveDirective, NgFor, CoeditorUserIconComponent, UserIconComponent, RouterLink, NzUploadComponent, NzDropdownDirective, NzDropdownMenuComponent, NzMenuDirective, NzMenuItemComponent, NzCheckboxComponent, NgTemplateOutlet, ComputingUnitSelectionComponent, NzPopoverDirective, NzSwitchComponent, NzBadgeComponent, NzTooltipDirective, DatePipe, NzSpaceCompactComponent, ], }) export class MenuComponent implements OnInit, OnDestroy { public executionState: ExecutionState; // set this to true when the workflow is started public ExecutionState = ExecutionState; // make Angular HTML access enum definition public ComputingUnitState = ComputingUnitState; // make Angular HTML access enum definition public isWorkflowValid: boolean = true; // this will check whether the workflow error or not public isWorkflowEmpty: boolean = false; public isSaving: boolean = false; public isWorkflowModifiable: boolean = false; public workflowId?: number; public isExportDeactivate: boolean = false; public showRegion: boolean = false; public showGrid: boolean = false; public showNumWorkers: boolean = false; public showStatus: boolean = false; protected readonly DASHBOARD_USER_WORKFLOW = DASHBOARD_USER_WORKFLOW; @Input() public writeAccess: boolean = false; @Input() public pid?: number = undefined; @Input() public autoSaveState: string = ""; @Input() public currentWorkflowName: string = ""; // reset workflowName @Input() public currentExecutionName: string = ""; // reset executionName @Input() public particularVersionDate: string = ""; // placeholder for the metadata information of a particular workflow version @ViewChild("workflowNameInput") workflowNameInput: ElementRef | undefined; // variable bound with HTML to decide if the running spinner should show public runButtonText = "Run"; public runIcon = "play-circle"; public runDisable = false; public executionDuration = 0; private durationUpdateSubscription: Subscription = new Subscription(); // flag to display a particular version in the current canvas public displayParticularWorkflowVersion: boolean = false; public onClickRunHandler: () => void; // Computing unit status variables private computingUnitStatusSubscription: Subscription = new Subscription(); public selectedComputingUnit: DashboardWorkflowComputingUnit | null = null; public computingUnitStatus: ComputingUnitState = ComputingUnitState.NoComputingUnit; @ViewChild(ComputingUnitSelectionComponent) computingUnitSelectionComponent!: ComputingUnitSelectionComponent; constructor( public executeWorkflowService: ExecuteWorkflowService, public workflowActionService: WorkflowActionService, public workflowWebsocketService: WorkflowWebsocketService, private location: Location, public undoRedoService: UndoRedoService, public validationWorkflowService: ValidationWorkflowService, public workflowPersistService: WorkflowPersistService, public workflowVersionService: WorkflowVersionService, public userService: UserService, private datePipe: DatePipe, public workflowResultExportService: WorkflowResultExportService, public workflowUtilService: WorkflowUtilService, private userProjectService: UserProjectService, private notificationService: NotificationService, public operatorMenu: OperatorMenuService, public coeditorPresenceService: CoeditorPresenceService, private modalService: NzModalService, private reportGenerationService: ReportGenerationService, private panelService: PanelService, private computingUnitStatusService: ComputingUnitStatusService, protected config: GuiConfigService, private router: Router ) { workflowWebsocketService .subscribeToEvent("ExecutionDurationUpdateEvent") .pipe(untilDestroyed(this)) .subscribe(event => { this.executionDuration = event.duration; this.durationUpdateSubscription.unsubscribe(); if (event.isRunning) { this.durationUpdateSubscription = timer(1000, 1000) .pipe(untilDestroyed(this)) .subscribe(() => { this.executionDuration += 1000; }); } }); this.executionState = executeWorkflowService.getExecutionState().state; // return the run button after the execution is finished, either // when the value is valid or invalid const initBehavior = this.getRunButtonBehavior(); this.runButtonText = initBehavior.text; this.runIcon = initBehavior.icon; this.runDisable = initBehavior.disable; this.onClickRunHandler = initBehavior.onClick; this.registerWorkflowModifiableChangedHandler(); this.registerWorkflowIdUpdateHandler(); // Subscribe to computing unit this.subscribeToComputingUnitSelection(); this.subscribeToComputingUnitStatus(); } public ngOnInit(): void { this.executeWorkflowService .getExecutionStateStream() .pipe(untilDestroyed(this)) .subscribe(event => { this.executionState = event.current.state; this.applyRunButtonBehavior(this.getRunButtonBehavior()); }); // set the map of operatorStatusMap this.validationWorkflowService .getWorkflowValidationErrorStream() .pipe(untilDestroyed(this)) .subscribe(value => { this.isWorkflowEmpty = value.workflowEmpty; this.isWorkflowValid = Object.keys(value.errors).length === 0; this.applyRunButtonBehavior(this.getRunButtonBehavior()); }); // Subscribe to WorkflowResultExportService observable this.workflowResultExportService .getExportOnAllOperatorsStatusStream() .pipe(untilDestroyed(this)) .subscribe(hasResultToExport => { this.isExportDeactivate = !this.config.env.exportExecutionResultEnabled || !hasResultToExport; }); this.registerWorkflowMetadataDisplayRefresh(); this.handleWorkflowVersionDisplay(); } ngOnDestroy(): void { this.workflowResultExportService.resetFlags(); this.computingUnitStatusSubscription.unsubscribe(); } private subscribeToComputingUnitSelection(): void { this.computingUnitStatusService .getSelectedComputingUnit() .pipe(untilDestroyed(this)) .subscribe(unit => { this.selectedComputingUnit = unit; }); } /** * Subscribe to computing unit status changes from the ComputingUnitStatusService */ private subscribeToComputingUnitStatus(): void { // Subscribe to get the computing unit status this.computingUnitStatusSubscription.add( this.computingUnitStatusService .getStatus() .pipe(untilDestroyed(this)) .subscribe(status => { this.computingUnitStatus = status; this.applyRunButtonBehavior(this.getRunButtonBehavior()); }) ); } /** * Dynamically adjusts the width of the workflow name input field * by creating a hidden span element to measure the text width. */ public adjustWorkflowNameWidth(): void { const input = this.workflowNameInput?.nativeElement; if (!input) return; const tempSpan = document.createElement("span"); tempSpan.style.visibility = "hidden"; tempSpan.style.position = "absolute"; tempSpan.style.whiteSpace = "pre"; tempSpan.style.font = getComputedStyle(input).font; tempSpan.textContent = input.value || input.placeholder; document.body.appendChild(tempSpan); const width = Math.min(tempSpan.offsetWidth + 20, 800); // +20 for padding input.style.width = `${width}px`; document.body.removeChild(tempSpan); } toggleNumWorkers() { this.workflowActionService .getJointGraphWrapper() .mainPaper.el.classList.toggle("hide-worker-count", !this.showNumWorkers); this.applyOperatorStatusPosition(); } toggleStatus() { this.workflowActionService .getJointGraphWrapper() .mainPaper.el.classList.toggle("hide-operator-status", !this.showStatus); this.applyOperatorStatusPosition(); } private applyOperatorStatusPosition(): void { const refY = this.showNumWorkers ? -55 : -35; const paperModel = this.workflowActionService.getJointGraphWrapper().mainPaper.model as any; paperModel.getElements().forEach((el: any) => { el.attr(".operator-status/ref-x", -10); el.attr(".operator-status/ref-y", refY); }); } public async onClickOpenShareAccess(): Promise { const modalRef = this.modalService.create({ nzContent: ShareAccessComponent, nzData: { writeAccess: this.writeAccess, type: "workflow", id: this.workflowId, allOwners: await firstValueFrom(this.workflowPersistService.retrieveOwners()), inWorkspace: true, }, nzFooter: null, nzTitle: "Share this workflow with others", nzCentered: true, nzWidth: "800px", }); modalRef.afterClose.pipe(untilDestroyed(this)).subscribe(result => { if (result?.userRevokedOwnAccess) { this.router.navigate([DASHBOARD_USER_WORKFLOW]); } }); } // apply a behavior to the run button via bound variables public applyRunButtonBehavior(behavior: { text: string; icon: string; disable: boolean; onClick: () => void }) { this.runButtonText = behavior.text; this.runIcon = behavior.icon; this.runDisable = behavior.disable; this.onClickRunHandler = behavior.onClick; } public getRunButtonBehavior(): { text: string; icon: string; disable: boolean; onClick: () => void; } { // If workflow is invalid, always disable and show "Invalid Workflow" if (!this.isWorkflowValid) { return { text: "Invalid Workflow", icon: "warning", disable: true, onClick: () => {}, }; } // If workflow is empty, always disable and show "Empty Workflow" if (this.isWorkflowEmpty) { return { text: "Empty Workflow", icon: "info-circle", disable: true, onClick: () => {}, }; } // This handles the case where a unit exists but we're not connected to it if (this.computingUnitStatus !== ComputingUnitState.NoComputingUnit && !this.workflowWebsocketService.isConnected) { return { text: "Connecting", icon: "loading", disable: true, onClick: () => {}, }; } // no computing unit, show "Connect" button if (this.computingUnitStatus === ComputingUnitState.NoComputingUnit) { return { text: "Connect", icon: "plus-circle", disable: false, onClick: () => this.runWorkflow(), }; } // Handle execution states when connected to a running computing unit switch (this.executionState) { case ExecutionState.Uninitialized: case ExecutionState.Completed: case ExecutionState.Terminated: case ExecutionState.Killed: case ExecutionState.Failed: return { text: "Run", icon: "play-circle", disable: false, onClick: () => this.runWorkflow(), }; case ExecutionState.Initializing: return { text: "Submitting", icon: "loading", disable: true, onClick: () => {}, }; case ExecutionState.Running: return { text: "Pause", icon: "loading", disable: false, onClick: () => this.executeWorkflowService.pauseWorkflow(), }; case ExecutionState.Paused: return { text: "Resume", icon: "pause-circle", disable: false, onClick: () => this.executeWorkflowService.resumeWorkflow(), }; case ExecutionState.Pausing: return { text: "Pausing", icon: "loading", disable: true, onClick: () => {}, }; case ExecutionState.Resuming: return { text: "Resuming", icon: "loading", disable: true, onClick: () => {}, }; case ExecutionState.Recovering: return { text: "Recovering", icon: "loading", disable: true, onClick: () => {}, }; default: return { text: "Run", icon: "play-circle", disable: false, onClick: () => this.runWorkflow(), }; } } public onClickAddCommentBox(): void { this.workflowActionService.addCommentBox(this.workflowUtilService.getNewCommentBox()); } public handleKill(): void { this.executeWorkflowService.killWorkflow(); } public handleCheckpoint(): void { this.executeWorkflowService.takeGlobalCheckpoint(); } public onClickClosePanels(): void { this.panelService.closePanels(); } public onClickResetPanels(): void { this.panelService.resetPanels(); } /** * get the html to export all results. */ public onClickGenerateReport(): void { // Get notification and set nzDuration to 0 to prevent it from auto-closing this.notificationService.blank("", "The report is being generated...", { nzDuration: 0 }); const workflowName = this.currentWorkflowName; const WorkflowContent: WorkflowContent = this.workflowActionService.getWorkflowContent(); // Extract operatorIDs from the parsed payload const operatorIds = WorkflowContent.operators.map((operator: { operatorID: string }) => operator.operatorID); // Invokes the method of the report printing service this.reportGenerationService .generateWorkflowSnapshot(workflowName) .pipe(untilDestroyed(this)) .subscribe({ next: (workflowSnapshotURL: string) => { this.reportGenerationService .getAllOperatorResults(operatorIds) .pipe(untilDestroyed(this)) .subscribe({ next: (allResults: { operatorId: string; html: string }[]) => { const sortedResults = operatorIds.map( id => allResults.find(result => result.operatorId === id)?.html || "" ); // Generate the final report as HTML after all results are retrieved this.reportGenerationService.generateReportAsHtml(workflowSnapshotURL, sortedResults, workflowName); // Close the notification after the report is generated this.notificationService.remove(); this.notificationService.success("Report successfully generated."); }, error: (error: unknown) => { this.notificationService.error("Error in retrieving operator results: " + (error as Error).message); // Close the notification on error this.notificationService.remove(); }, }); }, error: (e: unknown) => { this.notificationService.error((e as Error).message); // Close the notification on error this.notificationService.remove(); }, }); } public toggleGrid(): void { this.workflowActionService.getJointGraphWrapper().mainPaper.setGridSize(this.showGrid ? 2 : 1); } public toggleRegion(): void { this.workflowActionService .getJointGraphWrapper() .jointGraph.getElements() .filter(el => el.get("type") === "region") // small improvement here too .forEach(el => el.attr("body/visibility", this.showRegion ? "visible" : "hidden")); } /** * This method will run the autoLayout function * */ public onClickAutoLayout(): void { if (!this.hasOperators()) { return; } this.workflowActionService.autoLayoutWorkflow(); } /** * This is the handler for the execution result export button. * */ public onClickExportExecutionResult(): void { this.modalService.create({ nzTitle: "Export All Operators Result", nzContent: ResultExportationComponent, nzData: { workflowName: this.currentWorkflowName, sourceTriggered: "menu", }, nzFooter: null, }); } /** * Restore paper default zoom ratio and paper offset */ public onClickRestoreZoomOffsetDefault(): void { this.workflowActionService.getJointGraphWrapper().restoreDefaultZoomAndOffset(); } /** * Delete all operators (including hidden ones) on the graph. */ public onClickDeleteAllOperators(): void { const allOperatorIDs = this.workflowActionService .getTexeraGraph() .getAllOperators() .map(op => op.operatorID); this.workflowActionService.deleteOperatorsAndLinks(allOperatorIDs); } public onClickImportWorkflow = (file: NzUploadFile): boolean => { const reader = new FileReader(); reader.readAsText(file as any); reader.onload = () => { try { const result = reader.result; if (typeof result !== "string") { throw new Error("incorrect format: file is not a string"); } const workflowContent = JSON.parse(result) as WorkflowContent; // set the workflow name using the file name without the extension const fileExtensionIndex = file.name.lastIndexOf("."); var workflowName: string; if (fileExtensionIndex === -1) { workflowName = file.name; } else { workflowName = file.name.substring(0, fileExtensionIndex); } if (workflowName.trim() === "") { workflowName = DEFAULT_WORKFLOW_NAME; } const workflow: Workflow = { content: workflowContent, name: workflowName, description: undefined, wid: undefined, creationTime: undefined, lastModifiedTime: undefined, readonly: false, isPublished: 0, }; this.workflowActionService.enableWorkflowModification(); // load the fetched workflow this.workflowActionService.reloadWorkflow(workflow, true); // clear stack this.undoRedoService.clearUndoStack(); this.undoRedoService.clearRedoStack(); } catch (error) { this.notificationService.error( "An error occurred when importing the workflow. Please import a workflow json file." ); console.error(error); } }; return false; }; public onClickExportWorkflow(): void { const workflowContent: WorkflowContent = this.workflowActionService.getWorkflowContent(); const workflowContentJson = JSON.stringify(workflowContent, null, 2); const fileName = this.currentWorkflowName + ".json"; saveAs(new Blob([workflowContentJson], { type: "text/plain;charset=utf-8" }), fileName); } /** * Calls Markdown Description Component */ public onClickEditDescription(): void { const currentWorkflow = this.workflowActionService.getWorkflow(); const currentDescription = currentWorkflow.description ?? ""; const modalRef = this.modalService.create({ nzTitle: "Edit Workflow Description", nzContent: MarkdownDescriptionComponent, nzData: { description: currentDescription, }, nzWidth: "900px", nzMaskClosable: true, nzKeyboard: true, nzClosable: true, nzFooter: null, }); const comp: MarkdownDescriptionComponent = modalRef.getContentComponent(); comp.descriptionChange.pipe(untilDestroyed(this)).subscribe((updatedDescription: string) => { const updatedWorkflow: Workflow = { ...currentWorkflow, description: updatedDescription, }; this.workflowActionService.setWorkflowMetadata(updatedWorkflow); if (this.userService.isLogin()) { this.persistWorkflow(); } modalRef.close(); }); } /** * Returns true if there's any operator on the graph; false otherwise */ public hasOperators(): boolean { return this.workflowActionService.getTexeraGraph().getAllOperators().length > 0; } public persistWorkflow(): void { this.isSaving = true; let localPid = this.pid; this.workflowPersistService .persistWorkflow(this.workflowActionService.getWorkflow()) .pipe( tap((updatedWorkflow: Workflow) => { this.workflowActionService.setWorkflowMetadata(updatedWorkflow); }), filter(workflow => isDefined(localPid) && isDefined(workflow.wid)), mergeMap(workflow => this.userProjectService.addWorkflowToProject(localPid!, workflow.wid!)), untilDestroyed(this) ) .subscribe({ error: (e: unknown) => this.notificationService.error((e as Error).message), }) .add(() => (this.isSaving = false)); } /** * Handler for changing workflow name input box, updates the cachedWorkflow and persist to database. */ onWorkflowNameChange() { this.workflowActionService.setWorkflowName(this.currentWorkflowName); if (this.userService.isLogin()) { this.persistWorkflow(); } } onClickCreateNewWorkflow() { this.workflowActionService.resetAsNewWorkflow(); this.location.go("/"); } registerWorkflowMetadataDisplayRefresh() { this.workflowActionService .workflowMetaDataChanged() .pipe(debounceTime(100)) .pipe(untilDestroyed(this)) .subscribe(() => { this.currentWorkflowName = this.workflowActionService.getWorkflowMetadata()?.name; // Use timeout to make sure this.adjustWorkflowNameWidth() runs // after currentWorkflowName is set. Otherwise, the input width may not match // the latest name right after refresh. setTimeout(() => this.adjustWorkflowNameWidth(), 0); this.autoSaveState = this.workflowActionService.getWorkflowMetadata().lastModifiedTime === undefined ? "" : "Saved at " + this.datePipe.transform( this.workflowActionService.getWorkflowMetadata().lastModifiedTime, "MM/dd/yyyy HH:mm:ss", Intl.DateTimeFormat().resolvedOptions().timeZone, "en" ); }); } onClickGetAllVersions() { this.workflowVersionService.displayWorkflowVersions(); } private handleWorkflowVersionDisplay(): void { this.workflowVersionService .getDisplayParticularVersionStream() .pipe(untilDestroyed(this)) .subscribe(displayVersionFlag => { this.particularVersionDate = this.workflowActionService.getWorkflowMetadata().creationTime === undefined ? "" : "" + this.datePipe.transform( this.workflowActionService.getWorkflowMetadata().creationTime, "MM/dd/yyyy HH:mm:ss", Intl.DateTimeFormat().resolvedOptions().timeZone, "en" ); this.displayParticularWorkflowVersion = displayVersionFlag; }); } closeParticularVersionDisplay() { this.workflowVersionService.closeParticularVersionDisplay(); } revertToVersion() { this.workflowVersionService.revertToVersion(); // after swapping the workflows to point to the particular version, persist it in DB this.persistWorkflow(); } cloneVersion() { this.workflowVersionService .cloneWorkflowVersion() .pipe( catchError(() => { this.notificationService.error("Failed to clone workflow. Please try again."); return of(null); }), untilDestroyed(this) ) .subscribe(new_wid => { if (new_wid) { this.notificationService.success("Workflow cloned successfully! New workflow ID: " + new_wid); this.closeParticularVersionDisplay(); } }); } private registerWorkflowModifiableChangedHandler(): void { this.workflowActionService .getWorkflowModificationEnabledStream() .pipe(untilDestroyed(this)) .subscribe(modifiable => (this.isWorkflowModifiable = modifiable)); } private registerWorkflowIdUpdateHandler(): void { this.workflowActionService .workflowMetaDataChanged() .pipe(untilDestroyed(this)) .subscribe(metadata => { this.workflowId = metadata.wid; // consider adding the oprerator reconnect }); } /** * Attempts to run a workflow based on the current state. * If no computing unit is selected but the feature is enabled, * it will first create and connect to a new computing unit. */ runWorkflow(): void { // Use the existing flags that were already updated via subscriptions if (!this.isWorkflowValid || this.isWorkflowEmpty) { return; } // If computing unit manager is enabled and no computing unit is selected if (this.computingUnitStatus === ComputingUnitState.NoComputingUnit) { // Create a default name based on the workflow name const defaultName = this.currentWorkflowName ? `${this.currentWorkflowName}'s Computing Unit` : "New Computing Unit"; // Set the default name in the computing unit selection component this.computingUnitSelectionComponent.newComputingUnitName = defaultName; // Show the existing modal in the ComputingUnitSelectionComponent this.computingUnitSelectionComponent.showAddComputeUnitModalVisible(); return; } // Regular workflow execution - already connected this.executeWorkflowService.executeWorkflowWithEmailNotification( this.currentExecutionName || "Untitled Execution", this.config.env.workflowEmailNotificationEnabled ); } protected readonly Privilege = Privilege; } ================================================ FILE: frontend/src/app/workspace/component/power-button/computing-unit-selection.component.html ================================================
    CPU
    Memory
    • {{ unit.computingUnit.name }} (Connecting)
    • Computing Unit
    Create Computing Unit
    Computing Unit Type
    Computing Unit Name
    Select RAM Size
    Select #CPU Core(s)
    Select #GPU(s)
    Adjust the Shared Memory Size
    Shared memory cannot be greater than total memory.
    JVM Memory Size: {{selectedJvmMemorySize}}
    Computing Unit Name
    Computing Unit URI

    CPU

    {{getCpuValue() | number:'1.4-4'}} / {{getCpuLimit()}} {{getCpuLimitUnit()}} ({{getCpuPercentage() | number:'1.1-1'}}%)

    RAM

    {{getMemoryValue() | number:'1.4-4'}} / {{getMemoryLimit()}} {{getMemoryLimitUnit()}} ({{getMemoryPercentage() | number:'1.1-1'}}%)

    GPU

    {{getGpuLimit()}} GPU(s)

    JVM Memory Size

    {{getJvmMemorySize()}}

    Shared Memory Size

    {{getSharedMemorySize()}}

    System Packages (read-only)
    Package
    Version
    {{ pve.name }}
    Pip Installation Output (Output will appear here after you click OK.)
    
                
    ================================================ FILE: frontend/src/app/workspace/component/power-button/computing-unit-selection.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ :host { display: inline-flex; flex: 0 0 auto; } .computing-units-selection { display: inline-flex; align-items: center; justify-content: flex-end; min-width: 220px; max-width: 280px; &.metrics-visible { min-width: 290px; max-width: none; gap: 10px; .computing-units-dropdown-button { margin-left: auto; } } } .computing-units-dropdown { width: 350px; max-height: 50vh; overflow-y: auto; } .computing-unit-option { display: block; width: 100%; padding: 0 !important; &.unit-connecting { color: #1890ff; } &.unit-disconnected { color: #ff4d4f; } &.unit-terminating { color: #faad14; } &[disabled] { opacity: 0.6; cursor: not-allowed !important; .unit-status-indicator { opacity: 1; font-weight: 500; } .computing-unit-terminate-icon { opacity: 1; cursor: pointer !important; &:hover { color: #ff4d4f; } } } } .computing-unit-row, .computing-unit-name, .create-computing-unit, .metric-item, .shm-input-row { display: flex; align-items: center; } .computing-unit-row { justify-content: space-between; width: 100%; gap: 10px; padding: 5px 12px; box-sizing: border-box; } .computing-unit-name { flex-grow: 1; gap: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .computing-unit-name span, .unit-status-indicator { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .unit-status-indicator { margin-left: 4px; font-size: 0.85em; font-style: italic; opacity: 0.8; } .computing-unit-terminate-icon { margin-left: auto; flex-shrink: 0; opacity: 0.85; color: #ff4d4f; cursor: pointer; &:hover { opacity: 1; transform: scale(1.1); } } .memory-selection, .cpu-selection, .gpu-selection, .unit-name-input { width: 100%; } .create-computing-unit { gap: 10px; justify-content: flex-start; } .create-compute-unit-container, .resource-metrics { display: grid; } .create-compute-unit-container { grid-template-columns: repeat(2, 1fr); gap: 10px; justify-content: start; align-items: center; } .select-unit { display: flex; flex-direction: column; align-items: flex-start; justify-content: center; gap: 10px; &.name-field { grid-column: span 2; } } .jvm-memory-slider { width: 100%; margin: 10px 0; } .memory-warning { margin-top: 10px; font-size: 0.9em; } .computing-units-dropdown-button { display: inline-flex; align-items: center; min-width: 220px; max-width: 280px; padding: 0 8px; overflow: hidden; white-space: nowrap; } .button-content { display: flex; align-items: center; width: 100%; min-width: 0; texera-user-avatar, nz-badge, i { flex-shrink: 0; } i.ant-dropdown-trigger { margin-left: auto; } } .unit-name-text, .connect-text { display: inline-block; flex: 1 1 auto; min-width: 0; max-width: 220px; margin: 0 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } :host ::ng-deep .ant-badge-status-dot { position: relative; top: -1px; vertical-align: middle; } .metrics-container { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 120px; min-width: 120px; height: 32px; gap: 3px; padding: 0; border: none; flex-shrink: 0; } .metric-item { width: 100%; height: 12px; gap: 8px; } .metric-label { width: 45px; flex-shrink: 0; font-size: 10px; line-height: 1; } .metric-bar-wrapper { display: flex; align-items: center; flex-grow: 1; width: 90px; min-width: 60px; height: 8px; padding: 0; } #cpu-progress-bar, #memory-progress-bar, :host ::ng-deep .ant-progress, :host ::ng-deep .ant-progress-inner { width: 100%; } #cpu-progress-bar, #memory-progress-bar { margin: 0 !important; padding: 0 !important; vertical-align: middle; } .resource-metrics { width: 250px; grid-template-columns: repeat(2, 1fr); gap: 5px; } .general-metric { display: flex; flex-direction: column; width: 100%; gap: 3px; padding: 10px; border-radius: 3px; background-color: #f9fafb; } .metric-name, .metric-value { margin: 0; } .metric-name { font-size: 10px; } .metric-unit { margin-left: 4px; color: #888; font-size: 0.9em; } .metric-percentage { margin-left: 6px; color: #555; font-size: 0.9em; font-weight: 500; } .shared-memory-group { width: 100%; .shm-input-row { width: 100%; gap: 8px; } .shm-size-input { width: 60px; min-width: 50px; flex-shrink: 0; } .shm-unit-select { width: 80px; min-width: 70px; flex-shrink: 0; } .shm-warning { margin-top: 4px; color: #faad14; font-size: 12px; white-space: nowrap; } } .unit-name-edit-input { width: 100%; max-width: 200px; padding: 2px 6px; border: 1px solid #d9d9d9; border-radius: 2px; background: white; font-size: inherit; &:focus { border-color: #40a9ff; box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); outline: none; } } ::ng-deep .error { color: red; font-weight: bold; } ::ng-deep .warning { color: #d4a000; font-weight: bold; } ::ng-deep .success { color: green; font-weight: bold; } ::ng-deep .pip-exit.ok { color: green; font-weight: bold; } ::ng-deep .pip-exit.err { color: red; font-weight: bold; } ================================================ FILE: frontend/src/app/workspace/component/power-button/computing-unit-selection.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { ComputingUnitSelectionComponent } from "./computing-unit-selection.component"; import { NzButtonModule } from "ng-zorro-antd/button"; import { CommonModule } from "@angular/common"; import { NzIconModule } from "ng-zorro-antd/icon"; import { ActivatedRoute, ActivatedRouteSnapshot, convertToParamMap, Data, Params, UrlSegment } from "@angular/router"; import { NzDropDownModule } from "ng-zorro-antd/dropdown"; import { NzModalModule, NzModalService } from "ng-zorro-antd/modal"; import { ComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; import { MockComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service"; import { commonTestProviders } from "../../../common/testing/test-utils"; describe("PowerButtonComponent", () => { let component: ComputingUnitSelectionComponent; let fixture: ComponentFixture; let activatedRouteMock: Partial; const activatedRouteSnapshotMock: Partial = { queryParams: {}, url: [] as UrlSegment[], params: {} as Params, fragment: null, data: {} as Data, paramMap: convertToParamMap({}), queryParamMap: convertToParamMap({}), outlet: "", routeConfig: null, root: null as any, parent: null as any, firstChild: null as any, children: [], pathFromRoot: [], }; activatedRouteMock = { snapshot: activatedRouteSnapshotMock as ActivatedRouteSnapshot, }; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ ComputingUnitSelectionComponent, HttpClientTestingModule, // Use TestingModule instead of HttpClientModule CommonModule, NzButtonModule, NzIconModule, NzDropDownModule, NzModalModule, // Add NzModalModule for the NzModalService ], providers: [ { provide: ActivatedRoute, useValue: activatedRouteMock }, { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService }, NzModalService, // Add NzModalService provider ...commonTestProviders, ], }).compileComponents(); fixture = TestBed.createComponent(ComputingUnitSelectionComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/workspace/component/power-button/computing-unit-selection.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ChangeDetectorRef, Component, OnInit, NgZone } from "@angular/core"; import { take } from "rxjs/operators"; import { WorkflowComputingUnitManagingService } from "../../../common/service/computing-unit/workflow-computing-unit/workflow-computing-unit-managing.service"; import { DashboardWorkflowComputingUnit, WorkflowComputingUnitType, } from "../../../common/type/workflow-computing-unit"; import { NotificationService } from "../../../common/service/notification/notification.service"; import { DEFAULT_WORKFLOW, WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; import { isDefined } from "../../../common/util/predicate"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { extractErrorMessage } from "../../../common/util/error"; import { ComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; import { NzModalService, NzModalComponent, NzModalContentDirective } from "ng-zorro-antd/modal"; import { WorkflowExecutionsService } from "../../../dashboard/service/user/workflow-executions/workflow-executions.service"; import { WorkflowExecutionsEntry } from "../../../dashboard/type/workflow-executions-entry"; import { ExecutionState } from "../../types/execute-workflow.interface"; import { ShareAccessComponent } from "../../../dashboard/component/user/share-access/share-access.component"; import { GuiConfigService } from "../../../common/service/gui-config.service"; import { ComputingUnitActionsService } from "../../../common/service/computing-unit/computing-unit-actions/computing-unit-actions.service"; import { ComputingUnitMetadataComponent, parseResourceUnit, parseResourceNumber, findNearestValidStep, unitTypeMessageTemplate, cpuResourceConversion, memoryResourceConversion, cpuPercentage, memoryPercentage, validateName, getComputingUnitBadgeColor, getComputingUnitStatusTooltip, getComputingUnitCpuStatus, getComputingUnitMemoryStatus, getComputingUnitCpuLimitUnit, isComputingUnitShmTooLarge, getJvmMemorySliderConfig, } from "../../../common/util/computing-unit.util"; import { PvePackageResponse, WorkflowPveService } from "../../service/virtual-environment/virtual-environment.service"; import { NgClass, NgIf, NgFor, DecimalPipe, TitleCasePipe } from "@angular/common"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzPopoverDirective } from "ng-zorro-antd/popover"; import { NzProgressComponent } from "ng-zorro-antd/progress"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { NzDropdownDirective, NzDropdownMenuComponent } from "ng-zorro-antd/dropdown"; import { UserAvatarComponent } from "../../../dashboard/component/user/user-avatar/user-avatar.component"; import { NzBadgeComponent } from "ng-zorro-antd/badge"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzMenuDirective, NzMenuItemComponent, NzMenuDividerDirective } from "ng-zorro-antd/menu"; import { NzInputDirective } from "ng-zorro-antd/input"; import { NzSelectComponent, NzOptionComponent } from "ng-zorro-antd/select"; import { FormsModule } from "@angular/forms"; import { NzSliderComponent } from "ng-zorro-antd/slider"; import { NzAlertComponent } from "ng-zorro-antd/alert"; import { NzCollapseComponent, NzCollapsePanelComponent } from "ng-zorro-antd/collapse"; type PveDraft = { name: string; pipOutput: string; prettyPipOutput: string; expanded: boolean; socket?: WebSocket; isInstalling: boolean; isLocked: boolean; }; @UntilDestroy() @Component({ selector: "texera-computing-unit-selection", templateUrl: "./computing-unit-selection.component.html", styleUrls: ["./computing-unit-selection.component.scss"], imports: [ NgClass, NgIf, ɵNzTransitionPatchDirective, NzPopoverDirective, NzProgressComponent, NzSpaceCompactItemDirective, NzButtonComponent, NzWaveDirective, NzDropdownDirective, UserAvatarComponent, NzBadgeComponent, NzTooltipDirective, NzIconDirective, NzDropdownMenuComponent, NzMenuDirective, NgFor, NzMenuItemComponent, NzInputDirective, NzMenuDividerDirective, NzModalComponent, NzSelectComponent, FormsModule, NzOptionComponent, NzSliderComponent, NzAlertComponent, NzModalContentDirective, NzCollapseComponent, NzCollapsePanelComponent, DecimalPipe, TitleCasePipe, ], }) export class ComputingUnitSelectionComponent implements OnInit { // variables for creating a virtual environment pves: PveDraft[] = []; systemPackages: { name: string; version: string }[] = []; pveModalVisible = false; // current workflow's Id, will change with wid in the workflowActionService.metadata protected readonly unitTypeMessageTemplate = unitTypeMessageTemplate; workflowId: number | undefined; lastSelectedCuid?: number; selectedComputingUnit: DashboardWorkflowComputingUnit | null = null; allComputingUnits: DashboardWorkflowComputingUnit[] = []; // variables for creating a computing unit addComputeUnitModalVisible = false; newComputingUnitName: string = ""; selectedMemory: string = ""; selectedCpu: string = ""; selectedGpu: string = "0"; // Default to no GPU selectedJvmMemorySize: string = "1G"; // Initial JVM memory size selectedComputingUnitType?: WorkflowComputingUnitType; // Selected computing unit type selectedShmSize: string = "64Mi"; // Shared memory size shmSizeValue: number = 64; // default to 64 shmSizeUnit: "Mi" | "Gi" = "Mi"; // default unit availableComputingUnitTypes: WorkflowComputingUnitType[] = []; localComputingUnitUri: string = ""; // URI for local computing unit // variables for renaming a computing unit editingNameOfUnit: number | null = null; editingUnitName: string = ""; // JVM memory slider configuration jvmMemorySliderValue: number = 1; // Initial value in GB jvmMemoryMarks: { [key: number]: string } = { 1: "1G" }; jvmMemoryMax: number = 1; jvmMemorySteps: number[] = [1]; // Available steps in binary progression (1,2,4,8...) showJvmMemorySlider: boolean = false; // Whether to show the slider // cpu&memory limit options from backend cpuOptions: string[] = []; memoryOptions: string[] = []; gpuOptions: string[] = []; // Add GPU options array constructor( private computingUnitService: WorkflowComputingUnitManagingService, private notificationService: NotificationService, protected config: GuiConfigService, private workflowActionService: WorkflowActionService, private computingUnitStatusService: ComputingUnitStatusService, private workflowExecutionsService: WorkflowExecutionsService, private modalService: NzModalService, private cdr: ChangeDetectorRef, private computingUnitActionsService: ComputingUnitActionsService, private workflowPveService: WorkflowPveService, private ngZone: NgZone ) {} ngOnInit(): void { // Fetch available computing unit types this.localComputingUnitUri = `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ""}/wsapi`; this.newComputingUnitName = "My Computing Unit"; this.computingUnitService .getComputingUnitTypes() .pipe(untilDestroyed(this)) .subscribe({ next: ({ typeOptions }) => { this.availableComputingUnitTypes = typeOptions; // Set default selected type if available if (typeOptions.includes("kubernetes")) { this.selectedComputingUnitType = "kubernetes"; } else if (typeOptions.length > 0) { this.selectedComputingUnitType = typeOptions[0]; } }, error: (err: unknown) => this.notificationService.error(`Failed to fetch computing unit types: ${extractErrorMessage(err)}`), }); this.computingUnitService .getComputingUnitLimitOptions() .pipe(untilDestroyed(this)) .subscribe({ next: ({ cpuLimitOptions, memoryLimitOptions, gpuLimitOptions }) => { this.cpuOptions = cpuLimitOptions; this.memoryOptions = memoryLimitOptions; this.gpuOptions = gpuLimitOptions; // fallback defaults this.selectedCpu = this.cpuOptions[0] ?? "1"; this.selectedMemory = this.memoryOptions[0] ?? "1Gi"; this.selectedGpu = this.gpuOptions[0] ?? "0"; // Initialize JVM memory slider based on selected memory this.updateJvmMemorySlider(); }, error: (err: unknown) => this.notificationService.error(`Failed to fetch resource options: ${extractErrorMessage(err)}`), }); // Subscribe to the current selected unit from the status service this.computingUnitStatusService .getSelectedComputingUnit() .pipe(untilDestroyed(this)) .subscribe(unit => { const wid = this.workflowActionService.getWorkflowMetadata()?.wid; // ── compare with the *previous* cuid, not the one we are just about to store ── if (isDefined(wid) && unit?.computingUnit.cuid !== this.lastSelectedCuid) { this.updateWorkflowModificationStatus(wid); } // update local caches **after** the comparison this.lastSelectedCuid = unit?.computingUnit.cuid; this.selectedComputingUnit = unit; }); this.computingUnitStatusService .getAllComputingUnits() .pipe(untilDestroyed(this)) .subscribe(units => { this.allComputingUnits = units; }); this.registerWorkflowMetadataSubscription(); } /** * Helper to query backend and (de)activate modification status. */ private updateWorkflowModificationStatus(wid: number): void { this.workflowExecutionsService .retrieveWorkflowExecutions(wid, [ExecutionState.Running, ExecutionState.Initializing]) .pipe(take(1), untilDestroyed(this)) .subscribe(execList => { if (execList.length > 0) { this.notificationService.info( "There are ongoing executions on this workflow. Modification of the workflow is currently disabled." ); this.workflowActionService.disableWorkflowModification(); } else { this.workflowActionService.enableWorkflowModification(); } }); } /** * utility function used for displaying the computing unit */ public trackByCuid(_idx: number, unit: DashboardWorkflowComputingUnit): number { return unit.computingUnit.cuid; } /** * Registers a subscription to listen for workflow metadata changes; * Calls `selectComputingUnit` when the `wid` changes; * The wid can change by time because of the workspace rendering; */ private registerWorkflowMetadataSubscription(): void { this.workflowActionService .workflowMetaDataChanged() .pipe(untilDestroyed(this)) .subscribe(() => { const wid = this.workflowActionService.getWorkflowMetadata()?.wid; if (wid !== this.workflowId) { this.workflowId = wid; if (isDefined(this.workflowId) && this.workflowId !== DEFAULT_WORKFLOW.wid) { this.workflowExecutionsService .retrieveLatestWorkflowExecution(this.workflowId) .pipe(untilDestroyed(this)) .subscribe({ next: (latestWorkflowExecution: WorkflowExecutionsEntry) => { this.selectComputingUnit(this.workflowId, latestWorkflowExecution.cuId); }, error: (err: unknown) => { const runningUnit = this.allComputingUnits.find(unit => unit.status === "Running"); if (runningUnit) { this.selectComputingUnit(this.workflowId, runningUnit.computingUnit.cuid); } }, }); } } }); } /** * Called whenever the selected computing unit changes. */ selectComputingUnit(wid: number | undefined, cuid: number | undefined): void { if (isDefined(cuid) && wid !== DEFAULT_WORKFLOW.wid) { this.computingUnitStatusService.selectComputingUnit(wid, cuid); } } isComputingUnitRunning(): boolean { return this.selectedComputingUnit != null && this.selectedComputingUnit.status === "Running"; } getButtonText(): string { if (!this.selectedComputingUnit) { return "Connect"; } else { return this.selectedComputingUnit.computingUnit.name; } } computeStatus(): string { if (!this.selectedComputingUnit) { return "processing"; } const status = this.selectedComputingUnit.status; if (status === "Running") { return "success"; } else if (status === "Pending" || status === "Terminating") { return "warning"; } else { return "error"; } } /** * Determines if a unit cannot be selected (disabled in the dropdown) */ cannotSelectUnit(unit: DashboardWorkflowComputingUnit): boolean { // Only allow selecting units that are in the Running state return unit.status !== "Running"; } isSelectedUnit(unit: DashboardWorkflowComputingUnit): boolean { return unit.computingUnit.uri === this.selectedComputingUnit?.computingUnit.uri; } // Determines if the GPU selection dropdown should be shown showGpuSelection(): boolean { // Don't show GPU selection if there are no options or only "0" option return this.gpuOptions.length > 1 || (this.gpuOptions.length === 1 && this.gpuOptions[0] !== "0"); } showAddComputeUnitModalVisible(): void { this.addComputeUnitModalVisible = true; } handleAddComputeUnitModalOk(): void { this.startComputingUnit(); this.addComputeUnitModalVisible = false; } handleAddComputeUnitModalCancel(): void { this.addComputeUnitModalVisible = false; } isShmTooLarge(): boolean { return isComputingUnitShmTooLarge(this.selectedMemory, this.shmSizeValue, this.shmSizeUnit); } /** * Start a new computing unit. */ startComputingUnit(): void { if (this.selectedComputingUnitType === "kubernetes" && this.newComputingUnitName.trim() === "") { this.notificationService.error("Name of the computing unit cannot be empty"); return; } if (this.selectedComputingUnitType === "local" && this.localComputingUnitUri.trim() === "") { this.notificationService.error("URI for local computing unit cannot be empty"); return; } if (!this.selectedComputingUnitType) { this.notificationService.error("Please select a valid computing unit type"); return; } const request = { type: this.selectedComputingUnitType, name: this.newComputingUnitName, cpu: this.selectedCpu, memory: this.selectedMemory, gpu: this.selectedGpu, jvmMemorySize: this.selectedJvmMemorySize, shmSize: `${this.shmSizeValue}${this.shmSizeUnit}`, localUri: this.localComputingUnitUri, }; this.computingUnitActionsService .create(request) .pipe(untilDestroyed(this)) .subscribe({ next: (unit: DashboardWorkflowComputingUnit) => { this.notificationService.success("Successfully created the new compute unit"); this.selectComputingUnit(this.workflowId, unit.computingUnit.cuid); }, error: (err: unknown) => this.notificationService.error(`Failed to start computing unit: ${extractErrorMessage(err)}`), }); } openComputingUnitMetadataModal(unit: DashboardWorkflowComputingUnit) { this.modalService.create({ nzTitle: "Computing Unit Information", nzContent: ComputingUnitMetadataComponent, nzData: unit, nzFooter: null, nzMaskClosable: true, nzWidth: "600px", }); } /** * Terminate a computing unit. * @param cuid The CUID of the unit to terminate. */ terminateComputingUnit(cuid: number): void { const unit = this.allComputingUnits.find(u => u.computingUnit.cuid === cuid); if (!unit) { this.notificationService.error("Invalid computing unit."); return; } this.computingUnitActionsService.confirmAndTerminate(cuid, unit); if (this.selectedComputingUnit?.computingUnit.type === "local") { this.workflowPveService .deleteEnvironments(cuid) .pipe(untilDestroyed(this)) .subscribe({ error: (err: unknown) => { console.error("Failed to delete PVE environments", err); }, }); } } /** * Start editing the name of a computing unit. */ startEditingUnitName(unit: DashboardWorkflowComputingUnit): void { if (!unit.isOwner) { this.notificationService.error("Only owners can rename computing units"); return; } this.editingNameOfUnit = unit.computingUnit.cuid; this.editingUnitName = unit.computingUnit.name; // Force change detection and focus the input this.cdr.detectChanges(); setTimeout(() => { const input = document.querySelector(".unit-name-edit-input") as HTMLInputElement; if (input) { input.focus(); input.select(); } }, 0); } /** * Confirm the new name and update the computing unit. */ confirmUpdateUnitName(cuid: number, newName: string): void { const trimmedName = newName.trim(); const validationError = validateName(trimmedName); if (validationError) { this.notificationService.error(validationError); this.cancelEditingUnitName(); return; } this.computingUnitService .renameComputingUnit(cuid, trimmedName) .pipe(untilDestroyed(this)) .subscribe({ next: () => { this.notificationService.success("Successfully renamed computing unit"); // Update the local unit name immediately for better UX const unit = this.allComputingUnits.find(u => u.computingUnit.cuid === cuid); if (unit) { unit.computingUnit.name = trimmedName; } // Also update the selected unit if it's the one being renamed if (this.selectedComputingUnit?.computingUnit.cuid === cuid) { this.selectedComputingUnit.computingUnit.name = trimmedName; } // Refresh the computing units list this.computingUnitStatusService.refreshComputingUnitList(); }, error: (err: unknown) => { this.notificationService.error(`Failed to rename computing unit: ${extractErrorMessage(err)}`); }, }) .add(() => { this.editingNameOfUnit = null; this.editingUnitName = ""; }); } /** * Cancel editing the computing unit name. */ cancelEditingUnitName(): void { this.editingNameOfUnit = null; this.editingUnitName = ""; } getCurrentComputingUnitCpuUsage(): string { return this.selectedComputingUnit ? this.selectedComputingUnit.metrics.cpuUsage : "NaN"; } getCurrentComputingUnitMemoryUsage(): string { return this.selectedComputingUnit ? this.selectedComputingUnit.metrics.memoryUsage : "NaN"; } getCurrentComputingUnitCpuLimit(): string { return this.selectedComputingUnit ? this.selectedComputingUnit.computingUnit.resource.cpuLimit : "NaN"; } getCurrentComputingUnitMemoryLimit(): string { return this.selectedComputingUnit ? this.selectedComputingUnit.computingUnit.resource.memoryLimit : "NaN"; } getCurrentComputingUnitGpuLimit(): string { return this.selectedComputingUnit ? this.selectedComputingUnit.computingUnit.resource.gpuLimit : "NaN"; } getCurrentComputingUnitJvmMemorySize(): string { return this.selectedComputingUnit ? this.selectedComputingUnit.computingUnit.resource.jvmMemorySize : "NaN"; } getCurrentSharedMemorySize(): string { return this.selectedComputingUnit ? this.selectedComputingUnit.computingUnit.resource.shmSize : "NaN"; } /** * Returns the badge color based on computing unit status */ getBadgeColor(status: string): string { return getComputingUnitBadgeColor(status); } getCpuLimit(): number { return parseResourceNumber(this.getCurrentComputingUnitCpuLimit()); } getGpuLimit(): string { return this.getCurrentComputingUnitGpuLimit(); } getJvmMemorySize(): string { return this.getCurrentComputingUnitJvmMemorySize(); } getSharedMemorySize(): string { return this.getCurrentSharedMemorySize(); } getCpuLimitUnit(): string { return getComputingUnitCpuLimitUnit(parseResourceUnit(this.getCurrentComputingUnitCpuLimit())); } getMemoryLimit(): number { return parseResourceNumber(this.getCurrentComputingUnitMemoryLimit()); } getMemoryLimitUnit(): string { return parseResourceUnit(this.getCurrentComputingUnitMemoryLimit()); } getCpuValue(): number { const usage = this.getCurrentComputingUnitCpuUsage(); const limit = this.getCurrentComputingUnitCpuLimit(); if (usage === "N/A" || limit === "N/A") return 0; const displayUnit = this.getCpuLimitUnit() === "CPU" ? "" : this.getCpuLimitUnit(); const usageValue = cpuResourceConversion(usage, displayUnit); return parseFloat(usageValue); } getMemoryValue(): number { const usage = this.getCurrentComputingUnitMemoryUsage(); const limit = this.getCurrentComputingUnitMemoryLimit(); if (usage === "N/A" || limit === "N/A") return 0; const displayUnit = this.getMemoryLimitUnit(); const usageValue = memoryResourceConversion(usage, displayUnit); return parseFloat(usageValue); } getCpuPercentage(): number { return cpuPercentage(this.getCurrentComputingUnitCpuUsage(), this.getCurrentComputingUnitCpuLimit()); } getMemoryPercentage(): number { return memoryPercentage(this.getCurrentComputingUnitMemoryUsage(), this.getCurrentComputingUnitMemoryLimit()); } getCpuStatus(): "success" | "exception" | "active" | "normal" { return getComputingUnitCpuStatus(this.getCpuPercentage()); } getMemoryStatus(): "success" | "exception" | "active" | "normal" { return getComputingUnitMemoryStatus(this.getMemoryPercentage()); } getCpuUnit(): string { return this.getCpuLimitUnit() === "CPU" ? "Cores" : this.getCpuLimitUnit(); } getMemoryUnit(): string { return this.getMemoryLimitUnit() === "" ? "B" : this.getMemoryLimitUnit(); } /** * Returns a descriptive tooltip for a specific unit's status */ getUnitStatusTooltip(unit: DashboardWorkflowComputingUnit): string { return getComputingUnitStatusTooltip(unit); } // Called when the component initializes updateJvmMemorySlider(): void { this.resetJvmMemorySlider(); } onJvmMemorySliderChange(value: number): void { // Ensure the value is one of the valid steps const validStep = findNearestValidStep(value, this.jvmMemorySteps); this.jvmMemorySliderValue = validStep; this.selectedJvmMemorySize = `${validStep}G`; } // Check if the maximum JVM memory value is selected isMaxJvmMemorySelected(): boolean { // Only show warning for larger memory sizes (>=4GB) where the slider is shown // AND when the maximum value is selected return this.showJvmMemorySlider && this.jvmMemorySliderValue === this.jvmMemoryMax && this.jvmMemoryMax >= 4; } // Completely reset the JVM memory slider based on the selected CU memory resetJvmMemorySlider(): void { const config = getJvmMemorySliderConfig(this.selectedMemory); this.jvmMemoryMax = config.jvmMemoryMax; this.showJvmMemorySlider = config.showJvmMemorySlider; this.jvmMemorySteps = config.jvmMemorySteps; this.jvmMemoryMarks = config.jvmMemoryMarks; this.jvmMemorySliderValue = config.jvmMemorySliderValue; this.selectedJvmMemorySize = config.selectedJvmMemorySize; } // Listen for memory selection changes onMemorySelectionChange(): void { // Store current JVM memory value for potential reuse const previousJvmMemory = this.jvmMemorySliderValue; // Reset slider configuration based on the new memory selection this.resetJvmMemorySlider(); // For CU memory > 3GB, preserve previous value if valid and >= 2GB // Get the current memory in GB const memoryValue = parseResourceNumber(this.selectedMemory); const memoryUnit = parseResourceUnit(this.selectedMemory); let cuMemoryInGb = memoryUnit === "Gi" ? memoryValue : memoryUnit === "Mi" ? Math.floor(memoryValue / 1024) : 1; // Only try to preserve previous value for larger memory sizes where slider is shown if ( cuMemoryInGb > 3 && previousJvmMemory >= 2 && previousJvmMemory <= this.jvmMemoryMax && this.jvmMemorySteps.includes(previousJvmMemory) ) { this.jvmMemorySliderValue = previousJvmMemory; this.selectedJvmMemorySize = `${previousJvmMemory}G`; } } getCreateModalTitle(): string { if (!this.selectedComputingUnitType) return "Create Computing Unit"; return unitTypeMessageTemplate[this.selectedComputingUnitType].createTitle; } public async onClickOpenShareAccess(cuid: number): Promise { this.computingUnitActionsService.openShareAccessModal(cuid, true); } onDropdownVisibilityChange(visible: boolean): void { if (visible) { this.computingUnitStatusService.refreshComputingUnitList(); } } trackByIndex(index: number): number { return index; } addEnvironment(): void { this.pves.push({ name: "", pipOutput: "", prettyPipOutput: "", expanded: true, isInstalling: false, isLocked: false, }); } showPVEmodalVisible(): void { this.pveModalVisible = true; this.getPVEs(); } closePveModal(): void { this.pves.forEach(pve => { pve.socket?.close(); pve.socket = undefined; pve.isInstalling = false; }); this.pveModalVisible = false; } getPVEs(): void { const cuId = this.selectedComputingUnit!.computingUnit.cuid; this.workflowPveService .fetchPVEs(cuId) .pipe(untilDestroyed(this)) .subscribe({ next: (resp: PvePackageResponse[]) => { this.pves = resp.map(pve => ({ name: pve.pveName, expanded: false, isInstalling: false, pipOutput: "", prettyPipOutput: "", isLocked: true, })); this.workflowPveService .getSystemPackages() .pipe(untilDestroyed(this)) .subscribe({ next: installedResp => { this.systemPackages = installedResp.system.map(pkgStr => { const [name, version] = pkgStr.split("=="); return { name: name.trim(), version: (version ?? "").trim(), }; }); }, error: (err: unknown) => { console.error("Failed to fetch system packages:", err); this.systemPackages = []; }, }); }, error: (err: unknown) => { console.error("Failed to fetch PVEs:", err); this.pves = []; this.systemPackages = []; }, }); } scrollToBottomOfPipModal(index: number) { setTimeout(() => { const pre = document.getElementById(`pip-log-${index}`) as HTMLElement | null; if (pre) { pre.scrollTop = pre.scrollHeight; } }, 50); } // Converts raw pip output for UI rendering by escaping unsafe characters and // applying styling to exit codes, errors, warnings, and common success messages. updatePrettyPipOutput(index: number) { const env = this.pves[index]; const escapeHtml = (s: string) => s .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); const raw = env.pipOutput ?? ""; const safe = escapeHtml(raw); env.prettyPipOutput = safe .replace(/^(\[pip\] Successfully installed.*)$/gm, '$1') .replace( /^(\[(?:PVE|pip|pve)\].*finished with exit code\s+0.*)$/gm, '$1' ) .replace(/^(\[PVE\] Running pip freeze.*)$/gm, '$1') .replace(/^(\[(?:PVE|pip|pve)\]\[ERR\].*)$/gm, '$1') .replace(/\n/g, "
    "); } createVirtualEnvironment(index: number): void { const cuId = this.selectedComputingUnit!.computingUnit.cuid; const env = this.pves[index]; const trimmedName = env.name.trim(); if (!/^[a-zA-Z0-9]+$/.test(trimmedName)) { this.notificationService.error("Environment name must contain only letters and numbers."); return; } const duplicateExists = this.pves.some((pve, i) => i !== index && (pve.name ?? "").trim() === trimmedName); if (duplicateExists) { this.notificationService.error("An environment with this name already exists."); return; } const packageArray: string[] = []; env.socket?.close(); const isLocal = this.selectedComputingUnit?.computingUnit.type === "local"; const websocketUrl = this.workflowPveService.createPveWebSocketUrl(cuId, trimmedName, isLocal, packageArray); console.log("PVE websocketUrl", websocketUrl); const socket = new WebSocket(websocketUrl); this.pves[index] = { ...env, name: trimmedName, socket, pipOutput: "Starting ...\n", isInstalling: true, isLocked: true, }; this.updatePrettyPipOutput(index); this.scrollToBottomOfPipModal(index); socket.onmessage = event => { console.log("PVE WS received:", event.data); this.ngZone.run(() => { const currentEnv = this.pves[index]; if (event.data === "__DONE__") { this.pves[index] = { ...currentEnv, socket: undefined, isInstalling: false, isLocked: true, }; socket.close(); this.workflowPveService .getSystemPackages() .pipe(untilDestroyed(this)) .subscribe({ next: resp => { this.systemPackages = resp.system.map(pkg => { const [name, version] = pkg.split("=="); return { name: name.trim(), version: (version ?? "").trim() }; }); this.cdr.detectChanges(); }, error: (e: unknown) => console.error("Failed to refresh packages", e), }); this.cdr.detectChanges(); return; } this.pves[index] = { ...currentEnv, pipOutput: `${currentEnv.pipOutput ?? ""}${event.data}\n`, }; this.updatePrettyPipOutput(index); this.scrollToBottomOfPipModal(index); this.cdr.detectChanges(); }); }; socket.onerror = err => { console.log("PVE WS error", err); this.ngZone.run(() => { const currentEnv = this.pves[index]; this.pves[index] = { ...currentEnv, pipOutput: `${currentEnv.pipOutput ?? ""}\n[WebSocket error]\n`, socket: undefined, isInstalling: false, isLocked: true, }; socket.close(); this.updatePrettyPipOutput(index); this.cdr.detectChanges(); }); }; socket.onclose = event => { console.log("PVE WS closed", { code: event.code, reason: event.reason, wasClean: event.wasClean, }); }; } } ================================================ FILE: frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.html ================================================

    {{ formTitle }}

    You can add a new column by:
    • Clicking on the blue "+" button
    • Selecting "Add New Column" in the drop-down list of the first field
    • Typing in the name of the new column
    • Selecting the attribute type
    • Typing in an expression using the Python syntax
    You can modify an existing column by:
    • Clicking on the blue "+" button
    • Selecting one existing column in the drop-down list of the first field
    • Selecting the attribute type
    • Typing in an expression using the Python syntax
    You can get the value of any existing attribute in the expression by:
    • Typing in tuple_["$attributeName"] in the expression field
    • Replacing $attributeName with the attributeName you want to access

    Example: Add a new column called IsExpensive where the value is True if the unit price is greater than 500
    Operations:
    • Clicking on the blue "+" button
    • Selecting "Add New Column" in the drop-down list of the first field
    • Typing in the name of the new column as IsExpensive
    • Selecting the attribute type as boolean
    • Typing in the expression as True if tuple_["Unit Price"] > 500 else False

    {{ operatorDescription }}

    Operator Version: {{ operatorVersion }}
    ================================================ FILE: frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ #formly-title { position: relative; h3 { font-weight: bold; margin: 0; display: inline-block; } button { position: absolute; top: 45%; transform: translateY(-55%); } } .operator-version { text-align: right; position: relative; right: -5%; top: 30px; font-size: 0.5em; color: gray; } .question-circle-button { position: absolute; right: 0; top: 50%; transform: translate(0, -50%); } ::ng-deep { // overwrite the color of the Formly error message box .property-editor-form { [role="alert"] { color: #856404; background-color: #fff3cd; border-color: #ffeeba; } nz-input-number { width: 100%; } } } .operator-description { margin: 0 16px 8px 16px; color: rgba(0, 0, 0, 0.65); font-size: 13px; line-height: 1.5; p { margin-bottom: 0; } } ================================================ FILE: frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, discardPeriodicTasks, fakeAsync, TestBed, tick } from "@angular/core/testing"; import { OperatorPropertyEditFrameComponent } from "./operator-property-edit-frame.component"; import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; import { OperatorMetadataService } from "../../../service/operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../../../service/operator-metadata/stub-operator-metadata.service"; import { FORM_DEBOUNCE_TIME_MS } from "../../../service/execute-workflow/execute-workflow.service"; import { DatePipe } from "@angular/common"; import { By } from "@angular/platform-browser"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { FormlyModule } from "@ngx-formly/core"; import { TEXERA_FORMLY_CONFIG } from "../../../../common/formly/formly-config"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { mockPoint, mockResultPredicate, mockScanPredicate, } from "../../../service/workflow-graph/model/mock-workflow-data"; import { mockScanSourceSchema, mockViewResultsSchema, } from "../../../service/operator-metadata/mock-operator-metadata.data"; import { configure } from "rxjs-marbles"; import { NO_ERRORS_SCHEMA, SimpleChange } from "@angular/core"; import { cloneDeep } from "lodash-es"; import Ajv from "ajv"; import { COLLAB_DEBOUNCE_TIME_MS } from "../../../../common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component"; import { FormlyNgZorroAntdModule } from "@ngx-formly/ng-zorro-antd"; import { ComputingUnitStatusService } from "../../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; import { MockComputingUnitStatusService } from "../../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service"; import { commonTestProviders } from "../../../../common/testing/test-utils"; const { marbles } = configure({ run: false }); describe("OperatorPropertyEditFrameComponent", () => { let component: OperatorPropertyEditFrameComponent; let fixture: ComponentFixture; let workflowActionService: WorkflowActionService; beforeEach(async () => { TestBed.overrideComponent(OperatorPropertyEditFrameComponent, { set: { template: '
    {{ formTitle }}
    ', }, }); await TestBed.configureTestingModule({ providers: [ WorkflowActionService, { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService }, DatePipe, ...commonTestProviders, ], imports: [ OperatorPropertyEditFrameComponent, BrowserAnimationsModule, FormsModule, FormlyModule.forRoot(TEXERA_FORMLY_CONFIG), FormlyNgZorroAntdModule, ReactiveFormsModule, HttpClientTestingModule, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(OperatorPropertyEditFrameComponent); component = fixture.componentInstance; workflowActionService = TestBed.inject(WorkflowActionService); }); it("should create", () => { fixture.detectChanges(); expect(component).toBeTruthy(); }); /** * test if the property editor correctly receives the operator highlight stream, * get the operator data (id, property, and metadata), and then display the form. */ it("should change the content of property editor from an empty panel correctly", () => { // check if the changePropertyEditor called after the operator // is highlighted has correctly updated the variables const predicate = { ...mockScanPredicate, operatorProperties: { tableName: "" }, }; // add and highlight an operator workflowActionService.addOperator(predicate, mockPoint); component.ngOnChanges({ currentOperatorId: new SimpleChange(undefined, predicate.operatorID, true), }); fixture.detectChanges(); // check variables are set correctly expect(component.formData).toEqual(predicate.operatorProperties); // check HTML form are displayed const formTitleElement = fixture.debugElement.query(By.css(".texera-workspace-property-editor-title")); const jsonSchemaFormElement = fixture.debugElement.query(By.css(".texera-workspace-property-editor-form")); // check the panel title (use textContent — jsdom doesn't compute the // layout-dependent innerText getter, which returns undefined here) expect((formTitleElement.nativeElement as HTMLElement).textContent?.trim()).toEqual( mockScanSourceSchema.additionalMetadata.userFriendlyName ); // TODO: Temporarilly disable this unit test because PR #1924 is failing the test, // dispite the fact that the code is working as expected. // This shall be fixed in the future. // // check if the form has the all the json schema property names // Object.entries(mockScanSourceSchema.jsonSchema.properties as any).forEach(entry => { // const propertyTitle = (entry[1] as JSONSchema7).title; // if (propertyTitle) { // expect((jsonSchemaFormElement.nativeElement as HTMLElement).innerHTML).toContain(propertyTitle); // } // const propertyDescription = (entry[1] as JSONSchema7).description; // if (propertyDescription) { // expect((jsonSchemaFormElement.nativeElement as HTMLElement).innerHTML).toContain(propertyDescription); // } // }); }); it("should change Texera graph property when the form is edited by the user", fakeAsync(() => { // add an operator and highlight the operator so that the // variables in property editor component is set correctly workflowActionService.addOperator(mockScanPredicate, mockPoint); component.ngOnChanges({ currentOperatorId: new SimpleChange(undefined, mockScanPredicate.operatorID, true), }); fixture.detectChanges(); tick(COLLAB_DEBOUNCE_TIME_MS); // stimulate a form change by the user const formChangeValue = { tableName: "twitter_sample" }; component.onFormChanges(formChangeValue); // maintain a counter of how many times the event is emitted let emitEventCounter = 0; component.operatorPropertyChangeStream.subscribe(() => emitEventCounter++); // fakeAsync enables tick, which waits for the set property debounce time to finish tick(FORM_DEBOUNCE_TIME_MS + 10); // then get the operator, because operator is immutable, the operator before the tick // is a different object reference from the operator after the tick const operator = workflowActionService.getTexeraGraph().getOperator(mockScanPredicate.operatorID); if (!operator) { throw new Error(`operator ${mockScanPredicate.operatorID} is undefined`); } discardPeriodicTasks(); expect(operator.operatorProperties).toEqual(formChangeValue); expect(emitEventCounter).toEqual(1); })); it.skip( "should debounce the user form input to avoid emitting event too frequently", marbles(m => { const jointGraphWrapper = workflowActionService.getJointGraphWrapper(); // add an operator and highlight the operator so that the // variables in property editor component is set correctly workflowActionService.addOperator(mockScanPredicate, mockPoint); jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID); // prepare the form user input event stream // simulate user types in `table` character by character const formUserInputMarbleString = "-a-b-c-d-e"; const formUserInputMarbleValue = { a: { tableName: "t" }, b: { tableName: "ta" }, c: { tableName: "tab" }, d: { tableName: "tabl" }, e: { tableName: "table" }, }; const formUserInputEventStream = m.hot(formUserInputMarbleString, formUserInputMarbleValue); // prepare the expected output stream after debounce time const formChangeEventMarbleString = // wait for the time of last marble string starting to emit "-".repeat(formUserInputMarbleString.length - 1) + // then wait for debounce time (each tick represents 10 ms) "-".repeat(FORM_DEBOUNCE_TIME_MS / 10) + "e-"; const formChangeEventMarbleValue = { e: { tableName: "table" } as object, }; const expectedFormChangeEventStream = m.hot(formChangeEventMarbleString, formChangeEventMarbleValue); m.bind(); // // TODO: FIX THIS // const actualFormChangeEventStream = component.operatorPropertyChangeStream; // // formUserInputEventStream.subscribe(); // m.expect(actualFormChangeEventStream).toBeObservable(expectedFormChangeEventStream); }) ); it("should not emit operator property change event if the new property is the same as the old property", fakeAsync(() => { // add an operator and highlight the operator so that the // variables in property editor component is set correctly workflowActionService.addOperator(mockScanPredicate, mockPoint); const mockOperatorProperty = { tableName: "table" }; // set operator property first before displaying the operator property in property panel workflowActionService.setOperatorProperty(mockScanPredicate.operatorID, mockOperatorProperty); component.ngOnChanges({ currentOperatorId: new SimpleChange(undefined, mockScanPredicate.operatorID, true), }); fixture.detectChanges(); // stimulate a form change with the same property component.onFormChanges(mockOperatorProperty); // maintain a counter of how many times the event is emitted let emitEventCounter = 0; component.operatorPropertyChangeStream.subscribe(() => emitEventCounter++); // fakeAsync enables tick, which waits for the set property debounce time to finish tick(FORM_DEBOUNCE_TIME_MS + 10); discardPeriodicTasks(); // assert that the form change event doesn't emit any time // because the form change value is the same expect(emitEventCounter).toEqual(0); })); it("should change operator to default values", () => { // result operator has default values, use ajv to fill in default values // expected form output should fill in all default values instead of an empty object workflowActionService.addOperator(mockResultPredicate, mockPoint); component.ngOnChanges({ currentOperatorId: new SimpleChange(undefined, mockResultPredicate.operatorID, true), }); fixture.detectChanges(); const ajv = new Ajv({ useDefaults: true }); const expectedResultOperatorProperties = cloneDeep(mockResultPredicate.operatorProperties); ajv.validate(mockViewResultsSchema.jsonSchema, expectedResultOperatorProperties); expect(component.formData).toEqual(expectedResultOperatorProperties); }); it("should set result operator version", () => { workflowActionService.addOperator(mockResultPredicate, mockPoint); component.ngOnChanges({ currentOperatorId: new SimpleChange(undefined, mockResultPredicate.operatorID, true), }); fixture.detectChanges(); expect(component.operatorVersion).toEqual(mockResultPredicate.operatorVersion); }); it("should set scan operator version", () => { workflowActionService.addOperator(mockScanPredicate, mockPoint); component.ngOnChanges({ currentOperatorId: new SimpleChange(undefined, mockScanPredicate.operatorID, true), }); fixture.detectChanges(); expect(component.operatorVersion).toEqual(mockScanPredicate.operatorVersion); }); }); ================================================ FILE: frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from "@angular/core"; import { ExecuteWorkflowService } from "../../../service/execute-workflow/execute-workflow.service"; import { WorkflowStatusService } from "../../../service/workflow-status/workflow-status.service"; import { Subject } from "rxjs"; import { AbstractControl, FormGroup, FormsModule, ReactiveFormsModule } from "@angular/forms"; import { FormlyFieldConfig, FormlyFormOptions, FormlyModule } from "@ngx-formly/core"; import Ajv from "ajv"; import { FormlyJsonschema } from "@ngx-formly/core/json-schema"; import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; import { cloneDeep, isEqual } from "lodash-es"; import { AttributeTypeAllOfRule, AttributeTypeConstRule, AttributeTypeEnumRule, AttributeTypeRuleSet, CustomJSONSchema7, hideTypes, } from "../../../types/custom-json-schema.interface"; import { isDefined } from "../../../../common/util/predicate"; import { ExecutionState, OperatorState, OperatorStatistics } from "src/app/workspace/types/execute-workflow.interface"; import { DynamicSchemaService } from "../../../service/dynamic-schema/dynamic-schema.service"; import { WorkflowCompilingService } from "../../../service/compile-workflow/workflow-compiling.service"; import { createOutputFormChangeEventStream, createShouldHideFieldFunc, setChildTypeDependency, setHideExpression, } from "src/app/common/formly/formly-utils"; import { TYPE_CASTING_OPERATOR_TYPE, TypeCastingDisplayComponent, } from "../typecasting-display/type-casting-display.component"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { filter } from "rxjs/operators"; import { NotificationService } from "../../../../common/service/notification/notification.service"; import { PresetWrapperComponent } from "src/app/common/formly/preset-wrapper/preset-wrapper.component"; import { WorkflowVersionService } from "../../../../dashboard/service/user/workflow-version/workflow-version.service"; import { QuillBinding } from "y-quill"; import Quill from "quill"; import QuillCursors from "quill-cursors"; import * as Y from "yjs"; import { OperatorSchema } from "src/app/workspace/types/operator-schema.interface"; import { AttributeType, PortSchema } from "../../../types/workflow-compiling.interface"; import { GuiConfigService } from "../../../../common/service/gui-config.service"; import { NgIf } from "@angular/common"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzPopoverDirective } from "ng-zorro-antd/popover"; import { NzFormDirective } from "ng-zorro-antd/form"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; Quill.register("modules/cursors", QuillCursors); /** * Property Editor uses JSON Schema to automatically generate the form from the JSON Schema of an operator. * For example, the JSON Schema of Sentiment Analysis could be: * 'properties': { * 'attribute': { 'type': 'string' }, * 'resultAttribute': { 'type': 'string' } * } * The automatically generated form will show two input boxes, one titled 'attribute' and one titled 'resultAttribute'. * More examples of the operator JSON schema can be found in `mock-operator-metadata.data.ts` * More about JSON Schema: Understanding JSON Schema - https://spacetelescope.github.io/understanding-json-schema/ * * OperatorMetadataService will fetch metadata about the operators, which includes the JSON Schema, from the backend. * * We use library `@ngx-formly` to generate form from json schema * https://github.com/ngx-formly/ngx-formly */ @UntilDestroy() @Component({ selector: "texera-formly-form-frame", templateUrl: "./operator-property-edit-frame.component.html", styleUrls: ["./operator-property-edit-frame.component.scss"], imports: [ NgIf, NzSpaceCompactItemDirective, NzButtonComponent, ɵNzTransitionPatchDirective, NzTooltipDirective, NzIconDirective, NzPopoverDirective, FormsModule, NzFormDirective, ReactiveFormsModule, FormlyModule, TypeCastingDisplayComponent, NzWaveDirective, ], }) export class OperatorPropertyEditFrameComponent implements OnInit, OnChanges, OnDestroy { @Input() currentOperatorId?: string; currentOperatorSchema?: OperatorSchema; readonly OperatorState = OperatorState; currentOperatorStatus?: OperatorStatistics; // re-declare enum for angular template to access it readonly ExecutionState = ExecutionState; // whether the editor can be edited interactive: boolean = false; // the source event stream of form change triggered by library at each user input sourceFormChangeEventStream = new Subject>(); // the output form change event stream after debounce time and filtering out values operatorPropertyChangeStream = createOutputFormChangeEventStream(this.sourceFormChangeEventStream, data => this.checkOperatorProperty(data) ); listeningToChange: boolean = true; // inputs and two-way bindings to formly component formlyFormGroup: FormGroup | undefined; formData: any; formlyOptions: FormlyFormOptions = {}; formlyFields: FormlyFieldConfig[] | undefined; formTitle: string | undefined; operatorDescription: string | undefined; // The field name and its css style to be overridden, e.g., for showing the diff between two workflows. // example: new Map([ // ["attribute", "outline: 3px solid green; transition: 0.3s ease-in-out outline;"], // ["condition", "background: red; border-color: red;"], // ]); fieldStyleOverride: Map = new Map([]); editingTitle: boolean = false; // used to fill in default values in json schema to initialize new operator ajv = new Ajv({ useDefaults: true, strict: false }); isTypeCasting: boolean = false; // for display component of some extra information public operatorVersion: string = ""; quillBinding?: QuillBinding; quill!: Quill; // used to tear down subscriptions that takeUntil(teardownObservable) private teardownObservable: Subject = new Subject(); constructor( private formlyJsonschema: FormlyJsonschema, private workflowActionService: WorkflowActionService, public executeWorkflowService: ExecuteWorkflowService, private dynamicSchemaService: DynamicSchemaService, private workflowCompilingService: WorkflowCompilingService, private notificationService: NotificationService, private changeDetectorRef: ChangeDetectorRef, private workflowVersionService: WorkflowVersionService, private workflowStatusSerivce: WorkflowStatusService, private config: GuiConfigService ) {} ngOnChanges(changes: SimpleChanges): void { this.currentOperatorId = changes.currentOperatorId?.currentValue; if (!this.currentOperatorId) { return; } this.rerenderEditorForm(); } ngOnInit(): void { // listen to the autocomplete event, remove invalid properties, and update the schema displayed on the form this.registerOperatorSchemaChangeHandler(); // when the operator's property is updated via program instead of user updating the json schema form, // this observable will be responsible in handling these events. this.registerOperatorPropertyChangeHandler(); // handle the form change event on the user interface to actually set the operator property this.registerOnFormChangeHandler(); this.registerDisableEditorInteractivityHandler(); this.registerOperatorDisplayNameChangeHandler(); this.workflowStatusSerivce .getStatusUpdateStream() .pipe(untilDestroyed(this)) .subscribe(update => { if (this.currentOperatorId) { this.currentOperatorStatus = update[this.currentOperatorId]; } }); } async ngOnDestroy() { // await this.checkAndSavePreset(); this.teardownObservable.complete(); } /** * Callback function provided to the Angular Json Schema Form library, * whenever the form data is changed, this function is called. * It only serves as a bridge from a callback function to RxJS Observable * @param event */ onFormChanges(event: Record): void { this.sourceFormChangeEventStream.next(event); } /** * Changes the property editor to use the new operator data. * Sets all the data needed by the json schema form and displays the form. */ rerenderEditorForm(): void { if (!this.currentOperatorId) { return; } this.currentOperatorSchema = this.dynamicSchemaService.getDynamicSchema(this.currentOperatorId); this.currentOperatorStatus = this.workflowStatusSerivce.getCurrentStatus()[this.currentOperatorId]; this.workflowActionService.getTexeraGraph().updateSharedModelAwareness("currentlyEditing", this.currentOperatorId); const operator = this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId); // set the operator data needed this.workflowActionService.setOperatorVersion(operator.operatorID, this.currentOperatorSchema.operatorVersion); this.operatorVersion = operator.operatorVersion.slice(0, 9); this.setFormlyFormBinding(this.currentOperatorSchema.jsonSchema); this.formTitle = operator.customDisplayName ?? this.currentOperatorSchema.additionalMetadata.userFriendlyName; this.operatorDescription = this.currentOperatorSchema.additionalMetadata.operatorDescription; /** * Important: make a deep copy of the initial property data object. * Prevent the form directly changes the value in the texera graph without going through workflow action service. */ this.formData = cloneDeep(operator.operatorProperties); // use ajv to initialize the default value to data according to schema, see https://ajv.js.org/#assigning-defaults // WorkflowUtil service also makes sure that the default values are filled in when operator is added from the UI // However, we perform an addition check for the following reasons: // 1. the operator might be added not directly from the UI, which violates the precondition // 2. the schema might change, which specifies a new default value // 3. formly doesn't emit change event when it fills in default value, causing an inconsistency between component and service this.ajv.validate(this.currentOperatorSchema.jsonSchema, this.formData); // manually trigger a form change event because default value might be filled in this.onFormChanges(this.formData); this.isTypeCasting = this.workflowActionService .getTexeraGraph() .getOperator(this.currentOperatorId) .operatorType.includes(TYPE_CASTING_OPERATOR_TYPE); // execute set interactivity immediately in another task because of a formly bug // whenever the form model is changed, formly can only disable it after the UI is rendered setTimeout(() => { this.setInteractivity(this.interactive); this.changeDetectorRef.detectChanges(); }, 0); } setInteractivity(interactive: boolean) { this.interactive = interactive; if (this.formlyFormGroup !== undefined) { if (this.interactive) { this.formlyFormGroup.enable(); } else { this.formlyFormGroup.disable(); } } } checkOperatorProperty(formData: object): boolean { // check if the component is displaying operator property if (this.currentOperatorId === undefined) { return false; } // check if the operator still exists, it might be deleted during debounce time const operator = this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId); if (!operator) { return false; } // only emit change event if the form data actually changes return !isEqual(formData, operator.operatorProperties); } /** * This method handles the schema change event from autocomplete. It will get the new schema * propagated from autocomplete and check if the operators' properties that users input * previously are still valid. If invalid, it will remove these fields and triggered an event so * that the user interface will be updated through registerOperatorPropertyChangeHandler() method. * * If the operator that experiences schema changed is the same as the operator that is currently * displaying on the property panel, this handler will update the current operator schema * to the new schema. */ registerOperatorSchemaChangeHandler(): void { this.dynamicSchemaService .getOperatorDynamicSchemaChangedStream() .pipe(filter(({ operatorID }) => operatorID === this.currentOperatorId)) .pipe(untilDestroyed(this)) .subscribe(_ => this.rerenderEditorForm()); } /** * This method captures the change in operator's property via program instead of user updating the * json schema form in the user interface. * * For instance, when the input doesn't match the new json schema and the UI needs to remove the * invalid fields, this form will capture those events. */ registerOperatorPropertyChangeHandler(): void { this.workflowActionService .getTexeraGraph() .getOperatorPropertyChangeStream() .pipe( filter(_ => this.listeningToChange), filter(_ => this.currentOperatorId !== undefined), filter(operatorChanged => operatorChanged.operator.operatorID === this.currentOperatorId) ) .pipe(untilDestroyed(this)) .subscribe(operatorChanged => { this.formData = cloneDeep(operatorChanged.operator.operatorProperties); this.changeDetectorRef.detectChanges(); }); } /** * This method handles the form change event and set the operator property * in the texera graph. */ registerOnFormChangeHandler(): void { this.operatorPropertyChangeStream.pipe(untilDestroyed(this)).subscribe(formData => { // set the operator property to be the new form data if (this.currentOperatorId) { this.listeningToChange = false; this.typeInferenceOnLambdaFunction(formData); this.workflowActionService.setOperatorProperty(this.currentOperatorId, cloneDeep(formData)); this.listeningToChange = true; } }); } typeInferenceOnLambdaFunction(formData: any): void { if (!this.currentOperatorId?.includes("PythonLambdaFunction")) { return; } const opInputSchema = this.workflowCompilingService.getOperatorInputSchemaMap(this.currentOperatorId); if (!opInputSchema) { return; } const firstPortInputSchema = opInputSchema[0]; if (!firstPortInputSchema) { return; } const schemaMap = new Map(firstPortInputSchema?.map(obj => [obj.attributeName, obj.attributeType])); formData.lambdaAttributeUnits.forEach((unit: any, index: number, a: any) => { if (unit.attributeName === "Add New Column" && !unit.newAttributeName) a[index].attributeType = ""; if (schemaMap.has(unit.attributeName)) a[index].attributeType = schemaMap.get(unit.attributeName); }); } registerDisableEditorInteractivityHandler(): void { this.workflowActionService .getWorkflowModificationEnabledStream() .pipe(untilDestroyed(this)) .subscribe(canModify => { if (this.currentOperatorId) { this.setInteractivity(canModify); this.changeDetectorRef.detectChanges(); } }); } setFormlyFormBinding(schema: CustomJSONSchema7) { var operatorPropertyDiff = this.workflowVersionService.operatorPropertyDiff; if (this.currentOperatorId != undefined && operatorPropertyDiff[this.currentOperatorId] != undefined) { this.fieldStyleOverride = operatorPropertyDiff[this.currentOperatorId]; } if (this.fieldStyleOverride.has("operatorVersion")) { var boundary = this.fieldStyleOverride.get("operatorVersion"); if (boundary) { document.getElementsByClassName("operator-version")[0].setAttribute("style", boundary.toString()); } } // intercept JsonSchema -> FormlySchema process, adding custom options // this requires a one-to-one mapping. // for relational custom options, have to do it after FormlySchema is generated. const jsonSchemaMapIntercept = ( mappedField: FormlyFieldConfig, mapSource: CustomJSONSchema7 ): FormlyFieldConfig => { // apply the overridden css style if applicable mappedField.expressions = { "templateOptions.attributes": () => { if ( isDefined(mappedField) && typeof mappedField.key === "string" && this.fieldStyleOverride.has(mappedField.key) ) { return { style: this.fieldStyleOverride.get(mappedField.key) }; } else { return {}; } }, }; // Disable dummy operator for user if (mappedField.key === "dummyOperator") { mappedField.expressions = { ...mappedField.expressions, "templateOptions.disabled": () => true, "templateOptions.readonly": () => true, }; } // Disable dummy property and value fields for user if (mappedField.key === "dummyProperty" || mappedField.key === "dummyValue") { mappedField.expressions = { ...mappedField.expressions, "templateOptions.readonly": () => true, "templateOptions.disabled": () => true, }; } // Disable dummy property list for all operators, except for dummy operator. if (mappedField.key === "dummyPropertyList") { mappedField.hide = this.currentOperatorSchema?.operatorType !== "Dummy"; mappedField.expressions = { ...mappedField.expressions, "templateOptions.disabled": () => true, "templateOptions.readonly": () => true, "templateOptions.canRemove": () => false, "templateOptions.canAdd": () => false, }; } // conditionally hide the field according to the schema if ( isDefined(mapSource.hideExpectedValue) && isDefined(mapSource.hideTarget) && isDefined(mapSource.hideType) && hideTypes.includes(mapSource.hideType) ) { mappedField.expressions = { ...mappedField.expressions, hide: createShouldHideFieldFunc( mapSource.hideTarget, mapSource.hideType, mapSource.hideExpectedValue, mapSource.hideOnNull ), }; } // if the title is fileName, then change it to custom autocomplete input template if (mappedField.key === "fileName") { mappedField.type = "inputautocomplete"; } if (mappedField.key === "datasetVersionPath") { mappedField.type = "datasetversionselector"; } if (this.currentOperatorSchema?.operatorType === "FileScanOp" && mappedField.key === "outputFileName") { mappedField.expressions = { ...mappedField.expressions, hide: (field: FormlyFieldConfig) => { const model = field.model as { extract?: boolean; attributeType?: string } | undefined; const attributeType = model?.attributeType; return !( model?.extract === true || attributeType === "single string" || attributeType === "binary" || attributeType === "large binary" ); }, }; } // if the title is python script (for Python UDF), then make this field a custom template 'codearea' if (mapSource?.description?.toLowerCase() === "input your code here") { if (mappedField.type) { mappedField.type = "codearea"; } } // if presetService is ready and operator property allows presets, setup formly field to display presets if ( this.config.env.userPresetEnabled && mapSource["enable-presets"] !== undefined && this.currentOperatorId !== undefined ) { PresetWrapperComponent.setupFieldConfig( mappedField, "operator", this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId).operatorType, this.currentOperatorId ); } // TODO: we temporarily disable this due to Yjs update causing issues in Formly. // if ( // this.currentOperatorId !== undefined && // ["string", "textarea"].includes(mappedField.type as string) && // (mappedField.key as string) !== "password" // ) { // CollabWrapperComponent.setupFieldConfig( // mappedField, // this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId).operatorType, // this.currentOperatorId, // mappedField.wrappers?.includes("preset-wrapper") // ); // } if (this.currentOperatorSchema?.operatorType === "Projection" && mappedField.key === "attributes") { mappedField.type = "repeat-section-dnd"; mappedField.props = { ...mappedField.props, reorder: () => this.onFormChanges(cloneDeep(this.formData)), }; } if (mappedField.validators === undefined) { mappedField.validators = {}; // set show to true, or else the error will only show after the user changes the field mappedField.validation = { show: true, }; } if (isDefined(mapSource.enum)) { mappedField.validators.inEnum = { expression: (c: AbstractControl) => mapSource.enum?.includes(c.value ?? ""), message: (error: any, field: FormlyFieldConfig) => `"${field.formControl?.value}" is no longer a valid option`, }; } // Add custom validators for attribute type if (isDefined(mapSource.attributeTypeRules)) { mappedField.validators.checkAttributeType = { expression: (control: AbstractControl, field: FormlyFieldConfig) => { if ( !( isDefined(this.currentOperatorId) && isDefined(mapSource.attributeTypeRules) && isDefined(mapSource.properties) ) ) { return true; } const findAttributeType = (propertyName: string): AttributeType | undefined => { if ( !isDefined(this.currentOperatorId) || !isDefined(mapSource.properties) || !isDefined(mapSource.properties[propertyName]) ) { return undefined; } const portIndex = (mapSource.properties[propertyName] as CustomJSONSchema7).autofillAttributeOnPort; if (!isDefined(portIndex)) { return undefined; } const attributeName: string = control.value[propertyName]; return this.workflowCompilingService.getOperatorInputAttributeType( this.currentOperatorId, portIndex, attributeName ); }; const checkEnumConstraint = (inputAttributeType: AttributeType, enumConstraint: AttributeTypeEnumRule) => { if (!enumConstraint.includes(inputAttributeType)) { throw TypeError(`it's expected to be ${enumConstraint.join(" or ")}.`); } }; const checkConstConstraint = ( inputAttributeType: AttributeType, constConstraint: AttributeTypeConstRule ) => { const data = constConstraint?.$data; if (!isDefined(data)) { return; } const dataAttributeType = findAttributeType(data); if (!isDefined(dataAttributeType)) { // if data attribute type is not defined, then data attribute is not yet selected. skip validation return; } if (inputAttributeType !== dataAttributeType) { // get data attribute name for error message const dataAttributeName = control.value[data]; throw TypeError(`it's expected to be the same type as '${dataAttributeName}' (${dataAttributeType}).`); } }; const checkAllOfConstraint = ( inputAttributeType: AttributeType, allOfConstraint: AttributeTypeAllOfRule ) => { // traverse through all "if-then" sets in "allOf" constraint for (const allOf of allOfConstraint) { // Only return false when "if" condition is satisfied but "then" condition is not satisfied let ifCondSatisfied = true; for (const [ifProp, ifConstraint] of Object.entries(allOf.if)) { // Currently, only support "valEnum" constraint // Find attribute value (not type) const ifAttributeValue = control.value[ifProp]; if (!ifConstraint.valEnum?.includes(ifAttributeValue)) { ifCondSatisfied = false; break; } } // Currently, only support "enum" constraint, // add more to the condition if needed if (ifCondSatisfied && isDefined(allOf.then.enum)) { try { checkEnumConstraint(inputAttributeType, allOf.then.enum); } catch { // parse if condition to readable string const ifCondStr = Object.entries(allOf.if) .map(([ifProp]) => `'${ifProp}' is ${control.value[ifProp]}`) .join(" and "); throw TypeError(`it's expected to be ${allOf.then.enum?.join(" or ")}, given that ${ifCondStr}`); } } } }; // Get the type of constrains for each property in AttributeTypeRuleSchema const checkConstraint = (propertyName: string, constraint: AttributeTypeRuleSet) => { const inputAttributeType = findAttributeType(propertyName); if (!isDefined(inputAttributeType)) { // when inputAttributeType is undefined, it means the property is not set return; } if (isDefined(constraint.enum)) { checkEnumConstraint(inputAttributeType, constraint.enum); } if (isDefined(constraint.const)) { checkConstConstraint(inputAttributeType, constraint.const); } if (isDefined(constraint.allOf)) { checkAllOfConstraint(inputAttributeType, constraint.allOf); } }; // iterate through all properties in attributeType for (const [prop, constraint] of Object.entries(mapSource.attributeTypeRules)) { try { checkConstraint(prop, constraint); } catch (err) { // have to get the type, attribute name and property name again // should consider reusing the part in findAttributeType() const attributeName = control.value[prop]; const port = (mapSource.properties[prop] as CustomJSONSchema7).autofillAttributeOnPort as number; const inputAttributeType = this.workflowCompilingService.getOperatorInputAttributeType( this.currentOperatorId, port, attributeName ); // @ts-ignore const message = err.message; if (field.validators === undefined) { field.validators = {}; } field.validators.checkAttributeType.message = `Warning: The type of '${attributeName}' is ${inputAttributeType}, but ` + message; return false; } } return true; }, }; } return mappedField; }; this.formlyFormGroup = new FormGroup({}); this.formlyOptions = {}; // convert the json schema to formly config, pass a copy because formly mutates the schema object const field = this.formlyJsonschema.toFieldConfig(cloneDeep(schema), { map: jsonSchemaMapIntercept, }); field.hooks = { onInit: fieldConfig => { if (!this.interactive) { fieldConfig?.form?.disable(); } }, }; const schemaProperties = schema.properties; const fields = field.fieldGroup; // adding custom options, relational N-to-M mapping. if (schemaProperties && fields) { Object.entries(schemaProperties).forEach(([propertyName, propertyValue]) => { if (typeof propertyValue === "boolean") { return; } if (propertyValue.toggleHidden) { setHideExpression(propertyValue.toggleHidden, fields, propertyName); } if (propertyValue.dependOn) { if (isDefined(this.currentOperatorId)) { const attributes: Readonly> | undefined = this.workflowCompilingService.getOperatorInputSchemaMap(this.currentOperatorId); setChildTypeDependency(attributes, propertyValue.dependOn, fields, propertyName); } } }); } // not return field.fieldGroup directly because // doing so the validator in the field will not be triggered this.formlyFields = [field]; } allowModifyOperatorLogic(): void { this.setInteractivity(true); } confirmModifyOperatorLogic(): void { if (this.currentOperatorId) { try { this.executeWorkflowService.modifyOperatorLogic(this.currentOperatorId); this.setInteractivity(false); } catch (e) { this.notificationService.error((e as Error).message); } } } /** * Connects the actual y-text structure of this operator's name to the editor's awareness manager. */ connectQuillToText() { this.registerQuillBinding(); const currentOperatorSharedType = this.workflowActionService .getTexeraGraph() .getSharedOperatorType(this.currentOperatorId); if (this.currentOperatorId) { if (!currentOperatorSharedType.has("customDisplayName")) { currentOperatorSharedType.set("customDisplayName", new Y.Text()); } const ytext = currentOperatorSharedType.get("customDisplayName"); this.quillBinding = new QuillBinding( ytext as Y.Text, this.quill, this.workflowActionService.getTexeraGraph().getSharedModelAwareness() ); } } /** * Stop editing title and hide the editor. */ disconnectQuillFromText() { this.quill.blur(); this.quillBinding = undefined; this.editingTitle = false; } private registerOperatorDisplayNameChangeHandler(): void { this.workflowActionService .getTexeraGraph() .getOperatorDisplayNameChangedStream() .pipe(untilDestroyed(this)) .subscribe(({ operatorID, newDisplayName }) => { if (operatorID === this.currentOperatorId) this.formTitle = newDisplayName; }); } /** * Initializes shared text editor. * @private */ private registerQuillBinding() { // Operator name editor const element = document.getElementById("customName") as Element; this.quill = new Quill(element, { modules: { cursors: true, toolbar: false, history: { // Local undo shouldn't undo changes // from remote users userOnly: true, }, // Disable newline on enter and instead quit editing keyboard: { bindings: { enter: { key: 13, handler: () => this.disconnectQuillFromText(), }, shift_enter: { key: 13, shiftKey: true, handler: () => this.disconnectQuillFromText(), }, }, }, }, formats: [], placeholder: "Start collaborating...", theme: "snow", }); } } ================================================ FILE: frontend/src/app/workspace/component/property-editor/port-property-edit-frame/port-property-edit-frame.component.html ================================================

    {{ formTitle }}

    ================================================ FILE: frontend/src/app/workspace/component/property-editor/port-property-edit-frame/port-property-edit-frame.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ #formly-title { position: relative; h3 { margin: 0; display: inline-block; } button { position: absolute; top: 45%; transform: translateY(-55%); } } .texera-workspace-property-editor-form { padding-top: 20px; } ================================================ FILE: frontend/src/app/workspace/component/property-editor/port-property-edit-frame/port-property-edit-frame.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { PortPropertyEditFrameComponent } from "./port-property-edit-frame.component"; import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { commonTestProviders } from "../../../../common/testing/test-utils"; describe("PortPropertyEditFrameComponent", () => { let component: PortPropertyEditFrameComponent; let fixture: ComponentFixture; let workflowActionService: WorkflowActionService; beforeEach(async () => { await TestBed.configureTestingModule({ providers: [WorkflowActionService, ...commonTestProviders], imports: [PortPropertyEditFrameComponent, HttpClientTestingModule], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(PortPropertyEditFrameComponent); component = fixture.componentInstance; workflowActionService = TestBed.inject(WorkflowActionService); fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/workspace/component/property-editor/port-property-edit-frame/port-property-edit-frame.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, Input, OnChanges, OnInit, SimpleChanges } from "@angular/core"; import { LogicalPort, PortDescription } from "../../../types/workflow-common.interface"; import { Subject } from "rxjs"; import { createOutputFormChangeEventStream } from "../../../../common/formly/formly-utils"; import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; import { isEqual } from "lodash"; import { CustomJSONSchema7 } from "../../../types/custom-json-schema.interface"; import { FormlyFieldConfig, FormlyFormOptions, FormlyModule } from "@ngx-formly/core"; import { FormGroup, FormsModule, ReactiveFormsModule } from "@angular/forms"; import { cloneDeep } from "lodash-es"; import { FormlyJsonschema } from "@ngx-formly/core/json-schema"; import { filter } from "rxjs/operators"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import * as Y from "yjs"; import { QuillBinding } from "y-quill"; import Quill from "quill"; import QuillCursors from "quill-cursors"; import { mockPortSchema } from "../../../service/operator-metadata/mock-operator-metadata.data"; import { DynamicSchemaService } from "../../../service/dynamic-schema/dynamic-schema.service"; import { NgIf } from "@angular/common"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzIconDirective } from "ng-zorro-antd/icon"; Quill.register("modules/cursors", QuillCursors); @UntilDestroy() @Component({ selector: "texera-port-property-edit-frame", templateUrl: "./port-property-edit-frame.component.html", styleUrls: ["./port-property-edit-frame.component.scss"], imports: [ NgIf, NzSpaceCompactItemDirective, NzButtonComponent, ɵNzTransitionPatchDirective, NzTooltipDirective, NzIconDirective, FormsModule, ReactiveFormsModule, FormlyModule, ], }) export class PortPropertyEditFrameComponent implements OnInit, OnChanges { @Input() currentPortID: LogicalPort | undefined; // whether the editor can be edited interactive: boolean = true; listeningToChange: boolean = true; formlyFormGroup: FormGroup | undefined; formData: any; formlyOptions: FormlyFormOptions = {}; formlyFields: FormlyFieldConfig[] | undefined; formTitle: string | undefined; editingTitle: boolean = false; quillBinding?: QuillBinding; quill!: Quill; // the source event stream of form change triggered by library at each user input sourceFormChangeEventStream = new Subject>(); // the output form change event stream after debounce time and filtering out values portPropertyChangeStream = createOutputFormChangeEventStream(this.sourceFormChangeEventStream, data => this.checkPort(data) ); constructor( private formlyJsonschema: FormlyJsonschema, private workflowActionService: WorkflowActionService, private dynamicSchemaService: DynamicSchemaService ) {} ngOnInit(): void { this.registerPortPropertyChangeHandler(); this.registerPortDisplayNameChangeHandler(); this.registerOnFormChangeHandler(); } ngOnChanges(changes: SimpleChanges): void { this.currentPortID = changes.currentPortID.currentValue; if (this.currentPortID) this.showPortPropertyEditor(this.currentPortID); } /** * Callback function provided to the Angular Json Schema Form library, * whenever the form data is changed, this function is called. * It only serves as a bridge from a callback function to RxJS Observable * @param event */ onFormChanges(event: Record): void { this.sourceFormChangeEventStream.next(event); } /** * Connects the actual y-text structure of this operator's name to the editor's awareness manager. */ connectQuillToText() { this.registerQuillBinding(); if (!this.currentPortID) return; const currentPortDescriptorSharedType = this.workflowActionService .getTexeraGraph() .getSharedPortDescriptionType(this.currentPortID); if (currentPortDescriptorSharedType === undefined) return; if (!currentPortDescriptorSharedType.has("displayName")) { currentPortDescriptorSharedType.set("displayName", new Y.Text()); } const ytext = currentPortDescriptorSharedType.get("displayName"); this.quillBinding = new QuillBinding( ytext as Y.Text, this.quill, this.workflowActionService.getTexeraGraph().getSharedModelAwareness() ); } /** * Stop editing title and hide the editor. */ disconnectQuillFromText() { this.quill.blur(); this.quillBinding = undefined; this.editingTitle = false; } private showPortPropertyEditor(operatorPortID: LogicalPort): void { if (!this.workflowActionService.getTexeraGraph().hasPort(operatorPortID)) { throw new Error( `change property editor: operator port ${operatorPortID.operatorID}, ${operatorPortID.portID}} does not exist` ); } this.currentPortID = operatorPortID; const portDescriptor = this.workflowActionService .getTexeraGraph() .getPortDescription(operatorPortID) as PortDescription; this.formTitle = portDescriptor.displayName; const currentOperatorSchema = this.dynamicSchemaService.getDynamicSchema(this.currentPortID.operatorID); // Only specific types of operators and input ports can have the following customization. if (!(currentOperatorSchema.additionalMetadata.allowPortCustomization && portDescriptor.portID.includes("input"))) return; const portInfo = { partitionInfo: portDescriptor?.partitionRequirement, dependencies: portDescriptor?.dependencies, }; this.formData = cloneDeep(portInfo); const portSchema = mockPortSchema.jsonSchema; this.setFormlyFormBinding(portSchema); } private checkPort(formData: Record): boolean { // check if the component is displaying the port if (!this.currentPortID) return false; if (!this.workflowActionService.getTexeraGraph().hasPort(this.currentPortID)) return false; const operatorPortDescription = this.workflowActionService.getTexeraGraph().getPortDescription(this.currentPortID); return !isEqual(formData, operatorPortDescription?.partitionRequirement); } /** * This method handles the form change event */ private registerOnFormChangeHandler(): void { this.portPropertyChangeStream.pipe(untilDestroyed(this)).subscribe(formData => { if (this.currentPortID) { this.listeningToChange = false; this.workflowActionService.setPortProperty(this.currentPortID, cloneDeep(formData)); this.listeningToChange = true; } }); } /** * This method captures the change in the operator's property via a program instead of user updating the * json schema form in the user interface. * * For instance, when the input doesn't match the new json schema and the UI needs to remove the * invalid fields, this form will capture those events. */ private registerPortPropertyChangeHandler(): void { this.workflowActionService .getTexeraGraph() .getPortPropertyChangedStream() .pipe( filter(_ => this.listeningToChange), filter(_ => this.currentPortID !== undefined), filter(event => isEqual(event.operatorPortID, this.currentPortID)), filter(event => !isEqual(this.formData, event.newProperty)) ) .pipe(untilDestroyed(this)) .subscribe(event => (this.formData = cloneDeep(event.newProperty))); } private setFormlyFormBinding(schema: CustomJSONSchema7) { this.formlyFormGroup = new FormGroup({}); this.formlyOptions = {}; // convert the json schema to formly config, pass a copy because formly mutates the schema object const field = this.formlyJsonschema.toFieldConfig(cloneDeep(schema)); field.hooks = { onInit: fieldConfig => { if (!this.interactive) { fieldConfig?.form?.disable(); } }, }; this.formlyFields = field.fieldGroup; } /** * Initializes shared text editor. * @private */ private registerQuillBinding() { // Operator name editor const element = document.getElementById("customName") as Element; this.quill = new Quill(element, { modules: { cursors: true, toolbar: false, history: { // Local undo shouldn't undo changes // from remote users userOnly: true, }, // Disable newline on enter and instead quit editing keyboard: { bindings: { enter: { key: 13, handler: () => this.disconnectQuillFromText(), }, shift_enter: { key: 13, shiftKey: true, handler: () => this.disconnectQuillFromText(), }, }, }, }, formats: [], placeholder: "Start collaborating...", theme: "snow", }); } private registerPortDisplayNameChangeHandler(): void { this.workflowActionService .getTexeraGraph() .getPortDisplayNameChangedSubject() .pipe(untilDestroyed(this)) .subscribe(({ operatorID, portID, newDisplayName }) => { if (operatorID === this.currentPortID?.operatorID && portID === this.currentPortID?.portID) this.formTitle = newDisplayName; }); } } ================================================ FILE: frontend/src/app/workspace/component/property-editor/property-editor.component.html ================================================

    Property

    ================================================ FILE: frontend/src/app/workspace/component/property-editor/property-editor.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ :host { display: block; height: 100%; width: 100%; position: fixed; z-index: 3; } #right-container { position: fixed; top: 10vh; right: 0; background: white; } #title { padding: 5px 9px; border-bottom: 1px solid #e0e0e0; position: absolute; top: 0; background: white; width: 100%; z-index: 2; } #content { height: 100%; overflow-y: auto; padding-top: 38px; } #property-editor { padding: 0 15px; } #property-buttons { position: absolute; top: 0; right: 0; z-index: 4; display: flex; } #docked-buttons { position: fixed; top: 10vh; right: 0; z-index: 4; } #divider { margin: 0; } .ant-menu-item { margin: 0 !important; height: 32px; line-height: 32px; padding: 0 9px; } .shadow { border-radius: 5px; box-shadow: 0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f; } ================================================ FILE: frontend/src/app/workspace/component/property-editor/property-editor.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { CommonModule } from "@angular/common"; import { NO_ERRORS_SCHEMA } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { PropertyEditorComponent } from "./property-editor.component"; import { mockPoint, mockResultPredicate, mockScanPredicate, } from "../../service/workflow-graph/model/mock-workflow-data"; import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; import { OperatorPropertyEditFrameComponent } from "./operator-property-edit-frame/operator-property-edit-frame.component"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { OperatorMetadataService } from "../../service/operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../../service/operator-metadata/stub-operator-metadata.service"; import { ComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; import { MockComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service"; import { commonTestProviders } from "../../../common/testing/test-utils"; describe("PropertyEditorComponent", () => { let component: PropertyEditorComponent; let fixture: ComponentFixture; let workflowActionService: WorkflowActionService; beforeEach(async () => { TestBed.overrideComponent(PropertyEditorComponent, { set: { template: '
    ', }, }); await TestBed.configureTestingModule({ imports: [PropertyEditorComponent, CommonModule, HttpClientTestingModule], providers: [ { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService }, ...commonTestProviders, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(PropertyEditorComponent); component = fixture.componentInstance; workflowActionService = TestBed.inject(WorkflowActionService); fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); /** * test if the property editor correctly receives the operator unhighlight stream * and clears all the operator data, and hide the form. */ it("should clear and hide the property editor panel correctly when no operator is highlighted", () => { const jointGraphWrapper = workflowActionService.getJointGraphWrapper(); // add and highlight an operator workflowActionService.addOperator(mockScanPredicate, mockPoint); jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID); fixture.detectChanges(); expect(component.currentComponent).toBe(OperatorPropertyEditFrameComponent); expect(component.componentInputs).toEqual({ currentOperatorId: mockScanPredicate.operatorID, }); // unhighlight the operator jointGraphWrapper.unhighlightOperators(mockScanPredicate.operatorID); expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([]); fixture.detectChanges(); // check if the clearPropertyEditor called after the operator // is unhighlighted has correctly updated the variables expect(component.currentComponent).toBeNull(); }); it("should clear and hide the property editor panel correctly when multiple operators are highlighted", () => { const jointGraphWrapper = workflowActionService.getJointGraphWrapper(); // add and highlight two operators workflowActionService.addOperatorsAndLinks( [ { op: mockScanPredicate, pos: mockPoint }, { op: mockResultPredicate, pos: mockPoint }, ], [] ); jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID, mockResultPredicate.operatorID); // assert that multiple operators are highlighted expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockResultPredicate.operatorID); expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockScanPredicate.operatorID); fixture.detectChanges(); // expect that the property editor is cleared expect(component.currentComponent).toBeNull(); }); it("should switch the content of property editor to another operator from the former operator correctly", () => { const jointGraphWrapper = workflowActionService.getJointGraphWrapper(); // add two operators workflowActionService.addOperator(mockScanPredicate, mockPoint); workflowActionService.addOperator(mockResultPredicate, mockPoint); // highlight the first operator jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID); fixture.detectChanges(); // check the variables expect(component.currentComponent).toBe(OperatorPropertyEditFrameComponent); expect(component.componentInputs).toEqual({ currentOperatorId: mockScanPredicate.operatorID, }); // unhighlight the operator jointGraphWrapper.unhighlightOperators(mockScanPredicate.operatorID); fixture.detectChanges(); expect(component.currentComponent).toBeNull(); // highlight the second operator jointGraphWrapper.highlightOperators(mockResultPredicate.operatorID); fixture.detectChanges(); expect(component.currentComponent).toBe(OperatorPropertyEditFrameComponent); expect(component.componentInputs).toEqual({ currentOperatorId: mockResultPredicate.operatorID, }); }); }); ================================================ FILE: frontend/src/app/workspace/component/property-editor/property-editor.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ChangeDetectorRef, Component, ElementRef, HostListener, OnDestroy, OnInit, Type, ViewChild, } from "@angular/core"; import { merge } from "rxjs"; import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; import { OperatorPropertyEditFrameComponent } from "./operator-property-edit-frame/operator-property-edit-frame.component"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { filter } from "rxjs/operators"; import { PortPropertyEditFrameComponent } from "./port-property-edit-frame/port-property-edit-frame.component"; import { NzResizeEvent, NzResizableDirective, NzResizeHandlesComponent } from "ng-zorro-antd/resizable"; import { calculateTotalTranslate3d } from "../../../common/util/panel-dock"; import { PanelService } from "../../service/panel/panel.service"; import { NzMenuDirective, NzMenuItemComponent, NzMenuDividerDirective } from "ng-zorro-antd/menu"; import { NgClass, NgIf, NgComponentOutlet } from "@angular/common"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { CdkDrag, CdkDragHandle } from "@angular/cdk/drag-drop"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { FormlyRepeatDndComponent } from "../../../common/formly/repeat-dnd/repeat-dnd.component"; /** * PropertyEditorComponent is the panel that allows user to edit operator properties. * Depending on the highlighted operator or link, it displays OperatorPropertyEditFrameComponent * or BreakpointPropertyEditFrameComponent accordingly * */ @UntilDestroy() @Component({ selector: "texera-property-editor", templateUrl: "property-editor.component.html", styleUrls: ["property-editor.component.scss"], imports: [ NzMenuDirective, NgClass, NgIf, NzMenuItemComponent, ɵNzTransitionPatchDirective, NzIconDirective, NzTooltipDirective, CdkDrag, NzResizableDirective, NzSpaceCompactItemDirective, NzButtonComponent, NzMenuDividerDirective, CdkDragHandle, NgComponentOutlet, NzResizeHandlesComponent, FormlyRepeatDndComponent, ], }) export class PropertyEditorComponent implements OnInit, OnDestroy { @ViewChild("contentWrapper") contentWrapperRef!: ElementRef; protected readonly window = window; id = -1; width = 260; height = Math.max(300, window.innerHeight * 0.6); currentComponent: Type | null = null; componentInputs = {}; dragPosition = { x: 0, y: 0 }; returnPosition = { x: 0, y: 0 }; constructor( public workflowActionService: WorkflowActionService, private changeDetectorRef: ChangeDetectorRef, private panelService: PanelService ) { const width = localStorage.getItem("right-panel-width"); if (width) this.width = Number(width); this.height = Number(localStorage.getItem("right-panel-height")) || this.height; } ngOnInit(): void { const style = localStorage.getItem("right-panel-style"); if (style) document.getElementById("right-container")!.style.cssText = style; const translates = document.getElementById("right-container")!.style.transform; const [xOffset, yOffset, _] = calculateTotalTranslate3d(translates); this.returnPosition = { x: -xOffset, y: -yOffset }; this.registerHighlightEventsHandler(); this.panelService.closePanelStream.pipe(untilDestroyed(this)).subscribe(() => this.closePanel()); this.panelService.resetPanelStream.pipe(untilDestroyed(this)).subscribe(() => { this.resetPanelPosition(); this.openPanel(); }); } private updateHeightBasedOnContent(): void { setTimeout(() => { const contentEl = this.contentWrapperRef?.nativeElement; if (contentEl) { const contentHeight = contentEl.scrollHeight; const maxHeight = this.window.innerHeight * 0.6; this.height = Math.min(contentHeight + 40, maxHeight); this.changeDetectorRef.detectChanges(); } }); } @HostListener("window:beforeunload") ngOnDestroy(): void { localStorage.setItem("right-panel-width", String(this.width)); localStorage.setItem("right-panel-height", String(this.height)); const rightContainer = document.getElementById("right-container"); if (rightContainer) { localStorage.setItem("right-panel-style", rightContainer.style.cssText); } } /** * This method changes the property editor according to how operators are highlighted on the workflow editor. * * Displays the form of the highlighted operator if only one operator is highlighted; * Displays the form of the link breakpoint if only one link is highlighted; * hides the form if no operator/link is highlighted or multiple operators and/or groups and/or links are highlighted. */ registerHighlightEventsHandler() { merge( this.workflowActionService.getJointGraphWrapper().getJointOperatorHighlightStream(), this.workflowActionService.getJointGraphWrapper().getJointOperatorUnhighlightStream(), this.workflowActionService.getJointGraphWrapper().getJointGroupHighlightStream(), this.workflowActionService.getJointGraphWrapper().getJointGroupUnhighlightStream(), this.workflowActionService.getJointGraphWrapper().getLinkHighlightStream(), this.workflowActionService.getJointGraphWrapper().getLinkUnhighlightStream(), this.workflowActionService.getJointGraphWrapper().getJointCommentBoxHighlightStream(), this.workflowActionService.getJointGraphWrapper().getJointCommentBoxUnhighlightStream(), this.workflowActionService.getJointGraphWrapper().getJointPortHighlightStream(), this.workflowActionService.getJointGraphWrapper().getJointPortUnhighlightStream() ) .pipe( filter(() => this.workflowActionService.getTexeraGraph().getSyncTexeraGraph()), untilDestroyed(this) ) .subscribe(_ => { const highlightedOperators = this.workflowActionService .getJointGraphWrapper() .getCurrentHighlightedOperatorIDs(); const highlightLinks = this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedLinkIDs(); this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedCommentBoxIDs(); const highlightedPorts = this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedPortIDs(); if (highlightedOperators.length === 1 && highlightLinks.length === 0 && highlightedPorts.length === 0) { this.currentComponent = OperatorPropertyEditFrameComponent; this.componentInputs = { currentOperatorId: highlightedOperators[0] }; } else if (highlightedPorts.length === 1 && highlightLinks.length === 0) { this.currentComponent = PortPropertyEditFrameComponent; this.componentInputs = { currentPortID: highlightedPorts[0] }; } else { this.currentComponent = null; this.componentInputs = {}; this.workflowActionService.getTexeraGraph().updateSharedModelAwareness("currentlyEditing", undefined); } this.changeDetectorRef.detectChanges(); this.updateHeightBasedOnContent(); }); } onResize({ width, height }: NzResizeEvent) { cancelAnimationFrame(this.id); this.id = requestAnimationFrame(() => { this.width = width!; this.height = height!; }); } openPanel() { this.width = 280; this.height = 300; this.updateHeightBasedOnContent(); } closePanel() { this.width = 0; this.height = 65; } resetPanelPosition() { this.dragPosition = { x: this.returnPosition.x, y: this.returnPosition.y }; } } ================================================ FILE: frontend/src/app/workspace/component/property-editor/typecasting-display/type-casting-display.component.html ================================================
    Attribute Name Attribute Type {{ attribute.attributeName }} {{ attribute.attributeType }}
    ================================================ FILE: frontend/src/app/workspace/component/property-editor/typecasting-display/type-casting-display.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { WorkflowCompilingService } from "../../../service/compile-workflow/workflow-compiling.service"; import { TypeCastingDisplayComponent } from "./type-casting-display.component"; import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; import { OperatorMetadataService } from "../../../service/operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../../../service/operator-metadata/stub-operator-metadata.service"; import { JointUIService } from "../../../service/joint-ui/joint-ui.service"; import { UndoRedoService } from "../../../service/undo-redo/undo-redo.service"; import { WorkflowUtilService } from "../../../service/workflow-graph/util/workflow-util.service"; import { commonTestProviders } from "../../../../common/testing/test-utils"; describe("TypecastingDisplayComponent", () => { let component: TypeCastingDisplayComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [TypeCastingDisplayComponent, HttpClientTestingModule], providers: [ { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, JointUIService, UndoRedoService, WorkflowUtilService, WorkflowActionService, WorkflowCompilingService, ...commonTestProviders, ], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(TypeCastingDisplayComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/workspace/component/property-editor/typecasting-display/type-casting-display.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, Input, OnChanges, OnInit } from "@angular/core"; import { WorkflowActionService } from "src/app/workspace/service/workflow-graph/model/workflow-action.service"; import { WorkflowCompilingService } from "../../../service/compile-workflow/workflow-compiling.service"; import { filter, map } from "rxjs/operators"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { AttributeType, SchemaAttribute } from "../../../types/workflow-compiling.interface"; import { NgIf, NgFor } from "@angular/common"; import { NzTableComponent, NzTheadComponent, NzTrDirective, NzTableCellDirective, NzThMeasureDirective, NzTbodyComponent, } from "ng-zorro-antd/table"; // correspond to operator type specified in backend OperatorDescriptor export const TYPE_CASTING_OPERATOR_TYPE = "TypeCasting"; @UntilDestroy() @Component({ selector: "texera-type-casting-display", templateUrl: "./type-casting-display.component.html", imports: [ NgIf, NzTableComponent, NzTheadComponent, NzTrDirective, NzTableCellDirective, NzThMeasureDirective, NzTbodyComponent, NgFor, ], }) export class TypeCastingDisplayComponent implements OnInit, OnChanges { @Input() currentOperatorId: string | undefined; schemaToDisplay: Partial[] = []; displayTypeCastingSchemaInformation: boolean = false; constructor( private workflowActionService: WorkflowActionService, private workflowCompilingService: WorkflowCompilingService ) {} ngOnInit(): void { this.registerTypeCastingPropertyChangeHandler(); this.registerInputSchemaChangeHandler(); } // invoke on first init and every time the input binding is changed ngOnChanges(): void { if (!this.currentOperatorId) { this.displayTypeCastingSchemaInformation = false; return; } const op = this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId); if (op.operatorType !== TYPE_CASTING_OPERATOR_TYPE) { this.displayTypeCastingSchemaInformation = false; return; } this.displayTypeCastingSchemaInformation = true; this.rerender(); } registerTypeCastingPropertyChangeHandler(): void { this.workflowActionService .getTexeraGraph() .getOperatorPropertyChangeStream() .pipe( filter(op => op.operator.operatorID === this.currentOperatorId), filter(op => op.operator.operatorType === TYPE_CASTING_OPERATOR_TYPE), map(event => event.operator) ) .pipe(untilDestroyed(this)) .subscribe(_ => { this.rerender(); }); } private registerInputSchemaChangeHandler() { this.workflowCompilingService .getCompilationStateInfoChangedStream() .pipe(untilDestroyed(this)) .subscribe(_ => { this.rerender(); }); } rerender(): void { if (!this.currentOperatorId) { return; } this.schemaToDisplay = []; const inputSchema = this.workflowCompilingService.getOperatorInputSchemaMap(this.currentOperatorId); const operatorPredicate = this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId); const castUnits: ReadonlyArray<{ attribute: string; resultType: AttributeType }> = operatorPredicate.operatorProperties["typeCastingUnits"] ?? []; const castTypeMap: Map = new Map(castUnits.map(unit => [unit.attribute, unit.resultType])); Object.values(inputSchema || {}).forEach(schema => schema?.forEach(attr => { if (castTypeMap.has(attr.attributeName)) { const castedAttr: Partial = { attributeName: attr.attributeName, attributeType: castTypeMap.get(attr.attributeName), }; this.schemaToDisplay.push(castedAttr); } else { this.schemaToDisplay.push(attr); } }) ); } } ================================================ FILE: frontend/src/app/workspace/component/result-exportation/result-exportation.component.html ================================================
    Export Type Filename Destination
    {{dataset.dataset.did?.toString()}}
    {{ dataset.dataset.name }}
    ================================================ FILE: frontend/src/app/workspace/component/result-exportation/result-exportation.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .centered-container { display: flex; flex-direction: column; /* Arrange children vertically */ align-items: center; /* Center horizontally */ justify-content: center; /* Center vertically */ text-align: center; } .datasets-container { background-color: white; } .dataset-id-container { background-color: grey; color: white; width: 35px; height: 35px; border-radius: 50%; display: flex; justify-content: center; align-items: center; /* Center vertically */ font-size: 14px; margin-left: 5px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); overflow: hidden; } .auto-option-content { width: 100%; height: 50%; display: flex; justify-content: space-between; } .dataset-name { margin-left: 10px; flex-grow: 1; /* This will make the name take up the remaining space */ } .dataset-option-link-btn { margin-right: 5px; } .input-wrapper { width: 100%; margin-bottom: 15px; } ================================================ FILE: frontend/src/app/workspace/component/result-exportation/result-exportation.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { Component, inject, OnInit } from "@angular/core"; import { WorkflowResultDownloadability, WorkflowResultExportService, } from "../../service/workflow-result-export/workflow-result-export.service"; import { DashboardDataset } from "../../../dashboard/type/dashboard-dataset.interface"; import { DatasetService } from "../../../dashboard/service/user/dataset/dataset.service"; import { NZ_MODAL_DATA, NzModalRef, NzModalService } from "ng-zorro-antd/modal"; import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; import { WorkflowResultService } from "../../service/workflow-result/workflow-result.service"; import { ComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; import { DashboardWorkflowComputingUnit } from "../../../common/type/workflow-computing-unit"; import { UserDatasetVersionCreatorComponent } from "../../../dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component"; import { NgIf, NgFor } from "@angular/common"; import { NzAlertComponent } from "ng-zorro-antd/alert"; import { FormsModule } from "@angular/forms"; import { NzFormDirective, NzFormItemComponent, NzFormLabelComponent, NzFormControlComponent } from "ng-zorro-antd/form"; import { NzRowDirective, NzColDirective } from "ng-zorro-antd/grid"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzSelectComponent, NzOptionComponent } from "ng-zorro-antd/select"; import { NzInputDirective } from "ng-zorro-antd/input"; import { NzAutocompleteTriggerDirective, NzAutocompleteComponent, NzAutocompleteOptionComponent, } from "ng-zorro-antd/auto-complete"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzDividerComponent } from "ng-zorro-antd/divider"; import { NzIconDirective } from "ng-zorro-antd/icon"; @UntilDestroy() @Component({ selector: "texera-result-exportation-modal", templateUrl: "./result-exportation.component.html", styleUrls: ["./result-exportation.component.scss"], imports: [ NgIf, NzAlertComponent, FormsModule, NzFormDirective, NzRowDirective, NzColDirective, NzFormItemComponent, NzFormLabelComponent, NzFormControlComponent, NzSpaceCompactItemDirective, NzSelectComponent, NzOptionComponent, NzInputDirective, NzAutocompleteTriggerDirective, NzAutocompleteComponent, NgFor, NzAutocompleteOptionComponent, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, NzDividerComponent, NzIconDirective, ], }) export class ResultExportationComponent implements OnInit { /* Two sources can trigger this dialog, one from context-menu which only export highlighted operators and second is menu which wants to export all operators */ sourceTriggered: string = inject(NZ_MODAL_DATA).sourceTriggered; workflowName: string = inject(NZ_MODAL_DATA).workflowName; inputFileName: string = inject(NZ_MODAL_DATA).defaultFileName ?? ""; rowIndex: number = inject(NZ_MODAL_DATA).rowIndex ?? -1; columnIndex: number = inject(NZ_MODAL_DATA).columnIndex ?? -1; destination: string = ""; exportType: string = inject(NZ_MODAL_DATA).exportType ?? ""; isTableOutput: boolean = false; isVisualizationOutput: boolean = false; containsBinaryData: boolean = false; inputDatasetName = ""; selectedComputingUnit: DashboardWorkflowComputingUnit | null = null; downloadability?: WorkflowResultDownloadability; userAccessibleDatasets: DashboardDataset[] = []; filteredUserAccessibleDatasets: DashboardDataset[] = []; /** * Gets the operator IDs to check for restrictions based on the source trigger. * Menu: all operators, Context menu: highlighted operators only */ private getOperatorIdsToCheck(): readonly string[] { if (this.sourceTriggered === "menu") { return this.workflowActionService .getTexeraGraph() .getAllOperators() .map(op => op.operatorID); } else { return this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs(); } } /** * Computed property: operator IDs that can be exported */ get exportableOperatorIds(): string[] { if (!this.downloadability) return []; return this.downloadability.getExportableOperatorIds(this.getOperatorIdsToCheck()); } /** * Computed property: operator IDs that are blocked from export */ get blockedOperatorIds(): string[] { if (!this.downloadability) return []; return this.downloadability.getBlockedOperatorIds(this.getOperatorIdsToCheck()); } /** * Computed property: whether all selected operators are blocked */ get isExportRestricted(): boolean { const operatorIds = this.getOperatorIdsToCheck(); return this.exportableOperatorIds.length === 0 && operatorIds.length > 0; } /** * Computed property: whether some (but not all) operators are blocked */ get hasPartialNonDownloadable(): boolean { return this.exportableOperatorIds.length > 0 && this.blockedOperatorIds.length > 0; } /** * Computed property: dataset labels that are blocking export */ get blockingDatasetLabels(): string[] { if (!this.downloadability) return []; return this.downloadability.getBlockingDatasets(this.getOperatorIdsToCheck()); } constructor( public workflowResultExportService: WorkflowResultExportService, private modalRef: NzModalRef, private modalService: NzModalService, private datasetService: DatasetService, private workflowActionService: WorkflowActionService, private workflowResultService: WorkflowResultService, private computingUnitStatusService: ComputingUnitStatusService ) {} ngOnInit(): void { this.datasetService .retrieveAccessibleDatasets() .pipe(untilDestroyed(this)) .subscribe(datasets => { this.userAccessibleDatasets = datasets.filter(dataset => dataset.accessPrivilege === "WRITE"); this.filteredUserAccessibleDatasets = [...this.userAccessibleDatasets]; }); this.workflowResultExportService .computeRestrictionAnalysis() .pipe(untilDestroyed(this)) .subscribe(downloadability => { this.downloadability = downloadability; this.updateOutputType(); }); this.computingUnitStatusService .getSelectedComputingUnit() .pipe(untilDestroyed(this)) .subscribe(unit => { this.selectedComputingUnit = unit; }); } updateOutputType(): void { if (!this.downloadability) { return; } const operatorIds = this.getOperatorIdsToCheck(); if (operatorIds.length === 0) { // No operators highlighted this.isTableOutput = false; this.isVisualizationOutput = false; this.containsBinaryData = false; return; } if (this.isExportRestricted) { this.isTableOutput = false; this.isVisualizationOutput = false; this.containsBinaryData = false; return; } // Assume they're all table or visualization until we find an operator that isn't let allTable = true; let allVisualization = true; let anyBinaryData = false; for (const operatorId of this.exportableOperatorIds) { const outputTypes = this.workflowResultService.determineOutputTypes(operatorId); if (!outputTypes.hasAnyResult) { continue; } if (!outputTypes.isTableOutput) { allTable = false; } if (!outputTypes.isVisualizationOutput) { allVisualization = false; } if (outputTypes.containsBinaryData) { anyBinaryData = true; } } this.isTableOutput = allTable; this.isVisualizationOutput = allVisualization; this.containsBinaryData = anyBinaryData; } onUserInputDatasetName(event: Event): void { const value = this.inputDatasetName; if (value) { this.filteredUserAccessibleDatasets = this.userAccessibleDatasets.filter( dataset => dataset.dataset.did && dataset.dataset.name.toLowerCase().includes(value.toLowerCase()) ); } else { this.filteredUserAccessibleDatasets = [...this.userAccessibleDatasets]; } } onClickExportResult(destination: "dataset" | "local", dataset: DashboardDataset = {} as DashboardDataset) { const datasetIds = destination === "dataset" ? [dataset.dataset.did].filter((id): id is number => id !== undefined) : []; this.workflowResultExportService.exportWorkflowExecutionResult( this.exportType, this.workflowName, datasetIds, this.rowIndex, this.columnIndex, this.inputFileName, this.sourceTriggered === "menu", destination, this.selectedComputingUnit ); this.modalRef.close(); } onClickCreateNewDataset(): void { const modal = this.modalService.create({ nzTitle: "Create New Dataset", nzContent: UserDatasetVersionCreatorComponent, nzData: { isCreatingVersion: false, }, nzFooter: null, nzWidth: 500, }); modal.afterClose.pipe(untilDestroyed(this)).subscribe((result: DashboardDataset | null) => { if (result) { this.userAccessibleDatasets.unshift(result); this.filteredUserAccessibleDatasets = [...this.userAccessibleDatasets]; this.inputDatasetName = result.dataset.name; } }); } /** * Getter that returns a comma-separated string of blocking dataset labels. * Used in the template to display which datasets are preventing export. * * @returns String like "Dataset1 (user1@example.com), Dataset2 (user2@example.com)" */ get blockingDatasetSummary(): string { return this.blockingDatasetLabels.join(", "); } } ================================================ FILE: frontend/src/app/workspace/component/result-panel/console-frame/console-frame.component.html ================================================
    • Show Timestamp
    • Show Source
    {{ entry.title }}
    {{ entry.message }}
    {{ entry.title }}
    {{ entry.source }}
    {{ (entry.timestamp.seconds * 1000 + entry.timestamp.nanos * 0.000001) | date : "M-d-yy, HH:mm:ss.SSS" }} {{ workerIdToAbbr(entry.workerId) }}
    ================================================ FILE: frontend/src/app/workspace/component/result-panel/console-frame/console-frame.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ :host { display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 0; -webkit-user-select: text; user-select: text; } .error-message { font-family: monaco, serif; font-size: 11px; font-style: normal; font-variant: normal; font-weight: 200; line-height: 14px; color: red; } .console-list-container { flex: 1 1 auto; min-height: 0; width: 100%; border: 1px solid #e8e8e8; border-radius: 2px; overflow: auto; -webkit-user-select: text; user-select: text; .console-message-entry { //border: purple solid; width: 100%; ::ng-deep .collapse-remove-padding .ant-collapse-header { padding: 0 !important; } .collapse-message-header { width: 100%; font-family: monaco, serif; font-size: 11px; font-style: normal; font-variant: normal; font-weight: bold; line-height: 14px; white-space: pre-line; color: red; -webkit-user-select: text; user-select: text; //border: solid black; } .console-message { width: 100%; font-family: monaco, serif; font-size: 11px; font-style: normal; font-variant: normal; font-weight: 200; line-height: 14px; white-space: pre-line; -webkit-user-select: text; user-select: text; cursor: text; //border: solid black; } .worker-tag { //width: 100%; font-family: monaco, serif; font-size: 8px; font-style: normal; font-variant: normal; //border: solid red; text-align: end; } .timestamp-tag { //width: 100%; font-family: monaco, serif; font-size: 8px; font-style: normal; font-variant: normal; //border: solid green; text-align: end; } .source-tag { //width: 100%; font-family: monaco, serif; font-size: 8px; font-style: normal; font-variant: normal; //border: solid #0e5df0; text-align: end; } } } .console-input-container { flex: 0 0 auto; width: 100%; margin-top: 8px; } ================================================ FILE: frontend/src/app/workspace/component/result-panel/console-frame/console-frame.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ConsoleFrameComponent } from "./console-frame.component"; import { OperatorMetadataService } from "../../../service/operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../../../service/operator-metadata/stub-operator-metadata.service"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { NzDropDownModule } from "ng-zorro-antd/dropdown"; import { ComputingUnitStatusService } from "../../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; import { MockComputingUnitStatusService } from "../../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service"; import { commonTestProviders } from "../../../../common/testing/test-utils"; describe("ConsoleFrameComponent", () => { let component: ConsoleFrameComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ConsoleFrameComponent, HttpClientTestingModule, NzDropDownModule], providers: [ { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService, }, ...commonTestProviders, ], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(ConsoleFrameComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/workspace/component/result-panel/console-frame/console-frame.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, ElementRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from "@angular/core"; import { ExecuteWorkflowService } from "../../../service/execute-workflow/execute-workflow.service"; import { ConsoleMessage } from "../../../types/workflow-common.interface"; import { ExecutionState } from "src/app/workspace/types/execute-workflow.interface"; import { WorkflowConsoleService } from "../../../service/workflow-console/workflow-console.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { CdkVirtualScrollViewport } from "@angular/cdk/scrolling"; import { presetPalettes } from "@ant-design/colors"; import { isDefined } from "../../../../common/util/predicate"; import { WorkflowWebsocketService } from "../../../service/workflow-websocket/workflow-websocket.service"; import { NotificationService } from "../../../../common/service/notification/notification.service"; import { UdfDebugService } from "../../../service/operator-debug/udf-debug.service"; import { NzDropdownADirective, NzDropdownDirective, NzDropdownMenuComponent } from "ng-zorro-antd/dropdown"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzMenuDirective, NzMenuItemComponent } from "ng-zorro-antd/menu"; import { NzSwitchComponent } from "ng-zorro-antd/switch"; import { FormsModule } from "@angular/forms"; import { NzListComponent, NzListItemComponent } from "ng-zorro-antd/list"; import { NgFor, NgIf, DatePipe } from "@angular/common"; import { NzRowDirective, NzColDirective } from "ng-zorro-antd/grid"; import { NzBadgeComponent } from "ng-zorro-antd/badge"; import { NzCollapseComponent, NzCollapsePanelComponent } from "ng-zorro-antd/collapse"; import { NzTagComponent } from "ng-zorro-antd/tag"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzInputGroupComponent, NzInputDirective } from "ng-zorro-antd/input"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzSelectComponent, NzOptionComponent } from "ng-zorro-antd/select"; @UntilDestroy() @Component({ selector: "texera-console-frame", templateUrl: "./console-frame.component.html", styleUrls: ["./console-frame.component.scss"], imports: [ NzDropdownADirective, NzDropdownDirective, ɵNzTransitionPatchDirective, NzIconDirective, NzDropdownMenuComponent, NzMenuDirective, NzMenuItemComponent, NzSwitchComponent, FormsModule, NzListComponent, NgFor, NzListItemComponent, NzRowDirective, NzColDirective, NzBadgeComponent, NgIf, NzCollapseComponent, NzCollapsePanelComponent, NzTagComponent, NzSpaceCompactItemDirective, NzInputGroupComponent, NzButtonComponent, NzWaveDirective, NzTooltipDirective, NzInputDirective, NzSelectComponent, NzOptionComponent, DatePipe, ], }) export class ConsoleFrameComponent implements OnInit, OnChanges { @Input() operatorId!: string; @Input() consoleInputEnabled?: boolean; @ViewChild(CdkVirtualScrollViewport) viewPort?: CdkVirtualScrollViewport; @ViewChild("consoleList", { read: ElementRef }) listElement?: ElementRef; // display print consoleMessages: ReadonlyArray = []; // Configuration Menu items // TODO: move Configuration Menu to a separate component showTimestamp: boolean = true; showSource: boolean = true; // WorkerId Menu related items. ALL_WORKERS: string = "All Workers"; workerIds: readonly string[] = []; command: string = ""; targetWorker: string = this.ALL_WORKERS; labelMapping = new Map([ ["PRINT", "default"], ["COMMAND", "processing"], ["DEBUGGER", "warning"], ["ERROR", "error"], ]); constructor( private executeWorkflowService: ExecuteWorkflowService, private workflowConsoleService: WorkflowConsoleService, private workflowWebsocketService: WorkflowWebsocketService, private notificationService: NotificationService, private udfDebugService: UdfDebugService ) {} ngOnChanges(changes: SimpleChanges): void { this.operatorId = changes.operatorId?.currentValue; this.renderConsole(); } ngOnInit(): void { // make sure the console is re-rendered upon state changes this.registerAutoConsoleRerender(); } registerAutoConsoleRerender(): void { this.executeWorkflowService .getExecutionStateStream() .pipe(untilDestroyed(this)) .subscribe(event => { if (event.previous.state === ExecutionState.Initializing && event.current.state === ExecutionState.Running) { // clear the console for the next execution this.clearConsole(); } else { // re-render the console, this may update the console with error messages or console messages this.renderConsole(); } }); this.workflowConsoleService .getConsoleMessageUpdateStream() .pipe(untilDestroyed(this)) .subscribe(_ => this.renderConsole()); } clearConsole(): void { this.consoleMessages = []; } renderConsole(): void { if (this.operatorId) { this.workerIds = this.executeWorkflowService.getWorkerIds(this.operatorId); // always display console messages this.displayConsoleMessages(this.operatorId); } } displayConsoleMessages(operatorId: string): void { this.consoleMessages = operatorId ? this.workflowConsoleService.getConsoleMessages(operatorId) || [] : []; setTimeout(() => { if (this.listElement) { this.listElement.nativeElement.scrollTop = this.listElement.nativeElement.scrollHeight; } }, 0); } submitDebugCommand(): void { if (!isDefined(this.operatorId)) { return; } let workers = []; if (this.targetWorker === this.ALL_WORKERS) { workers = [...this.workerIds]; } else { workers.push(this.targetWorker); } for (let worker of workers) { this.workflowWebsocketService.send("DebugCommandRequest", { operatorId: this.operatorId, workerId: worker, cmd: this.command, }); } this.command = ""; } workerIdToAbbr(workerId: string): string { return "W" + this.getWorkerIndex(workerId); } getWorkerColor(workerIndex: number): string { const presetPalettesSize = Object.keys(presetPalettes).length; // exclude red (index 0) and volcano (index 1) as they look as warning/error. // use *3 to diff colors between adjacent workers. const colorKey = Object.keys(presetPalettes)[2 + ((workerIndex * 3) % (presetPalettesSize - 2))]; // use shade index >=6 as they are dark enough. return presetPalettes[colorKey][ 6 + ((Math.floor(workerIndex / presetPalettesSize) * 3) % (presetPalettes[colorKey].length - 6)) ]; } getWorkerIndex(workerId: string): number { const tokens = workerId.split("-"); return parseInt(tokens.at(tokens.length - 1) || "0"); } onClickSkipTuples(): void { try { this.executeWorkflowService.skipTuples(this.workerIds); } catch (e) { this.notificationService.error((e as Error).message); } } onClickRetryTuples() { try { this.executeWorkflowService.retryExecution(this.workerIds); } catch (e) { this.notificationService.error((e as Error).message); } } onClickStep(): void { for (let worker of this.workerIds) { this.udfDebugService.doStep(this.operatorId, worker); } } onClickContinue(): void { for (let worker of this.workerIds) { this.udfDebugService.doContinue(this.operatorId, worker); } } getMessageLabel(message: ConsoleMessage): string { return this.labelMapping.get(message.msgType.name) ?? ""; } } ================================================ FILE: frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.html ================================================
    Showing errors for all operators:

    No error to display.

    {{category.key}}:

    {{ error.details }}

    ================================================ FILE: frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ :host { width: 100%; } .all-errors-notification { color: blue; } .goto-operator-icon { color: blueviolet; /* change color */ font-size: 15px; /* change size */ border-width: 2px; border-style: solid; border-color: blueviolet; } .error-message { font-family: monaco, serif; font-size: 11px; font-style: normal; font-variant: normal; font-weight: 200; line-height: 14px; color: red; overflow-x: auto; white-space: pre-line; margin-left: 30px; user-select: text; } .error-category { font-weight: bold; } ================================================ FILE: frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ErrorFrameComponent } from "./error-frame.component"; import { OperatorMetadataService } from "../../../service/operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../../../service/operator-metadata/stub-operator-metadata.service"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { NzDropDownModule } from "ng-zorro-antd/dropdown"; import { ComputingUnitStatusService } from "../../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; import { MockComputingUnitStatusService } from "../../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service"; import { commonTestProviders } from "../../../../common/testing/test-utils"; describe("ErrorFrameComponent", () => { let component: ErrorFrameComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ErrorFrameComponent, HttpClientTestingModule, NzDropDownModule], providers: [ { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService, }, ...commonTestProviders, ], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(ErrorFrameComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, Input, OnInit } from "@angular/core"; import { ExecuteWorkflowService } from "../../../service/execute-workflow/execute-workflow.service"; import { UntilDestroy } from "@ngneat/until-destroy"; import { WorkflowFatalError } from "../../../types/workflow-websocket.interface"; import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; import { WorkflowCompilingService } from "../../../service/compile-workflow/workflow-compiling.service"; import { NgIf, NgFor, KeyValuePipe } from "@angular/common"; import { NzCollapseComponent, NzCollapsePanelComponent } from "ng-zorro-antd/collapse"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; @UntilDestroy() @Component({ selector: "texera-error-frame", templateUrl: "./error-frame.component.html", styleUrls: ["./error-frame.component.scss"], imports: [ NgIf, NgFor, NzCollapseComponent, NzCollapsePanelComponent, ɵNzTransitionPatchDirective, NzIconDirective, KeyValuePipe, ], }) export class ErrorFrameComponent implements OnInit { @Input() operatorId?: string; // display error message: categoryToErrorMapping: ReadonlyMap> = new Map(); constructor( private executeWorkflowService: ExecuteWorkflowService, private workflowActionService: WorkflowActionService, private workflowCompilingService: WorkflowCompilingService ) {} ngOnInit(): void { this.renderError(); } onClickGotoButton(target: string) { this.workflowActionService.highlightOperators(false, target); } renderError(): void { // first fetch the error messages from the execution state store let errorMessages = this.executeWorkflowService.getErrorMessages(); const compilationErrorMap = this.workflowCompilingService.getWorkflowCompilationErrors(); // then fetch error from the compilation state store errorMessages = errorMessages.concat(Object.values(compilationErrorMap)); if (this.operatorId) { errorMessages = errorMessages.filter(err => err.operatorId === this.operatorId); } this.categoryToErrorMapping = errorMessages.reduce((acc, obj) => { const key = obj.type.name; if (!acc.has(key)) { acc.set(key, []); } acc.get(key)!.push(obj); return acc; }, new Map()); } } ================================================ FILE: frontend/src/app/workspace/component/result-panel/result-panel-modal.component.html ================================================ ================================================ FILE: frontend/src/app/workspace/component/result-panel/result-panel-modal.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, inject, OnChanges } from "@angular/core"; import { NZ_MODAL_DATA, NzModalRef } from "ng-zorro-antd/modal"; import { WorkflowResultService } from "../../service/workflow-result/workflow-result.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { PanelResizeService } from "../../service/workflow-result/panel-resize/panel-resize.service"; import { NgxJsonViewerModule } from "ngx-json-viewer"; /** * * The pop-up window that will be * displayed when the user clicks on a specific row * to show the displays of that row. * * User can exit the pop-up window by * 1. Clicking the dismiss button on the top-right hand corner * of the Modal * 2. Clicking the `Close` button at the bottom-right * 3. Clicking any shaded area that is not the pop-up window * 4. Pressing `Esc` button on the keyboard */ @UntilDestroy() @Component({ selector: "texera-row-modal-content", templateUrl: "./result-panel-modal.component.html", styleUrls: ["./result-panel-model.component.scss"], imports: [NgxJsonViewerModule], }) export class RowModalComponent implements OnChanges { // Index of current displayed row in currentResult readonly operatorId: string = inject(NZ_MODAL_DATA).operatorId; rowIndex: number = inject(NZ_MODAL_DATA).rowIndex; currentDisplayRowData: Record = {}; constructor( public modal: NzModalRef, private workflowResultService: WorkflowResultService, private resizeService: PanelResizeService ) { this.ngOnChanges(); } ngOnChanges(): void { this.workflowResultService .getPaginatedResultService(this.operatorId) ?.selectTuple(this.rowIndex, this.resizeService.pageSize) .pipe(untilDestroyed(this)) .subscribe(res => { this.currentDisplayRowData = res.tuple; }); } } ================================================ FILE: frontend/src/app/workspace/component/result-panel/result-panel-model.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .modal-body.content-body { overflow-x: auto; overflow-y: auto; height: 100%; width: 100%; } ================================================ FILE: frontend/src/app/workspace/component/result-panel/result-panel.component.html ================================================

    Result Panel{{operatorTitle ? ': ' + operatorTitle : ''}}

    No results available to display.

    ================================================ FILE: frontend/src/app/workspace/component/result-panel/result-panel.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ :host { display: block; height: 100%; width: 100%; position: fixed; z-index: 4; } #texera-workspace { position: relative; } #result-container { position: absolute; top: -300px; left: 0; background: white; border-radius: 5px; box-shadow: 0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f; } // make modal body scrollable and modal size fixed .modal-body { min-height: calc(50vh); overflow-y: auto !important; } // change the internal color of pretty json of the result details $type-colors: ( string: black, number: #27837a, boolean: #751866, date: #10536d, array: #999, object: #999, function: #999, "null": #fff, undefined: #fff, ); #content { overflow-y: auto; padding-top: 38px; width: inherit; height: inherit; } .result-content, :host ::ng-deep .result-content .ant-tabs-content, :host ::ng-deep .result-content .ant-tabs-tabpane { height: 100%; } #result-buttons { position: absolute; bottom: 0; left: 0; z-index: 4; display: flex; } #panel-button { position: fixed; top: 0; right: 0; z-index: 4; display: flex; } .ant-menu-item { margin: 0 !important; height: 32px; line-height: 32px; padding: 0 9px; } #divider { margin: 0; } .shadow { border-radius: 5px; box-shadow: 0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f; } #title { padding: 5px 9px; border-bottom: 1px solid #e0e0e0; position: absolute; top: 0; background: white; width: 100%; z-index: 2; } ================================================ FILE: frontend/src/app/workspace/component/result-panel/result-panel.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ResultPanelComponent } from "./result-panel.component"; import { ExecuteWorkflowService } from "../../service/execute-workflow/execute-workflow.service"; import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; import { OperatorMetadataService } from "../../service/operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../../service/operator-metadata/stub-operator-metadata.service"; import { By } from "@angular/platform-browser"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { NzModalModule } from "ng-zorro-antd/modal"; import { ExecutionState } from "../../types/execute-workflow.interface"; import { mockPoint, mockResultPredicate } from "../../service/workflow-graph/model/mock-workflow-data"; import { ComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; import { MockComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service"; import { commonTestProviders } from "../../../common/testing/test-utils"; describe("ResultPanelComponent", () => { let component: ResultPanelComponent; let fixture: ComponentFixture; let executeWorkflowService: ExecuteWorkflowService; let workflowActionService: WorkflowActionService; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ResultPanelComponent, HttpClientTestingModule, NzModalModule], providers: [ WorkflowActionService, ExecuteWorkflowService, { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService }, ...commonTestProviders, ], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(ResultPanelComponent); component = fixture.componentInstance; executeWorkflowService = TestBed.inject(ExecuteWorkflowService); workflowActionService = TestBed.inject(WorkflowActionService); fixture.detectChanges(); }); it("should create", () => expect(component).toBeTruthy()); it("should show nothing by default", () => { expect(component.frameComponentConfigs.size).toBe(0); }); it("should show the result panel if a workflow finishes execution", () => { workflowActionService.addOperator(mockResultPredicate, mockPoint); executeWorkflowService["updateExecutionState"]({ state: ExecutionState.Running, }); executeWorkflowService["updateExecutionState"]({ state: ExecutionState.Completed, }); fixture.detectChanges(); const resultPanelDiv = fixture.debugElement.query(By.css("#result-container")); const resultPanelHtmlElement: HTMLElement = resultPanelDiv.nativeElement; expect(resultPanelHtmlElement).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/workspace/component/result-panel/result-panel.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ChangeDetectorRef, Component, ElementRef, HostListener, OnDestroy, OnInit, Type, ViewChild, } from "@angular/core"; import { merge } from "rxjs"; import { ExecuteWorkflowService } from "../../service/execute-workflow/execute-workflow.service"; import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; import { ExecutionState, ExecutionStateInfo } from "../../types/execute-workflow.interface"; import { ResultTableFrameComponent } from "./result-table-frame/result-table-frame.component"; import { ConsoleFrameComponent } from "./console-frame/console-frame.component"; import { WorkflowResultService } from "../../service/workflow-result/workflow-result.service"; import { PanelResizeService } from "../../service/workflow-result/panel-resize/panel-resize.service"; import { filter } from "rxjs/operators"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { isPythonUdf, isSink } from "../../service/workflow-graph/model/workflow-graph"; import { WorkflowVersionService } from "../../../dashboard/service/user/workflow-version/workflow-version.service"; import { ErrorFrameComponent } from "./error-frame/error-frame.component"; import { WorkflowConsoleService } from "../../service/workflow-console/workflow-console.service"; import { NzResizeEvent, NzResizableDirective, NzResizeHandlesComponent } from "ng-zorro-antd/resizable"; import { VisualizationFrameContentComponent } from "../visualization-panel-content/visualization-frame-content.component"; import { calculateTotalTranslate3d } from "../../../common/util/panel-dock"; import { isDefined } from "../../../common/util/predicate"; import { CdkDragEnd, CdkDrag, CdkDragHandle } from "@angular/cdk/drag-drop"; import { PanelService } from "../../service/panel/panel.service"; import { WorkflowCompilingService } from "../../service/compile-workflow/workflow-compiling.service"; import { CompilationState } from "../../types/workflow-compiling.interface"; import { WorkflowFatalError } from "../../types/workflow-websocket.interface"; import { NzMenuDirective, NzMenuItemComponent, NzMenuDividerDirective } from "ng-zorro-antd/menu"; import { NgClass, NgIf, NgFor, NgComponentOutlet, KeyValuePipe } from "@angular/common"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzTooltipDirective } from "ng-zorro-antd/tooltip"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzTabsComponent, NzTabComponent } from "ng-zorro-antd/tabs"; import { FormlyRepeatDndComponent } from "../../../common/formly/repeat-dnd/repeat-dnd.component"; export const DEFAULT_WIDTH = 800; export const DEFAULT_HEIGHT = 500; /** * ResultPanelComponent is the bottom level area that displays the * execution result of a workflow after the execution finishes. */ @UntilDestroy() @Component({ selector: "texera-result-panel", templateUrl: "./result-panel.component.html", styleUrls: ["./result-panel.component.scss"], imports: [ NzMenuDirective, NgClass, NgIf, NzMenuItemComponent, ɵNzTransitionPatchDirective, NzTooltipDirective, NzIconDirective, NzMenuDividerDirective, CdkDrag, NzResizableDirective, NzSpaceCompactItemDirective, NzButtonComponent, CdkDragHandle, NzTabsComponent, NzTabComponent, NgFor, NgComponentOutlet, NzResizeHandlesComponent, FormlyRepeatDndComponent, KeyValuePipe, ], }) export class ResultPanelComponent implements OnInit, OnDestroy { @ViewChild("dynamicComponent") componentOutlets!: ElementRef; frameComponentConfigs: Map; componentInputs: {} }> = new Map(); protected readonly window = window; id = -1; width = DEFAULT_WIDTH; height = DEFAULT_HEIGHT; operatorTitle = ""; dragPosition = { x: 0, y: 0 }; returnPosition = { x: 0, y: 0 }; // the highlighted operator ID for display result table / visualization / breakpoint currentOperatorId?: string | undefined; previewWorkflowVersion: boolean = false; constructor( private executeWorkflowService: ExecuteWorkflowService, private workflowActionService: WorkflowActionService, private workflowCompilingService: WorkflowCompilingService, private workflowResultService: WorkflowResultService, private workflowVersionService: WorkflowVersionService, private changeDetectorRef: ChangeDetectorRef, private workflowConsoleService: WorkflowConsoleService, private resizeService: PanelResizeService, private panelService: PanelService ) { this.width = 0; this.height = Number(localStorage.getItem("result-panel-height")) || this.height; this.resizeService.changePanelSize(this.width, this.height); } ngOnInit(): void { const style = localStorage.getItem("result-panel-style"); if (style) document.getElementById("result-container")!.style.cssText = style; const translates = document.getElementById("result-container")!.style.transform; const [xOffset, yOffset, _] = calculateTotalTranslate3d(translates); this.returnPosition = { x: -xOffset, y: -yOffset }; this.updateReturnPosition(DEFAULT_HEIGHT, this.height); this.registerAutoRerenderResultPanel(); this.registerAutoOpenResultPanel(); this.handleResultPanelForVersionPreview(); this.panelService.closePanelStream.pipe(untilDestroyed(this)).subscribe(() => this.closePanel()); this.panelService.resetPanelStream.pipe(untilDestroyed(this)).subscribe(() => { this.resetPanelPosition(); this.openPanel(); }); this.workflowActionService.resultPanelOpen$.pipe(untilDestroyed(this)).subscribe(open => { if (open) { this.openPanel(); } else { this.closePanel(); } }); } @HostListener("window:beforeunload") ngOnDestroy(): void { localStorage.setItem("result-panel-width", String(this.width)); localStorage.setItem("result-panel-height", String(this.height)); const resultContainer = document.getElementById("result-container"); if (resultContainer) { localStorage.setItem("result-panel-style", resultContainer.style.cssText); } } handleResultPanelForVersionPreview() { this.workflowVersionService .getDisplayParticularVersionStream() .pipe(untilDestroyed(this)) .subscribe(displayVersionFlag => { this.previewWorkflowVersion = displayVersionFlag; }); } registerAutoOpenResultPanel() { this.executeWorkflowService .getExecutionStateStream() .pipe(untilDestroyed(this)) .subscribe(event => { const currentlyHighlighted = this.workflowActionService .getJointGraphWrapper() .getCurrentHighlightedOperatorIDs(); // display panel when execution is completed and highlight sink to show results // condition must be (Running -> Completed) to prevent cases like // (Uninitialized -> Completed) (a completed workflow is reloaded) if (event.previous.state === ExecutionState.Running && event.current.state === ExecutionState.Completed) { const activeSinkOperators = this.workflowActionService .getTexeraGraph() .getAllOperators() .filter(op => isSink(op)) .filter(op => !op.isDisabled) .map(op => op.operatorID); if (activeSinkOperators.length > 0) { if (!(currentlyHighlighted.length == 1 && activeSinkOperators.includes(currentlyHighlighted[0]))) { this.workflowActionService.getJointGraphWrapper().unhighlightOperators(...currentlyHighlighted); this.workflowActionService.getJointGraphWrapper().highlightOperators(activeSinkOperators[0]); } } } // display panel and highlight a python UDF operator when workflow starts running if (event.current.state === ExecutionState.Running) { const activePythonUDFOperators = this.workflowActionService .getTexeraGraph() .getAllOperators() .filter(op => isPythonUdf(op)) .filter(op => !op.isDisabled) .map(op => op.operatorID); if (activePythonUDFOperators.length > 0) { if (!(currentlyHighlighted.length == 1 && activePythonUDFOperators.includes(activePythonUDFOperators[0]))) { this.workflowActionService.getJointGraphWrapper().unhighlightOperators(...currentlyHighlighted); this.workflowActionService.getJointGraphWrapper().highlightOperators(activePythonUDFOperators[0]); } } } }); } registerAutoRerenderResultPanel() { merge( this.executeWorkflowService .getExecutionStateStream() .pipe(filter(event => ResultPanelComponent.needRerenderOnStateChange(event))), this.workflowCompilingService.getCompilationStateInfoChangedStream(), this.workflowActionService.getJointGraphWrapper().getJointOperatorHighlightStream(), this.workflowActionService.getJointGraphWrapper().getJointOperatorUnhighlightStream(), this.workflowResultService.getResultInitiateStream() ) .pipe(untilDestroyed(this)) .subscribe(_ => { this.rerenderResultPanel(); this.changeDetectorRef.detectChanges(); this.registerOperatorDisplayNameChangeHandler(); }); } rerenderResultPanel(): void { // if the workflow on the paper is a version preview then this is a temporary workaround until a future PR // TODO: let the results be tied with an execution ID instead of a workflow ID if (this.previewWorkflowVersion) { return; } // update highlighted operator const highlightedOperators = this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs(); const currentHighlightedOperator = highlightedOperators.length === 1 ? highlightedOperators[0] : undefined; if (this.currentOperatorId !== currentHighlightedOperator) { // clear everything, prepare for state change this.clearResultPanel(); this.currentOperatorId = currentHighlightedOperator; if (!this.currentOperatorId) { this.operatorTitle = ""; } } if ( this.executeWorkflowService.getExecutionState().state === ExecutionState.Failed || this.workflowCompilingService.getWorkflowCompilationState() === CompilationState.Failed ) { if (this.currentOperatorId == null) { this.displayError(this.currentOperatorId); } else { const errorMessages = this.getWorkflowFatalErrors(this.currentOperatorId); if (errorMessages.length > 0) { this.displayError(this.currentOperatorId); } else { this.frameComponentConfigs.delete("Static Error"); } } } else { this.frameComponentConfigs.delete("Static Error"); } if (this.currentOperatorId) { this.displayResult(this.currentOperatorId); const operator = this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId); if (this.workflowConsoleService.hasConsoleMessages(this.currentOperatorId) || isPythonUdf(operator)) { this.displayConsole(this.currentOperatorId, isPythonUdf(operator)); } } } clearResultPanel(): void { this.frameComponentConfigs.clear(); } displayConsole(operatorId: string, consoleInputEnabled: boolean) { this.frameComponentConfigs.set("Console", { component: ConsoleFrameComponent, componentInputs: { operatorId, consoleInputEnabled }, }); } displayError(operatorId: string | undefined) { this.frameComponentConfigs.set("Static Error", { component: ErrorFrameComponent, componentInputs: { operatorId }, }); } displayResult(operatorId: string) { const resultService = this.workflowResultService.getResultService(operatorId); const paginatedResultService = this.workflowResultService.getPaginatedResultService(operatorId); if (paginatedResultService) { // display table result if it has paginated results this.frameComponentConfigs.set("Result", { component: ResultTableFrameComponent, componentInputs: { operatorId }, }); } else if (resultService) { // display visualization result this.frameComponentConfigs.set("Result", { component: VisualizationFrameContentComponent, componentInputs: { operatorId }, }); } } private getWorkflowFatalErrors(operatorId?: string): readonly WorkflowFatalError[] { // first fetch the error messages from the execution state store let errorMessages = this.executeWorkflowService.getErrorMessages(); // then fetch error from the compilation state store errorMessages = errorMessages.concat(Object.values(this.workflowCompilingService.getWorkflowCompilationErrors())); // finally, if any operatorId is given, filter out those with matched Id if (operatorId) { errorMessages = errorMessages.filter(err => err.operatorId === operatorId); } return errorMessages; } private registerOperatorDisplayNameChangeHandler(): void { if (this.currentOperatorId) { const operator = this.workflowActionService.getTexeraGraph().getOperator(this.currentOperatorId); this.operatorTitle = operator.customDisplayName ?? ""; this.workflowActionService .getTexeraGraph() .getOperatorDisplayNameChangedStream() .pipe(untilDestroyed(this)) .subscribe(({ operatorID, newDisplayName }) => { if (operatorID === this.currentOperatorId) { this.operatorTitle = newDisplayName; } }); } } private static needRerenderOnStateChange(event: { previous: ExecutionStateInfo; current: ExecutionStateInfo; }): boolean { // transitioning from any state to failed state if (event.current.state === ExecutionState.Failed) { return true; } // force refresh after fixing all editing-time errors if (event.previous.state === ExecutionState.Failed) { return true; } // transition from uninitialized / completed to anything else indicates a new execution of the workflow return event.previous.state === ExecutionState.Uninitialized || event.previous.state === ExecutionState.Completed; } openPanel() { this.height = DEFAULT_HEIGHT; this.width = DEFAULT_WIDTH; this.resizeService.changePanelSize(this.width, this.height); } closePanel() { this.height = 32.5; this.width = 0; } resetPanelPosition() { this.dragPosition = { x: this.returnPosition.x, y: this.returnPosition.y }; } isPanelDocked() { return this.returnPosition.x === this.dragPosition.x && this.returnPosition.y === this.dragPosition.y; } handleStartDrag() { let visualizationResult = this.componentOutlets.nativeElement.querySelector("#html-content"); if (visualizationResult !== null) { visualizationResult.style.zIndex = -1; } } handleEndDrag({ source }: CdkDragEnd) { /** * records the most recent panel location, updating dragPosition when dragging is over */ const { x, y } = source.getFreeDragPosition(); this.dragPosition = { x: x, y: y }; let visualizationResult = this.componentOutlets.nativeElement.querySelector("#html-content"); if (visualizationResult !== null) { visualizationResult.style.zIndex = 0; } } onResize({ width, height }: NzResizeEvent) { cancelAnimationFrame(this.id); this.updateReturnPosition(this.height, height); this.id = requestAnimationFrame(() => { this.width = width!; this.height = height!; this.resizeService.changePanelSize(this.width, this.height); }); } updateReturnPosition(prevHeight: number, newHeight: number | undefined) { /** * Updating returnPosition ensures that even if the panel gets resized,it can be docked correctly to the left-bottom corner of the canvas. */ if (!isDefined(newHeight)) return; this.returnPosition = { x: this.returnPosition.x, y: this.returnPosition.y + prevHeight - newHeight }; } } ================================================ FILE: frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.html ================================================

    Empty result set

    {{ column.header }}
    Min
    Max
    Non-Null Count
    {{tableStats[column.header]['firstCat']}} (approximate)
    %
    {{tableStats[column.header]['secondCat']}} (approximate)
    %
    Other (approximate)
    %
    {{ column.getCell(row) }}
    ================================================ FILE: frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ :host ::ng-deep .ant-table-pagination.ant-pagination { margin: 16px 0; } // make the pagination button to the left :host ::ng-deep .ant-table-pagination-right { justify-content: left; } td.data-size { font-size: 12px; } th.header-size { font-size: 14px; padding: 2px; } .custom-stats-row th { background-color: #ccc; border-bottom: 1px solid #ccc; border-top: 1px solid #ccc; border-left: 1px solid #ccc; height: auto; padding: 2px 4px; } .custom-stats-row th:first-child { border-left: none; /* To remove the left border from the first column */ } .statsHeader { max-height: none; padding: 2px 0; } .statsLine { display: flex; justify-content: space-between; align-items: center; width: 100%; gap: 16px; } .statsLine h5 { margin: 0; line-height: 1.2; font-size: 12px; } .leftAlign { text-align: left; margin-right: 10px; } .rightAlign { text-align: right; } .borderless-button { border: none; background: none; padding: 0; margin: 0; cursor: pointer; &:hover { background-color: rgba(0, 0, 0, 0.05); } } .table-cell { position: relative; padding-right: 28px; } .cell-content { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .download-button { position: absolute; right: 4px; top: 50%; transform: translateY(-50%); opacity: 0; transition: opacity 0.2s ease-in-out; padding: 4px; background: transparent; border: none; cursor: pointer; i { font-size: 16px; // Slightly larger to match the cloud icon color: #1890ff; // Ant Design's primary blue color } } .table-row-hover:hover { .download-button { opacity: 0.7; &:hover { opacity: 1; } } } .table-footer { display: flex; justify-content: flex-end; margin-top: 16px; } .table-container { position: relative; } .download-all-button { position: absolute; bottom: 16px; right: 0; } ================================================ FILE: frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ResultTableFrameComponent } from "./result-table-frame.component"; import { OperatorMetadataService } from "../../../service/operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../../../service/operator-metadata/stub-operator-metadata.service"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { NzModalModule } from "ng-zorro-antd/modal"; import { NzTableModule } from "ng-zorro-antd/table"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; import { commonTestProviders } from "../../../../common/testing/test-utils"; import { GuiConfigService } from "../../../../common/service/gui-config.service"; describe("ResultTableFrameComponent", () => { let component: ResultTableFrameComponent; let fixture: ComponentFixture; const GUI_CONFIG_LIMIT = 15; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ResultTableFrameComponent, HttpClientTestingModule, NzModalModule, NzTableModule, NoopAnimationsModule], providers: [ { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, { provide: GuiConfigService, useValue: { env: { limitColumns: GUI_CONFIG_LIMIT, }, }, }, ...commonTestProviders, ], }).compileComponents(); fixture = TestBed.createComponent(ResultTableFrameComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); it("currentResult should not be modified if setupResultTable is called with empty (zero-length) execution result", () => { component.currentResult = [{ test: "property" }]; (component as any).setupResultTable([], 0); expect(component.currentResult).toEqual([{ test: "property" }]); }); it("should set columnLimit from gui-config", () => { expect(component.columnLimit).toEqual(GUI_CONFIG_LIMIT); }); }); ================================================ FILE: frontend/src/app/workspace/component/result-panel/result-table-frame/result-table-frame.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from "@angular/core"; import { NzModalRef, NzModalService } from "ng-zorro-antd/modal"; import { NzTableQueryParams, NzTableComponent, NzTheadComponent, NzTrDirective, NzTableCellDirective, NzThMeasureDirective, NzTbodyComponent, NzCellEllipsisDirective, } from "ng-zorro-antd/table"; import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; import { WorkflowResultService } from "../../../service/workflow-result/workflow-result.service"; import { PanelResizeService } from "../../../service/workflow-result/panel-resize/panel-resize.service"; import { isWebPaginationUpdate, OperatorState } from "../../../types/execute-workflow.interface"; import { IndexableObject, TableColumn } from "../../../types/result-table.interface"; import { RowModalComponent } from "../result-panel-modal.component"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { DomSanitizer, SafeHtml } from "@angular/platform-browser"; import { ResultExportationComponent } from "../../result-exportation/result-exportation.component"; import { WorkflowStatusService } from "../../../service/workflow-status/workflow-status.service"; import { GuiConfigService } from "../../../../common/service/gui-config.service"; import { NgIf, NgFor, NgClass } from "@angular/common"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzInputDirective } from "ng-zorro-antd/input"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; /** * The Component will display the result in an excel table format, * where each row represents a result from the workflow, * and each column represents the type of result the workflow returns. * * Clicking each row of the result table will create an pop-up window * and display the detail of that row in a pretty json format. */ @UntilDestroy() @Component({ selector: "texera-result-table-frame", templateUrl: "./result-table-frame.component.html", styleUrls: ["./result-table-frame.component.scss"], imports: [ NgIf, NzSpaceCompactItemDirective, NzInputDirective, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, NzIconDirective, NzTableComponent, NzTheadComponent, NzTrDirective, NgFor, NzTableCellDirective, NzThMeasureDirective, NgClass, NzTbodyComponent, NzCellEllipsisDirective, ], }) export class ResultTableFrameComponent implements OnInit, OnChanges { @Input() operatorId?: string; // display result table currentColumns?: TableColumn[]; currentResult: IndexableObject[] = []; // for more details // see https://ng.ant.design/components/table/en#components-table-demo-ajax isFrontPagination: boolean = true; isLoadingResult: boolean = false; // paginator section, used when displaying rows // this attribute stores whether front-end should handle pagination // if false, it means the pagination is managed by the server // this starts from **ONE**, not zero currentPageIndex: number = 1; totalNumTuples: number = 0; pageSize = 5; currentColumnOffset = 0; columnLimit = 25; columnSearch = ""; panelHeight = 0; tableStats: Record> = {}; prevTableStats: Record> = {}; widthPercent: string = ""; isOperatorFinished: boolean = false; constructor( private modalService: NzModalService, private workflowActionService: WorkflowActionService, private workflowResultService: WorkflowResultService, private resizeService: PanelResizeService, private changeDetectorRef: ChangeDetectorRef, private sanitizer: DomSanitizer, private workflowStatusService: WorkflowStatusService, private guiConfigService: GuiConfigService ) {} ngOnChanges(changes: SimpleChanges): void { this.operatorId = changes.operatorId?.currentValue; if (this.operatorId) { const paginatedResultService = this.workflowResultService.getPaginatedResultService(this.operatorId); if (paginatedResultService) { this.isFrontPagination = false; this.totalNumTuples = paginatedResultService.getCurrentTotalNumTuples(); this.currentPageIndex = paginatedResultService.getCurrentPageIndex(); this.changePaginatedResultData(); this.tableStats = paginatedResultService.getStats(); this.prevTableStats = this.tableStats; } } } ngOnInit(): void { this.workflowStatusService .getStatusUpdateStream() .pipe(untilDestroyed(this)) .subscribe(statusMap => { if (this.operatorId && statusMap[this.operatorId]?.operatorState === OperatorState.Completed) { this.isOperatorFinished = true; this.changeDetectorRef.detectChanges(); } else { this.isOperatorFinished = false; } }); this.columnLimit = this.guiConfigService.env.limitColumns; this.workflowResultService .getResultUpdateStream() .pipe(untilDestroyed(this)) .subscribe(update => { if (!this.operatorId) { return; } const opUpdate = update[this.operatorId]; if (!opUpdate || !isWebPaginationUpdate(opUpdate)) { return; } let columnCount = this.currentColumns?.length; if (columnCount) this.widthPercent = (1 / columnCount) * 100 + "%"; this.isFrontPagination = false; this.totalNumTuples = opUpdate.totalNumTuples; if (opUpdate.dirtyPageIndices.includes(this.currentPageIndex)) { this.changePaginatedResultData(); } this.changeDetectorRef.detectChanges(); }); this.workflowResultService .getResultTableStats() .pipe(untilDestroyed(this)) .subscribe(([prevStats, currentStats]) => { if (!this.operatorId) { return; } if (currentStats[this.operatorId]) { this.tableStats = currentStats[this.operatorId]; if (prevStats[this.operatorId] && this.checkKeys(this.tableStats, prevStats[this.operatorId])) { this.prevTableStats = prevStats[this.operatorId]; } else { this.prevTableStats = this.tableStats; } } }); this.resizeService.currentSize.pipe(untilDestroyed(this)).subscribe(size => { this.panelHeight = size.height; this.adjustPageSizeBasedOnPanelSize(size.height); let currentPageNum: number = Math.ceil(this.totalNumTuples / this.pageSize); while (this.currentPageIndex > currentPageNum && this.currentPageIndex > 1) { this.currentPageIndex -= 1; } }); if (this.operatorId) { const paginatedResultService = this.workflowResultService.getPaginatedResultService(this.operatorId); if (paginatedResultService) { } } } checkKeys( currentStats: Record>, prevStats: Record> ): boolean { let firstSet = Object.keys(currentStats); let secondSet = Object.keys(prevStats); if (firstSet.length != secondSet.length) { return false; } for (let i = 0; i < firstSet.length; i++) { if (firstSet[i] != secondSet[i]) { return false; } } return true; } compare(field: string, stats: string): SafeHtml { let current = this.tableStats[field][stats]; let previous = this.prevTableStats[field][stats]; let currentStr: string; let previousStr: string; if (typeof current === "number" && typeof previous === "number") { currentStr = current.toFixed(2); previousStr = previous !== undefined ? previous.toFixed(2) : currentStr; } else { currentStr = current.toLocaleString(); previousStr = previous !== undefined ? previous.toLocaleString() : currentStr; } let styledValue = ""; if (this.isOperatorFinished) { for (let i = 0; i < currentStr.length; i++) { styledValue += `${currentStr[i]}`; } return this.sanitizer.bypassSecurityTrustHtml(styledValue); } for (let i = 0; i < currentStr.length; i++) { const char = currentStr[i]; const prevChar = previousStr[i]; if (char !== prevChar) { styledValue += `${char}`; } else { styledValue += `${char}`; } } return this.sanitizer.bypassSecurityTrustHtml(styledValue); } /** * Adjusts the number of result rows displayed per page based on the * available vertical space of the Texera results panel. * * The method accounts for fixed UI elements within the panel—such as * headers, column navigation controls, pagination, and the search bar— * to determine the remaining space available for rendering result rows. * The page size is then recalculated using the height of a single table row. * * To maintain a stable user experience during panel resizes, the current * page index is recomputed so that the previously visible results remain * in view and the user does not experience an abrupt jump in the dataset. * * @param panelHeight - The total height (in pixels) of the results panel. */ private adjustPageSizeBasedOnPanelSize(panelHeight: number) { const TABLE_HEADER_HEIGHT = 38.62; const PANEL_HEADER_HEIGHT = 64.27; // Includes panel title and tab bar const COLUMN_NAVIGATION_HEIGHT = 56.6; // Previous/Next columns controls const PAGINATION_HEIGHT = 32.63; const SEARCH_BAR_HEIGHT_WITH_MARGIN = 77; // Approximate height for search bar and margins const ROW_HEIGHT = 38.62; const usedHeight = TABLE_HEADER_HEIGHT + PANEL_HEADER_HEIGHT + COLUMN_NAVIGATION_HEIGHT + PAGINATION_HEIGHT + SEARCH_BAR_HEIGHT_WITH_MARGIN; const newPageSize = Math.max(1, Math.floor((panelHeight - usedHeight) / ROW_HEIGHT)); const oldOffset = (this.currentPageIndex - 1) * this.pageSize; this.pageSize = newPageSize; this.resizeService.pageSize = newPageSize; this.currentPageIndex = Math.floor(oldOffset / newPageSize) + 1; } /** * Callback function for table query params changed event * params containing new page index, new page size, and more * (this function will be called when user switch page) * * @param params new parameters */ onTableQueryParamsChange(params: NzTableQueryParams) { if (this.isFrontPagination) { return; } if (!this.operatorId) { return; } this.currentPageIndex = params.pageIndex; this.changePaginatedResultData(); } /** * Opens the model to display the row details in * pretty json format when clicked. User can view the details * in a larger, expanded format. */ open(indexInPage: number, rowData: IndexableObject): void { const currentRowIndex = indexInPage + (this.currentPageIndex - 1) * this.pageSize; // open the modal component const modalRef: NzModalRef = this.modalService.create({ // modal title nzTitle: "Row Details", nzContent: RowModalComponent, nzData: { operatorId: this.operatorId, rowIndex: currentRowIndex }, // set the index value and page size to the modal for navigation // prevent browser focusing close button (ugly square highlight) nzAutofocus: null, // modal footer buttons nzFooter: [ { label: "<", onClick: () => { const component = modalRef.componentInstance; if (component) { component.rowIndex -= 1; this.currentPageIndex = Math.floor(component.rowIndex / this.pageSize) + 1; component.ngOnChanges(); } }, disabled: () => modalRef.componentInstance?.rowIndex === 0, }, { label: ">", onClick: () => { const component = modalRef.componentInstance; if (component) { component.rowIndex += 1; this.currentPageIndex = Math.floor(component.rowIndex / this.pageSize) + 1; component.ngOnChanges(); } }, disabled: () => modalRef.componentInstance?.rowIndex === this.totalNumTuples - 1, }, { label: "OK", onClick: () => { modalRef.destroy(); }, type: "primary", }, ], }); } // frontend table data must be changed, because: // 1. result panel is opened - must display currently selected page // 2. user selects a new page - must display new page data // 3. current page is dirty - must re-fetch data changePaginatedResultData(): void { if (!this.operatorId) { return; } const paginatedResultService = this.workflowResultService.getPaginatedResultService(this.operatorId); if (!paginatedResultService) { return; } this.isLoadingResult = true; paginatedResultService .selectPage(this.currentPageIndex, this.pageSize, this.currentColumnOffset, this.columnLimit, this.columnSearch) .pipe(untilDestroyed(this)) .subscribe(pageData => { if (this.currentPageIndex === pageData.pageIndex) { this.setupResultTable(pageData.table, paginatedResultService.getCurrentTotalNumTuples()); this.changeDetectorRef.detectChanges(); } }); } /** * Updates all the result table properties based on the execution result, * displays a new data table with a new paginator on the result panel. * * @param resultData rows of the result (may not be all rows if displaying result for workflow completed event) * @param totalRowCount */ setupResultTable(resultData: ReadonlyArray, totalRowCount: number) { if (!this.operatorId) { return; } if (resultData.length < 1) { return; } this.isLoadingResult = false; this.changeDetectorRef.detectChanges(); // creates a shallow copy of the readonly response.result, // this copy will be has type object[] because MatTableDataSource's input needs to be object[] this.currentResult = resultData.slice(); // 1. Get all the column names except '_id', using the first tuple // 2. Use those names to generate a list of display columns // 3. Pass the result data as array to generate a new data table let columns: { columnKey: any; columnText: string }[]; const columnKeys = Object.keys(resultData[0]).filter(x => x !== "_id"); columns = columnKeys.map(v => ({ columnKey: v, columnText: v })); // generate columnDef from first row, column definition is in order this.currentColumns = this.generateColumns(columns); this.totalNumTuples = totalRowCount; } /** * Generates all the column information for the result data table * * @param columns */ generateColumns(columns: { columnKey: any; columnText: string }[]): TableColumn[] { return columns.map((col, index) => ({ columnDef: col.columnKey, header: col.columnText, getCell: (row: IndexableObject) => row[col.columnKey].toString(), })); } downloadData(data: any, rowIndex: number, columnIndex: number, columnName: string): void { const realRowNumber = (this.currentPageIndex - 1) * this.pageSize + rowIndex; const defaultFileName = `${columnName}_${realRowNumber}`; const modal = this.modalService.create({ nzTitle: "Export Data and Save to a Dataset", nzContent: ResultExportationComponent, nzData: { exportType: "data", workflowName: this.workflowActionService.getWorkflowMetadata.name, defaultFileName: defaultFileName, rowIndex: realRowNumber, columnIndex: columnIndex, }, nzFooter: null, }); } onColumnShiftLeft(): void { if (this.currentColumnOffset > 0) { this.currentColumnOffset = Math.max(0, this.currentColumnOffset - this.columnLimit); this.changePaginatedResultData(); } } onColumnShiftRight(): void { if (this.currentColumns && this.currentColumns.length === this.columnLimit) { this.currentColumnOffset += this.columnLimit; this.changePaginatedResultData(); } } onColumnSearch(event: Event): void { const input = event.target as HTMLInputElement; this.columnSearch = input.value; this.currentColumnOffset = 0; this.changePaginatedResultData(); } } ================================================ FILE: frontend/src/app/workspace/component/visualization-panel-content/visualization-frame-content.component.html ================================================ ================================================ FILE: frontend/src/app/workspace/component/visualization-panel-content/visualization-frame-content.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ #html-content { display: block; width: 100%; height: 100%; border: none; } ================================================ FILE: frontend/src/app/workspace/component/visualization-panel-content/visualization-frame-content.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { AfterContentInit, Component, Input } from "@angular/core"; import { DomSanitizer } from "@angular/platform-browser"; import { WorkflowResultService } from "../../service/workflow-result/workflow-result.service"; import { auditTime, filter } from "rxjs/operators"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; @UntilDestroy() @Component({ selector: "texera-visualization-panel-content", templateUrl: "./visualization-frame-content.component.html", styleUrls: ["./visualization-frame-content.component.scss"], }) export class VisualizationFrameContentComponent implements AfterContentInit { // operatorId: string = inject(NZ_MODAL_DATA).operatorId; @Input() operatorId?: string; // progressive visualization update and redraw interval in milliseconds public static readonly UPDATE_INTERVAL_MS = 2000; htmlData: any = ""; constructor( private workflowResultService: WorkflowResultService, private sanitizer: DomSanitizer ) {} ngAfterContentInit() { // attempt to draw chart immediately this.drawChart(); // setup an event lister that re-draws the chart content every (n) milliseconds // auditTime makes sure the first re-draw happens after (n) milliseconds has elapsed this.workflowResultService .getResultUpdateStream() .pipe(auditTime(VisualizationFrameContentComponent.UPDATE_INTERVAL_MS)) .pipe(filter(rec => this.operatorId !== undefined && rec[this.operatorId] !== undefined)) .pipe(untilDestroyed(this)) .subscribe(() => { this.drawChart(); }); } drawChart() { if (!this.operatorId) { return; } const operatorResultService = this.workflowResultService.getResultService(this.operatorId); if (!operatorResultService) { return; } const data = operatorResultService.getCurrentResultSnapshot(); if (!data) { return; } const parser = new DOMParser(); const lastData = data[data.length - 1]; const doc = parser.parseFromString(Object(lastData)["html-content"], "text/html"); doc.documentElement.style.height = "100%"; doc.body.style.height = "95%"; const firstDiv = doc.body.querySelector("div"); if (firstDiv) firstDiv.style.height = "100%"; const serializer = new XMLSerializer(); const newHtmlString = serializer.serializeToString(doc); this.htmlData = this.sanitizer.bypassSecurityTrustHtml(newHtmlString); // this line bypasses angular security } } ================================================ FILE: frontend/src/app/workspace/component/workflow-editor/comment-box-modal/nz-modal-comment-box.component.html ================================================ ================================================ FILE: frontend/src/app/workspace/component/workflow-editor/comment-box-modal/nz-modal-comment-box.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ .modal-body { min-height: 20vh; max-height: 60vh; overflow-y: auto; width: 100%; p { word-wrap: break-word; overflow-wrap: break-word; word-break: normal; white-space: normal; hyphens: auto; } } .modal-dialog { overflow-y: initial !important; } .commentContent { display: flex; flex-wrap: wrap; } ================================================ FILE: frontend/src/app/workspace/component/workflow-editor/comment-box-modal/nz-modal-comment-box.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component, HostListener, inject, Inject, LOCALE_ID } from "@angular/core"; import { NZ_MODAL_DATA, NzModalRef } from "ng-zorro-antd/modal"; import { CommentBox } from "src/app/workspace/types/workflow-common.interface"; import { WorkflowActionService } from "src/app/workspace/service/workflow-graph/model/workflow-action.service"; import { UserService } from "src/app/common/service/user/user.service"; import { NotificationService } from "../../../../common/service/notification/notification.service"; import { User } from "src/app/common/type/user"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { formatDate, NgFor } from "@angular/common"; import { YType } from "../../../types/shared-editing.interface"; import { NzListComponent, NzListItemComponent, NzListItemActionsComponent, NzListItemActionComponent, } from "ng-zorro-antd/list"; import { NzCommentComponent, NzCommentAvatarDirective, NzCommentContentDirective } from "ng-zorro-antd/comment"; import { NzAvatarComponent } from "ng-zorro-antd/avatar"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzInputGroupComponent, NzInputDirective, NzAutosizeDirective } from "ng-zorro-antd/input"; import { FormsModule } from "@angular/forms"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { FormlyRepeatDndComponent } from "../../../../common/formly/repeat-dnd/repeat-dnd.component"; @UntilDestroy() @Component({ selector: "texera-nz-modal-comment-box", templateUrl: "./nz-modal-comment-box.component.html", styleUrls: ["./nz-modal-comment-box.component.scss"], imports: [ NzListComponent, NgFor, NzListItemComponent, NzCommentComponent, NzAvatarComponent, NzCommentAvatarDirective, NzCommentContentDirective, ɵNzTransitionPatchDirective, NzSpaceCompactItemDirective, NzInputGroupComponent, NzInputDirective, FormsModule, NzAutosizeDirective, NzButtonComponent, NzWaveDirective, NzIconDirective, NzListItemActionsComponent, NzListItemActionComponent, FormlyRepeatDndComponent, ], }) export class NzModalCommentBoxComponent { readonly commentBox: YType = inject(NZ_MODAL_DATA).commentBox; public user?: User; constructor( @Inject(LOCALE_ID) public locale: string, public workflowActionService: WorkflowActionService, public userService: UserService, public modal: NzModalRef, public notificationService: NotificationService ) { this.userService .userChanged() .pipe(untilDestroyed(this)) .subscribe(user => (this.user = user)); } inputValue = ""; submitting = false; editValue = ""; public onClickAddComment(): void { this.submitting = true; this.addComment(this.inputValue); this.inputValue = ""; this.submitting = false; } public addComment(content: string): void { if (!this.user) { return; } // A compromise: we create the timestamp in the frontend since the entire comment is managed together, in JSON format const creationTime: string = new Date().toISOString(); const creatorName = this.user.name; const creatorID = this.user.uid; this.workflowActionService.addComment( { content, creatorName, creatorID, creationTime }, this.commentBox.get("commentBoxID").toJSON() as string ); } public deleteComment(creatorID: number, creationTime: string): void { if (!this.user) { return; } this.workflowActionService.deleteComment( creatorID, creationTime, this.commentBox.get("commentBoxID").toJSON() as string ); } public toggleEditInput(creatorName: string, creationTime: string): void { const currTxArea = document.getElementById("txarea" + creatorName + creationTime); const currComment = document.getElementById("comment" + creatorName + creationTime); const btn = document.getElementById("editbtn" + creatorName + creationTime); if (currTxArea == null || btn == null || currComment == null) { return; } const hiddenTextArea = currTxArea.getAttribute("hidden"); const hiddenComment = currComment.getAttribute("hidden"); if (hiddenTextArea && !hiddenComment) { currComment.setAttribute("hidden", "hidden"); currTxArea.removeAttribute("hidden"); btn.removeAttribute("hidden"); if (currComment.textContent != null) { this.editValue = currComment.textContent; } } else { currTxArea.setAttribute("hidden", "hidden"); btn.setAttribute("hidden", "hidden"); currComment.removeAttribute("hidden"); this.editValue = ""; } } public editComment(creatorID: number, creatorName: string, creationTime: string): void { if (!this.user) { return; } const newContent = this.editValue; this.editValue = ""; this.workflowActionService.editComment( creatorID, creationTime, this.commentBox.get("commentBoxID").toJSON() as string, newContent ); const currTxArea = document.getElementById("txarea" + creatorName + creationTime); const btn = document.getElementById("editbtn" + creatorName + creationTime); if (currTxArea == null || btn == null) { return; } currTxArea.setAttribute("hidden", "hidden"); btn.setAttribute("hidden", "hidden"); } public replyToComment(creatorName: string, content: string) { this.inputValue += "@" + creatorName + ':"' + content + '"\n'; } toRelative(datetime: string): string { return formatDate(new Date(datetime), "MM/dd/yyyy, hh:mm:ss a z", this.locale); } @HostListener("window:keydown", ["$event"]) onKeyDown(event: KeyboardEvent) { if ((event.metaKey || event.ctrlKey) && event.key == "Enter") { this.onClickAddComment(); event.preventDefault(); } } } ================================================ FILE: frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.html ================================================
    • copy
    • cut
    • paste
    • disable
    • enable
    • view result
    • remove view result
    • reuse result
    • remove reusing result
    • delete
    • delete
    • execute to this operator
    • Export result
    ================================================ FILE: frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ li { padding-top: 2px; padding-bottom: 2px; font-size: 14px; min-width: 120px; span { margin-left: 2px; margin-right: 12px; } } ================================================ FILE: frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { OperatorMetadataService } from "src/app/workspace/service/operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "src/app/workspace/service/operator-metadata/stub-operator-metadata.service"; import { ContextMenuComponent } from "./context-menu.component"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { WorkflowActionService } from "src/app/workspace/service/workflow-graph/model/workflow-action.service"; import { WorkflowResultService } from "src/app/workspace/service/workflow-result/workflow-result.service"; import { WorkflowResultExportService } from "src/app/workspace/service/workflow-result-export/workflow-result-export.service"; import { OperatorMenuService } from "src/app/workspace/service/operator-menu/operator-menu.service"; import { of } from "rxjs"; import { ReactiveFormsModule } from "@angular/forms"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { NzDropDownModule } from "ng-zorro-antd/dropdown"; import { ValidationWorkflowService } from "src/app/workspace/service/validation/validation-workflow.service"; import { NzModalModule, NzModalService } from "ng-zorro-antd/modal"; import { commonTestProviders } from "../../../../../common/testing/test-utils"; // Import NzModalModule and NzModalService import type { Mocked } from "vitest"; import { JointGraphWrapper } from "src/app/workspace/service/workflow-graph/model/joint-graph-wrapper"; import { WorkflowGraph } from "src/app/workspace/service/workflow-graph/model/workflow-graph"; describe("ContextMenuComponent", () => { let component: ContextMenuComponent; let fixture: ComponentFixture; let workflowActionService: Mocked; let workflowResultService: Mocked; let workflowResultExportService: Mocked; let operatorMenuService: Mocked; let jointGraphWrapperSpy: Mocked; let validationWorkflowService: Mocked; beforeEach(async () => { // Create spies for the services jointGraphWrapperSpy = { getCurrentHighlightedOperatorIDs: vi.fn(), getCurrentHighlightedCommentBoxIDs: vi.fn(), getCurrentHighlightedLinkIDs: vi.fn(), } as unknown as Mocked; jointGraphWrapperSpy.getCurrentHighlightedOperatorIDs.mockReturnValue([]); jointGraphWrapperSpy.getCurrentHighlightedCommentBoxIDs.mockReturnValue([]); jointGraphWrapperSpy.getCurrentHighlightedLinkIDs.mockReturnValue([]); const texeraGraphSpy = { isOperatorDisabled: vi.fn(), hasLinkWithID: vi.fn(), bundleActions: vi.fn() }; const workflowActionServiceSpy = { getJointGraphWrapper: vi.fn(), getWorkflowModificationEnabledStream: vi.fn(), deleteOperatorsAndLinks: vi.fn(), deleteCommentBox: vi.fn(), getWorkflowMetadata: vi.fn(), getTexeraGraph: vi.fn(), deleteLinkWithID: vi.fn(), }; workflowActionServiceSpy.getJointGraphWrapper.mockReturnValue(jointGraphWrapperSpy); workflowActionServiceSpy.getWorkflowModificationEnabledStream.mockReturnValue(of(true)); workflowActionServiceSpy.getTexeraGraph.mockReturnValue(texeraGraphSpy); workflowActionServiceSpy.deleteOperatorsAndLinks.mockReturnValue(undefined); workflowActionServiceSpy.deleteCommentBox.mockReturnValue(undefined); workflowActionServiceSpy.deleteLinkWithID.mockReturnValue(undefined); workflowActionServiceSpy.getWorkflowMetadata.mockReturnValue({ name: "Test Workflow" }); // Mock return value // Set up TexeraGraph spy return values texeraGraphSpy.hasLinkWithID.mockReturnValue(false); texeraGraphSpy.bundleActions.mockImplementation((callback: Function) => callback()); const workflowResultServiceSpy = { getResultService: vi.fn(), hasAnyResult: vi.fn() }; const workflowResultExportServiceSpy = { exportOperatorsResultAsFile: vi.fn() }; // Create a mock for OperatorMenuService with necessary properties and methods operatorMenuService = { highlightedOperators$: of([] as readonly string[]), highlightedCommentBoxes$: of([] as readonly string[]), isDisableOperator: false, isDisableOperatorClickable: false, isToViewResult: false, isToViewResultClickable: false, isMarkForReuse: false, isReuseResultClickable: false, saveHighlightedElements: vi.fn(), performPasteOperation: vi.fn(), disableHighlightedOperators: vi.fn(), viewResultHighlightedOperators: vi.fn(), reuseResultHighlightedOperator: vi.fn(), executeUpToOperator: vi.fn(), } as unknown as Mocked; const validationWorkflowServiceSpy = { validateOperator: vi.fn() }; await TestBed.configureTestingModule({ providers: [ { provide: OperatorMetadataService, useClass: StubOperatorMetadataService }, { provide: WorkflowActionService, useValue: workflowActionServiceSpy }, { provide: WorkflowResultService, useValue: workflowResultServiceSpy }, { provide: WorkflowResultExportService, useValue: workflowResultExportServiceSpy }, { provide: OperatorMenuService, useValue: operatorMenuService }, { provide: ValidationWorkflowService, useValue: validationWorkflowServiceSpy }, NzModalService, // Provide NzModalService ...commonTestProviders, ], imports: [ ContextMenuComponent, HttpClientTestingModule, ReactiveFormsModule, BrowserAnimationsModule, NzDropDownModule, NzModalModule, // Import NzModalModule ], }).compileComponents(); workflowActionService = TestBed.inject(WorkflowActionService) as unknown as Mocked; workflowResultService = TestBed.inject(WorkflowResultService) as unknown as Mocked; workflowResultExportService = TestBed.inject( WorkflowResultExportService ) as unknown as Mocked; // operatorMenuService is already assigned validationWorkflowService = TestBed.inject( ValidationWorkflowService ) as unknown as Mocked; fixture = TestBed.createComponent(ContextMenuComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); describe("isSelectedOperatorValid", () => { it("should return false when multiple operators are highlighted", () => { jointGraphWrapperSpy.getCurrentHighlightedOperatorIDs.mockReturnValue(["op1", "op2"]); component.isWorkflowModifiable = true; expect(component.canExecuteOperator()).toBe(false); expect(validationWorkflowService.validateOperator).not.toHaveBeenCalled(); }); it("should return false when no operators are highlighted", () => { jointGraphWrapperSpy.getCurrentHighlightedOperatorIDs.mockReturnValue([]); component.isWorkflowModifiable = true; expect(component.canExecuteOperator()).toBe(false); expect(validationWorkflowService.validateOperator).not.toHaveBeenCalled(); }); it("should return false when workflow is not modifiable", () => { jointGraphWrapperSpy.getCurrentHighlightedOperatorIDs.mockReturnValue(["op1"]); component.isWorkflowModifiable = false; expect(component.canExecuteOperator()).toBe(false); expect(validationWorkflowService.validateOperator).not.toHaveBeenCalled(); }); it("should return true when single operator is highlighted, workflow is modifiable, and operator is valid", () => { jointGraphWrapperSpy.getCurrentHighlightedOperatorIDs.mockReturnValue(["op1"]); component.isWorkflowModifiable = true; validationWorkflowService.validateOperator.mockReturnValue({ isValid: true }); expect(component.canExecuteOperator()).toBe(true); expect(validationWorkflowService.validateOperator).toHaveBeenCalledWith("op1"); }); it("should return false when single operator is highlighted but operator is invalid", () => { jointGraphWrapperSpy.getCurrentHighlightedOperatorIDs.mockReturnValue(["op1"]); component.isWorkflowModifiable = true; validationWorkflowService.validateOperator.mockReturnValue({ isValid: false, messages: {} }); expect(component.canExecuteOperator()).toBe(false); expect(validationWorkflowService.validateOperator).toHaveBeenCalledWith("op1"); }); }); describe("canExecuteOperator", () => { let texeraGraphSpy: Mocked; beforeEach(() => { texeraGraphSpy = workflowActionService.getTexeraGraph() as unknown as Mocked; jointGraphWrapperSpy.getCurrentHighlightedOperatorIDs.mockReturnValue(["op1"]); component.isWorkflowModifiable = true; validationWorkflowService.validateOperator.mockReturnValue({ isValid: true }); texeraGraphSpy.isOperatorDisabled.mockReturnValue(false); }); it("should return false when multiple operators are highlighted", () => { jointGraphWrapperSpy.getCurrentHighlightedOperatorIDs.mockReturnValue(["op1", "op2"]); expect(component.canExecuteOperator()).toBe(false); expect(validationWorkflowService.validateOperator).not.toHaveBeenCalled(); expect(texeraGraphSpy.isOperatorDisabled).not.toHaveBeenCalled(); }); it("should return false when no operators are highlighted", () => { jointGraphWrapperSpy.getCurrentHighlightedOperatorIDs.mockReturnValue([]); expect(component.canExecuteOperator()).toBe(false); expect(validationWorkflowService.validateOperator).not.toHaveBeenCalled(); expect(texeraGraphSpy.isOperatorDisabled).not.toHaveBeenCalled(); }); it("should return false when workflow is not modifiable", () => { component.isWorkflowModifiable = false; expect(component.canExecuteOperator()).toBe(false); expect(validationWorkflowService.validateOperator).not.toHaveBeenCalled(); expect(texeraGraphSpy.isOperatorDisabled).not.toHaveBeenCalled(); }); it("should return true when all conditions are met (valid, enabled, modifiable)", () => { expect(component.canExecuteOperator()).toBe(true); expect(validationWorkflowService.validateOperator).toHaveBeenCalledWith("op1"); expect(texeraGraphSpy.isOperatorDisabled).toHaveBeenCalledWith("op1"); }); it("should return false when operator is invalid and not check disabled status", () => { validationWorkflowService.validateOperator.mockReturnValue({ isValid: false, messages: {} }); expect(component.canExecuteOperator()).toBe(false); expect(validationWorkflowService.validateOperator).toHaveBeenCalledWith("op1"); expect(texeraGraphSpy.isOperatorDisabled).not.toHaveBeenCalled(); }); it("should return false when operator is valid but disabled", () => { validationWorkflowService.validateOperator.mockReturnValue({ isValid: true }); texeraGraphSpy.isOperatorDisabled.mockReturnValue(true); expect(component.canExecuteOperator()).toBe(false); expect(validationWorkflowService.validateOperator).toHaveBeenCalledWith("op1"); expect(texeraGraphSpy.isOperatorDisabled).toHaveBeenCalledWith("op1"); }); it("should check disabled status only for valid operators", () => { // First test with invalid operator validationWorkflowService.validateOperator.mockReturnValue({ isValid: false, messages: {} }); component.canExecuteOperator(); expect(texeraGraphSpy.isOperatorDisabled).not.toHaveBeenCalled(); // Then test with valid operator validationWorkflowService.validateOperator.mockReturnValue({ isValid: true }); component.canExecuteOperator(); expect(texeraGraphSpy.isOperatorDisabled).toHaveBeenCalledWith("op1"); }); }); }); ================================================ FILE: frontend/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component } from "@angular/core"; import { OperatorMenuService } from "src/app/workspace/service/operator-menu/operator-menu.service"; import { WorkflowActionService } from "src/app/workspace/service/workflow-graph/model/workflow-action.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { WorkflowResultService } from "src/app/workspace/service/workflow-result/workflow-result.service"; import { WorkflowResultExportService } from "src/app/workspace/service/workflow-result-export/workflow-result-export.service"; import { NzModalService } from "ng-zorro-antd/modal"; import { ResultExportationComponent } from "../../../result-exportation/result-exportation.component"; import { ValidationWorkflowService } from "src/app/workspace/service/validation/validation-workflow.service"; import { GuiConfigService } from "../../../../../common/service/gui-config.service"; import { NzMenuDirective, NzMenuItemComponent } from "ng-zorro-antd/menu"; import { NgIf } from "@angular/common"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; @UntilDestroy() @Component({ selector: "texera-context-menu", templateUrl: "./context-menu.component.html", styleUrls: ["./context-menu.component.scss"], imports: [NzMenuDirective, NgIf, NzMenuItemComponent, ɵNzTransitionPatchDirective, NzIconDirective], }) export class ContextMenuComponent { public isWorkflowModifiable: boolean = false; public highlightedOperatorIds: readonly string[] = []; public highlightedCommentBoxIds: readonly string[] = []; constructor( public workflowActionService: WorkflowActionService, public operatorMenuService: OperatorMenuService, public workflowResultExportService: WorkflowResultExportService, protected config: GuiConfigService, private workflowResultService: WorkflowResultService, private modalService: NzModalService, private validationWorkflowService: ValidationWorkflowService ) { this.registerWorkflowModifiableChangedHandler(); this.operatorMenuService.highlightedOperators$ .pipe(untilDestroyed(this)) .subscribe(ids => (this.highlightedOperatorIds = ids)); this.operatorMenuService.highlightedCommentBoxes$ .pipe(untilDestroyed(this)) .subscribe(ids => (this.highlightedCommentBoxIds = ids)); } public canExecuteOperator(): boolean { if (!this.hasExactlyOneOperatorSelected() || !this.isWorkflowModifiable) { return false; } const operatorID = this.getSelectedOperatorID(); return this.isOperatorExecutable(operatorID); } private hasExactlyOneOperatorSelected(): boolean { return this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs().length === 1; } private getSelectedOperatorID(): string { return this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs()[0]; } private isOperatorExecutable(operatorID: string): boolean { return ( this.validationWorkflowService.validateOperator(operatorID).isValid && !this.workflowActionService.getTexeraGraph().isOperatorDisabled(operatorID) ); } public hasHighlightedLinks(): boolean { return this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedLinkIDs().length > 0; } public onCopy(): void { this.operatorMenuService.saveHighlightedElements(); } public onPaste(): void { this.operatorMenuService.performPasteOperation(); } public onCut(): void { this.onCopy(); this.onDelete(); } public onDelete(): void { // Capture all highlighted IDs before starting deletion to avoid modification during iteration const highlightedOperatorIDs = Array.from( this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs() ); const highlightedCommentBoxIDs = Array.from( this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedCommentBoxIDs() ); const highlightedLinkIDs = Array.from( this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedLinkIDs() ); // Bundle all deletions together for proper undo/redo support this.workflowActionService.getTexeraGraph().bundleActions(() => { // Delete operators and their connected links this.workflowActionService.deleteOperatorsAndLinks(highlightedOperatorIDs); // Delete standalone selected links highlightedLinkIDs.forEach(highlightedLinkID => { // Only delete if the link still exists (might have been deleted with operators) if (this.workflowActionService.getTexeraGraph().hasLinkWithID(highlightedLinkID)) { this.workflowActionService.deleteLinkWithID(highlightedLinkID); } }); // Delete comment boxes highlightedCommentBoxIDs.forEach(highlightedCommentBoxID => this.workflowActionService.deleteCommentBox(highlightedCommentBoxID) ); }); } private registerWorkflowModifiableChangedHandler() { this.workflowActionService .getWorkflowModificationEnabledStream() .pipe(untilDestroyed(this)) .subscribe(modifiable => (this.isWorkflowModifiable = modifiable)); } /** * This is the handler for the execution result export button for only highlighted operators. * */ public onClickExportHighlightedExecutionResult(): void { this.modalService.create({ nzTitle: "Export Highlighted Operators Result", nzContent: ResultExportationComponent, nzData: { workflowName: this.workflowActionService.getWorkflowMetadata()?.name, sourceTriggered: "context-menu", }, nzFooter: null, }); } } ================================================ FILE: frontend/src/app/workspace/component/workflow-editor/mini-map/mini-map.component.html ================================================
    ================================================ FILE: frontend/src/app/workspace/component/workflow-editor/mini-map/mini-map.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ #minimap-button { position: absolute; bottom: 0; right: 0; z-index: 4; } #minimap-center-button { position: absolute; bottom: 0; right: 30px; z-index: 4; } #minimap-zoom-out-button { position: absolute; bottom: 0; right: 60px; z-index: 4; } #minimap-zoom-in-button { position: absolute; bottom: 0; right: 90px; z-index: 4; } #mini-map-container { position: relative; overflow: hidden; width: 300px; height: 200px; } #mini-map { width: 100%; height: 100%; position: relative; } #mini-map-navigator { position: absolute; border: 2px solid #797a79; cursor: move; z-index: 1; } ================================================ FILE: frontend/src/app/workspace/component/workflow-editor/mini-map/mini-map.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { MiniMapComponent } from "./mini-map.component"; import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; import { OperatorMetadataService } from "../../../service/operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../../../service/operator-metadata/stub-operator-metadata.service"; import { JointUIService } from "../../../service/joint-ui/joint-ui.service"; import { UndoRedoService } from "../../../service/undo-redo/undo-redo.service"; import { WorkflowUtilService } from "../../../service/workflow-graph/util/workflow-util.service"; import { DragDropModule } from "@angular/cdk/drag-drop"; import { commonTestProviders } from "../../../../common/testing/test-utils"; describe("MiniMapComponent", () => { let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ providers: [ WorkflowActionService, WorkflowUtilService, JointUIService, UndoRedoService, { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, ...commonTestProviders, ], imports: [MiniMapComponent, HttpClientTestingModule, DragDropModule], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(MiniMapComponent); TestBed.inject(WorkflowActionService); fixture.detectChanges(); }); it("should create", () => { expect(fixture.componentInstance).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/workspace/component/workflow-editor/mini-map/mini-map.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { AfterViewInit, Component, HostListener, OnDestroy, ViewChild } from "@angular/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { WorkflowActionService } from "../../../service/workflow-graph/model/workflow-action.service"; import { MAIN_CANVAS } from "../workflow-editor.component"; import * as joint from "jointjs"; import { JointGraphWrapper } from "../../../service/workflow-graph/model/joint-graph-wrapper"; import { PanelService } from "../../../service/panel/panel.service"; import { CdkDrag } from "@angular/cdk/drag-drop"; import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space"; import { NzButtonComponent } from "ng-zorro-antd/button"; import { NzWaveDirective } from "ng-zorro-antd/core/wave"; import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch"; import { NzIconDirective } from "ng-zorro-antd/icon"; import { FormlyRepeatDndComponent } from "../../../../common/formly/repeat-dnd/repeat-dnd.component"; @UntilDestroy() @Component({ selector: "texera-mini-map", templateUrl: "mini-map.component.html", styleUrls: ["mini-map.component.scss"], imports: [ NzSpaceCompactItemDirective, NzButtonComponent, NzWaveDirective, ɵNzTransitionPatchDirective, NzIconDirective, CdkDrag, FormlyRepeatDndComponent, ], }) export class MiniMapComponent implements AfterViewInit, OnDestroy { @ViewChild("navigatorDrag", { static: false }) navigatorDrag!: CdkDrag; scale = 0; paper!: joint.dia.Paper; dragging = false; hidden = false; constructor( private workflowActionService: WorkflowActionService, private panelService: PanelService ) {} ngAfterViewInit() { const map = document.getElementById("mini-map")!; this.scale = map.offsetWidth / (MAIN_CANVAS.xMax - MAIN_CANVAS.xMin); new joint.dia.Paper({ el: map, model: this.workflowActionService.getJointGraphWrapper().jointGraph, background: { color: "#F6F6F6" }, interactive: false, width: map.offsetWidth, height: map.offsetHeight, }) .scale(this.scale) .translate(-MAIN_CANVAS.xMin * this.scale, -MAIN_CANVAS.yMin * this.scale); this.workflowActionService .getJointGraphWrapper() .getMainJointPaperAttachedStream() .pipe(untilDestroyed(this)) .subscribe(mainPaper => { this.paper = mainPaper; this.updateNavigator(); mainPaper.on("translate", () => this.updateNavigator()); mainPaper.on("scale", () => this.updateNavigator()); mainPaper.on("resize", () => this.updateNavigator()); }); this.hidden = JSON.parse(localStorage.getItem("mini-map") as string) || false; this.panelService.closePanelStream.pipe(untilDestroyed(this)).subscribe(() => (this.hidden = true)); this.panelService.resetPanelStream.pipe(untilDestroyed(this)).subscribe(() => (this.hidden = false)); } @HostListener("window:beforeunload") ngOnDestroy(): void { localStorage.setItem("mini-map", JSON.stringify(this.hidden)); } onDrag(event: any) { this.paper.translate( this.paper.translate().tx + -event.event.movementX / this.scale, this.paper.translate().ty + -event.event.movementY / this.scale ); } private updateNavigator(): void { if (!this.dragging) { const editor = document.getElementById("workflow-editor")!; const navigator = document.getElementById("mini-map-navigator")!; const editorRect = editor.getBoundingClientRect(); const point = this.paper.pageToLocalPoint({ x: editorRect.left, y: editorRect.top, }); navigator.style.transform = ""; navigator.style.left = (point.x - MAIN_CANVAS.xMin) * this.scale + "px"; navigator.style.top = (point.y - MAIN_CANVAS.yMin) * this.scale + "px"; navigator.style.width = (editor.offsetWidth / this.paper.scale().sx) * this.scale + "px"; navigator.style.height = (editor.offsetHeight / this.paper.scale().sy) * this.scale + "px"; } } public onClickZoomOut(): void { // if zoom is already at minimum, don't zoom out again. if (this.workflowActionService.getJointGraphWrapper().isZoomRatioMin()) { return; } // make the ratio small. this.workflowActionService .getJointGraphWrapper() .setZoomProperty( this.workflowActionService.getJointGraphWrapper().getZoomRatio() - JointGraphWrapper.ZOOM_CLICK_DIFF ); } /** * This method will increase the zoom ratio and send the new zoom ratio value * to the joint graph wrapper to change overall zoom ratio that is used in * zoom buttons and mouse wheel zoom. * * If the zoom ratio already reaches maximum, this method will not do anything. */ public onClickZoomIn(): void { // if zoom is already reach maximum, don't zoom in again. if (this.workflowActionService.getJointGraphWrapper().isZoomRatioMax()) { return; } // make the ratio big. this.workflowActionService .getJointGraphWrapper() .setZoomProperty( this.workflowActionService.getJointGraphWrapper().getZoomRatio() + JointGraphWrapper.ZOOM_CLICK_DIFF ); } public triggerCenter(): void { this.workflowActionService.getTexeraGraph().triggerCenterEvent(); if (this.navigatorDrag) this.navigatorDrag.reset(); } } ================================================ FILE: frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.html ================================================
    ================================================ FILE: frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ #workflow-editor-wrapper { height: 100%; } #workflow-editor { height: 100%; } ::ng-deep .hide-region .region { display: none; } ::ng-deep .agent-action { // Agent action highlights - temporary 5-second visual indicators // Styles are defined inline in JointJS element, but this provides a hook for customization pointer-events: none; } ::ng-deep .hide-worker-count .operator-worker-count { display: none; } // Inline panels overlay container .panels-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; overflow: hidden; z-index: 10; } // Code panel wrapper - larger for code display .code-panel-wrapper { position: absolute; width: 400px; height: 220px; pointer-events: auto; z-index: 10; } // Chat popover container for operator chat button .chat-popover-container { position: absolute; pointer-events: auto; z-index: 20; transform: translateX(-50%); } .chat-popover { width: 400px; max-height: 400px; background: white; border-radius: 8px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); overflow: hidden; display: flex; flex-direction: column; &.chat-popover-visualization { width: 620px; max-height: 480px; } } .chat-popover-content { padding: 12px; overflow-y: auto; max-height: 400px; .chat-popover-visualization & { max-height: 480px; } } ================================================ FILE: frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; import { UndoRedoService } from "../../service/undo-redo/undo-redo.service"; import { DragDropService } from "../../service/drag-drop/drag-drop.service"; import { WorkflowUtilService } from "../../service/workflow-graph/util/workflow-util.service"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ValidationWorkflowService } from "../../service/validation/validation-workflow.service"; import { WorkflowEditorComponent } from "./workflow-editor.component"; import { NzModalCommentBoxComponent } from "./comment-box-modal/nz-modal-comment-box.component"; import { OperatorMetadataService } from "../../service/operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../../service/operator-metadata/stub-operator-metadata.service"; import { JointUIService } from "../../service/joint-ui/joint-ui.service"; import { NzModalModule, NzModalRef, NzModalService } from "ng-zorro-antd/modal"; import { Overlay } from "@angular/cdk/overlay"; import * as joint from "jointjs"; import { marbles } from "rxjs-marbles"; import { mockCommentBox, mockPoint, mockResultPredicate, mockScanPredicate, mockScanResultLink, mockScanSentimentLink, mockSentimentPredicate, } from "../../service/workflow-graph/model/mock-workflow-data"; import { WorkflowStatusService } from "../../service/workflow-status/workflow-status.service"; import { ExecuteWorkflowService } from "../../service/execute-workflow/execute-workflow.service"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { OperatorLink, OperatorPredicate } from "../../types/workflow-common.interface"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; import { tap } from "rxjs/operators"; import { UserService } from "src/app/common/service/user/user.service"; import { StubUserService } from "src/app/common/service/user/stub-user.service"; import { WorkflowVersionService } from "../../../dashboard/service/user/workflow-version/workflow-version.service"; import { of } from "rxjs"; import { NzContextMenuService, NzDropDownModule } from "ng-zorro-antd/dropdown"; import { RouterTestingModule } from "@angular/router/testing"; import { createYTypeFromObject } from "../../types/shared-editing.interface"; import * as jQuery from "jquery"; import { ContextMenuComponent } from "./context-menu/context-menu/context-menu.component"; import { ComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; import { MockComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service"; import { commonTestProviders } from "../../../common/testing/test-utils"; const createJQueryEvent = (event: string, properties?: object): JQuery.Event => (jQuery as unknown as JQueryStatic).Event(event, properties); describe("WorkflowEditorComponent", () => { /** * This sub test suite test if the JointJS paper is integrated with our Angular component well. * It uses a fake stub Workflow model that only provides the binding of JointJS graph. * It tests if manipulating the JointJS graph is correctly shown in the UI. */ describe("JointJS Paper", () => { let component: WorkflowEditorComponent; let fixture: ComponentFixture; let jointGraph: joint.dia.Graph; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ RouterTestingModule, HttpClientTestingModule, NzModalModule, NzDropDownModule, WorkflowEditorComponent, ContextMenuComponent, ], providers: [ JointUIService, WorkflowUtilService, UndoRedoService, DragDropService, ValidationWorkflowService, WorkflowActionService, NzContextMenuService, Overlay, { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService }, WorkflowStatusService, ExecuteWorkflowService, ...commonTestProviders, ], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(WorkflowEditorComponent); component = fixture.componentInstance; // detect changes first to run ngAfterViewInit and bind Model fixture.detectChanges(); jointGraph = component.paper.model; }); it("should create", () => { expect(component).toBeTruthy(); }); it("should create element in the UI after adding operator in the model", () => { const operatorID = "test_one_operator_1"; const element = new joint.shapes.basic.Rect(); element.set("id", operatorID); jointGraph.addCell(element); expect(component.paper.findViewByModel(element.id)).toBeTruthy(); }); it("should create a graph of multiple cells in the UI", () => { const operator1 = "test_multiple_1_op_1"; const operator2 = "test_multiple_1_op_2"; const element1 = new joint.shapes.basic.Rect({ size: { width: 100, height: 50 }, position: { x: 100, y: 400 }, }); element1.set("id", operator1); const element2 = new joint.shapes.basic.Rect({ size: { width: 100, height: 50 }, position: { x: 100, y: 400 }, }); element2.set("id", operator2); const link1 = new joint.dia.Link({ source: { id: operator1 }, target: { id: operator2 }, }); jointGraph.addCell(element1); jointGraph.addCell(element2); jointGraph.addCell(link1); // check the model is added correctly expect(jointGraph.getElements().find(el => el.id === operator1)).toBeTruthy(); expect(jointGraph.getElements().find(el => el.id === operator2)).toBeTruthy(); expect(jointGraph.getLinks().find(link => link.id === link1.id)).toBeTruthy(); // check the view is updated correctly expect(component.paper.findViewByModel(element1.id)).toBeTruthy(); expect(component.paper.findViewByModel(element2.id)).toBeTruthy(); expect(component.paper.findViewByModel(link1.id)).toBeTruthy(); }); }); /** * This sub test suites test the Integration of WorkflowEditorComponent with external modules, * such as drag and drop module, and highlight operator module. */ describe("External Module Integration", () => { let component: WorkflowEditorComponent; let fixture: ComponentFixture; let workflowActionService: WorkflowActionService; let validationWorkflowService: ValidationWorkflowService; let dragDropService: DragDropService; let jointUIService: JointUIService; let nzModalService: NzModalService; let undoRedoService: UndoRedoService; let workflowVersionService: WorkflowVersionService; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ RouterTestingModule, HttpClientTestingModule, NzModalModule, NzDropDownModule, NoopAnimationsModule, WorkflowEditorComponent, NzModalCommentBoxComponent, ], providers: [ JointUIService, WorkflowUtilService, WorkflowActionService, UndoRedoService, ValidationWorkflowService, DragDropService, NzModalService, NzContextMenuService, { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, { provide: UserService, useClass: StubUserService, }, WorkflowStatusService, ExecuteWorkflowService, UndoRedoService, WorkflowVersionService, ...commonTestProviders, ], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(WorkflowEditorComponent); component = fixture.componentInstance; workflowActionService = TestBed.inject(WorkflowActionService); workflowActionService.setHighlightingEnabled(true); validationWorkflowService = TestBed.inject(ValidationWorkflowService); dragDropService = TestBed.inject(DragDropService); // detect changes to run ngAfterViewInit and bind Model jointUIService = TestBed.inject(JointUIService); nzModalService = TestBed.inject(NzModalService); undoRedoService = TestBed.inject(UndoRedoService); workflowVersionService = TestBed.inject(WorkflowVersionService); fixture.detectChanges(); }); it("should try to highlight the operator when user mouse clicks on an operator", () => { const jointGraphWrapper = workflowActionService.getJointGraphWrapper(); // install a spy on the highlight operator function and pass the call through vi.spyOn(jointGraphWrapper, "highlightOperators"); workflowActionService.addOperator(mockScanPredicate, mockPoint); // unhighlight the operator in case it's automatically highlighted jointGraphWrapper.unhighlightOperators(mockScanPredicate.operatorID); // find the joint Cell View object of the operator element const jointCellView = component.paper.findViewByModel(mockScanPredicate.operatorID); jointCellView.$el.trigger("mousedown"); fixture.detectChanges(); // assert the function is called once // expect(highlightOperatorFunctionSpy.calls.count()).toEqual(1); // assert the highlighted operator is correct expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([mockScanPredicate.operatorID]); }); it("should highlight the commentBox when user clicks on a commentBox", () => { const jointGraphWrapper = workflowActionService.getJointGraphWrapper(); vi.spyOn(jointGraphWrapper, "highlightCommentBoxes"); workflowActionService.addCommentBox(mockCommentBox); jointGraphWrapper.unhighlightCommentBoxes(mockCommentBox.commentBoxID); const jointCellView = component.paper.findViewByModel(mockCommentBox.commentBoxID); jointCellView.$el.trigger("mousedown"); fixture.detectChanges(); expect(jointGraphWrapper.getCurrentHighlightedCommentBoxIDs()).toEqual([mockCommentBox.commentBoxID]); }); it("should open commentBox as NzModal when user double clicks on a commentBox", () => { const modalRef: NzModalRef = nzModalService.create({ nzTitle: "CommentBox", nzContent: NzModalCommentBoxComponent, nzData: { commentBox: createYTypeFromObject(mockCommentBox) }, nzAutofocus: null, nzFooter: [ { label: "OK", onClick: () => { modalRef.destroy(); }, type: "primary", }, ], }); vi.spyOn(nzModalService, "create").mockReturnValue(modalRef); const jointGraphWrapper = workflowActionService.getJointGraphWrapper(); workflowActionService.addCommentBox(mockCommentBox); jointGraphWrapper.highlightCommentBoxes(mockCommentBox.commentBoxID); const jointCellView = component.paper.findViewByModel(mockCommentBox.commentBoxID); jointCellView.$el.trigger("dblclick"); expect(nzModalService.create).toHaveBeenCalled(); fixture.detectChanges(); modalRef.destroy(); }); it("should unhighlight all highlighted operators when user mouse clicks on the blank space", () => { const jointGraphWrapper = workflowActionService.getJointGraphWrapper(); // add and highlight two operators workflowActionService.addOperatorsAndLinks( [ { op: mockScanPredicate, pos: mockPoint }, { op: mockResultPredicate, pos: mockPoint }, ], [] ); jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID, mockResultPredicate.operatorID); // assert that both operators are highlighted expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockScanPredicate.operatorID); expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockResultPredicate.operatorID); // find a blank area on the JointJS paper const blankPoint = { x: mockPoint.x + 100, y: mockPoint.y + 100 }; expect(component.paper.findViewsFromPoint(blankPoint)).toEqual([]); // trigger a click on the blank area using JointJS paper's jQuery element const point = component.paper.localToClientPoint(blankPoint); const event = createJQueryEvent("mousedown", { clientX: point.x, clientY: point.y, }); component.paper.$el.trigger(event); fixture.detectChanges(); // assert that all operators are unhighlighted expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([]); }); it("should react to operator highlight event and change the appearance of the operator to be highlighted", () => { const jointGraphWrapper = workflowActionService.getJointGraphWrapper(); workflowActionService.addOperator(mockScanPredicate, mockPoint); // highlight the operator jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID); // find the joint Cell View object of the operator element const jointCellView = component.paper.findViewByModel(mockScanPredicate.operatorID); // find the cell's child element with the joint highlighter class name `joint-highlight-stroke` const jointHighlighterElements = jointCellView.$el.children(".joint-highlight-stroke"); // the element should have the highlighter element in it expect(jointHighlighterElements.length).toEqual(1); }); it("should react to operator unhighlight event and change the appearance of the operator to be unhighlighted", () => { const jointGraphWrapper = workflowActionService.getJointGraphWrapper(); workflowActionService.addOperator(mockScanPredicate, mockPoint); // highlight the oprator first jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID); // find the joint Cell View object of the operator element const jointCellView = component.paper.findViewByModel(mockScanPredicate.operatorID); // find the cell's child element with the joint highlighter class name `joint-highlight-stroke` const jointHighlighterElements = jointCellView.$el.children(".joint-highlight-stroke"); // the element should have the highlighter element in it right now expect(jointHighlighterElements.length).toEqual(1); // then unhighlight the operator jointGraphWrapper.unhighlightOperators(mockScanPredicate.operatorID); // the highlighter element should not exist const jointHighlighterElementAfterUnhighlight = jointCellView.$el.children(".joint-highlight-stroke"); expect(jointHighlighterElementAfterUnhighlight.length).toEqual(0); }); it("should react to operator validation and change the color of operator box if the operator is valid ", () => { workflowActionService.getJointGraphWrapper(); workflowActionService.addOperator(mockScanPredicate, mockPoint); workflowActionService.addOperator(mockResultPredicate, mockPoint); workflowActionService.addLink(mockScanResultLink); const newProperty = { tableName: "test-table" }; workflowActionService.setOperatorProperty(mockScanPredicate.operatorID, newProperty); const operator1 = component.paper.getModelById(mockScanPredicate.operatorID); const operator2 = component.paper.getModelById(mockResultPredicate.operatorID); expect(operator1.attr("rect/stroke")).not.toEqual("red"); expect(operator2.attr("rect/stroke")).not.toEqual("red"); }); it("should validate operator connections correctly", () => { const mockScan2Predicate = { ...mockScanPredicate, operatorID: "mockScan2", }; workflowActionService.addOperator(mockScanPredicate, mockPoint); workflowActionService.addOperator(mockScan2Predicate, mockPoint); workflowActionService.addOperator(mockSentimentPredicate, mockPoint); workflowActionService.addOperator(mockResultPredicate, mockPoint); // should allow a link from scan to sentiment expect( component["validateOperatorConnection"]( mockScanPredicate.operatorID, "output-0", mockSentimentPredicate.operatorID, "input-0" ) ).toBe(true); // add a link from scan to sentiment workflowActionService.addLink(mockScanSentimentLink); // should not allow a link from scan to sentiment anymore expect( component["validateOperatorConnection"]( mockScanPredicate.operatorID, "output-0", mockSentimentPredicate.operatorID, "input-0" ) ).toBe(false); // should not allow a link from scan 2 to sentiment anymore expect( component["validateOperatorConnection"]( mockScan2Predicate.operatorID, "output-0", mockSentimentPredicate.operatorID, "input-0" ) ).toBe(true); // should still allow a link from scan to view result expect( component["validateOperatorConnection"]( mockScanPredicate.operatorID, "output-0", mockResultPredicate.operatorID, "input-0" ) ).toBe(true); // add a link from scan to view result workflowActionService.addLink(mockScanResultLink); // should not allow a link from scan to view result anymore expect( component["validateOperatorConnection"]( mockScanPredicate.operatorID, "output-0", mockResultPredicate.operatorID, "input-0" ) ).toBe(false); // should not allow a link from sentiment to view result anymore expect( component["validateOperatorConnection"]( mockSentimentPredicate.operatorID, "output-0", mockResultPredicate.operatorID, "input-0" ) ).toBe(true); }); it("should validate operator connections with ports that allow multi-inputs correctly", () => { // union operator metadata specifys that input-0 port allows multiple inputs connected to the same port const mockUnionPredicate: OperatorPredicate = { operatorID: "union-1", operatorType: "Union", operatorVersion: "u1", operatorProperties: {}, inputPorts: [{ portID: "input-0" }], outputPorts: [{ portID: "output-0" }], showAdvanced: false, isDisabled: false, }; workflowActionService.getJointGraphWrapper(); workflowActionService.addOperator(mockScanPredicate, mockPoint); workflowActionService.addOperator(mockSentimentPredicate, mockPoint); workflowActionService.addOperator(mockUnionPredicate, mockPoint); // should allow a link from scan to union expect( component["validateOperatorConnection"]( mockScanPredicate.operatorID, "output-0", mockUnionPredicate.operatorID, "input-0" ) ).toBe(true); // should allow a link from sentiment to union expect( component["validateOperatorConnection"]( mockSentimentPredicate.operatorID, "output-0", mockUnionPredicate.operatorID, "input-0" ) ).toBe(true); // add a link from scan to union const mockScanUnionLink: OperatorLink = { linkID: "mockScanUnion", source: { operatorID: mockScanPredicate.operatorID, portID: "output-0", }, target: { operatorID: mockUnionPredicate.operatorID, portID: "input-0", }, }; workflowActionService.addLink(mockScanUnionLink); // should still allow a link from sentiment to union expect( component["validateOperatorConnection"]( mockSentimentPredicate.operatorID, "output-0", mockUnionPredicate.operatorID, "input-0" ) ).toBe(true); }); it( "should react to jointJS paper zoom event", marbles(m => { const mockScaleRatio = 0.5; m.hot("-e-") .pipe(tap(() => workflowActionService.getJointGraphWrapper().setZoomProperty(mockScaleRatio))) .subscribe(() => { const currentScale = component.paper.scale(); expect(currentScale.sx).toEqual(mockScaleRatio); expect(currentScale.sy).toEqual(mockScaleRatio); }); }) ); it( "should react to jointJS paper restore default offset event", marbles(m => { const mockTranslation = 20; const originalOffset = component.paper.translate(); component.paper.translate(mockTranslation, mockTranslation); expect(component.paper.translate().tx).not.toEqual(originalOffset.tx); expect(component.paper.translate().ty).not.toEqual(originalOffset.ty); m.hot("-e-") .pipe(tap(() => workflowActionService.getJointGraphWrapper().restoreDefaultZoomAndOffset())) .subscribe(() => { expect(component.paper.translate().tx).toEqual(originalOffset.tx); expect(component.paper.translate().ty).toEqual(originalOffset.ty); }); }) ); // // TODO: this test case related to websocket is not stable, find out why and fix it // xdescribe('when executionStatus is enabled', () => { // beforeAll(() => { // environment.executionStatusEnabled = true; // workflowStatusService = TestBed.get(WorkflowStatusService); // }); // afterAll(() => { // environment.executionStatusEnabled = false; // }); // it('should display/hide operator status tooltip when cursor hovers/leaves an operator', () => { // // install a spy on the highlight operator function and pass the call through // const showTooltipFunctionSpy = vi.spyOn(jointUIService, 'showOperatorStatusToolTip'); // const hideTooltipFunctionSpy = vi.spyOn(jointUIService, 'hideOperatorStatusToolTip'); // workflowActionService.addOperator(mockScanPredicate, mockPoint); // // find the joint Cell View object of the operator element // const jointCellView = component.getJointPaper().findViewByModel(mockScanPredicate.operatorID); // const tooltipView = component.getJointPaper().findViewByModel( // JointUIService.getOperatorStatusTooltipElementID(mockScanPredicate.operatorID)); // // workflow has not started yet // // trigger a mouseenter on the cell view using its jQuery element // jointCellView.$el.trigger('mouseenter'); // fixture.detectChanges(); // // assert the function is not called yet // expect(showTooltipFunctionSpy).not.toHaveBeenCalled(); // expect(tooltipView.model.attr('polygon')['display']).toBe('none'); // // mock start the workflow // component['operatorStatusTooltipDisplayEnabled'] = true; // // trigger event mouse enter // jointCellView.$el.trigger('mouseenter'); // fixture.detectChanges(); // // assert the function is called // expect(showTooltipFunctionSpy).toHaveBeenCalled(); // expect(tooltipView.model.attr('polygon')['display']).toBeUndefined(); // // trigger event mouse leave // jointCellView.$el.trigger('mouseleave'); // // assert the function is called // expect(hideTooltipFunctionSpy).toHaveBeenCalled(); // expect(tooltipView.model.attr('polygon')['display']).toBe('none'); // }); // it('should update operator status tooltip content when workflow-status.service emits processState', () => { // // spy on key function, create simple workflow // const changeOperatorTooltipInfoSpy = vi.spyOn(jointUIService, 'changeOperatorStatusTooltipInfo'); // workflowActionService.addOperator(mockScanPredicateForStatus, mockPoint); // const tooltipView = component.getJointPaper().findViewByModel( // JointUIService.getOperatorStatusTooltipElementID(mockScanPredicateForStatus.operatorID)); // // workflowStatusService emits a mock status // workflowStatusService['status'].next(mockStatus1 as ProcessStatus); // fixture.detectChanges(); // // function should be called and content should be updated properly // expect(component['operatorStatusTooltipDisplayEnabled']).toBeTruthy(); // expect(changeOperatorTooltipInfoSpy).toHaveBeenCalledTimes(1); // expect(tooltipView.model.attr('#operatorCount/text')) // .toBe('Output:' + (mockStatus1 as ProcessStatus).operatorStatistics[mockScanOperatorID].outputCount + ' tuples'); // expect(tooltipView.model.attr('#operatorSpeed/text')) // .toBe('Speed:' + (mockStatus1 as ProcessStatus).operatorStatistics[mockScanOperatorID].speed + ' tuples/ms'); // // workflowStatusService emits another mock status // workflowStatusService['status'].next(mockStatus2 as ProcessStatus); // fixture.detectChanges(); // // function should be called again and content should be updated properly // expect(changeOperatorTooltipInfoSpy).toHaveBeenCalledTimes(2); // expect(tooltipView.model.attr('#operatorCount/text')) // .toBe('Output:' + (mockStatus2 as ProcessStatus).operatorStatistics[mockScanOperatorID].outputCount + ' tuples'); // expect(tooltipView.model.attr('#operatorSpeed/text')) // .toBe('Speed:' + (mockStatus2 as ProcessStatus).operatorStatistics[mockScanOperatorID].speed + ' tuples/ms'); // }); // it('should change operator state when workflow-status.service emits processState', () => { // // spy on key function, create simple workflow // const changeOperatorStatesSpy = vi.spyOn(jointUIService, 'changeOperatorStates'); // workflowActionService.addOperator(mockScanPredicateForStatus, mockPoint); // const jointCellView = component.getJointPaper().findViewByModel(mockScanPredicateForStatus.operatorID); // // workflowStatusService emits a mock status // workflowStatusService['status'].next(mockStatus1 as ProcessStatus); // fixture.detectChanges(); // // function should be called and state name should be updated properly // expect(changeOperatorStatesSpy).toHaveBeenCalledTimes(1); // expect(jointCellView.model.attr('#operatorStates')['text']) // .toEqual(OperatorStates[(mockStatus1 as ProcessStatus).operatorStates[mockScanOperatorID]]); // // workflowStatusService emits another mock status // workflowStatusService['status'].next(mockStatus2 as ProcessStatus); // fixture.detectChanges(); // // function should be called again and state name should be updated properly // expect(changeOperatorStatesSpy).toHaveBeenCalledTimes(2); // expect(jointCellView.model.attr('#operatorStates')['text']) // .toEqual(OperatorStates[OperatorStates.Completed]); // }); // it('should throw error when processState contains non-existing operatorID', () => { // // workflowStatusService emits a processStatus with info for a scan operator // // however there is no scan operator on the joinGraph/texeraGraph // // an error should be thrown // workflowStatusService['status'].next(mockStatus1 as ProcessStatus); // fixture.detectChanges(); // expect(component['handleOperatorStatisticsUpdate']).toThrowError(); // expect(component['handleOperatorStatesChange']).toThrowError(); // }); // }); it("should delete the highlighted operator when user presses the backspace key", () => { const texeraGraph = workflowActionService.getTexeraGraph(); const jointGraphWrapper = workflowActionService.getJointGraphWrapper(); workflowActionService.addOperator(mockScanPredicate, mockPoint); jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID); // dispatch a keydown event on the backspace key const event = new KeyboardEvent("keydown", { key: "Backspace" }); (document.activeElement as HTMLElement)?.blur(); document.dispatchEvent(event); fixture.detectChanges(); // assert the highlighted operator is deleted expect(texeraGraph.hasOperator(mockScanPredicate.operatorID)).toBeFalsy(); }); it("should delete the highlighted operator when user presses the delete key", () => { const texeraGraph = workflowActionService.getTexeraGraph(); const jointGraphWrapper = workflowActionService.getJointGraphWrapper(); workflowActionService.addOperator(mockScanPredicate, mockPoint); jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID); // dispatch a keydown event on the backspace key const event = new KeyboardEvent("keydown", { key: "Delete" }); (document.activeElement as HTMLElement)?.blur(); document.dispatchEvent(event); fixture.detectChanges(); // assert the highlighted operator is deleted expect(texeraGraph.hasOperator(mockScanPredicate.operatorID)).toBeFalsy(); }); it("should delete all highlighted operators when user presses the backspace key", () => { const texeraGraph = workflowActionService.getTexeraGraph(); const jointGraphWrapper = workflowActionService.getJointGraphWrapper(); workflowActionService.addOperatorsAndLinks( [ { op: mockScanPredicate, pos: mockPoint }, { op: mockResultPredicate, pos: mockPoint }, ], [] ); jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID, mockResultPredicate.operatorID); // assert that all operators are highlighted expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockScanPredicate.operatorID); expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockResultPredicate.operatorID); // dispatch a keydown event on the backspace key const event = new KeyboardEvent("keydown", { key: "Backspace" }); (document.activeElement as HTMLElement)?.blur(); document.dispatchEvent(event); fixture.detectChanges(); // assert that all highlighted operators are deleted expect(texeraGraph.hasOperator(mockScanPredicate.operatorID)).toBeFalsy(); expect(texeraGraph.hasOperator(mockResultPredicate.operatorID)).toBeFalsy(); }); // the new method of copying and pasting would not pass this unit test, since the permisssion // to write access to system clipboard is needed, and in the unit test, there is no way of turning // on the permission as far as I am concerned // it(`should create and highlight a new operator with the same metadata when user // copies and pastes the highlighted operator`, () => { // const jointGraphWrapper = workflowActionService.getJointGraphWrapper(); // const texeraGraph = workflowActionService.getTexeraGraph(); // workflowActionService.addOperator(mockScanPredicate, mockPoint); // jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID); // // dispatch clipboard events for copy and paste // const copyEvent = new ClipboardEvent("copy"); // (document.activeElement as HTMLElement)?.blur(); // document.dispatchEvent(copyEvent); // const pasteEvent = new ClipboardEvent("paste"); // (document.activeElement as HTMLElement)?.blur(); // document.dispatchEvent(pasteEvent); // // the pasted operator should be highlighted // const pastedOperatorID = jointGraphWrapper.getCurrentHighlightedOperatorIDs()[0]; // expect(pastedOperatorID).toBeDefined(); // // get the pasted operator // let pastedOperator = null; // if (pastedOperatorID) { // pastedOperator = texeraGraph.getOperator(pastedOperatorID); // } // expect(pastedOperator).toBeDefined(); // // two operators should have same metadata // expect(pastedOperatorID).not.toEqual(mockScanPredicate.operatorID); // if (pastedOperator) { // expect(pastedOperator.operatorType).toEqual(mockScanPredicate.operatorType); // expect(pastedOperator.operatorProperties).toEqual(mockScanPredicate.operatorProperties); // expect(pastedOperator.inputPorts).toEqual(mockScanPredicate.inputPorts); // expect(pastedOperator.outputPorts).toEqual(mockScanPredicate.outputPorts); // expect(pastedOperator.showAdvanced).toEqual(mockScanPredicate.showAdvanced); // } // }); // the new method won't pass the unit test because as far as I am concerned, there's no way // to grant the permission to the system clipboard in the Karma framework // it(`should delete the highlighted operator, create and highlight a new operator with the same metadata // when user cuts and pastes the highlighted operator`, () => { // const jointGraphWrapper = workflowActionService.getJointGraphWrapper(); // const texeraGraph = workflowActionService.getTexeraGraph(); // workflowActionService.addOperator(mockScanPredicate, mockPoint); // jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID); // // dispatch clipboard events for cut and paste // const cutEvent = new ClipboardEvent("cut"); // (document.activeElement as HTMLElement)?.blur(); // document.dispatchEvent(cutEvent); // const pasteEvent = new ClipboardEvent("paste"); // (document.activeElement as HTMLElement)?.blur(); // document.dispatchEvent(pasteEvent); // // the copied operator should be deleted // expect(() => { // texeraGraph.getOperator(mockScanPredicate.operatorID); // }).toThrowError(new RegExp("does not exist")); // // the pasted operator should be highlighted // const pastedOperatorID = jointGraphWrapper.getCurrentHighlightedOperatorIDs()[0]; // expect(pastedOperatorID).toBeDefined(); // // get the pasted operator // let pastedOperator = null; // if (pastedOperatorID) { // pastedOperator = texeraGraph.getOperator(pastedOperatorID); // } // expect(pastedOperator).toBeDefined(); // // two operators should have same metadata // expect(pastedOperatorID).not.toEqual(mockScanPredicate.operatorID); // if (pastedOperator) { // expect(pastedOperator.operatorType).toEqual(mockScanPredicate.operatorType); // expect(pastedOperator.operatorProperties).toEqual(mockScanPredicate.operatorProperties); // expect(pastedOperator.inputPorts).toEqual(mockScanPredicate.inputPorts); // expect(pastedOperator.outputPorts).toEqual(mockScanPredicate.outputPorts); // expect(pastedOperator.showAdvanced).toEqual(mockScanPredicate.showAdvanced); // } // }); // TODO: this test is unstable, find out why and fix it // same reason as above: can't grant clipboard access when pasting during unit-testing // it("should place the pasted operator in a non-overlapping position", () => { // const jointGraphWrapper = workflowActionService.getJointGraphWrapper(); // workflowActionService.addOperator(mockScanPredicate, mockPoint); // jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID); // // dispatch clipboard events for copy and paste // const copyEvent = new ClipboardEvent("copy"); // (document.activeElement as HTMLElement)?.blur(); // document.dispatchEvent(copyEvent); // const pasteEvent = new ClipboardEvent("paste"); // (document.activeElement as HTMLElement)?.blur(); // document.dispatchEvent(pasteEvent); // fixture.detectChanges(); // // get the pasted operator // const pastedOperatorID = jointGraphWrapper.getCurrentHighlightedOperatorIDs()[0]; // if (pastedOperatorID) { // const pastedOperatorPosition = jointGraphWrapper.getElementPosition(pastedOperatorID); // expect(pastedOperatorPosition).not.toEqual(mockPoint); // } // }); it("should highlight multiple operators when user clicks on them with shift key pressed", () => { const jointGraphWrapper = workflowActionService.getJointGraphWrapper(); workflowActionService.addOperator(mockScanPredicate, mockPoint); workflowActionService.addOperator(mockResultPredicate, mockPoint); jointGraphWrapper.highlightOperators(mockResultPredicate.operatorID); // assert that only the last operator is highlighted expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockResultPredicate.operatorID); expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).not.toContain(mockScanPredicate.operatorID); // find the joint Cell View object of the first operator element const jointCellView = component.paper.findViewByModel(mockScanPredicate.operatorID); // trigger a shift click on the cell view using its jQuery element const event = createJQueryEvent("mousedown", { shiftKey: true }); jointCellView.$el.trigger(event); fixture.detectChanges(); // assert that both operators are highlighted expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockScanPredicate.operatorID); expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockResultPredicate.operatorID); }); it("should unhighlight the highlighted operator when user clicks on it with shift key pressed", () => { const jointGraphWrapper = workflowActionService.getJointGraphWrapper(); workflowActionService.addOperator(mockScanPredicate, mockPoint); jointGraphWrapper.highlightOperators(mockScanPredicate.operatorID); // assert that the operator is highlighted expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockScanPredicate.operatorID); // find the joint Cell View object of the operator element const jointCellView = component.paper.findViewByModel(mockScanPredicate.operatorID); // trigger a shift click on the cell view using its jQuery element const event = createJQueryEvent("mousedown", { shiftKey: true }); jointCellView.$el.trigger(event); fixture.detectChanges(); // assert that the operator is unhighlighted expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).not.toContain(mockScanPredicate.operatorID); }); it("should highlight all operators when user presses command + A", () => { const jointGraphWrapper = workflowActionService.getJointGraphWrapper(); workflowActionService.addOperator(mockScanPredicate, mockPoint); workflowActionService.addOperator(mockResultPredicate, mockPoint); // unhighlight operators in case of automatic highlight jointGraphWrapper.unhighlightOperators(mockScanPredicate.operatorID, mockResultPredicate.operatorID); // dispatch a keydown event on the command + A key comb const event = new KeyboardEvent("keydown", { key: "a", metaKey: true }); (document.activeElement as HTMLElement)?.blur(); document.dispatchEvent(event); fixture.detectChanges(); // assert that all operators are highlighted expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockScanPredicate.operatorID); expect(jointGraphWrapper.getCurrentHighlightedOperatorIDs()).toContain(mockResultPredicate.operatorID); }); //undo it("should undo action when user presses command + Z or control + Z", () => { vi.spyOn(workflowVersionService, "getDisplayParticularVersionStream").mockReturnValue(of(false)); vi.spyOn(undoRedoService, "canUndo").mockReturnValue(true); let undoSpy = vi.spyOn(undoRedoService, "undoAction"); fixture.detectChanges(); const commandZEvent = new KeyboardEvent("keydown", { key: "Z", metaKey: true, shiftKey: false }); (document.activeElement as HTMLElement)?.blur(); document.dispatchEvent(commandZEvent); fixture.detectChanges(); expect(undoSpy).toHaveBeenCalledTimes(1); const controlZEvent = new KeyboardEvent("keydown", { key: "Z", ctrlKey: true, shiftKey: false }); (document.activeElement as HTMLElement)?.blur(); document.dispatchEvent(controlZEvent); fixture.detectChanges(); expect(undoSpy).toHaveBeenCalledTimes(2); }); //redo it("should redo action when user presses command/control + Y or command/control + shift + Z", () => { vi.spyOn(workflowVersionService, "getDisplayParticularVersionStream").mockReturnValue(of(false)); vi.spyOn(undoRedoService, "canRedo").mockReturnValue(true); let redoSpy = vi.spyOn(undoRedoService, "redoAction"); fixture.detectChanges(); const commandYEvent = new KeyboardEvent("keydown", { key: "y", metaKey: true, shiftKey: false }); (document.activeElement as HTMLElement)?.blur(); document.dispatchEvent(commandYEvent); fixture.detectChanges(); expect(redoSpy).toHaveBeenCalledTimes(1); const controlYEvent = new KeyboardEvent("keydown", { key: "y", ctrlKey: true, shiftKey: false }); (document.activeElement as HTMLElement)?.blur(); document.dispatchEvent(controlYEvent); fixture.detectChanges(); expect(redoSpy).toHaveBeenCalledTimes(2); const commandShitZEvent = new KeyboardEvent("keydown", { key: "z", metaKey: true, shiftKey: true }); (document.activeElement as HTMLElement)?.blur(); document.dispatchEvent(commandShitZEvent); fixture.detectChanges(); expect(redoSpy).toHaveBeenCalledTimes(3); const controlShitZEvent = new KeyboardEvent("keydown", { key: "z", ctrlKey: true, shiftKey: true }); (document.activeElement as HTMLElement)?.blur(); document.dispatchEvent(controlShitZEvent); fixture.detectChanges(); expect(redoSpy).toHaveBeenCalledTimes(4); }); }); }); ================================================ FILE: frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit } from "@angular/core"; import { combineLatest, fromEvent, merge, Subject } from "rxjs"; import { NzModalCommentBoxComponent } from "./comment-box-modal/nz-modal-comment-box.component"; import { NzModalRef, NzModalService } from "ng-zorro-antd/modal"; import { DragDropService } from "../../service/drag-drop/drag-drop.service"; import { DynamicSchemaService } from "../../service/dynamic-schema/dynamic-schema.service"; import { ExecuteWorkflowService } from "../../service/execute-workflow/execute-workflow.service"; import { fromJointPaperEvent, JointUIService, linkPathStrokeColor } from "../../service/joint-ui/joint-ui.service"; import { ValidationWorkflowService } from "../../service/validation/validation-workflow.service"; import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; import { WorkflowStatusService } from "../../service/workflow-status/workflow-status.service"; import { ExecutionState, OperatorState } from "../../types/execute-workflow.interface"; import { LogicalPort, OperatorLink, OperatorPredicate } from "../../types/workflow-common.interface"; import { auditTime, filter, map, takeUntil, withLatestFrom } from "rxjs/operators"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { UndoRedoService } from "../../service/undo-redo/undo-redo.service"; import { WorkflowVersionService } from "../../../dashboard/service/user/workflow-version/workflow-version.service"; import { OperatorMenuService } from "../../service/operator-menu/operator-menu.service"; import { NzContextMenuService, NzDropdownMenuComponent } from "ng-zorro-antd/dropdown"; import { ActivatedRoute, Router } from "@angular/router"; import * as _ from "lodash"; import * as joint from "jointjs"; import { isDefined } from "../../../common/util/predicate"; import { GuiConfigService } from "../../../common/service/gui-config.service"; import { line, curveCatmullRomClosed } from "d3-shape"; import concaveman from "concaveman"; import { OperatorResultSummary, AgentService } from "../../service/agent/agent.service"; import { NzNoAnimationDirective } from "ng-zorro-antd/core/animation"; import { ContextMenuComponent } from "./context-menu/context-menu/context-menu.component"; import { NgIf } from "@angular/common"; import { AgentInteractionComponent } from "../agent/agent-interaction/agent-interaction.component"; // jointjs interactive options for enabling and disabling interactivity // https://resources.jointjs.com/docs/jointjs/v3.2/joint.html#dia.Paper.prototype.options.interactive const defaultInteractiveOption = { vertexAdd: false, labelMove: false }; const disableInteractiveOption = { linkMove: false, labelMove: false, arrowheadMove: false, vertexMove: false, vertexAdd: false, vertexRemove: false, elementMove: false, // TODO: This is only a temporary change, will introduce another level of disable option. addLinkFromMagnet: false, }; export const MAIN_CANVAS = { xMin: -960, xMax: 2688, // xMin * 2.8 yMin: -540, yMax: 1512, // yMin * 2.8 }; /** * WorkflowEditorComponent is the component for the main workflow editor part of the UI. * * This component is bound with the JointJS paper. JointJS handles the operations of the main workflow. * The JointJS UI events are wrapped into observables and exposed to other components / services. * * See JointJS documentation for the list of events that can be captured on the JointJS paper view. * https://resources.jointjs.com/docs/jointjs/v2.0/joint.html#dia.Paper.events * * @author Zuozhi Wang * @author Henry Chen * */ @UntilDestroy() @Component({ selector: "texera-workflow-editor", templateUrl: "workflow-editor.component.html", styleUrls: ["workflow-editor.component.scss"], imports: [NzDropdownMenuComponent, NzNoAnimationDirective, ContextMenuComponent, NgIf, AgentInteractionComponent], }) export class WorkflowEditorComponent implements OnInit, AfterViewInit, OnDestroy { editor!: HTMLElement; editorWrapper!: HTMLElement; paper!: joint.dia.Paper; private interactive: boolean = true; private _onProcessKeyboardActionObservable: Subject = new Subject(); private wrapper; private currentOpenedOperatorID: string | null = null; private removeButton!: new () => joint.linkTools.Button; private breakpointButton!: new () => joint.linkTools.Button; // Chat popover state (operator chat button) public chatPopoverOperator: { operatorId: string; displayName: string; position: { x: number; y: number }; } | null = null; // Cached agent result summaries for port label display constructor( private workflowActionService: WorkflowActionService, private dynamicSchemaService: DynamicSchemaService, private dragDropService: DragDropService, private validationWorkflowService: ValidationWorkflowService, private jointUIService: JointUIService, private workflowStatusService: WorkflowStatusService, private executeWorkflowService: ExecuteWorkflowService, private nzModalService: NzModalService, private changeDetectorRef: ChangeDetectorRef, private undoRedoService: UndoRedoService, private workflowVersionService: WorkflowVersionService, private operatorMenu: OperatorMenuService, private route: ActivatedRoute, private router: Router, public nzContextMenu: NzContextMenuService, private elementRef: ElementRef, private config: GuiConfigService, private agentService: AgentService ) { this.wrapper = this.workflowActionService.getJointGraphWrapper(); } private operatorSummaries: Map = new Map(); ngOnInit(): void { // Cache the tool constructors this.removeButton = WorkflowEditorComponent.getRemoveButton(); this.breakpointButton = WorkflowEditorComponent.getBreakpointButton(); this.agentService.operatorResultSummaries$.pipe(untilDestroyed(this)).subscribe(summaries => { this.operatorSummaries = summaries; if (this.chatPopoverOperator) { this.changeDetectorRef.detectChanges(); } }); } /** * This function is provided to JointJS to disallow links starting from an in port. * * https://resources.jointjs.com/docs/jointjs/v2.0/joint.html#dia.Paper.prototype.options.validateMagnet */ private static validateOperatorMagnet( cellView: joint.dia.CellView, magnet: SVGElement, event: joint.dia.Event ): boolean { return magnet && magnet.getAttribute("port-group") === "out"; } ngAfterViewInit() { this.editor = document.getElementById("workflow-editor")!; this.editorWrapper = document.getElementById("workflow-editor-wrapper")!; document.addEventListener("keydown", this._handleKeyboardAction.bind(this)); this.initializeJointPaper(); this.handleDisableJointPaperInteractiveness(); this.handleOperatorValidation(); this.handlePaperRestoreDefaultOffset(); this.handlePaperZoom(); this.handleWindowResize(); this.handleViewDeleteOperator(); if (this.workflowActionService.getHighlightingEnabled()) { this.handleCellHighlight(); } this.handleDisableOperator(); this.handleViewOperatorResult(); this.handleReuseCacheOperator(); this.registerOperatorDisplayNameChangeHandler(); this.handleViewDeleteLink(); this.handleViewAddPort(); this.handleViewRemovePort(); this.handlePortClick(); this.handlePaperPan(); this.handleOperatorSelectionEvents(); this.handlePortHighlightEvent(); this.registerPortDisplayNameChangeHandler(); this.handleOperatorStatisticsUpdate(); this.handleRegionEvents(); this.handleOperatorSuggestionHighlightEvent(); this.handleAgentHoverHighlight(); this.handleElementDelete(); this.handleElementSelectAll(); this.handleElementCopy(); this.handleElementCut(); this.handleElementPaste(); this.handleLinkCursorHover(); if (this.config.env.linkBreakpointEnabled && this.workflowActionService.getHighlightingEnabled()) { this.handleLinkBreakpoint(); } this.handlePointerEvents(); this.handleURLFragment(); this.invokeResize(); this.handleCenterEvent(); this.handleOperatorChatButton(); } ngOnDestroy(): void { document.removeEventListener("keydown", this._handleKeyboardAction.bind(this)); } private _handleKeyboardAction(event: any) { this._onProcessKeyboardActionObservable = new Subject(); this.workflowVersionService .getDisplayParticularVersionStream() .pipe(takeUntil(this._onProcessKeyboardActionObservable)) .subscribe(displayParticularWorkflowVersion => { if (!displayParticularWorkflowVersion) { // cmd/ctrl+z undo ; ctrl+y or cmd/ctrl + shift+z for redo if ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.key.toLowerCase() === "z") { // UNDO if (this.undoRedoService.canUndo()) { this.undoRedoService.undoAction(); } } else if ( ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.key.toLowerCase() === "y") || ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key.toLowerCase() === "z") ) { // redo if (this.undoRedoService.canRedo()) { this.undoRedoService.redoAction(); } } // below for future hotkeys } this._onProcessKeyboardActionObservable.complete(); }); } private initializeJointPaper(): void { // attach the JointJS graph (model) to the paper (view) this.paper = this.wrapper.attachMainJointPaper({ el: this.editor, background: { color: "#F6F6F6" }, // enable jointjs feature that automatically snaps a link to the closest port with a radius of 30px snapLinks: { radius: 40 }, // disable jointjs default action that can make a link not connect to an operator linkPinning: false, // provide a validation to determine if two ports could be connected (only output connect to input is allowed) validateConnection: (...args) => this.validateJointOperatorConnection(...args), // provide a validation to determine if the port where link starts from is an out port validateMagnet: (...args) => WorkflowEditorComponent.validateOperatorMagnet(...args), // marks all the available magnets or elements when a link is dragged markAvailable: true, // disable jointjs default action of adding vertexes to the link interactive: defaultInteractiveOption, // set a default link element used by jointjs when user creates a link on UI defaultLink: JointUIService.getDefaultLinkCell(), // disable jointjs default action that stops propagate click events on jointjs paper preventDefaultBlankAction: false, // prevents normal right click menu showing up on jointjs paper preventContextMenu: true, // draw dots in the background of the paper drawGrid: { name: "fixedDot", args: { color: "black", scaleFactor: 8, thickness: 1.2 }, }, gridSize: 1, // use approximate z-index sorting, this is a workaround of a bug in async rendering mode // see https://github.com/clientIO/joint/issues/1320 sorting: joint.dia.Paper.sorting.APPROX, width: this.editor.offsetWidth, height: this.editor.offsetHeight, }); this.editor.classList.add("hide-worker-count"); } private handleDisableJointPaperInteractiveness(): void { this.workflowActionService .getWorkflowModificationEnabledStream() .pipe(untilDestroyed(this)) .subscribe(enabled => { if (enabled) { this.interactive = true; this.paper.setInteractivity(defaultInteractiveOption); } else { this.interactive = false; this.paper.setInteractivity(disableInteractiveOption); } this.changeDetectorRef.detectChanges(); }); } /** * This method subscribe to workflowStatusService's status stream * for Each processStatus that has been emitted * 1. enable operatorStatusTooltipDisplay because tooltip will not be empty * 2. for each operator in current texeraGraph: * - find its Statistics in processStatus, thrown an error if not found * - generate its corresponding tooltip's id * - pass the tooltip id and Statistics to jointUIService * the specific tooltip content will be updated * - if operator is in a group, save statistics in group's operatorInfo * 3. Whenever a group is expanded * - for each operatorInfo, display statistics if there are some saved. */ private handleOperatorStatisticsUpdate(): void { this.workflowStatusService .getStatusUpdateStream() .pipe(untilDestroyed(this)) .subscribe(status => { this.workflowActionService .getTexeraGraph() .getAllOperators() .forEach(op => { if ( isDefined(status[op.operatorID]) && this.executeWorkflowService.getExecutionState().state === ExecutionState.Recovering ) { status[op.operatorID] = { ...status[op.operatorID], operatorState: OperatorState.Recovering, }; } this.jointUIService.changeOperatorStatistics( this.paper, op.operatorID, status[op.operatorID], this.isSource(op.operatorID), this.isSink(op.operatorID) ); }); }); this.executeWorkflowService .getExecutionStateStream() .pipe(untilDestroyed(this)) .subscribe(event => { if (event.previous.state === ExecutionState.Recovering) { let operatorState: OperatorState; if (event.current.state === ExecutionState.Paused) { operatorState = OperatorState.Paused; } else if (event.current.state === ExecutionState.Completed) { operatorState = OperatorState.Completed; } else if (event.current.state === ExecutionState.Running) { operatorState = OperatorState.Running; } else { throw new Error("unknown state transition from recovering state: " + event.current.state); } this.workflowActionService .getTexeraGraph() .getAllOperators() .forEach(op => { this.jointUIService.changeOperatorState(this.paper, op.operatorID, operatorState); }); } }); } private handleRegionEvents(): void { this.editor.classList.add("hide-region"); const Region = joint.dia.Element.define( "region", { attrs: { body: { fill: "rgba(158,158,158,0.2)", pointerEvents: "none", class: "region", }, }, }, { markup: [{ tagName: "path", selector: "body" }], } ); let regionMap: { regionElement: joint.dia.Element; operators: joint.dia.Cell[] }[] = []; // update region elements on execution this.executeWorkflowService .getRegionUpdateStream() .pipe(untilDestroyed(this)) .subscribe(event => { this.paper.model .getCells() .filter(element => element instanceof Region) .forEach(element => element.remove()); regionMap = event.regions.map(([id, region]) => { const element = new Region({ id: "region-" + id }); const ops = region.map(id => this.paper.getModelById(id)); this.paper.model.addCell(element); this.updateRegionElement(element, ops); return { regionElement: element, operators: ops }; }); }); this.paper.model.on("change:position", operator => { regionMap .filter(region => region.operators.includes(operator)) .forEach(region => this.updateRegionElement(region.regionElement, region.operators)); }); // update region element colors on execution this.executeWorkflowService .getRegionStateStream() .pipe(untilDestroyed(this)) .subscribe(region => { const colorMap: Record = { ExecutingDependeePortsPhase: "rgba(33,150,243,0.2)", ExecutingNonDependeePortsPhase: "rgba(255,213,79,0.2)", Completed: "rgba(76,175,80,0.2)", }; this.paper.getModelById("region-" + region.id).attr("body/fill", colorMap[region.state]); }); } private updateRegionElement(regionElement: joint.dia.Element, operators: joint.dia.Cell[]) { const points = operators.flatMap(op => { const { x, y, width, height } = op.getBBox(), padding = 15; return [ [x - padding, y - padding], [x + width + padding, y - padding], [x - padding, y + height + padding + 10], [x + width + padding, y + height + padding + 10], ]; }); regionElement.attr("body/d", line().curve(curveCatmullRomClosed)(concaveman(points, 2, 0) as [number, number][])); } /** * Handles restore offset default event by translating jointJS paper * back to original position */ private handlePaperRestoreDefaultOffset(): void { this.wrapper .getRestorePaperOffsetStream() .pipe(untilDestroyed(this)) .subscribe(() => { this.wrapper.setZoomProperty(1); this.paper.translate(0, 0); }); } /** * Handles zoom events to make the jointJS paper larger or smaller. */ private handlePaperZoom(): void { this.wrapper .getWorkflowEditorZoomStream() .pipe(untilDestroyed(this)) .subscribe(newRatio => this.paper.scale(newRatio, newRatio)); } private handlePaperPan(): void { fromJointPaperEvent(this.paper, "blank:pointerdown") .pipe(untilDestroyed(this)) .subscribe(() => fromEvent(document, "mousemove") .pipe(takeUntil(fromEvent(document, "mouseup"))) .subscribe(event => this.paper.translate( this.paper.translate().tx + event.movementX / this.paper.scale().sx, this.paper.translate().ty + event.movementY / this.paper.scale().sy ) ) ); } /** * This is the handler for window resize event * When the window is resized, trigger an event to set papaer offset and dimension * and limit the event to at most one every 30ms. * * When user open the result panel and resize, the paper will resize to the size relative * to the result panel, therefore we also need to listen to the event from opening * and closing of the result panel. */ private handleWindowResize(): void { // when the window is resized (limit to at most one event every 30ms). merge(fromEvent(window, "resize").pipe(auditTime(30))) .pipe(untilDestroyed(this)) .subscribe(() => this.paper.setDimensions(this.editorWrapper.offsetWidth, this.editorWrapper.offsetHeight)); } private handleCellHighlight(): void { this.handleHighlightMouseDBClickInput(); this.handleHighlightMouseInput(); this.handleElementHightlightEvent(); } private handleDisableOperator(): void { this.workflowActionService .getTexeraGraph() .getDisabledOperatorsChangedStream() .pipe(untilDestroyed(this)) .subscribe(event => { event.newDisabled.concat(event.newEnabled).forEach(opID => { const op = this.workflowActionService.getTexeraGraph().getOperator(opID); this.jointUIService.changeOperatorDisableStatus(this.paper, op); }); }); } private handleViewOperatorResult(): void { this.workflowActionService .getTexeraGraph() .getViewResultOperatorsChangedStream() .pipe(untilDestroyed(this)) .subscribe(event => { event.newViewResultOps.concat(event.newUnviewResultOps).forEach(opID => { const op = this.workflowActionService.getTexeraGraph().getOperator(opID); this.jointUIService.changeOperatorViewResultStatus(this.paper, op, op.viewResult); }); }); } private handleReuseCacheOperator(): void { this.workflowActionService .getTexeraGraph() .getReuseCacheOperatorsChangedStream() .pipe(untilDestroyed(this)) .subscribe(event => { event.newReuseCacheOps.concat(event.newUnreuseCacheOps).forEach(opID => { const op = this.workflowActionService.getTexeraGraph().getOperator(opID); this.jointUIService.changeOperatorReuseCacheStatus(this.paper, op); }); }); } private registerOperatorDisplayNameChangeHandler(): void { this.workflowActionService .getTexeraGraph() .getOperatorDisplayNameChangedStream() .pipe(untilDestroyed(this)) .subscribe(({ operatorID, newDisplayName }) => { const op = this.workflowActionService.getTexeraGraph().getOperator(operatorID); this.jointUIService.changeOperatorJointDisplayName(op, this.paper, newDisplayName); }); } private registerPortDisplayNameChangeHandler(): void { this.workflowActionService .getTexeraGraph() .getPortDisplayNameChangedSubject() .pipe(untilDestroyed(this)) .subscribe(({ operatorID, portID, newDisplayName }) => { const operatorJointElement = this.workflowActionService.getJointGraph().getCell(operatorID); operatorJointElement.portProp(portID, "attrs/.port-label", { text: newDisplayName, }); }); } private handleHighlightMouseDBClickInput(): void { // on user mouse double-clicks a comment box, open that comment box // on user mouse double-clicks an operator, highlight it and open result panel fromJointPaperEvent(this.paper, "cell:pointerdblclick") .pipe(untilDestroyed(this)) .subscribe(event => { const clickedElement = event[0].model; if (clickedElement.isElement()) { const elementID = clickedElement.id.toString(); this.wrapper.setMultiSelectMode(event[1].shiftKey); if (this.workflowActionService.getTexeraGraph().hasCommentBox(elementID)) { this.openCommentBox(elementID); } else if (this.workflowActionService.getTexeraGraph().hasOperator(elementID)) { this.workflowActionService.openResultPanel(); } } }); } /** * Handles user mouse down events to trigger logically highlight and unhighlight an operator or group. * If user clicks the operator/group while pressing the shift key, multiselect mode is turned on. * When pressing the shift key, user can unhighlight a highlighted operator/group by clicking on it. * User can also unhighlight all operators and groups by clicking on the blank area of the graph. */ private handleHighlightMouseInput(): void { // on user mouse clicks an operator/group cell, highlight that operator/group // operator status tooltips should never be highlighted merge(fromJointPaperEvent(this.paper, "cell:pointerdown"), fromJointPaperEvent(this.paper, "cell:contextmenu")) // event[0] is the JointJS CellView; event[1] is the original JQuery Event .pipe( filter(event => event[0].model.isElement()), filter( event => this.workflowActionService.getTexeraGraph().hasOperator(event[0].model.id.toString()) || this.workflowActionService.getTexeraGraph().hasCommentBox(event[0].model.id.toString()) ) ) .pipe(untilDestroyed(this)) .subscribe(event => { // multiselect mode on if holding shift this.wrapper.setMultiSelectMode(event[1].shiftKey); const elementID = event[0].model.id.toString(); const highlightedOperatorIDs = this.wrapper.getCurrentHighlightedOperatorIDs(); const highlightedCommentBoxIDs = this.wrapper.getCurrentHighlightedCommentBoxIDs(); if (event[1].shiftKey) { // if in multiselect toggle highlights on click if (highlightedOperatorIDs.includes(elementID)) { this.workflowActionService.unhighlightOperators(elementID); } else if (this.workflowActionService.getTexeraGraph().hasOperator(elementID)) { this.workflowActionService.highlightOperators(event[1].shiftKey, elementID); } if (highlightedCommentBoxIDs.includes(elementID)) { this.wrapper.unhighlightCommentBoxes(elementID); } else if (this.workflowActionService.getTexeraGraph().hasCommentBox(elementID)) { this.workflowActionService.highlightCommentBoxes(event[1].shiftKey, elementID); } // if in the multiselect mode, also highlight the links in between two highlighted operators const allLinks: OperatorLink[] = this.workflowActionService.getTexeraGraph().getAllLinks(); const linksToBeHighlighted: string[] = allLinks .filter(link => { const currentHighlightedOperatorIDs = this.wrapper.getCurrentHighlightedOperatorIDs(); for (let sourceOperatorID of currentHighlightedOperatorIDs) { // first make sure the link is not already highlighted if (!(link.linkID in this.wrapper.getCurrentHighlightedLinkIDs)) { if (sourceOperatorID === link.source.operatorID) { // iterate through all the other highlighted operators for (let targetOperatorID of currentHighlightedOperatorIDs.filter( each => each != sourceOperatorID )) { if (targetOperatorID === link.target.operatorID) { return true; } } } } } }) .map(link => link.linkID); this.workflowActionService.highlightLinks(event[1].shiftKey, ...linksToBeHighlighted); } else { // else only highlight a single operator or group if (this.workflowActionService.getTexeraGraph().hasOperator(elementID)) { this.workflowActionService.highlightOperators(event[1].shiftKey, elementID); } else if (this.workflowActionService.getTexeraGraph().hasCommentBox(elementID)) { this.wrapper.highlightCommentBoxes(elementID); } } }); // on user mouse clicks on blank area, unhighlight all operators and groups merge(fromJointPaperEvent(this.paper, "blank:pointerdown"), fromJointPaperEvent(this.paper, "blank:contextmenu")) .pipe(untilDestroyed(this)) .subscribe(() => { this.wrapper.unhighlightElements(this.wrapper.getCurrentHighlights()); }); } private handleElementHightlightEvent(): void { // handle logical operator and group highlight / unhighlight events to let JointJS // use our own custom highlighter const highlightOptions = { name: "stroke", options: { attrs: { "stroke-width": 2, stroke: "#4A95FF", }, }, }; // highlight on OperatorHighlightStream or GroupHighlightStream or CommentBoxHighlightStream merge( this.wrapper.getJointOperatorHighlightStream(), this.wrapper.getJointGroupHighlightStream(), this.wrapper.getJointCommentBoxHighlightStream() ) .pipe(untilDestroyed(this)) .subscribe(elementIDs => elementIDs.forEach(elementID => { this.paper.findViewByModel(elementID).highlight("rect.body", { highlighter: highlightOptions }); }) ); // unhighlight on OperatorUnhighlightStream or GroupUnhighlightStream or CommentBoxUnhighlightStream merge( this.wrapper.getJointOperatorUnhighlightStream(), this.wrapper.getJointGroupUnhighlightStream(), this.wrapper.getJointCommentBoxUnhighlightStream() ) .pipe(untilDestroyed(this)) .subscribe(elementIDs => elementIDs.forEach(elementID => { const elem = this.paper.findViewByModel(elementID); if (elem !== undefined) { elem.unhighlight("rect.body", { highlighter: highlightOptions }); } }) ); } private handlePortHighlightEvent(): void { this.wrapper .getJointPortHighlightStream() .pipe(untilDestroyed(this)) .subscribe(operatorPortIDs => { operatorPortIDs.forEach(operatorPortID => { const operatorJointElement = ( this.workflowActionService.getJointGraph().getCell(operatorPortID.operatorID) ); operatorJointElement.portProp(operatorPortID.portID, "attrs/.port-body", { r: 8, stroke: "#4A95FF", "stroke-width": 3, }); }); }); this.wrapper .getJointPortUnhighlightStream() .pipe(untilDestroyed(this)) .subscribe(operatorPortIDs => { operatorPortIDs.forEach(operatorPortID => { const operatorJointElement = ( this.workflowActionService.getJointGraph().getCell(operatorPortID.operatorID) ); operatorJointElement.portProp(operatorPortID.portID, "attrs/.port-body", { r: 5, stroke: "none", }); }); }); } private openCommentBox(commentBoxID: string): void { const commentBox = this.workflowActionService.getTexeraGraph().getSharedCommentBoxType(commentBoxID); const modalRef: NzModalRef = this.nzModalService.create({ // modal title nzTitle: "Comments", nzContent: NzModalCommentBoxComponent, // set component @Input attributes nzData: { commentBox: commentBox }, // set the index value and page size to the modal for navigation // prevent browser focusing close button (ugly square highlight) nzAutofocus: null, // modal footer buttons nzFooter: null, }); modalRef.afterClose.pipe(untilDestroyed(this)).subscribe(() => { this.wrapper.unhighlightCommentBoxes(commentBoxID); this.setURLFragment(null); }); } private handleOperatorSuggestionHighlightEvent(): void { const highlightOptions = { name: "stroke", options: { attrs: { "stroke-width": 5, stroke: "#551A8B70", }, }, }; this.dragDropService .getOperatorSuggestionHighlightStream() .pipe(untilDestroyed(this)) .subscribe(value => this.paper.findViewByModel(value).highlight("rect.body", { highlighter: highlightOptions })); this.dragDropService .getOperatorSuggestionUnhighlightStream() .pipe(untilDestroyed(this)) .subscribe(value => this.paper.findViewByModel(value).unhighlight("rect.body", { highlighter: highlightOptions }) ); } /** * Handles the event where the Delete button is clicked for an Operator, * and call workflowAction to delete the corresponding operator. * * JointJS doesn't have delete button built-in with an operator element, * the delete button is Texera's own customized element. * Therefore JointJS doesn't come with default handler for delete an operator, * we need to handle the callback event `element:delete`. * The name of this callback event is registered in `JointUIService.getCustomOperatorStyleAttrs` */ private handleViewDeleteOperator(): void { // bind the delete button event to call the delete operator function in joint model action fromJointPaperEvent(this.paper, "element:delete") .pipe( filter(() => this.interactive), map(value => value[0]) ) .pipe(untilDestroyed(this)) .subscribe(elementView => { if (this.workflowActionService.getTexeraGraph().hasOperator(elementView.model.id.toString())) { this.workflowActionService.deleteOperator(elementView.model.id.toString()); } if (this.workflowActionService.getTexeraGraph().hasCommentBox(elementView.model.id.toString())) { this.workflowActionService.deleteCommentBox(elementView.model.id.toString()); } }); } private handleViewAddPort(): void { fromJointPaperEvent(this.paper, "element:add-input-port") .pipe( filter(() => this.interactive), map(value => value[0]) ) .pipe(untilDestroyed(this)) .subscribe(elementView => { if (this.workflowActionService.getTexeraGraph().hasOperator(elementView.model.id.toString())) { this.workflowActionService.addPort(elementView.model.id.toString(), true, false); } }); fromJointPaperEvent(this.paper, "element:add-output-port") .pipe( filter(() => this.interactive), map(value => value[0]) ) .pipe(untilDestroyed(this)) .subscribe(elementView => { if (this.workflowActionService.getTexeraGraph().hasOperator(elementView.model.id.toString())) { this.workflowActionService.addPort(elementView.model.id.toString(), false); } }); } private handleViewRemovePort(): void { fromJointPaperEvent(this.paper, "element:remove-input-port") .pipe( filter(() => this.interactive), map(value => value[0]) ) .pipe(untilDestroyed(this)) .subscribe(elementView => { if (this.workflowActionService.getTexeraGraph().hasOperator(elementView.model.id.toString())) { this.workflowActionService.removePort(elementView.model.id.toString(), true); } }); fromJointPaperEvent(this.paper, "element:remove-output-port") .pipe( filter(() => this.interactive), map(value => value[0]) ) .pipe(untilDestroyed(this)) .subscribe(elementView => { if (this.workflowActionService.getTexeraGraph().hasOperator(elementView.model.id.toString())) { this.workflowActionService.removePort(elementView.model.id.toString(), false); } }); } private handlePortClick(): void { fromJointPaperEvent(this.paper, "element:magnet:pointerclick") .pipe(untilDestroyed(this)) .subscribe(event => { // set the multi-select mode this.wrapper.setMultiSelectMode(event[1].shiftKey); const clickedPortID: LogicalPort = { operatorID: event[0].model.id as string, portID: event[2].getAttribute("port") as string, }; if (event[1].shiftKey) { if (_.find(this.wrapper.getCurrentHighlightedPortIDs(), clickedPortID) !== undefined) { // if the link being clicked is already highlighted, unhighlight it this.workflowActionService.unhighlightPorts(clickedPortID); } else if (this.workflowActionService.getTexeraGraph().hasOperator(clickedPortID.operatorID)) { // highlight the link if the link has not already been highlighted this.workflowActionService.highlightPorts(event[1].shiftKey, clickedPortID); } } else { // if user doesn't click on the shift key, highlight only a single port if (this.workflowActionService.getTexeraGraph().hasOperator(clickedPortID.operatorID)) { this.workflowActionService.highlightPorts(event[1].shiftKey, clickedPortID); } } }); } private handleOperatorSelectionEvents(): void { fromJointPaperEvent(this.paper, "element:pointerdown") .pipe(untilDestroyed(this)) .subscribe(event => { const operatorID = event[0].model.id.toString(); if (this.currentOpenedOperatorID !== null && this.paper.getModelById(this.currentOpenedOperatorID)) { this.jointUIService.foldOperatorDetails(this.paper, this.currentOpenedOperatorID); } this.currentOpenedOperatorID = operatorID; this.jointUIService.unfoldOperatorDetails(this.paper, operatorID); }); fromJointPaperEvent(this.paper, "element:contextmenu") .pipe(untilDestroyed(this)) .subscribe(event => { const operatorID = event[0].model.id.toString(); if (this.currentOpenedOperatorID !== null && this.paper.getModelById(this.currentOpenedOperatorID)) { this.jointUIService.foldOperatorDetails(this.paper, this.currentOpenedOperatorID); } this.currentOpenedOperatorID = operatorID; this.jointUIService.unfoldOperatorDetails(this.paper, operatorID); }); // Handle right-click on links fromJointPaperEvent(this.paper, "link:contextmenu") .pipe(untilDestroyed(this)) .subscribe(event => { const linkID = event[0].model.id.toString(); // Highlight the link when right-clicked this.workflowActionService.highlightLinks(false, linkID); }); fromJointPaperEvent(this.paper, "blank:pointerdown") .pipe(untilDestroyed(this)) .subscribe(() => { if (this.currentOpenedOperatorID !== null && this.paper.getModelById(this.currentOpenedOperatorID)) { this.jointUIService.foldOperatorDetails(this.paper, this.currentOpenedOperatorID); this.currentOpenedOperatorID = null; } }); } /** * Handles the event where the Delete button is clicked for a Link, * and call workflowAction to delete the corresponding link. * * We handle link deletion on our own by defining a custom markup. * Therefore JointJS doesn't come with default handler for delete an operator, * we need to handle the callback event `tool:remove`. */ private handleViewDeleteLink(): void { fromJointPaperEvent(this.paper, "tool:remove") .pipe( filter(() => this.interactive), map(value => value[0]) ) .pipe(untilDestroyed(this)) .subscribe(elementView => { this.workflowActionService.deleteLinkWithID(elementView.model.id.toString()); }); } /** * if the operator is valid , the border of the box will be default */ private handleOperatorValidation(): void { this.validationWorkflowService .getOperatorValidationStream() .pipe(untilDestroyed(this)) .subscribe(value => this.jointUIService.changeOperatorColor(this.paper, value.operatorID, value.validation.isValid) ); } /** * This function is provided to JointJS to disable some invalid connections on the UI. * If the connection is invalid, users are not able to connect the links on the UI. * * https://resources.jointjs.com/docs/jointjs/v2.0/joint.html#dia.Paper.prototype.options.validateConnection */ private validateJointOperatorConnection( sourceView: joint.dia.CellView, sourceMagnet: SVGElement | undefined, targetView: joint.dia.CellView, targetMagnet: SVGElement | undefined, end: joint.dia.LinkEnd, linkView: joint.dia.LinkView ): boolean { // user cannot draw connection starting from the input port (left side) if (sourceMagnet && sourceMagnet.getAttribute("port-group") === "in") { return false; } // user cannot connect to the output port (right side) if (targetMagnet && targetMagnet.getAttribute("port-group") === "out") { return false; } const sourceCellID = sourceView.model.id.toString(); const sourcePortID = sourceMagnet?.getAttribute("port"); const targetCellID = targetView.model.id.toString(); const targetPortID = targetMagnet?.getAttribute("port"); return this.validateOperatorConnection(sourceCellID, sourcePortID, targetCellID, targetPortID); } private validateOperatorConnection( sourceCellID: string, sourcePortID: string | null | undefined, targetCellID: string, targetPortID: string | null | undefined ): boolean { // cannot connect to itself if (sourceCellID === targetCellID) { return false; } // must connect to ports if (!sourcePortID || !targetPortID) { return false; } // must connect to operators if ( !this.workflowActionService.getTexeraGraph().hasOperator(sourceCellID) || !this.workflowActionService.getTexeraGraph().hasOperator(targetCellID) ) { return false; } // find all the links that are connected to the target operator and port const connectedLinksToTargetPort = this.workflowActionService .getTexeraGraph() .getAllLinks() .filter(link => link.target.operatorID === targetCellID && link.target.portID === targetPortID); // check if this link already exists, duplicate links are not allowed const isDuplicateLink = connectedLinksToTargetPort.filter( link => link.source.operatorID === sourceCellID && link.source.portID === sourcePortID ).length > 0; if (isDuplicateLink) { return false; } let disallowMultiInput = false; if (this.workflowActionService.getTexeraGraph().hasOperator(targetCellID)) { const portIndex = this.workflowActionService .getTexeraGraph() .getOperator(targetCellID) .inputPorts.findIndex(p => p.portID === targetPortID); if (portIndex >= 0) { const portInfo = this.dynamicSchemaService.getDynamicSchema(targetCellID).additionalMetadata.inputPorts[portIndex]; disallowMultiInput = portInfo?.disallowMultiLinks ?? false; } } return !(connectedLinksToTargetPort.length > 0 && disallowMultiInput); } /** * Deletes currently highlighted operators and groups when user presses the delete key. * When the focus is not on root document body, operator should not be deleted */ private handleElementDelete(): void { fromEvent(document, "keydown") .pipe( filter(() => document.activeElement === document.body), filter(() => this.interactive), filter(event => event.key === "Backspace" || event.key === "Delete") ) .pipe(untilDestroyed(this)) .subscribe(() => this.deleteElements()); } private deleteElements(): void { // Capture all highlighted IDs before starting deletion to avoid modification during iteration const highlightedOperatorIDs = Array.from(this.wrapper.getCurrentHighlightedOperatorIDs()); const highlightedCommentBoxIDs = Array.from(this.wrapper.getCurrentHighlightedCommentBoxIDs()); const highlightedLinkIDs = Array.from(this.wrapper.getCurrentHighlightedLinkIDs()); // Bundle all deletions together for proper undo/redo support this.workflowActionService.getTexeraGraph().bundleActions(() => { // Delete operators and their connected links this.workflowActionService.deleteOperatorsAndLinks(highlightedOperatorIDs); // Delete standalone selected links highlightedLinkIDs.forEach(highlightedLinkID => { // Only delete if the link still exists (might have been deleted with operators) if (this.workflowActionService.getTexeraGraph().hasLinkWithID(highlightedLinkID)) { this.workflowActionService.deleteLinkWithID(highlightedLinkID); } }); // Delete comment boxes highlightedCommentBoxIDs.forEach(highlightedCommentBoxID => this.workflowActionService.deleteCommentBox(highlightedCommentBoxID) ); }); } /** * Highlight all operators and groups on the graph when user presses command/ctrl + A. */ private handleElementSelectAll(): void { fromEvent(document, "keydown") .pipe( filter(() => document.activeElement === document.body), filter(event => (event.metaKey || event.ctrlKey) && event.key === "a") ) .pipe(untilDestroyed(this)) .subscribe(event => { event.preventDefault(); const allOperators = this.workflowActionService .getTexeraGraph() .getAllOperators() .map(operator => operator.operatorID); const allLinks = this.workflowActionService .getTexeraGraph() .getAllLinks() .map(link => link.linkID); const allCommentBoxes = this.workflowActionService .getTexeraGraph() .getAllCommentBoxes() .map(CommentBox => CommentBox.commentBoxID); this.wrapper.setMultiSelectMode(allOperators.length + allCommentBoxes.length > 1); this.workflowActionService.highlightLinks(allLinks.length > 1, ...allLinks); this.workflowActionService.highlightOperators(allOperators.length > 1, ...allOperators); this.workflowActionService.highlightCommentBoxes( allOperators.length + allCommentBoxes.length > 1, ...allCommentBoxes ); }); } /** * Caches the currently highlighted operators' info when user * triggers the copy event (i.e. presses command/ctrl + c on * keyboard or selects copy option from the browser menu). */ private handleElementCopy(): void { fromEvent(document, "copy") .pipe( filter(_ => document.activeElement === document.body), withLatestFrom(this.operatorMenu.highlightedOperators$, this.operatorMenu.highlightedCommentBoxes$), untilDestroyed(this) ) .subscribe(([_, highlightedOperators, highlightedCommentBoxes]) => { if (highlightedOperators.length > 0 || highlightedCommentBoxes.length > 0) { this.operatorMenu.saveHighlightedElements(); } }); } /** * Caches the currently highlighted operators' info and deletes it * when user triggers the cut event (i.e. presses command/ctrl + x * on keyboard or selects cut option from the browser menu). */ private handleElementCut(): void { fromEvent(document, "cut") .pipe( filter(() => document.activeElement === document.body), filter(() => this.interactive), withLatestFrom(this.operatorMenu.highlightedOperators$, this.operatorMenu.highlightedCommentBoxes$), untilDestroyed(this) ) .subscribe(([_, highlightedOperators, highlightedCommentBoxes]) => { if (highlightedOperators.length > 0 || highlightedCommentBoxes.length > 0) { this.operatorMenu.saveHighlightedElements(); this.deleteElements(); } }); } /** * Pastes the cached operators onto the workflow graph and highlights them * when user triggers the paste event (i.e. presses command/ctrl + v on * keyboard or selects paste option from the browser menu). */ private handleElementPaste(): void { fromEvent(document, "paste") .pipe( filter(() => document.activeElement === document.body), filter(() => this.interactive), untilDestroyed(this) ) .subscribe(() => this.operatorMenu.performPasteOperation()); } /** * handle the events of the cursor enter/leave a jointJS link cell * * Originally, such "hover -> appear" feature came as a default setting with JointJS library * However, in order to achieve conditional disappearance for the breakpoint button, * every interaction between the cursor and the link tools, including the delete button, * need to be handled manually */ private handleLinkCursorHover(): void { // When the cursor hovers over a link, the delete button and the breakpoint button appear fromJointPaperEvent(this.paper, "link:mouseenter") .pipe(map(value => value[0])) .pipe(untilDestroyed(this)) .subscribe(linkView => { // Create an array to hold the tools const tools: joint.dia.ToolView[] = [new this.removeButton()]; // If breakpoints are enabled, also add the breakpoint button if (this.config.env.linkBreakpointEnabled) { tools.push(new this.breakpointButton()); } const toolsView = new joint.dia.ToolsView({ tools }); linkView.addTools(toolsView); }); /** * When the cursor leaves a link, the delete button disappears. * If there is no breakpoint present on that link, the breakpoint button also disappears, * otherwise, the breakpoint button is not changed. */ fromJointPaperEvent(this.paper, "link:mouseleave") .pipe(map(value => value[0])) .pipe(untilDestroyed(this)) .subscribe(elementView => { // ensure that the link element exists if (this.paper.getModelById(elementView.model.id)) { const LinksWithBreakpoint = this.wrapper.getLinkIDsWithBreakpoint(); if (!LinksWithBreakpoint.includes(elementView.model.id.toString())) { this.paper.getModelById(elementView.model.id).findView(this.paper).hideTools(); } this.paper.getModelById(elementView.model.id).attr({ ".tool-remove": { display: "none" }, }); } }); } /** * handles events/observables related to the breakpoint */ private handleLinkBreakpoint(): void { this.handleLinkBreakpointToolAttachment(); this.handleLinkBreakpointButtonClick(); this.handleLinkBreakpointHighlightEvents(); this.handleLinkBreakpointToggleEvents(); } // when a link is added, append a breakpoint link-tool to its LinkView private handleLinkBreakpointToolAttachment(): void { this.wrapper .getJointLinkCellAddStream() .pipe(this.wrapper.jointGraphContext.bufferWhileAsync, untilDestroyed(this)) .subscribe(link => { const linkView = link.findView(this.paper); const breakpointButtonTool = this.breakpointButton; const breakpointButton = new breakpointButtonTool(); const toolsView = new joint.dia.ToolsView({ name: "basic-tools", tools: [breakpointButton], }); linkView.addTools(toolsView); // tools remain hidden until the cursor hovers over it or a break point is added linkView.hideTools(); }); } /** * handles the events of the breakpoint button is clicked for a link * and converts that event to a workflow action */ private handleLinkBreakpointButtonClick(): void { fromJointPaperEvent(this.paper, "tool:breakpoint") .pipe(untilDestroyed(this)) .subscribe(event => { // set the multi-select mode this.wrapper.setMultiSelectMode(event[1].shiftKey); const clickedLinkID = event[0].model.id.toString(); if (event[1].shiftKey) { if (this.wrapper.getCurrentHighlightedLinkIDs().includes(clickedLinkID)) { // if the link being clicked is already highlighted, unhighlight it this.workflowActionService.unhighlightLinks(clickedLinkID); } else if (this.workflowActionService.getTexeraGraph().hasLinkWithID(clickedLinkID)) { // highlight the link if the link has not already been highlighted this.workflowActionService.highlightLinks(event[1].shiftKey, clickedLinkID); } } else { // if user doesn't click on the shift key, highlight only a single link if (this.workflowActionService.getTexeraGraph().hasLinkWithID(clickedLinkID)) { this.workflowActionService.highlightLinks(event[1].shiftKey, clickedLinkID); } } }); } /** * Highlight/unhighlight the link according to the observable value received. */ private handleLinkBreakpointHighlightEvents(): void { this.wrapper .getLinkHighlightStream() .pipe(untilDestroyed(this)) .subscribe(linkIDs => { linkIDs.forEach(linkID => { this.paper.getModelById(linkID).attr({ ".connection": { stroke: "orange" }, ".marker-source": { fill: "orange" }, ".marker-target": { fill: "orange" }, }); }); }); this.wrapper .getLinkUnhighlightStream() .pipe(untilDestroyed(this)) .subscribe(linkIDs => { linkIDs.forEach(linkID => { this.paper.findViewByModel(linkID); if (this.paper.getModelById(linkID)) { // ensure that the link still exist this.paper.getModelById(linkID).attr({ ".connection": { stroke: linkPathStrokeColor }, ".marker-source": { fill: "none" }, ".marker-target": { fill: "none" }, }); } }); }); } /** * show/hide the breakpoint button according to the observable value received */ private handleLinkBreakpointToggleEvents(): void { this.wrapper .getLinkBreakpointShowStream() .pipe(this.wrapper.jointGraphContext.bufferWhileAsync, untilDestroyed(this)) .subscribe(linkID => { this.paper.getModelById(linkID.linkID).findView(this.paper).showTools(); }); this.wrapper .getLinkBreakpointHideStream() .pipe(this.wrapper.jointGraphContext.bufferWhileAsync, untilDestroyed(this)) .subscribe(linkID => { this.paper.getModelById(linkID.linkID).findView(this.paper).hideTools(); }); } private isSource(operatorID: string): boolean { return this.workflowActionService.getTexeraGraph().getOperator(operatorID).inputPorts.length == 0; } private isSink(operatorID: string): boolean { return this.workflowActionService.getTexeraGraph().getOperator(operatorID).outputPorts.length == 0; } /** * Handles mouse events to enable shared cursor. */ private handlePointerEvents(): void { fromEvent(this.editor, "mousemove") .pipe(untilDestroyed(this)) .subscribe(e => { const jointPoint = this.paper.clientToLocalPoint({ x: e.clientX, y: e.clientY }); this.workflowActionService.getTexeraGraph().updateSharedModelAwareness("userCursor", jointPoint); }); fromEvent(this.editor, "mouseleave") .pipe(untilDestroyed(this)) .subscribe(() => { this.workflowActionService.getTexeraGraph().updateSharedModelAwareness("isActive", false); }); fromEvent(this.editor, "mouseenter") .pipe(untilDestroyed(this)) .subscribe(() => { this.workflowActionService.getTexeraGraph().updateSharedModelAwareness("isActive", true); }); } private setURLFragment(fragment: string | null): void { this.router.navigate([], { relativeTo: this.route, fragment: fragment !== null ? fragment : undefined, preserveFragment: false, }); } private handleURLFragment(): void { // when operator/link/comment box is highlighted/unhighlighted, update URL fragment merge( this.wrapper.getJointOperatorHighlightStream(), this.wrapper.getJointOperatorUnhighlightStream(), this.wrapper.getLinkHighlightStream(), this.wrapper.getLinkUnhighlightStream(), this.wrapper.getJointCommentBoxHighlightStream(), this.wrapper.getJointCommentBoxUnhighlightStream() ) .pipe(untilDestroyed(this)) .subscribe(() => { // add element ID to URL fragment when only one element is highlighted // clear URL fragment when no element or multiple elements are highlighted // from state -> to state // case 1a: no highlighted -> highlight one element // case 1b: more than one elements highlighted -> unhighlight some elements so that only one element is highlighted // for case 1: set URL fragment to the highlighted element // case 2a: one element highlighted -> unhighlight the element // case 2b: one element highlighted -> highlight another element // for case 2: clear URL fragment // other cases, do nothing const highlightedIds = this.wrapper.getCurrentHighlightedIDs(); if (highlightedIds.length === 1) { this.setURLFragment(highlightedIds[0]); } else { this.setURLFragment(null); } }); // special case: open comment box when URL fragment is set this.workflowActionService .getTexeraGraph() .getCommentBoxAddStream() .pipe(untilDestroyed(this)) .subscribe(box => { if (this.route.snapshot.fragment === box.commentBoxID) { this.openCommentBox(box.commentBoxID); } }); } invokeResize() { const resizeEvent = new Event("resize"); setTimeout(() => { window.dispatchEvent(resizeEvent); }, 175); } /** * Handles the center event triggered from the group */ private handleCenterEvent(): void { const CENTER_OFFSET_RATIO = 0.15; // Offset ratio used to leave margin when centering this.workflowActionService .getTexeraGraph() .getCenterEventStream() .pipe(untilDestroyed(this)) .subscribe(() => { this.workflowActionService.calculateTopLeftOperatorPosition(); const centerCoord = this.workflowActionService.getCenterPoint(); const offsetX = this.editor.offsetWidth * CENTER_OFFSET_RATIO; const offsetY = this.editor.offsetHeight * CENTER_OFFSET_RATIO; const targetCoord = { x: centerCoord.x - offsetX, y: centerCoord.y - offsetY, }; this.paper.translate(-targetCoord.x, -targetCoord.y); }); } /** * Handle agent hover highlighting to show "viewed", "added", and "modified" labels on operators */ private handleAgentHoverHighlight(): void { const setupAgentHoverSubscription = () => { this.agentService .getAllAgents() .pipe(untilDestroyed(this)) .subscribe(agents => { agents.forEach(agent => { // Subscribe to each agent's hover operators stream this.agentService .getHoveredMessageOperatorsObservable(agent.id) .pipe(untilDestroyed(this)) .subscribe(({ viewedOperatorIds, addedOperatorIds, modifiedOperatorIds }) => { // Clear all previous labels first this.clearAllAgentActionLabels(); // Show "viewed" labels on viewed operators viewedOperatorIds.forEach(operatorId => { if (this.workflowActionService.getTexeraGraph().hasOperator(operatorId)) { this.jointUIService.showAgentActionLabel(this.paper, operatorId, "viewed", agent.name); } }); // Show "added" labels on added operators addedOperatorIds.forEach(operatorId => { if (this.workflowActionService.getTexeraGraph().hasOperator(operatorId)) { this.jointUIService.showAgentActionLabel(this.paper, operatorId, "added", agent.name); } }); // Show "modified" labels on modified operators modifiedOperatorIds.forEach(operatorId => { if (this.workflowActionService.getTexeraGraph().hasOperator(operatorId)) { this.jointUIService.showAgentActionLabel(this.paper, operatorId, "modified", agent.name); } }); }); }); }); }; // Subscribe to agent changes to set up hover subscriptions this.agentService.agentChange$.pipe(untilDestroyed(this)).subscribe(() => { setupAgentHoverSubscription(); }); // Initial setup setupAgentHoverSubscription(); } /** * Clear all agent action labels from all operators */ private clearAllAgentActionLabels(): void { this.workflowActionService .getTexeraGraph() .getAllOperators() .forEach(op => { this.jointUIService.hideAgentActionLabel(this.paper, op.operatorID); }); } /** * Handle the chat button click on operators. * Opens a chat popover for the operator to interact with agents. */ private handleOperatorChatButton(): void { fromJointPaperEvent(this.paper, "element:chat") .pipe( map(value => value[0]), untilDestroyed(this) ) .subscribe(elementView => { const operatorId = elementView.model.id.toString(); if (!this.workflowActionService.getTexeraGraph().hasOperator(operatorId)) { return; } // Toggle chat popover for this operator if (this.chatPopoverOperator?.operatorId === operatorId) { // Close if clicking the same operator this.chatPopoverOperator = null; } else { // Open chat popover for this operator const operator = this.workflowActionService.getTexeraGraph().getOperator(operatorId); const operatorSchema = this.dynamicSchemaService.getDynamicSchema(operatorId); const displayName = operator.customDisplayName ?? operatorSchema?.additionalMetadata.userFriendlyName ?? operator.operatorType; const position = this.getOperatorChatPopoverPosition(operatorId); if (position) { this.chatPopoverOperator = { operatorId, displayName, position, }; } } this.changeDetectorRef.detectChanges(); }); // Close chat popover when clicking on blank area fromJointPaperEvent(this.paper, "blank:pointerdown") .pipe(untilDestroyed(this)) .subscribe(() => { if (this.chatPopoverOperator) { this.closeChatPopover(); } }); // Update chat popover and context positions when operator moves this.paper.model.on("change:position", (cell: joint.dia.Cell) => { const cellId = cell.id.toString(); // Update popover position if the chat operator moves if (this.chatPopoverOperator && cellId === this.chatPopoverOperator.operatorId) { const position = this.getOperatorChatPopoverPosition(this.chatPopoverOperator.operatorId); if (position) { this.chatPopoverOperator = { ...this.chatPopoverOperator, position }; } } this.changeDetectorRef.detectChanges(); }); // Update position on zoom/pan this.wrapper .getWorkflowEditorZoomStream() .pipe(untilDestroyed(this)) .subscribe(() => { if (this.chatPopoverOperator) { const position = this.getOperatorChatPopoverPosition(this.chatPopoverOperator.operatorId); if (position) { this.chatPopoverOperator = { ...this.chatPopoverOperator, position }; } } this.changeDetectorRef.detectChanges(); }); } /** * Get the screen position for the chat popover relative to an operator. */ private getOperatorChatPopoverPosition(operatorId: string): { x: number; y: number } | null { const jointCell = this.paper.getModelById(operatorId); if (!jointCell) { return null; } const bbox = jointCell.getBBox(); const scale = this.paper.scale(); const translate = this.paper.translate(); // Position popover below the operator, centered horizontally // Add extra offset for the display name text below the operator box const screenX = (bbox.x + bbox.width / 2) * scale.sx + translate.tx; const screenY = (bbox.y + bbox.height) * scale.sy + translate.ty + 40; return { x: screenX, y: screenY }; } /** * Close the chat popover. */ closeChatPopover(): void { this.chatPopoverOperator = null; this.changeDetectorRef.detectChanges(); } getOperatorSampleRecords(operatorId: string): Record[] | undefined { return this.operatorSummaries.get(operatorId)?.sampleRecords; } getOperatorResultStatistics(operatorId: string): Record | undefined { return this.operatorSummaries.get(operatorId)?.resultStatistics; } isOperatorVisualization(operatorId: string): boolean { return this.operatorSummaries.get(operatorId)?.sampleRecords?.[0]?.["__is_visualization__"] === true; } /** * Info button on link between operator shown when user hovers over links */ private static getBreakpointButton(): new () => joint.linkTools.Button { return joint.linkTools.Button.extend({ name: "info-button", options: { markup: [ { tagName: "circle", selector: "info-button", attributes: { r: 10, fill: "#001DFF", cursor: "pointer", }, }, { tagName: "path", selector: "icon", attributes: { d: "M -2 4 2 4 M 0 3 0 0 M -2 -1 1 -1 M -1 -4 1 -4", fill: "none", stroke: "#FFFFFF", "stroke-width": 2, "pointer-events": "none", }, }, ], distance: -60, offset: 0, action: function (event: JQuery.Event, linkView: joint.dia.LinkView) { // when this button is clicked, it triggers an joint paper event if (linkView.paper) { linkView.paper.trigger("tool:breakpoint", linkView, event); } }, }, }); } /** * Remove button on link between operator shown when user hovers over links */ private static RemoveButton: new () => joint.linkTools.Button; private static getRemoveButton(): new () => joint.linkTools.Button { if (!WorkflowEditorComponent.RemoveButton) { WorkflowEditorComponent.RemoveButton = joint.linkTools.Button.extend({ name: "remove-button", options: { markup: [ { tagName: "circle", selector: "button", attributes: { r: 9, fill: "none", stroke: "#D8656A", "stroke-width": 2, "pointer-events": "visibleFill", cursor: "pointer", }, }, { tagName: "path", selector: "icon", attributes: { d: "M -4 -4 L 4 4 M 4 -4 L -4 4", fill: "none", stroke: "#D8656A", "stroke-width": 2, "stroke-linecap": "round", "pointer-events": "none", }, }, ], distance: -90, offset: 0, action: function (evt: JQuery.Event, linkView: joint.dia.LinkView) { if (linkView.paper) { linkView.paper.trigger("tool:remove", linkView, evt); } }, }, }); } return WorkflowEditorComponent.RemoveButton; } } ================================================ FILE: frontend/src/app/workspace/component/workspace.component.html ================================================
    ================================================ FILE: frontend/src/app/workspace/component/workspace.component.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ #result { position: absolute; bottom: 0; } texera-menu { position: absolute; top: 0; left: 0; z-index: 1; width: 100%; background-color: white; } texera-mini-map { position: absolute; bottom: 0; right: 0; z-index: 3; } texera-workflow-editor { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: #f6f6f6; } .spinner-container { display: flex; justify-content: center; align-items: center; height: 100%; } :host { user-select: none; } ================================================ FILE: frontend/src/app/workspace/component/workspace.component.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Location } from "@angular/common"; import { NO_ERRORS_SCHEMA } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { ActivatedRoute, Router } from "@angular/router"; import { NzMessageService } from "ng-zorro-antd/message"; import { EMPTY, of, Subject, throwError } from "rxjs"; import { NotificationService } from "../../common/service/notification/notification.service"; import { UserService } from "../../common/service/user/user.service"; import { WorkflowPersistService } from "../../common/service/workflow-persist/workflow-persist.service"; import { Workflow } from "../../common/type/workflow"; import { CodeEditorService } from "../service/code-editor/code-editor.service"; import { WorkflowCompilingService } from "../service/compile-workflow/workflow-compiling.service"; import { OperatorMetadataService } from "../service/operator-metadata/operator-metadata.service"; import { UndoRedoService } from "../service/undo-redo/undo-redo.service"; import { WorkflowConsoleService } from "../service/workflow-console/workflow-console.service"; import { WorkflowActionService } from "../service/workflow-graph/model/workflow-action.service"; import { OperatorReuseCacheStatusService } from "../service/workflow-status/operator-reuse-cache-status.service"; import { EntityType, HubService } from "../../hub/service/hub.service"; import { commonTestProviders } from "../../common/testing/test-utils"; import { WorkspaceComponent } from "./workspace.component"; describe("WorkspaceComponent", () => { let component: WorkspaceComponent; let fixture: ComponentFixture; let workflowActionService: any; let workflowPersistService: any; let operatorMetadataService: any; let userService: any; let undoRedoService: any; let notificationService: any; let hubService: any; let codeEditorService: any; let messageService: any; let routerMock: any; let locationMock: any; let metadataChangedSubject: Subject; let stubGraph: { triggerCenterEvent: ReturnType; hasElementWithID: ReturnType }; const stubWorkflow: Workflow = { wid: 42, name: "test", creationTime: 0, lastModifiedTime: 0, content: { operators: [], operatorPositions: {}, links: [], commentBoxes: [], settings: { dataTransferBatchSize: 100 }, }, } as unknown as Workflow; function configureRoute(params: Record = {}, queryParams: Record = {}) { return { snapshot: { params, queryParams, fragment: null as string | null }, }; } async function createFixture(routeOverride: any = configureRoute()) { metadataChangedSubject = new Subject(); stubGraph = { triggerCenterEvent: vi.fn(), hasElementWithID: vi.fn().mockReturnValue(false), }; workflowActionService = { setHighlightingEnabled: vi.fn(), resetAsNewWorkflow: vi.fn(), disableWorkflowModification: vi.fn(), enableWorkflowModification: vi.fn(), reloadWorkflow: vi.fn(), setNewSharedModel: vi.fn(), setWorkflowMetadata: vi.fn(), clearWorkflow: vi.fn(), highlightElements: vi.fn(), getTexeraGraph: vi.fn().mockReturnValue(stubGraph), getWorkflow: vi.fn().mockReturnValue(stubWorkflow), getWorkflowMetadata: vi.fn().mockReturnValue({ wid: 42, readonly: false }), workflowChanged: vi.fn().mockReturnValue(EMPTY), workflowMetaDataChanged: vi.fn().mockReturnValue(metadataChangedSubject.asObservable()), }; workflowPersistService = { isWorkflowPersistEnabled: vi.fn().mockReturnValue(true), persistWorkflow: vi.fn().mockReturnValue(of(stubWorkflow)), retrieveWorkflow: vi.fn().mockReturnValue(of(stubWorkflow)), }; operatorMetadataService = { getOperatorMetadata: vi.fn().mockReturnValue(of({})), }; userService = { isLogin: vi.fn().mockReturnValue(true), getCurrentUser: vi.fn().mockReturnValue({ uid: 7 }), }; undoRedoService = { clearUndoStack: vi.fn(), clearRedoStack: vi.fn(), }; notificationService = { error: vi.fn() }; hubService = { postView: vi.fn().mockReturnValue(of(0)) }; codeEditorService = { vc: undefined }; messageService = { error: vi.fn() }; routerMock = { navigate: vi.fn() }; locationMock = { go: vi.fn() }; // TODO(#5015): drop this template override once CodeEditorComponent's // own spec is fixed. Real child rendering would let us assert // editor-lifecycle wiring; today we stub the host element so the // heavyweight children don't compile in the test build. TestBed.overrideComponent(WorkspaceComponent, { set: { template: '
    ', imports: [], providers: [] }, }); await TestBed.configureTestingModule({ imports: [WorkspaceComponent, HttpClientTestingModule], providers: [ { provide: WorkflowActionService, useValue: workflowActionService }, { provide: WorkflowPersistService, useValue: workflowPersistService }, { provide: OperatorMetadataService, useValue: operatorMetadataService }, { provide: UserService, useValue: userService }, { provide: UndoRedoService, useValue: undoRedoService }, { provide: NotificationService, useValue: notificationService }, { provide: HubService, useValue: hubService }, { provide: CodeEditorService, useValue: codeEditorService }, { provide: NzMessageService, useValue: messageService }, { provide: Router, useValue: routerMock }, { provide: Location, useValue: locationMock }, { provide: ActivatedRoute, useValue: routeOverride }, // The three services listed in the constructor only to force their // initialization aren't exercised by any test here; provide stubs. { provide: WorkflowCompilingService, useValue: {} }, { provide: WorkflowConsoleService, useValue: {} }, { provide: OperatorReuseCacheStatusService, useValue: {} }, ...commonTestProviders, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(WorkspaceComponent); component = fixture.componentInstance; // ngOnDestroy clears the ViewContainerRef bound to `#codeEditor`. Tests that // exercise individual methods skip change detection, so the @ViewChild query // is never resolved; assign a stub to keep TestBed teardown from throwing. component.codeEditorViewRef = { clear: vi.fn() } as any; } describe("ngOnInit", () => { it("parses numeric pid from route query params", async () => { await createFixture(configureRoute({}, { pid: "13" })); component.ngOnInit(); expect(component.pid).toBe(13); }); it("treats non-numeric pid as undefined", async () => { await createFixture(configureRoute({}, { pid: "not-a-number" })); component.ngOnInit(); expect(component.pid).toBeUndefined(); }); it("enables highlighting on the workflow action service", async () => { await createFixture(); component.ngOnInit(); expect(workflowActionService.setHighlightingEnabled).toHaveBeenCalledWith(true); }); }); describe("ngAfterViewInit", () => { it("cold start (no wid in route): does not flip isLoading and registers metadata listener", async () => { await createFixture(configureRoute({})); fixture.detectChanges(); // triggers ngOnInit + ngAfterViewInit expect(component.isLoading).toBe(false); expect(workflowActionService.disableWorkflowModification).not.toHaveBeenCalled(); expect(operatorMetadataService.getOperatorMetadata).toHaveBeenCalled(); }); it("warm start (wid in route): sets isLoading=true and disables modification before load", async () => { await createFixture(configureRoute({ id: "42" })); // retrieveWorkflow is consumed inside loadWorkflowWithId — keep it pending so // we can observe the pre-completion loading state. workflowPersistService.retrieveWorkflow.mockReturnValue(new Subject()); fixture.detectChanges(); expect(component.isLoading).toBe(true); expect(workflowActionService.disableWorkflowModification).toHaveBeenCalled(); }); }); describe("loadWorkflowWithId", () => { it("on success: hands the workflow to the action service, clears undo/redo, and turns off loading", async () => { await createFixture(configureRoute({ id: "42" })); fixture.detectChanges(); expect(workflowActionService.setNewSharedModel).toHaveBeenCalledWith(42, { uid: 7 }); expect(workflowActionService.reloadWorkflow).toHaveBeenCalledWith(stubWorkflow); expect(undoRedoService.clearUndoStack).toHaveBeenCalled(); expect(undoRedoService.clearRedoStack).toHaveBeenCalled(); expect(component.isLoading).toBe(false); }); it("on failure: resets to a new workflow, surfaces an access error, and turns off loading", async () => { await createFixture(configureRoute({ id: "42" })); workflowPersistService.retrieveWorkflow.mockReturnValue(throwError(() => new Error("403"))); fixture.detectChanges(); expect(workflowActionService.resetAsNewWorkflow).toHaveBeenCalled(); expect(workflowActionService.enableWorkflowModification).toHaveBeenCalled(); expect(messageService.error).toHaveBeenCalledWith(expect.stringContaining("don't have access")); expect(component.isLoading).toBe(false); }); it("flags broken workflows via NotificationService.error but still loads them", async () => { const brokenWorkflow = { ...stubWorkflow, content: { ...stubWorkflow.content, // link references operator IDs that aren't in `operators: []` → broken. links: [{ source: { operatorID: "ghost-a" }, target: { operatorID: "ghost-b" } }], }, } as unknown as Workflow; await createFixture(configureRoute({ id: "42" })); workflowPersistService.retrieveWorkflow.mockReturnValue(of(brokenWorkflow)); fixture.detectChanges(); expect(notificationService.error).toHaveBeenCalledWith(expect.stringContaining("broken")); // Workflow still flows through reload — the error is informational, not blocking. expect(workflowActionService.reloadWorkflow).toHaveBeenCalledWith(brokenWorkflow); }); it("when URL fragment matches an element in the graph, highlights it", async () => { const route = configureRoute({ id: "42" }); route.snapshot.fragment = "operator-1"; await createFixture(route); stubGraph.hasElementWithID.mockReturnValue(true); fixture.detectChanges(); expect(stubGraph.hasElementWithID).toHaveBeenCalledWith("operator-1"); expect(workflowActionService.highlightElements).toHaveBeenCalledWith(false, "operator-1"); }); it("when URL fragment does not match any element, surfaces an error and clears the fragment", async () => { const route = configureRoute({ id: "42" }); route.snapshot.fragment = "stale-id"; await createFixture(route); // Default mock already returns false, but state explicitly for clarity. stubGraph.hasElementWithID.mockReturnValue(false); fixture.detectChanges(); expect(notificationService.error).toHaveBeenCalledWith(expect.stringContaining("stale-id")); // Two router.navigate calls: one preserving fragment, one clearing it. expect(routerMock.navigate).toHaveBeenLastCalledWith([], { relativeTo: route }); }); }); describe("triggerCenter", () => { it("delegates to the texera graph", async () => { await createFixture(); component.triggerCenter(); expect(stubGraph.triggerCenterEvent).toHaveBeenCalledTimes(1); }); }); describe("registerAutoPersistWorkflow", () => { it("is idempotent — only subscribes to workflowChanged once across repeated calls", async () => { await createFixture(); component.registerAutoPersistWorkflow(); component.registerAutoPersistWorkflow(); component.registerAutoPersistWorkflow(); expect(workflowActionService.workflowChanged).toHaveBeenCalledTimes(1); }); }); describe("updateViewCount", () => { it("posts a view event with the route's wid and the current user's uid", async () => { const route = configureRoute({ id: "42" }); await createFixture(route); fixture.detectChanges(); expect(hubService.postView).toHaveBeenCalledWith("42", 7, EntityType.Workflow); }); it("falls back to uid=0 when no user is signed in", async () => { const route = configureRoute({ id: "42" }); await createFixture(route); userService.getCurrentUser.mockReturnValue(undefined); // Re-trigger after mutating the mock; createFixture has already wired it. component.updateViewCount(); expect(hubService.postView).toHaveBeenCalledWith("42", 0, EntityType.Workflow); }); }); describe("onWIDChange", () => { it("syncs writeAccess from metadata.readonly each time the metadata changes", async () => { await createFixture(); fixture.detectChanges(); expect(component.writeAccess).toBe(false); // default before any emission workflowActionService.getWorkflowMetadata.mockReturnValue({ wid: 42, readonly: false }); metadataChangedSubject.next(); expect(component.writeAccess).toBe(true); workflowActionService.getWorkflowMetadata.mockReturnValue({ wid: 42, readonly: true }); metadataChangedSubject.next(); expect(component.writeAccess).toBe(false); }); it("ignores metadata emissions that have no wid yet", async () => { await createFixture(); fixture.detectChanges(); workflowActionService.getWorkflowMetadata.mockReturnValue({ wid: undefined, readonly: false }); metadataChangedSubject.next(); // writeAccess stays at its initial false — no metadata.wid means we don't know // whether the workflow is editable yet. expect(component.writeAccess).toBe(false); }); }); describe("ngOnDestroy", () => { it("persists the workflow on destroy when the user is signed in and persist is enabled", async () => { await createFixture(); component.ngOnDestroy(); expect(workflowPersistService.persistWorkflow).toHaveBeenCalledWith(stubWorkflow); expect(workflowActionService.clearWorkflow).toHaveBeenCalled(); }); it("skips the persist call when the user is not signed in", async () => { await createFixture(); userService.isLogin.mockReturnValue(false); component.ngOnDestroy(); expect(workflowPersistService.persistWorkflow).not.toHaveBeenCalled(); // Cleanup of the workflow state still happens regardless. expect(workflowActionService.clearWorkflow).toHaveBeenCalled(); }); }); describe("copilotEnabled", () => { it("passes through to GuiConfigService.env.copilotEnabled", async () => { await createFixture(); // MockGuiConfigService defaults `copilotEnabled` to false. expect(component.copilotEnabled).toBe(false); }); }); }); ================================================ FILE: frontend/src/app/workspace/component/workspace.component.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Location, NgIf } from "@angular/common"; import { AfterViewInit, ChangeDetectorRef, Component, HostListener, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef, } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { UserService } from "../../common/service/user/user.service"; import { WorkflowPersistService } from "../../common/service/workflow-persist/workflow-persist.service"; import { Workflow } from "../../common/type/workflow"; import { OperatorMetadataService } from "../service/operator-metadata/operator-metadata.service"; import { UndoRedoService } from "../service/undo-redo/undo-redo.service"; import { WorkflowActionService } from "../service/workflow-graph/model/workflow-action.service"; import { NzMessageService } from "ng-zorro-antd/message"; import { debounceTime, distinctUntilChanged, filter, switchMap, throttleTime } from "rxjs/operators"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { forkJoin, of } from "rxjs"; import { isDefined } from "../../common/util/predicate"; import { NotificationService } from "src/app/common/service/notification/notification.service"; import { WorkflowConsoleService } from "../service/workflow-console/workflow-console.service"; import { OperatorReuseCacheStatusService } from "../service/workflow-status/operator-reuse-cache-status.service"; import { CodeEditorService } from "../service/code-editor/code-editor.service"; import { WorkflowMetadata } from "src/app/dashboard/type/workflow-metadata.interface"; import { EntityType, HubService } from "../../hub/service/hub.service"; import { THROTTLE_TIME_MS } from "../../hub/component/workflow/detail/hub-workflow-detail.component"; import { WorkflowCompilingService } from "../service/compile-workflow/workflow-compiling.service"; import { DASHBOARD_USER_WORKSPACE } from "../../app-routing.constant"; import { GuiConfigService } from "../../common/service/gui-config.service"; import { checkIfWorkflowBroken } from "../../common/util/workflow-check"; import { NzSpinComponent } from "ng-zorro-antd/spin"; import { ResultPanelComponent } from "./result-panel/result-panel.component"; import { WorkflowEditorComponent } from "./workflow-editor/workflow-editor.component"; import { MenuComponent } from "./menu/menu.component"; import { MiniMapComponent } from "./workflow-editor/mini-map/mini-map.component"; import { LeftPanelComponent } from "./left-panel/left-panel.component"; import { AgentPanelComponent } from "./agent/agent-panel/agent-panel.component"; import { PropertyEditorComponent } from "./property-editor/property-editor.component"; import { FormlyRepeatDndComponent } from "../../common/formly/repeat-dnd/repeat-dnd.component"; export const SAVE_DEBOUNCE_TIME_IN_MS = 5000; @UntilDestroy() @Component({ selector: "texera-workspace", templateUrl: "./workspace.component.html", styleUrls: ["./workspace.component.scss"], providers: [ // uncomment this line for manual testing without opening backend server // { provide: OperatorMetadataService, useClass: StubOperatorMetadataService }, ], imports: [ NzSpinComponent, ResultPanelComponent, WorkflowEditorComponent, MenuComponent, MiniMapComponent, LeftPanelComponent, NgIf, AgentPanelComponent, PropertyEditorComponent, FormlyRepeatDndComponent, ], }) export class WorkspaceComponent implements AfterViewInit, OnInit, OnDestroy { public pid?: number = undefined; public writeAccess: boolean = false; public isLoading: boolean = false; @ViewChild("codeEditor", { read: ViewContainerRef }) codeEditorViewRef!: ViewContainerRef; /** * Optional agent ID to activate when the workspace loads. * When provided (from agent dashboard), the agent panel will open * and connect to this agent automatically. */ @Input() agentIdToActivate?: string; /** * Flag to ensure auto persist is registered only once. This prevents multiple * subscriptions and avoids accidental persistence of an empty workflow * before the actual workflow is loaded from backend. */ private autoPersistRegistered = false; constructor( private userService: UserService, // list additional 3 services in constructor so they are initialized even if no one use them directly // TODO: make their lifecycle better private workflowCompilingService: WorkflowCompilingService, private workflowConsoleService: WorkflowConsoleService, private operatorReuseCacheStatusService: OperatorReuseCacheStatusService, // end of additional services private undoRedoService: UndoRedoService, private workflowPersistService: WorkflowPersistService, private workflowActionService: WorkflowActionService, private location: Location, private route: ActivatedRoute, private operatorMetadataService: OperatorMetadataService, private message: NzMessageService, private router: Router, private notificationService: NotificationService, private hubService: HubService, private codeEditorService: CodeEditorService, private config: GuiConfigService, private changeDetectorRef: ChangeDetectorRef ) {} ngOnInit() { /** * On initialization of the workspace, there are two possibilities regarding which component has * routed to this component: * * 1. Routed to this component from within UserProjectSection component * - track the pid identifying that project * - upon persisting of a workflow, must also ensure it is also added to the project * * 2. Routed to this component from SavedWorkflowSection component * - there is no related project, parseInt will return NaN. * - NaN || undefined will result in undefined. */ this.pid = parseInt(this.route.snapshot.queryParams.pid) || undefined; this.workflowActionService.setHighlightingEnabled(true); } ngAfterViewInit(): void { /** * On initialization of the workspace, there could be two cases: * * 1. Accessed by URL `/`, no workflow is in the URL (Cold Start): - - A new `WorkflowActionService.DEFAULT_WORKFLOW` is created, which is an empty workflow with undefined id. * - After an Auto-persist being triggered by a WorkflowAction event, it will create a new workflow in the database * and update the URL with its new ID from database. * 2. Accessed by URL `/workflow/:id` (refresh manually, or redirected from dashboard workflow list): * - It will retrieve the workflow from database with the given ID. Because it has an ID, it will be linked to the database * - Auto-persist will be triggered upon all workspace events. * * WorkflowActionService is the single source of the workflow representation. WorkflowPersistService reflects * changes from WorkflowActionService. */ // clear the current workspace, reset as `WorkflowActionService.DEFAULT_WORKFLOW` this.workflowActionService.resetAsNewWorkflow(); // if a workflow id is present in the route, display loading spinner immediately while loading const widInRoute = this.route.snapshot.params.id; if (widInRoute) { this.isLoading = true; this.workflowActionService.disableWorkflowModification(); } this.onWIDChange(); this.updateViewCount(); this.registerLoadOperatorMetadata(); this.codeEditorService.vc = this.codeEditorViewRef; } @HostListener("window:beforeunload") ngOnDestroy() { if (this.userService.isLogin() && this.workflowPersistService.isWorkflowPersistEnabled()) { const workflow = this.workflowActionService.getWorkflow(); this.workflowPersistService.persistWorkflow(workflow).pipe(untilDestroyed(this)).subscribe(); } this.codeEditorViewRef.clear(); this.workflowActionService.clearWorkflow(); } registerAutoPersistWorkflow(): void { // make sure it is only registered once if (this.autoPersistRegistered) { return; } this.autoPersistRegistered = true; this.workflowActionService .workflowChanged() .pipe(debounceTime(SAVE_DEBOUNCE_TIME_IN_MS)) .pipe(untilDestroyed(this)) .subscribe(() => { if (this.userService.isLogin() && this.workflowPersistService.isWorkflowPersistEnabled()) { this.workflowPersistService .persistWorkflow(this.workflowActionService.getWorkflow()) .pipe(untilDestroyed(this)) .subscribe((updatedWorkflow: Workflow) => { if (this.workflowActionService.getWorkflowMetadata().wid !== updatedWorkflow.wid) { this.location.go(`${DASHBOARD_USER_WORKSPACE}/${updatedWorkflow.wid}`); } this.workflowActionService.setWorkflowMetadata(updatedWorkflow); }); // to sync up with the updated information, such as workflow.wid } }); } loadWorkflowWithId(wid: number): void { // disable the workspace until the workflow is fetched from the backend this.isLoading = true; this.workflowActionService.disableWorkflowModification(); forkJoin({ operatorMetadata: this.operatorMetadataService.getOperatorMetadata(), workflow: this.workflowPersistService.retrieveWorkflow(wid), }) .pipe(untilDestroyed(this)) .subscribe( ({ workflow }) => { if (checkIfWorkflowBroken(workflow)) { this.notificationService.error( "Sorry! The workflow is broken and cannot be persisted. Please contact the system admin." ); } this.workflowActionService.setNewSharedModel(wid, this.userService.getCurrentUser()); // remember URL fragment const fragment = this.route.snapshot.fragment; // load the fetched workflow this.workflowActionService.reloadWorkflow(workflow); this.workflowActionService.enableWorkflowModification(); // set the URL fragment to previous value // because reloadWorkflow will highlight/unhighlight all elements // which will change the URL fragment this.router.navigate([], { relativeTo: this.route, fragment: fragment !== null ? fragment : undefined, preserveFragment: false, }); // highlight the operator, comment box, or link in the URL fragment if (fragment) { if (this.workflowActionService.getTexeraGraph().hasElementWithID(fragment)) { this.workflowActionService.highlightElements(false, fragment); } else { this.notificationService.error(`Element ${fragment} doesn't exist`); // remove the fragment from the URL this.router.navigate([], { relativeTo: this.route }); } } // clear stack this.undoRedoService.clearUndoStack(); this.undoRedoService.clearRedoStack(); this.setLoadingState(false); this.registerAutoPersistWorkflow(); this.triggerCenter(); }, () => { this.workflowActionService.resetAsNewWorkflow(); // enable workspace for modification this.workflowActionService.enableWorkflowModification(); // clear stack this.undoRedoService.clearUndoStack(); this.undoRedoService.clearRedoStack(); this.message.error("You don't have access to this workflow, please log in with an appropriate account"); this.setLoadingState(false); } ); } registerLoadOperatorMetadata() { const wid = this.route.snapshot.params.id; // load workflow with wid if presented in the URL if (wid) { // show loading spinner right away while waiting for workflow to load this.isLoading = true; // temporarily disable modification to prevent editing an empty workflow before real data is loaded this.workflowActionService.disableWorkflowModification(); this.loadWorkflowWithId(Number(wid)); return; } this.operatorMetadataService .getOperatorMetadata() .pipe(untilDestroyed(this)) .subscribe(() => { // no workflow to load; directly register auto persist for brand-new workflow this.registerAutoPersistWorkflow(); }); } onWIDChange() { this.workflowActionService .workflowMetaDataChanged() .pipe( switchMap(() => of(this.workflowActionService.getWorkflowMetadata())), filter((metadata: WorkflowMetadata) => isDefined(metadata.wid)), distinctUntilChanged() ) .pipe(untilDestroyed(this)) .subscribe((metadata: WorkflowMetadata) => { this.writeAccess = !metadata.readonly; }); } updateViewCount() { let wid = this.route.snapshot.params.id; let uid = this.userService.getCurrentUser()?.uid; this.hubService .postView(wid, uid ? uid : 0, EntityType.Workflow) .pipe(throttleTime(THROTTLE_TIME_MS)) .pipe(untilDestroyed(this)) .subscribe(); } public triggerCenter(): void { this.workflowActionService.getTexeraGraph().triggerCenterEvent(); } private setLoadingState(isLoading: boolean): void { this.isLoading = isLoading; this.changeDetectorRef.detectChanges(); } public get copilotEnabled(): boolean { return this.config.env.copilotEnabled; } } ================================================ FILE: frontend/src/app/workspace/service/agent/agent-types.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import type { ModelMessage } from "ai"; // Re-export ModelMessage for use in other modules export type { ModelMessage }; /** * Operator access information for a tool call. * Tracks which operators were viewed, added, or modified. */ export interface ToolOperatorAccess { viewedOperatorIds: string[]; addedOperatorIds: string[]; modifiedOperatorIds: string[]; } /** * Agent lifecycle state. */ export enum AgentState { UNAVAILABLE = "Unavailable", AVAILABLE = "Available", GENERATING = "Generating", STOPPING = "Stopping", } /** * ReActStep - Represents a single reasoning and acting step in the agent's response. * Each step contains the agent's reasoning text, tool calls, results, and metadata. */ export interface ReActStep { messageId: string; stepId: number; timestamp: Date; role: "user" | "agent"; content: string; isBegin: boolean; isEnd: boolean; toolCalls?: any[]; toolResults?: any[]; usage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number; cachedInputTokens?: number; }; /** Messages array sent to the LLM for this step (only when context optimization is active) */ inputMessages?: any[]; // Map from tool call index to operator access information operatorAccess?: Map; // Versioning fields: /** Unique step ID string for tree references */ id: string; /** Parent step ID — forms the version tree */ parentId?: string; /** Source of the user message: "chat" or "feedback" */ messageSource?: string; /** Workflow state before this step executed */ beforeWorkflowContent?: any; /** Workflow state after this step executed */ afterWorkflowContent?: any; } ================================================ FILE: frontend/src/app/workspace/service/agent/agent.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable, NgZone } from "@angular/core"; import { HttpClient, HttpHeaders } from "@angular/common/http"; import { Observable, Subject, BehaviorSubject, catchError, filter, map, of, shareReplay, defer, throwError, interval, switchMap, takeUntil, } from "rxjs"; import { NotificationService } from "../../../common/service/notification/notification.service"; import { WorkflowPersistService } from "../../../common/service/workflow-persist/workflow-persist.service"; import { AppSettings } from "../../../common/app-setting"; import { AuthService } from "../../../common/service/user/auth.service"; import { AgentState, ReActStep, ModelMessage } from "./agent-types"; import { Workflow, WorkflowContent } from "../../../common/type/workflow"; import { ComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; /** * Agent settings for API (serializable format). */ export interface AgentSettingsApi { /** Maximum character limit for operator results (uses symmetric truncation) */ maxOperatorResultCharLimit?: number; /** Maximum character limit per cell (truncates individual cell values beyond this limit) */ maxOperatorResultCellCharLimit?: number; /** Serialization mode for operator results */ operatorResultSerializationMode?: "tsv"; /** Tool execution timeout in seconds */ toolTimeoutSeconds?: number; /** Workflow execution timeout in minutes */ executionTimeoutMinutes?: number; /** List of disabled tool names */ disabledTools?: string[]; /** Maximum number of steps per message */ maxSteps?: number; /** List of allowed operator types (empty = all operators allowed) */ allowedOperatorTypes?: string[]; } /** * Agent information for tracking created agents (API version). */ export interface AgentInfo { id: string; name: string; modelType: string; isBaselineMode: boolean; createdAt: Date; /** State is fetched from API */ state?: AgentState; delegate?: { userInfo: { uid: number; name: string; email: string; role: string }; workflowId?: number; workflowName?: string; }; /** Current agent settings */ settings?: AgentSettingsApi; } /** * Available model types for agent creation. */ export interface ModelType { id: string; name: string; description: string; icon: string; } /** * API response types */ /** * Summary of operator execution results for annotation display. */ export interface OperatorResultSummary { state: string; inputTuples: number; outputTuples: number; inputPortShapes?: { portIndex: number; rows: number; columns: number }[]; outputColumns?: number; error?: string; warnings?: string[]; consoleLogCount?: number; totalRowCount?: number; sampleRecords?: Record[]; resultStatistics?: Record; } interface ApiAgentInfo { id: string; name: string; modelType: string; state: string; createdAt: string; delegate?: { userToken: string; userInfo: { uid: number; name: string; email: string; role: string }; workflowId?: number; workflowName?: string; }; settings?: AgentSettingsApi; } interface ApiAgentListResponse { agents: ApiAgentInfo[]; } interface ApiReActStepsResponse { steps: any[]; state: string; } interface ApiMessageResponse { response: string; steps: any[]; usage: { inputTokens: number; outputTokens: number; totalTokens: number }; stats: any; stopped: boolean; error?: string; workflow: any; } interface LiteLLMModel { id: string; object: string; created: number; owned_by: string; } interface LiteLLMModelsResponse { data: LiteLLMModel[]; object: string; } /** * Agent state tracking for observables */ interface AgentStateTracking { stateSubject: BehaviorSubject; reActStepsSubject: BehaviorSubject; hoveredMessageSubject: BehaviorSubject<{ viewedOperatorIds: string[]; addedOperatorIds: string[]; modifiedOperatorIds: string[]; }>; /** Current HEAD step ID in the version tree */ headIdSubject: BehaviorSubject; workflowSubject: BehaviorSubject; workflowId?: number; stopPolling$: Subject; /** When true, workflow updates come from WS — polling is suppressed */ wsWorkflowActive: boolean; /** WebSocket connection for real-time updates */ websocket?: WebSocket; /** Whether this agent is currently active (tab selected) */ isActive: boolean; } /** * Manages the workspace's agents via the agent-service HTTP/WebSocket * API. Owns the local agent list, per-agent state tracking (ReAct steps, HEAD * pointer, workflow snapshot), and the canvas annotation toggles consumed by * workflow-editor. */ @Injectable({ providedIn: "root", }) export class AgentService { /** Base URL for agent service API */ private readonly AGENT_API_BASE = "/api"; /** Local cache of agent info */ private agents = new Map(); /** State tracking for each agent */ private agentStateTracking = new Map(); /** Subject for agent list changes */ private agentChangeSubject = new Subject(); public agentChange$ = this.agentChangeSubject.asObservable(); /** Cached model types */ private modelTypes$: Observable | null = null; // ============================================================================ // Canvas annotation state (port shapes, step badges, scroll-to-step) // ============================================================================ /** Whether to show output port shapes (rows, columns) on operators */ private showPortShapesSubject = new BehaviorSubject(true); public showPortShapes$ = this.showPortShapesSubject.asObservable(); /** Subject emitting scroll-to-step requests */ private scrollToStepSubject = new Subject<{ agentId: string; messageId: string; stepId: number }>(); public scrollToStep$ = this.scrollToStepSubject.asObservable(); constructor( private http: HttpClient, private notificationService: NotificationService, private workflowPersistService: WorkflowPersistService, private ngZone: NgZone, private computingUnitStatusService: ComputingUnitStatusService ) { // Sync local cache with backend on service initialization // This handles cases where the backend was restarted this.syncAgentsWithBackend(); } /** * Build HTTP headers for agent-service requests. * Includes X-Agent-Workflow-Id for consistent hash routing in k8s. */ private agentHeaders(agentId?: string): { headers: HttpHeaders } { let headers = new HttpHeaders(); if (agentId) { const wid = this.agentStateTracking.get(agentId)?.workflowId; if (wid !== undefined) { headers = headers.set("X-Agent-Workflow-Id", String(wid)); } } return { headers }; } /** * Sync local agent cache with the backend. * Removes any agents from local cache that no longer exist on the backend. * This is called on service initialization and handles backend restarts. */ private syncAgentsWithBackend(): void { this.http .get(`${this.AGENT_API_BASE}/agents`) .pipe(catchError(() => of({ agents: [] }))) .subscribe(response => { const backendAgentIds = new Set(response.agents.map(a => a.id)); // Remove any local agents that don't exist on the backend const localAgentIds = Array.from(this.agents.keys()); for (const localId of localAgentIds) { if (!backendAgentIds.has(localId)) { this.agents.delete(localId); this.stopStatePolling(localId); } } // Update local cache with backend state for (const apiAgent of response.agents) { const existingAgent = this.agents.get(apiAgent.id); if (existingAgent) { // Update state from backend existingAgent.state = this.mapStateToAgentState(apiAgent.state); const tracking = this.agentStateTracking.get(apiAgent.id); if (tracking) { tracking.stateSubject.next(existingAgent.state); } } } // Notify subscribers if there were changes if (localAgentIds.length !== this.agents.size) { this.agentChangeSubject.next(); } }); } /** * Convert API state string to AgentState enum */ private mapStateToAgentState(state: string): AgentState { switch (state) { case "AVAILABLE": return AgentState.AVAILABLE; case "GENERATING": return AgentState.GENERATING; case "STOPPING": return AgentState.STOPPING; case "UNAVAILABLE": default: return AgentState.UNAVAILABLE; } } /** * Convert API ReActStep to frontend ReActStep format. * The backend now sends ReActSteps in the aligned format, so minimal conversion is needed. */ private convertApiReActStep(apiStep: any): ReActStep { // Convert operator access from object to Map if present let operatorAccess: Map | undefined; if (apiStep.operatorAccess) { operatorAccess = new Map(); for (const [key, value] of Object.entries(apiStep.operatorAccess)) { operatorAccess.set(parseInt(key), value); } } return { messageId: apiStep.messageId, stepId: apiStep.stepId || 0, timestamp: new Date(apiStep.timestamp), role: apiStep.role || "agent", content: apiStep.content || "", isBegin: apiStep.isBegin || false, isEnd: apiStep.isEnd || false, toolCalls: apiStep.toolCalls, toolResults: apiStep.toolResults?.map((tr: any) => ({ ...tr, // Ensure compatibility: backend uses 'output', frontend expects 'result' or 'output' result: tr.output || tr.result, output: tr.output || tr.result, })), usage: apiStep.usage, inputMessages: apiStep.inputMessages, operatorAccess, // Versioning fields id: apiStep.id || `${apiStep.messageId}-${apiStep.stepId || 0}`, parentId: apiStep.parentId, messageSource: apiStep.messageSource, beforeWorkflowContent: apiStep.beforeWorkflowContent, afterWorkflowContent: apiStep.afterWorkflowContent, }; } /** * Get or create state tracking for an agent. * If tracking exists but doesn't have workflowId and one is provided, updates it. * Note: WebSocket connection is NOT started automatically - call activateAgent() to connect. */ private getOrCreateStateTracking(agentId: string, workflowId?: number): AgentStateTracking { let tracking = this.agentStateTracking.get(agentId); if (!tracking) { tracking = { stateSubject: new BehaviorSubject(AgentState.UNAVAILABLE), reActStepsSubject: new BehaviorSubject([]), hoveredMessageSubject: new BehaviorSubject<{ viewedOperatorIds: string[]; addedOperatorIds: string[]; modifiedOperatorIds: string[]; }>({ viewedOperatorIds: [], addedOperatorIds: [], modifiedOperatorIds: [] }), headIdSubject: new BehaviorSubject(null), workflowSubject: new BehaviorSubject(null), workflowId, stopPolling$: new Subject(), wsWorkflowActive: false, isActive: false, }; this.agentStateTracking.set(agentId, tracking); // Note: WebSocket connection is NOT started here - lazy initialization via activateAgent() } else if (workflowId && !tracking.workflowId) { // Tracking exists but doesn't have workflowId - update it tracking.workflowId = workflowId; } return tracking; } /** * Start workflow polling for an existing tracking. * Polls workflow content from backend database every second. * Polling is suppressed when the agent service provides workflow via WebSocket. */ private startWorkflowPolling(tracking: AgentStateTracking): void { if (!tracking.workflowId) return; const wid = tracking.workflowId; interval(1000) .pipe( filter(() => !tracking.wsWorkflowActive), switchMap(() => this.workflowPersistService.retrieveWorkflow(wid).pipe(catchError(() => of(null)))), takeUntil(tracking.stopPolling$) ) .subscribe(workflow => { if (workflow) { this.ngZone.run(() => { tracking.workflowSubject.next(workflow); }); } }); } /** * Start WebSocket connection for real-time ReActSteps updates */ private startStatePolling(agentId: string, tracking: AgentStateTracking): void { // Build WebSocket URL const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const wsUrl = `${wsProtocol}//${window.location.host}${this.AGENT_API_BASE}/agents/${agentId}/react`; const ws = new WebSocket(wsUrl); tracking.websocket = ws; ws.onmessage = event => { try { const message = JSON.parse(event.data); this.ngZone.run(() => { this.handleWebSocketMessage(agentId, tracking, message); }); } catch (error) { console.error("Failed to parse agent WebSocket message:", error); } }; ws.onerror = error => { console.error(`Agent ${agentId} WebSocket error:`, error); }; ws.onclose = event => { // Only clean up if this is still the current websocket; otherwise a rapid // deactivate/reactivate may have already swapped it. if (tracking.websocket === ws) { tracking.websocket = undefined; if (event.code !== 1000) { tracking.stateSubject.next(AgentState.UNAVAILABLE); } } }; // Start workflow polling if workflowId is set this.startWorkflowPolling(tracking); } /** * Handle incoming WebSocket messages */ private handleWebSocketMessage(agentId: string, tracking: AgentStateTracking, message: any): void { switch (message.type) { case "init": // Initial state and steps if (message.state) { tracking.stateSubject.next(this.mapStateToAgentState(message.state)); } if (message.steps && Array.isArray(message.steps)) { const steps = message.steps.map((s: any) => this.convertApiReActStep(s)); tracking.reActStepsSubject.next(steps); } // Handle initial HEAD pointer if (message.headId !== undefined) { tracking.headIdSubject.next(message.headId); } // Handle initial workflow content from agent service (ground truth) if (message.workflowContent) { tracking.wsWorkflowActive = true; const workflow: Workflow = { ...(message.workflowMetadata || tracking.workflowSubject.getValue() || {}), content: message.workflowContent, }; tracking.workflowSubject.next(workflow as Workflow); } // Handle initial operator results if (message.operatorResults) { this.updateOperatorResultSummaries(message.operatorResults); } break; case "step": // New step received - update existing step or append new one if (message.step) { const convertedStep = this.convertApiReActStep(message.step); const currentSteps = tracking.reActStepsSubject.getValue(); // Check if step with same messageId and stepId already exists const existingIndex = currentSteps.findIndex( s => s.messageId === convertedStep.messageId && s.stepId === convertedStep.stepId ); if (existingIndex >= 0) { // Update existing step (e.g., when isEnd changes from false to true) const updatedSteps = [...currentSteps]; updatedSteps[existingIndex] = convertedStep; tracking.reActStepsSubject.next(updatedSteps); } else { // Append new step tracking.reActStepsSubject.next([...currentSteps, convertedStep]); } // Advance HEAD to the step's id (each step advances HEAD) if (convertedStep.id) { tracking.headIdSubject.next(convertedStep.id); } // If the step has afterWorkflowContent, update the workflow if (convertedStep.afterWorkflowContent) { tracking.wsWorkflowActive = true; const existingWorkflow = tracking.workflowSubject.getValue(); const workflow = { ...(existingWorkflow || {}), content: convertedStep.afterWorkflowContent, } as Workflow; tracking.workflowSubject.next(workflow); } } break; case "state": // State update if (message.state) { tracking.stateSubject.next(this.mapStateToAgentState(message.state)); } break; case "complete": // Message processing complete if (message.state) { tracking.stateSubject.next(this.mapStateToAgentState(message.state)); } // Update operator results on completion if (message.operatorResults) { this.updateOperatorResultSummaries(message.operatorResults); } break; case "headChange": // HEAD moved (checkout) — update HEAD, visible steps, and workflow if (message.headId !== undefined) { tracking.headIdSubject.next(message.headId); } if (message.steps && Array.isArray(message.steps)) { const steps = message.steps.map((s: any) => this.convertApiReActStep(s)); tracking.reActStepsSubject.next(steps); } // Update workflow content from agent service (ground truth) if (message.workflowContent) { tracking.wsWorkflowActive = true; const workflow: Workflow = { ...(message.workflowMetadata || tracking.workflowSubject.getValue() || {}), content: message.workflowContent, }; tracking.workflowSubject.next(workflow as Workflow); } // Update operator results on HEAD change if (message.operatorResults) { this.updateOperatorResultSummaries(message.operatorResults); } break; case "error": // Error occurred console.error(`Agent ${agentId} error:`, message.error); // If agent not found on backend (e.g., backend restarted), clean up local state if (message.error === "Agent not found") { this.agents.delete(agentId); tracking.stateSubject.next(AgentState.UNAVAILABLE); this.stopStatePolling(agentId); this.agentChangeSubject.next(); this.notificationService.warning("Agent was removed (backend may have restarted)"); } else { this.notificationService.error(message.error || "Agent error occurred"); } break; default: console.warn("Unknown agent WebSocket message type:", message.type); } } /** * Stop WebSocket connection and polling for an agent (internal cleanup) */ private stopStatePolling(agentId: string): void { const tracking = this.agentStateTracking.get(agentId); if (tracking) { // Close WebSocket if open if (tracking.websocket) { tracking.websocket.close(); tracking.websocket = undefined; } tracking.stopPolling$.next(); tracking.stopPolling$.complete(); this.agentStateTracking.delete(agentId); } } /** * Activate an agent - starts WebSocket connection and workflow polling. * Call this when the user selects an agent's tab. * @param agentId The agent to activate * @returns true if activation succeeded, false otherwise */ public activateAgent(agentId: string): boolean { const agent = this.agents.get(agentId); if (!agent) { return false; } const tracking = this.getOrCreateStateTracking(agentId, agent.delegate?.workflowId); if (tracking.isActive && tracking.websocket) { return true; } tracking.isActive = true; if (!tracking.websocket || tracking.websocket.readyState !== WebSocket.OPEN) { this.startStatePolling(agentId, tracking); } return true; } /** * Deactivate an agent - closes WebSocket connection and stops workflow polling. * Call this when the user switches away from an agent's tab. * @param agentId The agent to deactivate */ public deactivateAgent(agentId: string): void { const tracking = this.agentStateTracking.get(agentId); if (!tracking) { return; } // Already inactive if (!tracking.isActive) { return; } tracking.isActive = false; // Close WebSocket connection if (tracking.websocket) { tracking.websocket.close(); tracking.websocket = undefined; } // Stop workflow polling; recreate stopPolling$ for future activations. tracking.stopPolling$.next(); tracking.stopPolling$ = new Subject(); } /** * Check if an agent is currently active (has WebSocket connection). */ public isAgentActivelyConnected(agentId: string): boolean { const tracking = this.agentStateTracking.get(agentId); return tracking?.isActive === true && tracking?.websocket?.readyState === WebSocket.OPEN; } /** * Get all agents that are currently actively connected (have open WebSocket). * @returns Array of agent IDs that are actively connected */ public getActivelyConnectedAgentIds(): string[] { const connectedIds: string[] = []; for (const [agentId, tracking] of this.agentStateTracking) { if (tracking.isActive && tracking.websocket?.readyState === WebSocket.OPEN) { connectedIds.push(agentId); } } return connectedIds; } /** * Get the workflow ID associated with an agent. */ public getAgentWorkflowId(agentId: string): number | undefined { const agent = this.agents.get(agentId); return agent?.delegate?.workflowId; } /** * Create a new agent with the specified model type. * Uses the user's current auth token for delegate mode. * @param modelType - The LLM model type to use * @param customName - Optional custom name for the agent * @param workflowId - Optional workflow ID for delegate mode */ public createAgent(modelType: string, customName?: string, workflowId?: number): Observable { return defer(() => { const userToken = AuthService.getAccessToken(); const body: any = { modelType, name: customName, }; // Include user token and workflowId for delegate mode if available if (userToken) { body.userToken = userToken; if (workflowId !== undefined) { body.workflowId = workflowId; } // Include computing unit ID for workflow execution const selectedUnit = this.computingUnitStatusService.getSelectedComputingUnitValue(); if (selectedUnit) { body.computingUnitId = selectedUnit.computingUnit.cuid; } } return this.http.post(`${this.AGENT_API_BASE}/agents`, body).pipe( map(response => { const agentInfo: AgentInfo = { id: response.id, name: response.name, modelType: response.modelType, isBaselineMode: false, createdAt: new Date(response.createdAt), state: this.mapStateToAgentState(response.state), delegate: response.delegate ? { userInfo: response.delegate.userInfo, workflowId: response.delegate.workflowId, workflowName: response.delegate.workflowName, } : undefined, settings: response.settings, }; this.agents.set(response.id, agentInfo); // Pass workflowId to enable workflow polling from backend database const tracking = this.getOrCreateStateTracking(response.id, workflowId); // Set the initial state from the API response (agent is AVAILABLE after creation) tracking.stateSubject.next(agentInfo.state || AgentState.AVAILABLE); this.agentChangeSubject.next(); return agentInfo; }), catchError((error: unknown) => { const err = error as { error?: { error?: string }; message?: string }; const errorMsg = err.error?.error || err.message || "Failed to create agent"; this.notificationService.error(errorMsg); return throwError(() => new Error(errorMsg)); }) ); }); } /** * Get an agent by ID. */ public getAgent(agentId: string): Observable { return defer(() => { const agent = this.agents.get(agentId); if (agent) { return of(agent); } // Fetch from API if not in cache return this.http.get(`${this.AGENT_API_BASE}/agents/${agentId}`, this.agentHeaders(agentId)).pipe( map(response => { const agentInfo: AgentInfo = { id: response.id, name: response.name, modelType: response.modelType, isBaselineMode: false, createdAt: new Date(response.createdAt), state: this.mapStateToAgentState(response.state), delegate: response.delegate ? { userInfo: response.delegate.userInfo, workflowId: response.delegate.workflowId, workflowName: response.delegate.workflowName, } : undefined, settings: response.settings, }; this.agents.set(response.id, agentInfo); return agentInfo; }), catchError(() => throwError(() => new Error(`Agent with ID ${agentId} not found`))) ); }); } /** * Get all agents. * Also syncs local cache with backend - removes any stale agents that no longer exist on the backend. */ public getAllAgents(): Observable { return this.http.get(`${this.AGENT_API_BASE}/agents`).pipe( map(response => { const agents = response.agents.map(a => ({ id: a.id, name: a.name, modelType: a.modelType, isBaselineMode: false, createdAt: new Date(a.createdAt), state: this.mapStateToAgentState(a.state), delegate: a.delegate ? { userInfo: a.delegate.userInfo, workflowId: a.delegate.workflowId, workflowName: a.delegate.workflowName, } : undefined, settings: a.settings, })); // Build a set of backend agent IDs for quick lookup const backendAgentIds = new Set(agents.map(a => a.id)); // Remove any local agents that don't exist on the backend // This handles the case when agent-service restarts const localAgentIds = Array.from(this.agents.keys()); for (const localId of localAgentIds) { if (!backendAgentIds.has(localId)) { this.agents.delete(localId); this.stopStatePolling(localId); } } // Update local cache with agents from backend for (const agent of agents) { this.agents.set(agent.id, agent); } return agents; }), catchError(() => of(Array.from(this.agents.values()))) ); } /** * Delete an agent by ID. */ public deleteAgent(agentId: string): Observable { return this.http .delete<{ deleted: boolean }>(`${this.AGENT_API_BASE}/agents/${agentId}`, this.agentHeaders(agentId)) .pipe( map(response => { if (response.deleted) { this.agents.delete(agentId); this.stopStatePolling(agentId); this.agentChangeSubject.next(); } return response.deleted; }), catchError(() => { this.agents.delete(agentId); this.stopStatePolling(agentId); this.agentChangeSubject.next(); return of(true); }) ); } /** * Fetch available models from the API. */ public fetchModelTypes(): Observable { if (!this.modelTypes$) { this.modelTypes$ = this.http.get(`${AppSettings.getApiEndpoint()}/models`).pipe( map(response => response.data.map((model: LiteLLMModel) => ({ id: model.id, name: this.formatModelName(model.id), description: `Model: ${model.id}`, icon: "robot", })) ), catchError((error: unknown) => { console.error("Failed to fetch models from API:", error); return of([]); }), shareReplay(1) ); } return this.modelTypes$; } private formatModelName(modelId: string): string { return modelId .split("-") .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(" "); } /** * Get the count of active agents. */ public getAgentCount(): Observable { return of(this.agents.size); } /** * Send a message to an agent via WebSocket. * The message is sent through the WebSocket connection for real-time streaming. */ public sendMessage(agentId: string, message: string, messageSource: "chat" | "feedback" = "chat"): void { const agent = this.agents.get(agentId); if (!agent) { this.notificationService.error(`Agent with ID ${agentId} not found`); return; } const tracking = this.agentStateTracking.get(agentId); if (!tracking || !tracking.websocket || tracking.websocket.readyState !== WebSocket.OPEN) { this.notificationService.error("WebSocket connection not available"); return; } const wsMessage = { type: "message", content: message, messageSource, }; try { tracking.websocket.send(JSON.stringify(wsMessage)); } catch (error) { console.error("Failed to send message to agent:", error); this.notificationService.error("Failed to send message"); } } /** * Get the ReActSteps observable stream. */ public getReActStepsObservable(agentId: string): Observable { const tracking = this.getOrCreateStateTracking(agentId); return tracking.reActStepsSubject.asObservable(); } /** * Get the current ReActSteps. */ public getReActSteps(agentId: string): Observable { return this.http .get(`${this.AGENT_API_BASE}/agents/${agentId}/react-steps`, this.agentHeaders(agentId)) .pipe( map(response => response.steps.map((s: any) => this.convertApiReActStep(s))), catchError(() => of([])) ); } /** * Clear all messages for an agent. */ public clearMessages(agentId: string): void { this.http.post(`${this.AGENT_API_BASE}/agents/${agentId}/clear`, {}, this.agentHeaders(agentId)).subscribe({ next: () => { const tracking = this.agentStateTracking.get(agentId); if (tracking) { tracking.reActStepsSubject.next([]); } }, error: (error: unknown) => { console.error(`Error clearing messages for agent ${agentId}:`, error); }, }); } /** * Stop generation for an agent via WebSocket. */ public stopGeneration(agentId: string): void { const tracking = this.agentStateTracking.get(agentId); if (tracking?.websocket && tracking.websocket.readyState === WebSocket.OPEN) { // Send stop via WebSocket for immediate effect try { tracking.websocket.send(JSON.stringify({ type: "stop" })); } catch (error) { console.error("Failed to send stop command:", error); } } else { // Fallback to HTTP if WebSocket not available this.http.post(`${this.AGENT_API_BASE}/agents/${agentId}/stop`, {}, this.agentHeaders(agentId)).subscribe({ error: (error: unknown) => { console.error(`Error stopping agent ${agentId}:`, error); }, }); } } /** * Get the current state of an agent. */ public getAgentState(agentId: string): Observable { return defer(() => { const tracking = this.agentStateTracking.get(agentId); if (tracking) { return of(tracking.stateSubject.getValue()); } return of(AgentState.UNAVAILABLE); }); } /** * Get the state observable stream for an agent. */ public getAgentStateObservable(agentId: string): Observable { const tracking = this.getOrCreateStateTracking(agentId); return tracking.stateSubject.asObservable(); } /** * Check if an agent is connected. */ public isAgentConnected(agentId: string): Observable { return this.getAgentState(agentId).pipe(map(state => state !== AgentState.UNAVAILABLE)); } /** * Get HEAD step ID observable for an agent. */ public getHeadIdObservable(agentId: string): Observable { const tracking = this.getOrCreateStateTracking(agentId); return tracking.headIdSubject.asObservable(); } /** * Get current HEAD step ID for an agent. */ public getHeadId(agentId: string): string | null { const tracking = this.agentStateTracking.get(agentId); return tracking ? tracking.headIdSubject.getValue() : null; } /** * Checkout to a specific step (move HEAD, restore workflow). * The backend broadcasts headChange + visible steps via WebSocket to all clients. */ public checkoutStep(agentId: string, stepId: string): Observable { return this.http.post(`${this.AGENT_API_BASE}/agents/${agentId}/checkout`, { stepId }); } /** * Get visible steps for an agent (current snapshot). */ public getVisibleSteps(agentId: string): ReActStep[] { const tracking = this.agentStateTracking.get(agentId); return tracking ? tracking.reActStepsSubject.getValue() : []; } /** * Get system information for an agent (system prompt and tools). * Fetches from agent-service API. */ public getSystemInfo(agentId: string): Observable<{ systemPrompt: string; tools: Array<{ name: string; description: string; inputSchema: any; enabled: boolean }>; }> { return this.http .get<{ systemPrompt: string; tools: Array<{ name: string; description: string; inputSchema: any; enabled: boolean }>; }>(`${this.AGENT_API_BASE}/agents/${agentId}/system-info`, this.agentHeaders(agentId)) .pipe( catchError(() => of({ systemPrompt: "Unable to retrieve system prompt", tools: [], }) ) ); } /** * Set hovered message (local UI state). */ public setHoveredMessage(agentId: string, step: ReActStep | null): void { const tracking = this.agentStateTracking.get(agentId); if (tracking) { if (step && step.operatorAccess) { const viewedOperatorIds: string[] = []; const addedOperatorIds: string[] = []; const modifiedOperatorIds: string[] = []; step.operatorAccess.forEach(access => { viewedOperatorIds.push(...access.viewedOperatorIds); addedOperatorIds.push(...access.addedOperatorIds); modifiedOperatorIds.push(...access.modifiedOperatorIds); }); tracking.hoveredMessageSubject.next({ viewedOperatorIds: [...new Set(viewedOperatorIds)], addedOperatorIds: [...new Set(addedOperatorIds)], modifiedOperatorIds: [...new Set(modifiedOperatorIds)], }); } else { tracking.hoveredMessageSubject.next({ viewedOperatorIds: [], addedOperatorIds: [], modifiedOperatorIds: [], }); } } } /** * Get hovered message operators observable. */ public getHoveredMessageOperatorsObservable( agentId: string ): Observable<{ viewedOperatorIds: string[]; addedOperatorIds: string[]; modifiedOperatorIds: string[] }> { const tracking = this.getOrCreateStateTracking(agentId); return tracking.hoveredMessageSubject.asObservable(); } /** * Get ReActSteps that viewed or modified a specific operator. */ public getReActStepsByOperatorAccess( agentId: string, operatorId: string ): Observable<{ viewedBy: ReActStep[]; modifiedBy: ReActStep[] }> { return this.getReActSteps(agentId).pipe( map(allSteps => { const viewedBy: ReActStep[] = []; const modifiedBy: ReActStep[] = []; for (const step of allSteps) { if (step.operatorAccess) { step.operatorAccess.forEach(access => { if (access.viewedOperatorIds.includes(operatorId) && !viewedBy.includes(step)) { viewedBy.push(step); } if (access.modifiedOperatorIds.includes(operatorId) && !modifiedBy.includes(step)) { modifiedBy.push(step); } }); } } return { viewedBy, modifiedBy }; }) ); } /** * Get workflow observable for an agent. * This observable emits the full Workflow object from the backend database * whenever the agent's workflow changes. */ public getWorkflowObservable(agentId: string): Observable { const tracking = this.agentStateTracking.get(agentId); if (tracking) { return tracking.workflowSubject.asObservable(); } return of(null); } /** * Ensure workflow polling is started for an agent. * Call this when you have the workflowId but tracking may have been created without it. */ public ensureWorkflowPolling(agentId: string, workflowId: number): void { this.getOrCreateStateTracking(agentId, workflowId); } /** * Get agent settings. */ public getAgentSettings(agentId: string): Observable { return this.http .get(`${this.AGENT_API_BASE}/agents/${agentId}/settings`, this.agentHeaders(agentId)) .pipe( catchError(() => of({ maxOperatorResultCharLimit: 20000, maxOperatorResultCellCharLimit: 4000, toolTimeoutSeconds: 120, executionTimeoutMinutes: 10, disabledTools: [], maxSteps: 10, allowedOperatorTypes: [], }) ) ); } /** * Update agent settings. * Only provided values will be updated. */ public updateAgentSettings(agentId: string, settings: Partial): Observable { return this.http .patch( `${this.AGENT_API_BASE}/agents/${agentId}/settings`, settings, this.agentHeaders(agentId) ) .pipe( map(response => { // Update local cache if we have this agent const agent = this.agents.get(agentId); if (agent) { agent.settings = response; } return response; }), catchError((error: unknown) => { const err = error as { error?: { error?: string }; message?: string }; const errorMsg = err.error?.error || err.message || "Failed to update agent settings"; this.notificationService.error(errorMsg); return throwError(() => new Error(errorMsg)); }) ); } /** * Get all available operator types for an agent. */ public getAvailableOperatorTypes(agentId: string): Observable> { return this.http .get< Array<{ type: string; description: string }> >(`${this.AGENT_API_BASE}/agents/${agentId}/operator-types`, this.agentHeaders(agentId)) .pipe(catchError(() => of([]))); } // ============================================================================ // Context Filtering Methods // ============================================================================ /** * Get ReActSteps relevant to the specified operator IDs. * Fetches from the backend which filters steps based on which operators they affected. * * @param agentId - The agent ID * @param operatorIds - The operator IDs to filter by * @returns Observable with filtered ReActSteps */ public getStepsByOperatorIds(agentId: string, operatorIds: string[]): Observable<{ steps: ReActStep[] }> { return this.http .post<{ steps: ReActStep[]; }>(`${this.AGENT_API_BASE}/agents/${agentId}/steps-by-operators`, { operatorIds }, this.agentHeaders(agentId)) .pipe( map(response => ({ steps: response.steps.map((s: any) => this.convertApiReActStep(s)), })), catchError(() => of({ steps: [], }) ) ); } // ============================================================================ // Canvas annotation toggles // ============================================================================ /** * Toggle whether output port shapes are shown on operators. */ public togglePortShapes(show: boolean): void { this.showPortShapesSubject.next(show); } public getShowPortShapes(): boolean { return this.showPortShapesSubject.getValue(); } /** * Request scrolling to a specific step in the agent chat. */ public requestScrollToStep(agentId: string, messageId: string, stepId: number): void { this.scrollToStepSubject.next({ agentId, messageId, stepId }); } // ============================================================================ // Operator Result Annotation Methods // ============================================================================ /** Whether operator result annotations are currently visible */ private resultAnnotationsVisibleSubject = new BehaviorSubject(false); public resultAnnotationsVisible$ = this.resultAnnotationsVisibleSubject.asObservable(); /** Current operator result summaries (operatorId → summary) */ private operatorResultSummariesSubject = new BehaviorSubject>(new Map()); public operatorResultSummaries$ = this.operatorResultSummariesSubject.asObservable(); /** * Toggle operator result annotations on/off. * When toggling on, fetches the latest results from the active agent. */ public toggleResultAnnotations(agentId?: string): void { const newState = !this.resultAnnotationsVisibleSubject.getValue(); if (newState) { const id = agentId ?? this.getActivelyConnectedAgentIds()[0]; if (!id) { // No active agent — nothing to fetch return; } this.fetchOperatorResults(id); } else { this.resultAnnotationsVisibleSubject.next(false); } } /** * Update operator result summaries from a WebSocket or API response. */ private updateOperatorResultSummaries(results: Record): void { const summaries = new Map(); for (const [opId, data] of Object.entries(results)) { summaries.set(opId, data); } this.operatorResultSummariesSubject.next(summaries); } /** * Fetch operator results from the backend (fallback if WebSocket data not available). */ public fetchOperatorResults(agentId: string): void { this.http .get<{ results: Record }>( `${this.AGENT_API_BASE}/agents/${agentId}/operator-results`, this.agentHeaders(agentId) ) .pipe(catchError(() => of({ results: {} as Record }))) .subscribe(response => { this.updateOperatorResultSummaries(response.results); this.resultAnnotationsVisibleSubject.next(true); }); } /** * Get current result annotations visibility. */ public getResultAnnotationsVisible(): boolean { return this.resultAnnotationsVisibleSubject.getValue(); } } ================================================ FILE: frontend/src/app/workspace/service/ai-analyst/ai-analyst.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Define a response type for OpenAI API interface OpenAIResponse { choices: { message: { content: string; }; }[]; } import { Injectable } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { catchError, Observable, of } from "rxjs"; import { map } from "rxjs/operators"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { AppSettings } from "../../../common/app-setting"; const AI_ASSISTANT_API_BASE_URL = `${AppSettings.getApiEndpoint()}`; const api_Url_Is_Enabled = `${AI_ASSISTANT_API_BASE_URL}/aiassistant/isenabled`; const api_Url_Openai = `${AI_ASSISTANT_API_BASE_URL}/aiassistant/openai`; @Injectable({ providedIn: "root", }) /** * This class `AiAnalystService` is responsible for integrating with the AI Assistant feature to generate insightful comments * based on the provided prompts. It is mainly used for generating automated feedback or explanations for workflow components */ export class AiAnalystService { private isAIAssistantEnabled: boolean | null = null; constructor( private http: HttpClient, public workflowActionService: WorkflowActionService ) {} /** * Checks if the AI Assistant feature is enabled by sending a request to the API. * * @returns {Promise} A promise that resolves to a boolean indicating whether the AI Assistant is enabled. * Returns `false` if the request fails or the response is undefined. */ public isOpenAIEnabled(): Observable { if (this.isAIAssistantEnabled !== null) { return of(this.isAIAssistantEnabled); } return this.http.get(api_Url_Is_Enabled, { responseType: "text" }).pipe( map(response => { const isEnabled = response === "OpenAI"; return isEnabled; }), catchError(() => of(false)) ); } /** * Generates an insightful feedback for the given input prompt by utilizing the AI Assistant service. * * @param {string} inputPrompt - The operator information in JSON format, which will be used to generate the comment. * @returns {Promise} A promise that resolves to a string containing the generated comment or an error message * if the generation fails or the AI Assistant is not enabled. */ public sendPromptToOpenAI(inputPrompt: string): Observable { const prompt = inputPrompt; // Create an observable to handle the single request return new Observable(observer => { this.isOpenAIEnabled().subscribe( (AIEnabled: boolean) => { if (!AIEnabled) { observer.next(""); // If AI Assistant is not enabled, return an empty string observer.complete(); } else { // Perform the HTTP request without retries this.http .post(api_Url_Openai, { prompt }) .pipe( map(response => { const content = response.choices[0]?.message?.content.trim() || ""; return content; }) ) .subscribe({ next: content => { observer.next(content); // Return the response content if successful observer.complete(); }, error: () => { observer.next(""); // If there's an error, return an empty string observer.complete(); }, }); } }, () => { observer.next(""); // If AI Assistant status check fails, return an empty string observer.complete(); } ); }); } } ================================================ FILE: frontend/src/app/workspace/service/ai-assistant/ai-assistant.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { AppSettings } from "../../../common/app-setting"; import { HttpClient } from "@angular/common/http"; import { Observable, of } from "rxjs"; import { catchError, map } from "rxjs/operators"; // The type annotation return from the LLM export type TypeAnnotationResponse = { choices: ReadonlyArray<{ message: Readonly<{ content: string; }>; }>; }; export interface UnannotatedArgument extends Readonly<{ name: string; startLine: number; startColumn: number; endLine: number; endColumn: number; }> {} interface UnannotatedArgumentItem { readonly underlying: { readonly name: { readonly value: string }; readonly startLine: { readonly value: number }; readonly startColumn: { readonly value: number }; readonly endLine: { readonly value: number }; readonly endColumn: { readonly value: number }; }; } interface UnannotatedArgumentResponse { readonly underlying: { readonly result: { readonly value: ReadonlyArray; }; }; } // Define AI model type export const AI_ASSISTANT_API_BASE_URL = `${AppSettings.getApiEndpoint()}/aiassistant`; export const AI_MODEL = { OpenAI: "OpenAI", NoAiAssistant: "NoAiAssistant", } as const; export type AI_MODEL = (typeof AI_MODEL)[keyof typeof AI_MODEL]; @Injectable({ providedIn: "root", }) export class AIAssistantService { constructor(private http: HttpClient) {} /** * Checks if AI Assistant is enabled and returns the AI model in use. * * @returns {Observable} - An Observable that emits the type of AI model in use ("OpenAI" or "NoAiAssistant"). */ // To get the backend AI flag to check if the user want to use the AI feature // valid returns: ["OpenAI", "NoAiAssistant"] public checkAIAssistantEnabled(): Observable { const apiUrl = `${AI_ASSISTANT_API_BASE_URL}/isenabled`; return this.http.get(apiUrl, { responseType: "text" }).pipe( map(response => { const isEnabled: AI_MODEL = response === "OpenAI" ? "OpenAI" : "NoAiAssistant"; console.log( isEnabled === "OpenAI" ? "AI Assistant successfully started" : "No AI Assistant or OpenAI authentication key error" ); return isEnabled; }), catchError(() => { return of("NoAiAssistant" as AI_MODEL); }) ); } /** * Sends a request to the backend to get type annotation suggestions from LLM for the provided code. * * @param {string} code - The selected code for which the user wants type annotation suggestions. * @param {number} lineNumber - The line number where the selected code locates. * @param {string} allcode - The entire code of the UDF (User Defined Function) to provide context for the AI assistant. * * @returns {Observable} - An Observable that emits the type annotation suggestions * returned by the LLM. */ public getTypeAnnotations(code: string, lineNumber: number, allcode: string): Observable { const requestBody = { code, lineNumber, allcode }; return this.http.post(`${AI_ASSISTANT_API_BASE_URL}/annotationresult`, requestBody, {}); } public locateUnannotated(selectedCode: string, startLine: number): Observable { const requestBody = { selectedCode, startLine }; return this.http .post(`${AI_ASSISTANT_API_BASE_URL}/annotate-argument`, requestBody) .pipe( map(response => { if (response) { const result = response.underlying.result.value.map( (item: UnannotatedArgumentItem): UnannotatedArgument => ({ name: item.underlying.name.value, startLine: item.underlying.startLine.value, startColumn: item.underlying.startColumn.value, endLine: item.underlying.endLine.value, endColumn: item.underlying.endColumn.value, }) ); console.log("Unannotated Arguments:", result); return response.underlying.result.value.map( (item: UnannotatedArgumentItem): UnannotatedArgument => ({ name: item.underlying.name.value, startLine: item.underlying.startLine.value, startColumn: item.underlying.startColumn.value, endLine: item.underlying.endLine.value, endColumn: item.underlying.endColumn.value, }) ); } else { console.error("Unexpected response format:", response); return []; } }), catchError((error: unknown) => { console.error("Request to backend failed:", error); throw new Error("Request to backend failed"); }) ); } } ================================================ FILE: frontend/src/app/workspace/service/code-editor/code-editor.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable, ViewContainerRef } from "@angular/core"; import { BehaviorSubject, Observable } from "rxjs"; @Injectable({ providedIn: "root", }) export class CodeEditorService { public vc!: ViewContainerRef; private editorStates: Map> = new Map(); /** * Returns an observable representing whether the editor for the given operator is open. * @param operatorID The ID of the operator. * @returns Observable for the editor state. */ getEditorState(operatorID: string): Observable { if (!this.editorStates.has(operatorID)) { this.editorStates.set(operatorID, new BehaviorSubject(false)); } return this.editorStates.get(operatorID)!.asObservable(); } /** * Sets the editor state for the given operator. * @param operatorID The ID of the operator. * @param isOpen Whether the editor is open. */ setEditorState(operatorID: string, isOpen: boolean): void { if (!this.editorStates.has(operatorID)) { this.editorStates.set(operatorID, new BehaviorSubject(isOpen)); } else { this.editorStates.get(operatorID)!.next(isOpen); } } } ================================================ FILE: frontend/src/app/workspace/service/compile-workflow/workflow-compiling.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { HttpClient, HttpHeaders } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { EMPTY, merge, Observable, ReplaySubject } from "rxjs"; import { CustomJSONSchema7 } from "src/app/workspace/types/custom-json-schema.interface"; import { AppSettings } from "../../../common/app-setting"; import { areOperatorSchemasEqual, OperatorSchema } from "../../types/operator-schema.interface"; import { ExecuteWorkflowService } from "../execute-workflow/execute-workflow.service"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { catchError, debounceTime, mergeMap } from "rxjs/operators"; import { DynamicSchemaService } from "../dynamic-schema/dynamic-schema.service"; import { AttributeType, CompilationState, CompilationStateInfo, OperatorPortSchemaMap, PortSchema, WorkflowCompilationResponse, } from "../../types/workflow-compiling.interface"; import { WorkflowFatalError } from "../../types/workflow-websocket.interface"; import { LogicalPlan } from "../../types/execute-workflow.interface"; import { ValidationWorkflowService } from "../validation/validation-workflow.service"; import { WorkflowGraphReadonly } from "../workflow-graph/model/workflow-graph"; import { serializePortIdentity } from "../../../common/util/port-identity-serde"; import { addCompilationError, areAllPortSchemasEqual } from "../../../common/util/workflow-compilation-utils"; import { parseLogicalOperatorPortID } from "../../../common/util/logical-operator-port-serde"; // endpoint for workflow compile export const WORKFLOW_COMPILATION_ENDPOINT = "compile"; export const WORKFLOW_COMPILATION_DEBOUNCE_TIME_MS = 500; /** * Workflow Compiling Service provides mainly 3 functionalities: * 1. autocomplete attribute property of operators (previously done by the SchemaPropagationService) * 2. receive static errors (previously done by sending EditingTimeCompilationRequest and saving in the ExecutionStateInfo) * 3. manage PhysicalPlan (TODO: send the physical plan to the standalone WorkflowExecutingService once we have it) * * When user creates and connects operators in workflow, the WorkflowCompilingService's api will be triggered, which, * propagate the schemas, compiles the user's workflow to get the physical plan and static errors(if any). * * Specifically for schema autocomplete, by contract, property name `attribute` and `attributes` indicate the field is a column of the operator's input, * and schema propagation can provide autocomplete for the column names. */ @Injectable({ providedIn: "root", }) export class WorkflowCompilingService { private currentCompilationStateInfo: CompilationStateInfo = { state: CompilationState.Uninitialized, }; private compilationStateInfoChangedStream = new ReplaySubject(1); constructor( private httpClient: HttpClient, private workflowActionService: WorkflowActionService, private dynamicSchemaService: DynamicSchemaService, private validationWorkflowService: ValidationWorkflowService ) { // Subscribe to compilation state changes to apply schema propagation this.compilationStateInfoChangedStream.subscribe(() => { this.applySchemaPropagationResult(); }); // invoke the compilation service when there are any changes on workflow topology and properties. This includes: // - operator add, delete, property changed, disabled // - link add, delete merge( this.workflowActionService.getTexeraGraph().getLinkAddStream(), this.workflowActionService.getTexeraGraph().getLinkDeleteStream(), this.workflowActionService.getTexeraGraph().getOperatorAddStream(), this.workflowActionService.getTexeraGraph().getOperatorDeleteStream(), this.workflowActionService.getTexeraGraph().getOperatorPropertyChangeStream(), this.workflowActionService.getTexeraGraph().getDisabledOperatorsChangedStream() ) .pipe(debounceTime(WORKFLOW_COMPILATION_DEBOUNCE_TIME_MS)) .pipe( mergeMap(() => { const logicalPlan = ExecuteWorkflowService.getLogicalPlanRequest( this.validationWorkflowService.getValidTexeraGraph(), undefined ); return this.compile(logicalPlan); }) ) .subscribe(response => { if (response.physicalPlan) { this.currentCompilationStateInfo = { state: CompilationState.Succeeded, physicalPlan: response.physicalPlan, operatorOutputPortSchemaMap: response.operatorOutputSchemas, }; } else { this.currentCompilationStateInfo = { state: CompilationState.Failed, operatorOutputPortSchemaMap: response.operatorOutputSchemas, operatorErrors: response.operatorErrors, }; } this.compilationStateInfoChangedStream.next(this.currentCompilationStateInfo.state); }); } public getWorkflowCompilationState(): CompilationState { return this.currentCompilationStateInfo.state; } public getWorkflowCompilationErrors(): Readonly> { if ( this.currentCompilationStateInfo.state === CompilationState.Succeeded || this.currentCompilationStateInfo.state === CompilationState.Uninitialized ) { return {}; } return this.currentCompilationStateInfo.operatorErrors; } public getOperatorInputSchemaMap(operatorID: string): OperatorPortSchemaMap | undefined { if ( this.currentCompilationStateInfo.state == CompilationState.Uninitialized || !this.currentCompilationStateInfo.operatorOutputPortSchemaMap ) { return undefined; } return this.extractOperatorInputPortSchemaMap( operatorID, this.currentCompilationStateInfo.operatorOutputPortSchemaMap, this.workflowActionService.getTexeraGraph() ); } public getOperatorOutputSchemaMap(operatorID: string): OperatorPortSchemaMap | undefined { if ( this.currentCompilationStateInfo.state == CompilationState.Uninitialized || !this.currentCompilationStateInfo.operatorOutputPortSchemaMap ) { return undefined; } return this.currentCompilationStateInfo.operatorOutputPortSchemaMap[operatorID]; } public getPortInputSchema(operatorID: string, portIndex: number): PortSchema | undefined { return this.getOperatorInputSchemaMap(operatorID)?.[serializePortIdentity({ id: portIndex, internal: false })]; } public getOperatorInputAttributeType( operatorID: string, portIndex: number, attributeName: string ): AttributeType | undefined { return this.getPortInputSchema(operatorID, portIndex)?.find(e => e.attributeName === attributeName)?.attributeType; } /** * Apply the schema propagation result to an operator. * The schema propagation result contains the input attributes of operators. * * If an operator is not in the result, then: * 1. the operator's input attributes cannot be inferred. In this case, the operator dynamic schema is unchanged. * 2. the operator is a source operator. In this case, we need to fill in the attributes using the selected table. */ private applySchemaPropagationResult(): void { // for each operator, try to apply schema propagation result Array.from(this.dynamicSchemaService.getDynamicSchemaMap().keys()).forEach(operatorID => { const currentDynamicSchema = this.dynamicSchemaService.getDynamicSchema(operatorID); // Get the input schema for this operator using the centralized method const inputSchema = this.getOperatorInputSchemaMap(operatorID); let newDynamicSchema: OperatorSchema; if (inputSchema) { newDynamicSchema = WorkflowCompilingService.setOperatorInputAttrs(currentDynamicSchema, inputSchema); } else { // otherwise, the input attributes of the operator is unknown // if the operator is not a source operator, restore its original schema of input attributes if (currentDynamicSchema.additionalMetadata.inputPorts.length > 0) { newDynamicSchema = WorkflowCompilingService.restoreOperatorInputAttrs(currentDynamicSchema); } else { newDynamicSchema = currentDynamicSchema; } } if (!areOperatorSchemasEqual(currentDynamicSchema, newDynamicSchema)) { this.dynamicSchemaService.setDynamicSchema(operatorID, newDynamicSchema); } }); } /** * Extracts input schema per port for an operator by looking at the output schemas of operators that are connecting to it. * * @param operatorID The target operator's ID * @param outputSchemas Map of operator IDs to their output schemas per output port * @param workflowGraph to get input links from * @returns The extracted input schema per port or undefined */ private extractOperatorInputPortSchemaMap( operatorID: string, outputSchemas: Record, workflowGraph: WorkflowGraphReadonly ): OperatorPortSchemaMap | undefined { const inputLinks = workflowGraph.getInputLinksByOperatorId(operatorID); if (!inputLinks.length) return undefined; // Get the operator's dynamic schema to know what input ports it has const dynamicSchema = this.dynamicSchemaService.getDynamicSchema(operatorID); if (!dynamicSchema) return undefined; const inputPortSchemaMap = new Map(); dynamicSchema.additionalMetadata.inputPorts.forEach((inputPort, portIndex) => { const portId = serializePortIdentity({ id: portIndex, internal: false }); inputPortSchemaMap.set(portId, undefined); // Find all links that connect to this input port const linksToThisPort = inputLinks.filter(link => { const inputPort = parseLogicalOperatorPortID(link.target.portID); if (!inputPort) return false; return inputPort.portNumber === portIndex; }); if (linksToThisPort.length > 0) { // Check if multiple links have different schemas const schemas: (PortSchema | undefined)[] = linksToThisPort.map(link => { const sourcePortSchemaMap = outputSchemas[link.source.operatorID]; if (!sourcePortSchemaMap) { return undefined; } const outputPort = parseLogicalOperatorPortID(link.source.portID); if (!outputPort) { return undefined; } return sourcePortSchemaMap[serializePortIdentity({ id: outputPort.portNumber, internal: false })]; }); // Check if all schemas are the same using utility function if (schemas.length > 1 && !areAllPortSchemasEqual(schemas)) { // Set compilation state to failed and add error using utility function this.currentCompilationStateInfo = addCompilationError( this.currentCompilationStateInfo, operatorID, `Multiple links with different schemas connected to the same input port ${portIndex}`, `Port ${portIndex} received ${schemas.length} different schemas (some may be undefined)` ); return undefined; } // All port schemas of this input port has been checked to be the same, use the first schema to set if (schemas.length > 0) { inputPortSchemaMap.set(portId, schemas[0]); } } }); if (!inputPortSchemaMap.size) return undefined; return Object.fromEntries(inputPortSchemaMap); } /** * Used for automated propagation of input schema in workflow. * * When users are in the process of building a workflow, Texera can propagate schema forwards so * that users can easily set the properties of the next operator. For eg: If there are two operators Source:Scan and KeywordSearch and * a link is created between them, the attributed of the table selected in Source can be propagated to the KeywordSearch operator. */ private compile(logicalPlan: LogicalPlan): Observable { // create a Logical Plan based on the workflow graph // remove unnecessary information for schema propagation. const body = { operators: logicalPlan.operators, links: logicalPlan.links, opsToReuseResult: [], opsToViewResult: [], }; // make a http post request to the API endpoint with the logical plan object return this.httpClient .post( `${AppSettings.getApiEndpoint()}/${WORKFLOW_COMPILATION_ENDPOINT}`, JSON.stringify(body), { headers: new HttpHeaders({ "Content-Type": "application/json", }), } ) .pipe( catchError((err: unknown) => { console.warn("compile workflow API returns error", err); return EMPTY; }) ); } public static setOperatorInputAttrs( operatorSchema: OperatorSchema, inputPortSchemaMap: OperatorPortSchemaMap | undefined ): OperatorSchema { // If the inputSchema is empty, just return the original operator metadata. if (!inputPortSchemaMap || Object.keys(inputPortSchemaMap).length === 0) { return operatorSchema; } let newJsonSchema = operatorSchema.jsonSchema; const getAttrNames = (attrName: string, v: CustomJSONSchema7): string[] | undefined => { const i = v.autofillAttributeOnPort; if (i === undefined || i === null || !Number.isInteger(i)) { return undefined; } // Use serializePortIdentity to get the correct key for the input port const portId = serializePortIdentity({ id: i, internal: false }); const inputAttrAtPort = inputPortSchemaMap[portId]; if (!inputAttrAtPort) { return undefined; } const attrNames: string[] = inputAttrAtPort.map(attr => attr.attributeName); if (v.additionalEnumValue) { attrNames.push(v.additionalEnumValue); } // ajv does not support null values, so it converts all the nulls to empty strings. // https://github.com/ajv-validator/ajv/issues/1471 // the null -> "" change is done by Ajv.validate() with useDefault set to true. // It is converted during the property editor form initialization and workflow validation, instead of during schema propagation. if (!operatorSchema.jsonSchema.required?.includes(attrName)) { if (v.default) { if (typeof v.default !== "string") { throw new Error("default value must be a string"); } // We are adding the default value or "" into // the enum list to pass the frontend check for optional properties. attrNames.push(v.default); } else { attrNames.push(""); } } return attrNames; }; newJsonSchema = DynamicSchemaService.mutateProperty( newJsonSchema, (k, v) => v.autofill === "attributeName", (attrName, old) => ({ ...old, type: "string", enum: getAttrNames(attrName, old), uniqueItems: true, }) ); newJsonSchema = DynamicSchemaService.mutateProperty( newJsonSchema, (k, v) => v.autofill === "attributeNameList", (attrName, old) => ({ ...old, type: "array", uniqueItems: true, items: { ...(old.items as CustomJSONSchema7), type: "string", enum: getAttrNames(attrName, old), }, }) ); return { ...operatorSchema, jsonSchema: newJsonSchema, }; } public static restoreOperatorInputAttrs(operatorSchema: OperatorSchema): OperatorSchema { let newJsonSchema = operatorSchema.jsonSchema; newJsonSchema = DynamicSchemaService.mutateProperty( newJsonSchema, (k, v) => v.autofill === "attributeName", (attrName, old) => ({ ...old, type: "string", enum: undefined, uniqueItems: undefined, }) ); newJsonSchema = DynamicSchemaService.mutateProperty( newJsonSchema, (k, v) => v.autofill === "attributeNameList", (attrName, old) => ({ ...old, type: "array", uniqueItems: undefined, items: { ...(old.items as CustomJSONSchema7), type: "string", enum: undefined, }, }) ); return { ...operatorSchema, jsonSchema: newJsonSchema, }; } public getCompilationStateInfoChangedStream(): Observable { return this.compilationStateInfoChangedStream.asObservable(); } } ================================================ FILE: frontend/src/app/workspace/service/drag-drop/drag-drop.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { JointUIService } from "../joint-ui/joint-ui.service"; import { inject, TestBed } from "@angular/core/testing"; import { DragDropService } from "./drag-drop.service"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { UndoRedoService } from "../undo-redo/undo-redo.service"; import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../operator-metadata/stub-operator-metadata.service"; import { marbles } from "rxjs-marbles"; import { mockMultiInputOutputPredicate, mockResultPredicate, mockScanPredicate, mockScanResultLink, } from "../workflow-graph/model/mock-workflow-data"; import { OperatorLink, OperatorPredicate } from "../../types/workflow-common.interface"; import { VIEW_RESULT_OP_TYPE } from "../workflow-graph/model/workflow-graph"; import { commonTestProviders } from "../../../common/testing/test-utils"; describe("DragDropService", () => { let dragDropService: DragDropService; beforeEach(() => { TestBed.configureTestingModule({ providers: [ JointUIService, WorkflowActionService, UndoRedoService, WorkflowUtilService, DragDropService, { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, ...commonTestProviders, ], }); dragDropService = TestBed.inject(DragDropService); // custom equality disregards link ID (since I use DragDropService.getNew) /* TODO(vitest): no equivalent — port via expect.extend */ ((..._args: unknown[]) => {})( (link1: OperatorLink, link2: OperatorLink) => { if (typeof link1 === "object" && typeof link2 === "object") { return link1.source === link2.source && link1.target === link2.target; } } ); }); it("should be created", inject([DragDropService], (injectedService: DragDropService) => { expect(injectedService).toBeTruthy(); })); it("should successfully create a new operator link given 2 operator predicates", () => { const createdLink: OperatorLink = (dragDropService as any).getNewOperatorLink( mockScanPredicate, mockResultPredicate ); expect(createdLink.source).toEqual(mockScanResultLink.source); expect(createdLink.target).toEqual(mockScanResultLink.target); }); it("should find 3 input operatorPredicates and 3 output operatorPredicates for an operatorPredicate with 3 input / 3 output ports", () => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const workflowUtilService: WorkflowUtilService = TestBed.inject(WorkflowUtilService); const input1 = workflowUtilService.getNewOperatorPredicate("ScanSource"); const input2 = workflowUtilService.getNewOperatorPredicate("ScanSource"); const input3 = workflowUtilService.getNewOperatorPredicate("ScanSource"); const output1 = workflowUtilService.getNewOperatorPredicate(VIEW_RESULT_OP_TYPE); const output2 = workflowUtilService.getNewOperatorPredicate(VIEW_RESULT_OP_TYPE); const output3 = workflowUtilService.getNewOperatorPredicate(VIEW_RESULT_OP_TYPE); workflowActionService.addOperator(input1, { x: 0, y: 0 }); workflowActionService.addOperator(input2, { x: 0, y: 10 }); workflowActionService.addOperator(input3, { x: 0, y: 20 }); workflowActionService.addOperator(output1, { x: 100, y: 0 }); workflowActionService.addOperator(output2, { x: 100, y: 10 }); workflowActionService.addOperator(output3, { x: 100, y: 20 }); // Probe at the centroid between the input and output columns. With the // SUGGESTION_DISTANCE_THRESHOLD = 300, all 6 operators are in range; the // 3 to the left are ranked as inputs, the 3 to the right as outputs. // Order within each list is heap-internal and not guaranteed by the // implementation — assert membership only. const [inputOps, outputOps] = (dragDropService as any).findClosestOperators( { x: 50, y: 0 }, mockMultiInputOutputPredicate ); expect(inputOps).toHaveLength(3); expect(inputOps).toEqual(expect.arrayContaining([input1, input2, input3])); expect(outputOps).toHaveLength(3); expect(outputOps).toEqual(expect.arrayContaining([output1, output2, output3])); }); it('should publish operatorPredicates to highlight streams when calling "updateHighlighting(prevHighlights,newHighlights)"', async () => { TestBed.inject(WorkflowActionService); const highlights: string[] = []; const unhighlights: string[] = []; const expectedHighlights = [mockScanPredicate.operatorID, mockScanPredicate.operatorID]; const expectedUnhighlights = [mockScanPredicate.operatorID, mockResultPredicate.operatorID]; // allow test to run for 10ms before checking, since observables are async const timeout = new Promise(resolve => setTimeout(resolve, 10)); dragDropService.getOperatorSuggestionHighlightStream().subscribe(operatorID => { highlights.push(operatorID); }); dragDropService.getOperatorSuggestionUnhighlightStream().subscribe(operatorID => { unhighlights.push(operatorID); }); // highlighting update situations (dragDropService as any).updateHighlighting([mockScanPredicate], [mockScanPredicate]); // no change (dragDropService as any).updateHighlighting([], [mockScanPredicate]); // new highlight (dragDropService as any).updateHighlighting([mockScanPredicate], []); // new unhighlight (dragDropService as any).updateHighlighting([mockResultPredicate], [mockScanPredicate]); // new highlight and unhighlight // allow test to run for up to 500ms before checking, since observables are async await timeout; expect(highlights).toEqual(expectedHighlights); expect(unhighlights).toEqual(expectedUnhighlights); }); it("should not find any operator when the mouse coordinate is greater than the threshold defined", () => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); workflowActionService.addOperator(mockScanPredicate, { x: 0, y: 0 }); const [inputOps] = (dragDropService as any).findClosestOperators( { x: DragDropService.SUGGESTION_DISTANCE_THRESHOLD + 10, y: DragDropService.SUGGESTION_DISTANCE_THRESHOLD + 10, }, mockResultPredicate ); expect(inputOps).toEqual([]); }); it("should add the dropped operator with links to suggested neighbors and unhighlight prior suggestions", async () => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const workflowUtilService: WorkflowUtilService = TestBed.inject(WorkflowUtilService); const input1 = workflowUtilService.getNewOperatorPredicate("ScanSource"); const input2 = workflowUtilService.getNewOperatorPredicate("ScanSource"); const input3 = workflowUtilService.getNewOperatorPredicate("ScanSource"); const output1 = workflowUtilService.getNewOperatorPredicate(VIEW_RESULT_OP_TYPE); const output2 = workflowUtilService.getNewOperatorPredicate(VIEW_RESULT_OP_TYPE); const output3 = workflowUtilService.getNewOperatorPredicate(VIEW_RESULT_OP_TYPE); // Real main jointjs paper attached to a hidden DOM host so coordinate // transforms in `dragStarted` / mousemove / `dragDropped` resolve // without stubs. jsdom doesn't compute layout, so the SVG polyfill's // identity matrices collapse `pageToLocalPoint(x, y)` to (0, 0) // regardless of input — that's why operators are placed at x=±100 // around the origin below. const paperHost = document.createElement("div"); const flyingOpHost = document.createElement("div"); flyingOpHost.id = "flyingOP"; document.body.appendChild(paperHost); document.body.appendChild(flyingOpHost); try { workflowActionService.getJointGraphWrapper().attachMainJointPaper({ el: paperHost }); // Inputs at negative x and outputs at positive x so the (0, 0) drop // point classifies them correctly via `findClosestOperators` (which // compares operator x against mouse x). workflowActionService.addOperator(input1, { x: -100, y: 10 }); workflowActionService.addOperator(input2, { x: -100, y: 20 }); workflowActionService.addOperator(input3, { x: -100, y: 30 }); workflowActionService.addOperator(output1, { x: 100, y: 10 }); workflowActionService.addOperator(output2, { x: 100, y: 20 }); workflowActionService.addOperator(output3, { x: 100, y: 30 }); const unhighlights: string[] = []; dragDropService.getOperatorSuggestionUnhighlightStream().subscribe(id => unhighlights.push(id)); const links: OperatorLink[] = []; workflowActionService .getTexeraGraph() .getLinkAddStream() .subscribe(link => links.push(link)); // dragStarted creates a fresh `op` of the given type and subscribes // to window mousemove to populate suggestionInputs / suggestionOutputs. dragDropService.dragStarted("MultiInputOutput"); const droppedOp = (dragDropService as any).op as OperatorPredicate; // Drive the suggestion pipeline. Any mousemove will do — jsdom's // `pageToLocalPoint` collapses to (0, 0) regardless of the // dispatched coordinates. window.dispatchEvent(new MouseEvent("mousemove", { clientX: 0, clientY: 0 })); await new Promise(resolve => setTimeout(resolve, 0)); dragDropService.dragDropped({ x: 0, y: 0 }); // Tear down the window-level mousemove subscriptions installed by // `dragStarted`. Without this the `first()` mouseup observer stays // armed and a stray mousemove from a later spec re-enters this // service's suggestion pipeline. window.dispatchEvent(new MouseEvent("mouseup")); await new Promise(resolve => setTimeout(resolve, 0)); // Each suggested operator should have been unhighlighted at drop time. expect(unhighlights).toEqual( expect.arrayContaining([ input1.operatorID, input2.operatorID, input3.operatorID, output1.operatorID, output2.operatorID, output3.operatorID, ]) ); expect(unhighlights).toHaveLength(6); // 3 input→droppedOp links and 3 droppedOp→output links. expect(links).toHaveLength(6); const inputLinks = links.filter(l => l.target.operatorID === droppedOp.operatorID); const outputLinks = links.filter(l => l.source.operatorID === droppedOp.operatorID); expect(inputLinks.map(l => l.source.operatorID).sort()).toEqual( [input1.operatorID, input2.operatorID, input3.operatorID].sort() ); expect(outputLinks.map(l => l.target.operatorID).sort()).toEqual( [output1.operatorID, output2.operatorID, output3.operatorID].sort() ); } finally { // Always clean up the DOM hosts even if an assertion above threw, // so the JointJS papers don't leak into later specs. document.body.removeChild(paperHost); document.body.removeChild(flyingOpHost); } }); }); ================================================ FILE: frontend/src/app/workspace/service/drag-drop/drag-drop.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { OperatorLink, OperatorPredicate, Point } from "../../types/workflow-common.interface"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { fromEvent, Observable, Subject } from "rxjs"; import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; import { JointUIService } from "../joint-ui/joint-ui.service"; import { Injectable } from "@angular/core"; import { filter, first, map } from "rxjs/operators"; import TinyQueue from "tinyqueue"; import * as joint from "jointjs"; @Injectable({ providedIn: "root", }) export class DragDropService { public static readonly SUGGESTION_DISTANCE_THRESHOLD = 300; private op!: OperatorPredicate; private operatorDroppedSubject = new Subject(); private readonly operatorSuggestionHighlightStream = new Subject(); private readonly operatorSuggestionUnhighlightStream = new Subject(); private suggestionInputs: OperatorPredicate[] = []; private suggestionOutputs: OperatorPredicate[] = []; constructor( private jointUIService: JointUIService, private workflowUtilService: WorkflowUtilService, private workflowActionService: WorkflowActionService ) {} public dragStarted(operatorType: string): void { this.op = this.workflowUtilService.getNewOperatorPredicate(operatorType); const scale = this.workflowActionService.getJointGraphWrapper().getMainJointPaper()?.scale().sx ?? 1; new joint.dia.Paper({ el: document.getElementById("flyingOP")!, width: JointUIService.DEFAULT_OPERATOR_WIDTH * scale, height: JointUIService.DEFAULT_OPERATOR_HEIGHT * scale, model: new joint.dia.Graph().addCell(this.jointUIService.getJointOperatorElement(this.op, { x: 0, y: 0 })), }).scale(scale); this.handleOperatorRecommendationOnDrag(); } public dragDropped(dropPoint: Point): void { const coordinates = this.workflowActionService .getJointGraphWrapper() .getMainJointPaper() ?.pageToLocalPoint(dropPoint.x, dropPoint.y)!; // Check if the operator is dropped on top of an existing edge const intersectedLink = this.findIntersectedLink(coordinates); let newLinks: OperatorLink[]; if (intersectedLink) { newLinks = this.createEdgeReconnectionLinks(this.op, intersectedLink); } else { newLinks = this.getNewOperatorLinks(this.op, this.suggestionInputs, this.suggestionOutputs); } this.workflowActionService.addOperatorsAndLinks([{ op: this.op, pos: coordinates }], newLinks); this.resetSuggestions(); this.operatorDroppedSubject.next(); } get operatorDropStream() { return this.operatorDroppedSubject.asObservable(); } /** * Gets an observable for new suggestion event to highlight an operator to link with. * * Contains the operator ID to highlight for suggestion */ public getOperatorSuggestionHighlightStream(): Observable { return this.operatorSuggestionHighlightStream.asObservable(); } /** * Gets an observable for removing suggestion event to unhighlight an operator * * Contains the operator ID to unhighlight to remove previous suggestion */ public getOperatorSuggestionUnhighlightStream(): Observable { return this.operatorSuggestionUnhighlightStream.asObservable(); } /** * This is the handler for recommending operator to link to when * the user is dragging the ghost operator before dropping. * */ private handleOperatorRecommendationOnDrag(): void { let isOperatorDropped = false; let currentIntersectedLink: OperatorLink | null = null; fromEvent(window, "mouseup") .pipe(first()) .subscribe(() => { isOperatorDropped = true; // Clear any edge intersection highlighting when drag ends if (currentIntersectedLink) { this.clearEdgeIntersectionHighlight(currentIntersectedLink); currentIntersectedLink = null; } }); fromEvent(window, "mousemove") .pipe( map(value => [value.clientX, value.clientY]), filter(() => !isOperatorDropped) ) .subscribe(mouseCoordinates => { const currentMouseCoordinates = { x: mouseCoordinates[0], y: mouseCoordinates[1], }; let coordinates: Point | undefined = this.workflowActionService .getJointGraphWrapper() .getMainJointPaper() ?.pageToLocalPoint(currentMouseCoordinates.x, currentMouseCoordinates.y); if (!coordinates) { coordinates = currentMouseCoordinates; } let scale: { sx: number; sy: number } | undefined = this.workflowActionService .getJointGraphWrapper() .getMainJointPaper() ?.scale(); if (scale === undefined) { scale = { sx: 1, sy: 1 }; } const scaledMouseCoordinates = { x: coordinates.x / scale.sx, y: coordinates.y / scale.sy, }; // search for nearby operators as suggested input/output operators let newInputs, newOutputs: OperatorPredicate[]; [newInputs, newOutputs] = this.findClosestOperators(scaledMouseCoordinates, this.op); // update highlighting class vars to reflect new input/output operators this.updateHighlighting(this.suggestionInputs.concat(this.suggestionOutputs), newInputs.concat(newOutputs)); // assign new suggestions [this.suggestionInputs, this.suggestionOutputs] = [newInputs, newOutputs]; }); // Edge intersection detection fromEvent(window, "mousemove") .pipe( map(value => [value.clientX, value.clientY]), filter(() => !isOperatorDropped) ) .subscribe(mouseCoordinates => { const currentMouseCoordinates = { x: mouseCoordinates[0], y: mouseCoordinates[1], }; let coordinates: Point | undefined = this.workflowActionService .getJointGraphWrapper() .getMainJointPaper() ?.pageToLocalPoint(currentMouseCoordinates.x, currentMouseCoordinates.y); if (!coordinates) { coordinates = currentMouseCoordinates; } // Only check for edge intersection if the operator can be inserted into edges const hasInputPorts = this.op.inputPorts.length > 0; const hasOutputPorts = this.op.outputPorts.length > 0; let intersectedLink: OperatorLink | null = null; if (hasInputPorts && hasOutputPorts) { // Check for edge intersection for visual feedback intersectedLink = this.findIntersectedLink(coordinates); } // Update edge intersection highlighting only when it changes if (intersectedLink !== currentIntersectedLink) { // Clear previous highlighting if (currentIntersectedLink) { this.clearEdgeIntersectionHighlight(currentIntersectedLink); } // Add new highlighting if (intersectedLink) { this.highlightEdgeIntersection(intersectedLink); } currentIntersectedLink = intersectedLink; } }); } /** * Finds nearby operators that can input to currentOperator and accept it's outputs. * * Only looks for inputs left of mouseCoordinate/ outputs right of mouseCoordinate. * Only looks for operators within distance DragDropService.SUGGESTION_DISTANCE_THRESHOLD. * **Warning**: assumes operators only output one port each (IE always grabs 3 operators for 3 input ports * even if first operator has 3 free outputs to match 3 inputs) * @mouseCoordinate is the location of the currentOperator on the JointGraph when dragging ghost operator * @currentOperator is the current operator, used to determine how many inputs and outputs to search for * @returns [[inputting-ops ...], [output-accepting-ops ...]] */ private findClosestOperators( mouseCoordinate: Point, currentOperator: OperatorPredicate ): [OperatorPredicate[], OperatorPredicate[]] { const operatorLinks = this.workflowActionService.getTexeraGraph().getAllLinks(); const operatorList = this.workflowActionService.getTexeraGraph().getAllOperators(); const numInputOps: number = currentOperator.inputPorts.length; const numOutputOps: number = currentOperator.outputPorts.length; // These two functions are a performance concern const hasFreeOutputPorts = (operator: OperatorPredicate): boolean => { return ( operatorLinks.filter(link => link.source.operatorID === operator.operatorID).length < operator.outputPorts.length ); }; const hasFreeInputPorts = (operator: OperatorPredicate): boolean => { return ( operatorLinks.filter(link => link.target.operatorID === operator.operatorID).length < operator.inputPorts.length ); }; // closest operators sorted least to greatest by distance using priority queue const compare = ( a: { op: OperatorPredicate; dist: number }, b: { op: OperatorPredicate; dist: number } ): number => { return b.dist - a.dist; }; const inputOps: TinyQueue<{ op: OperatorPredicate; dist: number }> = new TinyQueue([], compare); const outputOps: TinyQueue<{ op: OperatorPredicate; dist: number }> = new TinyQueue([], compare); const greatestDistance = (queue: TinyQueue<{ op: OperatorPredicate; dist: number }>): number => { const greatest = queue.peek(); if (greatest) { return greatest.dist; } else { return 0; } }; // for each operator, check if in range/has free ports/is on the right side/is closer than prev closest ops/ operatorList.forEach(operator => { const operatorPosition = this.workflowActionService .getJointGraphWrapper() .getElementPosition(operator.operatorID); const distanceFromCurrentOperator = Math.sqrt( (mouseCoordinate.x - operatorPosition.x) ** 2 + (mouseCoordinate.y - operatorPosition.y) ** 2 ); if (distanceFromCurrentOperator < DragDropService.SUGGESTION_DISTANCE_THRESHOLD) { if ( numInputOps > 0 && operatorPosition.x < mouseCoordinate.x && (inputOps.length < numInputOps || distanceFromCurrentOperator < greatestDistance(inputOps)) && hasFreeOutputPorts(operator) ) { inputOps.push({ op: operator, dist: distanceFromCurrentOperator }); if (inputOps.length > numInputOps) { inputOps.pop(); } } else if ( numOutputOps > 0 && operatorPosition.x > mouseCoordinate.x && (outputOps.length < numOutputOps || distanceFromCurrentOperator < greatestDistance(outputOps)) && hasFreeInputPorts(operator) ) { outputOps.push({ op: operator, dist: distanceFromCurrentOperator }); if (outputOps.length > numOutputOps) { outputOps.pop(); } } } }); return [inputOps.data.map(x => x.op), outputOps.data.map(x => x.op)]; } /** * Updates highlighted operators based on the diff between prev * * @param prevHighlights are highlighted (some may be unhighlighted) * @param newHighlights will be highlighted after execution */ private updateHighlighting(prevHighlights: OperatorPredicate[], newHighlights: OperatorPredicate[]) { // unhighlight ops in prevHighlights but not in newHighlights prevHighlights .filter(operator => !newHighlights.includes(operator)) .forEach(operator => { this.operatorSuggestionUnhighlightStream.next(operator.operatorID); }); // highlight ops in newHghlights but not in prevHighlights newHighlights .filter(operator => !prevHighlights.includes(operator)) .forEach(operator => { this.operatorSuggestionHighlightStream.next(operator.operatorID); }); } /** Unhighlights suggestions and clears suggestion lists */ private resetSuggestions(): void { this.updateHighlighting(this.suggestionInputs.concat(this.suggestionOutputs), []); this.suggestionInputs = []; this.suggestionOutputs = []; } /** * This method will use an unique ID and 2 operator predicate to create and return * a new OperatorLink with initialized properties for the ports. * **Warning** links created w/o spacial awareness. May connect two distant ports when it makes more sense to connect closer ones' * @param sourceOperator gives output * @param targetOperator accepts input * @param operatorLinks optionally specify extant links (used to find which ports are occupied), defaults to all links. */ private getNewOperatorLink( sourceOperator: OperatorPredicate, targetOperator: OperatorPredicate, operatorLinks?: OperatorLink[] ): OperatorLink { if (operatorLinks === undefined) { operatorLinks = this.workflowActionService.getTexeraGraph().getAllLinks(); } // find the port that has not being connected const allPortsFromSource = operatorLinks .filter(link => link.source.operatorID === sourceOperator.operatorID) .map(link => link.source.portID); const allPortsFromTarget = operatorLinks .filter(link => link.target.operatorID === targetOperator.operatorID) .map(link => link.target.portID); const validSourcePortsID = sourceOperator.outputPorts.filter(port => !allPortsFromSource.includes(port.portID)); const validTargetPortsID = targetOperator.inputPorts.filter(port => !allPortsFromTarget.includes(port.portID)); const linkID = this.workflowUtilService.getLinkRandomUUID(); const source = { operatorID: sourceOperator.operatorID, portID: validSourcePortsID[0].portID, }; const target = { operatorID: targetOperator.operatorID, portID: validTargetPortsID[0].portID, }; return { linkID, source, target }; } /** *Get many links to one central "hub" operator * @param hubOperator * @param inputOperators * @param receiverOperators */ private getNewOperatorLinks( hubOperator: OperatorPredicate, inputOperators: OperatorPredicate[], receiverOperators: OperatorPredicate[] ): OperatorLink[] { // remember newly created links to prevent multiple link assignment to same port const occupiedLinks: OperatorLink[] = this.workflowActionService.getTexeraGraph().getAllLinks(); const newLinks: OperatorLink[] = []; const graph = this.workflowActionService.getJointGraphWrapper(); // sort ops by height, in order to pair them with ports closest to them // assumes that for an op with multiple input/output ports, ports in op.inputPorts/outPutports are rendered // [first ... last] => [North ... South] const heightSortedInputs: OperatorPredicate[] = inputOperators .slice(0) .sort((op1, op2) => graph.getElementPosition(op1.operatorID).y - graph.getElementPosition(op2.operatorID).y); const heightSortedOutputs: OperatorPredicate[] = receiverOperators .slice(0) .sort((op1, op2) => graph.getElementPosition(op1.operatorID).y - graph.getElementPosition(op2.operatorID).y); // if new operator has suggested links, create them if (heightSortedInputs !== undefined) { heightSortedInputs.forEach(inputOperator => { const newLink = this.getNewOperatorLink(inputOperator, hubOperator, occupiedLinks); newLinks.push(newLink); occupiedLinks.push(newLink); }); } if (heightSortedOutputs !== undefined) { heightSortedOutputs.forEach(outputOperator => { const newLink = this.getNewOperatorLink(hubOperator, outputOperator, occupiedLinks); newLinks.push(newLink); occupiedLinks.push(newLink); }); } return newLinks; } /** * Finds if the dropped operator intersects with any existing link on the workflow graph. * This checks if the operator's bounding box intersects with the edge, not just the cursor position. * * @param dropPoint The point where the operator is currently being dragged * @returns The intersected OperatorLink if found, null otherwise */ private findIntersectedLink(dropPoint: Point): OperatorLink | null { const allLinks = this.workflowActionService.getTexeraGraph().getAllLinks(); const paper = this.workflowActionService.getJointGraphWrapper().getMainJointPaper(); if (!paper) { return null; } // Get operator dimensions for bounding box calculation const operatorWidth = JointUIService.DEFAULT_OPERATOR_WIDTH; const operatorHeight = JointUIService.DEFAULT_OPERATOR_HEIGHT; // Create operator bounding box (centered on drop point) const operatorBounds = { x: dropPoint.x - operatorWidth / 2, y: dropPoint.y - operatorHeight / 2, width: operatorWidth, height: operatorHeight, }; for (const link of allLinks) { const jointLink = paper.getModelById(link.linkID) as joint.dia.Link; if (!jointLink) { continue; } const linkView = paper.findViewByModel(jointLink) as joint.dia.LinkView; if (!linkView) { continue; } // Get the path of the link const pathElement = linkView.el.querySelector(".connection") as SVGPathElement; if (!pathElement) { continue; } // Check if the operator bounding box intersects with the link path if (this.doesOperatorIntersectPath(operatorBounds, pathElement)) { return link; } } return null; } /** * Checks if an operator's bounding box intersects with an SVG path element. * * @param operatorBounds The bounding box of the operator * @param pathElement The SVG path element representing the link * @returns True if the operator intersects with the path, false otherwise */ private doesOperatorIntersectPath( operatorBounds: { x: number; y: number; width: number; height: number }, pathElement: SVGPathElement ): boolean { const pathLength = pathElement.getTotalLength(); const samples = Math.min(20, Math.max(5, Math.floor(pathLength / 20))); for (let i = 0; i <= samples; i++) { const lengthRatio = (i / samples) * pathLength; const pathPoint = pathElement.getPointAtLength(lengthRatio); // Check if this point on the path is within the operator's bounding box if ( pathPoint.x >= operatorBounds.x && pathPoint.x <= operatorBounds.x + operatorBounds.width && pathPoint.y >= operatorBounds.y && pathPoint.y <= operatorBounds.y + operatorBounds.height ) { return true; } } return false; } /** * Creates new links to reconnect operators when an operator is dropped on an edge. * This removes the original link and creates two new links: one from the source to the new operator, * and one from the new operator to the original target. * * @param newOperator The operator being inserted into the edge * @param intersectedLink The link that was intersected * @returns Array of new OperatorLink objects for reconnection */ private createEdgeReconnectionLinks(newOperator: OperatorPredicate, intersectedLink: OperatorLink): OperatorLink[] { const newLinks: OperatorLink[] = []; // Get source and target operators const sourceOperator = this.workflowActionService.getTexeraGraph().getOperator(intersectedLink.source.operatorID); const targetOperator = this.workflowActionService.getTexeraGraph().getOperator(intersectedLink.target.operatorID); if (!sourceOperator || !targetOperator) { return []; } // Check if the new operator has compatible ports const hasInputPorts = newOperator.inputPorts.length > 0; const hasOutputPorts = newOperator.outputPorts.length > 0; if (!hasInputPorts || !hasOutputPorts) { // If the new operator doesn't have both input and output ports, fall back to regular suggestions return this.getNewOperatorLinks(newOperator, this.suggestionInputs, this.suggestionOutputs); } // Delete the original link this.workflowActionService.deleteLinkWithID(intersectedLink.linkID); // Create link from source to new operator const sourceToNewLink: OperatorLink = { linkID: this.workflowUtilService.getLinkRandomUUID(), source: { operatorID: sourceOperator.operatorID, portID: intersectedLink.source.portID, }, target: { operatorID: newOperator.operatorID, portID: newOperator.inputPorts[0].portID, // Use first available input port }, }; newLinks.push(sourceToNewLink); // Create link from new operator to target const newToTargetLink: OperatorLink = { linkID: this.workflowUtilService.getLinkRandomUUID(), source: { operatorID: newOperator.operatorID, portID: newOperator.outputPorts[0].portID, // Use first available output port }, target: { operatorID: targetOperator.operatorID, portID: intersectedLink.target.portID, }, }; newLinks.push(newToTargetLink); return newLinks; } /** * Highlights an edge. * * @param link The link to highlight */ private highlightEdgeIntersection(link: OperatorLink): void { const paper = this.workflowActionService.getJointGraphWrapper().getMainJointPaper(); if (!paper) { return; } const jointLink = paper.getModelById(link.linkID); if (jointLink) { jointLink.attr({ ".connection": { stroke: "#FF6B35", "stroke-width": 4, "stroke-dasharray": "5,5", }, ".marker-source": { fill: "#FF6B35" }, ".marker-target": { fill: "#FF6B35" }, }); } } /** * Clears the highlighting on an edge. * * @param link The link to clear highlighting from */ private clearEdgeIntersectionHighlight(link: OperatorLink): void { const paper = this.workflowActionService.getJointGraphWrapper().getMainJointPaper(); if (!paper) { return; } const jointLink = paper.getModelById(link.linkID); if (jointLink) { jointLink.attr({ ".connection": { stroke: "#848484", // Default link color "stroke-width": 2, "stroke-dasharray": "none", }, ".marker-source": { fill: "none" }, ".marker-target": { fill: "none" }, }); } } } ================================================ FILE: frontend/src/app/workspace/service/dynamic-schema/dynamic-schema.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { OperatorSchema } from "../../types/operator-schema.interface"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { UndoRedoService } from "../undo-redo/undo-redo.service"; import { JointUIService } from "../joint-ui/joint-ui.service"; import { inject, TestBed } from "@angular/core/testing"; import { marbles } from "rxjs-marbles"; import { DynamicSchemaService } from "./dynamic-schema.service"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../operator-metadata/stub-operator-metadata.service"; import { mockPoint, mockScanPredicate } from "../workflow-graph/model/mock-workflow-data"; import { OperatorPredicate } from "../../types/workflow-common.interface"; import { mockScanSourceSchema } from "../operator-metadata/mock-operator-metadata.data"; import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; import { commonTestProviders } from "../../../common/testing/test-utils"; describe("DynamicSchemaService", () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [ { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, JointUIService, WorkflowActionService, WorkflowUtilService, UndoRedoService, DynamicSchemaService, ...commonTestProviders, ], }); }); it("should be created", inject([DynamicSchemaService], (service: DynamicSchemaService) => { expect(service).toBeTruthy(); })); it("should update dynamic schema map when operator is added/deleted", () => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const dynamicSchemaService: DynamicSchemaService = TestBed.inject(DynamicSchemaService); workflowActionService.addOperator(mockScanPredicate, mockPoint); expect(dynamicSchemaService.getDynamicSchemaMap().size === 1); workflowActionService.deleteOperator(mockScanPredicate.operatorID); expect(dynamicSchemaService.getDynamicSchemaMap().size === 0); }); it("should call all initial schema transformers when creating a new dynamic schema", () => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const dynamicSchemaService: DynamicSchemaService = TestBed.inject(DynamicSchemaService); const testTransformers = { transformer1: (op: OperatorPredicate, schema: OperatorSchema) => schema, transformer2: (op: OperatorPredicate, schema: OperatorSchema) => schema, }; const transformer1Spy = vi.spyOn(testTransformers, "transformer1"); const transformer2Spy = vi.spyOn(testTransformers, "transformer2"); dynamicSchemaService.registerInitialSchemaTransformer(testTransformers.transformer1); dynamicSchemaService.registerInitialSchemaTransformer(testTransformers.transformer2); workflowActionService.addOperator(mockScanPredicate, mockPoint); expect(transformer1Spy).toHaveBeenCalledTimes(1); expect(transformer2Spy).toHaveBeenCalledTimes(1); }); it( "should emit event when dynamic schema is changed", marbles(m => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const dynamicSchemaService: DynamicSchemaService = TestBed.inject(DynamicSchemaService); const newSchema: OperatorSchema = { ...mockScanSourceSchema, jsonSchema: { properties: { tableName: { type: "string", }, }, type: "object", }, }; const trigger = m.hot("-a-c-", { a: () => workflowActionService.addOperator(mockScanPredicate, mockPoint), c: () => dynamicSchemaService.setDynamicSchema(mockScanPredicate.operatorID, newSchema), }); trigger.subscribe(eventFunc => eventFunc()); const expected = m.hot("-d-e-", { d: { operatorID: mockScanPredicate.operatorID }, e: { operatorID: mockScanPredicate.operatorID }, }); m.expect(dynamicSchemaService.getOperatorDynamicSchemaChangedStream()).toBeObservable(expected); }) ); it( "should not emit event if the updated dynamic schema is same", marbles(m => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const dynamicSchemaService: DynamicSchemaService = TestBed.inject(DynamicSchemaService); const trigger = m.hot("-a-c-", { a: () => workflowActionService.addOperator(mockScanPredicate, mockPoint), c: () => dynamicSchemaService.setDynamicSchema(mockScanPredicate.operatorID, mockScanSourceSchema), }); trigger.subscribe(eventFunc => eventFunc()); const expected = m.hot("-d---", { d: { operatorID: mockScanPredicate.operatorID }, }); m.expect(dynamicSchemaService.getOperatorDynamicSchemaChangedStream()).toBeObservable(expected); }) ); }); ================================================ FILE: frontend/src/app/workspace/service/dynamic-schema/dynamic-schema.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { JSONSchema7, JSONSchema7Definition } from "json-schema"; import { cloneDeep, isEqual } from "lodash-es"; import { Observable, Subject } from "rxjs"; import { CustomJSONSchema7 } from "../../types/custom-json-schema.interface"; import { OperatorSchema } from "../../types/operator-schema.interface"; import { OperatorPredicate } from "../../types/workflow-common.interface"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; export type SchemaTransformer = (operator: OperatorPredicate, schema: OperatorSchema) => OperatorSchema; /** * Dynamic Schema Service associates each operator with its own OperatorSchema, * which could be different from the (static) schema of the operator type. * * Dynamic Schema of an operator can be changed through * when an operator is first added, other modules can transform the initial schema by registering hook functions * after an operator is added, modules, other modules can dynamically set the schema based on its need * * Currently, dynamic schema is changed through the following scenarios: * - attribute names autocomplete by WorkflowCompilingService * */ @Injectable({ providedIn: "root", }) export class DynamicSchemaService { // dynamic schema of operators in the current workflow, specific to an operator and different from the static schema // directly calling `set()` is prohibited, it must go through `setDynamicSchema()` private dynamicSchemaMap = new Map(); private initialSchemaTransformers: SchemaTransformer[] = []; // this stream is used to capture the event when the dynamic schema of an existing operator is changed private operatorDynamicSchemaChangedStream = new Subject<{ operatorID: string; }>(); constructor( private workflowActionService: WorkflowActionService, private operatorMetadataService: OperatorMetadataService ) { // when an operator is added, add it to the dynamic schema map this.workflowActionService .getTexeraGraph() .getOperatorAddStream() .subscribe(operator => { this.setDynamicSchema(operator.operatorID, this.getInitialDynamicSchema(operator)); }); // when an operator is deleted, remove it from the dynamic schema map this.workflowActionService .getTexeraGraph() .getOperatorDeleteStream() .subscribe(event => this.dynamicSchemaMap.delete(event.deletedOperatorID)); } /** * Register an hook function that transforms the *initial* dynamic schema when an operator is first added. * The SchemaTransformer is a function that takes the current schema and returns a new schema. * * Note: multiple transformers might be invoked when first constructing the initial schema, * transformers needs to be careful to not override other transformer's work. */ public registerInitialSchemaTransformer(schemaTransformer: SchemaTransformer) { this.initialSchemaTransformers.push(schemaTransformer); } /** * Returns the observable which outputs the operatorID of which the dynamic schema has changed. */ public getOperatorDynamicSchemaChangedStream(): Observable<{ operatorID: string; }> { return this.operatorDynamicSchemaChangedStream.asObservable(); } /** * Returns the current dynamic schema of all operators. */ public getDynamicSchemaMap(): ReadonlyMap { return this.dynamicSchemaMap; } public dynamicSchemaExists(operatorID: string): boolean { return this.dynamicSchemaMap.has(operatorID); } /** * Based on the operatorID, get the current dynamic operator schema that is created through autocomplete */ public getDynamicSchema(operatorID: string): OperatorSchema { const dynamicSchema = this.dynamicSchemaMap.get(operatorID); if (!dynamicSchema) { throw new Error(`dynamic schema not found for ${operatorID}`); } return dynamicSchema; } /** * Sets the dynamic schema of an operator. If the new schema is different, also emit dynamic schema changed event. * * The new dynamic schema is validated against the current operator properties. * If the changed new dynamic schema invalidates some property, then the invalid properties fields will be dropped. * */ public setDynamicSchema(operatorID: string, dynamicSchema: OperatorSchema): void { const currentDynamicSchema = this.dynamicSchemaMap.get(operatorID); // do nothing if old & new schema are the same if (isEqual(currentDynamicSchema, dynamicSchema)) { return; } // set the new dynamic schema this.dynamicSchemaMap.set(operatorID, dynamicSchema); this.operatorDynamicSchemaChangedStream.next({ operatorID }); } /** * Gets the initial dynamic schema of an operator type, which might be different from its static schema. * Currently, the only case is to change the source operators to have autocomplete of available tablenames. * * @param operator */ private getInitialDynamicSchema(operator: OperatorPredicate): OperatorSchema { let initialSchema = this.operatorMetadataService.getOperatorSchema(operator.operatorType); this.initialSchemaTransformers.forEach(transformer => (initialSchema = transformer(operator, initialSchema))); return initialSchema; } /** * Helper function to change a property in a json schema of an operator schema. * It recursively walks through the property field of a JSON schema, and tries to find the property name. * Once it finds the property name, it invokes the mutationFunction to get the new property and replaces the old property. * The mutationFunction optionally takes a input with current property of the propertyName and outputs the new mutated property. * * Returns a new object containing the new json schema property. */ public static mutateProperty( jsonSchemaToChange: CustomJSONSchema7, matchFunc: (propertyName: string, propertyValue: CustomJSONSchema7) => boolean, mutationFunc: (propertyName: string, propertyValue: CustomJSONSchema7) => CustomJSONSchema7 ): CustomJSONSchema7 { // recursively walks the JSON schema property tree to find the property name const mutatePropertyRecurse = (jsonSchema: JSONSchema7) => { const schemaProperties = jsonSchema.properties; const schemaDefinitions = jsonSchema.definitions; const schemaItems = jsonSchema.items; // nested JSON schema property can have 2 types: object or array const mutateObjectProperty = (objectProperty: { [key: string]: JSONSchema7Definition }) => { Object.entries(objectProperty).forEach(([propertyName, propertyValue]) => { if (typeof propertyValue === "boolean") { return; } if (matchFunc(propertyName, propertyValue as CustomJSONSchema7)) { objectProperty[propertyName] = mutationFunc(propertyName, propertyValue as CustomJSONSchema7); } else { mutatePropertyRecurse(propertyValue); } }); }; const mutateArrayProperty = (arrayProperty: JSONSchema7Definition[]) => { arrayProperty.forEach(item => { if (typeof item !== "boolean") { mutatePropertyRecurse(item); } }); }; if (schemaProperties) { mutateObjectProperty(schemaProperties); } if (schemaDefinitions) { mutateObjectProperty(schemaDefinitions); } if (schemaItems && typeof schemaItems !== "boolean") { if (Array.isArray(schemaItems)) { mutateArrayProperty(schemaItems); } else { mutatePropertyRecurse(schemaItems); } } }; // deep copy the schema first to avoid changing the original schema object const jsonSchemaCopy = cloneDeep(jsonSchemaToChange); mutatePropertyRecurse(jsonSchemaCopy); return jsonSchemaCopy; } } ================================================ FILE: frontend/src/app/workspace/service/execute-workflow/execute-workflow.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import "zone.js/testing"; import { DOCUMENT } from "@angular/core"; import { ExecutionState, LogicalPlan } from "../../types/execute-workflow.interface"; import { fakeAsync, flush, inject, TestBed, tick } from "@angular/core/testing"; import { ExecuteWorkflowService, FORM_DEBOUNCE_TIME_MS } from "./execute-workflow.service"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { UndoRedoService } from "../undo-redo/undo-redo.service"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../operator-metadata/stub-operator-metadata.service"; import { JointUIService } from "../joint-ui/joint-ui.service"; import { Observable, of } from "rxjs"; import { mockLogicalPlan_scan_result, mockWorkflowPlan_scan_result } from "./mock-workflow-plan"; import { HttpClient } from "@angular/common/http"; import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; import { WorkflowSnapshotService } from "../../../dashboard/service/user/workflow-snapshot/workflow-snapshot.service"; import { WorkflowSettings } from "src/app/common/type/workflow"; import { ComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; import { AuthService } from "src/app/common/service/user/auth.service"; import { StubAuthService } from "src/app/common/service/user/stub-auth.service"; import { UserService } from "src/app/common/service/user/user.service"; import { StubUserService } from "src/app/common/service/user/stub-user.service"; import { MockComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service"; import { commonTestProviders } from "../../../common/testing/test-utils"; class StubHttpClient { public post(): Observable { return of("a"); } } describe("ExecuteWorkflowService", () => { let service: ExecuteWorkflowService; let mockWorkflowSnapshotService: WorkflowSnapshotService; let mockDocument: Document; beforeEach(() => { mockDocument = { location: { origin: "https://texera.example.com", }, } as Document; TestBed.configureTestingModule({ providers: [ ExecuteWorkflowService, WorkflowActionService, WorkflowUtilService, { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService }, UndoRedoService, JointUIService, { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, { provide: HttpClient, useClass: StubHttpClient }, { provide: DOCUMENT, useValue: mockDocument }, { provide: AuthService, useClass: StubAuthService }, { provide: UserService, useClass: StubUserService }, ...commonTestProviders, ], }); service = TestBed.inject(ExecuteWorkflowService); mockWorkflowSnapshotService = TestBed.inject(WorkflowSnapshotService); }); it("should be created", inject([ExecuteWorkflowService], (injectedService: ExecuteWorkflowService) => { expect(injectedService).toBeTruthy(); })); it("should generate a logical plan request based on the workflow graph that is passed to the function", () => { const newLogicalPlan: LogicalPlan = ExecuteWorkflowService.getLogicalPlanRequest(mockWorkflowPlan_scan_result); expect(newLogicalPlan).toEqual(mockLogicalPlan_scan_result); }); it("should msg backend when executing workflow", fakeAsync(() => { const logicalPlan: LogicalPlan = ExecuteWorkflowService.getLogicalPlanRequest(mockWorkflowPlan_scan_result); const wsSendSpy = vi.spyOn((service as any).workflowWebsocketService, "send"); const settings = service["workflowActionService"].getWorkflowSettings(); service.sendExecutionRequest("", logicalPlan, settings, false, undefined); tick(FORM_DEBOUNCE_TIME_MS + 1); flush(); expect(wsSendSpy).toHaveBeenCalledTimes(1); })); it("it should raise an error when pauseWorkflow() is called without an execution state", () => { (service as any).currentState = { state: ExecutionState.Uninitialized }; expect(function () { service.pauseWorkflow(); }).toThrowError( new RegExp("cannot pause workflow, the current execution state is " + (service as any).currentState.state) ); }); it("it should raise an error when resumeWorkflow() is called without an execution state", () => { (service as any).currentState = { state: ExecutionState.Uninitialized }; expect(function () { service.resumeWorkflow(); }).toThrowError( new RegExp("cannot resume workflow, the current execution state is " + (service as any).currentState.state) ); }); it("should execute workflow with email notification successfully", () => { const executionName = "Test Execution"; const emailNotificationEnabled = true; const targetOperatorId = "test-operator-id"; const logicalPlanSpy = vi.spyOn(ExecuteWorkflowService, "getLogicalPlanRequest").mockReturnValue({} as LogicalPlan); const settingsSpy = vi .spyOn(service["workflowActionService"], "getWorkflowSettings") .mockReturnValue({} as WorkflowSettings); const resetExecutionStateSpy = vi.spyOn(service, "resetExecutionState"); const resetStatusSpy = vi.spyOn(service["workflowStatusService"], "resetStatus"); const sendExecutionRequestSpy = vi.spyOn(service, "sendExecutionRequest"); service.executeWorkflowWithEmailNotification(executionName, emailNotificationEnabled, targetOperatorId); expect(logicalPlanSpy).toHaveBeenCalledWith(service["workflowActionService"].getTexeraGraph(), targetOperatorId); expect(settingsSpy).toHaveBeenCalled(); expect(resetExecutionStateSpy).toHaveBeenCalled(); expect(resetStatusSpy).toHaveBeenCalled(); expect(sendExecutionRequestSpy).toHaveBeenCalledWith( executionName, expect.any(Object), expect.any(Object), emailNotificationEnabled ); }); it("should handle failure when executing workflow with email notification", () => { const executionName = "Test Execution"; const emailNotificationEnabled = true; const targetOperatorId = "test-operator-id"; const logicalPlanSpy = vi.spyOn(ExecuteWorkflowService, "getLogicalPlanRequest").mockImplementation(() => { throw "Logical plan error"; }); const resetExecutionStateSpy = vi.spyOn(service, "resetExecutionState"); const resetStatusSpy = vi.spyOn(service["workflowStatusService"], "resetStatus"); const sendExecutionRequestSpy = vi.spyOn(service, "sendExecutionRequest"); expect(() => { service.executeWorkflowWithEmailNotification(executionName, emailNotificationEnabled, targetOperatorId); }).toThrowError("Logical plan error"); expect(logicalPlanSpy).toHaveBeenCalledWith(service["workflowActionService"].getTexeraGraph(), targetOperatorId); expect(resetExecutionStateSpy).not.toHaveBeenCalled(); expect(resetStatusSpy).not.toHaveBeenCalled(); expect(sendExecutionRequestSpy).not.toHaveBeenCalled(); }); }); ================================================ FILE: frontend/src/app/workspace/service/execute-workflow/execute-workflow.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Inject, Injectable, DOCUMENT } from "@angular/core"; import { Observable, Subject } from "rxjs"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { WorkflowGraphReadonly } from "../workflow-graph/model/workflow-graph"; import { ExecutionState, ExecutionStateInfo, LogicalLink, LogicalOperator, LogicalPlan, } from "../../types/execute-workflow.interface"; import { WorkflowWebsocketService } from "../workflow-websocket/workflow-websocket.service"; import { OperatorCurrentTuples, RegionStateEvent, RegionUpdateEvent, ReplayExecutionInfo, TexeraWebsocketEvent, WorkflowFatalError, } from "../../types/workflow-websocket.interface"; import { isEqual } from "lodash-es"; import { PAGINATION_INFO_STORAGE_KEY, ResultPaginationInfo } from "../../types/result-table.interface"; import { sessionGetObject, sessionSetObject } from "../../../common/util/storage"; import { Version as version } from "src/environments/version"; import { NotificationService } from "src/app/common/service/notification/notification.service"; import { exhaustiveGuard } from "../../../common/util/switch"; import { WorkflowStatusService } from "../workflow-status/workflow-status.service"; import { intersection } from "../../../common/util/set"; import { WorkflowSettings } from "../../../common/type/workflow"; import { ComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; // TODO: change this declaration export const FORM_DEBOUNCE_TIME_MS = 150; export const EXECUTE_WORKFLOW_ENDPOINT = "queryplan/execute"; export const PAUSE_WORKFLOW_ENDPOINT = "pause"; export const RESUME_WORKFLOW_ENDPOINT = "resume"; /** * ExecuteWorkflowService sends the current workflow data to the backend * for execution, then receives backend's response and broadcast it to other components. * * ExecuteWorkflowService transforms the frontend workflow graph * into backend API compatible workflow graph before sending the request. * * Components should call executeWorkflow() function to execute the current workflow * * Components and Services should subscribe to getExecuteStartedStream() * in order to capture the event of workflow graph starts executing. * * Components and Services subscribe to getExecuteEndedStream() * for the event of the execution result (or errro) returned by the backend. * * @author Zuozhi Wang * @author Henry Chen */ @Injectable({ providedIn: "root", }) export class ExecuteWorkflowService { private currentState: ExecutionStateInfo = { state: ExecutionState.Uninitialized, }; private executionStateStream = new Subject<{ previous: ExecutionStateInfo; current: ExecutionStateInfo; }>(); private regionUpdateStream = new Subject(); private regionStateStream = new Subject(); // TODO: move this to another service, or redesign how this // information is stored on the frontend. private assignedWorkerIds: Map = new Map(); constructor( private workflowActionService: WorkflowActionService, private workflowWebsocketService: WorkflowWebsocketService, private workflowStatusService: WorkflowStatusService, private notificationService: NotificationService, @Inject(DOCUMENT) private document: Document, private computingUnitStatusService: ComputingUnitStatusService ) { workflowWebsocketService.websocketEvent().subscribe(event => { switch (event.type) { case "RegionUpdateEvent": this.regionUpdateStream.next(event); break; case "RegionStateEvent": this.regionStateStream.next(event); break; case "WorkerAssignmentUpdateEvent": this.assignedWorkerIds.set(event.operatorId, event.workerIds); break; default: // workflow status related event this.handleReconfigurationEvent(event); const newState = this.handleExecutionEvent(event); if (newState !== undefined) { this.updateExecutionState(newState); } } }); } public handleReconfigurationEvent(event: TexeraWebsocketEvent) { switch (event.type) { case "ModifyLogicResponse": if (!event.isValid) { this.notificationService.error(event.errorMessage); } else { this.notificationService.info("reconfiguration registered"); } return; case "ModifyLogicCompletedEvent": this.notificationService.info("reconfiguration on operator(s) " + event.opIds + " complete"); } } public handleExecutionEvent(event: TexeraWebsocketEvent): ExecutionStateInfo | undefined { switch (event.type) { case "WorkflowStateEvent": let newState = ExecutionState[event.state]; switch (newState) { case ExecutionState.Paused: if (this.currentState.state === ExecutionState.Paused) { return this.currentState; } else { return { state: ExecutionState.Paused, currentTuples: {} }; } case ExecutionState.Failed: // for failed state, backend will send an additional message after this status event. return undefined; default: return { state: newState }; } case "RecoveryStartedEvent": return { state: ExecutionState.Recovering }; case "OperatorCurrentTuplesUpdateEvent": let pausedCurrentTuples: Readonly>; if (this.currentState.state === ExecutionState.Paused) { pausedCurrentTuples = this.currentState.currentTuples; } else { pausedCurrentTuples = {}; } const currentTupleUpdate: Record = {}; currentTupleUpdate[event.operatorID] = event; const newCurrentTuples: Record = { ...currentTupleUpdate, ...pausedCurrentTuples, }; return { state: ExecutionState.Paused, currentTuples: newCurrentTuples, }; case "WorkflowErrorEvent": return { state: ExecutionState.Failed, errorMessages: event.fatalErrors.map(err => { return { ...err, message: err.message.replace("\\n", "
    ") }; }), }; default: return undefined; } } public getExecutionState(): ExecutionStateInfo { return this.currentState; } public getErrorMessages(): ReadonlyArray { if (this.currentState?.state === ExecutionState.Failed) { return this.currentState.errorMessages; } return []; } public executeWorkflowWithEmailNotification( executionName: string, emailNotificationEnabled: boolean, targetOperatorId?: string ): void { const logicalPlan = ExecuteWorkflowService.getLogicalPlanRequest( this.workflowActionService.getTexeraGraph(), targetOperatorId ); const settings = this.workflowActionService.getWorkflowSettings(); this.resetExecutionState(); this.workflowStatusService.resetStatus(); this.sendExecutionRequest(executionName, logicalPlan, settings, emailNotificationEnabled); } public executeWorkflow(executionName: string, targetOperatorId?: string): void { this.executeWorkflowWithEmailNotification(executionName, false, targetOperatorId); } public executeWorkflowWithReplay(replayExecutionInfo: ReplayExecutionInfo): void { const logicalPlan = ExecuteWorkflowService.getLogicalPlanRequest(this.workflowActionService.getTexeraGraph()); const settings = this.workflowActionService.getWorkflowSettings(); this.resetExecutionState(); this.workflowStatusService.resetStatus(); this.sendExecutionRequest( `Replay run of ${replayExecutionInfo.eid} to ${replayExecutionInfo.interaction}`, logicalPlan, settings, false, replayExecutionInfo ); } public sendExecutionRequest( executionName: string, logicalPlan: LogicalPlan, workflowSettings: WorkflowSettings, emailNotificationEnabled: boolean, replayExecutionInfo: ReplayExecutionInfo | undefined = undefined ): void { // Get the current computing unit ID from the status service const selectedUnit = this.computingUnitStatusService.getSelectedComputingUnitValue(); const computingUnitId = selectedUnit?.computingUnit.cuid; // Log a warning if no computing unit is selected if (computingUnitId === undefined) { console.warn("No computing unit selected for workflow execution"); } const workflowExecuteRequest = { executionName: executionName, engineVersion: version.hash, logicalPlan: logicalPlan, replayFromExecution: replayExecutionInfo, workflowSettings: workflowSettings, emailNotificationEnabled: emailNotificationEnabled, computingUnitId: computingUnitId, // Include the computing unit ID }; // wait for the form debounce to complete, then send window.setTimeout(() => { this.workflowWebsocketService.send("WorkflowExecuteRequest", workflowExecuteRequest); }, FORM_DEBOUNCE_TIME_MS); // add flag for new execution of workflow // so when next time the result panel is displayed, it will use new data // instead of those stored in the session storage const resultPaginationInfo = sessionGetObject(PAGINATION_INFO_STORAGE_KEY); if (resultPaginationInfo) { sessionSetObject(PAGINATION_INFO_STORAGE_KEY, { ...resultPaginationInfo, newWorkflowExecuted: true, }); } } public pauseWorkflow(): void { if (this.currentState === undefined || this.currentState.state !== ExecutionState.Running) { throw new Error("cannot pause workflow, the current execution state is " + this.currentState?.state); } this.workflowWebsocketService.send("WorkflowPauseRequest", {}); } public killWorkflow(): void { if ( this.currentState.state === ExecutionState.Uninitialized || this.currentState.state === ExecutionState.Completed ) { throw new Error("cannot kill workflow, the current execution state is " + this.currentState.state); } this.workflowWebsocketService.send("WorkflowKillRequest", {}); } public takeGlobalCheckpoint(): void { if ( this.currentState.state === ExecutionState.Uninitialized || this.currentState.state === ExecutionState.Completed ) { throw new Error("cannot take checkpoint, the current execution state is " + this.currentState.state); } this.workflowWebsocketService.send("WorkflowCheckpointRequest", {}); } public resumeWorkflow(): void { if (this.currentState.state !== ExecutionState.Paused) { throw new Error("cannot resume workflow, the current execution state is " + this.currentState.state); } this.workflowWebsocketService.send("WorkflowResumeRequest", {}); } public skipTuples(workers: ReadonlyArray): void { if (this.currentState.state !== ExecutionState.Paused) { throw new Error("cannot skip tuples, the current execution state is " + this.currentState.state); } this.workflowWebsocketService.send("SkipTupleRequest", { workers }); } public retryExecution(workers: ReadonlyArray): void { if (this.currentState.state !== ExecutionState.Paused) { throw new Error("cannot retry the current tuple, the current execution state is " + this.currentState.state); } this.workflowWebsocketService.send("RetryRequest", { workers }); } public modifyOperatorLogic(operatorID: string): void { if (this.currentState.state !== ExecutionState.Paused) { throw new Error("cannot modify logic, the current execution state is " + this.currentState.state); } const op = this.workflowActionService.getTexeraGraph().getOperator(operatorID); const operator: LogicalOperator = { ...op.operatorProperties, operatorID: op.operatorID, operatorType: op.operatorType, }; this.workflowWebsocketService.send("ModifyLogicRequest", { operator }); } public getExecutionStateStream(): Observable<{ previous: ExecutionStateInfo; current: ExecutionStateInfo; }> { return this.executionStateStream.asObservable(); } public getRegionUpdateStream(): Observable { return this.regionUpdateStream.asObservable(); } public getRegionStateStream(): Observable { return this.regionStateStream.asObservable(); } public resetExecutionState(): void { this.currentState = { state: ExecutionState.Uninitialized, }; } private updateExecutionState(stateInfo: ExecutionStateInfo): void { if (isEqual(this.currentState, stateInfo)) { return; } this.updateWorkflowActionLock(stateInfo); const previousState = this.currentState; // update current state this.currentState = stateInfo; // emit event this.executionStateStream.next({ previous: previousState, current: this.currentState, }); } /** * enables or disables workflow action service based on execution state */ private updateWorkflowActionLock(stateInfo: ExecutionStateInfo): void { switch (stateInfo.state) { case ExecutionState.Completed: case ExecutionState.Terminated: case ExecutionState.Failed: case ExecutionState.Uninitialized: case ExecutionState.Killed: this.workflowActionService.enableWorkflowModification(); return; case ExecutionState.Paused: case ExecutionState.Pausing: case ExecutionState.Recovering: case ExecutionState.Resuming: case ExecutionState.Running: case ExecutionState.Initializing: this.workflowActionService.disableWorkflowModification(); return; default: return exhaustiveGuard(stateInfo); } } /** * Transform a workflowGraph object to the HTTP request body according to the backend API. * * All the operators in the workflowGraph will be transformed to LogicalOperator objects, * where each operator has an operatorID and operatorType along with * the properties of the operator. * * All the links in the workflowGraph will be transformed to LogicalLink objects, * where each link will store its source id as its origin and target id as its destination. * * @param workflowGraph * @param targetOperatorId */ public static getLogicalPlanRequest(workflowGraph: WorkflowGraphReadonly, targetOperatorId?: string): LogicalPlan { const getInputPortOrdinal = (operatorID: string, inputPortID: string): number => { return workflowGraph.getOperator(operatorID).inputPorts.findIndex(port => port.portID === inputPortID); }; const getOutputPortOrdinal = (operatorID: string, outputPortID: string): number => { return workflowGraph.getOperator(operatorID).outputPorts.findIndex(port => port.portID === outputPortID); }; const subDAG = workflowGraph.getSubDAG(targetOperatorId); const operators: LogicalOperator[] = subDAG.operators.map(op => ({ ...op.operatorProperties, operatorID: op.operatorID, operatorType: op.operatorType, inputPorts: op.inputPorts, outputPorts: op.outputPorts, })); const links: LogicalLink[] = subDAG.links.map(link => { const outputPortIdx = getOutputPortOrdinal(link.source.operatorID, link.source.portID); const inputPortIdx = getInputPortOrdinal(link.target.operatorID, link.target.portID); return { fromOpId: link.source.operatorID, fromPortId: { id: outputPortIdx, internal: false }, toOpId: link.target.operatorID, toPortId: { id: inputPortIdx, internal: false }, }; }); const operatorIds = new Set(subDAG.operators.map(op => op.operatorID)); const opsToViewResult: string[] = Array.from(intersection(operatorIds, workflowGraph.getOperatorsToViewResult())); const opsToReuseResult: string[] = Array.from( intersection(operatorIds, workflowGraph.getOperatorsMarkedForReuseResult()) ); return { operators, links, opsToViewResult, opsToReuseResult }; } public getWorkerIds(operatorId: string): ReadonlyArray { return this.assignedWorkerIds.get(operatorId) || []; } } ================================================ FILE: frontend/src/app/workspace/service/execute-workflow/mock-result-data.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { WebDataUpdate, WebPaginationUpdate } from "../../types/execute-workflow.interface"; import { OperatorPredicate, Point } from "../../types/workflow-common.interface"; import { IndexableObject } from "ng-zorro-antd/core/types"; export const mockData: IndexableObject[] = [ { id: 1, layer: "Disk Space and I/O Managers", duty: "Manage space on disk (pages), including extents", slides: "slide 2", }, { id: 2, layer: "Buffer Manager", duty: "DB-oriented page replacement schemes", slides: "slide 3", }, { id: 3, layer: "System Catalog", duty: "Info about physical data, tables, indexes", slides: "slides 4 and 5", }, { id: 4, layer: "Access methods", duty: "Index structures for access based on field values.", slides: "B+ tree: slides 6 and 7. Hashing: slide 8. Indexing Performance: slide 9.", }, { id: 5, layer: "Plan Executor + Relational Operators", duty: "Runtime side of query processing", slides: "Sorting: slide 10. Selection+Projection: slide 11. Join: slide 12. Set operations: slide 13.", }, { id: 6, layer: "Query Optimizer", duty: "Rewrite query logically. Perform cost-based optimization", slides: "Cost estimation: slide 14. SystemR Optimizer: slide 15", }, ]; export const mockResultSnapshotUpdate: WebDataUpdate = { mode: { type: "SetSnapshotMode" }, table: mockData, }; export const mockResultPaginationUpdate: WebPaginationUpdate = { mode: { type: "PaginationMode" }, dirtyPageIndices: [1], totalNumTuples: mockData.length, }; export const mockResultOperator: OperatorPredicate = { operatorID: "operator-sink", operatorType: "ViewResults", operatorProperties: {}, inputPorts: [], outputPorts: [], showAdvanced: false, isDisabled: false, operatorVersion: "1.0", }; export const mockResultPoint: Point = { x: 1, y: 1, }; ================================================ FILE: frontend/src/app/workspace/service/execute-workflow/mock-workflow-plan.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { WorkflowGraph } from "../workflow-graph/model/workflow-graph"; import { mockResultPredicate, mockScanPredicate, mockScanResultLink, mockScanSentimentLink, mockSentimentPredicate, mockSentimentResultLink, } from "../workflow-graph/model/mock-workflow-data"; import { LogicalPlan } from "../../types/execute-workflow.interface"; export const mockWorkflowPlan_scan_result: WorkflowGraph = new WorkflowGraph( [mockScanPredicate, mockResultPredicate], [mockScanResultLink] ); export const mockLogicalPlan_scan_result: LogicalPlan = { operators: [ { ...mockResultPredicate.operatorProperties, operatorID: mockResultPredicate.operatorID, operatorType: mockResultPredicate.operatorType, inputPorts: mockResultPredicate.inputPorts, outputPorts: mockResultPredicate.outputPorts, }, { ...mockScanPredicate.operatorProperties, operatorID: mockScanPredicate.operatorID, operatorType: mockScanPredicate.operatorType, inputPorts: mockScanPredicate.inputPorts, outputPorts: mockScanPredicate.outputPorts, }, ], links: [ { fromOpId: mockScanPredicate.operatorID, fromPortId: { id: 0, internal: false }, toOpId: mockResultPredicate.operatorID, toPortId: { id: 0, internal: false }, }, ], opsToViewResult: [], opsToReuseResult: [], }; export const mockWorkflowPlan_scan_sentiment_result: WorkflowGraph = new WorkflowGraph( [mockScanPredicate, mockSentimentPredicate, mockResultPredicate], [mockScanSentimentLink, mockSentimentResultLink] ); export const mockLogicalPlan_scan_sentiment_result: LogicalPlan = { operators: [ { ...mockScanPredicate.operatorProperties, operatorID: mockScanPredicate.operatorID, operatorType: mockScanPredicate.operatorType, }, { ...mockSentimentPredicate.operatorProperties, operatorID: mockSentimentPredicate.operatorID, operatorType: mockSentimentPredicate.operatorType, }, { ...mockResultPredicate.operatorProperties, operatorID: mockResultPredicate.operatorID, operatorType: mockResultPredicate.operatorType, }, ], links: [ { fromOpId: mockScanPredicate.operatorID, fromPortId: { id: 0, internal: false }, toOpId: mockSentimentPredicate.operatorID, toPortId: { id: 0, internal: false }, }, { fromOpId: mockSentimentPredicate.operatorID, fromPortId: { id: 0, internal: false }, toOpId: mockResultPredicate.operatorID, toPortId: { id: 0, internal: false }, }, ], opsToViewResult: [], opsToReuseResult: [], }; ================================================ FILE: frontend/src/app/workspace/service/joint-ui/joint-ui.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // import { mockResultPredicate, mockPoint } from './../workflow-graph/model/mock-workflow-data'; // import { TestBed, inject } from '@angular/core/testing'; // import * as joint from 'jointjs'; // import { JointUIService, deleteButtonPath, sourceOperatorHandle, targetOperatorHandle } from './joint-ui.service'; // import { OperatorMetadataService } from '../operator-metadata/operator-metadata.service'; // import { StubOperatorMetadataService } from '../operator-metadata/stub-operator-metadata.service'; // import { mockScanPredicate, mockSentimentPredicate } from '../workflow-graph/model/mock-workflow-data'; // import { mockScanStatistic1, mockScanStatistic2 } from '../workflow-status/mock-workflow-status'; // describe('JointUIService', () => { // let service: JointUIService; // beforeEach(() => { // TestBed.configureTestingModule({ // providers: [ // JointUIService, // { provide: OperatorMetadataService, useClass: StubOperatorMetadataService }, // ], // }); // service = TestBed.get(JointUIService); // }); // it('should be created', inject([JointUIService], (injectedService: JointUIService) => { // expect(injectedService).toBeTruthy(); // })); // /** // * Check if the getJointOperatorElement() can successfully creates a JointJS Element // */ // it('should create an JointJS Element successfully when the function is called', () => { // const result = service.getJointOperatorElement( // mockScanPredicate, mockPoint); // expect(result).toBeTruthy(); // }); // /** // * Check if the error in getJointOperatorElement() is correctly thrown // */ // it('should throw an error with an non existing operator', () => { // expect(() => { // service.getJointOperatorElement( // { // operatorID: 'nonexistOperator', // operatorType: 'nonexistOperatorType', // operatorProperties: {}, // inputPorts: [], // outputPorts: [], // showAdvanced: true // }, // mockPoint // ); // }).toThrowError(new RegExp(`doesn't exist`)); // }); // /** // * Check if the getJointTooltipElement() can successfully creates a JointJS Element // */ // it('should create an JointJS Element successfully when the function is called', () => { // const result = service.getJointOperatorStatusTooltipElement( // mockScanPredicate, mockPoint); // expect(result).toBeTruthy(); // }); // /** // * Check if the error in getJointTooltipElement() is correctly thrown // */ // it('should throw an error with an non existing operator', () => { // expect(() => { // service.getJointOperatorStatusTooltipElement( // { // operatorID: 'nonexistOperator', // operatorType: 'nonexistOperatorType', // operatorProperties: {}, // inputPorts: [], // outputPorts: [], // showAdvanced: true // }, // mockPoint // ); // }).toThrowError(new RegExp(`doesn't exist`)); // }); // /** // * Check if showTooltip/hideTooltip works properly // */ // it('should reveal/hide tooltip and its content when showToolTip/hideTooltip is called', () => { // // creating a JointJS graph with one operator and its tooltip // const jointGraph = new joint.dia.Graph(); // const jointPaperOptions: joint.dia.Paper.Options = {model: jointGraph}; // const paper = new joint.dia.Paper(jointPaperOptions); // jointGraph.addCell([ // service.getJointOperatorElement( // mockScanPredicate, // mockPoint // ), // service.getJointOperatorStatusTooltipElement( // mockScanPredicate, // mockPoint // ) // ]); // // tooltip should not be shown when operator is just created // // disply attr should be none // const tooltipId = JointUIService.getOperatorStatusTooltipElementID(mockScanPredicate.operatorID); // const graph_tooltip1 = jointGraph.getCell(tooltipId); // expect(graph_tooltip1.attr('polygon')['display']).toEqual('none'); // expect(graph_tooltip1.attr('#operatorCount')['display']).toEqual('none'); // expect(graph_tooltip1.attr('#operatorSpeed')['display']).toEqual('none'); // // showTooltip removes display == none attr to show tooltip // service.showOperatorStatusToolTip(paper, tooltipId); // expect(graph_tooltip1.attr('polygon')['display']).toBeUndefined(); // expect(graph_tooltip1.attr('#operatorCount')['display']).toBeUndefined(); // expect(graph_tooltip1.attr('#operatorSpeed')['display']).toBeUndefined(); // // hideTooltip adds display == none attr to hide tooltip // service.hideOperatorStatusToolTip(paper, tooltipId); // expect(graph_tooltip1.attr('polygon')['display']).toEqual('none'); // expect(graph_tooltip1.attr('#operatorCount')['display']).toEqual('none'); // expect(graph_tooltip1.attr('#operatorSpeed')['display']).toEqual('none'); // }); // /** // * check if tooltip content can be updated properly // */ // it('should update the content in the tooltip when changeOperatorTooltipInfo is called', () => { // // creating a JointJS graph with one operator and its tooltip // const jointGraph = new joint.dia.Graph(); // const jointPaperOptions: joint.dia.Paper.Options = {model: jointGraph}; // const paper = new joint.dia.Paper(jointPaperOptions); // jointGraph.addCell([ // service.getJointOperatorElement( // mockScanPredicate, // mockPoint // ), // service.getJointOperatorStatusTooltipElement( // mockScanPredicate, // mockPoint // ) // ]); // const tooltipId = JointUIService.getOperatorStatusTooltipElementID(mockScanPredicate.operatorID); // const graph_tooltip = jointGraph.getCell(tooltipId); // // tooltip should not contain any content when created // expect(graph_tooltip.attr('#operatorCount')['text']).toBeUndefined(); // expect(graph_tooltip.attr('#operatorSpeed')['text']).toBeUndefined(); // // updating it with mock statistics // service.changeOperatorStatusTooltipInfo(paper, tooltipId, mockScanStatistic1); // expect(graph_tooltip.attr('#operatorCount')['text']).toEqual('Output:' + mockScanStatistic1.outputCount + ' tuples'); // expect(graph_tooltip.attr('#operatorSpeed')['text']).toEqual('Speed:' + mockScanStatistic1.speed + ' tuples/s'); // // updating it with another mock statistics // service.changeOperatorStatusTooltipInfo(paper, tooltipId, mockScanStatistic2); // expect(graph_tooltip.attr('#operatorCount')['text']).toEqual('Output:' + mockScanStatistic2.outputCount + ' tuples'); // expect(graph_tooltip.attr('#operatorSpeed')['text']).toEqual('Speed:' + mockScanStatistic2.speed + ' tuples/s'); // }); // it('should change the operator state name and color when changeOperatorStates is called', () => { // // creating a JointJS graph with one operator and its tooltip // const jointGraph = new joint.dia.Graph(); // const jointPaperOptions: joint.dia.Paper.Options = {model: jointGraph}; // const paper = new joint.dia.Paper(jointPaperOptions); // jointGraph.addCell( // service.getJointOperatorElement( // mockScanPredicate, // mockPoint // )); // // operator state name and color should be changed correctly // const graph_operator = jointGraph.getCell(mockScanPredicate.operatorID); // expect(graph_operator.attr('#operatorStates')['text']).toEqual('Ready'); // expect(graph_operator.attr('#operatorStates')['fill']).toEqual('green'); // service.changeOperatorStates(paper, mockScanPredicate.operatorID, OperatorStates.Initializing); // expect(graph_operator.attr('#operatorStates')['text']).toEqual('Initializing'); // expect(graph_operator.attr('#operatorStates')['fill']).toEqual('orange'); // service.changeOperatorStates(paper, mockScanPredicate.operatorID, OperatorStates.Running); // expect(graph_operator.attr('#operatorStates')['text']).toEqual('Running'); // expect(graph_operator.attr('#operatorStates')['fill']).toEqual('orange'); // service.changeOperatorStates(paper, mockScanPredicate.operatorID, OperatorStates.Pausing); // expect(graph_operator.attr('#operatorStates')['text']).toEqual('Pausing'); // expect(graph_operator.attr('#operatorStates')['fill']).toEqual('red'); // service.changeOperatorStates(paper, mockScanPredicate.operatorID, OperatorStates.Paused); // expect(graph_operator.attr('#operatorStates')['text']).toEqual('Paused'); // expect(graph_operator.attr('#operatorStates')['fill']).toEqual('red'); // service.changeOperatorStates(paper, mockScanPredicate.operatorID, OperatorStates.Completed); // expect(graph_operator.attr('#operatorStates')['text']).toEqual('Completed'); // expect(graph_operator.attr('#operatorStates')['fill']).toEqual('green'); // }); // /** // * Check if the number of inPorts and outPorts created by getJointOperatorElement() // * matches the port number specified by the operator metadata // */ // it('should create correct number of inPorts and outPorts based on operator metadata', () => { // const element1 = service.getJointOperatorElement(mockScanPredicate, mockPoint); // const element2 = service.getJointOperatorElement(mockSentimentPredicate, mockPoint); // const element3 = service.getJointOperatorElement(mockResultPredicate, mockPoint); // const inPortCount1 = element1.getPorts().filter(port => port.group === 'in').length; // const outPortCount1 = element1.getPorts().filter(port => port.group === 'out').length; // const inPortCount2 = element2.getPorts().filter(port => port.group === 'in').length; // const outPortCount2 = element2.getPorts().filter(port => port.group === 'out').length; // const inPortCount3 = element3.getPorts().filter(port => port.group === 'in').length; // const outPortCount3 = element3.getPorts().filter(port => port.group === 'out').length; // expect(inPortCount1).toEqual(0); // expect(outPortCount1).toEqual(1); // expect(inPortCount2).toEqual(1); // expect(outPortCount2).toEqual(1); // expect(inPortCount3).toEqual(1); // expect(outPortCount3).toEqual(0); // }); // /** // * Check if the custom attributes / svgs are correctly used by the JointJS graph // */ // it('should apply the custom SVG styling to the JointJS element', () => { // const graph = new joint.dia.Graph(); // // operator and its tooltip element should be added together // graph.addCell([ // service.getJointOperatorElement( // mockScanPredicate, // mockPoint // ), // service.getJointOperatorStatusTooltipElement( // mockScanPredicate, // mockPoint // ) // ]); // graph.addCell([ // service.getJointOperatorElement( // mockResultPredicate, // { x: 500, y: 100 } // ), // service.getJointOperatorStatusTooltipElement( // mockResultPredicate, // { x: 500, y: 100 } // ) // ]); // const link = JointUIService.getJointLinkCell({ // linkID: 'link-1', // source: { operatorID: 'operator1', portID: 'out0' }, // target: { operatorID: 'operator2', portID: 'in0' }, // }); // graph.addCell(link); // const graph_operator1 = graph.getCell(mockScanPredicate.operatorID); // const graph_operator2 = graph.getCell(mockResultPredicate.operatorID); // const graph_link = graph.getLinks()[0]; // const graph_tooltip1 = graph.getCell(JointUIService.getOperatorStatusTooltipElementID(mockScanPredicate.operatorID)); // // testing getCustomTooltipStyleAttrs() // // style: {'pointer-events': 'none'} makes tooltip unselectable thus not draggable // expect(graph_tooltip1.attr('polygon')).toEqual({ // fill: '#FFFFFF', 'follow-scale': true, stroke: 'purple', 'stroke-width': '2', // rx: '5px', ry: '5px', refPoints: '0,30 150,30 150,120 85,120 75,150 65,120 0,120', // display: 'none', style: {'pointer-events': 'none'} // }); // expect(graph_tooltip1.attr('#operatorCount')).toEqual({ // fill: '#595959', 'font-size': '12px', ref: 'polygon', // 'y-alignment': 'middle', // 'x-alignment': 'left', // 'ref-x': .05, 'ref-y': .2, // display: 'none', style: {'pointer-events': 'none'} // }); // expect(graph_tooltip1.attr('#operatorSpeed')).toEqual({ // fill: '#595959', // ref: 'polygon', // 'x-alignment': 'left', // 'font-size': '12px', // 'ref-x': .05, 'ref-y': .5, // display: 'none', style: {'pointer-events': 'none'} // }); // // testing getCustomOperatorStyleAttrs() // expect(graph_operator1.attr('#operatorStates')).toEqual({ // text: 'Ready' , fill: 'green', 'font-size': '14px', 'visible' : false, // 'ref-x': 0.5, 'ref-y': -10, ref: 'rect', 'y-alignment': 'middle', 'x-alignment': 'middle' // }); // expect(graph_operator1.attr('rect')).toEqual( // { fill: '#FFFFFF', 'follow-scale': true, stroke: 'red', 'stroke-width': '2', // rx: '5px', ry: '5px' } // ); // expect(graph_operator2.attr('rect')).toEqual( // { fill: '#FFFFFF', 'follow-scale': true, stroke: 'red', 'stroke-width': '2', // rx: '5px', ry: '5px' } // ); // expect(graph_operator1.attr('.delete-button')).toEqual( // { // x: 60, y: -20, cursor: 'pointer', // fill: '#D8656A', event: 'element:delete' // } // ); // expect(graph_operator2.attr('.delete-button')).toEqual( // { // x: 60, y: -20, cursor: 'pointer', // fill: '#D8656A', event: 'element:delete' // } // ); // // testing getDefaultLinkElement() // expect(graph_link.attr('.marker-source/d')).toEqual(sourceOperatorHandle); // expect(graph_link.attr('.marker-target/d')).toEqual(targetOperatorHandle); // expect(graph_link.attr('.tool-remove path/d')).toEqual(deleteButtonPath); // }); // }); describe("JointUIService", () => { // Pre-existing spec body is commented out. Placeholder keeps Vitest's // discovery happy; rewriting the real tests against the new test // runner is tracked in #4861. it.todo("add unit tests for JointUIService"); }); ================================================ FILE: frontend/src/app/workspace/service/joint-ui/joint-ui.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { OperatorSchema } from "../../types/operator-schema.interface"; import { CommentBox, OperatorLink, OperatorPredicate, Point } from "../../types/workflow-common.interface"; import { OperatorState, OperatorStatistics } from "../../types/execute-workflow.interface"; import * as joint from "jointjs"; import { fromEventPattern, Observable } from "rxjs"; import { Coeditor } from "../../../common/type/user"; import { OperatorResultCacheStatus } from "../../types/workflow-websocket.interface"; /** * Defines the SVG path for the delete button */ export const deleteButtonPath = "M14.59 8L12 10.59 9.41 8 8 9.41 10.59 12 8 14.59 9.41 16 12 13.41" + " 14.59 16 16 14.59 13.41 12 16 9.41 14.59 8zM12 2C6.47 2 2 6.47 2" + " 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"; /** * Defines the HTML SVG element for the delete button and customizes the look */ export const deleteButtonSVG = ` delete operator `; export const addPortButtonPath = ` `; export const removePortButtonPath = ` `; export const addInputPortButtonSVG = ` ${addPortButtonPath} add port `; export const removeInputPortButtonSVG = ` ${removePortButtonPath} remove port `; export const addOutputPortButtonSVG = ` ${addPortButtonPath} add port `; export const removeOutputPortButtonSVG = ` ${removePortButtonPath} remove port `; /** * Defines the SVG for the chat button (message icon) * This button allows users to send feedback to agents about this operator */ export const chatButtonSVG = ` Chat with agent about this operator `; /** * Defines the handle (the square at the end) of the source operator for a link */ export const sourceOperatorHandle = "M 0 0 L 0 8 L 8 8 L 8 0 z"; /** * Defines the handle (the arrow at the end) of the target operator for a link */ export const targetOperatorHandle = "M 12 0 L 0 6 L 12 12 z"; export const operatorReuseCacheTextClass = "texera-operator-result-reuse-text"; export const operatorReuseCacheIconClass = "texera-operator-result-reuse-icon"; export const operatorViewResultIconClass = "texera-operator-view-result-icon"; export const operatorStateClass = "texera-operator-state"; export const operatorCoeditorEditingClass = "texera-operator-coeditor-editing"; export const operatorCoeditorChangedPropertyClass = "texera-operator-coeditor-changed-property"; export const operatorAgentActionProgressClass = "texera-operator-agent-action-progress"; export const operatorIconClass = "texera-operator-icon"; export const operatorNameClass = "texera-operator-name"; export const operatorFriendlyNameClass = "texera-operator-friendly-name"; export const operatorPortMetricsClass = "texera-operator-port-metrics"; const operatorWorkerCountClass = "operator-worker-count"; export const linkPathStrokeColor = "#919191"; /** * Extends a basic Joint operator element and adds our own HTML markup. * Our own HTML markup includes the SVG element for the delete button, * which will show a red delete button on the top right corner */ class TexeraCustomJointElement extends joint.shapes.devs.Model { static getMarkup(dynamicInputPorts: boolean, dynamicOutputPorts: boolean): string { return ` ${deleteButtonSVG} ${chatButtonSVG} ${dynamicInputPorts ? addInputPortButtonSVG : ""} ${dynamicInputPorts ? removeInputPortButtonSVG : ""} ${dynamicOutputPorts ? addOutputPortButtonSVG : ""} ${dynamicOutputPorts ? removeOutputPortButtonSVG : ""} `; } } class TexeraCustomCommentElement extends joint.shapes.devs.Model { markup = ` ${deleteButtonSVG} `; } /** * JointUIService controls the shape of an operator and a link * when they are displayed by JointJS. * * This service alters the basic JointJS element by: * - setting the ID of the JointJS element to be the same as Texera's OperatorID * - changing the look of the operator box (size, colors, lines, etc..) * - adding input and output ports to the box based on the operator metadata * - changing the SVG element and CSS styles of operators, links, ports, etc.. * - adding a new delete button and the callback function of the delete button, * (original JointJS element doesn't have a built-in delete button) * * @author Henry Chen * @author Zuozhi Wang */ @Injectable({ providedIn: "root", }) export class JointUIService { public static readonly DEFAULT_OPERATOR_WIDTH = 60; public static readonly DEFAULT_OPERATOR_HEIGHT = 60; public static readonly DEFAULT_GROUP_MARGIN = 50; public static readonly DEFAULT_GROUP_MARGIN_BOTTOM = 40; public static readonly DEFAULT_COMMENT_WIDTH = 32; public static readonly DEFAULT_COMMENT_HEIGHT = 32; private operatorSchemas: ReadonlyArray = []; constructor(private operatorMetadataService: OperatorMetadataService) { // initialize the operator information // subscribe to operator metadata observable this.operatorMetadataService.getOperatorMetadata().subscribe(value => (this.operatorSchemas = value.operators)); } /** * Gets the JointJS UI Element object based on the operator predicate. * A JointJS Element could be added to the JointJS graph to let JointJS display the operator accordingly. * * The function checks if the operatorType exists in the metadata, * if it doesn't, the program will throw an error. * * The function returns an element that has our custom style, * which are specified in getCustomOperatorStyleAttrs() and getCustomPortStyleAttrs() * * * @param operator OperatorPredicate, the type of the operator * @param point Point, the top-left-originated position of the operator element (relative to JointJS paper, not absolute position) * * @returns JointJS Element */ public getJointOperatorElement(operator: OperatorPredicate, point: Point): joint.dia.Element { // check if the operatorType exists in the operator metadata const operatorSchema = this.operatorSchemas.find(op => op.operatorType === operator.operatorType); if (operatorSchema === undefined) { throw new Error(`operator type ${operator.operatorType} doesn't exist`); } // construct a custom Texera JointJS operator element // and customize the styles of the operator box and ports const operatorElement = new TexeraCustomJointElement({ position: point, size: { width: JointUIService.DEFAULT_OPERATOR_WIDTH, height: JointUIService.DEFAULT_OPERATOR_HEIGHT, }, attrs: JointUIService.getCustomOperatorStyleAttrs( operator, operator.customDisplayName ?? operatorSchema.additionalMetadata.userFriendlyName, operatorSchema.operatorType, operatorSchema.additionalMetadata.userFriendlyName ), ports: { groups: { in: { attrs: JointUIService.getCustomPortStyleAttrs() }, out: { attrs: JointUIService.getCustomPortStyleAttrs() }, }, }, markup: TexeraCustomJointElement.getMarkup( operator.dynamicInputPorts ?? false, operator.dynamicOutputPorts ?? false ), }); // set operator element ID to be operator ID operatorElement.set("id", operator.operatorID); operatorElement.set("z", 1); // set the input ports and output ports based on operator predicate operator.inputPorts.forEach(port => operatorElement.addPort({ group: "in", id: port.portID, attrs: { ".port-label": { text: port.displayName ?? "", event: "input-port-label:pointerdown", }, }, label: { position: { name: "left", args: { x: -5, y: 10 }, }, }, }) ); operator.outputPorts.forEach(port => operatorElement.addPort({ group: "out", id: port.portID, attrs: { ".port-label": { text: port.displayName ?? "", event: "output-port-label:pointerdown", }, }, label: { position: { name: "right", args: { x: 5, y: -10 }, }, }, }) ); return operatorElement; } public changeOperatorStatistics( jointPaper: joint.dia.Paper, operatorID: string, statistics: OperatorStatistics | undefined, isSource: boolean, isSink: boolean ): void { if (!statistics) { this.changeOperatorState(jointPaper, operatorID, OperatorState.Uninitialized); return; } this.changeOperatorState(jointPaper, operatorID, statistics.operatorState); const element = jointPaper.getModelById(operatorID) as joint.shapes.devs.Model; const allPorts = element.getPorts(); const inPorts = allPorts.filter(p => p.group === "in"); const outPorts = allPorts.filter(p => p.group === "out"); const inputMetrics = statistics.inputPortMetrics; const outputMetrics = statistics.outputPortMetrics; const workerCount = statistics.numWorkers ?? 1; element.attr(`.${operatorWorkerCountClass}/text`, "#workers: " + String(workerCount)); inPorts.forEach(portDef => { const portId = portDef.id; if (portId != null) { const parts = portId.split("-"); const numericSuffix = parts.length > 1 ? parts[1] : portId; const count: number = inputMetrics[numericSuffix] ?? 0; const rawAttrs = (portDef.attrs as any) || {}; const oldText: string = (rawAttrs[".port-label"] && rawAttrs[".port-label"].text) || ""; let originalName = oldText.includes(":") ? oldText.split(":", 1)[0].trim() : oldText; if (!originalName) { originalName = portId; } const labelText = count.toLocaleString(); element.portProp(portId, "attrs/.port-label/text", labelText); } }); outPorts.forEach(portDef => { const portId = portDef.id; if (portId != null) { const parts = portId.split("-"); const numericSuffix = parts.length > 1 ? parts[1] : portId; const count: number = outputMetrics[numericSuffix] ?? 0; const rawAttrs = (portDef.attrs as any) || {}; const oldText: string = (rawAttrs[".port-label"] && rawAttrs[".port-label"].text) || ""; let originalName = oldText.includes(":") ? oldText.split(":", 1)[0].trim() : oldText; if (!originalName) { originalName = portId; } const labelText = count.toLocaleString(); element.portProp(portId, "attrs/.port-label/text", labelText); } }); this.changeOperatorState(jointPaper, operatorID, statistics.operatorState); } public foldOperatorDetails(jointPaper: joint.dia.Paper, operatorID: string): void { jointPaper.getModelById(operatorID).attr({ [`.${operatorStateClass}`]: { visibility: "hidden" }, [`.${operatorPortMetricsClass}`]: { visibility: "hidden" }, ".delete-button": { visibility: "hidden" }, ".chat-button": { visibility: "hidden" }, ".add-input-port-button": { visibility: "hidden" }, ".add-output-port-button": { visibility: "hidden" }, ".remove-input-port-button": { visibility: "hidden" }, ".remove-output-port-button": { visibility: "hidden" }, }); } public unfoldOperatorDetails(jointPaper: joint.dia.Paper, operatorID: string): void { jointPaper.getModelById(operatorID).attr({ [`.${operatorStateClass}`]: { visibility: "visible" }, [`.${operatorPortMetricsClass}`]: { visibility: "visible" }, ".delete-button": { visibility: "visible" }, ".chat-button": { visibility: "visible" }, ".add-input-port-button": { visibility: "visible" }, ".add-output-port-button": { visibility: "visible" }, ".remove-input-port-button": { visibility: "visible" }, ".remove-output-port-button": { visibility: "visible" }, }); const element = jointPaper.getModelById(operatorID) as joint.shapes.devs.Model; if (!element) { return; } } public changeOperatorState(jointPaper: joint.dia.Paper, operatorID: string, operatorState: OperatorState): void { let fillColor: string; switch (operatorState) { case OperatorState.Ready: fillColor = "#a6bd37"; break; case OperatorState.Completed: fillColor = "green"; break; case OperatorState.Pausing: case OperatorState.Paused: fillColor = "magenta"; break; case OperatorState.Running: fillColor = "orange"; break; default: fillColor = "gray"; break; } jointPaper.getModelById(operatorID).attr({ [`.${operatorStateClass}`]: { text: operatorState.toString(), fill: fillColor }, "rect.body": { stroke: fillColor }, [`.${operatorPortMetricsClass}`]: { fill: fillColor }, [`.${operatorWorkerCountClass}`]: { fill: fillColor }, }); const element = jointPaper.getModelById(operatorID) as joint.shapes.devs.Model; const allPorts = element.getPorts(); const inPorts = allPorts.filter(p => p.group === "in"); inPorts.forEach(p => { if (p.id != null) { element.portProp(p.id, "attrs/.port-label/fill", fillColor); } }); const outPorts = allPorts.filter(p => p.group === "out"); outPorts.forEach(p => { if (p.id != null) { element.portProp(p.id, "attrs/.port-label/fill", fillColor); } }); } /** * This method will change the operator's color based on the validation status * valid : default color * invalid: red * * @param jointPaper * @param operatorID * @param isOperatorValid */ public changeOperatorColor(jointPaper: joint.dia.Paper, operatorID: string, isOperatorValid: boolean): void { if (isOperatorValid) { jointPaper.getModelById(operatorID).attr("rect.body/stroke", "#CFCFCF"); } else { jointPaper.getModelById(operatorID).attr("rect.body/stroke", "red"); } } public changeOperatorDisableStatus(jointPaper: joint.dia.Paper, operator: OperatorPredicate): void { jointPaper.getModelById(operator.operatorID).attr("rect.body/fill", JointUIService.getOperatorFillColor(operator)); } public changeOperatorViewResultStatus( jointPaper: joint.dia.Paper, operator: OperatorPredicate, viewResult?: boolean ): void { const icon = JointUIService.getOperatorViewResultIcon(operator); jointPaper.getModelById(operator.operatorID).attr(`.${operatorViewResultIconClass}/xlink:href`, icon); } public changeOperatorReuseCacheStatus( jointPaper: joint.dia.Paper, operator: OperatorPredicate, cacheStatus?: OperatorResultCacheStatus ): void { JointUIService.getOperatorCacheDisplayText(operator, cacheStatus); const cacheIcon = JointUIService.getOperatorCacheIcon(operator, cacheStatus); jointPaper.getModelById(operator.operatorID).attr(`.${operatorReuseCacheIconClass}/xlink:href`, cacheIcon); const icon = JointUIService.getOperatorViewResultIcon(operator); jointPaper.getModelById(operator.operatorID).attr(`.${operatorViewResultIconClass}/xlink:href`, icon); } public changeOperatorJointDisplayName( operator: OperatorPredicate, jointPaper: joint.dia.Paper, displayName: string ): void { jointPaper.getModelById(operator.operatorID).attr(`.${operatorNameClass}/text`, displayName); } public getCommentElement(commentBox: CommentBox): joint.dia.Element { const basic = new joint.shapes.standard.Rectangle(); if (commentBox.commentBoxPosition) basic.position(commentBox.commentBoxPosition.x, commentBox.commentBoxPosition.y); else basic.position(0, 0); basic.resize(120, 50); const commentElement = new TexeraCustomCommentElement({ position: commentBox.commentBoxPosition || { x: 0, y: 0 }, size: { width: JointUIService.DEFAULT_COMMENT_WIDTH, height: JointUIService.DEFAULT_COMMENT_HEIGHT, }, attrs: JointUIService.getCustomCommentStyleAttrs(), }); commentElement.set("id", commentBox.commentBoxID); return commentElement; } /** * This function converts a Texera source and target OperatorPort to * a JointJS link cell that could be added to the JointJS. * * @param link * @returns JointJS Link Cell */ public static getJointLinkCell(link: OperatorLink): joint.dia.Link { const jointLinkCell = JointUIService.getDefaultLinkCell(); jointLinkCell.set("source", { id: link.source.operatorID, port: link.source.portID, }); jointLinkCell.set("target", { id: link.target.operatorID, port: link.target.portID, }); jointLinkCell.set("id", link.linkID); jointLinkCell.set("z", 0); return jointLinkCell; } /** * This function will creates a custom JointJS link cell using * custom attributes / styles to display the operator. * * This function defines the svg properties for each part of link, such as the * shape of the arrow or the link. Other styles are defined in the * "app/workspace/component/workflow-editor/workflow-editor.component.scss". * * The reason for separating styles in svg and css is that while we can * change the shape of the operators in svg, according to JointJS official * website, https://resources.jointjs.com/tutorial/element-styling , * CSS properties have higher precedence over SVG attributes. * * As a result, a separate css/scss file is required to override the default * style of the operatorLink. * * @returns JointJS Link */ public static getDefaultLinkCell(): joint.dia.Link { return new joint.dia.Link({ router: { name: "manhattan", }, connector: { name: "rounded", }, toolMarkup: ` Remove link. `, attrs: { ".connection": { stroke: linkPathStrokeColor, "stroke-width": "2px", }, ".connection-wrap": { "stroke-width": "0px", // 'display': 'inline' }, ".marker-source": { d: sourceOperatorHandle, stroke: "none", fill: "#919191", }, ".marker-arrowhead-group-source .marker-arrowhead": { d: sourceOperatorHandle, }, ".marker-target": { d: targetOperatorHandle, stroke: "none", fill: "#919191", }, ".marker-arrowhead-group-target .marker-arrowhead": { d: targetOperatorHandle, }, ".tool-remove": { fill: "#D8656A", width: 24, display: "none", }, ".tool-remove path": { d: deleteButtonPath, }, ".tool-remove circle": {}, }, }); } /** * This function changes the default svg of the operator ports. * It hides the port label that will display 'out/in' beside the operators. * * @returns the custom attributes of the ports */ public static getCustomPortStyleAttrs(): joint.attributes.SVGAttributes { return { ".port-body": { fill: "#A0A0A0", r: 5, stroke: "none", }, ".port-label": { visibility: "visible", event: "input-label:evt", dblclick: "input-label:dbclick", pointerdblclick: "input-label:pointerdblclick", ref: ".port-body", "ref-y": 0.5, "y-alignment": "middle", }, }; } /** * This function create a custom svg style for the operator * @returns the custom attributes of the tooltip. */ public static getCustomOperatorStatusTooltipStyleAttrs(): joint.shapes.devs.ModelSelectors { return { "element-node": { style: { "pointer-events": "none" }, }, polygon: { fill: "#FFFFFF", "follow-scale": true, stroke: "purple", "stroke-width": "2", rx: "5px", ry: "5px", refPoints: "0,30 150,30 150,120 85,120 75,150 65,120 0,120", display: "none", style: { "pointer-events": "none" }, }, "#operatorCount": { fill: "#595959", "font-size": "12px", ref: "polygon", "y-alignment": "middle", "x-alignment": "left", "ref-x": 0.05, "ref-y": 0.2, display: "none", style: { "pointer-events": "none" }, }, }; } /** * This function creates a custom svg style for the operator. * This function also makes the delete button defined above to emit the delete event that will * be captured by JointJS paper using event name *element:delete* * * @param operator * @param operatorDisplayName the name of the operator that will display on the UI * @param operatorType * @param operatorFriendlyName * @returns the custom attributes of the operator */ public static getCustomOperatorStyleAttrs( operator: OperatorPredicate, operatorDisplayName: string, operatorType: string, operatorFriendlyName: string ): joint.shapes.devs.ModelSelectors { return { ".texera-operator-coeditor-editing": { text: "", "font-size": "14px", "font-weight": "bold", visibility: "hidden", "ref-x": -50, "ref-y": 100, ref: "rect.body", "y-alignment": "middle", "x-alignment": "start", }, ".texera-operator-coeditor-changed-property": { text: "", "font-weight": "bold", "font-size": "14px", visibility: "hidden", "ref-x": 0.5, "ref-y": 120, ref: "rect.body", "y-alignment": "middle", "x-alignment": "middle", }, ".texera-operator-agent-action-progress": { text: "", "font-size": "11px", "font-weight": "bold", "font-family": "'Inter', 'SF Pro Display', -apple-system, sans-serif", visibility: "hidden", "ref-x": 0.5, "ref-y": 95, ref: "rect.body", "text-anchor": "middle", "x-alignment": "middle", "y-alignment": "middle", }, ".texera-operator-state": { text: "", "font-size": "14px", visibility: "hidden", "ref-x": 0.5, "ref-y": 100, ref: "rect.body", "y-alignment": "middle", "x-alignment": "middle", }, ".texera-operator-abbreviated-count": { text: "", fill: "green", "font-size": "14px", visibility: "visible", "ref-x": 0.5, "ref-y": -30, ref: "rect.body", "y-alignment": "middle", "x-alignment": "middle", }, ".texera-operator-port-metrics": { text: "", fill: "green", "font-size": "14px", visibility: "hidden", "ref-x": 0.5, "ref-y": -70, ref: "rect.body", "y-alignment": "middle", "x-alignment": "middle", }, ".texera-operator-processed-count": { text: "", fill: "green", "font-size": "14px", visibility: "hidden", "ref-x": 0.5, "ref-y": -50, ref: "rect.body", "y-alignment": "middle", "x-alignment": "middle", }, ".texera-operator-output-count": { text: "", fill: "green", "font-size": "14px", visibility: "hidden", "ref-x": 0.5, "ref-y": -30, ref: "rect.body", "y-alignment": "middle", "x-alignment": "middle", }, "rect.body": { fill: JointUIService.getOperatorFillColor(operator), "follow-scale": true, stroke: "red", "stroke-width": "2", rx: "5px", ry: "5px", }, "rect.boundary": { fill: "rgba(0, 0, 0, 0)", width: this.DEFAULT_OPERATOR_WIDTH + 20, height: this.DEFAULT_OPERATOR_HEIGHT + 20, ref: "rect.body", "ref-x": -10, "ref-y": -10, }, "path.right-boundary": { ref: "rect.body", d: "M 20 80 C 0 60 0 20 20 0", stroke: "rgba(0,0,0,0)", "stroke-width": "10", fill: "transparent", "ref-x": 70, "ref-y": -10, }, "path.left-boundary": { ref: "rect.body", d: "M 0 80 C 20 60 20 20 0 0", stroke: "rgba(0,0,0,0)", "stroke-width": "10", fill: "transparent", "ref-x": -30, "ref-y": -10, }, ".texera-operator-name": { text: operatorDisplayName, fill: "#595959", "font-size": "14px", "ref-x": 0.5, "ref-y": 80, ref: "rect.body", "y-alignment": "middle", "x-alignment": "middle", }, ".texera-operator-friendly-name": { text: operatorFriendlyName, fill: "#888888", "font-size": "10px", "ref-x": 0.5, "ref-y": -12, ref: "rect.body", "y-alignment": "middle", "x-alignment": "middle", }, [`.${operatorWorkerCountClass}`]: { "ref-x": -5, "ref-y": -35, }, ".delete-button": { x: 60, y: -20, cursor: "pointer", fill: "#D8656A", event: "element:delete", visibility: "hidden", }, ".chat-button": { x: 85, y: -20, cursor: "pointer", fill: "#1890ff", event: "element:chat", visibility: "hidden", }, ".add-input-port-button": { x: -25, y: 65, cursor: "pointer", fill: "#565656", event: "element:add-input-port", visibility: "hidden", }, ".remove-input-port-button": { x: -25, y: 85, cursor: "pointer", fill: "#565656", event: "element:remove-input-port", visibility: "hidden", }, ".add-output-port-button": { x: 65, y: 65, cursor: "pointer", fill: "#565656", event: "element:add-output-port", visibility: "hidden", }, ".remove-output-port-button": { x: 65, y: 85, cursor: "pointer", fill: "#565656", event: "element:remove-output-port", visibility: "hidden", }, ".texera-operator-icon": { "xlink:href": "assets/operator_images/" + operatorType + ".png", width: 35, height: 35, "ref-x": 0.5, "ref-y": 0.5, ref: "rect.body", "x-alignment": "middle", "y-alignment": "middle", }, ".texera-operator-result-reuse-text": { text: JointUIService.getOperatorCacheDisplayText(operator) === "" ? "" : "cache", fill: "#595959", "font-size": "14px", visible: true, "ref-x": 80, "ref-y": 60, ref: "rect.body", "y-alignment": "middle", "x-alignment": "middle", }, ".texera-operator-result-reuse-icon": { "xlink:href": JointUIService.getOperatorCacheIcon(operator), width: 40, height: 40, "ref-x": 75, "ref-y": 50, ref: "rect.body", "x-alignment": "middle", "y-alignment": "middle", }, ".texera-operator-view-result-icon": { "xlink:href": JointUIService.getOperatorViewResultIcon(operator), width: 20, height: 20, "ref-x": 49, "ref-y": 9, ref: "rect.body", "x-alignment": "middle", "y-alignment": "middle", }, }; } public static getOperatorFillColor(operator: OperatorPredicate): string { const isDisabled = operator.isDisabled ?? false; return isDisabled ? "#E0E0E0" : "#FFFFFF"; } public static getOperatorCacheDisplayText( operator: OperatorPredicate, cacheStatus?: OperatorResultCacheStatus ): string { if (cacheStatus === undefined || !operator.markedForReuse) { return ""; } return cacheStatus; } public static getOperatorCacheIcon(operator: OperatorPredicate, cacheStatus?: OperatorResultCacheStatus): string { if (!operator.markedForReuse) { return ""; } if (cacheStatus === "cache valid") { return "assets/svg/operator-reuse-cache-valid.svg"; } else { return "assets/svg/operator-reuse-cache-invalid.svg"; } } public static getOperatorViewResultIcon(operator: OperatorPredicate): string { if (operator.viewResult) { return "assets/svg/operator-view-result.svg"; } else { return ""; } } public static getCustomCommentStyleAttrs(): joint.shapes.devs.ModelSelectors { return { rect: { fill: "#F2F4F5", "follow-scale": true, stroke: "#CED4D9", "stroke-width": "0", rx: "5px", ry: "5px", }, image: { "xlink:href": "assets/operator_images/icons8-chat_bubble.png", width: 32, height: 32, "ref-x": 0.5, "ref-y": 0.5, ref: "rect", "x-alignment": "middle", "y-alignment": "middle", }, ".delete-button": { x: 22, y: -16, cursor: "pointer", fill: "#D8656A", event: "element:delete", }, }; } public static getJointUserPointerCell(coeditor: Coeditor, position: Point, color: string): joint.dia.Element { const userCursor = new joint.shapes.standard.Circle({ id: this.getJointUserPointerName(coeditor), }); userCursor.resize(15, 15); userCursor.position(position.x, position.y); userCursor.attr("body/fill", color); userCursor.attr("body/stroke", color); userCursor.attr("text", { text: coeditor.name, "ref-x": 15, "ref-y": 20, stroke: coeditor.color, }); return userCursor; } public static getJointUserPointerName(coeditor: Coeditor) { return "pointer_" + coeditor.clientId; } /** * Shows agent action labels (viewed/added/modified) on operators. * Displays bold agent name and action type as text below the operator. */ public showAgentActionLabel( jointPaper: joint.dia.Paper, operatorID: string, actionType: "viewed" | "added" | "modified", agentName: string = "Agent" ): void { const element = jointPaper.getModelById(operatorID); if (!element) { return; } const labelText = `${agentName}: ${actionType}`; element.attr({ [`.${operatorAgentActionProgressClass}`]: { text: labelText, fill: "#52c41a", "font-weight": "bold", visibility: "visible", }, }); } /** * Hides agent action labels on operators. */ public hideAgentActionLabel(jointPaper: joint.dia.Paper, operatorID: string): void { const element = jointPaper.getModelById(operatorID); if (!element) { return; } element.attr({ [`.${operatorAgentActionProgressClass}`]: { text: "", visibility: "hidden", }, }); } } export function fromJointPaperEvent( paper: joint.dia.Paper, eventName: T, context?: any ): Observable> { return fromEventPattern( handler => paper.on(eventName, handler, context), // addHandler (handler, signal) => paper.off(eventName as string, handler, context) // removeHandler ); } ================================================ FILE: frontend/src/app/workspace/service/operator-debug/udf-debug.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { TestBed } from "@angular/core/testing"; import { UdfDebugService } from "./udf-debug.service"; import { WorkflowWebsocketService } from "../workflow-websocket/workflow-websocket.service"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { WorkflowStatusService } from "../workflow-status/workflow-status.service"; import { ExecuteWorkflowService } from "../execute-workflow/execute-workflow.service"; import { Observable, Subject } from "rxjs"; import { OperatorState, OperatorStatistics } from "../../types/execute-workflow.interface"; import { WorkflowGraphReadonly } from "../workflow-graph/model/workflow-graph"; import { mockPoint, mockPythonUDFPredicate } from "../workflow-graph/model/mock-workflow-data"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../operator-metadata/stub-operator-metadata.service"; import * as Y from "yjs"; import { ConsoleUpdateEvent } from "../../types/workflow-common.interface"; import { TexeraWebsocketEvent } from "../../types/workflow-websocket.interface"; import { commonTestProviders } from "../../../common/testing/test-utils"; import type { Mocked } from "vitest"; describe("UdfDebugServiceSpec", () => { let service: UdfDebugService; let workflowActionService: WorkflowActionService; let mockWorkflowWebsocketService: Mocked; let mockWorkflowStatusService: Mocked; let mockExecuteWorkflowService: Mocked; let statusUpdateStream: Subject>; let consoleUpdateEventStream: Subject; let texeraGraph: WorkflowGraphReadonly; let stubWorker = "worker1"; beforeEach(() => { // Create mock services mockWorkflowWebsocketService = { send: vi.fn(), subscribeToEvent: vi.fn(), } as unknown as Mocked; mockWorkflowStatusService = { getStatusUpdateStream: vi.fn() } as unknown as Mocked; mockExecuteWorkflowService = { getWorkerIds: vi.fn() } as unknown as Mocked; // Initialize the mock streams statusUpdateStream = new Subject(); consoleUpdateEventStream = new Subject(); // Set mock return values mockWorkflowStatusService.getStatusUpdateStream.mockReturnValue(statusUpdateStream.asObservable()); mockWorkflowWebsocketService.subscribeToEvent.mockReturnValue( consoleUpdateEventStream.asObservable() as Observable ); mockExecuteWorkflowService.getWorkerIds.mockReturnValue([stubWorker]); // Configure the TestBed TestBed.configureTestingModule({ providers: [ UdfDebugService, WorkflowActionService, { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, { provide: WorkflowWebsocketService, useValue: mockWorkflowWebsocketService }, { provide: WorkflowStatusService, useValue: mockWorkflowStatusService }, { provide: ExecuteWorkflowService, useValue: mockExecuteWorkflowService }, ...commonTestProviders, ], }); workflowActionService = TestBed.inject(WorkflowActionService); texeraGraph = workflowActionService.getTexeraGraph(); workflowActionService.addOperator(mockPythonUDFPredicate, mockPoint); // Spy on the necessary methods vi.spyOn(texeraGraph, "createOperatorDebugState"); vi.spyOn(texeraGraph, "getOperatorDebugState"); service = TestBed.inject(UdfDebugService); }); afterEach(() => { // Clean up the streams after each test statusUpdateStream.complete(); consoleUpdateEventStream.complete(); }); it("should initialize debug handlers on service creation", () => { expect(texeraGraph.createOperatorDebugState).toHaveBeenCalledWith(mockPythonUDFPredicate.operatorID); }); it("should retrieve the debug state of an operator", () => { const state = service.getDebugState(mockPythonUDFPredicate.operatorID); expect(state).toBeInstanceOf(Y.Map); expect(state.size).toBe(0); // Initially empty }); it("should get the condition of a breakpoint", () => { const debugState = service.getDebugState(mockPythonUDFPredicate.operatorID); debugState.set("1", { breakpointId: 1, condition: "x > 5", hit: false }); const condition = service.getCondition(mockPythonUDFPredicate.operatorID, 1); expect(condition).toBe("x > 5"); }); it("should return empty string if condition does not exist", () => { const condition = service.getCondition(mockPythonUDFPredicate.operatorID, 2); expect(condition).toBe(""); }); it("should update the breakpoint condition if different", () => { const debugState = service.getDebugState(mockPythonUDFPredicate.operatorID); debugState.set("1", { breakpointId: 1, condition: "x > 5", hit: false }); service.doUpdateBreakpointCondition(mockPythonUDFPredicate.operatorID, 1, "x < 10"); expect(mockWorkflowWebsocketService.send).toHaveBeenCalledWith("DebugCommandRequest", { operatorId: mockPythonUDFPredicate.operatorID, workerId: stubWorker, cmd: "condition 1 x < 10", }); expect(debugState.get("1")?.condition).toBe("x < 10"); }); it("should not update the breakpoint condition if it is the same", () => { const debugState = service.getDebugState(mockPythonUDFPredicate.operatorID); debugState.set("1", { breakpointId: 1, condition: "x > 5", hit: false }); service.doUpdateBreakpointCondition(mockPythonUDFPredicate.operatorID, 1, "x > 5"); expect(mockWorkflowWebsocketService.send).not.toHaveBeenCalled(); }); it("should modify a breakpoint (remove existing)", () => { const debugState = service.getDebugState(mockPythonUDFPredicate.operatorID); debugState.set("1", { breakpointId: 1, condition: "", hit: false }); service.doModifyBreakpoint(mockPythonUDFPredicate.operatorID, 1); expect(mockWorkflowWebsocketService.send).toHaveBeenCalledWith("DebugCommandRequest", { operatorId: mockPythonUDFPredicate.operatorID, workerId: stubWorker, cmd: "clear 1", }); expect(debugState.has("1")).toBe(true); // The state is supposed to be cleared later by console update events. }); it("should modify a breakpoint (add new)", () => { const debugState = service.getDebugState(mockPythonUDFPredicate.operatorID); service.doModifyBreakpoint(mockPythonUDFPredicate.operatorID, 10); expect(mockWorkflowWebsocketService.send).toHaveBeenCalledWith("DebugCommandRequest", { operatorId: mockPythonUDFPredicate.operatorID, workerId: stubWorker, cmd: "break 10", }); // it should change the state yet expect(debugState.has("10")).toBe(false); }); it("should continue the workflow execution", () => { service.doContinue(mockPythonUDFPredicate.operatorID, stubWorker); expect(mockWorkflowWebsocketService.send).toHaveBeenCalledWith("DebugCommandRequest", { operatorId: mockPythonUDFPredicate.operatorID, workerId: stubWorker, cmd: "continue", }); }); it("should step through the workflow execution", () => { service.doStep(mockPythonUDFPredicate.operatorID, stubWorker); expect(mockWorkflowWebsocketService.send).toHaveBeenCalledWith("DebugCommandRequest", { operatorId: mockPythonUDFPredicate.operatorID, workerId: stubWorker, cmd: "next", }); }); it("should clear the debug state on state change to Uninitialized", () => { const debugState = service.getDebugState(mockPythonUDFPredicate.operatorID); const operatorId = mockPythonUDFPredicate.operatorID; debugState.set(operatorId, { breakpointId: 1, condition: "x > 5", hit: false }); statusUpdateStream.next({ [operatorId]: { operatorState: OperatorState.Uninitialized, aggregatedInputRowCount: 0, aggregatedOutputRowCount: 0, inputPortMetrics: {}, outputPortMetrics: {}, }, }); expect(debugState.size).toBe(0); }); it("should handle console update events (breakpoint creation)", () => { const message: ConsoleUpdateEvent = { operatorId: mockPythonUDFPredicate.operatorID, messages: [ { workerId: stubWorker, timestamp: { nanos: 0, seconds: 0 }, title: "Breakpoint 1 at /path/to/file.py:10", source: "(Pdb)", msgType: { name: "DEBUGGER" }, message: "", }, ], }; vi.spyOn(service, "doContinue"); consoleUpdateEventStream.next(message); const debugState = service.getDebugState(mockPythonUDFPredicate.operatorID); expect(debugState.get("10")).toEqual({ breakpointId: 1, condition: "", hit: false }); // should call doContinue for all workers if no breakpoints are hit expect(service.doContinue).toHaveBeenCalled(); expect(service.doContinue).toHaveBeenCalledWith(mockPythonUDFPredicate.operatorID, "worker1"); }); it("should not call doContinue if a breakpoint is hit", () => { const debugState = service.getDebugState(mockPythonUDFPredicate.operatorID); debugState.set("10", { breakpointId: 1, condition: "", hit: true }); const message: ConsoleUpdateEvent = { operatorId: mockPythonUDFPredicate.operatorID, messages: [ { workerId: stubWorker, timestamp: { nanos: 0, seconds: 0 }, title: "Breakpoint 2 at /path/to/file.py:11", source: "(Pdb)", msgType: { name: "DEBUGGER" }, message: "", }, ], }; vi.spyOn(service, "doContinue"); consoleUpdateEventStream.next(message); expect(service.doContinue).not.toHaveBeenCalled(); }); it("should handle breakpoint deletion and remove it from the debug state", () => { const operatorId = mockPythonUDFPredicate.operatorID; // Pre-set a breakpoint in the debug state const debugState = service.getDebugState(operatorId); debugState.set("10", { breakpointId: 1, condition: "", hit: false }); // Simulate a deletion message const message: ConsoleUpdateEvent = { operatorId, messages: [ { workerId: stubWorker, timestamp: { nanos: 0, seconds: 0 }, title: "Deleted breakpoint 1 at /path/to/file.py:10", source: "(Pdb)", msgType: { name: "DEBUGGER" }, message: "", }, ], }; vi.spyOn(service, "doContinue"); consoleUpdateEventStream.next(message); // Ensure the breakpoint was deleted from the debug state expect(debugState.has("10")).toBe(false); // Verify that doContinue was called for all workers expect(service.doContinue).toHaveBeenCalled(); expect(service.doContinue).toHaveBeenCalledWith(operatorId, "worker1"); }); it("should handle console update events (breakpoint deletion)", () => { const operatorId = mockPythonUDFPredicate.operatorID; // Pre-set a breakpoint as hit in the debug state const debugState = service.getDebugState(operatorId); debugState.set("10", { breakpointId: 1, condition: "", hit: true }); // Simulate a deletion message const message: ConsoleUpdateEvent = { operatorId, messages: [ { workerId: stubWorker, timestamp: { nanos: 0, seconds: 0 }, title: "Deleted breakpoint 1 at /path/to/file.py:10", source: "(Pdb)", msgType: { name: "DEBUGGER" }, message: "", }, ], }; vi.spyOn(service, "doContinue"); consoleUpdateEventStream.next(message); // Ensure the breakpoint is retained with an undefined breakpointId expect(debugState.get("10")).toEqual({ breakpointId: undefined, condition: "", hit: true }); // Verify that doContinue was not called due to a hit breakpoint expect(service.doContinue).not.toHaveBeenCalled(); }); it("should handle console update events (breakpoint deletion) without sending continue if a hit breakpoint exists", () => { const operatorId = mockPythonUDFPredicate.operatorID; // Pre-set a hit breakpoint and another non-hit breakpoint in the debug state const debugState = service.getDebugState(operatorId); debugState.set("10", { breakpointId: 1, condition: "", hit: true }); debugState.set("11", { breakpointId: 2, condition: "", hit: false }); // Simulate a deletion message const message: ConsoleUpdateEvent = { operatorId, messages: [ { workerId: stubWorker, timestamp: { nanos: 0, seconds: 0 }, title: "Deleted breakpoint 2 at /path/to/file.py:11", source: "(Pdb)", msgType: { name: "DEBUGGER" }, message: "", }, ], }; vi.spyOn(service, "doContinue"); consoleUpdateEventStream.next(message); // Ensure the non-hit breakpoint was deleted expect(debugState.has("11")).toBe(false); // Verify that doContinue was not called due to the remaining hit breakpoint expect(service.doContinue).not.toHaveBeenCalled(); }); it("should call doContinue for all workers if no breakpoints are hit", () => { const operatorId = mockPythonUDFPredicate.operatorID; // Ensure no breakpoints are hit in the debug state const debugState = service.getDebugState(operatorId); debugState.set("10", { breakpointId: 1, condition: "", hit: false }); const message: ConsoleUpdateEvent = { operatorId, messages: [ { workerId: stubWorker, timestamp: { nanos: 0, seconds: 0 }, title: "*** Blank or comment", source: "(Pdb)", msgType: { name: "DEBUGGER" }, message: "", }, ], }; vi.spyOn(service, "doContinue"); // Spy on the doContinue method consoleUpdateEventStream.next(message); // Emit the message }); it("should handle console update events (breakpoint blank message)", () => { const operatorId = mockPythonUDFPredicate.operatorID; // Set a hit breakpoint in the debug state const debugState = service.getDebugState(operatorId); debugState.set("10", { breakpointId: 1, condition: "", hit: true }); const message: ConsoleUpdateEvent = { operatorId, messages: [ { workerId: stubWorker, timestamp: { nanos: 0, seconds: 0 }, title: "*** Blank or comment", source: "(Pdb)", msgType: { name: "DEBUGGER" }, message: "", }, ], }; vi.spyOn(service, "doContinue"); consoleUpdateEventStream.next(message); // Emit the message // Ensure doContinue was not called due to a hit breakpoint expect(service.doContinue).not.toHaveBeenCalled(); debugState.delete("10"); consoleUpdateEventStream.next(message); // Emit the message // Ensure doContinue is called for each worker expect(service.doContinue).toHaveBeenCalled(); expect(service.doContinue).toHaveBeenCalledWith(operatorId, "worker1"); }); it("should handle console update events (stepping message)", () => { vi.spyOn(service as any, "markBreakpointAsHit"); const message = { operatorId: mockPythonUDFPredicate.operatorID, messages: [ { workerId: stubWorker, timestamp: { nanos: 0, seconds: 0 }, title: "> /path/to/file.py(10)()", source: "(Pdb)", msgType: { name: "DEBUGGER" }, message: "", }, ], }; consoleUpdateEventStream.next(message); expect((service as UdfDebugService)["markBreakpointAsHit"]).toHaveBeenCalledWith( mockPythonUDFPredicate.operatorID, 10 ); }); it("should mark a breakpoint as hit", () => { const debugState = service.getDebugState(mockPythonUDFPredicate.operatorID); service["markBreakpointAsHit"](mockPythonUDFPredicate.operatorID, 10); expect(debugState.get("10")).toEqual({ breakpointId: undefined, condition: "", hit: true }); }); it("should mark continue by resetting hit statuses and removing temporary breakpoints", () => { const debugState = service.getDebugState(mockPythonUDFPredicate.operatorID); debugState.set("1", { breakpointId: 1, condition: "x > 5", hit: false }); debugState.set("2", { breakpointId: undefined, condition: "", hit: true }); // Temporary breakpoint service["markContinue"](mockPythonUDFPredicate.operatorID); expect(debugState.get("1")).toEqual({ breakpointId: 1, condition: "x > 5", hit: false }); expect(debugState.has("2")).toBe(false); }); }); ================================================ FILE: frontend/src/app/workspace/service/operator-debug/udf-debug.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { WorkflowWebsocketService } from "../workflow-websocket/workflow-websocket.service"; import { OperatorState } from "../../types/execute-workflow.interface"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { isDefined } from "../../../common/util/predicate"; import { WorkflowStatusService } from "../workflow-status/workflow-status.service"; import { ExecuteWorkflowService } from "../execute-workflow/execute-workflow.service"; import { filter, map, switchMap } from "rxjs/operators"; /** * This service provides functionalities for debugging UDF operators. */ @Injectable({ providedIn: "root", }) export class UdfDebugService { constructor( private workflowWebsocketService: WorkflowWebsocketService, private workflowActionService: WorkflowActionService, private workflowStatusService: WorkflowStatusService, private executeWorkflowService: ExecuteWorkflowService ) { // Initializes debug handlers for all operators in the workflow graph. const graph = this.workflowActionService.getTexeraGraph(); graph.getAllOperators().forEach(op => { graph.createOperatorDebugState(op.operatorID); this.registerOperatorStateChangeHandler(op.operatorID); this.registerConsoleUpdateHandler(op.operatorID); }); } /** * Retrieves the debug state for a specific operator. * * @param operatorId - The unique ID of the operator. * @returns A Y.Map containing the operator's debug state. */ getDebugState(operatorId: string) { return this.workflowActionService.getTexeraGraph().getOperatorDebugState(operatorId); } /** * Gets the condition of a breakpoint for a specific line. * * @param operatorId - The unique ID of the operator. * @param lineNumber - The line number where the breakpoint is set. * @returns The condition string for the breakpoint. */ getCondition(operatorId: string, lineNumber: number): string { const line = String(lineNumber); const debugState = this.getDebugState(operatorId); return debugState.has(line) ? debugState.get(line)!.condition : ""; } /** * Updates the condition of a breakpoint if it differs from the existing one. * * @param operatorId - The unique ID of the operator. * @param lineNumber - The line number where the breakpoint is set. * @param condition - The new condition to be applied to the breakpoint. */ doUpdateBreakpointCondition(operatorId: string, lineNumber: number, condition: string) { if (condition === this.getCondition(operatorId, lineNumber)) return; const workerIds = this.executeWorkflowService.getWorkerIds(operatorId); const debugState = this.getDebugState(operatorId); const breakpointInfo = debugState.get(String(lineNumber)); if (isDefined(breakpointInfo)) { workerIds.forEach(workerId => { this.workflowWebsocketService.send("DebugCommandRequest", { operatorId, workerId, cmd: `condition ${breakpointInfo.breakpointId} ${condition}`, }); }); debugState.set(String(lineNumber), { ...breakpointInfo, condition }); } } /** * Adds or removes a breakpoint based on its existence. * * @param operatorId - The unique ID of the operator. * @param lineNumber - The line number to add or remove the breakpoint from. */ doModifyBreakpoint(operatorId: string, lineNumber: number) { const workerIds = this.executeWorkflowService.getWorkerIds(operatorId); const debugState = this.getDebugState(operatorId); const cmd = debugState.has(String(lineNumber)) ? "clear" : "break"; const breakpointId = debugState.get(String(lineNumber))?.breakpointId || ""; workerIds.forEach(workerId => { this.workflowWebsocketService.send("DebugCommandRequest", { operatorId, workerId, cmd: `${cmd} ${cmd === "clear" ? breakpointId : lineNumber}`, }); }); } /** * Continues the execution by resetting the temporary breakpoints. * * @param operatorId - The unique ID of the operator. * @param workerId - The ID of the worker to continue execution on. */ doContinue(operatorId: string, workerId: string) { this.markContinue(operatorId); this.workflowWebsocketService.send("DebugCommandRequest", { operatorId, workerId, cmd: "continue", }); } /** * Steps through the execution. * * @param operatorId - The unique ID of the operator. * @param workerId - The ID of the worker to step execution on. */ doStep(operatorId: string, workerId: string) { this.markContinue(operatorId); this.workflowWebsocketService.send("DebugCommandRequest", { operatorId, workerId, cmd: "next", }); } /** * Registers a handler for state changes of an operator. * * @param operatorId - The unique ID of the operator. */ private registerOperatorStateChangeHandler(operatorId: string) { this.workflowStatusService .getStatusUpdateStream() .pipe(filter(event => event[operatorId]?.operatorState === OperatorState.Uninitialized)) .subscribe(() => this.getDebugState(operatorId).clear()); } /** * Registers console update handlers for an operator. * * @param operatorId - The unique ID of the operator. */ private registerConsoleUpdateHandler(operatorId: string) { const debugMessageStream = this.workflowWebsocketService.subscribeToEvent("ConsoleUpdateEvent").pipe( filter(evt => evt.operatorId === operatorId && evt.messages.length > 0), switchMap(evt => evt.messages), filter(msg => msg.source === "(Pdb)" && msg.msgType.name === "DEBUGGER") ); // Handle stepping message. // Example: // > /path/to/file.py(10)() debugMessageStream .pipe( filter(msg => msg.title.startsWith(">")), map(msg => this.extractInfo(msg.title)) ) .subscribe(({ lineNum }) => { if (!isDefined(lineNum)) return; this.markBreakpointAsHit(operatorId, lineNum); }); // Handle breakpoint creation message. // Example: // Breakpoint 1 at /path/to/file.py:10 debugMessageStream .pipe( filter(msg => msg.title.startsWith("Breakpoint")), map(msg => this.extractInfo(msg.title)) ) .subscribe(({ breakpointId, lineNum }) => { if (isDefined(breakpointId) && isDefined(lineNum)) { this.getDebugState(operatorId).set(String(lineNum), { breakpointId, condition: "", hit: false, }); } this.continueIfNotHittingBreakpoint(operatorId); }); // Handle breakpoint deletion message. // Example: // Deleted breakpoint 1 at /path/to/file.py:10 debugMessageStream .pipe( filter(msg => msg.title.startsWith("Deleted")), map(msg => this.extractInfo(msg.title)) ) .subscribe(({ lineNum }) => { if (!isDefined(lineNum)) { return; } const debugState = this.getDebugState(operatorId); if (!debugState.has(String(lineNum))) { return; } const breakpointInfo = debugState.get(String(lineNum))!; debugState.delete(String(lineNum)); // if the breakpoint was hit, we need to keep it in the debug state if (breakpointInfo.hit) { debugState.set(String(lineNum), { ...breakpointInfo, breakpointId: undefined }); } this.continueIfNotHittingBreakpoint(operatorId); }); // Handle breakpoint blank message. // Example: // *** Blank or comment debugMessageStream.pipe(filter(msg => msg.title.startsWith("*** Blank or comment"))).subscribe(() => { this.continueIfNotHittingBreakpoint(operatorId); }); } /** * Marks a breakpoint as hit, creating a temporary one if needed. * * @param operatorId - The unique ID of the operator. * @param lineNum - The line number of the breakpoint to mark as hit. */ private markBreakpointAsHit(operatorId: string, lineNum: number) { const line = String(lineNum); const debugState = this.getDebugState(operatorId); if (!debugState.has(line)) { debugState.set(line, { breakpointId: undefined, condition: "", hit: false }); } const breakpoint = debugState.get(line)!; debugState.set(line, { ...breakpoint, hit: true }); } /** * Resets hit status and removes temporary breakpoints. * * @param operatorId - The unique ID of the operator. */ private markContinue(operatorId: string) { const debugState = this.getDebugState(operatorId); debugState.forEach((value, key) => { if (value.hit) debugState.set(key, { ...value, hit: false }); if (!value.breakpointId) debugState.delete(key); }); } /** * Extracts breakpoint information from a message. * * @param message - The message string containing breakpoint information. * @returns An object containing breakpointId and lineNum. */ private extractInfo(message: string): { breakpointId?: number; lineNum?: number } { const match = message.match(/(?:Breakpoint|Deleted breakpoint) (\d+) at .+:(\d+)/); if (match) return { breakpointId: parseInt(match[1], 10), lineNum: parseInt(match[2], 10) }; const lineMatch = message.match(/\.py\((\d+)\)|:(\d+)/); if (lineMatch) return { lineNum: parseInt(lineMatch[1] || lineMatch[2], 10) }; return {}; } /** * Checks if any breakpoint is currently hit (paused) in the debug state * for the given operator. * * @param {string} operatorId - The unique ID of the operator. * @returns {boolean} - Returns `true` if any breakpoint is hit, otherwise `false`. */ private isHittingBreakpoint(operatorId: string): boolean { const debugState = this.getDebugState(operatorId); return Array.from(debugState.values()).some(breakpoint => breakpoint.hit); } /** * Sends a "continue" command to all workers of the specified operator * if no breakpoints are currently hit. If a breakpoint is hit, the * function exits without sending the "continue" command. * * @param {string} operatorId - The unique ID of the operator. */ private continueIfNotHittingBreakpoint(operatorId: string): void { // If any breakpoint is hit, do not send the "continue" command. if (this.isHittingBreakpoint(operatorId)) { return; } // Retrieve all worker IDs and send the "continue" command to each worker. this.executeWorkflowService.getWorkerIds(operatorId).forEach(workerId => this.doContinue(operatorId, workerId)); } } ================================================ FILE: frontend/src/app/workspace/service/operator-menu/operator-menu.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { TestBed } from "@angular/core/testing"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../operator-metadata/stub-operator-metadata.service"; import { OperatorMenuService } from "./operator-menu.service"; import { HttpClientModule } from "@angular/common/http"; import { ComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service"; import { MockComputingUnitStatusService } from "../../../common/service/computing-unit/computing-unit-status/mock-computing-unit-status.service"; import { commonTestProviders } from "../../../common/testing/test-utils"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { mockCommentBox, mockPoint, mockResultPredicate, mockScanPredicate, mockSentimentPredicate, } from "../workflow-graph/model/mock-workflow-data"; import { Subscription } from "rxjs"; describe("OperatorMenuService", () => { let service: OperatorMenuService; let workflowActionService: WorkflowActionService; let opsLatest: readonly string[] = []; let boxesLatest: readonly string[] = []; let subs: Subscription; beforeEach(() => { TestBed.configureTestingModule({ providers: [ { provide: OperatorMetadataService, useClass: StubOperatorMetadataService }, { provide: ComputingUnitStatusService, useClass: MockComputingUnitStatusService }, ...commonTestProviders, ], imports: [HttpClientModule], }); workflowActionService = TestBed.inject(WorkflowActionService); service = TestBed.inject(OperatorMenuService); subs = new Subscription(); subs.add(service.highlightedOperators$.subscribe(ids => (opsLatest = ids))); subs.add(service.highlightedCommentBoxes$.subscribe(ids => (boxesLatest = ids))); }); afterEach(() => subs.unsubscribe()); it("should be created", () => { expect(service).toBeTruthy(); }); it("starts with empty highlighted snapshots", () => { expect(opsLatest).toEqual([]); expect(boxesLatest).toEqual([]); }); it("does not expose mutable BehaviorSubjects on the public API", () => { // service must not let outside code call .next() on its internal state. expect((service as any).highlightedOperators).toBeUndefined(); expect((service as any).highlightedCommentBoxes).toBeUndefined(); }); it("emits the new highlighted operator IDs on highlightedOperators$", () => { workflowActionService.addOperator(mockScanPredicate, mockPoint); workflowActionService.getJointGraphWrapper().highlightOperators(mockScanPredicate.operatorID); expect(opsLatest).toEqual([mockScanPredicate.operatorID]); }); it("emits the new highlighted comment box IDs on highlightedCommentBoxes$", () => { workflowActionService.addCommentBox(mockCommentBox); workflowActionService.getJointGraphWrapper().highlightCommentBoxes(mockCommentBox.commentBoxID); expect(boxesLatest).toEqual([mockCommentBox.commentBoxID]); }); it("emits exactly once on highlightedOperators$ per highlight change (no fan-out)", () => { const emissions: string[][] = []; const sub = service.highlightedOperators$.subscribe(ids => emissions.push([...ids])); // BehaviorSubject seed expect(emissions.length).toBe(1); workflowActionService.addOperator(mockScanPredicate, mockPoint); workflowActionService.getJointGraphWrapper().highlightOperators(mockScanPredicate.operatorID); // a single highlight event must produce a single emission, not 4 (one per dependent handler). expect(emissions.length).toBe(2); expect(emissions[1]).toEqual([mockScanPredicate.operatorID]); workflowActionService.getJointGraphWrapper().unhighlightOperators(mockScanPredicate.operatorID); expect(emissions.length).toBe(3); expect(emissions[2]).toEqual([]); sub.unsubscribe(); }); describe("button state recomputation", () => { it("makes disable button clickable when an operator is highlighted and modification is enabled", () => { workflowActionService.addOperator(mockScanPredicate, mockPoint); workflowActionService.getJointGraphWrapper().highlightOperators(mockScanPredicate.operatorID); expect(service.isDisableOperatorClickable).toBe(true); expect(service.isDisableOperator).toBe(true); }); it("flips isDisableOperator to enable after the operator is disabled", () => { workflowActionService.addOperator(mockScanPredicate, mockPoint); workflowActionService.getJointGraphWrapper().highlightOperators(mockScanPredicate.operatorID); workflowActionService.disableOperators([mockScanPredicate.operatorID]); // all highlighted operators are now disabled, so clicking should re-enable them. expect(service.isDisableOperator).toBe(false); }); it("excludes sinks from view-result targets", () => { workflowActionService.addOperatorsAndLinks( [ { op: mockScanPredicate, pos: mockPoint }, { op: mockResultPredicate, pos: mockPoint }, ], [] ); const wrapper = workflowActionService.getJointGraphWrapper(); // start from a clean highlight state — addOperator may auto-highlight new operators. wrapper.unhighlightOperators(...wrapper.getCurrentHighlightedOperatorIDs()); // highlighting only a sink: view-result should not be clickable. wrapper.highlightOperators(mockResultPredicate.operatorID); expect(service.isToViewResultClickable).toBe(false); expect(service.isReuseResultClickable).toBe(false); // highlighting only a non-sink: view-result becomes clickable. wrapper.unhighlightOperators(mockResultPredicate.operatorID); wrapper.highlightOperators(mockScanPredicate.operatorID); expect(service.isToViewResultClickable).toBe(true); expect(service.isReuseResultClickable).toBe(true); }); it("recomputes when modification-enabled stream fires without a highlight change", () => { workflowActionService.addOperator(mockScanPredicate, mockPoint); workflowActionService.getJointGraphWrapper().highlightOperators(mockScanPredicate.operatorID); expect(service.isDisableOperatorClickable).toBe(true); workflowActionService.disableWorkflowModification(); expect(service.isDisableOperatorClickable).toBe(false); workflowActionService.enableWorkflowModification(); expect(service.isDisableOperatorClickable).toBe(true); }); it("recomputes when view-result state of a highlighted non-sink operator changes", () => { workflowActionService.addOperatorsAndLinks( [ { op: mockScanPredicate, pos: mockPoint }, { op: mockSentimentPredicate, pos: mockPoint }, ], [] ); workflowActionService .getJointGraphWrapper() .highlightOperators(mockScanPredicate.operatorID, mockSentimentPredicate.operatorID); expect(service.isToViewResult).toBe(true); workflowActionService.setViewOperatorResults([mockScanPredicate.operatorID, mockSentimentPredicate.operatorID]); // both highlighted non-sinks are now viewing results → next click should toggle off. expect(service.isToViewResult).toBe(false); }); }); }); ================================================ FILE: frontend/src/app/workspace/service/operator-menu/operator-menu.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { isSink } from "../workflow-graph/model/workflow-graph"; import { BehaviorSubject, merge, Observable } from "rxjs"; import { CommentBox, OperatorLink, OperatorPredicate, Point } from "../../types/workflow-common.interface"; import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; import { NotificationService } from "src/app/common/service/notification/notification.service"; import { ExecuteWorkflowService } from "../execute-workflow/execute-workflow.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; type OperatorPositions = { [key: string]: Point; }; // this type associates the old link ID with the new link type LinkWithID = { [key: string]: OperatorLink; }; // This type represents what the serialized string in the clipboard should look like type SerializedString = { operators: OperatorPredicate[]; operatorPositions: OperatorPositions; links: OperatorLink[]; commentBoxes: CommentBox[]; }; /** * This service provides shared state of menu options related to controlling an operator. * This menu state and operations are shared by * - navigation menu * - right-click menu * - keyboard shortcuts */ @UntilDestroy() @Injectable({ providedIn: "root", }) export class OperatorMenuService { private readonly _highlightedOperators$ = new BehaviorSubject([]); private readonly _highlightedCommentBoxes$ = new BehaviorSubject([]); public readonly highlightedOperators$: Observable = this._highlightedOperators$.asObservable(); public readonly highlightedCommentBoxes$: Observable = this._highlightedCommentBoxes$.asObservable(); // whether the disable-operator-button should be enabled public isDisableOperatorClickable: boolean = false; public isDisableOperator: boolean = true; public isToViewResult: boolean = false; public isToViewResultClickable: boolean = false; public isReuseResultClickable: boolean = false; public isMarkForReuse: boolean = true; public readonly COPY_OFFSET = 20; constructor( private workflowActionService: WorkflowActionService, private workflowUtilService: WorkflowUtilService, private notificationService: NotificationService, private executeWorkflowService: ExecuteWorkflowService ) { const jointGraphWrapper = this.workflowActionService.getJointGraphWrapper(); const texeraGraph = this.workflowActionService.getTexeraGraph(); merge( jointGraphWrapper.getJointOperatorHighlightStream(), jointGraphWrapper.getJointOperatorUnhighlightStream(), jointGraphWrapper.getJointGroupHighlightStream(), jointGraphWrapper.getJointGroupUnhighlightStream() ) .pipe(untilDestroyed(this)) .subscribe(() => { this._highlightedOperators$.next(jointGraphWrapper.getCurrentHighlightedOperatorIDs()); this.recomputeMenuState(); }); merge( jointGraphWrapper.getJointCommentBoxHighlightStream(), jointGraphWrapper.getJointCommentBoxUnhighlightStream() ) .pipe(untilDestroyed(this)) .subscribe(() => { this._highlightedCommentBoxes$.next(jointGraphWrapper.getCurrentHighlightedCommentBoxIDs()); }); merge( texeraGraph.getDisabledOperatorsChangedStream(), texeraGraph.getViewResultOperatorsChangedStream(), texeraGraph.getReuseCacheOperatorsChangedStream(), this.workflowActionService.getWorkflowModificationEnabledStream() ) .pipe(untilDestroyed(this)) .subscribe(() => this.recomputeMenuState()); } /** * callback function when user clicks the "disable operator" icon: * this.isDisableOperator indicates whether the operators should be disabled or enabled */ public disableHighlightedOperators(): void { const highlighted = this._highlightedOperators$.value; if (this.isDisableOperator) { this.workflowActionService.disableOperators(highlighted); } else { this.workflowActionService.enableOperators(highlighted); } } public viewResultHighlightedOperators(): void { const targets = this.highlightedOperatorIdsExcludingSinks(); if (this.isToViewResult) { this.workflowActionService.setViewOperatorResults(targets); } else { this.workflowActionService.unsetViewOperatorResults(targets); } } public reuseResultHighlightedOperator(): void { const targets = this.highlightedOperatorIdsExcludingSinks(); if (this.isMarkForReuse) { this.workflowActionService.markReuseResults(targets); } else { this.workflowActionService.removeMarkReuseResults(targets); } } /** * Recomputes the three button states from current state. Called whenever * highlighted operators change or any underlying texera-graph state changes — * a single linear update path that replaces the previous fan-out via shared BehaviorSubject. */ private recomputeMenuState(): void { const texeraGraph = this.workflowActionService.getTexeraGraph(); const modificationEnabled = this.workflowActionService.checkWorkflowModificationEnabled(); const highlighted = this._highlightedOperators$.value; const highlightedExcludingSinks = this.highlightedOperatorIdsExcludingSinks(); const allDisabled = highlighted.every(op => texeraGraph.isOperatorDisabled(op)); this.isDisableOperator = !allDisabled; this.isDisableOperatorClickable = highlighted.length !== 0 && modificationEnabled; const allViewing = highlightedExcludingSinks.every(op => texeraGraph.isViewingResult(op)); this.isToViewResult = !allViewing; this.isToViewResultClickable = highlightedExcludingSinks.length !== 0 && modificationEnabled; const allMarkedForReuse = highlightedExcludingSinks.every(op => texeraGraph.isMarkedForReuseResult(op)); this.isMarkForReuse = !allMarkedForReuse; this.isReuseResultClickable = highlightedExcludingSinks.length !== 0 && modificationEnabled; } private highlightedOperatorIdsExcludingSinks(): string[] { const texeraGraph = this.workflowActionService.getTexeraGraph(); return this._highlightedOperators$.value.filter(op => !isSink(texeraGraph.getOperator(op))); } /** * saves highlighted elements to the system clipboard */ public saveHighlightedElements(): void { // get all the currently selected operators and links const highlightedOperatorIDs = this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs(); // initialize the serialized string const serializedString: SerializedString = { operators: [], operatorPositions: {}, links: [], commentBoxes: [], }; // define the copies that will be put in the serialized json string when copying const operatorsCopy: OperatorPredicate[] = []; const operatorPositionsCopy: OperatorPositions = {}; const linksCopy: OperatorLink[] = []; const commentBoxesCopy: CommentBox[] = []; // fill in the operators copy with all the currently highlighted operators for sorting later (the original highlighted operator IDs is a readonly string array, so it can't be sorted) highlightedOperatorIDs.forEach(operatorID => { operatorsCopy.push(this.workflowActionService.getTexeraGraph().getOperator(operatorID)); }); // sort all the highlighted operators by their layer number operatorsCopy.sort( (first, second) => this.workflowActionService.getJointGraphWrapper().getCellLayer(first.operatorID) - this.workflowActionService.getJointGraphWrapper().getCellLayer(second.operatorID) ); operatorsCopy.forEach(op => { operatorPositionsCopy[op.operatorID] = this.workflowActionService .getJointGraphWrapper() .getElementPosition(op.operatorID); }); serializedString.operators = operatorsCopy; serializedString.operatorPositions = operatorPositionsCopy; // get all the highlighted links, and sort them by their layers const highlighghtedLinkIDs = this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedLinkIDs(); highlighghtedLinkIDs.forEach(linkID => { linksCopy.push(this.workflowActionService.getTexeraGraph().getLinkWithID(linkID)); }); linksCopy.sort( (first, second) => this.workflowActionService.getJointGraphWrapper().getCellLayer(first.linkID) - this.workflowActionService.getJointGraphWrapper().getCellLayer(second.linkID) ); serializedString.links = linksCopy; //get all the highlighted comment boxes, and sort them by their layers const highlightedCommentBoxIDs = this.workflowActionService .getJointGraphWrapper() .getCurrentHighlightedCommentBoxIDs(); highlightedCommentBoxIDs.forEach(commentBoxID => { commentBoxesCopy.push(this.workflowActionService.getTexeraGraph().getCommentBox(commentBoxID)); }); commentBoxesCopy.sort( (first, second) => this.workflowActionService.getJointGraphWrapper().getCellLayer(first.commentBoxID) - this.workflowActionService.getJointGraphWrapper().getCellLayer(second.commentBoxID) ); serializedString.commentBoxes = commentBoxesCopy; // store the stringified copied operators into the clipboard navigator.clipboard.writeText(JSON.stringify(serializedString)).catch(() => { // if the Promise returned from writeText rejects, it means the write to clipboard permission is not granted // although if the current tab is active, permission shouldn't be needed this.notificationService.error("Copy failed. You don't have the permission to write to the clipboard."); }); } public executeUpToOperator() { // get the highlighted operatorId. This feature supports one and only one selected operator. const highlightedOperatorIds = this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs(); if (highlightedOperatorIds.length !== 1) { this.notificationService.error("Can only execute to exactly one target operator."); return; } const targetOperatorId = highlightedOperatorIds[0]; this.executeWorkflowService.executeWorkflow("", targetOperatorId); } public performPasteOperation() { // by reading from the clipboard, permission needs to be granted // a permission prompt automatically shows up by calling readText() navigator.clipboard.readText().then( text => { try { // convert the JSON string in the system clipboard to a JS Map var elementsInClipboard: Map = new Map(Object.entries(JSON.parse(text))); // check if the fields in a normal serialized string exist after converting the JSON string // if not, throw an error, which is propagated and produces an alert for the user if ( !elementsInClipboard.has("operators") && !elementsInClipboard.has("operatorPositions") && !elementsInClipboard.has("links") && !elementsInClipboard.has("groups") && !elementsInClipboard.has("commentBoxes") ) { throw new Error("You haven't copied any element yet."); } } catch (e) { // if the text in the clipboard is not a JSON object, then it means the user hasn't copied an element this.notificationService.error("You haven't copied any element yet."); return; } // define the arguments required for actually adding operators and links const operatorsAndPositions: { op: OperatorPredicate; pos: Point }[] = []; const positions: Point[] = []; // calling get() will give either the value or undefined // at this point, after checking the existence of fields in the operators in the clipboard, // the fields "links" and "operatorPositions" should exist const linksInClipboard: OperatorLink[] = elementsInClipboard.get("links") as OperatorLink[]; const operatorPositionsInClipboard: OperatorPositions = elementsInClipboard.get( "operatorPositions" ) as OperatorPositions; // get all the operators from the clipboard, which are already sorted by their layers let copiedOps: OperatorPredicate[] = elementsInClipboard.get("operators") as OperatorPredicate[]; let linksCopy: LinkWithID = {}; copiedOps.forEach(copiedOperator => { // copyOperator assigns a new randomly generated operator ID to the new operator const newOperator = this.copyOperator(copiedOperator); for (let link of linksInClipboard) { if (linksCopy[link.linkID] === undefined) { const newLinkID = this.workflowUtilService.getLinkRandomUUID(); linksCopy[link.linkID] = { linkID: newLinkID, source: { operatorID: "", portID: "" }, target: { operatorID: "", portID: "" }, }; } if (link.source.operatorID === copiedOperator.operatorID) { // if current copied operator is the source operator of current link, we assign the new operator ID to be the source operator for the current link, and the port ID should remain unchanged const source = { operatorID: newOperator.operatorID, portID: link.source.portID, }; const originalLinkProperties = linksCopy[link.linkID]; linksCopy[link.linkID] = { ...originalLinkProperties, source: source, }; } else if (link.target.operatorID === copiedOperator.operatorID) { // if current copied operator is the target operator of current link, we assign the new operator ID to be the target operator for the current link, and the port ID should remain unchanged const target = { operatorID: newOperator.operatorID, portID: link.target.portID, }; const originalLinkProperties = linksCopy[link.linkID]; linksCopy[link.linkID] = { ...originalLinkProperties, target: target, }; } } const position: Point = operatorPositionsInClipboard[copiedOperator.operatorID] as Point; positions.push(position); // calculate the new positions for the pasted operators const newOperatorPosition = this.calcOperatorPosition(position, positions); operatorsAndPositions.push({ op: newOperator, pos: newOperatorPosition, }); positions.push(newOperatorPosition); }); const links = Object.values(linksCopy); // actually add all operators and links to the workflow try { this.workflowActionService.addOperatorsAndLinks(operatorsAndPositions, links); } catch (e) { this.notificationService.info( "Some of the links that you selected don't have operators attached to both ends of them. These links won't be pasted, since links can't exist without operators." ); } //add copied comment boxes and calculate new positions for the pasted comment boxes let commentBoxesCopy: CommentBox[] = elementsInClipboard.get("commentBoxes") as CommentBox[]; commentBoxesCopy.forEach(commentBoxCopy => { const commentBoxPosition: Point = commentBoxCopy.commentBoxPosition as Point; positions.push(commentBoxPosition); const newCommentBoxPosition = this.calcOperatorPosition(commentBoxPosition, positions); positions.push(newCommentBoxPosition); const newCommentBoxID = this.workflowUtilService.getCommentBoxRandomUUID(); const newCommentBox: CommentBox = { commentBoxID: newCommentBoxID, comments: commentBoxCopy.comments, commentBoxPosition: newCommentBoxPosition, }; this.workflowActionService.addCommentBox(newCommentBox); }); }, // if the Promise returned from readText rejects, the read clipboard permission is not granted, and we send a warning to the user () => { this.notificationService.error("Paste failed. This site has been blocked from reading the clipboard."); } ); } /** * Utility function to create a new operator that contains same * info as the copied operator. * @param operator */ private copyOperator(operator: OperatorPredicate): OperatorPredicate { return { ...operator, operatorID: operator.operatorType + "-" + this.workflowUtilService.getOperatorRandomUUID(), }; } /** * Utility function to calculate the position to paste the operator. * If a previously pasted operator is moved or deleted, the operator will be * pasted to the emptied position. Otherwise, it will be pasted to a position * that's non-overlapping and calculated according to the copy operator offset. * @param pos * @param positions */ private calcOperatorPosition(pos: Point, positions: Point[]): Point { const position = { x: pos.x + this.COPY_OFFSET, y: pos.y + this.COPY_OFFSET, }; return this.getNonOverlappingPosition(position, positions); } /** * Utility function to find a non-overlapping position for the pasted operator. * The function will check if the current position overlaps with an existing * operator. If it does, the function will find a new non-overlapping position. * @param position * @param positions */ private getNonOverlappingPosition(position: Point, positions: Point[]): Point { let overlapped = false; const operatorPositions = positions.concat( this.workflowActionService .getTexeraGraph() .getAllCommentBoxes() .map(CommentBox => CommentBox.commentBoxPosition) ); do { for (const operatorPosition of operatorPositions) { if (operatorPosition.x === position.x && operatorPosition.y === position.y) { position = { x: position.x + this.COPY_OFFSET, y: position.y + this.COPY_OFFSET, }; overlapped = true; break; } overlapped = false; } } while (overlapped); return position; } } ================================================ FILE: frontend/src/app/workspace/service/operator-metadata/mock-operator-metadata.data.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { GroupInfo, OperatorMetadata, OperatorSchema } from "../../types/operator-schema.interface"; import { CustomJSONSchema7 } from "../../types/custom-json-schema.interface"; import { VIEW_RESULT_OP_TYPE } from "../workflow-graph/model/workflow-graph"; import { PortSchema } from "../../types/workflow-common.interface"; // Exports constants related to operator schema and operator metadata for testing purposes. export const mockScanSourceSchema: OperatorSchema = { operatorType: "ScanSource", additionalMetadata: { userFriendlyName: "Source: Scan", operatorDescription: "Read records from a table one by one", operatorGroupName: "Source", inputPorts: [], outputPorts: [{}], }, jsonSchema: { properties: { tableName: { type: "string", description: "name of source table", title: "table name", }, }, required: ["tableName"], type: "object", }, operatorVersion: "scan", }; export const mockPresetEnabledSchema: OperatorSchema = { operatorType: "PresetEnabledOp", additionalMetadata: { userFriendlyName: "DEBUG_userFriendlyName", operatorDescription: "DEBUG_operatorDescription", operatorGroupName: "Analysis", inputPorts: [], outputPorts: [{}], }, jsonSchema: { properties: { presetProperty: { type: "string", description: "property that can be saved in presets", title: "presetProperty", "enable-presets": true, }, normalProperty: { type: "string", description: "property that is excluded in presets", title: "normalProperty" }, }, required: ["normalProperty"], type: "object", }, operatorVersion: "preset1", }; export const mockFileSourceSchema: OperatorSchema = { operatorType: "FileSource", jsonSchema: { type: "object", properties: { fileName: { type: "string", title: "file name" }, }, required: ["fileName"], }, additionalMetadata: { userFriendlyName: "Source: File", operatorDescription: "Read the content of one file or multiple files", operatorGroupName: "Source", inputPorts: [], outputPorts: [{}], }, operatorVersion: "fileSource1", }; export const mockNlpSentimentSchema: OperatorSchema = { operatorType: "NlpSentiment", additionalMetadata: { userFriendlyName: "Sentiment Analysis", operatorDescription: "Sentiment analysis based on Stanford NLP package", operatorGroupName: "Analysis", inputPorts: [{}], outputPorts: [{}], }, jsonSchema: { properties: { attribute: { type: "string", title: "attribute", autofill: "attributeName", autofillAttributeOnPort: 0, }, resultAttribute: { type: "string", title: "result attribute" }, }, required: ["attribute", "resultAttribute"], type: "object", }, operatorVersion: "Nlp1", }; export const mockKeywordSourceSchema: OperatorSchema = { operatorType: "KeywordSource", jsonSchema: { type: "object", properties: { query: { type: "string", title: "query" }, attributes: { type: "array", items: { type: "string" }, title: "attributes", autofill: "attributeNameList", autofillAttributeOnPort: 0, }, tableName: { type: "string", title: "table name" }, spanListName: { type: "string", title: "span list name" }, }, required: ["query", "attributes", "tableName"], }, additionalMetadata: { userFriendlyName: "Source: Keyword", operatorDescription: "Perform an index-based search on a table using a keyword", operatorGroupName: "Analysis", inputPorts: [], outputPorts: [{}], }, operatorVersion: "keywordSource1", }; export const mockKeywordSearchSchema: OperatorSchema = { operatorType: "KeywordMatcher", jsonSchema: { type: "object", properties: { query: { type: "string", title: "query" }, attributes: { type: "array", items: { type: "string" }, title: "attributes", autofill: "attributeNameList", autofillAttributeOnPort: 0, }, spanListName: { type: "string", title: "span list name" }, }, required: ["query", "attributes"], }, additionalMetadata: { userFriendlyName: "Keyword Search", operatorDescription: "Search the documents using a keyword", operatorGroupName: "Analysis", inputPorts: [{}], outputPorts: [{}], }, operatorVersion: "keywordMatcher1", }; export const mockAggregationSchema: OperatorSchema = { operatorType: "Aggregation", jsonSchema: { type: "object", properties: { listOfAggregations: { type: "array", items: { type: "object", properties: { attribute: { type: "string", title: "attribute", autofill: "attributeName", autofillAttributeOnPort: 0, }, aggregator: { type: "string", enum: ["min", "max", "average", "sum", "count"], uniqueItems: true, title: "aggregator", }, resultAttribute: { type: "string", title: "result attribute" }, }, }, title: "list of aggregations", }, }, required: ["listOfAggregations"], }, additionalMetadata: { userFriendlyName: "Aggregation", operatorDescription: "Aggregate one or more columns to find min, max, sum, average, count of the column", operatorGroupName: "Analysis", inputPorts: [{}], outputPorts: [{}], }, operatorVersion: "agg1", }; export const mockViewResultsSchema: OperatorSchema = { operatorType: VIEW_RESULT_OP_TYPE, jsonSchema: { properties: { limit: { default: 10, type: "integer", title: "limit", }, offset: { default: 0, type: "integer", title: "offset", }, }, type: "object", }, additionalMetadata: { userFriendlyName: "View Results", operatorDescription: "View the results of the workflow", operatorGroupName: "View Results", inputPorts: [{}], outputPorts: [], }, operatorVersion: "view1", }; export const mockMultiInputOutputSchema: OperatorSchema = { operatorType: "MultiInputOutput", jsonSchema: { properties: {}, type: "object", }, additionalMetadata: { userFriendlyName: "3-I/O Mock op", operatorDescription: "Mock operator with 3 inputs and 3 outputs", operatorGroupName: "Analysis", inputPorts: [{}, {}, {}], outputPorts: [{}, {}, {}], }, operatorVersion: "multiInput1", }; export const mockUnionSchema: OperatorSchema = { operatorType: "Union", jsonSchema: { properties: {}, type: "object", }, additionalMetadata: { userFriendlyName: "Union", operatorDescription: "Union multiple inputs", operatorGroupName: "Analysis", inputPorts: [{}], outputPorts: [{}], }, operatorVersion: "union1", }; export const mockPythonUDFSchema: OperatorSchema = { operatorType: "PythonUDF", additionalMetadata: { userFriendlyName: "Python UDF", operatorDescription: "custom operator in Java", operatorGroupName: "UDF", inputPorts: [{}], outputPorts: [{}], }, jsonSchema: { properties: {}, type: "object", }, operatorVersion: "p1", }; export const mockJavaUDFSchema: OperatorSchema = { operatorType: "JavaUDF", additionalMetadata: { userFriendlyName: "Java UDF", operatorDescription: "custom operator in Java", operatorGroupName: "UDF", inputPorts: [{}], outputPorts: [{}], }, jsonSchema: { properties: {}, type: "object", }, operatorVersion: "p1", }; export const mockOperatorSchemaList: ReadonlyArray = [ mockScanSourceSchema, mockFileSourceSchema, mockKeywordSourceSchema, mockKeywordSearchSchema, mockNlpSentimentSchema, mockAggregationSchema, mockViewResultsSchema, mockMultiInputOutputSchema, mockPresetEnabledSchema, mockUnionSchema, mockPythonUDFSchema, mockJavaUDFSchema, ]; export const mockOperatorGroup: ReadonlyArray = [ { groupName: "Source" }, { groupName: "Analysis" }, { groupName: "View Results" }, ]; export const mockOperatorMetaData: OperatorMetadata = { operators: mockOperatorSchemaList, groups: mockOperatorGroup, }; export const testJsonSchema: CustomJSONSchema7 = { properties: { attribute: { type: "string", title: "attribute", autofill: "attributeName", autofillAttributeOnPort: 0, }, resultAttribute: { type: "string", title: "result attribute", }, }, required: ["attribute", "resultAttribute"], type: "object", }; export const mockPortSchema: PortSchema = { jsonSchema: { type: "object", properties: { partitionInfo: { type: "object", oneOf: [ { title: "none", properties: { type: { const: "none" }, }, }, { title: "hash", properties: { type: { const: "hash" }, hashAttributeNames: { type: "array", items: { type: "string" }, title: "attribute names" }, }, }, { title: "range", properties: { type: { const: "range" }, rangeAttributeNames: { type: "array", items: { type: "string" }, title: "attribute names" }, rangeMin: { type: "integer", title: "range min" }, rangeMax: { type: "integer", title: "range max" }, }, }, { title: "single", properties: { type: { const: "single" }, }, }, { title: "broadcast", properties: { type: { const: "broadcast" }, }, }, ], title: "partition info", }, dependencies: { type: "array", items: { type: "integer" }, title: "dependencies", }, }, required: ["partitionInfo"], }, }; ================================================ FILE: frontend/src/app/workspace/service/operator-metadata/operator-metadata.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { TestBed } from "@angular/core/testing"; import { HttpClient } from "@angular/common/http"; import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing"; import { OperatorMetadataService } from "./operator-metadata.service"; import { mockOperatorMetaData } from "./mock-operator-metadata.data"; import { commonTestProviders } from "../../../common/testing/test-utils"; describe("OperatorMetadataService", () => { let service: OperatorMetadataService; let httpClient: HttpClient; let httpTestingController: HttpTestingController; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [OperatorMetadataService, HttpClient, ...commonTestProviders], }); httpClient = TestBed.inject(HttpClient); httpTestingController = TestBed.inject(HttpTestingController); service = TestBed.inject(OperatorMetadataService); }); it("should be created", () => { expect(service).toBeTruthy(); }); it("should send http request once", () => { service.getOperatorMetadata().subscribe(value => expect(value).toBeTruthy()); httpTestingController.expectOne(request => request.method === "GET"); }); it("should check if operatorType exists correctly", () => { service.getOperatorMetadata().subscribe(() => { expect(service.operatorTypeExists("ScanSource")).toBeTruthy(); expect(service.operatorTypeExists("InvalidOperatorType")).toBeFalsy(); }); const req = httpTestingController.match(request => request.method === "GET"); req[0].flush(mockOperatorMetaData); }); }); ================================================ FILE: frontend/src/app/workspace/service/operator-metadata/operator-metadata.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable } from "rxjs"; import { AppSettings } from "../../../common/app-setting"; import { OperatorMetadata, OperatorSchema } from "../../types/operator-schema.interface"; import { shareReplay } from "rxjs/operators"; export const OPERATOR_METADATA_ENDPOINT = "resources/operator-metadata"; const addDictionaryAPIAddress = "/api/resources/dictionary/"; const getDictionaryAPIAddress = "/api/upload/dictionary/"; // interface only containing public methods export type IOperatorMetadataService = Pick; /** * OperatorMetadataService talks to the backend to fetch the operator metadata, which contains a list of operator schemas. * Each operator schema contains all the information related to an operator, for example, operatorType, userFriendlyName, * and the jsonSchema of its properties. * * Components and Services should call getOperatorMetadata() and subscribe to the Observable to get the metadata, * after the metadata is fetched from the backend, it will be broadcast through the observable. * * The mock operator metadata is also available in mock-operator-metadata.ts for testing. * It contains the schemas for 3 operators. * @author Zuozhi Wang * */ @Injectable({ providedIn: "root", }) export class OperatorMetadataService { // holds the current version of operator metadata private currentOperatorMetadata: OperatorMetadata | undefined; private operatorMetadataObservable = this.httpClient .get(`${AppSettings.getApiEndpoint()}/${OPERATOR_METADATA_ENDPOINT}`) .pipe(shareReplay(1)); constructor(private httpClient: HttpClient) { this.getOperatorMetadata().subscribe(data => { this.currentOperatorMetadata = data; }); } /** * Gets an Observable for operator metadata. * This observable will emit OperatorMetadataValue after the data is fetched from the backend. * * // TODO: refactor this to 2 functions: getOperatorMetadataStream() and getOperatorMetadata() */ public getOperatorMetadata(): Observable { return this.operatorMetadataObservable; } public getOperatorSchema(operatorType: string): OperatorSchema { if (!this.currentOperatorMetadata) { throw new Error("operator metadata is undefined"); } const operatorSchema = this.currentOperatorMetadata.operators.find(schema => schema.operatorType === operatorType); if (!operatorSchema) { throw new Error(`can\'t find operator schema of type ${operatorType}`); } return operatorSchema; } /** * Returns true if the operator type exists *in the current operator metadata*. * For example, if the first HTTP request to the backend hasn't returned yet, * the current operator metadata is empty, and no operator type exists. * * @param operatorType - Operator name string that we are checking for existence *in the current operator metadata* * @param userFriendlyNameFilter - If true, checks if operatorType matches an operator's user friendly or type name * @param caseInsensitive - If true, operatorType checking becomes case insensitive */ public operatorTypeExists( operatorType: string, userFriendlyNameFilter: boolean = false, caseInsensitive: boolean = false ): boolean { if (!this.currentOperatorMetadata) { return false; } const operator = this.currentOperatorMetadata.operators.filter(op => { let operatorTypeInMetadata = op.operatorType; let operatorNameInMetadata = op.additionalMetadata.userFriendlyName; if (caseInsensitive) { operatorTypeInMetadata = operatorTypeInMetadata.toLowerCase(); operatorNameInMetadata = operatorNameInMetadata.toLowerCase(); operatorType = operatorType.toLowerCase(); } if (userFriendlyNameFilter) { return operatorTypeInMetadata === operatorType || operatorNameInMetadata === operatorType; } else { return operatorTypeInMetadata === operatorType; } }); if (operator.length === 0) { return false; } return true; } } ================================================ FILE: frontend/src/app/workspace/service/operator-metadata/stub-operator-metadata.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { Observable, of } from "rxjs"; import { mockOperatorMetaData } from "./mock-operator-metadata.data"; import { OperatorMetadata, OperatorSchema } from "../../types/operator-schema.interface"; import { IOperatorMetadataService } from "./operator-metadata.service"; import { shareReplay } from "rxjs/operators"; @Injectable() export class StubOperatorMetadataService implements IOperatorMetadataService { private operatorMetadataObservable = of(mockOperatorMetaData).pipe(shareReplay(1)); public getOperatorSchema(operatorType: string): OperatorSchema { const operatorSchema = mockOperatorMetaData.operators.find(schema => schema.operatorType === operatorType); if (!operatorSchema) { throw new Error(`can\'t find operator schema of type ${operatorType}`); } return operatorSchema; } public getOperatorMetadata(): Observable { return this.operatorMetadataObservable; } public operatorTypeExists(operatorType: string): boolean { const operator = mockOperatorMetaData.operators.filter(op => op.operatorType === operatorType); if (operator.length === 0) { return false; } return true; } } ================================================ FILE: frontend/src/app/workspace/service/panel/panel.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Subject } from "rxjs"; import { Injectable } from "@angular/core"; @Injectable({ providedIn: "root", }) export class PanelService { private closePanelSubject = new Subject(); private resetPanelSubject = new Subject(); get resetPanelStream() { return this.resetPanelSubject.asObservable(); } resetPanels() { this.resetPanelSubject.next(); } get closePanelStream() { return this.closePanelSubject.asObservable(); } closePanels() { this.closePanelSubject.next(); } } ================================================ FILE: frontend/src/app/workspace/service/preset/preset.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { TestBed } from "@angular/core/testing"; import { NzMessageService } from "ng-zorro-antd/message"; import { config, of } from "rxjs"; import { UserConfigService } from "src/app/common/service/user/config/user-config.service"; import { CustomJSONSchema7 } from "../../types/custom-json-schema.interface"; import { JointUIService } from "../joint-ui/joint-ui.service"; import { mockPresetEnabledSchema } from "../operator-metadata/mock-operator-metadata.data"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../operator-metadata/stub-operator-metadata.service"; import { UndoRedoService } from "../undo-redo/undo-redo.service"; import { mockPoint, mockPresetEnabledPredicate } from "../workflow-graph/model/mock-workflow-data"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; import { commonTestProviders } from "../../../common/testing/test-utils"; import { Preset, PresetService } from "./preset.service"; // Ajv 8 defaults to strict mode and rejects unknown keywords at compile time, so // `isValidOperatorPreset` (which compiles operator schemas containing the // 'enable-presets' marker) throws before it can validate. Register the keyword // once as a no-op so the validation paths are exercisable in tests. const ajvInstance = (PresetService as any).ajv; if (!ajvInstance.getKeyword("enable-presets")) { ajvInstance.addKeyword({ keyword: "enable-presets", schemaType: "boolean" }); } describe("PresetService", () => { let userConfigStub: { fetchKey: ReturnType; set: ReturnType; delete: ReturnType; }; let messageStub: { success: ReturnType; error: ReturnType; info: ReturnType; warning: ReturnType; }; let presetService: PresetService; let workflowActionService: WorkflowActionService; // RxJS 7 reports errors thrown from a subscribe `next` handler via // `config.onUnhandledError` on a macrotask, not synchronously, so a // try/catch around the call would not see them. Capture them explicitly. const captureRxjsUnhandled = async (run: () => void) => { const captured: unknown[] = []; const previous = config.onUnhandledError; config.onUnhandledError = err => captured.push(err); try { run(); await new Promise(resolve => setTimeout(resolve, 0)); } finally { config.onUnhandledError = previous; } return captured; }; const presetType = "operator"; const presetTarget = mockPresetEnabledPredicate.operatorType; const presetDictKey = `${presetType}-${presetTarget}`; beforeEach(() => { userConfigStub = { fetchKey: vi.fn().mockReturnValue(of(null)), set: vi.fn().mockReturnValue(of(void 0)), delete: vi.fn().mockReturnValue(of(void 0)), }; messageStub = { success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn(), }; TestBed.configureTestingModule({ providers: [ PresetService, WorkflowActionService, WorkflowUtilService, JointUIService, UndoRedoService, { provide: OperatorMetadataService, useClass: StubOperatorMetadataService }, { provide: UserConfigService, useValue: userConfigStub }, { provide: NzMessageService, useValue: messageStub }, ...commonTestProviders, ], }); presetService = TestBed.inject(PresetService); workflowActionService = TestBed.inject(WorkflowActionService); }); it("should be created", () => { expect(presetService).toBeTruthy(); }); describe("preset I/O", () => { it("emits an event on applyPresetStream when a preset is applied", () => { const seen: { type: string; target: string; preset: Preset }[] = []; const sub = presetService.applyPresetStream.subscribe(value => seen.push(value)); const preset: Preset = { presetProperty: "applied" }; presetService.applyPreset("nonOperatorType", "anyTarget", preset); expect(seen).toEqual([{ type: "nonOperatorType", target: "anyTarget", preset }]); sub.unsubscribe(); }); it("emits an event on savePresetsStream when presets are saved", () => { const seen: { type: string; target: string; presets: Preset[] }[] = []; const sub = presetService.savePresetsStream.subscribe(value => seen.push(value)); const presets: Preset[] = [{ presetProperty: "v1" }]; presetService.savePresets(presetType, presetTarget, presets); expect(seen).toEqual([{ type: presetType, target: presetTarget, presets }]); sub.unsubscribe(); }); it("writes through UserConfigService.set when saving a non-empty preset list", () => { const presets: Preset[] = [{ presetProperty: "v1" }]; presetService.savePresets(presetType, presetTarget, presets); expect(userConfigStub.set).toHaveBeenCalledTimes(1); expect(userConfigStub.set).toHaveBeenCalledWith(presetDictKey, JSON.stringify(presets)); expect(userConfigStub.delete).not.toHaveBeenCalled(); }); it("calls UserConfigService.delete instead of set when saving an empty preset list", () => { presetService.savePresets(presetType, presetTarget, []); expect(userConfigStub.delete).toHaveBeenCalledTimes(1); expect(userConfigStub.delete).toHaveBeenCalledWith(presetDictKey); expect(userConfigStub.set).not.toHaveBeenCalled(); }); it("displays the success toast by default when saving presets", () => { presetService.savePresets(presetType, presetTarget, [{ presetProperty: "v1" }]); expect(messageStub.success).toHaveBeenCalledWith("Preset saved"); }); it("suppresses the toast when displayMessage is explicitly null", () => { presetService.savePresets(presetType, presetTarget, [{ presetProperty: "v1" }], null); expect(messageStub.success).not.toHaveBeenCalled(); expect(messageStub.error).not.toHaveBeenCalled(); }); it("createPreset appends to existing presets and writes back", () => { const existing: Preset[] = [{ presetProperty: "v1" }]; userConfigStub.fetchKey.mockReturnValue(of(JSON.stringify(existing))); presetService.createPreset(presetType, presetTarget, { presetProperty: "v2" }); expect(userConfigStub.set).toHaveBeenCalledWith( presetDictKey, JSON.stringify([{ presetProperty: "v1" }, { presetProperty: "v2" }]) ); }); it("createPreset does not write the preset back when it already exists", async () => { userConfigStub.fetchKey.mockReturnValue(of(JSON.stringify([{ presetProperty: "v1" }]))); const errors = await captureRxjsUnhandled(() => presetService.createPreset(presetType, presetTarget, { presetProperty: "v1" }) ); expect(userConfigStub.set).not.toHaveBeenCalled(); expect(userConfigStub.delete).not.toHaveBeenCalled(); expect(errors).toHaveLength(1); expect((errors[0] as Error).message).toMatch(/already exists/); }); it("updatePreset does not write the preset back when the original preset is missing", async () => { userConfigStub.fetchKey.mockReturnValue(of(JSON.stringify([{ presetProperty: "v1" }]))); const errors = await captureRxjsUnhandled(() => presetService.updatePreset(presetType, presetTarget, { presetProperty: "missing" }, { presetProperty: "v3" }) ); expect(userConfigStub.set).not.toHaveBeenCalled(); expect(errors).toHaveLength(1); expect((errors[0] as Error).message).toMatch(/doesn't exist/); }); it("deletePreset removes the matching preset via savePresets", () => { const a: Preset = { presetProperty: "v1" }; const b: Preset = { presetProperty: "v2" }; userConfigStub.fetchKey.mockReturnValue(of(JSON.stringify([a, b]))); presetService.deletePreset(presetType, presetTarget, b); expect(userConfigStub.set).toHaveBeenCalledWith(presetDictKey, JSON.stringify([a])); }); it("deletePreset clears the dictionary entry when the last preset is removed", () => { const only: Preset = { presetProperty: "v1" }; userConfigStub.fetchKey.mockReturnValue(of(JSON.stringify([only]))); presetService.deletePreset(presetType, presetTarget, only); // savePresets routes empty arrays to delete(), not set(). expect(userConfigStub.delete).toHaveBeenCalledWith(presetDictKey); expect(userConfigStub.set).not.toHaveBeenCalled(); }); it("getPresets returns the parsed preset array stored in user config", () => { const stored: Preset[] = [{ presetProperty: "v1" }]; userConfigStub.fetchKey.mockReturnValue(of(JSON.stringify(stored))); let result: readonly Preset[] | undefined; presetService.getPresets(presetType, presetTarget).subscribe(v => (result = v)); expect(result).toEqual(stored); }); it("getPresets yields an empty array when no entry exists", () => { userConfigStub.fetchKey.mockReturnValue(of(null)); let result: readonly Preset[] | undefined; presetService.getPresets(presetType, presetTarget).subscribe(v => (result = v)); expect(result).toEqual([]); }); it("getPresets emits an error when the stored value is not a valid preset array", () => { userConfigStub.fetchKey.mockReturnValue(of(JSON.stringify([{ presetProperty: 42 }, "not-an-object"]))); let err: unknown; // throws inside an rxjs map() — surface via the error subscriber, not toThrow. presetService.getPresets(presetType, presetTarget).subscribe({ next: () => {}, error: (e: unknown) => (err = e), }); expect(err).toBeInstanceOf(Error); expect((err as Error).message).toMatch(/formatted incorrectly/); }); }); describe("operator preset application", () => { beforeEach(() => { workflowActionService.addOperator(mockPresetEnabledPredicate, mockPoint); workflowActionService.setOperatorProperty(mockPresetEnabledPredicate.operatorID, { presetProperty: "before", normalProperty: "untouched", }); }); it("does not set operator properties when applyPreset uses a non-operator type", () => { presetService.applyPreset("notAnOperator", mockPresetEnabledPredicate.operatorID, { presetProperty: "applied" }); expect( workflowActionService.getTexeraGraph().getOperator(mockPresetEnabledPredicate.operatorID).operatorProperties ).toEqual({ presetProperty: "before", normalProperty: "untouched" }); }); it("merges preset values into operator properties when a valid preset is applied", () => { presetService.applyPreset("operator", mockPresetEnabledPredicate.operatorID, { presetProperty: "applied" }); // normalProperty is preserved because applyPreset merges, rather than replaces. expect( workflowActionService.getTexeraGraph().getOperator(mockPresetEnabledPredicate.operatorID).operatorProperties ).toEqual({ presetProperty: "applied", normalProperty: "untouched" }); }); it("does not change operator properties when an invalid preset is applied", async () => { const errors = await captureRxjsUnhandled(() => presetService.applyPreset("operator", mockPresetEnabledPredicate.operatorID, { notAPresetProperty: "applied", }) ); expect( workflowActionService.getTexeraGraph().getOperator(mockPresetEnabledPredicate.operatorID).operatorProperties ).toEqual({ presetProperty: "before", normalProperty: "untouched" }); expect(errors).toHaveLength(1); expect((errors[0] as Error).message).toMatch(/Error applying preset/); }); it("ignores apply events targeting an operator that does not exist on the graph", () => { // unknown operator IDs are silently skipped so cross-workflow events don't raise. expect(() => presetService.applyPreset("operator", "missing-op-id", { presetProperty: "applied" })).not.toThrow(); }); }); describe("operator preset validation", () => { beforeEach(() => { workflowActionService.addOperator(mockPresetEnabledPredicate, mockPoint); }); it("rejects an empty preset", () => { expect(presetService.isValidOperatorPreset({}, mockPresetEnabledPredicate.operatorID)).toBe(false); }); it("rejects presets containing only properties that are not preset-enabled", () => { expect(presetService.isValidOperatorPreset({ wrongProperty: "x" }, mockPresetEnabledPredicate.operatorID)).toBe( false ); }); it("rejects presets with empty string values", () => { expect(presetService.isValidOperatorPreset({ presetProperty: "" }, mockPresetEnabledPredicate.operatorID)).toBe( false ); }); it("accepts presets that match the preset schema with non-empty values", () => { expect( presetService.isValidOperatorPreset({ presetProperty: "applied" }, mockPresetEnabledPredicate.operatorID) ).toBe(true); }); it("isValidNewOperatorPreset returns false when the preset already exists", () => { const existing: Preset = { presetProperty: "applied" }; userConfigStub.fetchKey.mockReturnValue(of(JSON.stringify([existing]))); let result: boolean | undefined; presetService .isValidNewOperatorPreset(existing, mockPresetEnabledPredicate.operatorID) .subscribe(v => (result = v)); expect(result).toBe(false); }); it("isValidNewOperatorPreset returns true when the preset is novel", () => { userConfigStub.fetchKey.mockReturnValue(of(JSON.stringify([{ presetProperty: "applied" }]))); let result: boolean | undefined; presetService .isValidNewOperatorPreset({ presetProperty: "novel" }, mockPresetEnabledPredicate.operatorID) .subscribe(v => (result = v)); expect(result).toBe(true); }); it("isValidNewOperatorPreset short-circuits to false when the preset itself is invalid", () => { let result: boolean | undefined; presetService.isValidNewOperatorPreset({}, mockPresetEnabledPredicate.operatorID).subscribe(v => (result = v)); expect(result).toBe(false); }); }); describe("static schema helpers", () => { it("getOperatorPresetSchema keeps only enable-preset properties and marks them required", () => { const operatorSchema = { type: "object", properties: { presetProperty: { type: "string", description: "property that can be saved in presets", title: "presetProperty", "enable-presets": true, }, normalProperty: { type: "string", description: "property that is excluded in presets", title: "normalProperty", }, }, required: ["normalProperty"], }; expect(PresetService.getOperatorPresetSchema(operatorSchema)).toEqual({ type: "object", properties: { presetProperty: { type: "string", description: "property that can be saved in presets", title: "presetProperty", "enable-presets": true, }, }, required: ["presetProperty"], additionalProperties: false, }); }); it("getOperatorPresetSchema throws when the operator schema has no properties", () => { expect(() => PresetService.getOperatorPresetSchema({ type: "object", properties: {} }) ).toThrow(); }); it("getOperatorPresetSchema throws when no property is preset-enabled", () => { expect(() => PresetService.getOperatorPresetSchema({ type: "object", properties: { normalProperty: { type: "string", title: "normalProperty" }, }, }) ).toThrow(); }); describe("getOperatorPreset", () => { it("throws when operator properties are empty", () => { expect(() => PresetService.getOperatorPreset(mockPresetEnabledSchema.jsonSchema, {})).toThrow(); }); it("throws when operator properties miss a required preset property", () => { expect(() => PresetService.getOperatorPreset(mockPresetEnabledSchema.jsonSchema, { wrongProperty: "x" }) ).toThrow(); }); it("returns the preset when properties cover all preset fields", () => { expect(PresetService.getOperatorPreset(mockPresetEnabledSchema.jsonSchema, { presetProperty: "v" })).toEqual({ presetProperty: "v", }); }); it("strips non-preset properties when returning the preset", () => { expect( PresetService.getOperatorPreset(mockPresetEnabledSchema.jsonSchema, { presetProperty: "v", otherProperty: "extra", }) ).toEqual({ presetProperty: "v" }); }); }); describe("filterOperatorPresetProperties", () => { it("returns empty when input is empty (never adds keys)", () => { expect(PresetService.filterOperatorPresetProperties(mockPresetEnabledSchema.jsonSchema, {})).toEqual({}); }); it("filters out non-preset properties only when at least one preset property is present", () => { // Ajv 8's removeAdditional traversal short-circuits when `required` fails, // so an input that contains *only* non-preset keys is left untouched. // The "+ extras" case below covers the normal stripping path. expect( PresetService.filterOperatorPresetProperties(mockPresetEnabledSchema.jsonSchema, { wrongProperty: "x" }) ).toEqual({ wrongProperty: "x" }); }); it("keeps preset properties and strips extras", () => { expect( PresetService.filterOperatorPresetProperties(mockPresetEnabledSchema.jsonSchema, { presetProperty: "v", otherProperty: "extra", }) ).toEqual({ presetProperty: "v" }); }); }); }); }); ================================================ FILE: frontend/src/app/workspace/service/preset/preset.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import Ajv from "ajv"; import { cloneDeep, has, indexOf, isEqual, merge, pickBy } from "lodash"; import { NzMessageService } from "ng-zorro-antd/message"; import { Observable, of, Subject } from "rxjs"; import { UserConfigService } from "src/app/common/service/user/config/user-config.service"; import { asType, isType } from "src/app/common/util/assert"; import { CustomJSONSchema7 } from "../../types/custom-json-schema.interface"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { first, map } from "rxjs/operators"; /** * Preset service enables saving and applying of Presets, which are objects * that represent a collection of settings that can be applied all together. * The intent is to allow a user to save settings pertaining to some texera object, and reuse them later * * Currently this mainly works for Operator properties. EX: for MysqlSource, users may reuse presets of address/port/username/database/table * Operator presets are determined by the presence of the 'enable-presets' in the individual properties of each OperatorSchema (see CustomJSONSchema7) * This service relies on DictionaryService for storage, which in turn requires the client to be logged in * @author Albert Liu */ /** * determines icon used by NzMessageService (the little notification that shows when a preset is saved) * success == green checkmark * error == red x-mark * warning == yellow !-mark * info == blue i-mark */ type AlertMessageType = "success" | "error" | "info" | "warning"; const PresetSchema: CustomJSONSchema7 = { type: "object", additionalProperties: { type: "string", pattern: "^\\S.*$", }, }; const PresetArraySchema: CustomJSONSchema7 = { type: "array", items: PresetSchema, uniqueItems: true, }; export type Preset = { [key: string]: string | number | boolean }; export type PresetDictionary = { [Key: string]: Preset[]; }; @Injectable({ providedIn: "root", }) export class PresetService { private static DICT_PREFIX = "Preset"; // key prefix when storing data in dictionary service private static ajv = new Ajv(); private static ajvStrip = new Ajv({ useDefaults: true, removeAdditional: true, strict: false }); // removes extra properties from an object that aren't described by schema private static isPreset = PresetService.ajv.compile(PresetSchema); private static isPresetArray = PresetService.ajv.compile(PresetArraySchema); public readonly applyPresetStream: Observable<{ type: string; target: string; preset: Preset }>; public readonly savePresetsStream: Observable<{ type: string; target: string; presets: Preset[] }>; private applyPresetSubject = new Subject<{ type: string; target: string; preset: Preset }>(); // event stream for applying presets to a target (usually type "operator" with specific operatorID as target) private savePresetSubject = new Subject<{ type: string; target: string; presets: Preset[] }>(); // event stream for saving preset[]s to a target (usually type "operator" an operatorType as target) constructor( private userConfigService: UserConfigService, private messageService: NzMessageService, private workflowActionService: WorkflowActionService, private operatorMetadataService: OperatorMetadataService ) { this.applyPresetStream = this.applyPresetSubject.asObservable(); this.savePresetsStream = this.savePresetSubject.asObservable(); this.handleApplyOperatorPresets(); } /** * broadcast applyPreset event, triggering any subscriber actions * By default, type "operator" applyPreset events trigger preset being applied to the targeted operator * @param type string, usually "operator" * @param target string, usually an operatorID * @param preset a subset of operator properties that will be applied */ public applyPreset(type: string, target: string, preset: Preset) { this.applyPresetSubject.next({ type: type, target: target, preset: preset }); } /** * broadcast savePresets event and also save preset to presetDict, which is a *view* (in the database sense) of DictionaryService's dictionary that only stores presets * @param type string, usually "operator" * @param target string, usualy operatorType * @param presets Preset[] * @param displayMessage message to display when saving presets * @param messageType see AlertMessageType, determines icon used in popup message */ public savePresets( type: string, target: string, presets: Preset[], displayMessage?: string | null, messageType: AlertMessageType = "success" ) { if (presets.length > 0) { this.userConfigService.set(`${type}-${target}`, JSON.stringify(presets)); } else { this.userConfigService.delete(`${type}-${target}`); } this.savePresetSubject.next({ type: type, target: target, presets: presets }); this.displaySavePresetMessage(messageType, displayMessage); } /** * broadcast savePresets event and also save preset to presetDict, which is a *view* (in the database sense) of DictionaryService's dictionary that only stores presets * @param type string, usually "operator" * @param target string, usualy operatorType * @param presets Preset[] * @param displayMessage message to display when saving presets * @param messageType see AlertMessageType, determines icon used in popup message */ public createPreset( type: string, target: string, preset: Preset, displayMessage?: string | null, messageType: AlertMessageType = "success" ) { this.userConfigService .fetchKey(`${type}-${target}`) .pipe(first()) .subscribe(presetsString => { let presets = JSON.parse(presetsString ?? "[]") as Preset[]; if (contains(presets, preset)) { throw new Error("attempting to create preset that already exists"); } presets.push(preset); this.savePresets(type, target, presets, displayMessage, messageType); }); } /** * broadcast savePresets event and also save preset to presetDict, which is a *view* (in the database sense) of DictionaryService's dictionary that only stores presets * @param type string, usually "operator" * @param target string, usualy operatorType * @param presets Preset[] * @param displayMessage message to display when saving presets * @param messageType see AlertMessageType, determines icon used in popup message */ public updatePreset( type: string, target: string, originalPreset: Preset, replacementPreset: Preset, displayMessage?: string | null, messageType: AlertMessageType = "success" ) { this.userConfigService .fetchKey(`${type}-${target}`) .pipe(first()) .subscribe(presetsString => { let presets = JSON.parse(presetsString ?? "[]") as Preset[]; if (!contains(presets, originalPreset)) { throw new Error("attempting to update preset that doesn't exist"); } else if (contains(presets, replacementPreset)) { // implicit deletion by replacing original with existing preset presets.splice(indexOf(presets, originalPreset), 1); } else { presets[indexOf(presets, originalPreset)] = replacementPreset; } this.savePresets(type, target, presets, displayMessage, messageType); }); } /** * broadcast savePresets event and also save preset to presetDict, which is a *view* (in the database sense) of DictionaryService's dictionary that only stores presets * @param type string, usually "operator" * @param target string, usualy operatorType * @param presets Preset[] * @param displayMessage message to display when saving presets * @param messageType see AlertMessageType, determines icon used in popup message */ public updateOrCreatePreset( type: string, target: string, originalPreset: Preset, replacementPreset: Preset, displayMessage?: string | null, messageType: AlertMessageType = "success" ) { this.userConfigService .fetchKey(`${type}-${target}`) .pipe(first()) .subscribe(oldpresets => { let presets = JSON.parse(oldpresets ?? "[]") as Preset[]; if (isEqual(originalPreset, replacementPreset)) { // no modification: no update required } else if (!contains(presets, originalPreset) && !contains(presets, replacementPreset)) { presets.push(replacementPreset); } else if (!contains(presets, originalPreset) && contains(presets, replacementPreset)) { // no modification: old preset doesn't exist to be updated, new preset already exists } else if (contains(presets, originalPreset) && contains(presets, replacementPreset)) { // implicit deletion by replacing original with existing preset presets.splice(indexOf(presets, originalPreset), 1); } else { presets[indexOf(presets, originalPreset)] = replacementPreset; } this.savePresets(type, target, presets, displayMessage, messageType); }); } /** * broadcast savePresets event and also save preset to presetDict, which is a *view* (in the database sense) of DictionaryService's dictionary that only stores presets * removes preset if it exists * @param type string, usually "operator" * @param target string, usualy operatorType * @param preset preset to remove * @param displayMessage message to display when saving presets * @param messageType see AlertMessageType, determines icon used in popup message */ public deletePreset( type: string, target: string, preset: Preset, displayMessage?: string | null, messageType: AlertMessageType = "error" ) { this.getPresets(type, target) .pipe(first()) .subscribe(presets => { let modifiedPresets = presets.filter(oldPreset => !isEqual(oldPreset, preset)); this.savePresets(type, target, modifiedPresets, displayMessage, messageType); }); } /** * get presets from presetDict * @param type string, usually "operator" * @param target string, usualy operatorType * @returns Preset[] */ public getPresets(type: string, target: string): Observable> { return this.userConfigService.fetchKey(`${type}-${target}`).pipe( map(presets => { let parsedPresets = JSON.parse(presets ?? "[]"); if (this.isValidPresetArray(parsedPresets)) { return parsedPresets; } else { throw new Error(`stored preset data ${presets} is formatted incorrectly`); } }) ); } /** * extracts preset schema from operator schema and validates a preset with it * @param preset * @param operatorID * @returns boolean */ public isValidOperatorPreset(preset: Preset, operatorID: string): boolean { const presetSchema = PresetService.getOperatorPresetSchema( this.operatorMetadataService.getOperatorSchema( this.workflowActionService.getTexeraGraph().getOperator(operatorID).operatorType ).jsonSchema ); const fitsSchema = PresetService.ajv.compile(presetSchema)(preset); const noEmptyProperties = Object.keys(preset).every( (key: string) => !isType(preset[key], "string") || (preset[key]).trim().length > 0 ); return fitsSchema && noEmptyProperties; } /** * extracts preset schema from operator schema and validates a preset with it. * also checks if preset exists in presetDict already. * @param preset * @param operatorID * @returns boolean */ public isValidNewOperatorPreset(preset: Preset, operatorID: string): Observable { if (!this.isValidOperatorPreset(preset, operatorID)) return of(false); return this.getPresets( "operator", this.workflowActionService.getTexeraGraph().getOperator(operatorID).operatorType ).pipe( first(), map(presets => { console.log(!presets.some(existingPreset => isEqual(preset, existingPreset)), "vn"); return !presets.some(existingPreset => isEqual(preset, existingPreset)); }) ); } public isValidPreset(preset: any): preset is Preset { return asType(PresetService.isPreset(preset), "boolean"); } public isValidPresetArray(presets: any[]): presets is Preset[] { return asType(PresetService.isPresetArray(presets), "boolean"); } private displaySavePresetMessage(messageType: AlertMessageType, displayMessage?: string | null) { if (displayMessage === null) return; // do not display explicitly null message if (displayMessage === undefined) { // if undefined, display default messages switch (messageType) { case "error": this.messageService.error("Preset deleted"); break; case "info": throw new Error("no default save preset info message"); // break; case "success": this.messageService.success("Preset saved"); break; case "warning": throw new Error("no default save preset warning message"); // break; } } else { // display explicitly passed message and messageType switch (messageType) { case "error": this.messageService.error(displayMessage); break; case "info": this.messageService.info(displayMessage); break; case "success": this.messageService.success(displayMessage); break; case "warning": this.messageService.warning(displayMessage); break; } } } /** * when presets are applied, check for operator presets, and apply them using workflowActionService * to change operator properties */ private handleApplyOperatorPresets() { this.applyPresetStream.subscribe({ next: applyEvent => { if ( applyEvent.type === "operator" && this.workflowActionService.getTexeraGraph().hasOperator(applyEvent.target) ) { if (this.isValidOperatorPreset(applyEvent.preset, applyEvent.target)) { this.workflowActionService.setOperatorProperty( applyEvent.target, merge( cloneDeep( this.workflowActionService.getTexeraGraph().getOperator(applyEvent.target).operatorProperties ), applyEvent.preset ) ); } else { const schema = PresetService.getOperatorPresetSchema( this.operatorMetadataService.getOperatorSchema( this.workflowActionService.getTexeraGraph().getOperator(applyEvent.target).operatorType ).jsonSchema ); throw new Error( `Error applying preset: preset ${applyEvent.preset} was not a valid preset for ${applyEvent.target} with schema ${schema}` ); } } }, }); } /** * get preset schema from operator schema. * preset schema is just the operator schema with only properties that have 'enable-presets': true * all properties are required * @param operatorSchema * @returns preset schema */ public static getOperatorPresetSchema(operatorSchema: CustomJSONSchema7): CustomJSONSchema7 { const copy = cloneDeep(operatorSchema); if (operatorSchema.properties === undefined) throw new Error(`provided operator schema ${operatorSchema} has no properties`); const properties = pickBy( copy.properties, prop => has(prop, "enable-presets") && (prop as any)["enable-presets"] === true ); if (isEqual(properties, {})) throw new Error(`provided operator schema ${operatorSchema} has no preset properties`); return { type: "object", properties: properties, required: Object.keys(properties), additionalProperties: false, }; } /** * get preset from operator properties if it has a preset schema and a valid preset (all properties are assigned) * Throws an error if operatorProperties doesn't have all the properties in the presetSchema, unlike filterOperatorProperties. * @param operatorSchema * @param operatorProperties * @returns Preset */ public static getOperatorPreset(operatorSchema: CustomJSONSchema7, operatorProperties: object): Preset { const copy = cloneDeep(operatorProperties as Preset); const presetSchema = this.getOperatorPresetSchema(operatorSchema); const strip = this.ajvStrip.compile(presetSchema); // this validator also removes extra properties that aren't a part of the preset const result = strip(copy); if (asType(result, "boolean") === true) return copy; throw new Error( `provided operator properties ${operatorProperties} does not conform to preset schema ${presetSchema}` ); } /** * get the subset of operatorProperties that only includes properties that are in its PresetSchema * this doesn't always yield a complete preset, unlike getOperatorPreset * @param operatorSchema * @param operatorProperties * @returns */ public static filterOperatorPresetProperties(operatorSchema: CustomJSONSchema7, operatorProperties: object): Preset { const copy = cloneDeep(operatorProperties as Preset); const presetSchema = this.getOperatorPresetSchema(operatorSchema); const strip = this.ajvStrip.compile(presetSchema); // this validator also removes extra properties that aren't a part of the preset strip(copy); return copy; } } function contains(arr: any[], value: any) { return arr.some(elem => isEqual(elem, value)); } ================================================ FILE: frontend/src/app/workspace/service/report-generation/report-generation.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import html2canvas from "html2canvas"; import { forkJoin, Observable, Observer } from "rxjs"; import { map } from "rxjs/operators"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { WorkflowResultService } from "../workflow-result/workflow-result.service"; import { NotificationService } from "src/app/common/service/notification/notification.service"; import { AiAnalystService } from "../ai-analyst/ai-analyst.service"; import { AppSettings } from "src/app/common/app-setting"; const AI_ASSISTANT_API_BASE_URL = `${AppSettings.getApiEndpoint()}`; @Injectable({ providedIn: "root", }) export class ReportGenerationService { private isAIAssistantEnabled: boolean | null = null; constructor( private http: HttpClient, public workflowActionService: WorkflowActionService, private workflowResultService: WorkflowResultService, private notificationService: NotificationService, private aiAnalystService: AiAnalystService ) {} /** * Captures a snapshot of the workflow editor and returns it as a base64-encoded PNG image URL. * @param {string} workflowName - The name of the workflow. * @returns {Observable} An observable that emits the base64-encoded PNG image URL of the workflow snapshot. */ public generateWorkflowSnapshot(workflowName: string): Observable { return new Observable((observer: Observer) => { const element = document.querySelector("#workflow-editor") as HTMLElement; if (!element) { observer.error("Workflow editor element not found"); return; } // Query all the images (from SVG or other tags) const images = element.querySelectorAll("image"); // Create promises to load and convert images to Base64 const promises: Promise[] = Array.from(images).map(img => { const imgSrc = img.getAttribute("xlink:href") || img.getAttribute("href"); if (imgSrc) { return this.fetchImageAsBase64(imgSrc) .then(base64 => { // Set the Base64 image as the source of the SVG or img element img.setAttribute("href", base64); }) .catch(error => { console.error(`Failed to load image: ${imgSrc}`, error); }); } return Promise.resolve(); // If there's no src, resolve immediately }); // Wait for all images to be converted to Base64 Promise.all(promises) .then(() => { // Render the element after all images are ready return html2canvas(element, { logging: true, useCORS: true, allowTaint: true, foreignObjectRendering: true, }); }) .then((canvas: HTMLCanvasElement) => { const dataUrl: string = canvas.toDataURL("image/png"); observer.next(dataUrl); observer.complete(); }) .catch(error => { observer.error(error); }); }); } private fetchImageAsBase64(imageUrl: string): Promise { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.onload = function () { const reader = new FileReader(); reader.onloadend = function () { resolve(reader.result as string); // Base64 string result }; reader.onerror = function () { reject("Failed to convert image to Base64"); }; reader.readAsDataURL(xhr.response); // Convert to Base64 }; xhr.onerror = function () { reject(`Failed to load image from ${imageUrl}`); }; xhr.open("GET", imageUrl); xhr.responseType = "blob"; // Get the image as binary data xhr.send(); }); } /** * Retrieves and processes results for all specified operators within the workflow. * * This function iterates over each operator ID, fetches the corresponding result details via `retrieveOperatorInfoReport`, * and collects these results into an array. The function returns an observable that emits the processed results, * which can be used to generate a comprehensive HTML report or for further processing. * * @param {string[]} operatorIds - An array of operator IDs representing each operator in the workflow. * @returns {Observable<{operatorId: string, html: string}[]>} - An observable that emits an array of objects, * each containing an `operatorId` and its corresponding HTML representation of the result. * This result array can be used to generate an HTML report or for other purposes. */ public getAllOperatorResults(operatorIds: string[]): Observable<{ operatorId: string; html: string }[]> { const observables = operatorIds.map(operatorId => { const allResults: { operatorId: string; html: string }[] = []; return this.retrieveOperatorInfoReport(operatorId, allResults).pipe(map(() => allResults[0])); }); return forkJoin(observables); } /** * Retrieves and processes results for all specified operators within the workflow. * This function iterates over each operator ID, fetches the corresponding result details via `retrieveOperatorInfoReport`, * and collects these results into an array. The function returns an observable that emits the processed results, * which can be used to generate a comprehensive HTML report or for further processing. * * @param operatorId * @param allResults * @returns {Promise} */ public retrieveOperatorInfoReport( operatorId: string, allResults: { operatorId: string; html: string }[] ): Observable { return new Observable(observer => { this.aiAnalystService.isOpenAIEnabled().subscribe(AIEnabled => { try { // Retrieve the result service and paginated result service for the operator const resultService = this.workflowResultService.getResultService(operatorId); const paginatedResultService = this.workflowResultService.getPaginatedResultService(operatorId); const workflowContent = this.workflowActionService.getWorkflowContent(); const operatorDetails = workflowContent.operators.find(op => op.operatorID === operatorId); const operatorDetailsHtml = `

    Operator Details

    `; this.generateComment(operatorDetails).subscribe(comment => { if (paginatedResultService) { paginatedResultService.selectPage(1, 10).subscribe({ next: (pageData: any) => { const table: any[] = pageData.table; if (!table.length) { allResults.push({ operatorId, html: `

    Operator ID: ${operatorId}

    No results found for operator

    ${comment}
    `, }); observer.next(); observer.complete(); return; } const columns: string[] = Object.keys(table[0]); const rows: any[][] = table.map(row => columns.map(col => row[col])); const htmlContent: string = `

    Operator ID: ${operatorId}

    ${columns .map( col => `` ) .join("")} ${rows .map( row => `${row .map( cell => `` ) .join("")}` ) .join("")}
    ${col}
    ${String( cell )}
    ${comment}
    `; allResults.push({ operatorId, html: htmlContent }); observer.next(); observer.complete(); }, error: (error: unknown) => { const errorMessage = (error as Error).message || "Unknown error"; this.notificationService.error( `Error processing results for operator ${operatorId}: ${errorMessage}` ); observer.error(error); }, }); } else if (resultService) { const data = resultService.getCurrentResultSnapshot(); if (data) { const parser = new DOMParser(); const lastData = data[data.length - 1]; const doc = parser.parseFromString(Object(lastData)["html-content"], "text/html"); doc.documentElement.style.height = "50%"; doc.body.style.height = "50%"; const firstDiv = doc.body.querySelector("div"); if (firstDiv) firstDiv.style.height = "100%"; const serializer = new XMLSerializer(); const newHtmlString = serializer.serializeToString(doc); const visualizationHtml = `

    Operator ID: ${operatorId}

    ${newHtmlString}
    ${comment}
    `; allResults.push({ operatorId, html: visualizationHtml }); observer.next(); observer.complete(); } else { allResults.push({ operatorId, html: `

    Operator ID: ${operatorId}

    No data found for operator

    ${comment}
    `, }); observer.next(); observer.complete(); } } else { allResults.push({ operatorId, html: `

    Operator ID: ${operatorId}

    No results found for operator

    ${comment}
    `, }); observer.next(); observer.complete(); } }); } catch (error: unknown) { const errorMessage = (error as Error).message || "Unknown error"; this.notificationService.error( `Unexpected error in retrieveOperatorInfoReport for operator ${operatorId}: ${errorMessage}` ); observer.error(error); } }); }); } /** * Generates an HTML report containing the workflow snapshot and all operator results, and triggers a download of the report. * * @param {string} workflowSnapshot - The base64-encoded PNG image URL of the workflow snapshot. * @param {string[]} allResults - An array of HTML strings representing the results of each operator. * @param {string} workflowName - The name of the workflow, used for naming the final report. * @returns {void} */ public generateReportAsHtml(workflowSnapshot: string, allResults: string[], workflowName: string): void { const workflowContent = this.workflowActionService.getWorkflowContent(); // Call generateSummaryComment and subscribe to its result this.generateSummaryComment(workflowContent).subscribe(comment => { const finalComment = comment; // Get the generated comment const htmlContent = ` Operator Results

    ${workflowName} Static State

    Workflow Snapshot
    ${allResults.map(result => `
    ${result}
    `).join("")}

    Summary

    ${finalComment}
    `; const blob = new Blob([htmlContent], { type: "text/html" }); const url = URL.createObjectURL(blob); const fileName = `${workflowName}-report.html`; const a = document.createElement("a"); a.href = url; a.download = fileName; a.click(); URL.revokeObjectURL(url); }); } /** * Generates an insightful comment for the given operator information by utilizing the AI Assistant service. * The comment is tailored for an educated audience without a deep understanding of statistics. * * @param {any} operatorInfo - The operator information in JSON format, which will be used to generate the comment. * @returns {Promise} A promise that resolves to a string containing the generated comment or an error message * if the generation fails or the AI Assistant is not enabled. */ public generateComment(operatorInfo: any): Observable { const prompt = ` You are a statistical analysis expert. You will be provided with operator information in JSON format and an HTML result. Your task is to analyze the data and provide a detailed, which means at least 80 words, insightful comment tailored for an audience that is highly educated but does not understand statistics. Operator Info: ${JSON.stringify(operatorInfo, null, 2)} The output cannot be in markdown format, and must be plain text. Follow these steps to generate your response: Parse the provided JSON data under “Operator Info.” Use the appropriate template based on the operator type to create a comment: For general operators, use: “This operator processes data to achieve specific goals.” For “CSVFileScan” type operators, use: “Briefly introduce the data composition, such as included data types.” For “PythonUDFV2” type operators, use: “Refer to the ‘code’ section in the operator detail to understand its purpose. Ensure correct HTML rendering of results.” For “Visualization” type operators, use: “This type of operator is usually associated with a chart or plot. You need to remind them that the graph is interactive and to care about the size and variation of the data.” Again, the output comment should follow the format specified above and should be insightful for non-experts.`; // Call the openai function and pass the generated prompt return this.aiAnalystService.sendPromptToOpenAI(prompt); } /** * Generates a concise and insightful summary comment for the given operator information by utilizing the AI Assistant service. * The summary is tailored for an educated audience without a deep understanding of statistics, focusing on the key findings, * notable patterns, and potential areas of improvement related to the workflow and its components, particularly UDFs. * * @param {any} operatorInfo - The operator information in JSON format, which will be used to generate the summary comment. * @returns {Promise} A promise that resolves to a string containing the generated summary comment or an error message * if the generation fails or the AI Assistant is not enabled. */ public generateSummaryComment(operatorInfo: any): Observable { const prompt = ` You are a statistical analysis expert. You will be provided with operator information in JSON format and an HTML result. You should provide a concise (which means at least 150 words) and insightful summary comment for an audience who is highly educated but does not understand statistics (non-experts). Operator Info: ${JSON.stringify(operatorInfo, null, 2)} The output cannot be in markdown format, and must be plain text. Follow these steps to generate your response: The summary should: 1. Highlight the key findings and overall performance of the workflow, with particular attention to the UDFs as they are often the most critical components. 2. Mention any notable patterns, trends, or anomalies observed in the operator results, especially those related to UDFs. 3. Suggest potential areas of improvement or further investigation, particularly regarding the efficiency and accuracy of the UDFs. 4. Ensure the summary helps the reader gain a comprehensive understanding of the workflow and its global implications. Again, the output comment should follow the format specified above and should be insightful for non-experts.`; // Call the openai function and pass the generated prompt return this.aiAnalystService.sendPromptToOpenAI(prompt); } } ================================================ FILE: frontend/src/app/workspace/service/undo-redo/undo-redo.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { StubOperatorMetadataService } from "../operator-metadata/stub-operator-metadata.service"; import { JointUIService } from "../joint-ui/joint-ui.service"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { mockPoint, mockResultPredicate, mockScanPredicate } from "../workflow-graph/model/mock-workflow-data"; import { inject, TestBed } from "@angular/core/testing"; import { UndoRedoService } from "./undo-redo.service"; import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; import { commonTestProviders } from "../../../common/testing/test-utils"; describe("UndoRedoService", () => { let service: UndoRedoService; let workflowActionService: WorkflowActionService; beforeEach(() => { TestBed.configureTestingModule({ providers: [ UndoRedoService, WorkflowActionService, WorkflowUtilService, JointUIService, { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, ...commonTestProviders, ], }); service = TestBed.inject(UndoRedoService); workflowActionService = TestBed.inject(WorkflowActionService); }); it("should be created", inject([UndoRedoService], (injectedService: UndoRedoService) => { expect(injectedService).toBeTruthy(); })); it("executing command should append to stack", () => { workflowActionService.addOperator(mockScanPredicate, mockPoint); expect(service.getUndoLength()).toEqual(1); expect(service.getRedoLength()).toEqual(0); }); it("redoing command should move from undo to redo stack and vice versa", () => { workflowActionService.addOperator(mockScanPredicate, mockPoint); service.undoAction(); expect(service.getUndoLength()).toEqual(0); expect(service.getRedoLength()).toEqual(1); service.redoAction(); expect(service.getUndoLength()).toEqual(1); expect(service.getRedoLength()).toEqual(0); }); it("executing new action clears redo stack", () => { workflowActionService.addOperator(mockScanPredicate, mockPoint); service.undoAction(); expect(service.getUndoLength()).toEqual(0); expect(service.getRedoLength()).toEqual(1); workflowActionService.addOperator(mockResultPredicate, mockPoint); expect(service.getUndoLength()).toEqual(1); expect(service.getRedoLength()).toEqual(0); }); }); ================================================ FILE: frontend/src/app/workspace/service/undo-redo/undo-redo.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import * as Y from "yjs"; /** * After the introduction of shared-editing, this service basically wraps the internal yjs undo-redo manager, except it * also adds some of our custom conditions for being able to undo/redo. */ @Injectable({ providedIn: "root", }) export class UndoRedoService { // lets us know whether to listen to the JointJS observables, most of the time we don't public listenJointCommand: boolean = true; // private testGraph: WorkflowGraphReadonly; private undoManager?: Y.UndoManager; private workFlowModificationEnabled = true; public setUndoManager(undoManager: Y.UndoManager) { this.undoManager = undoManager; } public enableWorkFlowModification() { this.workFlowModificationEnabled = true; } public disableWorkFlowModification() { this.workFlowModificationEnabled = false; } public undoAction(): void { if (!this.workFlowModificationEnabled) { console.error("attempted to undo a workflow-modifying command while workflow modification is disabled"); return; } if (this.undoManager && this.undoManager.canUndo()) { this.setListenJointCommand(false); this.undoManager.undo(); this.setListenJointCommand(true); } } public redoAction(): void { if (!this.workFlowModificationEnabled) { console.error("attempted to redo a workflow-modifying command while workflow modification is disabled"); return; } if (this.undoManager && this.undoManager.canRedo()) { this.setListenJointCommand(false); this.undoManager.redo(); this.setListenJointCommand(true); } } public setListenJointCommand(toggle: boolean): void { this.listenJointCommand = toggle; } public getUndoLength(): number { return this.undoManager?.undoStack.length; } public getRedoLength(): number { return this.undoManager?.redoStack.length; } public canUndo(): boolean { if (this.undoManager) return this.workFlowModificationEnabled && this.undoManager?.canUndo(); else return false; } public canRedo(): boolean { if (this.undoManager) return this.workFlowModificationEnabled && this.undoManager?.canRedo(); else return false; } public clearUndoStack(): void { this.undoManager?.clear(true, false); } public clearRedoStack(): void { this.undoManager?.clear(false, true); } } ================================================ FILE: frontend/src/app/workspace/service/validation/validation-workflow.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { inject, TestBed } from "@angular/core/testing"; import { ValidationWorkflowService } from "./validation-workflow.service"; import { mockPoint, mockResultPredicate, mockScanPredicate, mockScanResultLink, mockScanSentimentLink, mockSentimentPredicate, } from "../workflow-graph/model/mock-workflow-data"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { UndoRedoService } from "../undo-redo/undo-redo.service"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../operator-metadata/stub-operator-metadata.service"; import { JointUIService } from "../joint-ui/joint-ui.service"; import { marbles } from "rxjs-marbles"; import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; import { map } from "rxjs/operators"; import { commonTestProviders } from "../../../common/testing/test-utils"; describe("ValidationWorkflowService", () => { let validationWorkflowService: ValidationWorkflowService; let workflowActionservice: WorkflowActionService; beforeEach(() => { TestBed.configureTestingModule({ providers: [ WorkflowActionService, WorkflowUtilService, UndoRedoService, ValidationWorkflowService, JointUIService, { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, ...commonTestProviders, ], }); validationWorkflowService = TestBed.inject(ValidationWorkflowService); workflowActionservice = TestBed.inject(WorkflowActionService); }); it("should be created", inject([ValidationWorkflowService], (service: ValidationWorkflowService) => { expect(service).toBeTruthy(); })); it("should receive true from validateOperator when operator box is connected and required properties are complete ", () => { workflowActionservice.addOperator(mockScanPredicate, mockPoint); workflowActionservice.addOperator(mockResultPredicate, mockPoint); workflowActionservice.addLink(mockScanResultLink); const newProperty = { tableName: "test-table" }; workflowActionservice.setOperatorProperty(mockScanPredicate.operatorID, newProperty); expect(validationWorkflowService.validateOperator(mockResultPredicate.operatorID).isValid).toBeTruthy(); expect(validationWorkflowService.validateOperator(mockScanPredicate.operatorID).isValid).toBeTruthy(); }); it( "should subscribe the changes of validateOperatorStream when operator box is connected and required properties are complete ", marbles(m => { const testEvents = m.hot("-a-b-c----d-", { a: () => workflowActionservice.addOperator(mockScanPredicate, mockPoint), b: () => workflowActionservice.addOperator(mockResultPredicate, mockPoint), c: () => workflowActionservice.addLink(mockScanResultLink), d: () => workflowActionservice.setOperatorProperty(mockScanPredicate.operatorID, { tableName: "test-table" }), }); testEvents.subscribe(action => action()); const expected = m.hot("-u-v-(yz)-m-", { u: { operatorID: "1", isValid: false }, v: { operatorID: "3", isValid: false }, y: { operatorID: "1", isValid: false }, z: { operatorID: "3", isValid: true }, m: { operatorID: "1", isValid: true }, }); m.expect( validationWorkflowService.getOperatorValidationStream().pipe( map(value => ({ operatorID: value.operatorID, isValid: value.validation.isValid, })) ) ).toBeObservable(expected); }) ); it("should receive false from validateOperator when operator box is not connected or required properties are not complete ", () => { workflowActionservice.addOperator(mockScanPredicate, mockPoint); workflowActionservice.addOperator(mockResultPredicate, mockPoint); workflowActionservice.addLink(mockScanResultLink); expect(validationWorkflowService.validateOperator(mockResultPredicate.operatorID).isValid).toBeTruthy(); expect(validationWorkflowService.validateOperator(mockScanPredicate.operatorID).isValid).toBeFalsy(); }); // TODO: this test is incompatible with shared editing. // it( // "should subscribe the changes of validateOperatorStream when one operator box is deleted after valid status ", // marbles(m => { // const testEvents = m.hot("-a-b-c----d-e-----", { // a: () => workflowActionservice.addOperator(mockScanPredicate, mockPoint), // b: () => workflowActionservice.addOperator(mockResultPredicate, mockPoint), // c: () => workflowActionservice.addLink(mockScanResultLink), // d: () => workflowActionservice.setOperatorProperty(mockScanPredicate.operatorID, { tableName: "test-table" }), // e: () => workflowActionservice.deleteOperator(mockResultPredicate.operatorID), // }); // // testEvents.subscribe(action => action()); // // const expected = m.hot("-t-u-(vw)-x-(yz)-)", { // t: { operatorID: "1", isValid: false }, // u: { operatorID: "3", isValid: false }, // v: { operatorID: "1", isValid: false }, // w: { operatorID: "3", isValid: true }, // x: { operatorID: "1", isValid: true }, // y: { operatorID: "1", isValid: false }, // If one of the oprator is deleted, the other one is invaild since it is isolated // z: { operatorID: "3", isValid: false }, // }); // // m.expect( // validationWorkflowService.getOperatorValidationStream().pipe( // map(value => ({ // operatorID: value.operatorID, // isValid: value.validation.isValid, // })) // ) // ).toBeObservable(expected); // }) // ); it( "should subscribe the changes of validateOperatorStream when operator link is deleted after valid status ", marbles(m => { const testEvents = m.hot("-a-b-c----d-e-f--", { a: () => workflowActionservice.addOperator(mockScanPredicate, mockPoint), b: () => workflowActionservice.addOperator(mockSentimentPredicate, mockPoint), c: () => workflowActionservice.addLink(mockScanSentimentLink), d: () => workflowActionservice.setOperatorProperty(mockScanPredicate.operatorID, { tableName: "test-table" }), e: () => workflowActionservice.setOperatorProperty(mockSentimentPredicate.operatorID, { attribute: "test-attribute", resultAttribute: "result-attribtue", }), f: () => workflowActionservice.deleteLinkWithID(mockScanSentimentLink.linkID), }); testEvents.subscribe(action => action()); const expected = m.hot("-s-t-(uv)-w-x-(yz)-", { s: { operatorID: "1", isValid: false }, t: { operatorID: "2", isValid: false }, u: { operatorID: "1", isValid: false }, v: { operatorID: "2", isValid: false }, w: { operatorID: "1", isValid: true }, x: { operatorID: "2", isValid: true }, y: { operatorID: "1", isValid: true }, z: { operatorID: "2", isValid: false }, // If the link is deleted, the one missing input link is invalid }); m.expect( validationWorkflowService.getOperatorValidationStream().pipe( map(value => ({ operatorID: value.operatorID, isValid: value.validation.isValid, })) ) ).toBeObservable(expected); }) ); it("should consider disabled operators when validating workflow", () => { workflowActionservice.addOperator(mockScanPredicate, mockPoint); workflowActionservice.addOperator(mockResultPredicate, mockPoint); workflowActionservice.addLink(mockScanResultLink); workflowActionservice.setOperatorProperty(mockScanPredicate.operatorID, { tableName: "test-table", }); expect(Object.entries(validationWorkflowService.getCurrentWorkflowValidationError().errors).length).toEqual(0); const mockScanPredicate2 = { ...mockScanPredicate, operatorID: "mockScan2", }; const mockResultPredicate2 = { ...mockResultPredicate, operatorID: "mockResult2", }; const mockScanResultLink2 = { linkID: "mock-scan-result-link-2", source: { operatorID: mockScanPredicate2.operatorID, portID: mockScanPredicate2.outputPorts[0].portID, }, target: { operatorID: mockResultPredicate2.operatorID, portID: mockResultPredicate2.inputPorts[0].portID, }, }; workflowActionservice.addOperator(mockScanPredicate2, mockPoint); workflowActionservice.addOperator(mockResultPredicate2, mockPoint); workflowActionservice.addLink(mockScanResultLink2); console.log(validationWorkflowService.getCurrentWorkflowValidationError().errors); expect(Object.entries(validationWorkflowService.getCurrentWorkflowValidationError().errors).length).toEqual(1); workflowActionservice.getTexeraGraph().disableOperator(mockScanPredicate2.operatorID); workflowActionservice.getTexeraGraph().disableOperator(mockResultPredicate2.operatorID); expect(Object.entries(validationWorkflowService.getCurrentWorkflowValidationError().errors).length).toEqual(0); }); }); ================================================ FILE: frontend/src/app/workspace/service/validation/validation-workflow.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { BehaviorSubject, merge, Observable, Subject } from "rxjs"; import { Injectable } from "@angular/core"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { OperatorSchema } from "../../types/operator-schema.interface"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import Ajv from "ajv"; import { map } from "rxjs/operators"; import { DynamicSchemaService } from "../dynamic-schema/dynamic-schema.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { WorkflowGraph, WorkflowGraphReadonly } from "../workflow-graph/model/workflow-graph"; export type ValidationError = { isValid: false; messages: Record; }; export type Validation = { isValid: true } | ValidationError; export type ValidationOutput = { errors: Record; workflowEmpty: boolean; }; /** * ValidationWorkflowService handles the logic to check whether the operator is valid * 1. When the user add/delete operators/links * 2. When the user complete/delete operator properties * 3. When the operator ports are all connected * * The operator will become valid if all the ports are connected and its required properties * are completed by the users. * * AJV is a javascript library that is used to validate a data object against a structure defined * using a JSON Schema. * * @author Angela Wang */ @UntilDestroy() @Injectable({ providedIn: "root", }) export class ValidationWorkflowService { public static readonly VALIDATION_OPERATOR_INPUT_MESSAGE = "inputs"; public static readonly VALIDATION_OPERATOR_OUTPUT_MESSAGE = "outputs"; private operatorSchemaList: ReadonlyArray = []; // stream of an individual's validation status is updated, whether it's validation sucess or validation error private readonly operatorValidationStream = new Subject<{ operatorID: string; validation: Validation; }>(); // stream of global validation error status is updated, only errors will be reported private readonly workflowValidationErrorStream = new BehaviorSubject({ errors: {}, workflowEmpty: false, }); private ajv = new Ajv({ allErrors: true, strict: false }); // this map record --> private workflowErrors: Record = {}; private workflowEmpty: boolean = false; /** * subcribe the add opertor event, delete operator event, add link event, delete link event * and change operator property event. observe each change and record changes in operatorValidationStream * @param texeraGraph * @param workflowActionService */ constructor( private operatorMetadataService: OperatorMetadataService, private workflowActionService: WorkflowActionService, private dynamicSchemaService: DynamicSchemaService ) { // fetch operator schema list this.operatorMetadataService.getOperatorMetadata().subscribe(metadata => { this.operatorSchemaList = metadata.operators; this.initializeValidation(); }); } public getCurrentWorkflowValidationError(): { errors: Record; } { return this.workflowValidationErrorStream.getValue(); } /** * Gets observable for operatorErrorMap change event * * map: a Map { return this.workflowValidationErrorStream.asObservable(); } /** * Gets the observable for operator validation change event. * Contains a boolean variable and an operator ID: * - status: the new status for the validation of operator * - operatorID: operator being validated */ public getOperatorValidationStream(): Observable<{ operatorID: string; validation: Validation; }> { return this.operatorValidationStream.asObservable(); } public validateOperator(operatorID: string): Validation { if (this.workflowActionService.getTexeraGraph().isOperatorDisabled(operatorID)) { return { isValid: true }; } const jsonSchemaValidation = this.validateJsonSchema(operatorID); const operatorConnectionValidation = this.validateOperatorConnection(operatorID); return ValidationWorkflowService.combineValidation(jsonSchemaValidation, operatorConnectionValidation); } private updateValidationState(operatorID: string, validation: Validation) { if (!validation.isValid) { this.workflowErrors[operatorID] = validation; } else { delete this.workflowErrors[operatorID]; } // emit event to streams after updating workflowErrors to keep subscribers' view of the state consistent this.operatorValidationStream.next({ validation, operatorID }); this.workflowValidationErrorStream.next({ errors: this.workflowErrors, workflowEmpty: this.workflowEmpty }); } private checkIfWorkflowEmpty() { const operators = this.workflowActionService.getTexeraGraph().getAllOperators(); this.workflowEmpty = operators.length === 0; // If there are operators, check if they're all disabled if (!this.workflowEmpty) { this.workflowEmpty = operators.every(operator => this.workflowActionService.getTexeraGraph().isOperatorDisabled(operator.operatorID) ); } } private updateValidationStateOnDelete(operatorID: string) { this.checkIfWorkflowEmpty(); delete this.workflowErrors[operatorID]; this.workflowValidationErrorStream.next({ errors: this.workflowErrors, workflowEmpty: this.workflowEmpty }); } /** * Initialize all the event listener for validation on the workflow editor */ private initializeValidation(): void { // when initialized, first validate any initial operators existing in the editor before the event handlers // have been configured. This will happen when the saved workflow reload on the browser this.workflowActionService .getTexeraGraph() .getAllOperators() .forEach(operator => { this.updateValidationState(operator.operatorID, this.validateOperator(operator.operatorID)); }); // push an validation result after checking if the workflow is empty. this.checkIfWorkflowEmpty(); this.workflowValidationErrorStream.next({ errors: this.workflowErrors, workflowEmpty: this.workflowEmpty }); // Capture operator dynamic schema changed event // dynamic schema changed event is also triggered when an operator is newly added this.dynamicSchemaService .getOperatorDynamicSchemaChangedStream() .subscribe(op => this.updateValidationState(op.operatorID, this.validateOperator(op.operatorID))); // Capture the operator delete event but not validate the deleted operator this.workflowActionService .getTexeraGraph() .getOperatorDeleteStream() .subscribe(operator => this.updateValidationStateOnDelete(operator.deletedOperatorID)); // Capture the link add and delete event and validate the source and target operators of this link merge( this.workflowActionService.getTexeraGraph().getLinkAddStream(), this.workflowActionService .getTexeraGraph() .getLinkDeleteStream() .pipe(map(link => link.deletedLink)) ).subscribe(link => { if (this.workflowActionService.getTexeraGraph().hasOperator(link.source.operatorID)) { this.updateValidationState(link.source.operatorID, this.validateOperator(link.source.operatorID)); } if (this.workflowActionService.getTexeraGraph().hasOperator(link.target.operatorID)) { this.updateValidationState(link.target.operatorID, this.validateOperator(link.target.operatorID)); } }); // capture the port change event and validate the operator of this port this.workflowActionService .getTexeraGraph() .getPortAddedOrDeletedStream() .subscribe(portChange => { this.updateValidationState( portChange.newOperator.operatorID, this.validateOperator(portChange.newOperator.operatorID) ); }); // Capture the operator property change event and validate the current operator being changed this.workflowActionService .getTexeraGraph() .getOperatorPropertyChangeStream() .subscribe(value => this.updateValidationState(value.operator.operatorID, this.validateOperator(value.operator.operatorID)) ); // on enable / disable operator - re-validate the changed operators this.workflowActionService .getTexeraGraph() .getDisabledOperatorsChangedStream() .subscribe(event => { const operatorsToRevalidate = new Set(); // for every changed operator: event.newDisabled.concat(event.newEnabled).forEach(op => { // revalidate itself operatorsToRevalidate.add(op); // revalidate all its input operators const inputs = this.workflowActionService.getTexeraGraph().getInputLinksByOperatorId(op); inputs.forEach(link => operatorsToRevalidate.add(link.source.operatorID)); // revliadate all its output operators const outputs = this.workflowActionService.getTexeraGraph().getOutputLinksByOperatorId(op); outputs.forEach(link => operatorsToRevalidate.add(link.target.operatorID)); }); operatorsToRevalidate.forEach(op => this.updateValidationState(op, this.validateOperator(op))); }); // Add subscription to workflow changes this.workflowActionService .workflowChanged() .pipe(untilDestroyed(this)) .subscribe(() => { this.checkIfWorkflowEmpty(); this.workflowValidationErrorStream.next({ errors: this.workflowErrors, workflowEmpty: this.workflowEmpty, }); }); } /** * This method is used to check whether all required properties of the operator have been completed. * If completed correctly, the operator is valid. */ private validateJsonSchema(operatorID: string): Validation { const operator = this.workflowActionService.getTexeraGraph().getOperator(operatorID); if (operator === undefined) { throw new Error(`operator with ID ${operatorID} doesn't exist`); } // try to fetch dynamic schema first const operatorSchema = this.dynamicSchemaService.getDynamicSchema(operatorID); if (operatorSchema === undefined) { throw new Error("operatorSchema doesn't exist"); } const isValid = this.ajv.validate(operatorSchema.jsonSchema, operator.operatorProperties); if (isValid) { return { isValid: true }; } const errors = this.ajv.errors; const validationError: Record = {}; if (errors) { errors.forEach(error => (validationError[error.keyword] = error.message ? error.message : "")); } return { isValid: false, messages: validationError }; } /** * This method is used to check whether all input ports of the operator have been connected. * if all input ports of the operator are connected, the operator is valid. */ private validateOperatorConnection(operatorID: string): Validation { const operator = this.workflowActionService.getTexeraGraph().getOperator(operatorID); if (operator === undefined) { throw new Error(`operator with ID ${operatorID} doesn't exist`); } const operatorSchema = this.operatorSchemaList.find(schema => schema.operatorType === operator.operatorType); if (operatorSchema === undefined) { throw new Error("operatorSchema doesn't exist"); } const texeraGraph = this.workflowActionService.getTexeraGraph(); // check if input links satisfy the requirement const numInputLinksByPort = new Map(); texeraGraph.getInputLinksByOperatorId(operatorID).forEach(inLink => { if (texeraGraph.isLinkEnabled(inLink.linkID)) { const portID = inLink.target.portID; const num = numInputLinksByPort.get(portID) ?? 0; numInputLinksByPort.set(portID, num + 1); } }); let satisfyInput = true; let inputPortsViolationMessage = ""; for (let i = 0; i < operator.inputPorts.length; i++) { const port = operator.inputPorts[i]; const portNumInputs = numInputLinksByPort.get(port.portID) ?? 0; if (port.disallowMultiInputs) { if (portNumInputs !== 1) { satisfyInput = false; inputPortsViolationMessage += `${port.displayName ?? ""} requires 1 input, has ${portNumInputs}`; } } else { if (portNumInputs < 1) { satisfyInput = false; inputPortsViolationMessage += `${port.displayName ?? ""} requires at least 1 inputs, has ${portNumInputs}`; } } } if (satisfyInput) { return { isValid: true }; } else { const messages: Record = {}; if (!satisfyInput) { messages[ValidationWorkflowService.VALIDATION_OPERATOR_INPUT_MESSAGE] = inputPortsViolationMessage; } return { isValid: false, messages: messages }; } } public static combineValidation(...validations: Validation[]): Validation { let isValid = true; let messages = {}; validations.forEach(validation => { isValid = isValid && validation.isValid; if (!validation.isValid) { messages = { ...messages, ...validation.messages }; } }); if (isValid) { return { isValid }; } else { return { isValid, messages }; } } /** * Gets a filtered version of the TexeraGraph containing only valid operators and their corresponding links. * This method will create a copy of the TexeraGraph and do the validation on top of it. * * @returns A json-schema-wise valid TexeraGraph */ public getValidTexeraGraph(): WorkflowGraphReadonly { const texeraGraph = this.workflowActionService.getTexeraGraph(); const allOperators = texeraGraph.getAllOperators(); const allLinks = texeraGraph.getAllLinks(); // Filter valid operators using validation service const validOperators = allOperators.filter(operator => { const validation = this.validateOperator(operator.operatorID); return validation.isValid; }); // Filter links to only include those connecting valid operators const validOperatorIds = new Set(validOperators.map(op => op.operatorID)); const validLinks = allLinks.filter( link => validOperatorIds.has(link.source.operatorID) && validOperatorIds.has(link.target.operatorID) ); return new WorkflowGraph(validOperators, validLinks, texeraGraph.getAllCommentBoxes()); } } ================================================ FILE: frontend/src/app/workspace/service/virtual-environment/virtual-environment.service.ts ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { Injectable } from "@angular/core"; import { Observable } from "rxjs"; import { HttpClient, HttpParams } from "@angular/common/http"; import { AuthService } from "../../../common/service/user/auth.service"; export interface PackageResponse { system: string[]; user: string[]; } export interface PvePackageResponse { pveName: string; userPackages: string[]; } @Injectable({ providedIn: "root" }) export class WorkflowPveService { constructor(private http: HttpClient) {} getAccessToken(): string | null { const token = AuthService.getAccessToken(); return token && token.trim().length > 0 ? token : null; } private buildBaseParams(): HttpParams { let params = new HttpParams(); const token = this.getAccessToken(); if (token) { params = params.set("access-token", token); } return params; } getSystemPackages(): Observable { const params = this.buildBaseParams(); return this.http.get("/pve/system", { params }); } fetchPVEs(cuid: number): Observable { const params = this.buildBaseParams().set("cuid", cuid.toString()); return this.http.get("/pve/pves", { params }); } deleteEnvironments(cuid: number) { return this.http.delete(`/pve/pves/${cuid}`); } createPveWebSocketUrl(cuid: number, pveName: string, isLocal: boolean, packages: string[] = []): string { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const query = encodeURIComponent(JSON.stringify(packages)); const token = this.getAccessToken(); const tokenParam = token ? `&access-token=${encodeURIComponent(token)}` : ""; return ( `${protocol}//${window.location.host}/wsapi/pve` + `?packages=${query}` + `&cuid=${cuid}` + `&pveName=${encodeURIComponent(pveName)}` + `&isLocal=${isLocal}` + tokenParam ); } } ================================================ FILE: frontend/src/app/workspace/service/workflow-console/workflow-console.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { TestBed } from "@angular/core/testing"; import { WorkflowConsoleService } from "./workflow-console.service"; import { commonTestProviders } from "../../../common/testing/test-utils"; describe("WorkflowConsoleService", () => { let service: WorkflowConsoleService; beforeEach(() => { TestBed.configureTestingModule({ providers: [WorkflowConsoleService, ...commonTestProviders], }); service = TestBed.inject(WorkflowConsoleService); }); it("should be created", () => { expect(service).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/workspace/service/workflow-console/workflow-console.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { WorkflowWebsocketService } from "../workflow-websocket/workflow-websocket.service"; import { ConsoleMessage, ConsoleUpdateEvent } from "../../types/workflow-common.interface"; import { Observable, Subject } from "rxjs"; import { RingBuffer } from "ring-buffer-ts"; import { ExecutionState } from "../../types/execute-workflow.interface"; import { GuiConfigService } from "../../../common/service/gui-config.service"; @Injectable({ providedIn: "root", }) export class WorkflowConsoleService { private consoleMessages: Map> = new Map(); private consoleMessagesUpdateStream = new Subject(); constructor( private workflowWebsocketService: WorkflowWebsocketService, private config: GuiConfigService ) { this.registerAutoClearConsoleMessages(); this.registerPythonConsoleUpdateEventHandler(); } registerPythonConsoleUpdateEventHandler() { this.workflowWebsocketService .subscribeToEvent("ConsoleUpdateEvent") .subscribe((pythonConsoleUpdateEvent: ConsoleUpdateEvent) => { const operatorId = pythonConsoleUpdateEvent.operatorId; const messages = this.consoleMessages.get(operatorId) || new RingBuffer(this.config.env.operatorConsoleMessageBufferSize); messages.add(...pythonConsoleUpdateEvent.messages); this.consoleMessages.set(operatorId, messages); this.consoleMessagesUpdateStream.next(); }); } registerAutoClearConsoleMessages() { this.workflowWebsocketService.subscribeToEvent("WorkflowStateEvent").subscribe(event => { if (event.state === ExecutionState.Initializing) { this.consoleMessages.clear(); } }); } getConsoleMessages(operatorId: string): ReadonlyArray | undefined { return this.consoleMessages.get(operatorId)?.toArray(); } hasConsoleMessages(operatorId: string): boolean { return this.consoleMessages.has(operatorId); } getConsoleMessageUpdateStream(): Observable { return this.consoleMessagesUpdateStream.asObservable(); } } ================================================ FILE: frontend/src/app/workspace/service/workflow-graph/model/coeditor-presence.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { TestBed } from "@angular/core/testing"; import { CoeditorPresenceService } from "./coeditor-presence.service"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { NzDropdownMenuComponent, NzDropDownModule } from "ng-zorro-antd/dropdown"; import { CoeditorUserIconComponent } from "../../../component/menu/coeditor-user-icon/coeditor-user-icon.component"; import { WorkflowActionService } from "./workflow-action.service"; import { HttpClient } from "@angular/common/http"; import { StubUserService } from "../../../../common/service/user/stub-user.service"; import { UserService } from "../../../../common/service/user/user.service"; import { commonTestProviders } from "../../../../common/testing/test-utils"; describe("CoeditorPresenceService", () => { let service: CoeditorPresenceService; let workflowActionService: WorkflowActionService; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule, NzDropDownModule, CoeditorUserIconComponent], providers: [ WorkflowActionService, CoeditorPresenceService, HttpClient, NzDropdownMenuComponent, { provide: UserService, useClass: StubUserService }, ...commonTestProviders, ], }); service = TestBed.inject(CoeditorPresenceService); workflowActionService = TestBed.inject(WorkflowActionService); }); it("should be created", () => { expect(service).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/workspace/service/workflow-graph/model/coeditor-presence.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { WorkflowGraph } from "./workflow-graph"; import * as joint from "jointjs"; import { JointGraphWrapper } from "./joint-graph-wrapper"; import { Coeditor, CoeditorState } from "../../../../common/type/user"; import { WorkflowActionService } from "./workflow-action.service"; import { JointUIService } from "../../joint-ui/joint-ui.service"; import { Observable, Subject } from "rxjs"; import { isEqual } from "lodash"; /** * "Coeditor" means "collaboratively editing user". * CoeditorPresenceService handles user-presence updates from other editors in the same shared-editing room * and shows them on the UI. It also keeps some states in itself for some necessary UI update information, * like which co-editors are currently highlighting a particular operator. */ @Injectable({ providedIn: "root", }) export class CoeditorPresenceService { private readonly coeditorOpenedCodeEditorSubject = new Subject<{ operatorId: string }>(); private readonly coeditorClosedCodeEditorSubject = new Subject<{ operatorId: string }>(); public shadowingModeEnabled = false; public shadowingCoeditor?: Coeditor; public coeditors: Coeditor[] = []; private jointGraph: joint.dia.Graph; private texeraGraph: WorkflowGraph; private jointGraphWrapper: JointGraphWrapper; private coeditorCurrentlyEditing = new Map(); private coeditorOperatorHighlights = new Map(); private coeditorOperatorPropertyChanged = new Map(); private coeditorEditingCode = new Map(); private coeditorStates = new Map(); private currentlyEditingTimers = new Map>(); constructor(private workflowActionService: WorkflowActionService) { this.texeraGraph = workflowActionService.getTexeraGraph() as WorkflowGraph; this.jointGraph = workflowActionService.getJointGraph(); this.jointGraphWrapper = workflowActionService.getJointGraphWrapper(); this.observeUserState(); this.texeraGraph.newYDocLoadedSubject.subscribe(_ => { this.observeUserState(); }); } /** * Start shawoding an co-editor. * @param coeditor */ public shadowCoeditor(coeditor: Coeditor): void { this.shadowingModeEnabled = true; this.shadowingCoeditor = coeditor; if (coeditor.clientId) { const currentlyEditing = this.coeditorCurrentlyEditing.get(coeditor.clientId); if (currentlyEditing) { this.workflowActionService.highlightOperators(false, currentlyEditing); const currentlyEditingCode = this.coeditorEditingCode.get(coeditor.clientId); if (currentlyEditingCode) this.coeditorOpenedCodeEditorSubject.next({ operatorId: currentlyEditing }); } } } /** * End shadowing. */ public stopShadowing() { this.shadowingModeEnabled = false; } public getCoeditorOpenedCodeEditorSubject(): Observable<{ operatorId: string }> { return this.coeditorOpenedCodeEditorSubject.asObservable(); } public getCoeditorClosedCodeEditorSubject(): Observable<{ operatorId: string }> { return this.coeditorClosedCodeEditorSubject.asObservable(); } /** * Listens to changes of co-editors' presence infos and lets {@link CoeditorPresenceService} handle them. */ private observeUserState(): void { // destroy previous user states if any for (const coeditor of this.coeditors) { if (coeditor.clientId) this.removeCoeditor(coeditor.clientId); } // first time logic const currentStates = this.getCoeditorStatesArray().filter( userState => userState.user && userState.user.clientId && userState.user.clientId !== this.getLocalClientId() ); for (const state of currentStates) { this.addCoeditor(state); } this.texeraGraph.sharedModel.awareness.on( "change", (change: { added: number[]; updated: number[]; removed: number[] }) => { for (const clientId of change.added) { const coeditorState = this.getCoeditorStatesMap().get(clientId); if (coeditorState && coeditorState.user.clientId !== this.getLocalClientId()) this.addCoeditor(coeditorState); } for (const clientId of change.removed) { if (!this.getCoeditorStatesMap().has(clientId)) this.removeCoeditor(clientId.toString()); } for (const clientId of change.updated) { const coeditorState = this.getCoeditorStatesMap().get(clientId); if (coeditorState && clientId.toString() !== this.getLocalClientId()) { if (!this.hasCoeditor(clientId.toString())) { this.addCoeditor(coeditorState); } else { this.updateCoeditorState(clientId.toString(), coeditorState); } } } } ); } /** * Returns whether this co-editor is already recorded here. * @param clientId */ private hasCoeditor(clientId?: string): boolean { return this.coeditors.find(v => v.clientId === clientId) !== undefined; } /** * Adds a new co-editor and initialize UI-updates for this editor. * @param coeditorState */ private addCoeditor(coeditorState: CoeditorState): void { const coeditor = coeditorState.user; if (!this.hasCoeditor(coeditor.clientId) && coeditor.clientId) { this.coeditors.push(coeditor); this.coeditorStates.set(coeditor.clientId, coeditorState); this.updateCoeditorState(coeditor.clientId, coeditorState); } } /** * Removes a co-editor and clean up states recorded in this service. * @param clientId */ private removeCoeditor(clientId: string): void { for (let i = 0; i < this.coeditors.length; i++) { const coeditor = this.coeditors[i]; if (coeditor.clientId === clientId) { this.updateCoeditorState(clientId, { user: coeditor, userCursor: { x: 0, y: 0 }, currentlyEditing: undefined, isActive: false, highlighted: undefined, changed: undefined, }); this.coeditors.splice(i); } } this.coeditorStates.delete(clientId); if (this.shadowingModeEnabled && this.shadowingCoeditor?.clientId === clientId) { this.stopShadowing(); } } /** * Given a new {@link CoeditorState} with specified clientId, this method updates this co-editor's * presence information and corresponding UIs. This is an incremental update, i.e., it will first check existing * states and only update what is new. The update is handled separately in each sub-method called. * @param clientId * @param coeditorState */ private updateCoeditorState(clientId: string, coeditorState: CoeditorState): void { this.updateCoeditorCursor(coeditorState); this.updateCoeditorHighlightedOperators(clientId, coeditorState); this.updateCoeditorCurrentlyEditing(clientId, coeditorState); this.updateCoeditorChangedProperty(clientId, coeditorState); this.updateCoeditorOpenAndCloseCode(clientId, coeditorState); } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Below are methods to update different co-editor states. // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// private updateCoeditorCursor(coeditorState: CoeditorState) { // Update pointers const existingPointer: joint.dia.Cell | undefined = this.jointGraph.getCell( JointUIService.getJointUserPointerName(coeditorState.user) ); const userColor = coeditorState.user.color; if (existingPointer) { if (coeditorState.isActive) { if (coeditorState.userCursor !== existingPointer.position()) { existingPointer.remove(); if (userColor) { const newPoint = JointUIService.getJointUserPointerCell( coeditorState.user, coeditorState.userCursor, userColor ); this.jointGraph.addCell(newPoint); this.jointGraphWrapper.getMainJointPaper().findViewByModel(newPoint.id).setInteractivity(false); } } } else existingPointer.remove(); } else { if (coeditorState.isActive && userColor) { // create new user point (directly updating the point would cause unknown errors) const newPoint = JointUIService.getJointUserPointerCell( coeditorState.user, coeditorState.userCursor, userColor ); this.jointGraph.addCell(newPoint); this.jointGraphWrapper.getMainJointPaper().findViewByModel(newPoint.id).setInteractivity(false); } } } private updateCoeditorHighlightedOperators(clientId: string, coeditorState: CoeditorState) { // Update operator highlights const previousHighlighted = this.coeditorOperatorHighlights.get(clientId); const currentHighlighted = coeditorState.highlighted; if (!isEqual(previousHighlighted, currentHighlighted)) { if (previousHighlighted) { for (const operatorId of previousHighlighted) { if (!currentHighlighted || !currentHighlighted.includes(operatorId)) { this.jointGraphWrapper.deleteCoeditorOperatorHighlight(coeditorState.user, operatorId); } } } if (currentHighlighted) { for (const operatorId of currentHighlighted) { if (!previousHighlighted || !previousHighlighted.includes(operatorId)) { this.jointGraphWrapper.addCoeditorOperatorHighlight(coeditorState.user, operatorId); } } this.coeditorOperatorHighlights.set(clientId, currentHighlighted); } else { this.coeditorOperatorHighlights.delete(clientId); } } } private updateCoeditorCurrentlyEditing(clientId: string, coeditorState: CoeditorState) { // Update currently editing status const previousEditing = this.coeditorCurrentlyEditing.get(clientId); const previousIntervalId = this.currentlyEditingTimers.get(clientId); const currentEditing = coeditorState.currentlyEditing; if (previousEditing !== currentEditing) { if ( previousEditing && previousIntervalId && this.workflowActionService.getTexeraGraph().hasOperator(previousEditing) ) { this.jointGraphWrapper.removeCurrentEditing(coeditorState.user, previousEditing, previousIntervalId); this.coeditorCurrentlyEditing.delete(clientId); this.currentlyEditingTimers.delete(clientId); if (this.shadowingModeEnabled && this.shadowingCoeditor?.clientId === coeditorState.user.clientId) { this.workflowActionService.unhighlightOperators(previousEditing); } } if (currentEditing && this.workflowActionService.getTexeraGraph().hasOperator(currentEditing)) { const intervalId = this.jointGraphWrapper.setCurrentEditing(coeditorState.user, currentEditing); this.coeditorCurrentlyEditing.set(clientId, currentEditing); this.currentlyEditingTimers.set(clientId, intervalId); if (this.shadowingModeEnabled && this.shadowingCoeditor?.clientId === coeditorState.user.clientId) { this.workflowActionService.highlightOperators(false, currentEditing); } } } } private updateCoeditorChangedProperty(clientId: string, coeditorState: CoeditorState) { // Update property changed status const previousChanged = this.coeditorOperatorPropertyChanged.get(clientId); const currentChanged = coeditorState.changed; if (previousChanged !== currentChanged) { if (currentChanged) { this.coeditorOperatorPropertyChanged.set(clientId, currentChanged); // Set for 3 seconds this.jointGraphWrapper.setPropertyChanged(coeditorState.user, currentChanged); setTimeout(() => { this.coeditorOperatorPropertyChanged.delete(clientId); this.jointGraphWrapper.removePropertyChanged(coeditorState.user, currentChanged); }, 2000); } } } private updateCoeditorOpenAndCloseCode(clientId: string, coeditorState: CoeditorState) { const previousEditingCode = this.coeditorEditingCode.get(clientId); const currentEditingCode = coeditorState.editingCode; if (previousEditingCode !== currentEditingCode) { if (currentEditingCode) { this.coeditorEditingCode.set(clientId, currentEditingCode); if ( this.shadowingModeEnabled && this.shadowingCoeditor?.clientId === clientId.toString() && coeditorState.currentlyEditing ) { this.coeditorOpenedCodeEditorSubject.next({ operatorId: coeditorState.currentlyEditing }); } } else { if (previousEditingCode) { this.coeditorEditingCode.delete(clientId); if ( this.shadowingModeEnabled && this.shadowingCoeditor?.clientId === clientId.toString() && coeditorState.currentlyEditing ) { this.coeditorClosedCodeEditorSubject.next({ operatorId: coeditorState.currentlyEditing }); } } } } } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Below are internal utility methods // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// private getCoeditorStatesArray(): CoeditorState[] { return Array.from(this.texeraGraph.sharedModel.awareness.getStates().values() as IterableIterator); } private getCoeditorStatesMap(): Map { return this.texeraGraph.sharedModel.awareness.getStates() as Map; } private getLocalClientId() { return this.texeraGraph.sharedModel.clientId; } } ================================================ FILE: frontend/src/app/workspace/service/workflow-graph/model/joint-graph-wrapper.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { WorkflowActionService } from "./workflow-action.service"; import { UndoRedoService } from "../../undo-redo/undo-redo.service"; import { OperatorMetadataService } from "../../operator-metadata/operator-metadata.service"; import { JointUIService } from "../../joint-ui/joint-ui.service"; import { JointGraphWrapper } from "./joint-graph-wrapper"; import { TestBed } from "@angular/core/testing"; import { marbles } from "rxjs-marbles"; import { mockPoint, mockResultPredicate, mockScanPredicate, mockScanResultLink, mockScanSentimentLink, mockSentimentPredicate, mockSentimentResultLink, } from "./mock-workflow-data"; import * as joint from "jointjs"; import { StubOperatorMetadataService } from "../../operator-metadata/stub-operator-metadata.service"; import { WorkflowUtilService } from "../util/workflow-util.service"; import { map, share, tap } from "rxjs/operators"; import { commonTestProviders } from "../../../../common/testing/test-utils"; import { GuiConfigService } from "../../../../common/service/gui-config.service"; import { MockGuiConfigService } from "../../../../common/service/gui-config.service.mock"; describe("JointGraphWrapperService", () => { let jointGraph: joint.dia.Graph; let jointGraphWrapper: JointGraphWrapper; let jointUIService: JointUIService; beforeEach(() => { TestBed.configureTestingModule({ providers: [ JointUIService, WorkflowActionService, WorkflowUtilService, UndoRedoService, { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, ...commonTestProviders, ], }); jointGraph = new joint.dia.Graph(); jointGraphWrapper = new JointGraphWrapper(jointGraph); jointUIService = TestBed.inject(JointUIService); }); it( "should emit operator delete event correctly when operator is deleted by JointJS", marbles(m => { jointGraph.addCell(jointUIService.getJointOperatorElement(mockScanPredicate, mockPoint)); m.hot("-e-") .pipe(tap(() => jointGraph.getCell(mockScanPredicate.operatorID).remove())) .subscribe(); const jointOperatorDeleteStream = jointGraphWrapper.getJointElementCellDeleteStream().pipe(map(() => "e")); const expectedStream = m.hot("-e-"); m.expect(jointOperatorDeleteStream).toBeObservable(expectedStream); }) ); it( "should emit link add event correctly when a link is connected by JointJS", marbles(m => { jointGraph.addCell(jointUIService.getJointOperatorElement(mockScanPredicate, mockPoint)); jointGraph.addCell(jointUIService.getJointOperatorElement(mockResultPredicate, mockPoint)); const mockScanResultLinkCell = JointUIService.getJointLinkCell(mockScanResultLink); m.hot("-e-") .pipe(tap(() => jointGraph.addCell(mockScanResultLinkCell))) .subscribe(); const jointLinkAddStream = jointGraphWrapper.getJointLinkCellAddStream().pipe(map(() => "e")); const expectedStream = m.hot("-e-"); m.expect(jointLinkAddStream).toBeObservable(expectedStream); }) ); it( "should emit link delete event correctly when a link is deleted by JointJS", marbles(m => { jointGraph.addCell(jointUIService.getJointOperatorElement(mockScanPredicate, mockPoint)); jointGraph.addCell(jointUIService.getJointOperatorElement(mockResultPredicate, mockPoint)); const mockScanResultLinkCell = JointUIService.getJointLinkCell(mockScanResultLink); jointGraph.addCell(mockScanResultLinkCell); m.hot("---e-") .pipe(tap(() => jointGraph.getCell(mockScanResultLink.linkID).remove())) .subscribe(); const jointLinkDeleteStream = jointGraphWrapper.getJointLinkCellDeleteStream().pipe(map(() => "e")); const expectedStream = m.hot("---e-"); m.expect(jointLinkDeleteStream).toBeObservable(expectedStream); }) ); /** * When the user deletes an operator in the UI, jointJS will delete the connected links automatically. * * This test verifies that when an operator is deleted, causing the one connected link to be deleted, * the JointJS event Observable streams are emitted correctly. * It should emit one operator delete event and one link delete event at the same time. */ it( `should emit operator delete event and link delete event correctly when an operator along with one connected link are deleted by JointJS`, marbles(m => { jointGraph.addCell(jointUIService.getJointOperatorElement(mockScanPredicate, mockPoint)); jointGraph.addCell(jointUIService.getJointOperatorElement(mockResultPredicate, mockPoint)); const mockScanResultLinkCell = JointUIService.getJointLinkCell(mockScanResultLink); jointGraph.addCell(mockScanResultLinkCell); m.hot("-e-") .pipe(tap(() => jointGraph.getCell(mockScanPredicate.operatorID).remove())) .subscribe(); const jointOperatorDeleteStream = jointGraphWrapper.getJointElementCellDeleteStream().pipe(map(() => "e")); const jointLinkDeleteStream = jointGraphWrapper.getJointLinkCellDeleteStream().pipe(map(() => "e")); const expectedStream = "-e-"; m.expect(jointOperatorDeleteStream).toBeObservable(expectedStream); m.expect(jointLinkDeleteStream).toBeObservable(expectedStream); }) ); /** * * This test verifies that when an operator is deleted, causing *multiple* connected links to be deleted, * the JointJS event Observalbe streams are emitted correctly. * It should emit one operator delete event and one link delete event at the same time. */ it( `should emit operator delete event and link delete event correctly when an operator along with multiple links are deleted by JointJS`, marbles(m => { jointGraph.addCell(jointUIService.getJointOperatorElement(mockScanPredicate, mockPoint)); jointGraph.addCell(jointUIService.getJointOperatorElement(mockSentimentPredicate, mockPoint)); jointGraph.addCell(jointUIService.getJointOperatorElement(mockResultPredicate, mockPoint)); const mockScanSentimentLinkCell = JointUIService.getJointLinkCell(mockScanSentimentLink); const mockSentimentResultLinkCell = JointUIService.getJointLinkCell(mockSentimentResultLink); jointGraph.addCell(mockScanSentimentLinkCell); jointGraph.addCell(mockSentimentResultLinkCell); m.hot("-e--") .pipe(tap(() => jointGraph.getCell(mockSentimentPredicate.operatorID).remove())) .subscribe(); const jointOperatorDeleteStream = jointGraphWrapper.getJointElementCellDeleteStream().pipe(map(() => "e")); const jointLinkDeleteStream = jointGraphWrapper.getJointLinkCellDeleteStream().pipe(map(() => "e")); const expectedStream = "-e--"; const expectedMultiStream = "-(ee)--"; m.expect(jointOperatorDeleteStream).toBeObservable(expectedStream); m.expect(jointLinkDeleteStream).toBeObservable(expectedMultiStream); }) ); it( "should emit a highlight event correctly when an operator is highlighted", marbles(m => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const localJointGraphWrapper = workflowActionService.getJointGraphWrapper(); // add one operator, it should be automatically highlighted workflowActionService.addOperator(mockScanPredicate, mockPoint); expect(workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs()).toEqual([ mockScanPredicate.operatorID, ]); // unhighlight the current operator workflowActionService.getJointGraphWrapper().unhighlightOperators(mockScanPredicate.operatorID); expect(workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs()).toEqual([]); // prepare marble operation for highlighting an operator const highlightActionMarbleEvent = m.hot("-a-|", { a: mockScanPredicate.operatorID }).pipe(share()); // highlight that operator at events highlightActionMarbleEvent.subscribe(value => localJointGraphWrapper.highlightOperators(value)); // prepare expected output highlight event stream const expectedHighlightEventStream = m.hot("-a-", { a: [mockScanPredicate.operatorID], }); // expect the output event stream is correct m.expect(localJointGraphWrapper.getJointOperatorHighlightStream()).toBeObservable(expectedHighlightEventStream); // expect the current highlighted operator is correct highlightActionMarbleEvent.subscribe({ complete: () => { expect(localJointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([mockScanPredicate.operatorID]); }, }); }) ); it( "should emit a highlight event correctly when multiple operators are highlighted", marbles(m => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const localJointGraphWrapper = workflowActionService.getJointGraphWrapper(); // add two operators, they should be automatically highlighted workflowActionService.addOperatorsAndLinks( [ { op: mockScanPredicate, pos: mockPoint }, { op: mockResultPredicate, pos: mockPoint }, ], [] ); expect(workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs()).toEqual([ mockScanPredicate.operatorID, mockResultPredicate.operatorID, ]); // unhighlight current operators workflowActionService .getJointGraphWrapper() .unhighlightOperators(...mockScanPredicate.operatorID, mockResultPredicate.operatorID); expect(workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs()).toEqual([]); // prepare marble operation for highlighting two operators const highlightActionMarbleEvent = m .hot("-a-|", { a: [mockScanPredicate.operatorID, mockResultPredicate.operatorID], }) .pipe(share()); // highlight those operators at events highlightActionMarbleEvent.subscribe(value => localJointGraphWrapper.highlightOperators(...value)); // prepare expected output highlight event stream const expectedHighlightEventStream = m.hot("-a-", { a: [mockScanPredicate.operatorID, mockResultPredicate.operatorID], }); // expect the output event stream is correct m.expect(localJointGraphWrapper.getJointOperatorHighlightStream()).toBeObservable(expectedHighlightEventStream); // expect the current highlighted operators are correct highlightActionMarbleEvent.subscribe({ complete: () => { expect(localJointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([ mockScanPredicate.operatorID, mockResultPredicate.operatorID, ]); }, }); }) ); it( "should emit an unhighlight event correctly when an operator is unhighlighted", marbles(m => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const localJointGraphWrapper = workflowActionService.getJointGraphWrapper(); // add and highlight an operator workflowActionService.addOperator(mockScanPredicate, mockPoint); workflowActionService.getJointGraphWrapper().highlightOperators(mockScanPredicate.operatorID); // prepare marble operation for unhighlighting an operator const unhighlightActionMarbleEvent = m.hot("-a-|").pipe(share()); // unhighlight that operator at events unhighlightActionMarbleEvent.subscribe(() => localJointGraphWrapper.unhighlightOperators(mockScanPredicate.operatorID) ); // prepare expected output unhighlight event stream const expectedUnhighlightEventStream = m.hot("-a-", { a: [mockScanPredicate.operatorID], }); // expect the output event stream is correct m.expect(localJointGraphWrapper.getJointOperatorUnhighlightStream()).toBeObservable( expectedUnhighlightEventStream ); // expect no operator is currently highlighted unhighlightActionMarbleEvent.subscribe({ complete: () => { expect(localJointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([]); }, }); }) ); it( "should emit an unhighlight event correctly when multiple operators are unhighlighted", marbles(m => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const localJointGraphWrapper = workflowActionService.getJointGraphWrapper(); // add and highlight two operators workflowActionService.addOperatorsAndLinks( [ { op: mockScanPredicate, pos: mockPoint }, { op: mockResultPredicate, pos: mockPoint }, ], [] ); workflowActionService .getJointGraphWrapper() .highlightOperators(...mockScanPredicate.operatorID, mockResultPredicate.operatorID); // prepare marble operation for unhighlighting two operators const unhighlightActionMarbleEvent = m.hot("-a-|").pipe(share()); // unhighlight those operators at events unhighlightActionMarbleEvent.subscribe(() => localJointGraphWrapper.unhighlightOperators(...mockScanPredicate.operatorID, mockResultPredicate.operatorID) ); // prepare expected output unhighlight event stream const expectedUnhighlightEventStream = m.hot("-a-", { a: [mockScanPredicate.operatorID, mockResultPredicate.operatorID], }); // expect the output event stream is correct m.expect(localJointGraphWrapper.getJointOperatorUnhighlightStream()).toBeObservable( expectedUnhighlightEventStream ); // expect no operator is currently highlighted unhighlightActionMarbleEvent.subscribe({ complete: () => { expect(localJointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([]); }, }); }) ); it( "should unhighlight previous highlighted operator if a new operator is highlighted", marbles(m => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const localJointGraphWrapper = workflowActionService.getJointGraphWrapper(); workflowActionService.addOperator(mockScanPredicate, mockPoint); workflowActionService.addOperator(mockResultPredicate, mockPoint); // unhighlight the last operator in case of automatic highlight workflowActionService.getJointGraphWrapper().unhighlightOperators(mockResultPredicate.operatorID); // prepare marble operation for highlighting one operator, then highlight another const highlightActionMarbleEvent = m .hot("-a-b-|", { a: mockScanPredicate.operatorID, b: mockResultPredicate.operatorID, }) .pipe(share()); // highlight that operator at events highlightActionMarbleEvent.subscribe(value => localJointGraphWrapper.highlightOperators(value)); // prepare expected output highlight event stream const expectedHighlightEventStream = m.hot("-a-b-", { a: [mockScanPredicate.operatorID], b: [mockResultPredicate.operatorID], }); // expect the output event stream is correct m.expect(localJointGraphWrapper.getJointOperatorHighlightStream()).toBeObservable(expectedHighlightEventStream); // expect the current highlighted operator is correct highlightActionMarbleEvent.subscribe({ complete: () => { expect(localJointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([mockResultPredicate.operatorID]); }, }); }) ); it( "should ignore the action if trying to highlight the same currently highlighted operator", marbles(m => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const localJointGraphWrapper = workflowActionService.getJointGraphWrapper(); // add an operator, it should be automatically highlighted workflowActionService.addOperator(mockScanPredicate, mockPoint); // unhighlight it workflowActionService.getJointGraphWrapper().unhighlightOperators(mockScanPredicate.operatorID); // prepare marble operation for highlighting the same operator twice const highlightActionMarbleEvent = m .hot("-a-b-|", { a: mockScanPredicate.operatorID, b: mockScanPredicate.operatorID, }) .pipe(share()); // highlight that operator at events highlightActionMarbleEvent.subscribe(value => localJointGraphWrapper.highlightOperators(value)); // prepare expected output highlight event stream: the second highlight is ignored const expectedHighlightEventStream = m.hot("-a---", { a: [mockScanPredicate.operatorID], }); // expect the output event stream is correct m.expect(localJointGraphWrapper.getJointOperatorHighlightStream()).toBeObservable(expectedHighlightEventStream); }) ); it( "should unhighlight the currently highlighted operator if it is deleted", marbles(m => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const localJointGraphWrapper = workflowActionService.getJointGraphWrapper(); // add and highlight the operator workflowActionService.addOperator(mockScanPredicate, mockPoint); localJointGraphWrapper.highlightOperators(mockScanPredicate.operatorID); expect(localJointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([mockScanPredicate.operatorID]); // prepare the delete operator action marble test const deleteOperatorActionMarble = m.hot("-a-").pipe(share()); deleteOperatorActionMarble.subscribe(() => workflowActionService.deleteOperator(mockScanPredicate.operatorID)); // expect that the unhighlight event stream is triggered const expectedEventStream = m.hot("-a-", { a: [mockScanPredicate.operatorID], }); m.expect(localJointGraphWrapper.getJointOperatorUnhighlightStream()).toBeObservable(expectedEventStream); // expect that the current highlighted operator is undefined deleteOperatorActionMarble.subscribe({ complete: () => expect(localJointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([]), }); }) ); it("should get operator position successfully if the operator exists in the paper", () => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const localJointGraphWrapper = workflowActionService.getJointGraphWrapper(); workflowActionService.addOperator(mockScanPredicate, mockPoint); expect(localJointGraphWrapper.getElementPosition(mockScanPredicate.operatorID)).toEqual(mockPoint); }); it("should throw an error if operator does not exist in the paper when calling 'getElementPosition()'", () => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const localJointGraphWrapper = workflowActionService.getJointGraphWrapper(); expect(function () { localJointGraphWrapper.getElementPosition(mockScanPredicate.operatorID); }).toThrowError(`element with ID ${mockScanPredicate.operatorID} doesn't exist`); }); it("should throw an error if the id we are using is linkID when calling 'getElementPosition()'", () => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const localJointGraphWrapper = workflowActionService.getJointGraphWrapper(); workflowActionService.addOperator(mockScanPredicate, mockPoint); workflowActionService.addOperator(mockResultPredicate, mockPoint); workflowActionService.addLink(mockScanResultLink); expect(function () { localJointGraphWrapper.getElementPosition(mockScanResultLink.linkID); }).toThrowError(`${mockScanResultLink.linkID} is not an element`); }); it("should repositions the operator successfully if the operator exists in the paper", () => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const localJointGraphWrapper = workflowActionService.getJointGraphWrapper(); workflowActionService.addOperator(mockScanPredicate, mockPoint); // changes the operator's position localJointGraphWrapper.setElementPosition(mockScanPredicate.operatorID, 10, 10); const expectedPosition = { x: mockPoint.x + 10, y: mockPoint.y + 10 }; expect(localJointGraphWrapper.getElementPosition(mockScanPredicate.operatorID)).toEqual(expectedPosition); }); it("should successfully set a new zoom property", () => { const mockNewZoomProperty = 0.5; let currentZoomRatio = jointGraphWrapper.getZoomRatio(); expect(currentZoomRatio).toEqual(1); jointGraphWrapper.setZoomProperty(mockNewZoomProperty); currentZoomRatio = jointGraphWrapper.getZoomRatio(); expect(currentZoomRatio).toEqual(mockNewZoomProperty); }); it( "should triggle getWorkflowEditorZoomStream when new zoom ratio is set", marbles(m => { const mockNewZoomProperty = 0.5; m.hot("-e-") .pipe(tap(event => jointGraphWrapper.setZoomProperty(mockNewZoomProperty))) .subscribe(); const zoomStream = jointGraphWrapper.getWorkflowEditorZoomStream().pipe(map(value => "e")); const expectedStream = "-e-"; m.expect(zoomStream).toBeObservable(expectedStream); }) ); it( "should trigger getRestorePaperOffsetStream when resumeDefaultZoomAndOffset is called", marbles(m => { m.hot("-e-") .pipe(tap(() => jointGraphWrapper.restoreDefaultZoomAndOffset())) .subscribe(); const restoreStream = jointGraphWrapper.getRestorePaperOffsetStream().pipe(map(value => "e")); const expectedStream = "-e-"; m.expect(restoreStream).toBeObservable(expectedStream); }) ); it("should move all highlighted operators together when any one of them is moved", () => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const localJointGraphWrapper = workflowActionService.getJointGraphWrapper(); // add and highlight two operators workflowActionService.addOperatorsAndLinks( [ { op: mockScanPredicate, pos: mockPoint }, { op: mockResultPredicate, pos: mockPoint }, ], [] ); localJointGraphWrapper.highlightOperators(...mockScanPredicate.operatorID, mockResultPredicate.operatorID); // change one operator's position localJointGraphWrapper.setElementPosition(mockScanPredicate.operatorID, 10, 10); const expectedPosition = { x: mockPoint.x + 10, y: mockPoint.y + 10 }; // expect both operators to be in the new position expect(localJointGraphWrapper.getElementPosition(mockScanPredicate.operatorID)).toEqual(expectedPosition); expect(localJointGraphWrapper.getElementPosition(mockResultPredicate.operatorID)).toEqual(expectedPosition); }); describe("when linkBreakpoint is enabled", () => { let mockConfigService: MockGuiConfigService; beforeEach(() => { // Get the mock service and enable linkBreakpoint for each test in this describe block mockConfigService = TestBed.inject(GuiConfigService) as unknown as MockGuiConfigService; mockConfigService.setConfig({ linkBreakpointEnabled: true }); }); afterEach(() => { // Reset to default after each test if (mockConfigService) { mockConfigService.setConfig({ linkBreakpointEnabled: false }); } }); it( "should emit link highlight event correctly when a link is selected", marbles(m => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const localJointGraphWrapper = workflowActionService.getJointGraphWrapper(); workflowActionService.addOperator(mockScanPredicate, mockPoint); workflowActionService.addOperator(mockResultPredicate, mockPoint); workflowActionService.addLink(mockScanResultLink); // prepare marble operation for highlighting one link, then highlight an operator const highlightActionMarbleEvent = m.hot("-a-|", { a: mockScanResultLink.linkID }).pipe(share()); // highlight at events highlightActionMarbleEvent.subscribe(value => { localJointGraphWrapper.highlightLink(value); }); // prepare expected output highlight event stream const expectedLinkHighlightEventStream = m.hot("-a-", { a: [mockScanResultLink.linkID], }); m.expect(localJointGraphWrapper.getLinkHighlightStream()).toBeObservable(expectedLinkHighlightEventStream); // expect the current highlighted link to be correct highlightActionMarbleEvent.subscribe({ complete: () => { expect(localJointGraphWrapper.getCurrentHighlightedLinkIDs()).toEqual([mockScanResultLink.linkID]); }, }); }) ); it( "should emit an unhighlight event correctly when an link is unhighlighted", marbles(m => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const localJointGraphWrapper = workflowActionService.getJointGraphWrapper(); // add one operator workflowActionService.addOperator(mockScanPredicate, mockPoint); workflowActionService.addOperator(mockResultPredicate, mockPoint); workflowActionService.addLink(mockScanResultLink); // highlight the operator localJointGraphWrapper.highlightLink(mockScanResultLink.linkID); // prepare marble operation for unhighlighting an operator const unhighlightActionMarbleEvent = m.hot("-a-|").pipe(share()); // unhighlight that operator at events unhighlightActionMarbleEvent.subscribe(() => localJointGraphWrapper.unhighlightLink(mockScanResultLink.linkID)); // prepare expected output highlight event stream const expectedUnhighlightEventStream = m.hot("-a-", { a: [mockScanResultLink.linkID], }); // expect the output event stream is correct m.expect(localJointGraphWrapper.getLinkUnhighlightStream()).toBeObservable(expectedUnhighlightEventStream); // expect the current highlighted operator is correct unhighlightActionMarbleEvent.subscribe({ complete: () => { expect(localJointGraphWrapper.getCurrentHighlightedLinkIDs()).toEqual([]); }, }); }) ); it( "should emit an unhighlight event correctly when an highlighted link is deleted", marbles(m => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const localJointGraphWrapper = workflowActionService.getJointGraphWrapper(); // add one operator workflowActionService.addOperator(mockScanPredicate, mockPoint); workflowActionService.addOperator(mockResultPredicate, mockPoint); workflowActionService.addLink(mockScanResultLink); // highlight the operator localJointGraphWrapper.highlightLink(mockScanResultLink.linkID); // prepare marble operation for unhighlighting an operator const deleteActionMarbleEvent = m.hot("-a-|").pipe(share()); // unhighlight that operator at events deleteActionMarbleEvent.subscribe(() => workflowActionService.deleteLinkWithID(mockScanResultLink.linkID)); // prepare expected output highlight event stream const expectedUnhighlightEventStream = m.hot("-a-", { a: [mockScanResultLink.linkID], }); // expect the output event stream is correct m.expect(localJointGraphWrapper.getLinkUnhighlightStream()).toBeObservable(expectedUnhighlightEventStream); // expect the current highlighted operator is correct deleteActionMarbleEvent.subscribe({ complete: () => { expect(localJointGraphWrapper.getCurrentHighlightedLinkIDs()).toEqual([]); }, }); }) ); it( "should unhighlight previous highlighted link if another link is selected/highlighted", marbles(m => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const localJointGraphWrapper = workflowActionService.getJointGraphWrapper(); workflowActionService.addOperator(mockScanPredicate, mockPoint); workflowActionService.addOperator(mockSentimentPredicate, mockPoint); workflowActionService.addOperator(mockResultPredicate, mockPoint); workflowActionService.addLink(mockScanSentimentLink); workflowActionService.addLink(mockSentimentResultLink); // prepare marble operation for highlighting one link, then highlight an operator const highlightActionMarbleEvent = m .hot("-a-b-|", { a: mockScanSentimentLink.linkID, b: mockSentimentResultLink.linkID, }) .pipe(share()); // highlight at events highlightActionMarbleEvent.subscribe(value => { localJointGraphWrapper.highlightLink(value); }); // prepare expected output highlight event stream const expectedLinkHighlightEventStream = m.hot("-a-b-", { a: [mockScanSentimentLink.linkID], b: [mockSentimentResultLink.linkID], }); m.expect(localJointGraphWrapper.getLinkHighlightStream()).toBeObservable(expectedLinkHighlightEventStream); // prepare expected output unhighlight event stream const expectedLinUnhighlightEventStream = m.hot("---a-", { a: [mockScanSentimentLink.linkID], }); m.expect(localJointGraphWrapper.getLinkUnhighlightStream()).toBeObservable(expectedLinUnhighlightEventStream); // expect the current highlighted link to be correct highlightActionMarbleEvent.subscribe({ complete: () => { expect(localJointGraphWrapper.getCurrentHighlightedLinkIDs()).toEqual([mockSentimentResultLink.linkID]); }, }); }) ); it( "should unhighlight previous highlighted links if an operator is highlighted", marbles(m => { const workflowActionService: WorkflowActionService = TestBed.inject(WorkflowActionService); const localJointGraphWrapper = workflowActionService.getJointGraphWrapper(); workflowActionService.addOperator(mockScanPredicate, mockPoint); workflowActionService.addOperator(mockResultPredicate, mockPoint); workflowActionService.addLink(mockScanResultLink); // prepare marble operation for highlighting one link, then highlight an operator const highlightActionMarbleEvent = m .hot("-a-b-|", { a: mockScanResultLink.linkID, b: mockResultPredicate.operatorID, }) .pipe(share()); // highlight at events highlightActionMarbleEvent.subscribe(value => { if (value === mockResultPredicate.operatorID) { localJointGraphWrapper.highlightOperators(value); } else { localJointGraphWrapper.highlightLink(value); } }); // prepare expected output highlight event stream const expectedLinkHighlightEventStream = m.hot("-a---", { a: [mockScanResultLink.linkID], }); const expectedOperatorHighlightEventStream = m.hot("---b-", { b: [mockResultPredicate.operatorID], }); // prepare expected output highlight event stream const expectedLinkUnhighlightEventStream = m.hot("---c-", { c: [mockScanResultLink.linkID], }); // expect the output event stream is correct m.expect(localJointGraphWrapper.getLinkHighlightStream()).toBeObservable(expectedLinkHighlightEventStream); m.expect(localJointGraphWrapper.getJointOperatorHighlightStream()).toBeObservable( expectedOperatorHighlightEventStream ); m.expect(localJointGraphWrapper.getLinkUnhighlightStream()).toBeObservable(expectedLinkUnhighlightEventStream); // expect the current highlighted operator is correct highlightActionMarbleEvent.subscribe({ complete: () => { expect(localJointGraphWrapper.getCurrentHighlightedOperatorIDs()).toEqual([mockResultPredicate.operatorID]); }, }); }) ); }); }); ================================================ FILE: frontend/src/app/workspace/service/workflow-graph/model/joint-graph-wrapper.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { fromEvent, Observable, ReplaySubject, Subject } from "rxjs"; import { filter, map } from "rxjs/operators"; import { LogicalPort, Point } from "../../../types/workflow-common.interface"; import * as joint from "jointjs"; import * as dagre from "dagre"; import * as graphlib from "graphlib"; import { ObservableContextManager } from "src/app/common/util/context"; import { Coeditor, User } from "../../../../common/type/user"; import { operatorCoeditorChangedPropertyClass, operatorCoeditorEditingClass } from "../../joint-ui/joint-ui.service"; import { dia } from "jointjs/types/joint"; import * as _ from "lodash"; import Selectors = dia.Cell.Selectors; type linkIDType = { linkID: string }; type JointModelEventInfo = { add: boolean; merge: boolean; remove: boolean; changes: { added: joint.dia.Cell[]; merged: joint.dia.Cell[]; removed: joint.dia.Cell[]; }; }; // argument type of callback event on a JointJS Model, // which is a 3-element tuple: // 1. the JointJS model (Cell) of the event // 2 and 3. additional information of the event type JointModelEvent = [joint.dia.Cell, { graph: joint.dia.Graph; models: joint.dia.Cell[] }, JointModelEventInfo]; type JointLinkChangeEvent = [joint.dia.Link, { x: number; y: number }, { ui: boolean; updateConnectionOnly: boolean }]; type JointPositionChangeEvent = [joint.dia.Element, { x: number; y: number }]; type PositionInfo = { currPos: Point; lastPos: Point | undefined; }; export type JointHighlights = Readonly<{ operators: readonly string[]; links: readonly string[]; commentBoxes: readonly string[]; ports: readonly LogicalPort[]; }>; export type JointGraphContextType = Readonly<{ async: boolean; }>; const DefaultContext: JointGraphContextType = { async: false, }; /** * JointGraphWrapper wraps jointGraph to provide: * - getters of the properties (to hide the methods that could alther the jointGraph directly) * - event streams of JointGraph in RxJS Observables (instead of the callback functions to fit our use of RxJS) * * JointJS Graph only contains information related the UI, such as: * - position of operator elements * - events of a cell (operator or link) being dragging around * - events of adding/deleting a link on the UI, * this doesn't necessarily corresponds to adding/deleting a link logically on the graph * because the link might not connect to a target operator while user is dragging the link * * If an external module needs to access more properties of JointJS graph, * or to make changes **irrelevant** to the graph data structure, but related direcly to the UI, * (such as changing the color of an operator), more methods can be added in this class. * * For an overview of the services in WorkflowGraphModule, see workflow-graph-design.md */ export class JointGraphWrapper { // zoom diff represents the ratio that is zoom in/out everytime, for clicking +/- buttons or using mousewheel public static readonly ZOOM_CLICK_DIFF: number = 0.05; public static readonly INIT_ZOOM_VALUE: number = 1; public static readonly ZOOM_MINIMUM: number = 0.7; public static readonly ZOOM_MAXIMUM: number = 1.3; public jointGraphContext = JointGraphWrapper.jointGraphContextFactory(); public mainPaper!: joint.dia.Paper; private mainJointPaperAttachedStream: Subject = new ReplaySubject(1); private elementPositions: Map = new Map(); private listenPositionChange: boolean = true; // flag that indicates whether multiselect mode is on private multiSelect: boolean = false; private reloadingWorkflow: boolean = false; // the currently highlighted operators' IDs private currentHighlightedOperators: string[] = []; // event stream of highlighting an operator private jointOperatorHighlightStream = new Subject(); // event stream of un-highlighting an operator private jointOperatorUnhighlightStream = new Subject(); // event stream of highlighting a group private jointGroupHighlightStream = new Subject(); // event stream of un-highlighting a group private jointGroupUnhighlightStream = new Subject(); // event stream of highlighing a link private jointLinkHighlightStream = new Subject(); // event stream of unhighlighing a link private jointLinkUnhighlightStream = new Subject(); private jointCommentBoxHighlightStream = new Subject(); private jointCommentBoxUnhighlightStream = new Subject(); private jointPortHighlightStream = new Subject(); private jointPortUnhighlightStream = new Subject(); private currentHighlightedCommentBoxes: string[] = []; // event stream of zooming the jointJS paper private workflowEditorZoomSubject: Subject = new Subject(); // event stream of restoring zoom / offset default of the jointJS paper private restorePaperOffsetSubject: Subject = new Subject(); // event stream of showing the breakpoint button of a link private jointLinkBreakpointShowStream = new Subject(); // event stream of hiding the breakpoint button of a link private jointLinkBreakpointHideStream = new Subject(); // the currently highlighted links' ids private currentHighlightedLinks: string[] = []; // the linkIDs of those links with a breakpoint private currentHighlightedPorts: LogicalPort[] = []; // the IDs of ports currently being edited private linksWithBreakpoints: string[] = []; // current zoom ratio private zoomRatio: number = JointGraphWrapper.INIT_ZOOM_VALUE; /** * This will capture all events in JointJS * involving the 'add' operation */ private jointCellAddStream = fromEvent(this.jointGraph, "add").pipe(map(value => value[0])); /** * This will capture all events in JointJS * involving the 'remove' operation */ private jointCellDeleteStream = fromEvent(this.jointGraph, "remove").pipe(map(value => value[0])); constructor(public jointGraph: joint.dia.Graph) { // handle if the currently highlighted operator/group/link is deleted, it should be unhighlighted this.handleElementDeleteUnhighlight(); this.jointCellAddStream.pipe(filter(cell => cell.isElement())).subscribe(element => { const initPosition = { currPos: (element as joint.dia.Element).position(), lastPos: undefined, }; this.elementPositions.set(element.id.toString(), initPosition); }); this.jointCellDeleteStream .pipe(filter(cell => cell.isElement())) .subscribe(element => this.elementPositions.delete(element.id.toString())); } /** * Let the JointGraph model be attached to the joint paper (paperOptions will be passed to Joint Paper constructor). * * We don't want to expose JointModel as a public variable, so instead we let JointPaper to pass the constructor options, * and JointModel can be still attached to it without being publicly accessible by other modules. * * @param paperOptions JointJS paper options */ public attachMainJointPaper(paperOptions: joint.dia.Paper.Options): joint.dia.Paper { paperOptions.model = this.jointGraph; const paper = new joint.dia.Paper(paperOptions); this.mainPaper = paper; this.mainJointPaperAttachedStream.next(this.mainPaper); this.jointGraphContext.attachPaper(paper); return paper; } public getMainJointPaper(): joint.dia.Paper { return this.mainPaper; } public getMainJointPaperAttachedStream(): Observable { return this.mainJointPaperAttachedStream; } /** * This method is used to toggle the multiselect mode. * @param multiSelect */ public setMultiSelectMode(multiSelect: boolean): void { this.multiSelect = multiSelect; } public setReloadingWorkflow(reloadingWorkflow: boolean): void { this.reloadingWorkflow = reloadingWorkflow; } public getReloadingWorkflow(): boolean { return this.reloadingWorkflow; } /** * Gets the operator ID of the current highlighted operators. * Returns an empty list if there is no highlighted operator. * * The returned array is not the original one so that other * services/components can't modify it directly. */ public getCurrentHighlightedOperatorIDs(): readonly string[] { return this.currentHighlightedOperators; } /** * get the ids of all the links that are currently highlighted */ public getCurrentHighlightedLinkIDs(): readonly string[] { return this.currentHighlightedLinks; } public getCurrentHighlightedPortIDs(): readonly LogicalPort[] { return this.currentHighlightedPorts; } public getCurrentHighlightedCommentBoxIDs(): readonly string[] { return this.currentHighlightedCommentBoxes; } public getCurrentHighlights(): JointHighlights { return { operators: this.currentHighlightedOperators, links: this.currentHighlightedLinks, commentBoxes: this.currentHighlightedCommentBoxes, ports: this.currentHighlightedPorts, }; } public getCurrentHighlightedIDs(): readonly string[] { return [ ...this.currentHighlightedOperators, ...this.currentHighlightedLinks, ...this.currentHighlightedCommentBoxes, ]; } /** * Returns an Observable stream capturing the element position change event in JointJS graph. * An element can be an operator or a group. * * - elementID: the moved element's ID * - oldPosition: the element's position before moving * - newPosition: where the element is moved to */ public getElementPositionChangeEvent(): Observable<{ elementID: string; oldPosition: Point; newPosition: Point; }> { return fromEvent(this.jointGraph, "change:position").pipe( map(e => { const elementID = e[0].id.toString(); const oldPosition = this.elementPositions.get(elementID); const newPosition = { x: e[1].x, y: e[1].y }; if (!oldPosition) { throw new Error(`internal error: cannot find element position for ${elementID}`); } if ( !oldPosition.lastPos || oldPosition.currPos.x !== newPosition.x || oldPosition.currPos.y !== newPosition.y ) { oldPosition.lastPos = oldPosition.currPos; } this.elementPositions.set(elementID, { currPos: newPosition, lastPos: oldPosition.lastPos, }); return { elementID: elementID, oldPosition: oldPosition.lastPos, newPosition: newPosition, }; }) ); } public unhighlightElements(elements: JointHighlights): void { this.unhighlightOperators(...elements.operators); this.unhighlightLinks(...elements.links); this.unhighlightCommentBoxes(...elements.commentBoxes); this.unhighlightPorts(...elements.ports); } /** * Highlights operators in the given list. * * Emits an event to the operator highlight stream with a list of operatorIDs * that are highlighted. * * @param operatorIDs */ public highlightOperators(...operatorIDs: string[]): void { const highlightedOperatorIDs: string[] = []; operatorIDs.forEach(operatorID => { this.highlightElement(operatorID, this.currentHighlightedOperators, highlightedOperatorIDs); }); if (highlightedOperatorIDs.length > 0) { this.jointOperatorHighlightStream.next(highlightedOperatorIDs); } } /** * Unhighlights operators in the given list. * * Emits an event to the operator unhighlight stream with a list of operatorIDs * that are unhighlighted. * * @param operatorIDs */ public unhighlightOperators(...operatorIDs: string[]): void { const unhighlightedOperatorIDs: string[] = []; operatorIDs.forEach(operatorID => this.unhighlightElement(operatorID, this.currentHighlightedOperators, unhighlightedOperatorIDs) ); if (unhighlightedOperatorIDs.length > 0) { this.jointOperatorUnhighlightStream.next(unhighlightedOperatorIDs); } } /** * Highlights the link with given linkID. * Emits an event to the link highlight stream. * @param linkIDs */ public highlightLinks(...linkIDs: string[]): void { const highlightedLinkIDs: string[] = []; linkIDs.forEach(linkID => this.highlightElement(linkID, this.currentHighlightedLinks, highlightedLinkIDs)); if (highlightedLinkIDs.length > 0) { this.jointLinkHighlightStream.next(highlightedLinkIDs); } } /** * Unhighlights the given highlighted link. * Emits an event to the link unhighlight stream. * @param linkIDs */ public unhighlightLinks(...linkIDs: string[]): void { const unhighlightedLinkIDs: string[] = []; linkIDs.forEach(linkID => this.unhighlightElement(linkID, this.currentHighlightedLinks, unhighlightedLinkIDs)); if (unhighlightedLinkIDs.length > 0) { this.jointLinkUnhighlightStream.next(unhighlightedLinkIDs); } } public highlightCommentBoxes(...commentBoxIDs: string[]): void { const highlightedCommentBoxesIDs: string[] = []; commentBoxIDs.forEach(commentBoxID => this.highlightElement(commentBoxID, this.currentHighlightedCommentBoxes, highlightedCommentBoxesIDs) ); if (highlightedCommentBoxesIDs.length > 0) { this.jointCommentBoxHighlightStream.next(highlightedCommentBoxesIDs); } } public unhighlightCommentBoxes(...commentBoxIDs: string[]): void { const unhighlightedCommentBoxesIDs: string[] = []; commentBoxIDs.forEach(commentBoxID => this.unhighlightElement(commentBoxID, this.currentHighlightedCommentBoxes, unhighlightedCommentBoxesIDs) ); if (unhighlightedCommentBoxesIDs.length > 0) { this.jointCommentBoxUnhighlightStream.next(unhighlightedCommentBoxesIDs); } } public highlightPorts(...operatorPortIDs: LogicalPort[]): void { const highlightedLogicalPortIDs: LogicalPort[] = []; operatorPortIDs .filter(operatorPortID => _.find(this.currentHighlightedPorts, operatorPortID) === undefined) .forEach(operatorPortID => { if (!this.multiSelect) this.unhighlightPorts(...this.currentHighlightedPorts); this.currentHighlightedPorts.push(operatorPortID); highlightedLogicalPortIDs.push(operatorPortID); }); this.jointPortHighlightStream.next(highlightedLogicalPortIDs); } public unhighlightPorts(...operatorPortIDs: LogicalPort[]): void { const unhighlightedLogicalPortIDs: LogicalPort[] = []; operatorPortIDs .filter(operatorPortID => _.find(this.currentHighlightedPorts, operatorPortID) !== undefined) .forEach(operatorPortID => { this.currentHighlightedPorts.splice(_.indexOf(this.currentHighlightedPorts, operatorPortID), 1); unhighlightedLogicalPortIDs.push(operatorPortID); }); this.jointPortUnhighlightStream.next(unhighlightedLogicalPortIDs); } /** * Gets the event stream of an operator being highlighted. */ public getJointOperatorHighlightStream(): Observable { return this.jointOperatorHighlightStream.pipe(this.jointGraphContext.bufferWhileAsync); } /** * Gets the event stream of an operator being unhighlighted. * The operator could be unhighlighted because it's deleted. */ public getJointOperatorUnhighlightStream(): Observable { return this.jointOperatorUnhighlightStream.pipe(this.jointGraphContext.bufferWhileAsync); } /** * get the ids of all the links that have a breakpoint */ public getLinkIDsWithBreakpoint(): readonly string[] { return this.linksWithBreakpoints; } /** * get the event stream of a link being highlighted. */ public getLinkHighlightStream(): Observable { return this.jointLinkHighlightStream.pipe(this.jointGraphContext.bufferWhileAsync); } /** * get the event stream of a link being unhighlighted. */ public getLinkUnhighlightStream(): Observable { return this.jointLinkUnhighlightStream.pipe(this.jointGraphContext.bufferWhileAsync); } /** * get the event stream of showing the breakpoint button of a link */ public getLinkBreakpointShowStream(): Observable { return this.jointLinkBreakpointShowStream.asObservable(); } /** * get the event stream of hiding the breakpoint button of a link */ public getLinkBreakpointHideStream(): Observable { return this.jointLinkBreakpointHideStream.asObservable(); } /** * Gets the event stream of an operator being dragged. */ public getJointGroupHighlightStream(): Observable { return this.jointGroupHighlightStream.pipe(this.jointGraphContext.bufferWhileAsync); } /** * Gets the event stream of a group being unhighlighted. * The group could be unhighlighted because it's deleted. */ public getJointGroupUnhighlightStream(): Observable { return this.jointGroupUnhighlightStream.asObservable().pipe(this.jointGraphContext.bufferWhileAsync); } public getJointCommentBoxHighlightStream(): Observable { return this.jointCommentBoxHighlightStream.asObservable(); } public getJointCommentBoxUnhighlightStream(): Observable { return this.jointCommentBoxUnhighlightStream.asObservable(); } public getJointPortHighlightStream(): Observable { return this.jointPortHighlightStream.asObservable(); } public getJointPortUnhighlightStream(): Observable { return this.jointPortUnhighlightStream.asObservable(); } /** * Returns an Observable stream capturing the element cell delete event in JointJS graph. * An element cell can be an operator or an group. */ public getJointElementCellDeleteStream(): Observable { return this.jointCellDeleteStream.pipe( filter(cell => cell.isElement()), map(cell => cell) ); } /** * Returns an Observable stream capturing the link cell add event in JointJS graph. * * Notice that a link added to JointJS graph doesn't mean it will be added to Texera Workflow Graph as well * because the link might not be valid (not connected to a target operator and port yet). * This event only represents that a link cell is visually added to the UI. * */ public getJointLinkCellAddStream(): Observable { return this.jointCellAddStream.pipe( filter(cell => cell.isLink()), map(cell => cell) ); } /** * Returns an Observable stream capturing the link cell delete event in JointJS graph. * * Notice that a link deleted from JointJS graph doesn't mean the same event happens for Texera Workflow Graph * because the link might not be valid and doesn't exist logically in the Workflow Graph. * This event only represents that a link cell visually disappears from the UI. * */ public getJointLinkCellDeleteStream(): Observable { return this.jointCellDeleteStream.pipe( filter(cell => cell.isLink()), map(cell => cell) ); } /** * This method will update the zoom ratio, which will be used * in calculating the position of the operator dropped on the UI. * * @param ratio new ratio from zooming */ public setZoomProperty(ratio: number): void { this.zoomRatio = ratio; this.workflowEditorZoomSubject.next(this.zoomRatio); } /** * Check if the zoom ratio reaches the minimum. */ public isZoomRatioMin(): boolean { return this.zoomRatio <= JointGraphWrapper.ZOOM_MINIMUM; } /** * Check if the zoom ratio reaches the maximum. */ public isZoomRatioMax(): boolean { return this.zoomRatio >= JointGraphWrapper.ZOOM_MAXIMUM; } /** * Returns an observable stream containing the new zoom ratio * for the jointJS paper. */ public getWorkflowEditorZoomStream(): Observable { return this.workflowEditorZoomSubject.asObservable(); } /** * This method will fetch current zoom ratio of the paper. */ public getZoomRatio(): number { return this.zoomRatio; } public autoLayoutJoint(): void { joint.layout.DirectedGraph.layout( [...this.jointGraph.getElements().filter(el => el.attributes.type !== "region"), ...this.jointGraph.getLinks()], { dagre: dagre, graphlib: graphlib, nodeSep: 100, edgeSep: 150, rankSep: 80, ranker: "tight-tree", rankDir: "LR", resizeClusters: true, } ); } /** * This method will restore the default zoom ratio and offset for * the jointjs paper by sending an event to restorePaperSubject. */ public restoreDefaultZoomAndOffset(): void { this.setZoomProperty(JointGraphWrapper.INIT_ZOOM_VALUE); this.restorePaperOffsetSubject.next(); } /** * Returns an Observable stream capturing the event of restoring * default offset */ public getRestorePaperOffsetStream(): Observable { return this.restorePaperOffsetSubject.asObservable(); } /** * Returns an Observable stream capturing the link cell delete event in JointJS graph. * * Notice that the link change event will be triggered whenever the link's source or target is changed: * - one end of the link is attached to a port * - one end of the link is detached to a port and become a point (coordinate) in the paper * - one end of the link is moved from one point to another point in the paper */ public getJointLinkCellChangeStream(): Observable { return fromEvent(this.jointGraph, "change:source change:target").pipe(map(value => value[0])); } /** * This method will get the element position on the JointJS paper. * An element can be an operator or a group. */ public getElementPosition(elementID: string): Point { const cell: joint.dia.Cell | undefined = this.jointGraph.getCell(elementID); if (!cell) { throw new Error(`element with ID ${elementID} doesn't exist`); } if (!cell.isElement()) { throw new Error(`${elementID} is not an element`); } const element = cell; const position = element.position(); return { x: position.x, y: position.y }; } /** * This method repositions the element according to given offsets. * An element can be an operator or a group. */ public setElementPosition(elementID: string, offsetX: number, offsetY: number): void { const cell: joint.dia.Cell | undefined = this.jointGraph.getCell(elementID); if (!cell) { throw new Error(`element with ID ${elementID} doesn't exist`); } if (!cell.isElement()) { throw new Error(`${elementID} is not an element`); } const element = cell; element.translate(offsetX, offsetY); } /** * This method repositions the element according to given absolute positions. * An element can be an operator or a group. */ public setAbsolutePosition(elementID: string, posX: number, poY: number): void { const cell: joint.dia.Cell | undefined = this.jointGraph.getCell(elementID); if (!cell) { throw new Error(`element with ID ${elementID} doesn't exist`); } if (!cell.isElement()) { throw new Error(`${elementID} is not an element`); } const element = cell; element.position(posX, poY); } /** * Highlights the link with given linkID. * Emits an event to the link highlight stream. * If the target link is already highlighted, the action will be ignored. * At current design, there can only be one link highlighted at a time, * no mutiselect mode for links. * Before a link is highlighted, all the currently highlighted operators will * be unhighlighted. * * @param linkID */ public highlightLink(linkID: string): void { if (!this.jointGraph.getCell(linkID)) { throw new Error(`link with ID ${linkID} doesn't exist`); } if (this.currentHighlightedLinks.includes(linkID)) { return; } // only allow one link highlighted at a time if (this.currentHighlightedLinks.length > 0) { const highlightedLinks = Object.assign([], this.currentHighlightedLinks); highlightedLinks.forEach(highlightedLink => this.unhighlightLink(highlightedLink)); } this.getCurrentHighlightedOperatorIDs().forEach(operatorID => this.unhighlightOperators(operatorID)); this.currentHighlightedLinks.push(linkID); this.jointLinkHighlightStream.next([linkID]); } /** * Unhighlights the given highlighted link. * Emits an event to the link unhighlight stream. * @param linkID */ public unhighlightLink(linkID: string): void { if (!this.currentHighlightedLinks.includes(linkID)) { return; } const unhighlightedLinkIndex = this.currentHighlightedLinks.indexOf(linkID); this.currentHighlightedLinks.splice(unhighlightedLinkIndex, 1); this.jointLinkUnhighlightStream.next([linkID]); } /** * This method gets the cell's layer (z attribute) on the JointJS paper. * A cell can be an operator, a link, or a group element. */ public getCellLayer(cellID: string): number { const cell: joint.dia.Cell | undefined = this.jointGraph.getCell(cellID); if (!cell) { throw new Error(`cell with ID ${cellID} doesn't exist`); } return cell.attributes.z || 0; } /** * Returns the boolean value that indicates whether * or not listen to operator position change. */ public getListenPositionChange(): boolean { return this.listenPositionChange; } /** * Sets the boolean value that indicates whether * or not listen to operator position change. */ public setListenPositionChange(listenPositionChange: boolean): void { this.listenPositionChange = listenPositionChange; } /** * Highlights the element with given elementID. * * An element can be either an operator or a group. If the element is already * highlighted, the action will be ignored. * * When the multiselect mode is off: * there is only one element that could be highlighted at a time, therefore * if there are other highlighted elements, they will be unhighlighted. */ private highlightElement( elementID: string, currentHighlightedElements: string[], highlightedElements: string[] ): void { // try to get the element using element ID if (!this.jointGraph.getCell(elementID)) { throw new Error(`element with ID ${elementID} doesn't exist`); } // if the element is already highlighted, don't do anything if (currentHighlightedElements.includes(elementID)) { return; } // if the multiselect mode is off, unhighlight other highlighted elements first if (!this.multiSelect) { this.unhighlightOperators(...this.getCurrentHighlightedOperatorIDs()); this.unhighlightLinks(...this.getCurrentHighlightedLinkIDs()); this.unhighlightCommentBoxes(...this.getCurrentHighlightedCommentBoxIDs()); this.unhighlightPorts(...this.getCurrentHighlightedPortIDs()); } // highlight the element and add it to the list of highlighted elements currentHighlightedElements.push(elementID); highlightedElements.push(elementID); } /** * Unhighlights the given highlighted element (operator or group). * This function fills the unhighlightedElements array to include the unhighlighted elements. */ private unhighlightElement( elementID: string, currentHighlightedElements: string[], unhighlightedElements: string[] ): void { if (!currentHighlightedElements.includes(elementID)) { return; } currentHighlightedElements.splice(currentHighlightedElements.indexOf(elementID), 1); unhighlightedElements.push(elementID); } /** * Subscribes to cell delete event stream, * checks if the deleted cell (operator, link, or group) is currently highlighted * and unhighlight it if it is. */ private handleElementDeleteUnhighlight(): void { this.jointCellDeleteStream.subscribe(deletedCell => { const deletedCellID = deletedCell.id.toString(); if (this.currentHighlightedOperators.includes(deletedCellID)) { this.unhighlightOperators(deletedCellID); } else if (this.currentHighlightedLinks.includes(deletedCellID)) { this.unhighlightLinks(deletedCellID); } }); } public static jointGraphContextFactory() { class JointGraphContext extends ObservableContextManager(DefaultContext) { private static jointPaper: joint.dia.Paper | undefined; public static async() { return this._async(this.getContext()); } // Custom RXJS operator to buffer output while the jointgraph // is in an async context public static bufferWhileAsync(source: Observable): Observable { const subject = new Subject(); const buffer: T[] = []; const clearBuffer = () => { while (buffer.length > 0) { subject.next(buffer.pop() as T); } }; source.subscribe({ next: evt => { if (JointGraphContext.async()) { buffer.push(evt); } else { clearBuffer(); subject.next(evt); } }, error: (err: unknown) => { clearBuffer(); subject.error(err); }, complete: () => { clearBuffer(); subject.complete(); }, }); return subject; } public static attachPaper(jointPaper: joint.dia.Paper) { this.jointPaper = jointPaper; this.jointPaper.options.async = this.async(); } protected static enter(context: JointGraphContextType): void { super.enter(context); if (this.jointPaper !== undefined) { this.jointPaper.options.async = this.async(); } } protected static exit(): void { if (this.jointPaper !== undefined) { const CURRENT_ASYNC_MODE = this._async(this.getContext()); const NEW_ASYNC_MODE = this._async(this.prevContext()); this.jointPaper.options.async = NEW_ASYNC_MODE; if (CURRENT_ASYNC_MODE && !NEW_ASYNC_MODE) this.jointPaper.updateViews(); } super.exit(); } private static _async(context: JointGraphContextType) { return context.async; } } return JointGraphContext; } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Below are methods for coeditor-presence. // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// public deleteCoeditorOperatorHighlight(coeditor: Coeditor, operatorId: string) { const operatorElement = this.getMainJointPaper()?.findViewByModel(operatorId); if (operatorElement) { const currentStrokeIds = joint.highlighters.mask.get(operatorElement).map(stroke => stroke.id); const highlightIdToDelete = `coeditorHighlight_${coeditor.clientId}_${operatorId}`; if (currentStrokeIds.includes(highlightIdToDelete)) { const deletedIndex = currentStrokeIds.indexOf(highlightIdToDelete); joint.highlighters.mask.remove(operatorElement, highlightIdToDelete); currentStrokeIds.splice(deletedIndex, 1); const currentStrokes = joint.highlighters.mask.get(operatorElement); // Update other highlights on this operator to make the diameters consistent. for (let i = deletedIndex; i < currentStrokeIds.length; i++) { const previousStroke = currentStrokes[i]; const highlightId = currentStrokeIds[i]; if (highlightId) { joint.highlighters.mask.remove(operatorElement, highlightId); joint.highlighters.mask.add(operatorElement, "rect.body", highlightId, { ...previousStroke.options, padding: 5 + 5 * i, }); } } } } } public addCoeditorOperatorHighlight(coeditor: Coeditor, operatorId: string) { const operatorElement = this.getMainJointPaper()?.findViewByModel(operatorId); if (operatorElement) { const currentStrokeIds = joint.highlighters.mask.get(operatorElement).map(stroke => stroke.id); const highlightId = `coeditorHighlight_${coeditor.clientId}_${operatorId}`; if (!currentStrokeIds.includes(highlightId)) { joint.highlighters.mask.add(operatorElement, "rect.body", highlightId, { padding: 5 + 5 * currentStrokeIds.length, rx: 5, ry: 5, attrs: { "stroke-width": 2, stroke: coeditor.color, }, }); } } } public setCurrentEditing(coeditor: Coeditor, currentEditing: string): ReturnType { // Calculate location const statusText = coeditor.name + " is viewing/editing..."; const color = coeditor.color; this.getMainJointPaper() ?.getModelById(currentEditing) .attr({ [`.${operatorCoeditorEditingClass}`]: { text: statusText, fill: color, visibility: "visible", }, }); // "Animation" const getCurrentlyEditingText = (): string => { return (this.getMainJointPaper()?.getModelById(currentEditing).attributes.attrs as Selectors)[ `.${operatorCoeditorEditingClass}` ]?.text as string; }; return setInterval(() => { const currentText = getCurrentlyEditingText(); if (currentText.includes(coeditor.name)) { let nextText = ""; if (currentText.length === statusText.length) { nextText = coeditor.name + " is viewing/editing."; } else if (currentText.length === statusText.length - 1) { nextText = coeditor.name + " is viewing/editing..."; } else if (currentText.length === statusText.length - 2) { nextText = coeditor.name + " is viewing/editing.."; } this.getMainJointPaper() ?.getModelById(currentEditing) .attr({ [`.${operatorCoeditorEditingClass}`]: { text: nextText, }, }); } }, 300); } public removeCurrentEditing(coeditor: User, previousEditing: string, intervalId: ReturnType) { clearInterval(intervalId); this.getMainJointPaper() ?.getModelById(previousEditing) .attr({ [`.${operatorCoeditorEditingClass}`]: { text: "", visibility: "hidden", }, }); } public setPropertyChanged(coeditor: User, currentChanged: string) { // Calculate location const statusText = coeditor.name + " changed property!"; const color = coeditor.color; this.getMainJointPaper() ?.getModelById(currentChanged) .attr({ [`.${operatorCoeditorChangedPropertyClass}`]: { text: statusText, fill: color, visibility: "visible", }, }); } public removePropertyChanged(coeditor: User, currentChanged: string) { this.getMainJointPaper() ?.getModelById(currentChanged) .attr({ [`.${operatorCoeditorChangedPropertyClass}`]: { text: "", visibility: "hidden", }, }); } } ================================================ FILE: frontend/src/app/workspace/service/workflow-graph/model/mock-workflow-data.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { CommentBox, OperatorLink, OperatorPredicate, Point } from "../../../types/workflow-common.interface"; import { VIEW_RESULT_OP_TYPE } from "./workflow-graph"; /** * Provides mock data related operators and links: * * Operators: * - 1: ScanSource * - 2: NlpSentiment * - 3: ViewResults * - 4: MultiInputOutputOperator * - 5: PresetEnabledOperator * * Links: * - link-1: ScanSource -> ViewResults * - link-2: ScanSource -> NlpSentiment * - link-3: NlpSentiment -> ScanSource * * Invalid links: * - link-4: (no source port) -> NlpSentiment * - link-5: (NlpSentiment) -> (no target port) * */ export const mockPoint: Point = { x: 100, y: 100, }; export const mockScanPredicate: OperatorPredicate = { operatorID: "1", operatorType: "ScanSource", operatorVersion: "scan", operatorProperties: {}, inputPorts: [], outputPorts: [{ portID: "output-0" }], showAdvanced: true, isDisabled: false, }; export const mockSentimentPredicate: OperatorPredicate = { operatorID: "2", operatorType: "NlpSentiment", operatorVersion: "nlp1", operatorProperties: {}, inputPorts: [{ portID: "input-0" }], outputPorts: [{ portID: "output-0" }], showAdvanced: true, isDisabled: false, }; export const mockResultPredicate: OperatorPredicate = { operatorID: "3", operatorType: VIEW_RESULT_OP_TYPE, operatorVersion: "view1", operatorProperties: {}, inputPorts: [{ portID: "input-0" }], outputPorts: [], showAdvanced: true, isDisabled: false, }; export const mockMultiInputOutputPredicate: OperatorPredicate = { operatorID: "4", operatorType: "MultiInputOutput", operatorVersion: "m1", operatorProperties: {}, inputPorts: [{ portID: "input-0" }, { portID: "input-1" }, { portID: "input-2" }], outputPorts: [{ portID: "output-0" }, { portID: "output-1" }, { portID: "output-2" }], showAdvanced: true, isDisabled: false, }; export const mockPresetEnabledPredicate: OperatorPredicate = { operatorID: "5", operatorType: "PresetEnabledOp", operatorVersion: "p1", operatorProperties: {}, inputPorts: [], outputPorts: [], showAdvanced: true, }; export const mockJavaUDFPredicate: OperatorPredicate = { operatorID: "6", operatorType: "JavaUDF", operatorVersion: "p1", operatorProperties: {}, inputPorts: [{ portID: "input-0" }], outputPorts: [{ portID: "output-0" }], showAdvanced: false, isDisabled: false, }; export const mockPythonUDFPredicate: OperatorPredicate = { operatorID: "7", operatorType: "PythonUDF", operatorVersion: "p1", operatorProperties: {}, inputPorts: [{ portID: "input-0" }], outputPorts: [{ portID: "output-0" }], showAdvanced: false, isDisabled: false, }; export const mockScanResultLink: OperatorLink = { linkID: "link-1", source: { operatorID: mockScanPredicate.operatorID, portID: mockScanPredicate.outputPorts[0].portID, }, target: { operatorID: mockResultPredicate.operatorID, portID: mockResultPredicate.inputPorts[0].portID, }, }; export const mockScanSentimentLink: OperatorLink = { linkID: "link-2", source: { operatorID: mockScanPredicate.operatorID, portID: mockScanPredicate.outputPorts[0].portID, }, target: { operatorID: mockSentimentPredicate.operatorID, portID: mockSentimentPredicate.inputPorts[0].portID, }, }; export const mockSentimentResultLink: OperatorLink = { linkID: "link-3", source: { operatorID: mockSentimentPredicate.operatorID, portID: mockSentimentPredicate.outputPorts[0].portID, }, target: { operatorID: mockResultPredicate.operatorID, portID: mockResultPredicate.inputPorts[0].portID, }, }; export const mockFalseResultSentimentLink: OperatorLink = { linkID: "link-4", source: { operatorID: mockResultPredicate.operatorID, portID: undefined as any, }, target: { operatorID: mockSentimentPredicate.operatorID, portID: mockSentimentPredicate.inputPorts[0].portID, }, }; export const mockFalseSentimentScanLink: OperatorLink = { linkID: "link-5", source: { operatorID: mockSentimentPredicate.operatorID, portID: mockSentimentPredicate.outputPorts[0].portID, }, target: { operatorID: mockScanPredicate.operatorID, portID: undefined as any, }, }; export const mockCommentBox: CommentBox = { commentBoxID: "1", comments: [], commentBoxPosition: mockPoint, }; ================================================ FILE: frontend/src/app/workspace/service/workflow-graph/model/shared-model-change-handler.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { WorkflowGraph } from "./workflow-graph"; import { JointGraphWrapper } from "./joint-graph-wrapper"; import * as Y from "yjs"; import { CommentBox, OperatorLink, OperatorPredicate, Point, PortDescription, } from "../../../types/workflow-common.interface"; import { JointUIService } from "../../joint-ui/joint-ui.service"; import * as joint from "jointjs"; import { YType } from "../../../types/shared-editing.interface"; import { isDefined } from "../../../../common/util/predicate"; import { GuiConfigService } from "../../../../common/service/gui-config.service"; /** * SyncJointModelService listens to changes to the TexeraGraph (SharedModel) and updates Joint graph correspondingly, * regardless of whether the changes are local or from other co-editors. * * In all the handlers, this service will also trigger the necessary subjects, which were written in * {@link WorkflowGraph} in previous implementations and now all migrated here. * */ export class SharedModelChangeHandler { private config: GuiConfigService | null = null; constructor( private texeraGraph: WorkflowGraph, private jointGraph: joint.dia.Graph, private jointGraphWrapper: JointGraphWrapper, private jointUIService: JointUIService ) { this.handleOperatorAddAndDelete(); this.handleLinkAddAndDelete(); this.handleElementPositionChange(); this.handleCommentBoxAddAndDelete(); this.handleOperatorDeep(); this.handleCommentBoxDeep(); this.texeraGraph.newYDocLoadedSubject.subscribe(_ => { this.handleOperatorAddAndDelete(); this.handleLinkAddAndDelete(); this.handleElementPositionChange(); this.handleCommentBoxAddAndDelete(); this.handleOperatorDeep(); this.handleCommentBoxDeep(); }); } setConfigService(config: GuiConfigService): void { this.config = config; } /** * Reflects add and delete operator changes from TexeraGraph onto JointGraph. * @private */ private handleOperatorAddAndDelete(): void { // A new key in the map means a new operator this.texeraGraph.sharedModel.operatorIDMap.observe((event: Y.YMapEvent>) => { const jointElementsToAdd: joint.dia.Element[] = []; const newOpIDs: string[] = []; event.changes.keys.forEach((change, key) => { if (change.action === "add") { const newOperator = this.texeraGraph.sharedModel.operatorIDMap.get(key) as YType; // Also find its position if (this.texeraGraph.sharedModel.elementPositionMap?.has(key)) { const newPos = this.texeraGraph.sharedModel.elementPositionMap?.get(key) as Point; // Add the operator into joint graph const jointOperator = this.jointUIService.getJointOperatorElement(newOperator.toJSON(), newPos); jointElementsToAdd.push(jointOperator); newOpIDs.push(key); } else { throw new Error(`operator with key ${key} does not exist in position map`); } } if (change.action === "delete") { // Disables JointGraph -> TexeraGraph sync temporarily this.texeraGraph.setSyncTexeraGraph(false); // Unhighlight every operator and link to prevent sync error. if (event.transaction.local) { this.jointGraphWrapper.unhighlightElements({ operators: this.jointGraphWrapper.getCurrentHighlightedOperatorIDs(), links: this.jointGraphWrapper.getCurrentHighlightedLinkIDs(), commentBoxes: [], ports: this.jointGraphWrapper.getCurrentHighlightedPortIDs(), }); } this.jointGraph.getCell(key).remove(); // Emit the event streams here, after joint graph is synced. this.texeraGraph.setSyncTexeraGraph(true); this.texeraGraph.operatorDeleteSubject.next({ deletedOperatorID: key }); } }); if (this.config?.env.asyncRenderingEnabled) { // Group add this.jointGraphWrapper.jointGraphContext.withContext({ async: true }, () => { this.jointGraph.addCells(jointElementsToAdd); }); } else { // Add one by one for (let i = 0; i < jointElementsToAdd.length; i++) { this.jointGraph.addCell(jointElementsToAdd[i]); } } // Emit the event streams here, after joint graph is synced and before highlighting. for (let i = 0; i < newOpIDs.length; i++) { const newOpID = newOpIDs[i]; const newOperator = this.texeraGraph.sharedModel.operatorIDMap.get(newOpID) as YType; this.texeraGraph.operatorAddSubject.next(newOperator.toJSON()); } if (event.transaction.local && !this.jointGraphWrapper.getReloadingWorkflow()) { // Only highlight when this is added by current user. this.jointGraphWrapper.setMultiSelectMode(newOpIDs.length > 1); this.jointGraphWrapper.highlightOperators(...newOpIDs); } }); } /** * Syncs link add and delete. * @private */ private handleLinkAddAndDelete(): void { this.texeraGraph.sharedModel.operatorLinkMap.observe((event: Y.YMapEvent) => { const jointElementsToAdd: joint.dia.Link[] = []; const linksToAdd: OperatorLink[] = []; const keysToDelete: string[] = []; const linksToDelete: OperatorLink[] = []; event.changes.keys.forEach((change, key) => { if (change.action === "add") { const newLink = this.texeraGraph.sharedModel.operatorLinkMap.get(key) as OperatorLink; // Validate the link first if (this.validateAndRepairNewLink(newLink)) { const jointLinkCell = JointUIService.getJointLinkCell(newLink); jointElementsToAdd.push(jointLinkCell); linksToAdd.push(newLink); } } if (change.action === "delete") { keysToDelete.push(key); linksToDelete.push(change.oldValue); } }); // Disables JointGraph -> TexeraGraph sync temporarily this.texeraGraph.setSyncTexeraGraph(false); for (let i = 0; i < keysToDelete.length; i++) { if (this.texeraGraph.getSyncJointGraph() && this.jointGraph.getCell(keysToDelete[i])) this.jointGraph.getCell(keysToDelete[i]).remove(); } if (this.config?.env.asyncRenderingEnabled) { this.jointGraphWrapper.jointGraphContext.withContext({ async: true }, () => { this.jointGraph.addCells(jointElementsToAdd.filter(x => x !== undefined)); }); } else { for (let i = 0; i < jointElementsToAdd.length; i++) { this.jointGraph.addCell(jointElementsToAdd[i]); } } this.texeraGraph.setSyncTexeraGraph(true); // Emit event streams and highlight for (let i = 0; i < linksToAdd.length; i++) { const link = linksToAdd[i]; this.texeraGraph.linkAddSubject.next(link); } for (let i = 0; i < linksToDelete.length; i++) { const link = linksToDelete[i]; this.texeraGraph.linkDeleteSubject.next({ deletedLink: link }); } // Uncomment this if you also want link to be highlighted when adding but this conflicts with test cases. // if (event.transaction.local) { // this.jointGraphWrapper.setMultiSelectMode(this.jointGraphWrapper.getCurrentHighlightedOperatorIDs().length + linksToAdd.length > 1); // // Only highlight when this is added by current user. // this.jointGraphWrapper.highlightLinks(...linksToAdd.map(link => link.linkID)); // } }); } /** * Check the sanity of a newly added link. We have constraints on a new link (it should connect to operators and * ports that exist, and it should not be duplicated with another link connecting to the same operator ports.) Such * constraints are enforced if the change to the shared model comes from local UI (`WorkflowGraph.addLink()`). If * the change is initiated by the `UndoManager` or from remote collaborators, however, due to the limitations of Yjs, * it is not possible to check the sanity of this operation before it is applied to the shared model. To ensure the * integrity of the shared model, we validate the link add operation here instead, and repair the shared model if it * violates the constraints. * @param newLink A new link that has already been added to the shared model * @returns Whether this new link passes the sanity check. If it does, this change can be applied to the UI. Otherwise * this link is already deleted from the shared model. */ private validateAndRepairNewLink(newLink: OperatorLink): boolean { try { this.texeraGraph.assertLinkNotDuplicated(newLink); // Verify the link connects to operators and ports that exist. this.texeraGraph.assertLinkIsValid(newLink); return true; } catch (error) { // Invalid link, repair the shared model this.texeraGraph.sharedModel.operatorLinkMap.delete(newLink.linkID); // This is treated as a normal repair step and not an error. console.log("failed to add link. cause: ", (error as Error).message); return false; } } /** * Syncs element positions. Will temporarily block local updates. * @private */ private handleElementPositionChange(): void { this.texeraGraph.sharedModel.elementPositionMap?.observe((event: Y.YMapEvent) => { event.changes.keys.forEach((change, key) => { if (change.action === "update") { this.texeraGraph.setSyncTexeraGraph(false); const newPosition = this.texeraGraph.sharedModel.elementPositionMap?.get(key); if (newPosition) { this.jointGraphWrapper.setListenPositionChange(false); this.jointGraphWrapper.setAbsolutePosition(key, newPosition.x, newPosition.y); this.jointGraphWrapper.setListenPositionChange(true); } this.texeraGraph.setSyncTexeraGraph(true); } }); }); } /** * Syncs the addition and deletion of comment boxes. * @private */ private handleCommentBoxAddAndDelete(): void { this.texeraGraph.sharedModel.commentBoxMap.observe((event: Y.YMapEvent>) => { event.changes.keys.forEach((change, key) => { if (change.action === "add") { const commentBox = this.texeraGraph.sharedModel.commentBoxMap.get(key) as YType; const commentElement = this.jointUIService.getCommentElement(commentBox.toJSON()); this.jointGraph.addCell(commentElement); this.texeraGraph.commentBoxAddSubject.next(commentBox.toJSON()); } if (change.action === "delete") { this.jointGraph.getCell(key).remove(); } }); }); } /** * Syncs changes that are on nested-structures of operators, including changes on: * - customDisplayName * - operatorProperties * - operatorPorts * - viewResult * - isDisabled * @private */ private handleOperatorDeep(): void { this.texeraGraph.sharedModel.operatorIDMap.observeDeep((events: Y.YEvent>[]) => { events.forEach(event => { if (event.target !== this.texeraGraph.sharedModel.operatorIDMap) { const operatorID = event.path[0] as string; if (event.path.length === 1) { // Changes one level below the operatorPredicate type for (const entry of event.changes.keys.entries()) { const contentKey = entry[0]; if (contentKey === "viewResult") { const newViewOpResultStatus = this.texeraGraph.sharedModel.operatorIDMap .get(operatorID) ?.get("viewResult") as boolean; if (newViewOpResultStatus) { this.texeraGraph.viewResultOperatorChangedSubject.next({ newViewResultOps: [operatorID], newUnviewResultOps: [], }); } else { this.texeraGraph.viewResultOperatorChangedSubject.next({ newViewResultOps: [], newUnviewResultOps: [operatorID], }); } } else if (contentKey === "markedForReuse") { const newReuseCacheOps = this.texeraGraph.sharedModel.operatorIDMap .get(operatorID) ?.get("markedForReuse") as boolean; if (newReuseCacheOps) { this.texeraGraph.reuseOperatorChangedSubject.next({ newReuseCacheOps: [operatorID], newUnreuseCacheOps: [], }); } else { this.texeraGraph.reuseOperatorChangedSubject.next({ newReuseCacheOps: [], newUnreuseCacheOps: [operatorID], }); } } else if (contentKey === "isDisabled") { const newDisabledStatus = this.texeraGraph.sharedModel.operatorIDMap .get(operatorID) ?.get("isDisabled") as boolean; if (newDisabledStatus) { this.texeraGraph.disabledOperatorChangedSubject.next({ newDisabled: [operatorID], newEnabled: [], }); } else { this.texeraGraph.disabledOperatorChangedSubject.next({ newDisabled: [], newEnabled: [operatorID], }); } } else if (contentKey === "operatorProperties") { this.onOperatorPropertyChanged(operatorID, event.transaction.local); } } } else if (event.path[event.path.length - 1] === "customDisplayName") { const newName = this.texeraGraph.sharedModel.operatorIDMap .get(operatorID) ?.get("customDisplayName") as Y.Text; this.texeraGraph.operatorDisplayNameChangedSubject.next({ operatorID: operatorID, newDisplayName: newName.toJSON(), }); } else if (event.path.includes("operatorProperties")) { this.onOperatorPropertyChanged(operatorID, event.transaction.local); } else if (event.path.includes("inputPorts")) { this.handlePortEvent(event, operatorID, true); } else if (event.path.includes("outputPorts")) { this.handlePortEvent(event, operatorID, false); } else { throw new Error(`undefined operation on shared type: .${event}`); } } }); }); } /** * Handles the additon, deletion and deeper changes to operator ports. * @param event * @param operatorID * @param isInput Since input and output ports are separate properties, need to access them differently. * @private */ private handlePortEvent(event: Y.YEvent>, operatorID: string, isInput: boolean) { if (event.path.length === 2) { // Port added or deleted inferred by event.delta const addedPort = event.delta[1]?.insert; if (isDefined(addedPort)) { this.onPortAdded(operatorID, isInput, (addedPort as Y.Map[])[0].toJSON() as PortDescription); } else if (isDefined(event.delta[0]?.delete) || isDefined(event.delta[1]?.delete)) { this.onPortRemoved(operatorID, isInput); } } else { const changedOperator = this.texeraGraph.getOperator(operatorID); if (event.path.includes("displayName")) { // Display name changed (via shared text editor) const changedPort = isInput ? changedOperator.inputPorts[event.path[2] as number] : changedOperator.outputPorts[event.path[2] as number]; this.texeraGraph.portDisplayNameChangedSubject.next({ operatorID: operatorID, portID: changedPort.portID, newDisplayName: event.target.toJSON() as unknown as string, }); } else if (event.path.length >= 3) { // Port property changed const newPortDescription = isInput ? changedOperator.inputPorts[event.path[2] as number] : changedOperator.outputPorts[event.path[2] as number]; if (isDefined(newPortDescription.partitionRequirement) && isDefined(newPortDescription.dependencies)) this.texeraGraph.portPropertyChangedSubject.next({ operatorPortID: { operatorID: operatorID, portID: newPortDescription.portID, }, newProperty: { partitionInfo: newPortDescription.partitionRequirement, dependencies: newPortDescription.dependencies, }, }); } else { throw new Error(`undefined port operation on shared type: .${event}`); } } } /** * Also update awareness info here to accommodate different paths of updates. * @param operatorID * @param isLocal * @private */ private onOperatorPropertyChanged(operatorID: string, isLocal: boolean) { const operator = this.texeraGraph.getOperator(operatorID); this.texeraGraph.operatorPropertyChangeSubject.next({ operator: operator }); if (isLocal) { // emit operator property changed here const localState = this.texeraGraph.sharedModel.awareness.getLocalState(); if (localState && localState["currentlyEditing"] === operatorID) { this.texeraGraph.updateSharedModelAwareness("changed", operatorID); this.texeraGraph.updateSharedModelAwareness("changed", undefined); } } } private onPortAdded(operatorID: string, isInput: boolean, port: PortDescription) { const operatorJointElement = this.jointGraph.getCell(operatorID); const portGroup = isInput ? "in" : "out"; operatorJointElement.addPort({ group: portGroup, id: port.portID, attrs: { ".port-label": { text: port.displayName ?? "", }, }, }); const operator = this.texeraGraph.getOperator(operatorID); this.texeraGraph.portAddedOrDeletedSubject.next({ newOperator: operator }); } private onPortRemoved(operatorID: string, isInput: boolean) { const operatorJointElement = this.jointGraph.getCell(operatorID); const portGroup = isInput ? "in" : "out"; let lastPort; for (let p of operatorJointElement.getPorts()) { if (p.group === portGroup) { lastPort = p; } } if (lastPort) { operatorJointElement.removePort(lastPort); } const operator = this.texeraGraph.getOperator(operatorID); this.texeraGraph.portAddedOrDeletedSubject.next({ newOperator: operator }); } /** * Syncs changes that are on nested-structures of comment boxes, including changes of: * - adding comments * - deleting comments * - editing comments (processed as deleting and then adding in-place) * @private */ private handleCommentBoxDeep(): void { this.texeraGraph.sharedModel.commentBoxMap.observeDeep((events: Y.YEvent[]) => { events.forEach(event => { if (event.target !== this.texeraGraph.sharedModel.commentBoxMap) { const commentBox: CommentBox = this.texeraGraph.getCommentBox(event.path[0] as string); if (event.path.length === 2 && event.path[event.path.length - 1] === "comments") { const addedComments = Array.from(event.changes.added.values()); const deletedComments = Array.from(event.changes.deleted.values()); if (addedComments.length == deletedComments.length) { this.texeraGraph.commentBoxEditCommentSubject.next({ commentBox: commentBox }); } else { if (addedComments.length > 0) { const newComment = addedComments[0].content.getContent()[0]; this.texeraGraph.commentBoxAddCommentSubject.next({ addedComment: newComment, commentBox: commentBox }); } if (deletedComments.length > 0) { this.texeraGraph.commentBoxDeleteCommentSubject.next({ commentBox: commentBox }); } } } } }); }); } } ================================================ FILE: frontend/src/app/workspace/service/workflow-graph/model/shared-model.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import * as Y from "yjs"; import { WebsocketProvider } from "y-websocket"; import { Awareness } from "y-protocols/awareness"; import { BreakpointInfo, CommentBox, OperatorLink, OperatorPredicate, Point, } from "../../../types/workflow-common.interface"; import { CoeditorState, User } from "../../../../common/type/user"; import { getWebsocketUrl } from "../../../../common/util/url"; import { v4 as uuid } from "uuid"; import { YType } from "../../../types/shared-editing.interface"; /** * SharedModel encapsulates everything related to real-time shared editing for the current workflow. * Most of the yjs-related implementations are within this class. */ export class SharedModel { public yDoc: Y.Doc = new Y.Doc(); public wsProvider: WebsocketProvider; public awareness: Awareness; public operatorIDMap: Y.Map>; public commentBoxMap: Y.Map>; public operatorLinkMap: Y.Map; public elementPositionMap: Y.Map; public debugState: Y.Map>; public undoManager: Y.UndoManager; public clientId: string; /** * Initializes yjs-related structures and join the shared-editing room. A room number is required for initialization. * When wid is present, it will be used as part of the room number to enable shared editing. * When no wid is provided (new workflow canvas, landing page, etc.), a random room number will be assigned so that * users don't interfere with each other. * @param wid workflow ID number, used as part of the address for the shared-editing room. * @param user current (local) user info, used for initializing local awareness (user presence). * @param productionSharedEditingServer whether to use production shared editing server */ constructor( public wid?: number, public user?: User, private productionSharedEditingServer?: boolean ) { // Initialize Y-structures. this.debugState = this.yDoc.getMap("debugActions"); this.operatorIDMap = this.yDoc.getMap("operatorIDMap"); this.commentBoxMap = this.yDoc.getMap("commentBoxMap"); this.operatorLinkMap = this.yDoc.getMap("operatorLinkMap"); this.elementPositionMap = this.yDoc.getMap("elementPositionMap"); // Initialize Y-undo manager by aggregating intended Y-structures. Only structures included here will be undoable. this.undoManager = new Y.UndoManager( [this.operatorIDMap, this.elementPositionMap, this.operatorLinkMap, this.commentBoxMap], { captureTimeout: 100, } ); // Generate editing room number. const websocketUrl = this.getYWebSocketBaseUrl(); const suffix = wid ? `${wid}` : uuid(); this.wsProvider = new WebsocketProvider(websocketUrl, suffix, this.yDoc); // Initialize local user awareness information. this.awareness = this.wsProvider.awareness; this.clientId = this.awareness.clientID.toString(); if (this.user) { const userState: CoeditorState = { user: { ...this.user, clientId: this.clientId }, isActive: true, userCursor: { x: 0, y: 0 }, }; this.awareness.setLocalState(userState); } } /** * Shared editing needs y-websocket to be running. The base url depends on whether reverse proxy is set up. For local * development, we need to use localhost; For production server which has reverse proxy, we can use the same base url * as the server. * @private */ private getYWebSocketBaseUrl() { return getWebsocketUrl("rtc", ""); } /** * Updates a particular field of local awareness state info. Will only execute update when user info is provided. * @param field the name of the particular state info. * @param value the updated state info. */ public updateAwareness(field: K, value: CoeditorState[K]): void { if (this.user) this.awareness.setLocalStateField(field, value); } /** * Groups a bunch of actions into one atomic transaction, so that they can be undone/redone in one call. * @param callback Put whatever need to be atomically done within this callback function. */ public transact(callback: Function) { this.yDoc.transact(() => callback()); } /** * Destroys internal structures related to Yjs and quit the editing room. */ public destroy(): void { this.awareness.destroy(); try { if (this.wsProvider.shouldConnect && this.wsProvider.wsconnected) this.wsProvider.disconnect(); } catch (e) {} this.yDoc.destroy(); } } ================================================ FILE: frontend/src/app/workspace/service/workflow-graph/model/sync-texera-model.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { UndoRedoService } from "../../undo-redo/undo-redo.service"; import { SyncTexeraModel } from "./sync-texera-model"; import { JointGraphWrapper } from "./joint-graph-wrapper"; import { WorkflowGraph } from "./workflow-graph"; import { OperatorLink } from "../../../types/workflow-common.interface"; import { mockResultPredicate, mockScanPredicate, mockScanResultLink, mockScanSentimentLink, mockSentimentPredicate, } from "./mock-workflow-data"; import { TestBed } from "@angular/core/testing"; import { marbles } from "rxjs-marbles"; import * as joint from "jointjs"; import { JointUIService } from "../../joint-ui/joint-ui.service"; import { WorkflowUtilService } from "../util/workflow-util.service"; import { StubOperatorMetadataService } from "../../operator-metadata/stub-operator-metadata.service"; import { OperatorMetadataService } from "../../operator-metadata/operator-metadata.service"; import { commonTestProviders } from "../../../../common/testing/test-utils"; describe("SyncTexeraModel", () => { let texeraGraph: WorkflowGraph; let jointGraph: joint.dia.Graph; let jointGraphWrapper: JointGraphWrapper; /** * Returns a mock JointJS operator Element object (joint.dia.Element) * The implementation code only uses the id attribute of the object. * * @param operatorID */ function getJointOperatorValue(operatorID: string): joint.dia.Element { return { id: operatorID, } as joint.dia.Element; } /** * Returns a mock JointJS Link object (joint.dia.Link) * It includes the attributes and functions same as JointJS * and are used by the implementation code. * @param link */ function getJointLinkValue(link: OperatorLink): joint.dia.Link { // getSourceElement, getTargetElement, and get all returns a function // that returns the corresponding value return { id: link.linkID, attributes: { source: { id: link.source.operatorID, port: link.source.portID }, target: { id: link.target.operatorID, port: link.target.portID }, }, // getSourceElement: () => ({ id: link.source.operatorID }), // getTargetElement: () => ({ id: link.target.operatorID }), // get: (port: string) => { // if (port === 'source') { // return { port: link.source.portID }; // } else if (port === 'target') { // return { port: link.target.portID }; // } else { // throw new Error('getJointLinkValue: mock is inconsistent with implementation'); // } // } } as any as joint.dia.Link; } /** * This helper function returns a mock JointJS link object (joint.dia.Link) * that is only connected to a source port, but detached from the target port. * * This scenario happens when the user is still moving the link * and it is not connected to a target port. * * @param link an operator link, but the target operator and target link is ignored */ function getIncompleteJointLink(link: OperatorLink): joint.dia.Link { // getSourceElement, getTargetElement, and get all returns a function // that returns the corresponding value return { id: link.linkID, getSourceElement: () => ({ id: link.source.operatorID }), getTargetElement: () => null, get: (port: string) => { if (port === "source") { return { port: link.source.portID }; } else if (port === "target") { return null; } else { throw new Error("getJointLinkValue: mock is inconsistent with implementation"); } }, } as joint.dia.Link; } beforeEach(() => { TestBed.configureTestingModule({ providers: [ UndoRedoService, WorkflowUtilService, JointUIService, { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, ...commonTestProviders, ], }); texeraGraph = new WorkflowGraph(); jointGraph = new joint.dia.Graph(); jointGraphWrapper = new JointGraphWrapper(jointGraph); }); // The delete event will not happen from JointJS. // /** // * Test JointJS delete operator `getJointElementCellDeleteStream` event stream handled properly // * // * Add one operator // * Then emit one delete operator event from JointJS // * // * addOperator // * jointDeleteOperator: ---d-| // * // * Expected: // * The workflow graph should not have the added operator // * The workflow graph should have 0 operators // */ // it( // "should delete an operator when the delete operator event happens from JointJS", // marbles(m => { // // add operators // texeraGraph.addOperator(mockScanPredicate); // // // prepare delete operator event stream // const deleteOpMarbleString = "---d-|"; // const deleteOpMarbleValues = { // d: getJointOperatorValue(mockScanPredicate.operatorID), // }; // vi.spyOn(jointGraphWrapper, "getJointElementCellDeleteStream").mockReturnValue( // m.hot(deleteOpMarbleString, deleteOpMarbleValues) // ); // // // construct the texera sync model with spied dependencies // const syncTexeraModel = new SyncTexeraModel( // texeraGraph, // jointGraphWrapper, // new OperatorGroup( // texeraGraph, // jointGraph, // jointGraphWrapper, // TestBed.inject(WorkflowUtilService), // TestBed.inject(JointUIService) // ) // ); // // // assert workflow graph // jointGraphWrapper.getJointElementCellDeleteStream().subscribe({ // complete: () => { // expect(texeraGraph.hasOperator(mockScanPredicate.operatorID)).toBeFalsy(); // expect(texeraGraph.getAllOperators().length).toEqual(0); // }, // }); // }) // ); // The delete event will not happen from JointJS. // /** // * Test JointJS delete operator `getJointElementCellDeleteStream` event stream handled properly // * // * Add two operators // * Then emit one delete operator event from JointJS // * // * addOperator // * jointDeleteOperator: -----d-| // * // * Expected: // * Only the deleted operator should be removed. // * The graph should have 1 operators and 0 links. // */ // it( // "should delete an operator and not touch other operators when the delete operator event happens from JointJS", // marbles(m => { // // add operators // texeraGraph.addOperator(mockScanPredicate); // texeraGraph.addOperator(mockResultPredicate); // // // prepare delete operator // const deleteOpMarbleString = "-----d-|"; // const deleteOpMarbleValues = { // d: getJointOperatorValue(mockScanPredicate.operatorID), // }; // vi.spyOn(jointGraphWrapper, "getJointElementCellDeleteStream").mockReturnValue( // m.hot(deleteOpMarbleString, deleteOpMarbleValues) // ); // // // construct the texera sync model with spied dependencies // // construct the texera sync model with spied dependencies // const syncTexeraModel = new SyncTexeraModel( // texeraGraph, // jointGraphWrapper, // new OperatorGroup( // texeraGraph, // jointGraph, // jointGraphWrapper, // TestBed.inject(WorkflowUtilService), // TestBed.inject(JointUIService) // ) // ); // // jointGraphWrapper.getJointElementCellDeleteStream().subscribe({ // complete: () => { // expect(texeraGraph.hasOperator(mockScanPredicate.operatorID)).toBeFalsy(); // expect(texeraGraph.hasOperator(mockResultPredicate.operatorID)).toBeTruthy(); // expect(texeraGraph.getAllOperators().length).toEqual(1); // expect(texeraGraph.getAllLinks().length).toEqual(0); // }, // }); // }) // ); /** * Test JointJS delete operator `getJointElementCellDeleteStream` event stream handled properly * * Add two operators * Delete on operator * * Then if the SyncTexeraModel Service should explicitly throw and error * if the JointModelService emits an operator delete event on the nonexist operator (should not happen), * then TexeraSyncService should explicitly throw an error (this case should not happen). * * Expected: * delete an nonexit operator, error is thrown */ it( "should explicitly throw an error if the JointJS operator delete event deletes a nonexist operator", marbles(m => { // add operators texeraGraph.addOperator(mockScanPredicate); texeraGraph.addOperator(mockResultPredicate); texeraGraph.deleteOperator(mockScanPredicate.operatorID); // prepare delete operator const deleteOpMarbleString = "-----d-|"; const deleteOpMarbleValues = { d: getJointOperatorValue(mockScanPredicate.operatorID), }; // mock delete the operator operation at the same time frame of jointJS deleting it // but executed before the handler vi.spyOn(jointGraphWrapper, "getJointElementCellDeleteStream").mockReturnValue( m.hot(deleteOpMarbleString, deleteOpMarbleValues) ); // construct the texera sync model with spied dependencies // TODO: expect error to be thrown // const syncTexeraModel = new SyncTexeraModel(texeraGraph, jointGraphWrapper, // new OperatorGroup(texeraGraph, jointGraph, jointGraphWrapper, TestBed.inject(WorkflowUtilService), // TestBed.inject(JointUIService))); // this should throw an error when the model is constructed and the // delete is called for second time on the same operator by the delete stream }) ); /** * Test JointJS add link `getJointLinkCellAddStream` event stream handled properly * * Add two operators * Then emit one add link event from JointJS * * addOperator * jointAddLink: -----p-| * * Expected: * The graph should have two operators and a link between the operators */ it( "should add a link when link add event happen from JointJS", marbles(m => { // add operators texeraGraph.addOperator(mockScanPredicate); texeraGraph.addOperator(mockResultPredicate); // prepare add link const addLinkMarbleString = "-----p-|"; const addLinkMarbleValues = { p: getJointLinkValue(mockScanResultLink), }; vi.spyOn(jointGraphWrapper, "getJointLinkCellAddStream").mockReturnValue( m.hot(addLinkMarbleString, addLinkMarbleValues) ); // construct the texera sync model with spied dependencies // construct the texera sync model with spied dependencies const syncTexeraModel = new SyncTexeraModel(texeraGraph, jointGraphWrapper); jointGraphWrapper.getJointLinkCellAddStream().subscribe({ complete: () => { expect(texeraGraph.getAllOperators().length).toEqual(2); expect(texeraGraph.getAllLinks().length).toEqual(1); expect(texeraGraph.hasLinkWithID(mockScanResultLink.linkID)).toBeTruthy(); expect(texeraGraph.getLinkWithID(mockScanResultLink.linkID)).toEqual(mockScanResultLink); expect(texeraGraph.hasLink(mockScanResultLink.source, mockScanResultLink.target)).toBeTruthy(); }, }); }) ); /** * Test JointJS add link `getJointLinkCellAddStream` event stream handled properly * when the added JointJS link is invalid. * * Add two operators * Then a user drags a link from a source port, * the link is visually added, * but the link is not yet connected to a target port. * This link is considered invalid and should not appear in the graph * * addOperator * jointAddLink: -----q-| (q is an incomplete Joint link) * * Expected: * The graph doesn't contain the incomplete link */ it( "should not create a link when an incomplete link is added in JointJS", marbles(m => { // add operators texeraGraph.addOperator(mockScanPredicate); texeraGraph.addOperator(mockResultPredicate); // prepare add link (incomplete link) const addLinkMarbleString = "-----q-|"; const addLinkMarbleValues = { q: getIncompleteJointLink(mockScanResultLink), }; vi.spyOn(jointGraphWrapper, "getJointLinkCellAddStream").mockReturnValue( m.hot(addLinkMarbleString, addLinkMarbleValues) ); // construct the texera sync model with spied dependencies const syncTexeraModel = new SyncTexeraModel(texeraGraph, jointGraphWrapper); jointGraphWrapper.getJointLinkCellDeleteStream().subscribe({ complete: () => { expect(texeraGraph.getAllLinks().length).toEqual(0); }, }); }) ); /** * Test JointJS delete link `getJointLinkCellDeleteStream` event stream handled properly * * Add two operators and one link * Then emit one delete link event from JointJS * * add operators + links: 1 -> 2 * jointDeleteLink: -------r-| * * Expected: * The link should be deleted */ it( "should delete a link when link delete event happens from JointJS", marbles(m => { // add operators texeraGraph.addOperator(mockScanPredicate); texeraGraph.addOperator(mockResultPredicate); // add links texeraGraph.addLink(mockScanResultLink); // prepare delete link const deleteLinkMarbleString = "-------r-|"; const deleteLinkMarbleValues = { r: getJointLinkValue(mockScanResultLink), }; vi.spyOn(jointGraphWrapper, "getJointLinkCellDeleteStream").mockReturnValue( m.hot(deleteLinkMarbleString, deleteLinkMarbleValues) ); // construct the texera sync model with spied dependencies const syncTexeraModel = new SyncTexeraModel(texeraGraph, jointGraphWrapper); jointGraphWrapper.getJointLinkCellDeleteStream().subscribe({ complete: () => { expect(texeraGraph.getAllLinks().length).toEqual(0); }, }); }) ); /** * Test JointJS delete link `getJointLinkCellDeleteStream` event stream handled properly, * when the deleted link is invalid and never existed in texera graph. * * Add two operators * Then a user drags a link from a source port, * the link is visually added, * but the link is not yet connected to a target port. * Then the user release the mouse and the link is visually deleted, * JointJS emits Link Delete event, * the workflow graph should ignore it. * * add operators * jointAddLink: -----q-| (q is an incomplete Joint link) * jointDeleteLink: -------r-| (the visual deletion of the incomplete link) * * Expected: * The graph doesn't contain the link */ it( "should ignore JointJS link delete event of an incomplete link", marbles(m => { // add operators texeraGraph.addOperator(mockScanPredicate); texeraGraph.addOperator(mockResultPredicate); // prepare add link (incomplete link) const addLinkMarbleString = "-----q-|"; const addLinkMarbleValues = { q: getIncompleteJointLink(mockScanResultLink), }; vi.spyOn(jointGraphWrapper, "getJointLinkCellAddStream").mockReturnValue( m.hot(addLinkMarbleString, addLinkMarbleValues) ); // prepare delete link (incomplete link) const deleteLinkMarbleString = "-------r-|"; const deleteLinkMarbleValues = { r: getIncompleteJointLink(mockScanResultLink), }; vi.spyOn(jointGraphWrapper, "getJointLinkCellDeleteStream").mockReturnValue( m.hot(deleteLinkMarbleString, deleteLinkMarbleValues) ); // construct the texera sync model with spied dependencies const syncTexeraModel = new SyncTexeraModel(texeraGraph, jointGraphWrapper); jointGraphWrapper.getJointLinkCellAddStream().subscribe({ complete: () => { expect(texeraGraph.getAllLinks().length).toEqual(0); }, }); }) ); /** * Test JointJS link change `getJointLinkCellChangeStream` event stream handled properly, * when the link change involves logical link delete * * Add two operators * Then add a link of these operators * Then the user drags the target port of the connected link, * the link is detached from the target port. * This link is now considered invalid and should be deleted from the graph * * add operators and links: 1 -> 2 * changeLink: -------q-| (link changes: detached from the target) * * The detatched link should be deleted from the graph. */ it( "should delete the link when a link is detached from the target port", marbles(m => { // add operators texeraGraph.addOperator(mockScanPredicate); texeraGraph.addOperator(mockResultPredicate); // add links texeraGraph.addLink(mockScanResultLink); // prepare change link (link detached from target port) const changeLinkMarbleString = "-------q-|"; const changeLinkMarbleValues = { q: getIncompleteJointLink(mockScanResultLink), }; vi.spyOn(jointGraphWrapper, "getJointLinkCellChangeStream").mockReturnValue( m.hot(changeLinkMarbleString, changeLinkMarbleValues) ); // construct the texera sync model with spied dependencies const syncTexeraModel = new SyncTexeraModel(texeraGraph, jointGraphWrapper); jointGraphWrapper.getJointLinkCellChangeStream().subscribe({ complete: () => { expect(texeraGraph.getAllLinks().length).toEqual(0); }, }); }) ); /** * Test JointJS link change `getJointLinkCellChangeStream` event stream handled properly, * when the link change involves logical link delete, * and the same change event involves an *immediate* link add. * * Add three operators * Then add a link from operator 1 to operator 2 * Then the user directly drags the target port from operator 2's input operator * to operator 3's input port. The link automatically attach to operator3's target port, * and JointJS only emits one link change event, * * addOperators: 1 -> 2 (will change to 1 -> 3 in after changeLink event) * addLink: -------p-| * changeLink: ---------t-| (link changes: target operator/port changed) * * Expected: * the link should be changed to the new target * */ it( "should delete and then re-add the link if link target is changed from one port to another", marbles(m => { // add operators texeraGraph.addOperator(mockScanPredicate); texeraGraph.addOperator(mockSentimentPredicate); texeraGraph.addOperator(mockResultPredicate); // prepare add link const addLinkMarbleString = "-------p-|"; const addLinkMarbleValues = { p: getJointLinkValue(mockScanResultLink), }; vi.spyOn(jointGraphWrapper, "getJointLinkCellAddStream").mockReturnValue( m.hot(addLinkMarbleString, addLinkMarbleValues) ); // create a mock changed link using another link's source/target // but the link ID remains the same const mockChangedLink = { ...mockScanSentimentLink, linkID: mockScanResultLink.linkID, }; // prepare change link (link detached from target port) const changeLinkMarbleString = "---------t-|"; const changeLinkMarbleValues = { t: getJointLinkValue(mockChangedLink), }; vi.spyOn(jointGraphWrapper, "getJointLinkCellChangeStream").mockReturnValue( m.hot(changeLinkMarbleString, changeLinkMarbleValues) ); // construct the texera sync model with spied dependencies const syncTexeraModel = new SyncTexeraModel(texeraGraph, jointGraphWrapper); jointGraphWrapper.getJointLinkCellChangeStream().subscribe({ complete: () => { expect(texeraGraph.getAllLinks().length).toEqual(1); expect(texeraGraph.hasLinkWithID(mockChangedLink.linkID)).toBeTruthy(); expect(texeraGraph.getLinkWithID(mockChangedLink.linkID)).toEqual(mockChangedLink); expect(texeraGraph.hasLink(mockScanResultLink.source, mockScanResultLink.target)).toBeFalsy(); expect(texeraGraph.hasLink(mockChangedLink.source, mockChangedLink.target)).toBeTruthy(); }, }); }) ); /** * Test JointJS link change `getJointLinkCellChangeStream` event stream handled properly, * when the link change involves logical link delete, * and a later change event involves a logical link add. * * Add three operators (1, 2, 3) and link 1 -> 3 * Then the user *gradually* drags the target port from operator 3's input port * to operator 2's input port. (1 -> 3) changed to (1 -> 2) * The link is detached, then move around the paper for a while, then re-attached to another port * * changeLink: ---------q-r-s-t-| (q: link detached with target being a point, r: target moved to another point, * s: target moved to another point, t: target re-attached to another port) * * Expected: * the link should be changed to the new target. * * TODO: finish change link test stream to compare to streams * TODO: This test's functionality is okay but content needs to be changed with the introduction of shared editing. * TODO: because SyncJointModel is needed. */ // it( // "should remove then add link if link target port is detached then dragged around then re-attached", // marbles(m => { // // add operators // texeraGraph.addOperator(mockScanPredicate); // texeraGraph.addOperator(mockSentimentPredicate); // texeraGraph.addOperator(mockResultPredicate); // // // add links // texeraGraph.addLink(mockScanResultLink); // // // create a mock changed link using another link's source/target // // but the link ID remains the same // const mockChangedLink = { // ...mockScanSentimentLink, // linkID: mockScanResultLink.linkID, // }; // // // prepare change link (link detached from target port) // const changeLinkMarbleString = "---------q-r-s-t-|"; // const changeLinkMarbleValues = { // q: getIncompleteJointLink(mockScanResultLink), // r: getIncompleteJointLink(mockScanResultLink), // s: getIncompleteJointLink(mockScanResultLink), // t: getJointLinkValue(mockChangedLink), // }; // vi.spyOn(jointGraphWrapper, "getJointLinkCellChangeStream").mockReturnValue( // m.hot(changeLinkMarbleString, changeLinkMarbleValues) // ); // // // construct the texera sync model with spied dependencies // const syncTexeraModel = new SyncTexeraModel( // texeraGraph, // jointGraphWrapper, // new OperatorGroup( // texeraGraph, // jointGraph, // jointGraphWrapper, // TestBed.inject(WorkflowUtilService), // TestBed.inject(JointUIService) // ) // ); // // jointGraphWrapper.getJointLinkCellChangeStream().subscribe({ // complete: () => { // expect(texeraGraph.getAllLinks().length).toEqual(1); // expect(texeraGraph.hasLink(mockChangedLink.source, mockChangedLink.target)).toBeTruthy(); // }, // }); // // // assert link delete stream: delete original link // const linkDeleteStream = texeraGraph.getLinkDeleteStream(); // const expectedDeleteStream = m.hot("---------q---", { // q: { deletedLink: mockScanResultLink }, // }); // m.expect(linkDeleteStream).toBeObservable(expectedDeleteStream); // // // assert link add stream: changed link after its re-attached (original link is added synchronously in the begining) // const linkAddStream = texeraGraph.getLinkAddStream(); // const expectedAddStream = m.hot("---------------t-", { // t: mockChangedLink, // }); // m.expect(linkAddStream).toBeObservable(expectedAddStream); // }) // ); // The delete event will not happen from JointJS. // /** // * Test JointJS delete operator `getJointElementCellDeleteStream` event stream handled properly, // * when the operator delete causes its connected links being deleted as well // * // * Add three operators // * Then add a link from operator 1 to operator 2 and a link from operator 2 to operator 3 // * // * addOperators + addLinks: 1 -> 2 -> 3 // * jointDeleteOperator: ---------d-| (delete operator 2) // * jointDeleteLink: ---------(gh)-| (mock event triggered automatically at the same time frame by jointJS) // * // * Expected: // * There will be 2 operators left // * There will be no links left // * Texera Operator Delete stream should emit event when the operator is deleted // * Texera Link Delete Stream should emit event twice when the operator is deleted // * // */ // it( // "should remove an operator and its connected links when that operator is deleted from jointJS", // marbles(m => { // // add operators // texeraGraph.addOperator(mockScanPredicate); // texeraGraph.addOperator(mockSentimentPredicate); // texeraGraph.addOperator(mockResultPredicate); // // // add links // texeraGraph.addLink(mockScanSentimentLink); // texeraGraph.addLink(mockSentimentResultLink); // // // prepare the delete oprator event // const deleteOperatorString = "---------d-|"; // const deleteOperatorValue = { // d: getJointOperatorValue(mockSentimentPredicate.operatorID), // }; // vi.spyOn(jointGraphWrapper, "getJointElementCellDeleteStream").mockReturnValue( // m.hot(deleteOperatorString, deleteOperatorValue) // ); // // /** // * once the operator is deleted, JointJS will automatically delete connected links // * and will trigger delete link events at the same timeframe // */ // const deleteLinkString = "---------(gh)-|"; // const deleteLinkValue = { // g: getJointLinkValue(mockScanSentimentLink), // h: getJointLinkValue(mockSentimentResultLink), // }; // // vi.spyOn(jointGraphWrapper, "getJointLinkCellDeleteStream").mockReturnValue( // m.hot(deleteLinkString, deleteLinkValue) // ); // // // construct texera model // const syncTexeraModel = new SyncTexeraModel( // texeraGraph, // jointGraphWrapper, // new OperatorGroup( // texeraGraph, // jointGraph, // jointGraphWrapper, // TestBed.inject(WorkflowUtilService), // TestBed.inject(JointUIService) // ) // ); // jointGraphWrapper.getJointElementCellDeleteStream().subscribe({ // complete: () => { // expect(texeraGraph.hasOperator(mockSentimentPredicate.operatorID)).toBeFalsy(); // expect(texeraGraph.hasOperator(mockScanPredicate.operatorID)).toBeTruthy(); // expect(texeraGraph.hasOperator(mockResultPredicate.operatorID)).toBeTruthy(); // expect(texeraGraph.getAllOperators().length).toEqual(2); // expect(texeraGraph.hasLinkWithID(mockScanSentimentLink.linkID)).toBeFalsy(); // expect(texeraGraph.hasLinkWithID(mockSentimentResultLink.linkID)).toBeFalsy(); // expect(texeraGraph.getAllLinks().length).toEqual(0); // }, // }); // }) // ); }); ================================================ FILE: frontend/src/app/workspace/service/workflow-graph/model/sync-texera-model.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { OperatorLink } from "../../../types/workflow-common.interface"; import { WorkflowGraph } from "./workflow-graph"; import { JointGraphWrapper } from "./joint-graph-wrapper"; import { filter, map, tap } from "rxjs/operators"; /** * SyncTexeraModel subscribes to the graph change events from JointJS, * then sync the changes to the TexeraGraph: * - link events: link add/delete/change * * For details of handling each JointJS event type, see the comments below for each function. * * For an overview of the services in WorkflowGraphModule, see workflow-graph-design.md * */ export class SyncTexeraModel { constructor( private texeraGraph: WorkflowGraph, private jointGraphWrapper: JointGraphWrapper ) { this.handleJointLinkEvents(); } /** * Handles JointJS link events: * JointJS link events reflect the changes to the link View in the UI. * Workflow link requires the link to have both source and target port to be considered valid. * * Cases where JointJS and Texera link have different semantics: * - When the user drags the link from one port, but not yet to connect to another port, * the link is added in the semantic of JointJS, but not in the semantic of Texera Workflow graph. * - When an invalid link that is not connected to a port disappears, * the link delete event is trigger by JointJS, but the link never existed from Texera's perspective. * - When the user drags and detaches the end of a valid link, the link is disconnected from the target port, * the link change event (not delete) is triggered by JointJS, but the link should be deleted from Texera's graph. * - When the user attaches the end of the link to a target port, * the link change event (not add) is triggered by JointJS, but it should be added to the Texera Graph. * - When the user drags the link around, the link change event will be trigger continuously, * when the target being a changing coordinate. But this event should not have any effect on the Texera Graph. * * To address the disparity of the semantics, the linkAdded / linkDeleted / linkChanged events need to be handled carefully. */ private handleJointLinkEvents(): void { /** * on link cell add: * we need to check if the link is a valid link in Texera's semantic (has both source and target port) * and only add valid links to the graph */ this.jointGraphWrapper .getJointLinkCellAddStream() .pipe( filter(link => this.isValidJointLink(link)), filter(() => this.texeraGraph.getSyncTexeraGraph()), map(link => SyncTexeraModel.getOperatorLink(link)) ) .subscribe(link => this.texeraGraph.addLink(link)); /** * on link cell delete: * we need to first check if the link is a valid link * then delete the link by the link ID */ this.jointGraphWrapper .getJointLinkCellDeleteStream() .pipe( filter(link => this.isValidJointLink(link)), filter(() => this.texeraGraph.getSyncTexeraGraph()), map(link => SyncTexeraModel.getOperatorLink(link)) ) .subscribe(link => this.texeraGraph.deleteLinkWithID(link.linkID)); /** * on link cell change: * link cell change could cause deletion of a link or addition of a link, or simply no effect * TODO: finish this documentation */ this.jointGraphWrapper .getJointLinkCellChangeStream() .pipe( filter(() => this.texeraGraph.getSyncTexeraGraph()), // we intentionally want the side effect (delete the link) to happen **before** other operations in the chain tap(link => { const linkID = link.id.toString(); if (this.texeraGraph.hasLinkWithID(linkID)) { const previousSyncJointGraph = this.texeraGraph.getSyncJointGraph(); this.texeraGraph.setSyncJointGraph(false); this.texeraGraph.deleteLinkWithID(linkID); this.texeraGraph.setSyncJointGraph(previousSyncJointGraph); } }), filter(link => this.isValidJointLink(link)), map(link => SyncTexeraModel.getOperatorLink(link)) ) .subscribe(link => { this.texeraGraph.addLink(link); }); } /** * Determines if a jointJS link is valid (both ends are connected to a port * of operator or are connected to a collapsed group). * If a JointJS link's target is still a point (not connected), it's not considered a valid link. * @param jointLink */ private isValidJointLink(jointLink: joint.dia.Link): boolean { return ( jointLink && jointLink.attributes && jointLink.attributes.source && jointLink.attributes.target && jointLink.attributes.source.id && jointLink.attributes.source.port && jointLink.attributes.target.id && jointLink.attributes.target.port && (this.texeraGraph.hasOperator(jointLink.attributes.source.id.toString()) || this.texeraGraph.hasOperator(jointLink.attributes.target.id.toString())) ); // the above two lines are causing unit test fail in sync-texera-model.spec.ts // since if operator is deleted first the link will become invalid and thus undeletable. } /** * Transforms a JointJS link (joint.dia.Link) to a Texera Link object * The JointJS link must be valid, otherwise an error will be thrown. * @param jointLink */ static getOperatorLink(jointLink: joint.dia.Link): OperatorLink { type jointLinkEndpointType = { id: string; port: string } | null | undefined; // the link should be a valid link (both source and target are connected to an operator) // isValidLink function is not reused because of Typescript strict null checking const jointSourceElement: jointLinkEndpointType = jointLink.attributes.source; const jointTargetElement: jointLinkEndpointType = jointLink.attributes.target; if (!jointSourceElement) { throw new Error("Invalid JointJS Link: no source element"); } if (!jointTargetElement) { throw new Error("Invalid JointJS Link: no target element"); } return { linkID: jointLink.id.toString(), source: { operatorID: jointSourceElement.id, portID: jointSourceElement.port, }, target: { operatorID: jointTargetElement.id, portID: jointTargetElement.port, }, }; } } ================================================ FILE: frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { StubOperatorMetadataService } from "./../../operator-metadata/stub-operator-metadata.service"; import { OperatorMetadataService } from "./../../operator-metadata/operator-metadata.service"; import { JointUIService } from "./../../joint-ui/joint-ui.service"; import { WorkflowGraph } from "./workflow-graph"; import { UndoRedoService } from "./../../undo-redo/undo-redo.service"; import { mockCommentBox, mockFalseResultSentimentLink, mockFalseSentimentScanLink, mockPoint, mockResultPredicate, mockScanPredicate, mockScanResultLink, mockScanSentimentLink, mockSentimentPredicate, mockSentimentResultLink, } from "./mock-workflow-data"; import { inject, TestBed } from "@angular/core/testing"; import { WorkflowActionService } from "./workflow-action.service"; import { OperatorPredicate } from "../../../types/workflow-common.interface"; import { WorkflowUtilService } from "../util/workflow-util.service"; import { commonTestProviders } from "../../../../common/testing/test-utils"; describe("WorkflowActionService", () => { let service: WorkflowActionService; let undoRedo: UndoRedoService; let texeraGraph: WorkflowGraph; let jointGraph: joint.dia.Graph; beforeEach(() => { TestBed.configureTestingModule({ providers: [ WorkflowActionService, WorkflowUtilService, JointUIService, UndoRedoService, { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, ...commonTestProviders, ], imports: [], }); service = TestBed.inject(WorkflowActionService); undoRedo = TestBed.inject(UndoRedoService); texeraGraph = (service as any).texeraGraph; jointGraph = (service as any).jointGraph; }); it("should be created", inject([WorkflowActionService], (injectedService: WorkflowActionService) => { expect(injectedService).toBeTruthy(); })); it("should add an operator to both jointjs and texera graph correctly", () => { service.addOperator(mockScanPredicate, mockPoint); expect(texeraGraph.hasOperator(mockScanPredicate.operatorID)).toBeTruthy(); expect(jointGraph.getCell(mockScanPredicate.operatorID)).toBeTruthy(); }); it("should add commentBox to both jointjs and texera graph correctly", () => { service.addCommentBox(mockCommentBox); expect(texeraGraph.hasCommentBox(mockCommentBox.commentBoxID)).toBeTruthy(); expect(jointGraph.getCell(mockCommentBox.commentBoxID)).toBeTruthy(); }); it("should throw an error when adding an existed operator", () => { service.addOperator(mockScanPredicate, mockPoint); expect(() => { service.addOperator(mockScanPredicate, mockPoint); }).toThrowError(new RegExp("exists")); }); it("should throw an error when adding an operator with invalid operator type", () => { const invalidOperator: OperatorPredicate = { ...mockScanPredicate, operatorType: "invalidOperatorTypeForTesting", }; expect(() => { service.addOperator(invalidOperator, mockPoint); }).toThrowError(new RegExp("invalid")); }); it("should delete an operator to both jointjs and texera graph correctly", () => { service.addOperator(mockScanPredicate, mockPoint); service.deleteOperator(mockScanPredicate.operatorID); expect(texeraGraph.hasOperator(mockScanPredicate.operatorID)).toBeFalsy(); expect(jointGraph.getCell(mockScanPredicate.operatorID)).toBeFalsy(); }); it("should throw an error when trying to delete an non-existing operator", () => { expect(() => { service.deleteOperator(mockScanPredicate.operatorID); }).toThrowError(new RegExp("does not exist|doesn't exist")); }); it("should add a link to both jointjs and texera graph correctly", () => { service.addOperator(mockScanPredicate, mockPoint); service.addOperator(mockResultPredicate, mockPoint); service.addLink(mockScanResultLink); expect(texeraGraph.hasLink(mockScanResultLink.source, mockScanResultLink.target)).toBeTruthy(); expect(texeraGraph.hasLinkWithID(mockScanResultLink.linkID)).toBeTruthy(); expect(jointGraph.getCell(mockScanResultLink.linkID)).toBeTruthy(); }); it("should throw appropriate errors when adding various types of incorrect links", () => { service.addOperator(mockScanPredicate, mockPoint); service.addOperator(mockResultPredicate, mockPoint); service.addLink(mockScanResultLink); // link already exist expect(() => { service.addLink(mockScanResultLink); }).toThrowError(new RegExp("already exists")); const sameLinkDifferentID = { ...mockScanResultLink, linkID: "link-2", }; // same link but different id already exist expect(() => { service.addLink(sameLinkDifferentID); }).toThrowError(new RegExp("exists")); // link's target operator or port doesn't exist expect(() => { service.addLink(mockScanSentimentLink); }).toThrowError(new RegExp("does not exist|doesn't exist")); // link's source operator or port doesn't exist expect(() => { service.addLink(mockSentimentResultLink); }).toThrowError(new RegExp("does not exist|doesn't exist")); // add another operator for tests below service.addOperator(mockSentimentPredicate, mockPoint); // link source portID doesn't exist (no output port for source operator) expect(() => { service.addLink(mockFalseResultSentimentLink); }).toThrowError(new RegExp("on output ports of the source operator")); // link target portID doesn't exist (no input port for target operator) expect(() => { service.addLink(mockFalseSentimentScanLink); }).toThrowError(new RegExp("on input ports of the target operator")); }); it("should delete a link by link ID from both jointjs and texera graph correctly", () => { service.addOperator(mockScanPredicate, mockPoint); service.addOperator(mockResultPredicate, mockPoint); service.addLink(mockScanResultLink); // test delete by link ID service.deleteLinkWithID(mockScanResultLink.linkID); expect(texeraGraph.hasLink(mockScanResultLink.source, mockScanResultLink.target)).toBeFalsy(); expect(texeraGraph.hasLinkWithID(mockScanResultLink.linkID)).toBeFalsy(); expect(jointGraph.getCell(mockScanResultLink.linkID)).toBeFalsy(); }); it("should delete a link by source and target from both jointjs and texera graph correctly", () => { service.addOperator(mockScanPredicate, mockPoint); service.addOperator(mockResultPredicate, mockPoint); service.addLink(mockScanResultLink); // test delete by link source and target service.deleteLink(mockScanResultLink.source, mockScanResultLink.target); expect(texeraGraph.hasLink(mockScanResultLink.source, mockScanResultLink.target)).toBeFalsy(); expect(texeraGraph.hasLinkWithID(mockScanResultLink.linkID)).toBeFalsy(); expect(jointGraph.getCell(mockScanResultLink.linkID)).toBeFalsy(); }); it("should throw an error when trying to delete non-existing link", () => { service.addOperator(mockScanPredicate, mockPoint); service.addOperator(mockResultPredicate, mockPoint); expect(() => { service.deleteLinkWithID(mockScanResultLink.linkID); }).toThrowError(new RegExp("does not exist|doesn't exist")); expect(() => { service.deleteLinkWithID(mockScanResultLink.linkID); }).toThrowError(new RegExp("does not exist|doesn't exist")); }); it("should set operator property to texera graph correctly", () => { service.addOperator(mockScanPredicate, mockPoint); const newProperty = { table: "test-table" }; service.setOperatorProperty(mockScanPredicate.operatorID, newProperty); const operator = texeraGraph.getOperator(mockScanPredicate.operatorID); if (!operator) { throw new Error(`operator ${mockScanPredicate.operatorID} doesn't exist`); } expect(operator.operatorProperties).toEqual(newProperty); }); it("should throw an error when trying to set operator property of an nonexist operator", () => { expect(() => { const newProperty = { table: "test-table" }; service.setOperatorProperty(mockScanPredicate.operatorID, newProperty); }).toThrowError(new RegExp("does not exist|doesn't exist")); }); it("should handle delete an operator causing connected links to be deleted correctly", () => { // add operator scan, sentiment, and result service.addOperator(mockScanPredicate, mockPoint); service.addOperator(mockSentimentPredicate, mockPoint); service.addOperator(mockResultPredicate, mockPoint); // add link scan -> result, and sentiment -> result service.addLink(mockScanResultLink); service.addLink(mockSentimentResultLink); // delete result operator, should cause two links to be deleted as well service.deleteOperator(mockResultPredicate.operatorID); expect(texeraGraph.getAllOperators().length).toEqual(2); expect(texeraGraph.getAllLinks().length).toEqual(0); }); it("should reformat the workflow", () => { service.addOperator(mockScanPredicate, mockPoint); service.addOperator(mockSentimentPredicate, mockPoint); service.addOperator(mockResultPredicate, mockPoint); // add link scan -> result, and sentiment -> result service.addLink(mockScanResultLink); service.addLink(mockSentimentResultLink); service.autoLayoutWorkflow(); // test it's actually reformated let sentimentOpPos = service.getJointGraphWrapper().getElementPosition(mockSentimentPredicate.operatorID); let resultOpPos = service.getJointGraphWrapper().getElementPosition(mockResultPredicate.operatorID); expect(sentimentOpPos).not.toEqual(mockPoint); expect(resultOpPos).not.toEqual(mockPoint); // test undo reformat restoring the original positions expect(undoRedo.canUndo()).toBeTruthy(); // // undoRedo.undoAction(); // sentimentOpPos = service.getJointGraphWrapper().getElementPosition(mockSentimentPredicate.operatorID); // resultOpPos = service.getJointGraphWrapper().getElementPosition(mockResultPredicate.operatorID); // // expect(sentimentOpPos).toEqual(mockPoint); // expect(resultOpPos).toEqual(mockPoint); }); }); ================================================ FILE: frontend/src/app/workspace/service/workflow-graph/model/workflow-action.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import * as joint from "jointjs"; import { BehaviorSubject, merge, Observable, Subject } from "rxjs"; import { ExecutionMode, Workflow, WorkflowContent, WorkflowSettings } from "../../../../common/type/workflow"; import { WorkflowMetadata } from "../../../../dashboard/type/workflow-metadata.interface"; import { Comment, CommentBox, LogicalPort, OperatorLink, OperatorPredicate, Point, PortDescription, } from "../../../types/workflow-common.interface"; import { JointUIService } from "../../joint-ui/joint-ui.service"; import { OperatorMetadataService } from "../../operator-metadata/operator-metadata.service"; import { UndoRedoService } from "../../undo-redo/undo-redo.service"; import { WorkflowUtilService } from "../util/workflow-util.service"; import { JointGraphWrapper } from "./joint-graph-wrapper"; import { SyncTexeraModel } from "./sync-texera-model"; import { WorkflowGraph, WorkflowGraphReadonly } from "./workflow-graph"; import { filter } from "rxjs/operators"; import { isDefined } from "../../../../common/util/predicate"; import { User } from "../../../../common/type/user"; import { SharedModelChangeHandler } from "./shared-model-change-handler"; import { GuiConfigService } from "../../../../common/service/gui-config.service"; export const DEFAULT_WORKFLOW_NAME = "Untitled Workflow"; export const DEFAULT_WORKFLOW = { name: DEFAULT_WORKFLOW_NAME, description: undefined, wid: 0, creationTime: undefined, lastModifiedTime: undefined, isPublished: 0, readonly: false, }; /** * * WorkflowActionService exposes functions (actions) to modify the workflow graph model of Texera, * such as addOperator, deleteOperator, addLink, deleteLink, etc. * * WorkflowActionService bundles a series of steps into atomic actions, like adding an operator and its outgoing link. * It also checks the validity of these actions, for example, throws an error if deleting a nonsexist operator. * * All changes(actions) to the workflow graph should be called through WorkflowActionService, * * With the introduction of shared editing using yjs, WorkflowActionService will only make changes to its internal * {@link WorkflowGraph}, and {@link SharedModelChangeHandler} will listen to changes to the * WorkflowGraph to update JointGraph. * * For an overview of the services and updates with shared editing in WorkflowGraphModule, see workflow-graph-design.md. * */ @Injectable({ providedIn: "root", }) export class WorkflowActionService { private readonly texeraGraph: WorkflowGraph; private readonly jointGraph: joint.dia.Graph; private readonly jointGraphWrapper: JointGraphWrapper; private readonly syncTexeraModel: SyncTexeraModel; private readonly sharedModelChangeHandler: SharedModelChangeHandler; // variable to temporarily hold the current workflow to switch view to a particular version private tempWorkflow?: Workflow; private workflowModificationEnabled = true; private enableModificationStream = new BehaviorSubject(true); private highlightingEnabled = false; private centerPoint: Point = { x: 0, y: 0 }; private workflowMetadata: WorkflowMetadata; private workflowMetadataChangeSubject: Subject = new Subject(); private resultPanelOpenSubject = new Subject(); public readonly resultPanelOpen$: Observable = this.resultPanelOpenSubject.asObservable(); private workflowSettings: WorkflowSettings; private workflowResetSubject = new Subject(); constructor( private operatorMetadataService: OperatorMetadataService, private jointUIService: JointUIService, private undoRedoService: UndoRedoService, private workflowUtilService: WorkflowUtilService, private config: GuiConfigService ) { this.texeraGraph = new WorkflowGraph(); this.jointGraph = new joint.dia.Graph(); this.jointGraphWrapper = new JointGraphWrapper(this.jointGraph); this.syncTexeraModel = new SyncTexeraModel(this.texeraGraph, this.jointGraphWrapper); this.sharedModelChangeHandler = new SharedModelChangeHandler( this.texeraGraph, this.jointGraph, this.jointGraphWrapper, this.jointUIService ); this.sharedModelChangeHandler.setConfigService(this.config); this.workflowMetadata = DEFAULT_WORKFLOW; this.workflowSettings = this.getDefaultSettings(); this.undoRedoService.setUndoManager(this.texeraGraph.sharedModel.undoManager); this.handleJointElementDrag(); } private getDefaultSettings(): WorkflowSettings { return { dataTransferBatchSize: this.config.env.defaultDataTransferBatchSize, executionMode: this.config.env.defaultExecutionMode, }; } /** * Workflow modification lock interface (allows or prevents commands that would modify the workflow graph). */ public enableWorkflowModification() { if (!this.workflowMetadata.readonly && !this.workflowModificationEnabled) { this.workflowModificationEnabled = true; this.enableModificationStream.next(true); this.undoRedoService.enableWorkFlowModification(); } } public disableWorkflowModification() { this.workflowModificationEnabled = false; this.enableModificationStream.next(false); this.undoRedoService.disableWorkFlowModification(); } public checkWorkflowModificationEnabled(): boolean { return this.workflowModificationEnabled; } public getWorkflowModificationEnabledStream(): Observable { return this.enableModificationStream.asObservable(); } /** * Gets joint paper, mainly used for co-editor presence. */ public getJointGraph(): joint.dia.Graph { return this.jointGraph; } /** * Gets the read-only version of the TexeraGraph * to access the properties and event streams. * * Texera Graph contains information about the logical workflow plan of Texera, * such as the types and properties of the operators. */ public getTexeraGraph(): WorkflowGraphReadonly { return this.texeraGraph; } /** * Gets the JointGraph Wrapper, which contains * getter for properties and event streams as RxJS Observables. * * JointJS Graph contains information about the UI, * such as the position of operator elements, and the event of user dragging a cell around. */ public getJointGraphWrapper(): JointGraphWrapper { return this.jointGraphWrapper; } public getCenterPoint(): Point { return this.centerPoint; } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Below are all the actions available. // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Adds an operator to the workflow graph at a point. * Throws an Error if the operator ID already existed in the Workflow Graph. * * @param operator * @param point */ public addOperator(operator: OperatorPredicate, point: Point): void { // turn off multiselect since there's only one operator added this.jointGraphWrapper.setMultiSelectMode(false); // check that the operator doesn't exist this.texeraGraph.assertOperatorNotExists(operator.operatorID); // check that the operator type exists if (!this.operatorMetadataService.operatorTypeExists(operator.operatorType)) { throw new Error(`operator type ${operator.operatorType} is invalid`); } this.texeraGraph.bundleActions(() => { // add operator to texera graph this.texeraGraph.addOperator(operator); this.texeraGraph.sharedModel.elementPositionMap?.set(operator.operatorID, point); }); } /** * Deletes an operator from the workflow graph, also deleting associated links. * Throws an Error if the operator ID doesn't exist in the Workflow Graph. * @param operatorID */ public deleteOperator(operatorID: string): void { this.unhighlightOperators(operatorID); this.texeraGraph.bundleActions(() => { this.getTexeraGraph() .getAllLinks() .filter(link => link.source.operatorID === operatorID || link.target.operatorID === operatorID) .forEach(link => this.deleteLinkWithID(link.linkID)); this.texeraGraph.assertOperatorExists(operatorID); this.texeraGraph.deleteOperator(operatorID); if (this.texeraGraph.sharedModel.elementPositionMap.has(operatorID)) this.texeraGraph.sharedModel.elementPositionMap.delete(operatorID); }); } public addPort(operatorID: string, isInput: boolean, disallowMultiInputs?: boolean): void { const operator = this.texeraGraph.getOperator(operatorID); // TODO: use uniform serde to calculate the portID const prefix = isInput ? "input-" : "output-"; let suffix = isInput ? operator.inputPorts.length : operator.outputPorts.length; let portID = prefix + suffix; // make sure portID has no conflict while (operator.inputPorts.find(p => p.portID === portID) !== undefined) { suffix += 1; portID = prefix + suffix; } const port: PortDescription = { portID, displayName: "", disallowMultiInputs, isDynamicPort: true, dependencies: [], }; if (!operator.dynamicInputPorts && isInput) { throw new Error(`operator ${operatorID} does not have dynamic input ports`); } if (!operator.dynamicOutputPorts && !isInput) { throw new Error(`operator ${operatorID} does not have dynamic output ports`); } if (!isInput && disallowMultiInputs !== undefined) { throw new Error("error: disallowMultiInputs property of an output port should not be specified"); } this.texeraGraph.bundleActions(() => { // add port to the operator this.texeraGraph.assertOperatorExists(operatorID); this.texeraGraph.addPort(operatorID, port, isInput); }); } public removePort(operatorID: string, isInput: boolean): void { this.texeraGraph.bundleActions(() => { this.texeraGraph.assertOperatorExists(operatorID); this.texeraGraph.removePort(operatorID, isInput); }); } /** * Unhighlight currently selected elements and adds a comment box. * @param commentBox */ public addCommentBox(commentBox: CommentBox): void { const currentHighlights = this.jointGraphWrapper.getCurrentHighlights(); this.jointGraphWrapper.unhighlightElements(currentHighlights); this.jointGraphWrapper.setMultiSelectMode(false); this.texeraGraph.bundleActions(() => { this.texeraGraph.addCommentBox({ ...commentBox, comments: [] }); for (const comment of commentBox.comments) { this.addComment(comment, commentBox.commentBoxID); } }); } /** * Adds given operators and links to the workflow graph. * @param operatorsAndPositions * @param links * @param commentBoxes */ public addOperatorsAndLinks( operatorsAndPositions: readonly { op: OperatorPredicate; pos: Point }[], links?: readonly OperatorLink[], commentBoxes?: ReadonlyArray ): void { // remember currently highlighted operators and groups const currentHighlights = this.jointGraphWrapper.getCurrentHighlights(); // unhighlight previous highlights this.jointGraphWrapper.unhighlightElements(currentHighlights); this.jointGraphWrapper.setMultiSelectMode(operatorsAndPositions.length > 1); this.texeraGraph.bundleActions(() => { for (const operatorsAndPosition of operatorsAndPositions) { this.addOperator(operatorsAndPosition.op, operatorsAndPosition.pos); } if (links) { for (let i = 0; i < links.length; i++) { this.addLink(links[i]); } } if (isDefined(commentBoxes)) { commentBoxes.forEach(commentBox => this.addCommentBox(commentBox)); } }); } /** * Deletes a comment box. * @param commentBoxID */ public deleteCommentBox(commentBoxID: string): void { this.texeraGraph.assertCommentBoxExists(commentBoxID); this.texeraGraph.deleteCommentBox(commentBoxID); } /** * Deletes given operators and links from the workflow graph. * @param operatorIDs */ public deleteOperatorsAndLinks(operatorIDs: readonly string[]): void { const operatorIDsCopy = Array.from(new Set(operatorIDs)); this.texeraGraph.bundleActions(() => { // delete links related to the deleted operator this.getTexeraGraph() .getAllLinks() .filter( link => operatorIDsCopy.includes(link.source.operatorID) || operatorIDsCopy.includes(link.target.operatorID) ) .forEach(link => this.deleteLinkWithID(link.linkID)); operatorIDsCopy.forEach(operatorID => { this.deleteOperator(operatorID); }); }); } /** * Handles the auto layout function * */ // Originally: drag Operator public autoLayoutWorkflow(): void { // This also changes element positions, but we handle this separately. this.texeraGraph.bundleActions(() => { this.undoRedoService.setListenJointCommand(false); this.jointGraphWrapper.autoLayoutJoint(); for (const operator of this.texeraGraph.getAllOperators()) { const operatorID = operator.operatorID; const newPosition = this.jointGraphWrapper.getElementPosition(operatorID); if (this.texeraGraph.sharedModel.elementPositionMap.get(operatorID) !== newPosition) { this.texeraGraph.sharedModel.elementPositionMap.set(operatorID, newPosition); } } for (const commentBox of this.texeraGraph.getAllCommentBoxes()) { const commentBoxID = commentBox.commentBoxID; const newPosition = this.jointGraphWrapper.getElementPosition(commentBoxID); if (this.texeraGraph.sharedModel.elementPositionMap.get(commentBoxID) !== newPosition) { this.texeraGraph.sharedModel.elementPositionMap.set(commentBoxID, newPosition); } } this.undoRedoService.setListenJointCommand(true); }); } /** * Calculating the top-left (minimum x and y) position of all operators */ public calculateTopLeftOperatorPosition(): void { this.texeraGraph.bundleActions(() => { this.undoRedoService.setListenJointCommand(false); const allOperators = this.getTexeraGraph().getAllOperators(); if (allOperators.length === 0) return; let minX = Infinity; let minY = Infinity; for (const operator of allOperators) { const operatorID = operator.operatorID; const position = this.jointGraphWrapper.getElementPosition(operatorID); if (position.x < minX) { minX = position.x; } if (position.y < minY) { minY = position.y; } } this.centerPoint = { x: minX, y: minY }; this.undoRedoService.setListenJointCommand(true); }); } /** * Adds a link to the workflow graph * Throws an Error if the link ID or the link with same source and target already exists. * @param link */ public addLink(link: OperatorLink): void { this.texeraGraph.assertLinkNotExists(link); this.texeraGraph.assertLinkIsValid(link); this.texeraGraph.addLink(link); } /** * Deletes a link with the linkID from the workflow graph * Throws an Error if the linkID doesn't exist in the workflow graph. * @param linkID */ public deleteLinkWithID(linkID: string): void { this.texeraGraph.assertLinkWithIDExists(linkID); this.unhighlightLinks(linkID); this.texeraGraph.deleteLinkWithID(linkID); } /** * Deletes a link based on the source and target port. * @param source * @param target */ public deleteLink(source: LogicalPort, target: LogicalPort): void { const link = this.getTexeraGraph().getLink(source, target); this.deleteLinkWithID(link.linkID); } /** * Replaces the property object with a new one. This is a coarse-grained method for shared-editing. * @param operatorID * @param newProperty */ public setOperatorProperty(operatorID: string, newProperty: object): void { this.texeraGraph.bundleActions(() => { this.texeraGraph.setOperatorProperty(operatorID, newProperty); }); } public setPortProperty(operatorPortID: LogicalPort, newProperty: object) { this.texeraGraph.bundleActions(() => { this.texeraGraph.setPortProperty(operatorPortID, newProperty); }); } public addComment(comment: Comment, commentBoxID: string): void { this.texeraGraph.bundleActions(() => { this.texeraGraph.addCommentToCommentBox(comment, commentBoxID); }); } public deleteComment(creatorID: number, creationTime: string, commentBoxID: string): void { this.texeraGraph.bundleActions(() => { this.texeraGraph.deleteCommentFromCommentBox(creatorID, creationTime, commentBoxID); }); } public editComment(creatorID: number, creationTime: string, commentBoxID: string, newContent: string): void { this.texeraGraph.bundleActions(() => { this.texeraGraph.editCommentInCommentBox(creatorID, creationTime, commentBoxID, newContent); }); } public highlightOperators(multiSelect: boolean, ...ops: string[]): void { this.getJointGraphWrapper().setMultiSelectMode(multiSelect); this.getJointGraphWrapper().highlightOperators(...ops); this.getTexeraGraph().updateSharedModelAwareness( "highlighted", this.jointGraphWrapper.getCurrentHighlightedOperatorIDs() ); } public unhighlightOperators(...ops: string[]): void { this.getJointGraphWrapper().unhighlightOperators(...ops); this.getTexeraGraph().updateSharedModelAwareness( "highlighted", this.jointGraphWrapper.getCurrentHighlightedOperatorIDs() ); } public highlightLinks(multiSelect: boolean, ...links: string[]): void { this.getJointGraphWrapper().setMultiSelectMode(multiSelect); this.getJointGraphWrapper().highlightLinks(...links); } public unhighlightLinks(...links: string[]): void { this.getJointGraphWrapper().unhighlightLinks(...links); } public highlightCommentBoxes(multiSelect: boolean, ...commentBoxIDs: string[]): void { this.getJointGraphWrapper().setMultiSelectMode(multiSelect); this.getJointGraphWrapper().highlightCommentBoxes(...commentBoxIDs); } public highlightElements(multiSelect: boolean, ...elementIDs: string[]): void { this.getJointGraphWrapper().setMultiSelectMode(multiSelect); this.highlightOperators(multiSelect, ...elementIDs.filter(id => this.texeraGraph.hasOperator(id))); this.highlightLinks(multiSelect, ...elementIDs.filter(id => this.texeraGraph.hasLinkWithID(id))); this.highlightCommentBoxes(multiSelect, ...elementIDs.filter(id => this.texeraGraph.hasCommentBox(id))); } public highlightPorts(multiSelect: boolean, ...ports: LogicalPort[]): void { this.getJointGraphWrapper().setMultiSelectMode(multiSelect); this.getJointGraphWrapper().highlightPorts(...ports); } public unhighlightPorts(...ports: LogicalPort[]): void { this.getJointGraphWrapper().unhighlightPorts(...ports); } public disableOperators(ops: readonly string[]): void { this.texeraGraph.bundleActions(() => { ops.forEach(op => { this.getTexeraGraph().disableOperator(op); }); }); } public enableOperators(ops: readonly string[]): void { this.texeraGraph.bundleActions(() => { ops.forEach(op => { this.getTexeraGraph().enableOperator(op); }); }); } public markReuseResults(ops: readonly string[]): void { this.texeraGraph.bundleActions(() => { ops.forEach(op => { this.getTexeraGraph().markReuseResult(op); }); }); } public removeMarkReuseResults(ops: readonly string[]): void { this.texeraGraph.bundleActions(() => { ops.forEach(op => { this.getTexeraGraph().removeMarkReuseResult(op); }); }); } public setViewOperatorResults(ops: readonly string[]): void { this.texeraGraph.bundleActions(() => { ops.forEach(op => { this.getTexeraGraph().setViewOperatorResult(op); }); }); } public unsetViewOperatorResults(ops: readonly string[]): void { this.texeraGraph.bundleActions(() => { ops.forEach(op => { this.getTexeraGraph().unsetViewOperatorResult(op); }); }); } public setOperatorVersion(operatorId: string, newVersion: string): void { this.getTexeraGraph().changeOperatorVersion(operatorId, newVersion); } public openResultPanel(): void { this.resultPanelOpenSubject.next(true); } public closeResultPanel(): void { this.resultPanelOpenSubject.next(false); } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Below are workflow-level and metadata-related methods. // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Refreshes the internal shared model and joins a new shared-editing room. * * This method also updates the undo manager. * @param workflowId optional, but needed if you want to join shared editing. * @param user optional, but needed if you want to have user presence. */ public setNewSharedModel(workflowId?: number, user?: User) { this.texeraGraph.loadNewYModel(workflowId, user, this.config.env.productionSharedEditingServer); this.undoRedoService.setUndoManager(this.texeraGraph.sharedModel.undoManager); } /** * Destroys shared-editing related structures and quits the shared editing session. */ public destroySharedModel(): void { this.texeraGraph.destroyYModel(); } /** * Reload the given workflow, update workflowMetadata and workflowContent. * This method is based on the assumption that this is on a new SharedModel. * * Warning: this resets the workflow but not the SharedModel, so make sure to quit the shared-editing session * ({@link destroySharedModel}) before using this method. */ public reloadWorkflow( workflow: Readonly | undefined, asyncRendering = this.config.env.asyncRenderingEnabled, restoreViewport = true ): void { this.jointGraphWrapper.setReloadingWorkflow(true); this.jointGraphWrapper.jointGraphContext.withContext({ async: asyncRendering }, () => { this.setWorkflowMetadata(workflow); // remove the existing operators on the paper currently this.deleteOperatorsAndLinks( this.getTexeraGraph() .getAllOperators() .map(op => op.operatorID) ); this.getTexeraGraph() .getAllCommentBoxes() .forEach(commentBox => this.deleteCommentBox(commentBox.commentBoxID)); this.jointGraphWrapper.jointGraph.clear(); if (workflow === undefined) { this.setNewSharedModel(); return; } const workflowContent: WorkflowContent = workflow.content; this.workflowSettings = workflowContent.settings || this.getDefaultSettings(); let operatorsAndPositions: { op: OperatorPredicate; pos: Point }[] = []; workflowContent.operators.forEach(op => { const opPosition = workflowContent.operatorPositions[op.operatorID]; if (!opPosition) { throw new Error(`position error: ${op.operatorID}`); } operatorsAndPositions.push({ op: op, pos: opPosition }); }); const links: OperatorLink[] = workflowContent.links; const commentBoxes = workflowContent.commentBoxes; operatorsAndPositions = this.updateOperatorVersions(operatorsAndPositions); this.addOperatorsAndLinks(operatorsAndPositions, links, commentBoxes); // restore the view point if (restoreViewport) { this.getJointGraphWrapper().restoreDefaultZoomAndOffset(); } }); this.jointGraphWrapper.setReloadingWorkflow(false); // After reloading a workflow, need to clear undo/redo stacks because some of the actions involved in reloading // may remain in the undo manager. this.undoRedoService.clearUndoStack(); this.undoRedoService.clearRedoStack(); } public workflowChanged(): Observable { return merge( this.getTexeraGraph().getOperatorAddStream(), this.getTexeraGraph().getOperatorDeleteStream(), this.getTexeraGraph().getLinkAddStream(), this.getTexeraGraph().getLinkDeleteStream(), this.getTexeraGraph().getPortAddedOrDeletedStream(), this.getTexeraGraph().getOperatorPropertyChangeStream(), this.getTexeraGraph().getBreakpointChangeStream(), this.getJointGraphWrapper().getElementPositionChangeEvent(), this.getTexeraGraph().getDisabledOperatorsChangedStream(), this.getTexeraGraph().getCommentBoxAddStream(), this.getTexeraGraph().getCommentBoxDeleteStream(), this.getTexeraGraph().getCommentBoxAddCommentStream(), this.getTexeraGraph().getCommentBoxDeleteCommentStream(), this.getTexeraGraph().getCommentBoxEditCommentStream(), this.getTexeraGraph().getViewResultOperatorsChangedStream(), this.getTexeraGraph().getReuseCacheOperatorsChangedStream(), this.getTexeraGraph().getOperatorDisplayNameChangedStream(), this.getTexeraGraph().getOperatorVersionChangedStream(), this.getTexeraGraph().getPortDisplayNameChangedSubject(), this.getTexeraGraph().getPortPropertyChangedStream(), this.workflowResetSubject.asObservable() ); } public workflowMetaDataChanged(): Observable { return this.workflowMetadataChangeSubject.asObservable(); } /** * This is not included in shared editing. * @param workflowMetaData */ public setWorkflowMetadata(workflowMetaData: WorkflowMetadata | undefined): void { if (this.workflowMetadata === workflowMetaData) { return; } const newMetadata = workflowMetaData === undefined ? DEFAULT_WORKFLOW : workflowMetaData; this.workflowMetadata = newMetadata; this.workflowMetadataChangeSubject.next(newMetadata); } public setWorkflowSettings(workflowSettings: WorkflowSettings | undefined): void { if (this.workflowSettings === workflowSettings) { return; } const newSettings = workflowSettings === undefined ? this.getDefaultSettings() : workflowSettings; this.workflowSettings = newSettings; } public getWorkflowSettings(): WorkflowSettings { return this.workflowSettings; } public getWorkflowMetadata(): WorkflowMetadata { return this.workflowMetadata; } public getWorkflowContent(): WorkflowContent { // collect workflow content const texeraGraph = this.getTexeraGraph(); const operators = texeraGraph.getAllOperators(); const links = texeraGraph.getAllLinks(); const operatorPositions: { [key: string]: Point } = {}; const commentBoxes = texeraGraph.getAllCommentBoxes(); const settings = this.workflowSettings; texeraGraph .getAllOperators() .forEach( op => (operatorPositions[op.operatorID] = this.texeraGraph.sharedModel.elementPositionMap?.get( op.operatorID ) as Point) ); return { operators, operatorPositions, links, commentBoxes, settings, }; } public getWorkflow(): Workflow { return { ...this.workflowMetadata, ...{ content: this.getWorkflowContent() }, }; } /** * Used for previewing a version. Will clean-up shared editing session before doing so. * @param workflow */ public setTempWorkflow(workflow: Workflow): void { if (this.texeraGraph.sharedModel.wsProvider.shouldConnect) { this.texeraGraph.sharedModel.wsProvider.disconnect(); } this.tempWorkflow = workflow; } /** * Used for ending version preview. Will re-connect to shared editing session after doing so. */ public resetTempWorkflow(): void { this.tempWorkflow = undefined; this.texeraGraph.sharedModel.wsProvider.connect(); } public getTempWorkflow(): Workflow | undefined { return this.tempWorkflow; } /** * This is not included in shared editing. * @param name */ public setWorkflowName(name: string): void { const newName = name.trim().length > 0 ? name : DEFAULT_WORKFLOW_NAME; this.setWorkflowMetadata({ ...this.workflowMetadata, name: newName }); } public setWorkflowDataTransferBatchSize(size: number): void { if (size > 0 && size != null) { this.setWorkflowSettings({ ...this.workflowSettings, dataTransferBatchSize: size }); } } public updateExecutionMode(mode: ExecutionMode): void { this.setWorkflowSettings({ ...this.workflowSettings, executionMode: mode }); } public clearWorkflow(): void { this.destroySharedModel(); this.setWorkflowMetadata(undefined); this.setWorkflowSettings(undefined); this.reloadWorkflow(undefined); this.setHighlightingEnabled(false); } public setWorkflowIsPublished(newPublishState: number): void { this.setWorkflowMetadata({ ...this.workflowMetadata, isPublished: newPublishState }); } /** * Need to quit shared-editing room at first. */ public resetAsNewWorkflow() { this.destroySharedModel(); this.reloadWorkflow(undefined); this.workflowResetSubject.next(); } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Below are private methods. // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Subscribes to element position changes from joint graph and updates them in TexeraGraph. * * Also subscribes to element position change event stream, * checks if the element (operator) is moved by user and * if the moved element is currently highlighted, * if it is, moves other highlighted elements (operators) along with it, * links will automatically move with operators. * * The subscriptions need and only need to be initiated once, * unlike observers in {@link SharedModelChangeHandler}. * @private */ private handleJointElementDrag(): void { this.jointGraphWrapper .getElementPositionChangeEvent() .pipe( filter(() => this.jointGraphWrapper.getListenPositionChange()), filter(() => this.undoRedoService.listenJointCommand), filter(() => this.texeraGraph.getSyncTexeraGraph()), filter(movedElement => this.jointGraphWrapper .getCurrentHighlightedOperatorIDs() .concat(this.jointGraphWrapper.getCurrentHighlightedCommentBoxIDs()) .includes(movedElement.elementID) ) ) .subscribe(movedElement => { this.texeraGraph.bundleActions(() => { if ( this.texeraGraph.sharedModel.elementPositionMap.get(movedElement.elementID) !== movedElement.newPosition ) { // For syncing ops/comment boxes in shared editing this.texeraGraph.sharedModel.elementPositionMap.set(movedElement.elementID, movedElement.newPosition); // For moving all highlighted operators const selectedElements = this.jointGraphWrapper .getCurrentHighlightedOperatorIDs() .concat(this.jointGraphWrapper.getCurrentHighlightedCommentBoxIDs()); const offsetX = movedElement.newPosition.x - movedElement.oldPosition.x; const offsetY = movedElement.newPosition.y - movedElement.oldPosition.y; this.jointGraphWrapper.setListenPositionChange(false); this.undoRedoService.setListenJointCommand(false); // Persistence and shared-editing syncing for comment boxes have different interfaces. // Setting positions inside commentBoxes here only for persistence. // Syncing uses elementPositionMap. selectedElements .filter(elementID => elementID.includes("commentBox")) .forEach(elementID => { this.texeraGraph.sharedModel.commentBoxMap .get(elementID) ?.set("commentBoxPosition", this.jointGraphWrapper.getElementPosition(elementID)); }); // Move other highlighted operators. selectedElements .filter(elementID => elementID !== movedElement.elementID) .forEach(elementID => { this.jointGraphWrapper.setElementPosition(elementID, offsetX, offsetY); this.texeraGraph.sharedModel.elementPositionMap.set( elementID, this.jointGraphWrapper.getElementPosition(elementID) ); }); this.jointGraphWrapper.setListenPositionChange(true); this.undoRedoService.setListenJointCommand(true); } }); }); } private updateOperatorVersions(operatorsAndPositions: { op: OperatorPredicate; pos: Point }[]) { const updatedOperators: { op: OperatorPredicate; pos: Point }[] = []; for (const operatorsAndPosition of operatorsAndPositions) { updatedOperators.push({ op: this.workflowUtilService.updateOperatorVersion(operatorsAndPosition.op), pos: operatorsAndPosition.pos, }); } return updatedOperators; } public setHighlightingEnabled(enabled: boolean): void { this.highlightingEnabled = enabled; } public getHighlightingEnabled() { return this.highlightingEnabled; } } ================================================ FILE: frontend/src/app/workspace/service/workflow-graph/model/workflow-graph.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { mockResultPredicate, mockScanPredicate, mockScanResultLink, mockScanSentimentLink, mockSentimentPredicate, mockSentimentResultLink, } from "./mock-workflow-data"; import { WorkflowGraph } from "./workflow-graph"; describe("WorkflowGraph", () => { let workflowGraph: WorkflowGraph; beforeEach(() => { workflowGraph = new WorkflowGraph(); }); it("should have an empty graph from the beginning", () => { expect(workflowGraph.getAllOperators().length).toEqual(0); expect(workflowGraph.getAllLinks().length).toEqual(0); }); it("should load an existing graph properly", () => { workflowGraph = new WorkflowGraph( [mockScanPredicate, mockSentimentPredicate, mockResultPredicate], [mockScanSentimentLink, mockSentimentResultLink] ); expect(workflowGraph.getAllOperators().length).toEqual(3); expect(workflowGraph.getAllLinks().length).toEqual(2); }); it("should add an operator and get it properly", () => { workflowGraph.addOperator(mockScanPredicate); expect(workflowGraph.getOperator(mockScanPredicate.operatorID)).toBeTruthy(); expect(workflowGraph.getAllOperators().length).toEqual(1); expect(workflowGraph.getAllOperators()[0]).toEqual(mockScanPredicate); }); it("should return undefined when get an operator with a nonexist operator ID", () => { expect(() => { workflowGraph.getOperator("nonexist"); }).toThrowError(new RegExp("does not exist")); }); it("should throw an error when trying to add an operator with an existing operator ID", () => { expect(() => { workflowGraph.addOperator(mockScanPredicate); workflowGraph.addOperator(mockScanPredicate); }).toThrowError(new RegExp("already exists")); }); it("should delete an operator properly", () => { workflowGraph.addOperator(mockScanPredicate); workflowGraph.deleteOperator(mockScanPredicate.operatorID); expect(workflowGraph.getAllOperators().length).toBe(0); }); it("should throw an error when tring to delete an operator that doesn't exist", () => { expect(() => { workflowGraph.deleteOperator("nonexist"); }).toThrowError(new RegExp("does not exist")); }); it("should add and get a link properly", () => { workflowGraph.addOperator(mockScanPredicate); workflowGraph.addOperator(mockResultPredicate); workflowGraph.addLink(mockScanResultLink); expect(workflowGraph.getLinkWithID(mockScanResultLink.linkID)).toEqual(mockScanResultLink); expect(workflowGraph.getLink(mockScanResultLink.source, mockScanResultLink.target)).toEqual(mockScanResultLink); expect(workflowGraph.getAllLinks().length).toEqual(1); }); it("should throw an error when try to add a link with an existingID", () => { workflowGraph.addOperator(mockScanPredicate); workflowGraph.addOperator(mockResultPredicate); workflowGraph.addOperator(mockSentimentPredicate); workflowGraph.addLink(mockScanResultLink); // create a mock link with modified target const mockLink = { ...mockScanResultLink, target: { operatorID: mockSentimentPredicate.operatorID, portID: mockSentimentPredicate.inputPorts[0].portID, }, }; expect(() => { workflowGraph.addLink(mockLink); }).toThrowError(new RegExp("already exists")); }); it("should throw an error when try to add a link with exising source and target but different ID", () => { workflowGraph.addOperator(mockScanPredicate); workflowGraph.addOperator(mockResultPredicate); workflowGraph.addOperator(mockSentimentPredicate); workflowGraph.addLink(mockScanResultLink); // create a mock link with modified ID const mockLink = { ...mockScanResultLink, linkID: "new-link-id", }; expect(() => { workflowGraph.addLink(mockLink); }).toThrowError(new RegExp("already exists")); }); it("should return undefined when tring to get a nonexist link by link ID", () => { expect(() => { workflowGraph.getLinkWithID("nonexist"); }).toThrowError(new RegExp("does not exist")); }); it("should throw an error when tring to get a nonexist link by link source and target", () => { expect(() => { workflowGraph.getLink( { operatorID: "source", portID: "source port" }, { operatorID: "target", portID: "taret port" } ); }).toThrowError(new RegExp("does not exist")); }); it("should delete a link by ID properly", () => { workflowGraph.addOperator(mockScanPredicate); workflowGraph.addOperator(mockResultPredicate); workflowGraph.addLink(mockScanResultLink); workflowGraph.deleteLinkWithID(mockScanResultLink.linkID); expect(workflowGraph.getAllLinks().length).toEqual(0); }); it("should delete a link by source and target properly", () => { workflowGraph.addOperator(mockScanPredicate); workflowGraph.addOperator(mockResultPredicate); workflowGraph.addLink(mockScanResultLink); workflowGraph.deleteLink(mockScanResultLink.source, mockScanResultLink.target); expect(workflowGraph.getAllLinks().length).toEqual(0); }); it("should throw an error when trying to delete a link (by ID) that doesn't exist", () => { expect(() => { workflowGraph.deleteLinkWithID(mockScanResultLink.linkID); }).toThrowError(new RegExp("does not exist")); }); it("should throw an error when trying to delete a link (by source and target) that doesn't exist", () => { expect(() => { workflowGraph.deleteLink( { operatorID: "source", portID: "source port" }, { operatorID: "target", portID: "taret port" } ); }).toThrowError(new RegExp("does not exist")); }); it("should set the operator property(attributes) properly", () => { workflowGraph.addOperator(mockScanPredicate); const testProperty = { tableName: "testTable" }; workflowGraph.setOperatorProperty(mockScanPredicate.operatorID, testProperty); const operator = workflowGraph.getOperator(mockScanPredicate.operatorID); if (!operator) { throw new Error("test fails: operator is undefined"); } expect(operator.operatorProperties).toEqual(testProperty); }); it("should throw an error when trying to set the property of an nonexist operator", () => { expect(() => { const testProperty = { tableName: "testTable" }; workflowGraph.setOperatorProperty(mockScanPredicate.operatorID, testProperty); }).toThrowError(new RegExp("doesn't exist")); }); it("it should get input links of the certain operator correctly", () => { workflowGraph.addOperator(mockScanPredicate); workflowGraph.addOperator(mockResultPredicate); workflowGraph.addLink(mockScanResultLink); expect(workflowGraph.getInputLinksByOperatorId("3").length).toEqual(1); }); it("it should get output links of the certain operator correctly", () => { workflowGraph.addOperator(mockScanPredicate); workflowGraph.addOperator(mockResultPredicate); workflowGraph.addLink(mockScanResultLink); expect(workflowGraph.getOutputLinksByOperatorId("1").length).toEqual(1); }); it("should disable and enable an operator", () => { workflowGraph.addOperator(mockScanPredicate); workflowGraph.addOperator(mockResultPredicate); workflowGraph.disableOperator(mockScanPredicate.operatorID); expect(workflowGraph.isOperatorDisabled(mockScanPredicate.operatorID)).toBe(true); expect(workflowGraph.isOperatorDisabled(mockResultPredicate.operatorID)).toBe(false); expect(workflowGraph.getDisabledOperators().size).toEqual(1); workflowGraph.enableOperator(mockScanPredicate.operatorID); expect(workflowGraph.isOperatorDisabled(mockScanPredicate.operatorID)).toBe(false); expect(workflowGraph.getDisabledOperators().size).toEqual(0); }); it("should calculate if link is disabled based on the disabled operator", () => { workflowGraph.addOperator(mockScanPredicate); workflowGraph.addOperator(mockResultPredicate); workflowGraph.addLink(mockScanResultLink); workflowGraph.disableOperator(mockScanPredicate.operatorID); expect(workflowGraph.isLinkEnabled(mockScanResultLink.linkID)).toBe(false); expect(workflowGraph.getAllEnabledLinks().length).toEqual(0); }); it("should set and un-set viewing result status of an operator", () => { workflowGraph.addOperator(mockScanPredicate); workflowGraph.addOperator(mockResultPredicate); workflowGraph.setViewOperatorResult(mockScanPredicate.operatorID); expect(workflowGraph.isViewingResult(mockScanPredicate.operatorID)).toBe(true); expect(workflowGraph.isViewingResult(mockResultPredicate.operatorID)).toBe(false); expect(workflowGraph.getOperatorsToViewResult().size).toEqual(1); workflowGraph.unsetViewOperatorResult(mockScanPredicate.operatorID); expect(workflowGraph.isViewingResult(mockScanPredicate.operatorID)).toBe(false); expect(workflowGraph.getDisabledOperators().size).toEqual(0); }); it("should ignore set the view result of the sink operator", () => { workflowGraph.addOperator(mockScanPredicate); workflowGraph.addOperator(mockResultPredicate); workflowGraph.setViewOperatorResult(mockResultPredicate.operatorID); expect(workflowGraph.isViewingResult(mockScanPredicate.operatorID)).toBe(false); expect(workflowGraph.isViewingResult(mockResultPredicate.operatorID)).toBe(false); expect(workflowGraph.getOperatorsToViewResult().size).toEqual(0); workflowGraph.unsetViewOperatorResult(mockResultPredicate.operatorID); expect(workflowGraph.isViewingResult(mockResultPredicate.operatorID)).toBe(false); expect(workflowGraph.getOperatorsToViewResult().size).toEqual(0); }); }); ================================================ FILE: frontend/src/app/workspace/service/workflow-graph/model/workflow-graph.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Observable, Subject } from "rxjs"; import { BreakpointInfo, Comment, CommentBox, LogicalPort, OperatorLink, OperatorPredicate, PartitionInfo, PortDescription, PortProperty, } from "../../../types/workflow-common.interface"; import { isEqual } from "lodash-es"; import { SharedModel } from "./shared-model"; import { CoeditorState, User } from "../../../../common/type/user"; import { createYTypeFromObject, updateYTypeFromObject, YType } from "../../../types/shared-editing.interface"; import { Awareness } from "y-protocols/awareness"; import * as Y from "yjs"; // define the restricted methods that could change the graph type restrictedMethods = | "sharedModel" | "newYDocLoadedSubject" | "addOperator" | "deleteOperator" | "addLink" | "deleteLink" | "deleteLinkWithID" | "setOperatorProperty" | "addPort" | "removePort" | "setLinkBreakpoint" | "operatorAddSubject" | "operatorDeleteSubject" | "operatorDisplayNameChangedSubject" | "linkAddSubject" | "linkDeleteSubject" | "operatorPropertyChangeSubject" | "breakpointChangeStream" | "commentBoxAddSubject" | "commentBoxDeleteSubject" | "commentBoxAddCommentSubject" | "commentBoxDeleteCommentSubject" | "commentBoxEditCommentSubject"; /** * WorkflowGraphReadonly is a type that only contains the readonly methods of WorkflowGraph. * * Methods that could alter the graph: add/delete operator or link, set operator property * are omitted from this type. */ export type WorkflowGraphReadonly = Omit; type OperatorPropertiesType = Readonly<{ [key: string]: any }>; export const PYTHON_UDF_V2_OP_TYPE = "PythonUDFV2"; export const PYTHON_UDF_SOURCE_V2_OP_TYPE = "PythonUDFSourceV2"; export const DUAL_INPUT_PORTS_PYTHON_UDF_V2_OP_TYPE = "DualInputPortsPythonUDFV2"; export const VIEW_RESULT_OP_TYPE = "SimpleSink"; export const VIEW_RESULT_OP_NAME = "View Results"; export function isSink(operator: OperatorPredicate): boolean { return operator.operatorType.toLocaleLowerCase().includes("sink"); } export function isPythonUdf(operator: OperatorPredicate): boolean { return [PYTHON_UDF_V2_OP_TYPE, PYTHON_UDF_SOURCE_V2_OP_TYPE, DUAL_INPUT_PORTS_PYTHON_UDF_V2_OP_TYPE].includes( operator.operatorType ); } /** * WorkflowGraph represents the Texera's logical WorkflowGraph, * it's a graph consisted of operators and links , * each operator and link has its own unique ID. * */ export class WorkflowGraph { public sharedModel!: SharedModel; public newYDocLoadedSubject = new Subject(); private readonly centerEventSubject = new Subject(); public readonly operatorAddSubject = new Subject(); public readonly operatorDeleteSubject = new Subject<{ deletedOperatorID: string; }>(); public readonly disabledOperatorChangedSubject = new Subject<{ newDisabled: string[]; newEnabled: string[]; }>(); public readonly viewResultOperatorChangedSubject = new Subject<{ newViewResultOps: string[]; newUnviewResultOps: string[]; }>(); public readonly reuseOperatorChangedSubject = new Subject<{ newReuseCacheOps: string[]; newUnreuseCacheOps: string[]; }>(); public readonly operatorDisplayNameChangedSubject = new Subject<{ operatorID: string; newDisplayName: string; }>(); public readonly linkAddSubject = new Subject(); public readonly linkDeleteSubject = new Subject<{ deletedLink: OperatorLink; }>(); public readonly operatorVersionChangedSubject = new Subject<{ operatorID: string; newOperatorVersion: string; }>(); public readonly operatorPropertyChangeSubject = new Subject<{ operator: OperatorPredicate; }>(); public readonly breakpointChangeStream = new Subject<{ oldBreakpoint: object | undefined; linkID: string; }>(); public readonly portAddedOrDeletedSubject = new Subject<{ newOperator: OperatorPredicate; }>(); public readonly commentBoxAddSubject = new Subject(); public readonly commentBoxDeleteSubject = new Subject<{ deletedCommentBox: CommentBox }>(); public readonly commentBoxAddCommentSubject = new Subject<{ addedComment: Comment; commentBox: CommentBox }>(); public readonly commentBoxDeleteCommentSubject = new Subject<{ commentBox: CommentBox }>(); public readonly commentBoxEditCommentSubject = new Subject<{ commentBox: CommentBox }>(); public readonly portDisplayNameChangedSubject = new Subject<{ operatorID: string; portID: string; newDisplayName: string; }>(); public readonly portPropertyChangedSubject = new Subject<{ operatorPortID: LogicalPort; newProperty: PortProperty; }>(); private syncTexeraGraph = true; private syncJointGraph = true; constructor( operatorPredicates: OperatorPredicate[] = [], operatorLinks: OperatorLink[] = [], commentBoxes: CommentBox[] = [] ) { // Initialize sharedModel in constructor to ensure config is loaded this.sharedModel = new SharedModel(); operatorPredicates.forEach(op => this.sharedModel.operatorIDMap.set(op.operatorID, createYTypeFromObject(op))); operatorLinks.forEach(link => this.sharedModel.operatorLinkMap.set(link.linkID, link)); commentBoxes.forEach(commentBox => this.sharedModel.commentBoxMap.set(commentBox.commentBoxID, createYTypeFromObject(commentBox)) ); this.newYDocLoadedSubject.next(undefined); } /** * Returns the boolean value that indicates whether * or not sync JointJS changes to texera graph. */ public getSyncTexeraGraph(): boolean { return this.syncTexeraGraph; } public setSyncJointGraph(syncJointGraph: boolean): void { this.syncJointGraph = syncJointGraph; } public getSyncJointGraph(): boolean { return this.syncJointGraph; } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Below are shared-editing-related methods. // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Exposes a shared operator type for fine-grained control. Do not use if not familiar with yjs. * @param operatorID */ public getSharedOperatorType(operatorID: string): YType { this.assertOperatorExists(operatorID); return this.sharedModel.operatorIDMap.get(operatorID) as YType; } public getSharedOperatorPropertyType(operatorID: string): YType { return this.getSharedOperatorType(operatorID).get("operatorProperties") as YType; } public getSharedPortDescriptionType(operatorPortID: LogicalPort): YType | undefined { const isInput = operatorPortID?.portID.includes("input"); const portListObject = isInput ? this.getOperator(operatorPortID.operatorID).inputPorts : this.getOperator(operatorPortID.operatorID).outputPorts; const portIdx = portListObject.findIndex(portDescription => portDescription.portID === operatorPortID?.portID); if (portIdx === -1) return undefined; return this.getSharedOperatorType(operatorPortID?.operatorID) .get(isInput ? "inputPorts" : "outputPorts") .get(portIdx) as YType; } /** * Exposes a shared comment box type for fine-grained control. Do not use if not familiar with yjs. * @param commentBoxID */ public getSharedCommentBoxType(commentBoxID: string): YType { this.assertCommentBoxExists(commentBoxID); return this.sharedModel.commentBoxMap.get(commentBoxID) as YType; } /** * Get the awareness API to connect a shared type to other third-party shared-editing libraries. */ public getSharedModelAwareness(): Awareness { return this.sharedModel.awareness; } /** * Updates a particular field of local awareness state info. Will only execute update when user info is provided. * @param field the name of the particular state info. * @param value the updated state info. */ public updateSharedModelAwareness(field: K, value: CoeditorState[K]) { this.sharedModel.updateAwareness(field, value); } /** * Replaces current {@link sharedModel} with a new one and destroy the old model if any. * @param workflowId optional, but needed if you want to join shared editing. * @param user optional, but needed if you want to have user presence. * @param productionSharedEditingServer whether to use production shared editing server */ public loadNewYModel(workflowId?: number, user?: User, productionSharedEditingServer?: boolean) { this.destroyYModel(); this.sharedModel = new SharedModel(workflowId, user, productionSharedEditingServer); this.newYDocLoadedSubject.next(undefined); } /** * Destroys shared-editing related structures and quits the shared editing session. */ public destroyYModel(): void { this.sharedModel.destroy(); } /** * Sets the boolean value that specifies whether sync JointJS changes to texera graph. */ public setSyncTexeraGraph(syncTexeraGraph: boolean): void { this.syncTexeraGraph = syncTexeraGraph; } /** * Groups a bunch of actions into one atomic transaction, so that they can be undone/redone in one call. * @param callback Put whatever need to be atomically done within this callback function. */ public bundleActions(callback: Function) { this.sharedModel.transact(callback); } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Below are action methods. // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Adds a new operator to the graph. * Throws an error the operator has a duplicate operatorID with an existing operator. * @param operator {@link OperatorPredicate} will be converted to a {@link YType} brefore * adding to the internal Y-graph. */ public addOperator(operator: OperatorPredicate): void { this.assertOperatorNotExists(operator.operatorID); const newOp = createYTypeFromObject(operator); this.sharedModel.operatorIDMap.set(operator.operatorID, newOp); } /** * Adds a comment box to the graph. * @param commentBox {@link CommentBox} will be converted to a {@link YType} before adding * to the internal Y-graph. */ public addCommentBox(commentBox: CommentBox): void { this.assertCommentBoxNotExists(commentBox.commentBoxID); const newCommentBox = createYTypeFromObject(commentBox); this.sharedModel.commentBoxMap.set(commentBox.commentBoxID, newCommentBox); } /** * Adds a single comment to an existing comment box. * @param comment the comment's content encapsulated in the {@link Comment} structure. It will be added * as-is to the list of comments, i.e., it won't be converted to {@link YType}. * @param commentBoxID the id of the comment box to add comment to. */ public addCommentToCommentBox(comment: Comment, commentBoxID: string): void { this.assertCommentBoxExists(commentBoxID); const commentBox = this.sharedModel.commentBoxMap.get(commentBoxID) as YType; if (commentBox != null) { commentBox.get("comments").push([comment]); } } /** * Searches the comment list by creatorID and creationTime and deletes the comment if found. * The deletion is on a y-list. * @param creatorID * @param creationTime * @param commentBoxID */ public deleteCommentFromCommentBox(creatorID: number, creationTime: string, commentBoxID: string): void { this.assertCommentBoxExists(commentBoxID); const commentBox = this.sharedModel.commentBoxMap.get(commentBoxID) as YType; if (commentBox != null) { commentBox.get("comments").forEach((comment, index) => { if (comment.creatorID === creatorID && comment.creationTime === creationTime) { commentBox.get("comments").delete(index); } }); } } /** * Edits a given comment. Due to yjs's limitation, the modification is actually done by * deleting and adding (in place). * @param creatorID * @param creationTime * @param commentBoxID * @param content */ public editCommentInCommentBox(creatorID: number, creationTime: string, commentBoxID: string, content: string): void { this.assertCommentBoxExists(commentBoxID); const commentBox = this.sharedModel.commentBoxMap.get(commentBoxID); if (commentBox != null) { commentBox.get("comments").forEach((comment, index) => { if (comment.creatorID === creatorID && comment.creationTime === creationTime) { let creatorName = comment.creatorName; let newComment: Comment = { content, creationTime, creatorName, creatorID }; this.sharedModel.yDoc.transact(() => { commentBox.get("comments").delete(index); commentBox.get("comments").insert(index, [newComment]); }); } }); } } /** * Deletes the operator from the graph by its ID. The deletion is on a y-map. * Throws an Error if the operator doesn't exist. * @param operatorID operator ID */ public deleteOperator(operatorID: string): void { const operator = this.getOperator(operatorID); if (!operator) { throw new Error(`operator with ID ${operatorID} doesn't exist`); } this.sharedModel.operatorIDMap.delete(operatorID); } /** * Deletes the comment box from the model's {@link commentBoxMap}. * @param commentBoxID */ public deleteCommentBox(commentBoxID: string): void { const commentBox = this.getCommentBox(commentBoxID); if (!commentBox) { throw new Error(`CommentBox with ID ${commentBoxID} does not exist`); } this.sharedModel.commentBoxMap.delete(commentBoxID); } /** * Disables the operator by setting the isDisabled attribute in the corresponding operator from the map. * @param operatorID */ public disableOperator(operatorID: string): void { const operator = this.getOperator(operatorID); if (!operator) { throw new Error(`operator with ID ${operatorID} doesn't exist`); } if (this.isOperatorDisabled(operatorID)) { return; } this.sharedModel.operatorIDMap.get(operatorID)?.set("isDisabled", true); } /** * Enables the operator by setting the isDisabled attribute in the corresponding operator from the map. * @param operatorID */ public enableOperator(operatorID: string): void { const operator = this.getOperator(operatorID); if (!operator) { throw new Error(`operator with ID ${operatorID} doesn't exist`); } if (!this.isOperatorDisabled(operatorID)) { return; } this.sharedModel.operatorIDMap.get(operatorID)?.set("isDisabled", false); } /** * Will use string instead of Y.Text since this is not supposed to be shared-editable. Also the event stream * is emitted synchronously since this does not need to be shared-edited. */ public changeOperatorVersion(operatorID: string, newOperatorVersion: string): void { const operator = this.getOperator(operatorID); if (operator.operatorVersion === newOperatorVersion) { return; } this.sharedModel.operatorIDMap.get(operatorID)?.set("operatorVersion", newOperatorVersion as any); this.operatorVersionChangedSubject.next({ operatorID, newOperatorVersion }); } /** * This method gets this status from readonly object version of the operator data as opposed to y-type data. * @param operatorID */ public isOperatorDisabled(operatorID: string): boolean { const operator = this.getOperator(operatorID); if (!operator) { throw new Error(`operator with ID ${operatorID} doesn't exist`); } return operator.isDisabled ?? false; } /** * Gets disabled operators by filtering from all operatorIDs in the OperatorIDMap. */ public getDisabledOperators(): ReadonlySet { return new Set( Array.from(this.sharedModel.operatorIDMap.keys() as IterableIterator).filter(op => this.isOperatorDisabled(op) ) ); } /** * Changes isViewingResult status which is an atomic boolean value as opposed to y-type data. * @param operatorID */ public setViewOperatorResult(operatorID: string): void { const operator = this.getOperator(operatorID); if (!operator) { throw new Error(`operator with ID ${operatorID} doesn't exist`); } if (isSink(operator)) { return; } if (this.isViewingResult(operatorID)) { return; } this.sharedModel.operatorIDMap.get(operatorID)?.set("viewResult", true); } /** * Changes isViewingResult status which is an atomic boolean value as opposed to y-type data. * @param operatorID */ public unsetViewOperatorResult(operatorID: string): void { const operator = this.getOperator(operatorID); if (!operator) { throw new Error(`operator with ID ${operatorID} doesn't exist`); } if (!this.isViewingResult(operatorID)) { return; } this.sharedModel.operatorIDMap.get(operatorID)?.set("viewResult", false); } /** * This method gets this status from readonly object version of the operator data as opposed to y-type data. * @param operatorID */ public isViewingResult(operatorID: string): boolean { const operator = this.getOperator(operatorID); if (!operator) { throw new Error(`operator with ID ${operatorID} doesn't exist`); } return operator.viewResult ?? false; } public getOperatorsToViewResult(): ReadonlySet { return new Set( Array.from(this.sharedModel.operatorIDMap.keys() as IterableIterator).filter(op => this.isViewingResult(op) ) ); } /** * Changes markedForReuse status which is an atomic boolean value as opposed to y-type data. * @param operatorID */ public markReuseResult(operatorID: string): void { const operator = this.getOperator(operatorID); if (!operator) { throw new Error(`operator with ID ${operatorID} doesn't exist`); } if (isSink(operator)) { return; } if (this.isMarkedForReuseResult(operatorID)) { return; } console.log("seeting marked for reuse in shared model"); this.sharedModel.operatorIDMap.get(operatorID)?.set("markedForReuse", true); } /** * Changes markedForReuse status which is an atomic boolean value as opposed to y-type data. * @param operatorID */ public removeMarkReuseResult(operatorID: string): void { const operator = this.getOperator(operatorID); if (!operator) { throw new Error(`operator with ID ${operatorID} doesn't exist`); } if (!this.isMarkedForReuseResult(operatorID)) { return; } this.sharedModel.operatorIDMap.get(operatorID)?.set("markedForReuse", false); } /** * This method gets this status from readonly object version of the operator data as opposed to y-type data. * @param operatorID */ public isMarkedForReuseResult(operatorID: string): boolean { const operator = this.getOperator(operatorID); if (!operator) { throw new Error(`operator with ID ${operatorID} doesn't exist`); } return operator.markedForReuse ?? false; } public getOperatorsMarkedForReuseResult(): ReadonlySet { return new Set( Array.from(this.sharedModel.operatorIDMap.keys() as IterableIterator).filter(op => this.isMarkedForReuseResult(op) ) ); } /** * Returns whether the operator exists in the graph. * @param operatorID operator ID */ public hasOperator(operatorID: string): boolean { return this.sharedModel.operatorIDMap.has(operatorID) as boolean; } /** * Returns whether the comment box exists in the graph. * @param commentBoxId */ public hasCommentBox(commentBoxId: string): boolean { return this.sharedModel.commentBoxMap.has(commentBoxId); } /** * Returns whether the element exists in the graph. * Can be an operator, comment box, or link. * @param id element ID */ public hasElementWithID(id: string): boolean { return this.hasOperator(id) || this.hasCommentBox(id) || this.hasLinkWithID(id); } /** * Gets the operator with the operatorID. The object version of the operator is returned, as opposed to y-type data. * Throws an Error if the operator doesn't exist. * @param operatorID operator ID */ public getOperator(operatorID: string): OperatorPredicate { if (!this.sharedModel.operatorIDMap.has(operatorID)) { throw new Error(`operator ${operatorID} does not exist`); } const yoperator = this.sharedModel.operatorIDMap.get(operatorID) as YType; return yoperator.toJSON(); } /** * Gets the comment box with the commentBoxID. The object version is returned, as opposed to y-type data. * Throws an Error if the comment box doesn't exist. * @param commentBoxID */ public getCommentBox(commentBoxID: string): CommentBox { const commentBox = this.sharedModel.commentBoxMap.get(commentBoxID) as YType; if (!commentBox) { throw new Error(`commentBox ${commentBoxID} does not exist`); } return commentBox.toJSON(); } public createOperatorDebugState(operatorId: string) { if (this.sharedModel.debugState.has(operatorId)) { return; } this.sharedModel.debugState.set(operatorId, new Y.Map()); } public getOperatorDebugState(operatorId: string): Y.Map { if (!this.sharedModel.debugState.has(operatorId)) { throw new Error(`operator ${operatorId} does not have a debug state`); } return this.sharedModel.debugState.get(operatorId)!; } /** * Returns an array of all operators in the graph. */ public getAllOperators(): OperatorPredicate[] { return Array.from(this.sharedModel.operatorIDMap.values() as IterableIterator>).map(v => v.toJSON() ); } /** * Returns an array of all enabled operators in the graph. */ public getAllEnabledOperators(): ReadonlyArray { return Array.from(this.sharedModel.operatorIDMap.values() as IterableIterator>) .map(v => v.toJSON()) .filter(op => !this.isOperatorDisabled(op.operatorID)); } /** * Returns an array of all the comment boxes in the graph. */ public getAllCommentBoxes(): CommentBox[] { return Array.from(this.sharedModel.commentBoxMap.values() as IterableIterator>).map(v => v.toJSON() ); } public addPort(operatorID: string, port: PortDescription, isInput: boolean): void { this.assertOperatorExists(operatorID); if (isInput) { const inputPorts = this.sharedModel.operatorIDMap.get(operatorID)?.get("inputPorts") as Y.Array< YType >; inputPorts.push([createYTypeFromObject(port)]); } else { const outputPorts = this.sharedModel.operatorIDMap.get(operatorID)?.get("outputPorts") as Y.Array< YType >; outputPorts.push([createYTypeFromObject(port)]); } } public removePort(operatorID: string, isInput: boolean): void { this.assertOperatorExists(operatorID); if (isInput) { const inputPorts = this.sharedModel.operatorIDMap.get(operatorID)?.get("inputPorts") as Y.Array< YType >; inputPorts.delete(inputPorts.length - 1, 1); } else { const outputPorts = this.sharedModel.operatorIDMap.get(operatorID)?.get("outputPorts") as Y.Array< YType >; outputPorts.delete(outputPorts.length - 1, 1); } } public hasPort(operatorPortID: LogicalPort): boolean { if (!this.hasOperator(operatorPortID.operatorID)) return false; const operator = this.getOperator(operatorPortID.operatorID); if (operatorPortID.portID.includes("input")) { return ( operator.inputPorts.find(portDescription => portDescription.portID === operatorPortID.portID) !== undefined ); } else if (operatorPortID.portID.includes("output")) { return ( operator.outputPorts.find(portDescription => portDescription.portID === operatorPortID.portID) !== undefined ); } else return false; } public getPortDescription(operatorPortID: LogicalPort): PortDescription | undefined { if (!this.hasPort(operatorPortID)) throw new Error(`operator port ${(operatorPortID.operatorID, operatorPortID.portID)} does not exist`); const operator = this.getOperator(operatorPortID.operatorID); if (operatorPortID.portID.includes("input")) { return operator.inputPorts.find(portDescription => portDescription.portID === operatorPortID.portID); } else if (operatorPortID.portID.includes("output")) { return operator.outputPorts.find(portDescription => portDescription.portID === operatorPortID.portID); } else return undefined; } /** * Adds a link to the operator graph. * Throws an error if * - the link already exists in the graph (duplicate ID or source-target) * - the link is invalid (invalid source or target operator/port) * @param link */ public addLink(link: OperatorLink): void { this.assertLinkNotExists(link); this.assertLinkIsValid(link); this.sharedModel.operatorLinkMap.set(link.linkID, link); } /** * Deletes a link by the linkID. * Throws an error if the linkID doesn't exist in the graph * @param linkID link ID */ public deleteLinkWithID(linkID: string): void { const link = this.getLinkWithID(linkID); if (!link) { throw new Error(`link with ID ${linkID} doesn't exist`); } this.sharedModel.operatorLinkMap.delete(linkID); } /** * Deletes a link by the source and target of the link. * Throws an error if the link doesn't exist in the graph * @param source source port * @param target target port */ public deleteLink(source: LogicalPort, target: LogicalPort): void { const link = this.getLink(source, target); if (!link) { throw new Error(`link from ${source.operatorID}.${source.portID} to ${target.operatorID}.${target.portID} doesn't exist`); } this.sharedModel.operatorLinkMap.delete(link.linkID); } /** * Returns whether the graph contains the link with the linkID * @param linkID link ID */ public hasLinkWithID(linkID: string): boolean { return this.sharedModel.operatorLinkMap.has(linkID); } /** * Returns whether the graph contains the link with the source and target * @param source source operator and port of the link * @param target target operator and port of the link */ public hasLink(source: LogicalPort, target: LogicalPort): boolean { try { const link = this.getLink(source, target); return true; } catch (e) { return false; } } public isLinkEnabled(linkID: string): boolean { const link = this.getLinkWithID(linkID); return !this.isOperatorDisabled(link.source.operatorID) && !this.isOperatorDisabled(link.target.operatorID); } /** * Returns a link with the linkID from operatorLinkMap. * Throws an error if the link doesn't exist. * @param linkID link ID */ public getLinkWithID(linkID: string): OperatorLink { const link = this.sharedModel.operatorLinkMap.get(linkID); if (!link) { throw new Error(`link ${linkID} does not exist`); } return link; } /** * Returns a link with the source and target from operatorLinkMap. * Returns undefined if the link doesn't exist. * @param source source operator and port of the link * @param target target operator and port of the link */ public getLink(source: LogicalPort, target: LogicalPort): OperatorLink { const links = this.getAllLinks().filter(value => isEqual(value.source, source) && isEqual(value.target, target)); if (links.length === 0) { throw new Error(`link with source ${source} and target ${target} does not exist`); } if (links.length > 1) { throw new Error("WorkflowGraph inconsistency: find duplicate links with same source and target"); } return links[0]; } /** * Returns an array of all the links in the graph. */ public getAllLinks(): OperatorLink[] { return Array.from(this.sharedModel.operatorLinkMap.values()); } /** * Returns an array of all the enabled links in the graph. */ public getAllEnabledLinks(): ReadonlyArray { return Array.from(this.sharedModel.operatorLinkMap.values()).filter(link => this.isLinkEnabled(link.linkID)); } /** * Returns an array of all input links of an operator in the graph. * @param operatorID */ public getInputLinksByOperatorId(operatorID: string): OperatorLink[] { return this.getAllLinks().filter(link => link.target.operatorID === operatorID); } /** * Returns an array of all output links of an operator in the graph. * @param operatorID */ public getOutputLinksByOperatorId(operatorID: string): OperatorLink[] { return this.getAllLinks().filter(link => link.source.operatorID === operatorID); } /** * Sets the property of the operator to use the newProperty object. * Will create a new y-object based on the new property, so the old y-object will be replaced and as such * fine-grained shared-editing will NOT be enabled. * * Also updates local awareness for changed property status. * * Throws an error if the operator doesn't exist. * @param operatorID operator ID * @param newProperty new property to set, the new y-object created from this will replace the old structure. */ public setOperatorProperty(operatorID: string, newProperty: object): void { if (!this.hasOperator(operatorID)) { throw new Error(`operator with ID ${operatorID} doesn't exist`); } const previousProperty = this.getSharedOperatorType(operatorID).get( "operatorProperties" ) as YType; // set the new copy back to the operator ID map updateYTypeFromObject(previousProperty, newProperty); } public setPortProperty(operatorPortID: LogicalPort, newProperty: object) { newProperty = newProperty as PortProperty; if (!this.hasPort(operatorPortID)) throw new Error(`operator port ${(operatorPortID.operatorID, operatorPortID.portID)} does not exist`); const portDescriptionSharedType = this.getSharedPortDescriptionType(operatorPortID); if (portDescriptionSharedType === undefined) return; portDescriptionSharedType.set( "partitionRequirement", createYTypeFromObject((newProperty as PortProperty).partitionInfo) as unknown as PartitionInfo ); portDescriptionSharedType.set( "dependencies", createYTypeFromObject>( (newProperty as PortProperty).dependencies ) as unknown as Y.Array ); } /** * Gets the observable event stream of an operator being added into the graph. */ public getOperatorAddStream(): Observable { return this.operatorAddSubject.asObservable(); } /** * Gets the observable event stream of an operator being deleted from the graph. * The observable value is only the deleted operator's ID since a deleted YMap * (the internal structure of the operator) cannot be retrieved. */ public getOperatorDeleteStream(): Observable<{ deletedOperatorID: string; }> { return this.operatorDeleteSubject.asObservable(); } public getDisabledOperatorsChangedStream(): Observable<{ newDisabled: ReadonlyArray; newEnabled: ReadonlyArray; }> { return this.disabledOperatorChangedSubject.asObservable(); } public getCommentBoxAddStream(): Observable { return this.commentBoxAddSubject.asObservable(); } public getCommentBoxDeleteStream(): Observable<{ deletedCommentBox: CommentBox }> { return this.commentBoxDeleteSubject.asObservable(); } public getCommentBoxAddCommentStream(): Observable<{ addedComment: Comment; commentBox: CommentBox }> { return this.commentBoxAddCommentSubject.asObservable(); } public getCommentBoxDeleteCommentStream(): Observable<{ commentBox: CommentBox }> { return this.commentBoxDeleteCommentSubject.asObservable(); } public getCommentBoxEditCommentStream(): Observable<{ commentBox: CommentBox }> { return this.commentBoxEditCommentSubject.asObservable(); } public getViewResultOperatorsChangedStream(): Observable<{ newViewResultOps: ReadonlyArray; newUnviewResultOps: ReadonlyArray; }> { return this.viewResultOperatorChangedSubject.asObservable(); } public getReuseCacheOperatorsChangedStream(): Observable<{ newReuseCacheOps: ReadonlyArray; newUnreuseCacheOps: ReadonlyArray; }> { return this.reuseOperatorChangedSubject.asObservable(); } public getOperatorDisplayNameChangedStream(): Observable<{ operatorID: string; newDisplayName: string; }> { return this.operatorDisplayNameChangedSubject.asObservable(); } public getOperatorVersionChangedStream(): Observable<{ operatorID: string; newOperatorVersion: string; }> { return this.operatorVersionChangedSubject.asObservable(); } /** *ets the observable event stream of a link being added into the graph. */ public getLinkAddStream(): Observable { return this.linkAddSubject.asObservable(); } /** * Gets the observable event stream of a link being deleted from the graph. * The observable value is the deleted link. */ public getLinkDeleteStream(): Observable<{ deletedLink: OperatorLink }> { return this.linkDeleteSubject.asObservable(); } /** * Gets the observable event stream of a change in operator's properties. * The observable value includes the operator with new property. */ public getOperatorPropertyChangeStream(): Observable<{ operator: OperatorPredicate; }> { return this.operatorPropertyChangeSubject.asObservable(); } /** * Gets the observable event stream of a link breakpoint is changed. */ public getBreakpointChangeStream(): Observable<{ oldBreakpoint: object | undefined; linkID: string; }> { return this.breakpointChangeStream.asObservable(); } public getPortAddedOrDeletedStream(): Observable<{ newOperator: OperatorPredicate; }> { return this.portAddedOrDeletedSubject.asObservable(); } public getPortDisplayNameChangedSubject(): Observable<{ operatorID: string; portID: string; newDisplayName: string; }> { return this.portDisplayNameChangedSubject; } public getPortPropertyChangedStream(): Observable<{ operatorPortID: LogicalPort; newProperty: PortProperty; }> { return this.portPropertyChangedSubject.asObservable(); } public getCenterEventStream(): Observable { return this.centerEventSubject.asObservable(); } public triggerCenterEvent(): void { this.centerEventSubject.next(); } /** * Checks if an operator with the OperatorID already exists in the graph. * Throws an Error if the operator doesn't exist. * @param graph * @param operator */ public assertOperatorExists(operatorID: string): void { if (!this.hasOperator(operatorID)) { throw new Error(`operator with ID ${operatorID} doesn't exist`); } } public assertCommentBoxExists(commentBoxID: string): void { if (!this.hasCommentBox(commentBoxID)) { throw new Error(`commentBox with ID ${commentBoxID} does not exist`); } } /** * Checks if an operator * Throws an Error if there's a duplicate operator ID * @param graph * @param operator */ public assertOperatorNotExists(operatorID: string): void { if (this.hasOperator(operatorID)) { throw new Error(`operator with ID ${operatorID} already exists`); } } public assertCommentBoxNotExists(commentBoxID: string): void { if (this.hasCommentBox(commentBoxID)) { throw new Error(`commentBox with ID ${commentBoxID} already exists`); } } /** * Asserts that the link doesn't exists in the graph by checking: * - duplicate link ID * - duplicate link source and target * Throws an Error if the link already exists. * @param graph * @param link */ public assertLinkNotExists(link: OperatorLink): void { if (this.hasLinkWithID(link.linkID)) { throw new Error(`link with ID ${link.linkID} already exists`); } if (this.hasLink(link.source, link.target)) { throw new Error(`link from ${link.source.operatorID}.${link.source.portID} to ${link.target.operatorID}.${link.target.portID} already exists`); } } public assertLinkWithIDExists(linkID: string): void { if (!this.hasLinkWithID(linkID)) { throw new Error(`link with ID ${linkID} doesn't exist`); } } public assertLinkExists(source: LogicalPort, target: LogicalPort): void { if (!this.hasLink(source, target)) { throw new Error(`link from ${source.operatorID}.${source.portID} to ${target.operatorID}.${target.portID} already exists`); } } /** * Checks if it's valid to add the given link to the graph. * Throws an Error if it's not a valid link because of: * - invalid source operator or port * - invalid target operator or port * @param graph * @param link */ public assertLinkIsValid(link: OperatorLink): void { const sourceOperator = this.getOperator(link.source.operatorID); if (!sourceOperator) { throw new Error(`link's source operator ${link.source.operatorID} doesn't exist`); } const targetOperator = this.getOperator(link.target.operatorID); if (!targetOperator) { throw new Error(`link's target operator ${link.target.operatorID} doesn't exist`); } if (sourceOperator.outputPorts.find(port => port.portID === link.source.portID) === undefined) { throw new Error(`link's source port ${link.source.portID} doesn't exist on output ports of the source operator ${link.source.operatorID}`); } if (targetOperator.inputPorts.find(port => port.portID === link.target.portID) === undefined) { throw new Error(`link's target port ${link.target.portID} doesn't exist on input ports of the target operator ${link.target.operatorID}`); } } /** * Checks if a link is unique in the graph. Throws an error if more than one link with the same source and target * as the given link exists. */ public assertLinkNotDuplicated(link: OperatorLink): void { const links = this.getAllLinks().filter( value => isEqual(value.source, link.source) && isEqual(value.target, link.target) ); if (links.length > 1) { throw new Error(`duplicate link found with same source and target as link ${link}`); } } /** * Retrieves a subgraph (subDAG) from the workflow graph. This method excludes disabled operators and links. * * This method can operate in two modes: * 1. If a `targetOperatorId` is provided, it performs a depth-first search (DFS) starting from * the specified operator to construct the subDAG. * 2. If no `targetOperatorId` is provided, it starts from all terminal operators (operators with no * outgoing links) and aggregates the paths from these sinks to construct the subDAG, potentially * covering the entire DAG if all paths are interconnected. * * @param targetOperatorId - The unique identifier of the operator from which to start the DFS. * This parameter is optional. If omitted, the search starts from all * terminal operators within the graph. * @returns An object containing two arrays: `operators` and `links`. The `operators` array * includes all operator objects that are part of the subDAG, and the `links` array * contains all the operator links that connect these operators within the subDAG. * */ public getSubDAG(targetOperatorId?: string) { const visited: Set = new Set(); const subDagOperators: OperatorPredicate[] = []; const subDagLinks: OperatorLink[] = []; function dfs(currentOperatorId: string, graph: WorkflowGraph) { if (visited.has(currentOperatorId)) { return; } visited.add(currentOperatorId); const currentOperator = graph.getOperator(currentOperatorId); if (currentOperator && !currentOperator.isDisabled) { subDagOperators.push(currentOperator); // Find links connected to the current operator const connectedLinks = graph.getAllEnabledLinks().filter(link => link.target.operatorID === currentOperatorId); connectedLinks.forEach(link => { subDagLinks.push(link); dfs(link.source.operatorID, graph); }); } } if (targetOperatorId !== undefined) { dfs(targetOperatorId, this); } else { // When no target operator ID is provided, start DFS from all terminal operators const allOperators = this.getAllOperators(); const allLinks = this.getAllEnabledLinks(); const terminalOperators = allOperators.filter( operator => !allLinks.some(link => link.source.operatorID === operator.operatorID) ); terminalOperators.forEach(terminalOperator => dfs(terminalOperator.operatorID, this)); } return { operators: subDagOperators, links: subDagLinks }; } } ================================================ FILE: frontend/src/app/workspace/service/workflow-graph/util/workflow-util.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { StubOperatorMetadataService } from "./../../operator-metadata/stub-operator-metadata.service"; import { OperatorMetadataService } from "./../../operator-metadata/operator-metadata.service"; import { inject, TestBed } from "@angular/core/testing"; import { WorkflowUtilService } from "./workflow-util.service"; import { mockScanSourceSchema } from "../../operator-metadata/mock-operator-metadata.data"; import { commonTestProviders } from "../../../../common/testing/test-utils"; describe("WorkflowUtilService", () => { let workflowUtilService: WorkflowUtilService; beforeEach(() => { TestBed.configureTestingModule({ providers: [ WorkflowUtilService, { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, ...commonTestProviders, ], }); workflowUtilService = TestBed.inject(WorkflowUtilService); }); it("should be created", inject([WorkflowUtilService], (service: WorkflowUtilService) => { expect(service).toBeTruthy(); })); it("should be able to generate an operator predicate properly given a valid operator type", () => { const operatorSchema = mockScanSourceSchema; const operatorPredicate = workflowUtilService.getNewOperatorPredicate(operatorSchema.operatorType); // assert predicate itself and operator type are correct expect(operatorPredicate).toBeTruthy(); expect(operatorPredicate.operatorType).toEqual(operatorSchema.operatorType); // assert num of input ports and output ports are correct expect(operatorPredicate.inputPorts.length).toEqual(operatorSchema.additionalMetadata.inputPorts.length); expect(operatorPredicate.outputPorts.length).toEqual(operatorSchema.additionalMetadata.outputPorts.length); // asssert that the portID of input and output ports are all distinct expect(new Set(operatorPredicate.inputPorts).size).toEqual(operatorPredicate.inputPorts.length); expect(new Set(operatorPredicate.outputPorts).size).toEqual(operatorPredicate.outputPorts.length); // assert that it creates the operator property to be an empty object expect(operatorPredicate.operatorProperties).toEqual({}); }); it("should throw an error when trying to generate an operator predicate with non exist operator type", () => { expect(() => { workflowUtilService.getNewOperatorPredicate("non-exist-operator-type"); }).toThrowError(new RegExp("doesn't exist")); }); it("should be able to generate different operator IDs", () => { const idSet = new Set(); const repeat = 100; for (let i = 0; i < repeat; i++) { idSet.add(workflowUtilService.getOperatorRandomUUID()); } // assert all IDs are distinct expect(idSet.size).toEqual(repeat); }); it("should be able to generate different link IDs", () => { const idSet = new Set(); const repeat = 100; for (let i = 0; i < repeat; i++) { idSet.add(workflowUtilService.getLinkRandomUUID()); } // assert all IDs are distinct expect(idSet.size).toEqual(repeat); }); it("should be able to assign different operator IDs to newly generated operators", () => { const operatorSchema = mockScanSourceSchema; const idSet = new Set(); const repeat = 100; for (let i = 0; i < repeat; i++) { idSet.add(workflowUtilService.getNewOperatorPredicate(operatorSchema.operatorType).operatorID); } // assert all IDs are distinct expect(idSet.size).toEqual(repeat); }); }); ================================================ FILE: frontend/src/app/workspace/service/workflow-graph/util/workflow-util.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Comment, CommentBox, OperatorPredicate, Point, PortDescription, } from "../../../types/workflow-common.interface"; import { OperatorMetadataService } from "../../operator-metadata/operator-metadata.service"; import { InputPortInfo, OperatorSchema, OutputPortInfo } from "../../../types/operator-schema.interface"; import { Injectable } from "@angular/core"; import { v4 as uuid } from "uuid"; import Ajv from "ajv"; import { Observable, Subject } from "rxjs"; import { Workflow, WorkflowContent } from "../../../../common/type/workflow"; import { jsonCast } from "../../../../common/util/storage"; /** * WorkflowUtilService provide utilities related to dealing with operator data. */ @Injectable({ providedIn: "root", }) export class WorkflowUtilService { private operatorSchemaList: ReadonlyArray = []; // used to fetch default values in json schema to initialize new operator private ajv = new Ajv({ useDefaults: true, strict: false }); private operatorSchemaListCreatedSubject: Subject = new Subject(); constructor(private operatorMetadataService: OperatorMetadataService) { this.operatorMetadataService.getOperatorMetadata().subscribe(value => { this.operatorSchemaList = value.operators; this.operatorSchemaListCreatedSubject.next(true); }); } public getOperatorSchemaListCreatedStream(): Observable { return this.operatorSchemaListCreatedSubject.asObservable(); } /** * Returns a list of all available operator types */ public getOperatorTypeList(): string[] { return this.operatorSchemaList.map(schema => schema.operatorType); } /** * Generates a new UUID for operator */ public getOperatorRandomUUID(): string { return "operator-" + uuid(); } /** * Generates a new UUID for link */ public getLinkRandomUUID(): string { return "link-" + uuid(); } /** * Generates a new UUID for group element */ public getGroupRandomUUID(): string { return "group-" + uuid(); } /** * Generates a new UUID for breakpoint */ public getBreakpointRandomUUID(): string { return "breakpoint-" + uuid(); } public getCommentBoxRandomUUID(): string { return "commentBox-" + uuid(); } // TODO: change this to drag-and-drop public getNewCommentBox(): CommentBox { const commentBoxID = this.getCommentBoxRandomUUID(); const comments: Comment[] = []; const commentBoxPosition: Point = { x: 500, y: 20 }; return { commentBoxID, comments, commentBoxPosition }; } /** * This method will use a unique ID and a operatorType to create and return a * new OperatorPredicate with default initial properties. * * @param operatorType type of an Operator * @returns a new OperatorPredicate of the operatorType */ public getNewOperatorPredicate(operatorType: string, customDisplayName?: string): OperatorPredicate { const operatorSchema = this.operatorSchemaList.find(schema => schema.operatorType === operatorType); if (operatorSchema === undefined) { throw new Error(`operatorType ${operatorType} doesn't exist in operator metadata`); } const operatorId = operatorSchema.operatorType + "-" + this.getOperatorRandomUUID(); const operatorProperties = {}; // Remove the ID field for the schema to prevent warning messages from Ajv const { ...schemaWithoutId } = operatorSchema.jsonSchema; // value inserted in the data will be the deep clone of the default in the schema const validate = this.ajv.compile(schemaWithoutId); validate(operatorProperties); const inputPorts: PortDescription[] = []; const outputPorts: PortDescription[] = []; // by default, the operator will not show advanced option in the properties to the user const showAdvanced = false; // by default, the operator is not disabled const isDisabled = false; // Use provided customDisplayName or default to the user friendly name from schema const displayName = customDisplayName ?? operatorSchema.additionalMetadata.userFriendlyName; const dynamicInputPorts = operatorSchema.additionalMetadata.dynamicInputPorts ?? false; const dynamicOutputPorts = operatorSchema.additionalMetadata.dynamicOutputPorts ?? false; for (let i = 0; i < operatorSchema.additionalMetadata.inputPorts.length; i++) { const portID = "input-" + i.toString(); const portInfo = operatorSchema.additionalMetadata.inputPorts[i]; inputPorts.push(WorkflowUtilService.inputPortToPortDescription(portID, portInfo)); } for (let i = 0; i < operatorSchema.additionalMetadata.outputPorts.length; i++) { const portID = "output-" + i.toString(); const portInfo = operatorSchema.additionalMetadata.outputPorts[i]; outputPorts.push(WorkflowUtilService.outputPortToPortDescription(portID, portInfo)); } const operatorVersion = operatorSchema.operatorVersion; return { operatorID: operatorId, operatorType, operatorVersion, operatorProperties, inputPorts, outputPorts, showAdvanced, isDisabled, customDisplayName: displayName, dynamicInputPorts, dynamicOutputPorts, }; } /** * helper function to parse WorkflowInfo from a JSON string. In some case, for example reading from backend, * the content would return as a JSON string. * @param workflow */ public static parseWorkflowInfo(workflow: Workflow): Workflow { if (workflow != null && typeof workflow.content === "string") { workflow.content = jsonCast(workflow.content); } return workflow; } private static inputPortToPortDescription(portID: string, inputPortInfo: InputPortInfo): PortDescription { return { portID, displayName: inputPortInfo.displayName ?? "", disallowMultiInputs: inputPortInfo.disallowMultiLinks ?? false, isDynamicPort: false, dependencies: inputPortInfo.dependencies ?? [], }; } private static outputPortToPortDescription(portID: string, outputPortInfo: OutputPortInfo): PortDescription { return { portID, displayName: outputPortInfo.displayName ?? "", disallowMultiInputs: false, isDynamicPort: false, }; } public updateOperatorVersion(op: OperatorPredicate) { const operatorType = op.operatorType; const operatorSchema = this.operatorSchemaList.find(schema => schema.operatorType === operatorType); if (operatorSchema === undefined) { throw new Error(`operatorType ${operatorType} doesn't exist in operator metadata`); } let inputPorts: PortDescription[] = []; if (op.inputPorts.length === 0) { // use the operatorSchema as a template to create ports for (let i = 0; i < operatorSchema.additionalMetadata.inputPorts.length; i++) { const portID = "input-" + i.toString(); const portInfo = operatorSchema.additionalMetadata.inputPorts[i]; inputPorts.push(WorkflowUtilService.inputPortToPortDescription(portID, portInfo)); } } else { inputPorts = op.inputPorts; } let outputPorts: PortDescription[] = []; if (op.outputPorts.length === 0) { // use the operatorSchema as a template to create ports for (let i = 0; i < operatorSchema.additionalMetadata.outputPorts.length; i++) { const portID = "output-" + i.toString(); const portInfo = operatorSchema.additionalMetadata.outputPorts[i]; outputPorts.push(WorkflowUtilService.outputPortToPortDescription(portID, portInfo)); } } else { outputPorts = op.outputPorts; } return { ...op, operatorVersion: operatorSchema.operatorVersion, inputPorts, outputPorts, }; } } ================================================ FILE: frontend/src/app/workspace/service/workflow-result/panel-resize/panel-resize.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { TestBed } from "@angular/core/testing"; import { PanelResizeService } from "./panel-resize.service"; describe("PanelResizeService", () => { let service: PanelResizeService; beforeEach(() => { service = TestBed.inject(PanelResizeService); }); it("should be created", () => { expect(service).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/workspace/service/workflow-result/panel-resize/panel-resize.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { BehaviorSubject } from "rxjs"; @Injectable({ providedIn: "root", }) export class PanelResizeService { private panelSizeSource = new BehaviorSubject<{ width: number; height: number }>({ width: 800, height: 300 }); currentSize = this.panelSizeSource.asObservable(); public pageSize = 1 + Math.floor((300 - 200) / 35); changePanelSize(width: number, height: number) { this.panelSizeSource.next({ width, height }); } } ================================================ FILE: frontend/src/app/workspace/service/workflow-result/workflow-result.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { TestBed } from "@angular/core/testing"; import { OperatorPaginationResultService, WorkflowResultService } from "./workflow-result.service"; import { WorkflowWebsocketService } from "../workflow-websocket/workflow-websocket.service"; import { firstValueFrom, of, Subject } from "rxjs"; import { SchemaAttribute } from "../../types/workflow-compiling.interface"; import { commonTestProviders } from "../../../common/testing/test-utils"; import type { Mocked } from "vitest"; describe("WorkflowResultService", () => { let service: WorkflowResultService; beforeEach(() => { TestBed.configureTestingModule({ providers: [WorkflowResultService, ...commonTestProviders], }); service = TestBed.inject(WorkflowResultService); }); it("should be created", () => { expect(service).toBeTruthy(); }); }); describe("OperatorPaginationResultService", () => { let service: OperatorPaginationResultService; let mockWorkflowWebsocketService: Mocked; beforeEach(() => { mockWorkflowWebsocketService = { subscribeToEvent: vi.fn(), send: vi.fn(), } as unknown as Mocked; mockWorkflowWebsocketService.subscribeToEvent.mockReturnValue(new Subject()); service = new OperatorPaginationResultService("testOperator", mockWorkflowWebsocketService); }); describe("getSchema", () => { it("should return the current schema", () => { const testSchema: SchemaAttribute[] = [ { attributeName: "id", attributeType: "integer" }, { attributeName: "name", attributeType: "string" }, ]; service["schema"] = testSchema; expect(service.getSchema()).toEqual(testSchema); }); }); describe("selectTuple", () => { it("should return the correct tuple and schema", async () => { const testSchema: SchemaAttribute[] = [ { attributeName: "id", attributeType: "integer" }, { attributeName: "name", attributeType: "string" }, ]; service["schema"] = testSchema; const testTable = [ { id: 1, name: "Alice" }, { id: 2, name: "Bob" }, { id: 3, name: "Charlie" }, ]; vi.spyOn(service, "selectPage").mockReturnValue( of({ requestID: "test", operatorID: "testOperator", pageIndex: 1, table: testTable, schema: testSchema, }) ); const result = await firstValueFrom(service.selectTuple(1, 3)); expect(result.tuple).toEqual({ id: 2, name: "Bob" }); expect(result.schema).toEqual(testSchema); }); it("should handle out-of-bounds tuple index", async () => { const testSchema: SchemaAttribute[] = [ { attributeName: "id", attributeType: "integer" }, { attributeName: "name", attributeType: "string" }, ]; service["schema"] = testSchema; const testTable = [ { id: 1, name: "Alice" }, { id: 2, name: "Bob" }, ]; vi.spyOn(service, "selectPage").mockReturnValue( of({ requestID: "test", operatorID: "testOperator", pageIndex: 1, table: testTable, schema: testSchema, }) ); const result = await firstValueFrom(service.selectTuple(2, 3)); expect(result.tuple).toBeUndefined(); expect(result.schema).toEqual(testSchema); }); }); }); ================================================ FILE: frontend/src/app/workspace/service/workflow-result/workflow-result.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { isWebDataUpdate, isWebPaginationUpdate, WebDataUpdate, WebPaginationUpdate, WebResultUpdate, WorkflowResultTableStats, WorkflowResultUpdate, } from "../../types/execute-workflow.interface"; import { WorkflowWebsocketService } from "../workflow-websocket/workflow-websocket.service"; import { PaginatedResultEvent, WorkflowAvailableResultEvent } from "../../types/workflow-websocket.interface"; import { map, Observable, of, pairwise, ReplaySubject, Subject } from "rxjs"; import { v4 as uuid } from "uuid"; import { IndexableObject } from "../../types/result-table.interface"; import { isDefined } from "../../../common/util/predicate"; import { SchemaAttribute } from "../../types/workflow-compiling.interface"; /** * WorkflowResultService manages the result data of a workflow execution. */ @Injectable({ providedIn: "root", }) export class WorkflowResultService { private paginatedResultServices = new Map(); private operatorResultServices = new Map(); // event stream of operator result update, undefined indicates the operator result is cleared private resultUpdateStream = new Subject>(); private resultTableStats = new ReplaySubject>>>(1); private resultInitiateStream = new Subject(); constructor(private wsService: WorkflowWebsocketService) { this.wsService.subscribeToEvent("WebResultUpdateEvent").subscribe(event => { this.handleResultUpdate(event.updates); this.handleTableStatsUpdate(event.tableStats); }); this.wsService .subscribeToEvent("WorkflowAvailableResultEvent") .subscribe(event => this.handleCleanResultCache(event)); this.resultTableStats.next({}); } public hasAnyResult(operatorID: string): boolean { return this.hasResult(operatorID) || this.hasPaginatedResult(operatorID); } public hasResult(operatorID: string): boolean { return isDefined(this.getResultService(operatorID)); } public hasPaginatedResult(operatorID: string): boolean { return isDefined(this.getPaginatedResultService(operatorID)); } public getResultUpdateStream(): Observable> { return this.resultUpdateStream; } public getResultTableStats(): Observable< [Record>>, Record>>] > { return this.resultTableStats.pipe(pairwise()); } public getResultInitiateStream(): Observable { return this.resultInitiateStream.asObservable(); } public getPaginatedResultService(operatorID: string): OperatorPaginationResultService | undefined { return this.paginatedResultServices.get(operatorID); } public getResultService(operatorID: string): OperatorResultService | undefined { return this.operatorResultServices.get(operatorID); } private handleCleanResultCache(event: WorkflowAvailableResultEvent): void { const removedOrInvalidatedOperators = new Set(); // remove operators that no longer have results this.operatorResultServices.forEach((_, op) => { if (!(op in event.availableOperators)) { this.operatorResultServices.delete(op); removedOrInvalidatedOperators.add(op); } }); this.paginatedResultServices.forEach((_, op) => { if (!(op in event.availableOperators)) { this.paginatedResultServices.delete(op); removedOrInvalidatedOperators.add(op); } }); // for each operator that has results: Object.entries(event.availableOperators).forEach(availableOp => { const op = availableOp[0]; const cacheValid = availableOp[1].cacheValid; const outputMode = availableOp[1].outputMode; // make sure to init or reuse result service for each operator const resultService = (() => { if (outputMode.type === "PaginationMode") { return this.getOrInitPaginatedResultService(op); } else { return this.getOrInitResultService(op); } })(); // invalidate frontend cache if needed if (!cacheValid) { resultService.reset(); removedOrInvalidatedOperators.add(op); } }); const invalidatedOperatorsUpdate: Record = {}; removedOrInvalidatedOperators.forEach(op => (invalidatedOperatorsUpdate[op] = undefined)); this.resultUpdateStream.next(invalidatedOperatorsUpdate); } private handleResultUpdate(event: WorkflowResultUpdate): void { Object.keys(event).forEach(operatorID => { const update = event[operatorID]; if (isWebPaginationUpdate(update)) { const paginatedResultService = this.getOrInitPaginatedResultService(operatorID); paginatedResultService.handleResultUpdate(update); // clear previously saved result service this.operatorResultServices.delete(operatorID); } else if (isWebDataUpdate(update)) { const resultService = this.getOrInitResultService(operatorID); resultService.handleResultUpdate(update); // clear previously saved paginated result service this.paginatedResultServices.delete(operatorID); } }); this.resultUpdateStream.next(event); } private handleTableStatsUpdate(event: WorkflowResultTableStats): void { Object.keys(event).forEach(operatorID => { const paginatedResultService = this.getOrInitPaginatedResultService(operatorID); paginatedResultService.handleStatsUpdate(event[operatorID]); }); this.resultTableStats.next(event); } private getOrInitPaginatedResultService(operatorID: string): OperatorPaginationResultService { let service = this.getPaginatedResultService(operatorID); if (!service) { service = new OperatorPaginationResultService(operatorID, this.wsService); this.paginatedResultServices.set(operatorID, service); this.resultInitiateStream.next(operatorID); } return service; } private getOrInitResultService(operatorID: string): OperatorResultService { let service = this.getResultService(operatorID); if (!service) { service = new OperatorResultService(operatorID); this.operatorResultServices.set(operatorID, service); this.resultInitiateStream.next(operatorID); } return service; } public determineOutputTypes(operatorId: string): { hasAnyResult: boolean; isTableOutput: boolean; isVisualizationOutput: boolean; containsBinaryData: boolean; } { const resultService = this.getResultService(operatorId); const paginatedResultService = this.getPaginatedResultService(operatorId); return { hasAnyResult: this.hasAnyResult(operatorId), isTableOutput: this.hasTableOutput(paginatedResultService), containsBinaryData: this.hasBinaryData(paginatedResultService), isVisualizationOutput: this.hasVisualizationOutput(resultService, paginatedResultService), }; } public determineOutputExtension(operatorId: string, defaultExtension: string = "csv"): string { if (defaultExtension === "data") return defaultExtension; var outputType = this.determineOutputTypes(operatorId); if (outputType.isVisualizationOutput) return "html"; if (outputType.isTableOutput && defaultExtension === "csv") return "csv"; return defaultExtension; } private hasTableOutput(paginatedResultService?: OperatorPaginationResultService): boolean { return paginatedResultService !== undefined; } private hasBinaryData(paginatedResultService?: OperatorPaginationResultService): boolean { return paginatedResultService?.getSchema().some(attribute => attribute.attributeType === "binary") ?? false; } private hasVisualizationOutput( resultService?: OperatorResultService, paginatedResultService?: OperatorPaginationResultService ): boolean { return resultService !== undefined && paginatedResultService === undefined; } } export class OperatorResultService { private resultSnapshot: ReadonlyArray | undefined; constructor(public operatorID: string) {} public getCurrentResultSnapshot(): ReadonlyArray | undefined { return this.resultSnapshot; } public reset(): void { this.resultSnapshot = undefined; } public handleResultUpdate(update: WebDataUpdate): void { if (update.mode.type === "SetSnapshotMode") { // update the result snapshot with latest update this.resultSnapshot = update.table; } else if (update.mode.type === "SetDeltaMode") { // intentionally do nothing, frontend does not accumulate delta results } } } export class OperatorPaginationResultService { private pendingRequests: Map> = new Map(); private resultCache: Map> = new Map(); private prevStatsCache: Record> = {}; private statsCache: Record> = {}; private currentPageIndex: number = 1; private currentTotalNumTuples: number = 0; private schema: ReadonlyArray = []; constructor( public operatorID: string, private workflowWebsocketService: WorkflowWebsocketService ) { this.workflowWebsocketService.subscribeToEvent("PaginatedResultEvent").subscribe(event => { this.schema = event.schema; this.handlePaginationResult(event); }); } public getStats(): Record> { return this.statsCache; } public getPrevStats(): Record> { return this.prevStatsCache; } public getCurrentPageIndex(): number { return this.currentPageIndex; } public getCurrentTotalNumTuples(): number { return this.currentTotalNumTuples; } public getSchema(): ReadonlyArray { return this.schema; } public selectTuple( tupleIndex: number, pageSize: number ): Observable<{ tuple: IndexableObject; schema: ReadonlyArray }> { // calculate the page index // remember that page index starts from 1 const pageIndex = Math.floor(tupleIndex / pageSize) + 1; return this.selectPage(pageIndex, pageSize).pipe( map(p => ({ tuple: p.table[tupleIndex % pageSize], schema: this.schema, })) ); } public selectPage( pageIndex: number, pageSize: number, columnOffset: number = 0, columnLimit: number = Number.MAX_SAFE_INTEGER, columnSearch: string = "" ): Observable { // update currently selected page this.currentPageIndex = pageIndex; // first fetch from frontend result cache const useCache = columnOffset === 0 && columnLimit === Number.MAX_SAFE_INTEGER && columnSearch === ""; const pageCache = useCache ? this.resultCache.get(pageIndex) : undefined; if (pageCache) { return of({ requestID: "", operatorID: this.operatorID, pageIndex: pageIndex, table: pageCache, schema: this.schema, }); } else { // fetch result data from server const requestID = uuid(); const operatorID = this.operatorID; this.workflowWebsocketService.send("ResultPaginationRequest", { requestID, operatorID, pageIndex, pageSize, columnOffset, columnLimit, columnSearch, }); const pendingRequestSubject = new Subject(); this.pendingRequests.set(requestID, pendingRequestSubject); return pendingRequestSubject; } } public reset(): void { this.pendingRequests.clear(); this.resultCache.clear(); this.currentPageIndex = 1; this.currentTotalNumTuples = 0; } public handleResultUpdate(update: WebPaginationUpdate): void { this.currentTotalNumTuples = update.totalNumTuples; update.dirtyPageIndices.forEach(dirtyPage => { this.resultCache.delete(dirtyPage); }); } public handleStatsUpdate(statsUpdate: Record>): void { if (!this.statsCache) { this.statsCache = statsUpdate; this.prevStatsCache = statsUpdate; } else { this.prevStatsCache = this.statsCache; this.statsCache = statsUpdate; } } private handlePaginationResult(res: PaginatedResultEvent): void { const pendingRequestSubject = this.pendingRequests.get(res.requestID); if (!pendingRequestSubject) { return; } pendingRequestSubject.next(res); pendingRequestSubject.complete(); this.pendingRequests.delete(res.requestID); } } ================================================ FILE: frontend/src/app/workspace/service/workflow-result-export/workflow-result-export.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { TestBed } from "@angular/core/testing"; import { WorkflowResultExportService } from "./workflow-result-export.service"; import { HttpClientTestingModule } from "@angular/common/http/testing"; import { WorkflowWebsocketService } from "../workflow-websocket/workflow-websocket.service"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { NotificationService } from "../../../common/service/notification/notification.service"; import { ExecuteWorkflowService } from "../execute-workflow/execute-workflow.service"; import { WorkflowResultService } from "../workflow-result/workflow-result.service"; import { of } from "rxjs"; import { ExecutionState } from "../../types/execute-workflow.interface"; import { DownloadService } from "src/app/dashboard/service/user/download/download.service"; import { DatasetService } from "../../../dashboard/service/user/dataset/dataset.service"; import { commonTestProviders } from "../../../common/testing/test-utils"; import type { Mocked } from "vitest"; import { JointGraphWrapper } from "../workflow-graph/model/joint-graph-wrapper"; import { WorkflowGraph } from "../workflow-graph/model/workflow-graph"; describe("WorkflowResultExportService", () => { let service: WorkflowResultExportService; let workflowWebsocketServiceSpy: Mocked; let workflowActionServiceSpy: Mocked; let notificationServiceSpy: Mocked; let executeWorkflowServiceSpy: Mocked; let workflowResultServiceSpy: Mocked; let downloadServiceSpy: Mocked; let datasetServiceSpy: Mocked; let jointGraphWrapperSpy: Mocked; let texeraGraphSpy: Mocked; beforeEach(() => { // Create spies for the required services jointGraphWrapperSpy = { getCurrentHighlightedOperatorIDs: vi.fn(), getJointOperatorHighlightStream: vi.fn(), getJointOperatorUnhighlightStream: vi.fn(), } as unknown as Mocked; jointGraphWrapperSpy.getCurrentHighlightedOperatorIDs.mockReturnValue([]); jointGraphWrapperSpy.getJointOperatorHighlightStream.mockReturnValue(of()); jointGraphWrapperSpy.getJointOperatorUnhighlightStream.mockReturnValue(of()); texeraGraphSpy = { getAllOperators: vi.fn(), getOperatorAddStream: vi.fn(), getOperatorDeleteStream: vi.fn(), getOperatorPropertyChangeStream: vi.fn(), getLinkAddStream: vi.fn(), getLinkDeleteStream: vi.fn(), getDisabledOperatorsChangedStream: vi.fn(), getAllLinks: vi.fn(), } as unknown as Mocked; texeraGraphSpy.getAllOperators.mockReturnValue([]); texeraGraphSpy.getOperatorAddStream.mockReturnValue(of()); texeraGraphSpy.getOperatorDeleteStream.mockReturnValue(of()); texeraGraphSpy.getOperatorPropertyChangeStream.mockReturnValue(of()); texeraGraphSpy.getLinkAddStream.mockReturnValue(of()); texeraGraphSpy.getLinkDeleteStream.mockReturnValue(of()); texeraGraphSpy.getDisabledOperatorsChangedStream.mockReturnValue(of()); texeraGraphSpy.getAllLinks.mockReturnValue([]); const wsSpy = { subscribeToEvent: vi.fn(), send: vi.fn() }; wsSpy.subscribeToEvent.mockReturnValue(of()); // Return an empty observable const waSpy = { getJointGraphWrapper: vi.fn(), getTexeraGraph: vi.fn(), getWorkflow: vi.fn() }; waSpy.getJointGraphWrapper.mockReturnValue(jointGraphWrapperSpy); waSpy.getTexeraGraph.mockReturnValue(texeraGraphSpy); waSpy.getWorkflow.mockReturnValue({ wid: "workflow1", name: "Test Workflow" }); const ntSpy = { success: vi.fn(), error: vi.fn(), loading: vi.fn() }; const ewSpy = { getExecutionStateStream: vi.fn(), getExecutionState: vi.fn() }; ewSpy.getExecutionStateStream.mockReturnValue(of({ previous: {}, current: { state: ExecutionState.Completed } })); ewSpy.getExecutionState.mockReturnValue({ state: ExecutionState.Completed }); const wrSpy = { hasAnyResult: vi.fn(), getResultService: vi.fn(), getPaginatedResultService: vi.fn() }; const downloadSpy = { downloadOperatorsResult: vi.fn() }; downloadSpy.downloadOperatorsResult.mockReturnValue(of(new Blob())); const datasetSpy = { retrieveAccessibleDatasets: vi.fn() }; datasetSpy.retrieveAccessibleDatasets.mockReturnValue(of([])); TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ WorkflowResultExportService, { provide: WorkflowWebsocketService, useValue: wsSpy }, { provide: WorkflowActionService, useValue: waSpy }, { provide: NotificationService, useValue: ntSpy }, { provide: ExecuteWorkflowService, useValue: ewSpy }, { provide: WorkflowResultService, useValue: wrSpy }, { provide: DownloadService, useValue: downloadSpy }, { provide: DatasetService, useValue: datasetSpy }, ...commonTestProviders, ], }); // Inject the service and spies service = TestBed.inject(WorkflowResultExportService); workflowWebsocketServiceSpy = TestBed.inject( WorkflowWebsocketService ) as unknown as Mocked; workflowActionServiceSpy = TestBed.inject(WorkflowActionService) as unknown as Mocked; notificationServiceSpy = TestBed.inject(NotificationService) as unknown as Mocked; executeWorkflowServiceSpy = TestBed.inject(ExecuteWorkflowService) as unknown as Mocked; workflowResultServiceSpy = TestBed.inject(WorkflowResultService) as unknown as Mocked; downloadServiceSpy = TestBed.inject(DownloadService) as unknown as Mocked; datasetServiceSpy = TestBed.inject(DatasetService) as unknown as Mocked; }); it("should be created", () => { expect(service).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/workspace/service/workflow-result-export/workflow-result-export.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { WorkflowWebsocketService } from "../workflow-websocket/workflow-websocket.service"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { BehaviorSubject, merge, Observable, of } from "rxjs"; import { NotificationService } from "../../../common/service/notification/notification.service"; import { ExecuteWorkflowService } from "../execute-workflow/execute-workflow.service"; import { ExecutionState, isNotInExecution } from "../../types/execute-workflow.interface"; import { catchError, filter, map, take } from "rxjs/operators"; import { WorkflowResultService } from "../workflow-result/workflow-result.service"; import { DownloadService, ExportWorkflowJsonResponse } from "../../../dashboard/service/user/download/download.service"; import { HttpResponse } from "@angular/common/http"; import { DashboardWorkflowComputingUnit } from "../../../common/type/workflow-computing-unit"; import { GuiConfigService } from "../../../common/service/gui-config.service"; /** * Result of workflow result downloadability analysis. * Contains information about which operators are restricted from exporting * due to non-downloadable dataset dependencies. */ export class WorkflowResultDownloadability { /** * Map of operator IDs to sets of blocking dataset labels. * Key: Operator ID * Value: Set of human-readable dataset labels (e.g., "dataset_name (owner@email.com)") * that are blocking this operator from being exported * * An operator appears in this map if it directly uses or depends on (through data flow) * one or more datasets that the current user is not allowed to download. */ restrictedOperatorMap: Map>; constructor(restrictedOperatorMap: Map>) { this.restrictedOperatorMap = restrictedOperatorMap; } /** * Filters operator IDs to return only those that are not restricted by dataset access controls. * * @param operatorIds Array of operator IDs to filter * @returns Array of operator IDs that can be exported */ getExportableOperatorIds(operatorIds: readonly string[]): string[] { return operatorIds.filter(operatorId => !this.restrictedOperatorMap.has(operatorId)); } /** * Filters operator IDs to return only those that are restricted by dataset access controls. * * @param operatorIds Array of operator IDs to filter * @returns Array of operator IDs that are blocked from export */ getBlockedOperatorIds(operatorIds: readonly string[]): string[] { return operatorIds.filter(operatorId => this.restrictedOperatorMap.has(operatorId)); } /** * Gets the list of dataset labels that are blocking export for the given operators. * Used to display user-friendly error messages about which datasets are causing restrictions. * * @param operatorIds Array of operator IDs to check * @returns Array of dataset labels (e.g., "Dataset1 (user@example.com)") */ getBlockingDatasets(operatorIds: readonly string[]): string[] { const labels = new Set(); operatorIds.forEach(operatorId => { const datasets = this.restrictedOperatorMap.get(operatorId); datasets?.forEach(label => labels.add(label)); }); return Array.from(labels); } } @Injectable({ providedIn: "root", }) export class WorkflowResultExportService { hasResultToExportOnHighlightedOperators: boolean = false; hasResultToExportOnAllOperators = new BehaviorSubject(false); constructor( private workflowWebsocketService: WorkflowWebsocketService, private workflowActionService: WorkflowActionService, private notificationService: NotificationService, private executeWorkflowService: ExecuteWorkflowService, private workflowResultService: WorkflowResultService, private downloadService: DownloadService, private config: GuiConfigService ) { this.registerResultToExportUpdateHandler(); } registerResultToExportUpdateHandler() { merge( this.executeWorkflowService .getExecutionStateStream() .pipe(filter(({ previous, current }) => current.state === ExecutionState.Completed)), this.workflowActionService.getJointGraphWrapper().getJointOperatorHighlightStream(), this.workflowActionService.getJointGraphWrapper().getJointOperatorUnhighlightStream() ).subscribe(() => { this.updateExportAvailabilityFlags(); }); } /** * Computes restriction analysis by calling the backend API. * * The backend analyzes the workflow to identify operators that are restricted from export * due to non-downloadable dataset dependencies. The restriction propagates through the * workflow graph via data flow. * * @returns Observable that emits the restriction analysis result */ public computeRestrictionAnalysis(): Observable { const workflowId = this.workflowActionService.getWorkflow().wid; if (!workflowId) { return of(new WorkflowResultDownloadability(new Map>())); } return this.downloadService.getWorkflowResultDownloadability(workflowId).pipe( map(backendResponse => { // Convert backend format to Map> const restrictedOperatorMap = new Map>(); Object.entries(backendResponse).forEach(([operatorId, datasetLabels]) => { restrictedOperatorMap.set(operatorId, new Set(datasetLabels)); }); return new WorkflowResultDownloadability(restrictedOperatorMap); }), catchError(() => { return of(new WorkflowResultDownloadability(new Map>())); }) ); } /** * Updates UI flags that control export button visibility and availability. * * Checks execution state and result availability to determine: * - hasResultToExportOnHighlightedOperators: for context menu export button * - hasResultToExportOnAllOperators: for top menu export button * * Export is only available when execution is idle and operators have results. */ private updateExportAvailabilityFlags(): void { const executionIdle = isNotInExecution(this.executeWorkflowService.getExecutionState().state); const highlightedOperators = this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs(); const highlightedHasResult = highlightedOperators.some( operatorId => this.workflowResultService.hasAnyResult(operatorId) || this.workflowResultService.getResultService(operatorId)?.getCurrentResultSnapshot() !== undefined ); this.hasResultToExportOnHighlightedOperators = executionIdle && highlightedHasResult; const allOperatorIds = this.workflowActionService .getTexeraGraph() .getAllOperators() .map(operator => operator.operatorID); const hasAnyResult = executionIdle && allOperatorIds.some( operatorId => this.workflowResultService.hasAnyResult(operatorId) || this.workflowResultService.getResultService(operatorId)?.getCurrentResultSnapshot() !== undefined ); this.hasResultToExportOnAllOperators.next(hasAnyResult); } /** * export the workflow execution result according the export type */ exportWorkflowExecutionResult( exportType: string, workflowName: string, datasetIds: number[], rowIndex: number, columnIndex: number, filename: string, exportAll: boolean = false, // if the user click export button on the top bar (a.k.a menu), // we should export all operators, otherwise, only highlighted ones // which means export button is selected from context-menu destination: "dataset" | "local" = "dataset", // default to dataset unit: DashboardWorkflowComputingUnit | null // computing unit for cluster setting ): void { this.computeRestrictionAnalysis() .pipe(take(1)) .subscribe(restrictionResult => this.performExport( exportType, workflowName, datasetIds, rowIndex, columnIndex, filename, exportAll, destination, unit, restrictionResult ) ); } /** * Performs the actual export operation with restriction validation. * * This method handles the core export logic: * 1. Validates configuration and computing unit availability * 2. Determines operator scope (all vs highlighted) * 3. Applies restriction filtering with user feedback * 4. Makes the export API call * 5. Handles response and shows appropriate notifications * * Shows error messages if all operators are blocked, warning messages if some are blocked. * * @param downloadability Downloadability analysis result containing restriction information */ private performExport( exportType: string, workflowName: string, datasetIds: number[], rowIndex: number, columnIndex: number, filename: string, exportAll: boolean, destination: "dataset" | "local", unit: DashboardWorkflowComputingUnit | null, downloadability: WorkflowResultDownloadability ): void { // Validates configuration and computing unit availability if (!this.config.env.exportExecutionResultEnabled) { return; } if (unit === null) { this.notificationService.error("Cannot export result: computing unit is not available"); return; } const workflowId = this.workflowActionService.getWorkflow().wid; if (!workflowId) { this.notificationService.error("Cannot export result: workflow ID is not available"); return; } // Determines operator scope const operatorIds = exportAll ? this.workflowActionService .getTexeraGraph() .getAllOperators() .map(operator => operator.operatorID) : [...this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs()]; if (operatorIds.length === 0) { return; } // Applies restriction filtering with user feedback const exportableOperatorIds = downloadability.getExportableOperatorIds(operatorIds); if (exportableOperatorIds.length === 0) { const datasets = downloadability.getBlockingDatasets(operatorIds); const suffix = datasets.length > 0 ? `: ${datasets.join(", ")}` : ""; this.notificationService.error( `Cannot export result: selection depends on dataset(s) that are not downloadable${suffix}` ); return; } if (exportableOperatorIds.length < operatorIds.length) { const datasets = downloadability.getBlockingDatasets(operatorIds); const suffix = datasets.length > 0 ? ` (${datasets.join(", ")})` : ""; this.notificationService.warning( `Some operators were skipped because their results depend on dataset(s) that are not downloadable${suffix}` ); } const operatorArray = exportableOperatorIds.map(operatorId => ({ id: operatorId, outputType: this.workflowResultService.determineOutputExtension(operatorId, exportType), })); // show loading this.notificationService.loading("Exporting..."); // Make request if (destination === "local") { // Dataset export to local filesystem (download handled by browser) this.downloadService.exportWorkflowResultToLocal( exportType, workflowId, workflowName, operatorArray, rowIndex, columnIndex, filename, unit ); } else { // Dataset export to dataset via API call this.downloadService .exportWorkflowResultToDataset( exportType, workflowId, workflowName, operatorArray, [...datasetIds], rowIndex, columnIndex, filename, unit ) .subscribe({ next: response => { // "dataset" => response is JSON // The server should return a JSON with {status, message} const jsonResponse = response as HttpResponse; const responseBody = jsonResponse.body; if (responseBody && responseBody.status === "success") { this.notificationService.success("Result exported successfully"); } else { this.notificationService.error(responseBody?.message || "An error occurred during export"); } }, error: (err: unknown) => { const errorMessage = (err as any)?.error?.message || (err as any)?.error || err; this.notificationService.error(`An error happened in exporting operator results: ${errorMessage}`); }, }); } } /** * Reset flags if the user leave workspace */ public resetFlags(): void { this.hasResultToExportOnHighlightedOperators = false; this.hasResultToExportOnAllOperators = new BehaviorSubject(false); } getExportOnAllOperatorsStatusStream(): Observable { return this.hasResultToExportOnAllOperators.asObservable(); } } ================================================ FILE: frontend/src/app/workspace/service/workflow-status/operator-reuse-cache-status.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { TestBed } from "@angular/core/testing"; import { OperatorMetadataService } from "../operator-metadata/operator-metadata.service"; import { StubOperatorMetadataService } from "../operator-metadata/stub-operator-metadata.service"; import { OperatorReuseCacheStatusService } from "./operator-reuse-cache-status.service"; import { HttpClientModule } from "@angular/common/http"; import { commonTestProviders } from "../../../common/testing/test-utils"; describe("OperatorCacheStatusService", () => { let service: OperatorReuseCacheStatusService; beforeEach(() => { TestBed.configureTestingModule({ providers: [ { provide: OperatorMetadataService, useClass: StubOperatorMetadataService, }, ...commonTestProviders, ], imports: [HttpClientModule], }); service = TestBed.inject(OperatorReuseCacheStatusService); }); it("should be created", () => { expect(service).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/workspace/service/workflow-status/operator-reuse-cache-status.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { WorkflowWebsocketService } from "../workflow-websocket/workflow-websocket.service"; import { JointUIService } from "../joint-ui/joint-ui.service"; @Injectable({ providedIn: "root", }) export class OperatorReuseCacheStatusService { constructor( private jointUIService: JointUIService, private workflowActionService: WorkflowActionService, private workflowWebsocketService: WorkflowWebsocketService ) { this.registerHandleCacheStatusUpdate(); } /** * Registers handler for cache status update from the backend. */ private registerHandleCacheStatusUpdate() { this.workflowActionService .getTexeraGraph() .getReuseCacheOperatorsChangedStream() .subscribe(event => { const mainJointPaper = this.workflowActionService.getJointGraphWrapper().getMainJointPaper(); if (!mainJointPaper) { return; } event.newReuseCacheOps.concat(event.newUnreuseCacheOps).forEach(opID => { const op = this.workflowActionService.getTexeraGraph().getOperator(opID); this.jointUIService.changeOperatorReuseCacheStatus(mainJointPaper, op); }); }); this.workflowWebsocketService.subscribeToEvent("CacheStatusUpdateEvent").subscribe(event => { const mainJointPaper = this.workflowActionService.getJointGraphWrapper().getMainJointPaper(); if (!mainJointPaper) { return; } Object.entries(event.cacheStatusMap).forEach(([opID, cacheStatus]) => { const op = this.workflowActionService.getTexeraGraph().getOperator(opID); this.jointUIService.changeOperatorReuseCacheStatus(mainJointPaper, op, cacheStatus); }); }); } } ================================================ FILE: frontend/src/app/workspace/service/workflow-status/workflow-status.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { Observable, Subject } from "rxjs"; import { OperatorState, OperatorStatistics } from "../../types/execute-workflow.interface"; import { WorkflowWebsocketService } from "../workflow-websocket/workflow-websocket.service"; @Injectable({ providedIn: "root", }) export class WorkflowStatusService { // status is responsible for passing websocket responses to other components private statusSubject = new Subject>(); private currentStatus: Record = {}; constructor(private workflowWebsocketService: WorkflowWebsocketService) { this.getStatusUpdateStream().subscribe(event => (this.currentStatus = event)); this.workflowWebsocketService.websocketEvent().subscribe(event => { if (event.type !== "OperatorStatisticsUpdateEvent") { return; } this.statusSubject.next(event.operatorStatistics); }); } public getStatusUpdateStream(): Observable> { return this.statusSubject.asObservable(); } public getCurrentStatus(): Record { return this.currentStatus; } public resetStatus(): void { const initStatus: Record = Object.keys(this.currentStatus).reduce( (accumulator, operatorId) => { accumulator[operatorId] = { operatorState: OperatorState.Uninitialized, aggregatedInputRowCount: 0, inputPortMetrics: {}, aggregatedOutputRowCount: 0, outputPortMetrics: {}, }; return accumulator; }, {} as Record ); this.statusSubject.next(initStatus); } public clearStatus(): void { this.currentStatus = {}; this.statusSubject.next({}); } } ================================================ FILE: frontend/src/app/workspace/service/workflow-websocket/workflow-websocket.service.spec.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { TestBed } from "@angular/core/testing"; import { WorkflowWebsocketService } from "./workflow-websocket.service"; import { commonTestProviders } from "../../../common/testing/test-utils"; /** Browser-like WebSocket test double used to verify websocket reopen and subscription cleanup behavior. */ class FakeWebSocket extends EventTarget { public static readonly CONNECTING = 0; public static readonly OPEN = 1; public static readonly CLOSING = 2; public static readonly CLOSED = 3; public readyState = FakeWebSocket.CONNECTING; constructor(public readonly url: string) { super(); Promise.resolve().then(() => { this.readyState = FakeWebSocket.OPEN; const onopen = this.onopen; onopen?.(new Event("open")); this.dispatchEvent(new Event("open")); }); } public onopen: ((ev: Event) => unknown) | null = null; public onclose: ((ev: CloseEvent) => unknown) | null = null; public onerror: ((ev: Event) => unknown) | null = null; public onmessage: ((ev: MessageEvent) => unknown) | null = null; public send() {} public close() { if (this.readyState === FakeWebSocket.CLOSED) { return; } this.readyState = FakeWebSocket.CLOSED; const closeEvent = new CloseEvent("close", { wasClean: true, code: 1000, reason: "" }); const onclose = this.onclose; onclose?.(closeEvent); this.dispatchEvent(closeEvent); } } describe("WorkflowWebsocketService", () => { let service: WorkflowWebsocketService; beforeEach(() => { TestBed.configureTestingModule({ providers: [WorkflowWebsocketService, ...commonTestProviders], }); service = TestBed.inject(WorkflowWebsocketService); }); it("should be created", () => { expect(service).toBeTruthy(); }); it("should close the previous status subscription when openWebsocket is called again", () => { const originalWebSocket = window.WebSocket; window.WebSocket = FakeWebSocket as unknown as typeof WebSocket; try { service.openWebsocket(1, 1, 1); const firstStatusSubscription = (service as any).statusUpdateSubscription; expect(firstStatusSubscription.closed).toBe(false); service.openWebsocket(1, 1, 1); expect(firstStatusSubscription.closed).toBe(true); const secondStatusSubscription = (service as any).statusUpdateSubscription; expect(secondStatusSubscription.closed).toBe(false); service.closeWebsocket(); expect(secondStatusSubscription.closed).toBe(true); } finally { window.WebSocket = originalWebSocket; } }); }); ================================================ FILE: frontend/src/app/workspace/service/workflow-websocket/workflow-websocket.service.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Injectable } from "@angular/core"; import { BehaviorSubject, interval, Observable, Subject, Subscription, timer } from "rxjs"; import { webSocket, WebSocketSubject } from "rxjs/webSocket"; import { TexeraWebsocketEvent, TexeraWebsocketEventTypeMap, TexeraWebsocketEventTypes, TexeraWebsocketRequest, TexeraWebsocketRequestTypeMap, TexeraWebsocketRequestTypes, } from "../../types/workflow-websocket.interface"; import { delayWhen, filter, map, retryWhen, tap } from "rxjs/operators"; import { AuthService } from "../../../common/service/user/auth.service"; import { getWebsocketUrl } from "src/app/common/util/url"; import { isDefined } from "../../../common/util/predicate"; import { GuiConfigService } from "../../../common/service/gui-config.service"; export const WS_HEARTBEAT_INTERVAL_MS = 10000; export const WS_RECONNECT_INTERVAL_MS = 3000; @Injectable({ providedIn: "root", }) export class WorkflowWebsocketService { private static readonly TEXERA_WEBSOCKET_ENDPOINT = "wsapi/workflow-websocket"; public numWorkers: number = -1; private websocket?: WebSocketSubject; private wsWithReconnectSubscription?: Subscription; private statusUpdateSubscription?: Subscription; private readonly webSocketResponseSubject: Subject = new Subject(); private readonly connectionStatusSubject = new BehaviorSubject(false); constructor(private config: GuiConfigService) { // setup heartbeat interval(WS_HEARTBEAT_INTERVAL_MS).subscribe(_ => this.send("HeartBeatRequest", {})); } /** Emit `true` when the socket is up, `false` when it is closed or lost. */ public getConnectionStatusStream(): Observable { return this.connectionStatusSubject.asObservable(); } public websocketEvent(): Observable { return this.webSocketResponseSubject; } /** * Subscribe to a particular type of workflow websocket event */ public subscribeToEvent( type: T ): Observable<{ type: T } & TexeraWebsocketEventTypeMap[T]> { return this.websocketEvent().pipe( filter(event => event.type === type), map(event => event as { type: T } & TexeraWebsocketEventTypeMap[T]) ); } public send(type: T, payload: TexeraWebsocketRequestTypeMap[T]): void { const request = { type, ...payload, } as any as TexeraWebsocketRequest; this.websocket?.next(request); } public get isConnected(): boolean { return this.connectionStatusSubject.value; } public closeWebsocket() { this.wsWithReconnectSubscription?.unsubscribe(); this.statusUpdateSubscription?.unsubscribe(); this.websocket?.complete(); this.updateConnectionStatus(false); } public openWebsocket(wId: number, uId?: number, cuId?: number) { this.closeWebsocket(); if (uId == undefined) { console.log(`uId is ${uId}, defaulting to uId = 1`); uId = 1; } const websocketUrl = getWebsocketUrl(WorkflowWebsocketService.TEXERA_WEBSOCKET_ENDPOINT, "") + "?wid=" + wId + "&uid=" + uId + (isDefined(cuId) ? `&cuid=${cuId}` : "") + (AuthService.getAccessToken() !== null ? "&access-token=" + AuthService.getAccessToken() : ""); console.log("websocketUrl", websocketUrl); this.websocket = webSocket(websocketUrl); // setup reconnection logic const wsWithReconnect = this.websocket.pipe( retryWhen(errors => errors.pipe( tap(_ => this.updateConnectionStatus(false)), // update connection status tap(_ => console.log(`websocket connection lost, reconnecting in ${WS_RECONNECT_INTERVAL_MS / 1000} seconds`) ), delayWhen(_ => timer(WS_RECONNECT_INTERVAL_MS)), // reconnect after delay tap(_ => { this.send("HeartBeatRequest", {}); // try to send heartbeat immediately after reconnect }) ) ) ); // set up event listener on re-connectable websocket observable this.wsWithReconnectSubscription = wsWithReconnect.subscribe(event => this.webSocketResponseSubject.next(event as TexeraWebsocketEvent) ); // refresh connection status this.statusUpdateSubscription = this.websocketEvent().subscribe(evt => { if (evt.type === "ClusterStatusUpdateEvent") { this.numWorkers = evt.numWorkers; } this.updateConnectionStatus(true); }); } private updateConnectionStatus(connected: boolean) { if (this.isConnected !== connected) { this.connectionStatusSubject.next(connected); } } } ================================================ FILE: frontend/src/app/workspace/types/collab-websocket.interface.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ export interface WIdRequest extends Readonly<{ wId: number; }> {} export interface InformWIdEvent extends Readonly<{ message: string }> {} export interface CommandRequest extends Readonly<{ commandMessage: string; }> {} export interface CommandEvent extends Readonly<{ commandMessage: string; }> {} export interface WorkflowAccessEvent extends Readonly<{ workflowReadonly: boolean; }> {} export type CollabWebsocketRequestTypeMap = { WIdRequest: WIdRequest; HeartBeatRequest: {}; CommandRequest: CommandRequest; AcquireLockRequest: {}; TryLockRequest: {}; RestoreVersionRequest: {}; }; export type CollabWebsocketEventTypeMap = { InformWIdResponse: InformWIdEvent; HeartBeatResponse: {}; CommandEvent: CommandEvent; ReleaseLockEvent: {}; LockGrantedEvent: {}; LockRejectedEvent: {}; RestoreVersionEvent: {}; WorkflowAccessEvent: WorkflowAccessEvent; }; // helper type definitions to generate the request and event types type ValueOf = T[keyof T]; type CustomUnionType = ValueOf<{ [P in keyof T]: { type: P; } & T[P]; }>; export type CollabWebsocketRequestTypes = keyof CollabWebsocketRequestTypeMap; export type CollabWebsocketRequest = CustomUnionType; export type CollabWebsocketEventTypes = keyof CollabWebsocketEventTypeMap; export type CollabWebsocketEvent = CustomUnionType; ================================================ FILE: frontend/src/app/workspace/types/custom-json-schema.interface.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { JSONSchema7, JSONSchema7Definition } from "json-schema"; export const hideTypes = ["regex", "equals"] as const; export type HideType = (typeof hideTypes)[number]; export type AttributeTypeEnumRule = ReadonlyArray; export type AttributeTypeConstRule = Readonly<{ $data?: string; }>; export type AttributeTypeAllOfRule = ReadonlyArray<{ if: { [key: string]: { valEnum?: string[]; }; }; then: { enum?: AttributeTypeEnumRule; }; }>; export type AttributeTypeRuleSet = Readonly<{ enum?: AttributeTypeEnumRule; const?: AttributeTypeConstRule; allOf?: AttributeTypeAllOfRule; }>; export type AttributeTypeRuleSchema = Readonly<{ [key: string]: AttributeTypeRuleSet; }>; export interface CustomJSONSchema7 extends JSONSchema7 { propertyOrder?: number; properties?: { [key: string]: CustomJSONSchema7 | boolean; }; items?: CustomJSONSchema7 | boolean | JSONSchema7Definition[]; // new custom properties: autofill?: "attributeName" | "attributeNameList"; autofillAttributeOnPort?: number; attributeTypeRules?: AttributeTypeRuleSchema; "enable-presets"?: boolean; // include property in schema of preset dependOn?: string; toggleHidden?: string[]; // the field names which will be toggle hidden or not by this field. hideExpectedValue?: string; hideTarget?: string; hideType?: HideType; hideOnNull?: boolean; additionalEnumValue?: string; } ================================================ FILE: frontend/src/app/workspace/types/execute-workflow.interface.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * This file contains some type declaration for the WorkflowGraph interface of the **backend**. * The API of the backend is (currently) not the same as the Graph representation in the frontend. * These interfaces confront to the backend API. */ import { OperatorCurrentTuples, WorkflowFatalError } from "./workflow-websocket.interface"; export interface PortIdentity extends Readonly<{ id: number; internal: boolean; }> {} export interface OutputPort extends Readonly<{ id: PortIdentity; displayName: string }> {} export interface InputPort extends Readonly<{ id: PortIdentity; displayName: string; disallowMultiLinks: boolean; dependencies: ReadonlyArray; }> {} export interface LogicalLink extends Readonly<{ fromOpId: string; fromPortId: PortIdentity; toOpId: string; toPortId: PortIdentity; }> {} export interface LogicalOperator extends Readonly<{ operatorID: string; operatorType: string; [uniqueAttributes: string]: any; }> {} /** * LogicalPlan is the backend interface equivalent of frontend interface WorkflowGraph, * they represent the same thing - the backend term currently used is LogicalPlan. * However, the format and content of the backend interface is different. */ export interface LogicalPlan extends Readonly<{ operators: LogicalOperator[]; links: LogicalLink[]; opsToViewResult?: string[]; opsToReuseResult?: string[]; }> {} export enum OperatorState { Uninitialized = "Uninitialized", Initializing = "Initializing", Ready = "Ready", Running = "Running", Pausing = "Pausing", Paused = "Paused", Resuming = "Resuming", Completed = "Completed", Recovering = "Recovering", } export interface OperatorStatistics extends Readonly<{ operatorState: OperatorState; aggregatedInputRowCount: number; inputPortMetrics: Record; aggregatedOutputRowCount: number; outputPortMetrics: Record; numWorkers?: number; }> {} export interface OperatorStatsUpdate extends Readonly<{ operatorStatistics: Record; }> {} export type PaginationMode = { type: "PaginationMode" }; export type SetSnapshotMode = { type: "SetSnapshotMode" }; export type SetDeltaMode = { type: "SetDeltaMode" }; export type WebOutputMode = PaginationMode | SetSnapshotMode | SetDeltaMode; export interface WebPaginationUpdate extends Readonly<{ mode: PaginationMode; totalNumTuples: number; dirtyPageIndices: ReadonlyArray; }> {} export interface WebDataUpdate extends Readonly<{ mode: SetSnapshotMode | SetDeltaMode; table: ReadonlyArray; }> {} export type WebResultUpdate = WebPaginationUpdate | WebDataUpdate; export type WorkflowResultUpdate = Record; export type WorkflowResultTableStats = Record>>; export interface WorkflowResultUpdateEvent extends Readonly<{ updates: WorkflowResultUpdate; tableStats: WorkflowResultTableStats; }> {} // user-defined type guards to check the type of the result update // because TypeScript can't do Tagged Unions on nested data types https://github.com/microsoft/TypeScript/issues/18758 // and the unions have to be defined as nested because of JSON serialization options export function isWebPaginationUpdate(update: WebResultUpdate): update is WebPaginationUpdate { return update !== undefined && update.mode.type === "PaginationMode"; } export function isWebDataUpdate(update: WebResultUpdate): update is WebDataUpdate { return (update !== undefined && update.mode.type === "SetSnapshotMode") || update.mode.type === "SetDeltaMode"; } export function isNotInExecution(state: ExecutionState) { return [ ExecutionState.Uninitialized, ExecutionState.Failed, ExecutionState.Killed, ExecutionState.Completed, ].includes(state); } export enum ExecutionState { Uninitialized = "Uninitialized", Initializing = "Initializing", Running = "Running", Pausing = "Pausing", Paused = "Paused", Resuming = "Resuming", Recovering = "Recovering", Completed = "Completed", Terminated = "Terminated", Failed = "Failed", Killed = "Killed", } export type ExecutionStateInfo = Readonly< | { state: | ExecutionState.Uninitialized | ExecutionState.Initializing | ExecutionState.Pausing | ExecutionState.Running | ExecutionState.Resuming | ExecutionState.Recovering; } | { state: ExecutionState.Paused; currentTuples: Readonly>; } | { state: ExecutionState.Completed | ExecutionState.Killed | ExecutionState.Terminated; } | { state: ExecutionState.Failed; errorMessages: ReadonlyArray; } >; ================================================ FILE: frontend/src/app/workspace/types/operator-schema.interface.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { CustomJSONSchema7 } from "./custom-json-schema.interface"; /** * This file contains multiple type declarations related to operator schema. * These type declarations should be the same with the backend API. * * This file include a sample mock data: * workspace/service/operator-metadata/mock-operator-metadata.data.ts * */ export interface InputPortInfo extends Readonly<{ displayName?: string; disallowMultiLinks?: boolean; dependencies?: { id: number; internal: boolean }[]; }> {} export interface OutputPortInfo extends Readonly<{ displayName?: string; }> {} export interface OperatorAdditionalMetadata extends Readonly<{ userFriendlyName: string; operatorGroupName: string; operatorDescription?: string; inputPorts: ReadonlyArray; outputPorts: ReadonlyArray; dynamicInputPorts?: boolean; dynamicOutputPorts?: boolean; supportReconfiguration?: boolean; allowPortCustomization?: boolean; }> {} export interface OperatorSchema extends Readonly<{ operatorType: string; jsonSchema: Readonly; additionalMetadata: OperatorAdditionalMetadata; operatorVersion: string; }> {} export interface GroupInfo extends Readonly<{ groupName: string; children?: GroupInfo[] | null; }> {} export interface OperatorMetadata extends Readonly<{ operators: ReadonlyArray; groups: ReadonlyArray; }> {} export function areOperatorSchemasEqual(schema1: OperatorSchema, schema2: OperatorSchema): boolean { if (schema1.operatorType !== schema2.operatorType || schema1.operatorVersion !== schema2.operatorVersion) { return false; } // Compare jsonSchema using a JSON string comparison if (JSON.stringify(schema1.jsonSchema) !== JSON.stringify(schema2.jsonSchema)) { return false; } // Compare additionalMetadata by checking fields explicitly const meta1 = schema1.additionalMetadata; const meta2 = schema2.additionalMetadata; if ( meta1.userFriendlyName !== meta2.userFriendlyName || meta1.operatorGroupName !== meta2.operatorGroupName || meta1.operatorDescription !== meta2.operatorDescription || meta1.supportReconfiguration !== meta2.supportReconfiguration || meta1.allowPortCustomization !== meta2.allowPortCustomization ) { return false; } // Compare inputPorts and outputPorts if (meta1.inputPorts.length !== meta2.inputPorts.length || meta1.outputPorts.length !== meta2.outputPorts.length) { return false; } // Check each port info for equality for (let i = 0; i < meta1.inputPorts.length; i++) { const port1 = meta1.inputPorts[i]; const port2 = meta2.inputPorts[i]; if (port1.displayName !== port2.displayName || port1.disallowMultiLinks !== port2.disallowMultiLinks) { return false; } } for (let i = 0; i < meta1.outputPorts.length; i++) { const port1 = meta1.outputPorts[i]; const port2 = meta2.outputPorts[i]; if (port1.displayName !== port2.displayName) { return false; } } // If all checks pass, they are equal return true; } ================================================ FILE: frontend/src/app/workspace/types/result-table.interface.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * This file contains type declarations related to result panel data table. */ /** * Since only type `any` is indexable in typescript, as shown in * https://basarat.gitbooks.io/typescript/docs/types/index-signatures.html, * we need to explicitly define an `Indexable Types` described in * https://www.typescriptlang.org/docs/handbook/interfaces.html * to make `row` indexable and execute operation like `row[col]`. */ export interface IndexableObject extends Readonly<{ [key: string]: object | string | boolean | symbol | number | Array; }> {} /** * This type represent the function type interface for * retreiving each attribute from each result row. * Given a row, extract the cell value of each column. */ type TableCellMethod = (row: IndexableObject) => object | string | number | boolean; /** * TableColumn specifies the information about each column. * It has: * - columnDef - the value to reference that column * - header - the header of that column, which is the text to be displayed on the GUI * - getCell - a function that returns the cell value that will be dispalyed in each cell of the data table */ export interface TableColumn extends Readonly<{ columnDef: string; header: string; getCell: TableCellMethod; }> {} export const PAGINATION_INFO_STORAGE_KEY = "result-panel-pagination-info"; export interface ViewResultOperatorInfo extends Readonly<{ currentResult: object[]; currentPageIndex: number; total: number; columnKeys: string[]; operatorID: string; }> {} /** * ResultPaginationInfo stores pagination information * that is needed for status retainment of the result panel */ export interface ResultPaginationInfo extends Readonly<{ newWorkflowExecuted: boolean; viewResultOperatorInfoMap: Map; }> {} ================================================ FILE: frontend/src/app/workspace/types/shared-editing.interface.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import * as Y from "yjs"; import * as _ from "lodash"; import { isDefined } from "../../common/util/predicate"; export type YTextify = T extends string ? Y.Text : T; export type YArrayify = T extends Array ? Y.Array : T; /** * YType is the yjs-object version of a normal js object with type T. * * Additionally, YType preserves keyof requirements from the original object. * * toJSON() converts the YType back to a normal js object. */ export type YType = Omit, "get" | "set" | "has" | "toJSON"> & { get(key: TKey): YArrayify>; set(key: TKey, value: YArrayify>): void; has(key: TKey): boolean; toJSON(): T; }; /** Creates a YType given a normal object. Returns either a YType, * or the original object if it is a primitive type other than string, because string will be converted to * Y.Text. * @param obj: a normal object, could be either a string, an array, or a complicated object with its own attributes. * Note it is NOT supposed to be a primitive type (if you pass a primitive type into this function the TS code will not * compile), but we handle the case of primitive type and return it as-is because we do the conversion recursively * to the deepest level in the obj using this same function, so during runtime this function might be called * on primitive types. */ export function createYTypeFromObject(obj: T): YType { if (obj === null || obj === undefined) return obj; const originalType = typeof (obj as any); switch (originalType) { case "bigint": case "boolean": case "function": case "number": case "symbol": case "undefined": return obj as any; case "string": return new Y.Text(obj as unknown as string) as unknown as YType; case "object": { const objType = obj.constructor.name; if (objType === "String") { return new Y.Text(obj as unknown as string) as unknown as YType; } else if (objType === "Array") { const yArray = new Y.Array(); // Create YType for each array item and push for (const item of obj as any) { if (isDefined(item)) yArray.push([createYTypeFromObject(item) as unknown]); } return yArray as unknown as YType; } else if (objType === "Object") { // return new const yMap = new Y.Map(); Object.keys(obj).forEach((k: string) => { const value = obj[k as keyof T] as any as object; if (value !== undefined) { yMap.set(k, createYTypeFromObject(value)); } }); return yMap as unknown as YType; } else { // All other objects that cannot be processed. throw TypeError(`Cannot create YType from ${objType}!`); } } } } /** * Updates a YType in-place given a new normal object version of this YType. * @param oldYObj The old YType to be updated. * @param newObj The new normal object, must be the same template type as the YType to be updated. */ export function updateYTypeFromObject(oldYObj: YType, newObj: T): boolean { if (newObj === null || newObj === undefined || oldYObj === null || oldYObj === undefined) return false; const originalNewObjType = typeof newObj; switch (originalNewObjType) { case "bigint": case "boolean": case "number": case "symbol": case "undefined": case "function": return false; case "string": { const yText = oldYObj as unknown as Y.Text; if (yText.toJSON() !== (newObj as unknown as string)) { // Inplace update. yText.delete(0, yText.length); yText.insert(0, newObj as unknown as string); } return true; } case "object": break; } const newObjType = newObj.constructor.name; const oldObjType = oldYObj.toJSON().constructor.name; if (newObjType !== oldObjType) return false; if (newObjType === "String") { const yText = oldYObj as unknown as Y.Text; if (yText.toJSON() !== (newObj as unknown as string)) { // Inplace update. yText.delete(0, yText.length); yText.insert(0, newObj as unknown as string); } } else if (newObjType === "Array") { const oldYObjAsYArray = oldYObj as unknown as Y.Array; const newObjAsArr = newObj as any[]; const oldObjAsArr = oldYObjAsYArray.toJSON(); const oldArrLen = oldObjAsArr.length; const newArrLen = newObjAsArr.length; const toYValue = (value: any) => { const res = createYTypeFromObject(value); return res === undefined ? null : res; }; // lcsLengthTable[i][j] = longest common subsequence length between // oldObjAsArr[i:] and newObjAsArr[j:]. const lcsLengthTable: number[][] = Array.from({ length: oldArrLen + 1 }, () => Array(newArrLen + 1).fill(0)); for (let oldIndex = oldArrLen - 1; oldIndex >= 0; oldIndex--) { for (let newIndex = newArrLen - 1; newIndex >= 0; newIndex--) { if (_.isEqual(oldObjAsArr[oldIndex], newObjAsArr[newIndex])) { lcsLengthTable[oldIndex][newIndex] = lcsLengthTable[oldIndex + 1][newIndex + 1] + 1; } else { lcsLengthTable[oldIndex][newIndex] = Math.max( lcsLengthTable[oldIndex + 1][newIndex], lcsLengthTable[oldIndex][newIndex + 1] ); } } } // Recover aligned equal positions. const matchedIndexPairs: Array<[number, number]> = []; let oldIndex = 0; let newIndex = 0; while (oldIndex < oldArrLen && newIndex < newArrLen) { if (_.isEqual(oldObjAsArr[oldIndex], newObjAsArr[newIndex])) { matchedIndexPairs.push([oldIndex, newIndex]); oldIndex++; newIndex++; } else if (lcsLengthTable[oldIndex + 1][newIndex] >= lcsLengthTable[oldIndex][newIndex + 1]) { oldIndex++; } else { newIndex++; } } // Build unmatched segments between aligned equal positions. const unmatchedSegments: Array<{ oldStartIndex: number; oldEndIndex: number; newStartIndex: number; newEndIndex: number; }> = []; let nextOldSegmentStart = 0; let nextNewSegmentStart = 0; for (const [matchedOldIndex, matchedNewIndex] of matchedIndexPairs) { if (nextOldSegmentStart < matchedOldIndex || nextNewSegmentStart < matchedNewIndex) { unmatchedSegments.push({ oldStartIndex: nextOldSegmentStart, oldEndIndex: matchedOldIndex, newStartIndex: nextNewSegmentStart, newEndIndex: matchedNewIndex, }); } nextOldSegmentStart = matchedOldIndex + 1; nextNewSegmentStart = matchedNewIndex + 1; } if (nextOldSegmentStart < oldArrLen || nextNewSegmentStart < newArrLen) { unmatchedSegments.push({ oldStartIndex: nextOldSegmentStart, oldEndIndex: oldArrLen, newStartIndex: nextNewSegmentStart, newEndIndex: newArrLen, }); } // Apply from right to left so array indices remain stable. for (let segmentIndex = unmatchedSegments.length - 1; segmentIndex >= 0; segmentIndex--) { const { oldStartIndex, oldEndIndex, newStartIndex, newEndIndex } = unmatchedSegments[segmentIndex]; const oldSegmentLength = oldEndIndex - oldStartIndex; const newSegmentLength = newEndIndex - newStartIndex; const overlappingLength = Math.min(oldSegmentLength, newSegmentLength); // Update overlapping items in place where possible. for (let segmentOffset = overlappingLength - 1; segmentOffset >= 0; segmentOffset--) { const arrayIndex = oldStartIndex + segmentOffset; const newValue = newObjAsArr[newStartIndex + segmentOffset]; if (!_.isEqual(oldObjAsArr[arrayIndex], newValue)) { if (!updateYTypeFromObject(oldYObjAsYArray.get(arrayIndex), newValue)) { oldYObjAsYArray.delete(arrayIndex, 1); oldYObjAsYArray.insert(arrayIndex, [toYValue(newValue)]); } } } // Delete remaining old items in this segment. if (oldSegmentLength > newSegmentLength) { oldYObjAsYArray.delete(oldStartIndex + overlappingLength, oldSegmentLength - newSegmentLength); } // Insert remaining new items in this segment. if (newSegmentLength > oldSegmentLength) { const insertedYValues = newObjAsArr.slice(newStartIndex + overlappingLength, newEndIndex).map(toYValue); oldYObjAsYArray.insert(oldStartIndex + overlappingLength, insertedYValues); } } } else if (newObjType === "Object") { const oldYObjAsYMap = oldYObj as unknown as Y.Map; const oldObj = oldYObjAsYMap.toJSON() as T; const keySet = new Set([...Object.keys(oldObj), ...Object.keys(newObj)]); keySet.forEach((k: string) => { const newValue = newObj[k as keyof T] as any; if (!_.isEqual(oldObj[k as keyof T], newValue)) { if (!updateYTypeFromObject(oldYObjAsYMap.get(k), newValue)) { if (newValue !== undefined) { oldYObjAsYMap.set(k, createYTypeFromObject(newValue)); } } } }); } else { return false; } return true; } ================================================ FILE: frontend/src/app/workspace/types/workflow-common.interface.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { JSONSchema7 } from "json-schema"; /** * This file contains multiple type declarations related to workflow-graph. * These type declarations should be identical to the backend API. */ export interface Point extends Readonly<{ x: number; y: number; }> {} export interface LogicalPort extends Readonly<{ operatorID: string; portID: string; }> {} export type PartitionInfo = | Readonly<{ type: "hash"; hashAttributeNames: string[] }> | Readonly<{ type: "range"; rangeAttributeNames: string[]; rangeMin: number; rangeMax: number }> | Readonly<{ type: "single" }> | Readonly<{ type: "broadcast" }> | Readonly<{ type: "none" }>; export interface PortSchema extends Readonly<{ jsonSchema: Readonly; }> {} export interface PortProperty extends Readonly<{ partitionInfo: PartitionInfo; dependencies: { id: number; internal: boolean }[] }> {} export interface PortDescription extends Readonly<{ portID: string; displayName?: string; disallowMultiInputs?: boolean; isDynamicPort?: boolean; partitionRequirement?: PartitionInfo; dependencies?: { id: number; internal: boolean }[]; }> {} export interface OperatorPredicate extends Readonly<{ operatorID: string; operatorType: string; operatorVersion: string; operatorProperties: Readonly<{ [key: string]: any }>; inputPorts: PortDescription[]; outputPorts: PortDescription[]; dynamicInputPorts?: boolean; dynamicOutputPorts?: boolean; showAdvanced: boolean; isDisabled?: boolean; viewResult?: boolean; markedForReuse?: boolean; customDisplayName?: string; }> {} export interface Comment extends Readonly<{ content: string; creationTime: string; creatorName: string; creatorID: number; }> {} export interface CommentBox { commentBoxID: string; comments: Comment[]; commentBoxPosition: Point; } export interface OperatorLink extends Readonly<{ linkID: string; source: LogicalPort; target: LogicalPort; }> {} /** * refer to src/main/scalapb/org/apache/texera/web/workflowruntimestate/ConsoleMessage.scala */ export type ConsoleMessage = Readonly<{ workerId: string; timestamp: { nanos: number; seconds: number; }; msgType: { name: string; }; source: string; title: string; message: string; }>; export type ConsoleUpdateEvent = Readonly<{ operatorId: string; messages: ReadonlyArray; }>; export type BreakpointInfo = Readonly<{ breakpointId: number | undefined; condition: string; hit: boolean; }>; ================================================ FILE: frontend/src/app/workspace/types/workflow-compiling.interface.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { PhysicalPlan } from "../../common/type/physical-plan"; import { WorkflowFatalError } from "./workflow-websocket.interface"; /** * The backend interface of the return object of a successful/failed workflow compilation * * An example data format for AutocompleteSuccessResult will look like: * { * physicalPlan: Physical Plan | Null(if compilation failed), * operatorInputSchemas: { * 'operatorID1' : [ ['attribute1','attribute2','attribute3'] ], * 'operatorID2' : [ [ {attributeName: 'name', attributeType: 'string'}, * {attributeName: 'text', attributeType: 'string'}, * {attributeName: 'follower_count', attributeType: 'string'} ] ] * * } * } */ export interface WorkflowCompilationResponse extends Readonly<{ physicalPlan?: PhysicalPlan; operatorOutputSchemas: { [key: string]: OperatorPortSchemaMap; }; operatorErrors: { [opId: string]: WorkflowFatalError; }; }> {} export enum CompilationState { Uninitialized = "Uninitialized", Succeeded = "Succeeded", Failed = "Failed", } export type CompilationStateInfo = Readonly< | { // indicates the compilation is successful state: CompilationState.Succeeded; // physicalPlan compiled from current logical plan physicalPlan: PhysicalPlan; // a map from opId to OperatorSchema operatorOutputPortSchemaMap: Readonly>; } | { state: CompilationState.Uninitialized; } | { state: CompilationState.Failed; operatorOutputPortSchemaMap: Readonly>; operatorErrors: Readonly>; } >; // possible types of an attribute export type AttributeType = "string" | "integer" | "double" | "boolean" | "long" | "timestamp" | "binary"; // schema: an array of attribute names and types export interface SchemaAttribute extends Readonly<{ attributeName: string; attributeType: AttributeType; }> {} export type PortSchema = ReadonlyArray; // schema of an operator: a map from serialized PortIdentity to port schema export type OperatorPortSchemaMap = Readonly>; ================================================ FILE: frontend/src/app/workspace/types/workflow-websocket.interface.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { ExecutionState, LogicalOperator, LogicalPlan, OperatorStatsUpdate, WebOutputMode, WorkflowResultUpdateEvent, } from "./execute-workflow.interface"; import { IndexableObject } from "./result-table.interface"; import { ConsoleUpdateEvent } from "./workflow-common.interface"; import { SchemaAttribute } from "./workflow-compiling.interface"; /** * @fileOverview Type Definitions of WebSocket (Ws) API * WebSocket API can be either * either "Request" types - messages from frontend to server * or "Event" types - messages from server to frontend. * * Each type definition MUST follow the following rules: * in either TexeraWebsocketRequestTypeMap or TexeraWebsocketEventTypeMap * add a map entry: * 1. key is the 'type' string, it must be the same as corresponding backend class name * 2. value is the payload this request/event needs */ export interface WorkflowExecuteRequest extends Readonly<{ executionName: string; engineVersion: string; logicalPlan: LogicalPlan; }> {} export interface ReplayExecutionInfo extends Readonly<{ eid: number; interaction: string; }> {} export interface WorkflowFatalError extends Readonly<{ message: string; details: string; operatorId: string; workerId: string; type: { name: string; }; timestamp: { nanos: number; seconds: number; }; }> {} export interface WorkflowErrorEvent extends Readonly<{ fatalErrors: ReadonlyArray; }> {} export type ModifyOperatorLogic = Readonly<{ operator: LogicalOperator; }>; export type WorkerTuples = Readonly<{ workerID: string; tuple: ReadonlyArray; }>; export type OperatorCurrentTuples = Readonly<{ operatorID: string; tuples: ReadonlyArray; }>; export type PaginationRequest = Readonly<{ requestID: string; operatorID: string; pageIndex: number; pageSize: number; columnOffset?: number; columnLimit?: number; columnSearch?: string; }>; export type PaginatedResultEvent = Readonly<{ requestID: string; operatorID: string; pageIndex: number; table: ReadonlyArray; schema: ReadonlyArray; }>; export type ResultExportRequest = Readonly<{ exportType: string; workflowId: number; workflowName: string; operatorId: string; operatorName: string; datasetIds: ReadonlyArray; rowIndex: number; columnIndex: number; filename: string; }>; export type ResultExportResponse = Readonly<{ status: "success" | "error"; message: string; }>; export type OperatorAvailableResult = Readonly<{ operatorID: string; cacheValid: boolean; outputMode: WebOutputMode; }>; export type WorkflowAvailableResultEvent = Readonly<{ availableOperators: ReadonlyArray; }>; export type OperatorResultCacheStatus = "cache invalid" | "cache valid"; export interface CacheStatusUpdateEvent extends Readonly<{ cacheStatusMap: Record; }> {} export type PythonExpressionEvaluateRequest = Readonly<{ expression: string; operatorId: string; }>; export type TypedValue = Readonly<{ expression: string; valueRef: string; valueStr: string; valueType: string; expandable: boolean; }>; export type EvaluatedValue = Readonly<{ value: TypedValue; attributes: TypedValue[]; }>; export type PythonExpressionEvaluateResponse = Readonly<{ expression: string; values: EvaluatedValue[]; }>; export type WorkerAssignmentUpdateEvent = Readonly<{ operatorId: string; workerIds: readonly string[]; }>; export type ExecutionDurationUpdateEvent = Readonly<{ duration: number; isRunning: boolean; }>; export type ClusterStatusUpdateEvent = Readonly<{ numWorkers: number; }>; export type RegionUpdateEvent = Readonly<{ regions: readonly [number, string[]][]; }>; export type RegionStateEvent = Readonly<{ id: number; state: string; }>; export type ModifyLogicResponse = Readonly<{ opId: string; isValid: boolean; errorMessage: string; }>; export type ModifyLogicCompletedEvent = Readonly<{ opIds: readonly string[]; }>; export type DebugCommandRequest = Readonly<{ operatorId: string; workerId: string; cmd: string; }>; export type WorkflowStateInfo = Readonly<{ state: ExecutionState; }>; export type TexeraWebsocketRequestTypeMap = { EditingTimeCompilationRequest: LogicalPlan; HeartBeatRequest: {}; ModifyLogicRequest: ModifyOperatorLogic; ResultExportRequest: ResultExportRequest; ResultPaginationRequest: PaginationRequest; RetryRequest: { workers: ReadonlyArray }; SkipTupleRequest: { workers: ReadonlyArray }; WorkflowExecuteRequest: WorkflowExecuteRequest; WorkflowKillRequest: {}; WorkflowPauseRequest: {}; WorkflowCheckpointRequest: {}; WorkflowResumeRequest: {}; PythonExpressionEvaluateRequest: PythonExpressionEvaluateRequest; DebugCommandRequest: DebugCommandRequest; }; export type TexeraWebsocketEventTypeMap = { HeartBeatResponse: {}; WorkflowStateEvent: WorkflowStateInfo; OperatorStatisticsUpdateEvent: OperatorStatsUpdate; WebResultUpdateEvent: WorkflowResultUpdateEvent; RecoveryStartedEvent: {}; WorkflowErrorEvent: WorkflowErrorEvent; ConsoleUpdateEvent: ConsoleUpdateEvent; OperatorCurrentTuplesUpdateEvent: OperatorCurrentTuples; PaginatedResultEvent: PaginatedResultEvent; ResultExportResponse: ResultExportResponse; WorkflowAvailableResultEvent: WorkflowAvailableResultEvent; CacheStatusUpdateEvent: CacheStatusUpdateEvent; PythonExpressionEvaluateResponse: PythonExpressionEvaluateResponse; WorkerAssignmentUpdateEvent: WorkerAssignmentUpdateEvent; ModifyLogicResponse: ModifyLogicResponse; ModifyLogicCompletedEvent: ModifyLogicCompletedEvent; ExecutionDurationUpdateEvent: ExecutionDurationUpdateEvent; ClusterStatusUpdateEvent: ClusterStatusUpdateEvent; RegionUpdateEvent: RegionUpdateEvent; RegionStateEvent: RegionStateEvent; }; // helper type definitions to generate the request and event types type ValueOf = T[keyof T]; type CustomUnionType = ValueOf<{ [P in keyof T]: { type: P; } & T[P]; }>; export type TexeraWebsocketRequestTypes = keyof TexeraWebsocketRequestTypeMap; export type TexeraWebsocketRequest = CustomUnionType; export type TexeraWebsocketEventTypes = keyof TexeraWebsocketEventTypeMap; export type TexeraWebsocketEvent = CustomUnionType; ================================================ FILE: frontend/src/assets/.gitkeep ================================================ ================================================ FILE: frontend/src/assets/logos/site.webmanifest ================================================ { "name": "", "short_name": "", "icons": [ { "src": "assets/logos/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "assets/logos/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } ], "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" } ================================================ FILE: frontend/src/environments/environment.default.ts ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. // The file contains the default environment template // it's used to store app settings and flags to turn on or off different features // AppEnv extends GuiConfig with the build-time production flag export type AppEnv = { /** * whether we are in production mode, this is a build-time flag */ production: boolean; /** * root API URL of the backend */ apiUrl: string; }; export const defaultEnvironment: AppEnv = { production: false, apiUrl: "api", }; ================================================ FILE: frontend/src/environments/environment.prod.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { AppEnv, defaultEnvironment } from "./environment.default"; /** * Production environment configuration. */ export const environment: AppEnv = { ...defaultEnvironment, production: true, }; ================================================ FILE: frontend/src/environments/environment.test.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { AppEnv, defaultEnvironment } from "./environment.default"; export const environment: AppEnv = { ...defaultEnvironment, }; ================================================ FILE: frontend/src/environments/environment.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // The file contents for the current environment will overwrite these during build. // The build system defaults to the dev environment which uses `environment.ts`, but if you do // `ng build --env=prod` then `environment.prod.ts` will be used instead. // The list of which env maps to which file can be found in `.angular-cli.json`. import { AppEnv, defaultEnvironment } from "./environment.default"; export const environment: AppEnv = { ...defaultEnvironment, }; ================================================ FILE: frontend/src/index.html ================================================ Texera ================================================ FILE: frontend/src/jsdom-svg-polyfill.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * jsdom doesn't implement the SVG geometry APIs (`SVGSVGElement#createSVGMatrix`, * `createSVGPoint`, `createSVGTransform`, `getScreenCTM`, `getCTM`, * `getBBox`). jointjs reaches into these during graph layout and crashes * the spec build with `TypeError: svgDocument.createSVGMatrix is not a * function` etc. * * The stubs below return identity-ish geometry: matrices/points behave like * the identity, bounding boxes report zero dimensions. That's enough for * jointjs construction code to not throw; specs that actually depend on * accurate geometry should run under Vitest browser mode rather than * jsdom (tracked in #4861), but the bulk of the texera specs only need * jointjs to instantiate cleanly. */ type AnyFn = (...args: unknown[]) => unknown; function fakeMatrix() { // Minimal SVGMatrix shape — just the methods jointjs touches. const m: Record = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }; m.multiply = () => fakeMatrix(); m.inverse = () => fakeMatrix(); m.translate = () => fakeMatrix(); m.scale = () => fakeMatrix(); m.scaleNonUniform = () => fakeMatrix(); m.rotate = () => fakeMatrix(); m.rotateFromVector = () => fakeMatrix(); m.flipX = () => fakeMatrix(); m.flipY = () => fakeMatrix(); m.skewX = () => fakeMatrix(); m.skewY = () => fakeMatrix(); return m; } function fakePoint() { const p: Record = { x: 0, y: 0 }; p.matrixTransform = () => fakePoint(); return p; } function fakeTransform() { return { type: 0, matrix: fakeMatrix(), angle: 0, setMatrix: () => undefined, setTranslate: () => undefined, setScale: () => undefined, setRotate: () => undefined, setSkewX: () => undefined, setSkewY: () => undefined, }; } function fakeRect() { return { x: 0, y: 0, width: 0, height: 0 }; } const SVG_GLOBAL = (globalThis as unknown as { SVGSVGElement?: { prototype: Record } }).SVGSVGElement; const SVG_ELEMENT_GLOBAL = (globalThis as unknown as { SVGGraphicsElement?: { prototype: Record } }) .SVGGraphicsElement; if (SVG_GLOBAL?.prototype) { const proto = SVG_GLOBAL.prototype; if (typeof proto.createSVGMatrix !== "function") proto.createSVGMatrix = fakeMatrix as AnyFn; if (typeof proto.createSVGPoint !== "function") proto.createSVGPoint = fakePoint as AnyFn; if (typeof proto.createSVGTransform !== "function") proto.createSVGTransform = fakeTransform as AnyFn; if (typeof proto.createSVGTransformFromMatrix !== "function") proto.createSVGTransformFromMatrix = fakeTransform as AnyFn; } if (SVG_ELEMENT_GLOBAL?.prototype) { const proto = SVG_ELEMENT_GLOBAL.prototype; if (typeof proto.getScreenCTM !== "function") proto.getScreenCTM = fakeMatrix as AnyFn; if (typeof proto.getCTM !== "function") proto.getCTM = fakeMatrix as AnyFn; if (typeof proto.getBBox !== "function") proto.getBBox = fakeRect as AnyFn; } /** * jsdom doesn't implement the legacy `document.queryCommandSupported`, * which monaco-editor probes during initialization. Without it the * editor's setup throws even when no spec actually exercises monaco. */ const docProto = (globalThis as unknown as { Document?: { prototype: Record } }).Document?.prototype; if (docProto && typeof docProto.queryCommandSupported !== "function") { docProto.queryCommandSupported = (() => false) as AnyFn; } /** * jsdom doesn't implement `requestIdleCallback` / `cancelIdleCallback` * (a Chrome-only API). Specs that pull in monaco-related modules * crash at construction with `ReferenceError: requestIdleCallback is * not defined`. * * Approximate with `setTimeout` so callbacks still fire. The deadline * argument is a coarse stub — enough for callers that only read * `didTimeout`. */ const idleGlobal = globalThis as unknown as Record; if (typeof idleGlobal.requestIdleCallback !== "function") { idleGlobal.requestIdleCallback = ((cb: (d: { didTimeout: boolean; timeRemaining: () => number }) => void) => setTimeout(() => cb({ didTimeout: false, timeRemaining: () => 50 }), 0)) as AnyFn; } if (typeof idleGlobal.cancelIdleCallback !== "function") { idleGlobal.cancelIdleCallback = ((id: number) => clearTimeout(id)) as AnyFn; } /** * y-websocket schedules a reconnect timer the moment a service that uses * collaborative editing is constructed. When that timer fires AFTER vitest * has begun tearing down the jsdom window, jsdom's WebSocket implementation * crashes during construction (`Cannot read properties of null (reading * '_cookieJar')` → `Invalid value used as weak map key`). Vitest catches * this as an unhandled error and fails the run even though every test * passed. * * Stub WebSocket with an inert no-op so the timer can fire without * touching jsdom. The collaborative-editing specs that actually exercise * WebSocket behaviour are excluded from the test suite (component specs + * the workflow-action suite is the only collaboration-touching active * spec). Real WebSocket testing belongs under Vitest browser mode. */ class InertWebSocket { static readonly CONNECTING = 0; static readonly OPEN = 1; static readonly CLOSING = 2; static readonly CLOSED = 3; readonly CONNECTING = 0; readonly OPEN = 1; readonly CLOSING = 2; readonly CLOSED = 3; readyState = 3; bufferedAmount = 0; binaryType: "blob" | "arraybuffer" = "blob"; url = ""; protocol = ""; extensions = ""; onopen: AnyFn | null = null; onerror: AnyFn | null = null; onmessage: AnyFn | null = null; onclose: AnyFn | null = null; send(): void {} close(): void {} addEventListener(): void {} removeEventListener(): void {} dispatchEvent(): boolean { return false; } constructor(_url?: string, _protocols?: string | string[]) {} } (globalThis as unknown as { WebSocket: typeof InertWebSocket }).WebSocket = InertWebSocket; /** * NgZorro's NzIconService dynamically fetches icon SVGs over HTTP from * `/assets/...` when the icon isn't pre-registered. jsdom's XHR * implementation rejects those requests with an `AggregateError`, and * downstream the icon lookup re-throws as `IconNotFoundError`. Vitest * catches both as unhandled errors, which CI treats as a hard failure * (locally Vitest only reports them as non-fatal warnings). * * Stubbing every spec with `NzIconModule.forChild([...])` for every * icon its template uses is impractical — there are dozens. Instead, * suppress the two specific error patterns at the process level: they * originate inside ngZorro's icon plumbing and don't affect the * assertions specs actually make. */ function isBenignIconError(err: unknown): boolean { const msg = err instanceof Error ? err.message : String(err); return ( msg.includes("[@ant-design/icons-angular]") || (err instanceof Error && err.name === "AggregateError" && /xhr-utils/.test(err.stack ?? "")) ); } process.on("uncaughtException", err => { if (isBenignIconError(err)) return; // Re-throwing inside `uncaughtException` aborts the Node process, which // crashes the Vitest worker mid-run and leaves the runner hanging. console.error(err); }); process.on("unhandledRejection", reason => { if (isBenignIconError(reason)) return; console.error(reason); }); ================================================ FILE: frontend/src/main.test.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Minimal entry for the test-only build configuration. The unit-test // builder uses the buildTarget's `main` to seed the bundle graph; pointing // it at the real `main.ts` pulls AppModule (and every component declared // there) into the spec compile, surfacing template type-check failures // for components that no active spec actually references. This stub // keeps the bundle graph minimal so only the modules each spec imports // directly get compiled. Tests' Angular setup is provided by the unit-test // builder itself (TestBed init). export {}; ================================================ FILE: frontend/src/main.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { enableProdMode, provideZoneChangeDetection } from "@angular/core"; import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; import { AppModule } from "./app/app.module"; import { environment } from "./environments/environment"; if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule, { applicationProviders: [provideZoneChangeDetection()], }) .then(() => { console.log("Texera application bootstrap completed successfully"); }) .catch(err => { console.error("Texera application bootstrap failed:", err); // Let the error propagate so index.html error handler can catch it throw err; }); ================================================ FILE: frontend/src/styles.scss ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ @import "@ali-hm/angular-tree-component/css/angular-tree-component.css"; .ant-menu-item, .ant-menu-submenu-title, img { user-select: none; } .ant-image-preview-img { width: 50%; height: 60%; } .ant-table-pagination { justify-content: center; } .dynamic-fields .ant-form-item { display: inline-block; width: 50%; padding-right: 4px; margin: 0; } .ant-form-item { font-size: 12px; } .ant-form-item-extra { font-size: 11px; line-height: 11px; } .ant-input { padding-top: 0; padding-bottom: 0; } .ant-select-selector { height: 24px !important; } .ant-select-selection-item { line-height: 24px !important; } .ant-form-item-label { padding-bottom: 0 !important; } hr { background-color: #d3d3d3; height: 1px; border: 0; } // due to innerHTML, this rule has to be global .highlight-search-terms { color: blue; } .box { border-radius: 5px; box-shadow: 0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f; } .ant-collapse-header { padding: 5px 8px !important; } .ant-collapse-content-box { padding: 0 !important; } .ant-tabs-tabpane { padding-right: 24px; } .annotation-highlight { background-color: #6a5acd; } // makes avatar text centered in dropdown menu .ant-avatar-string { position: relative; left: 0; } .pve-modal { .ant-modal { max-width: 980px; width: 92vw !important; } .ant-modal-body { padding: 16px 20px 18px; background: #fafafa; } .ant-modal-header { padding: 14px 20px; } .ant-modal-title { font-weight: 600; letter-spacing: 0.2px; } .footer-all { display: flex; justify-content: space-between; align-items: center; width: 100%; gap: 10px; padding: 8px 0; } nz-collapse { display: block; } .ant-collapse { border-radius: 12px; overflow: hidden; background: transparent; } .system-section { margin-top: 5px; margin-bottom: 5px; } .system-section .ant-collapse-item { overflow: hidden; background: #ffffff; border: 1px solid #eef0f3; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03); } .system-section .ant-collapse-header { font-weight: 600; } .system-panel-inner { display: flex; flex-direction: column; gap: 6px; } .package-header-row, .package-inputs { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; align-items: center; } .package-column-label { font-size: 12px; font-weight: 500; color: #666; } .system-row { opacity: 0.9; margin-bottom: 0px; } .system-input { background: #f5f6f8 !important; border-color: #e6e8ec !important; color: #5a667a; cursor: not-allowed; } .env-header { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 12px; } .env-title { font-weight: 600; font-size: 14px; color: #1f2a37; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .env-actions { display: inline-flex; align-items: center; gap: 8px; flex-shrink: 0; } .ant-collapse-item { background: #ffffff; border: 1px solid #eef0f3; overflow: hidden; margin-bottom: 12px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03); } .ant-collapse-header { padding: 12px 14px !important; align-items: center !important; } .ant-collapse-content-box { padding: 14px !important; } .ve-form { display: flex; flex-direction: column; gap: 14px; } .fieldRow { display: flex !important; align-items: center !important; gap: 12px !important; } .fieldLabel { width: 220px; margin: 0; font-weight: 700; white-space: nowrap; } .fieldInput { flex: 1; min-width: 0; } .package-row { display: flex; align-items: flex-end; justify-content: space-between; gap: 10px; bottom: 0px; border: 1px solid #eef0f3; background: #ffffff; } .package-inputs { flex: 1; display: grid; grid-template-columns: 1fr 1fr; gap: 14px; width: 100%; } .field { display: flex; flex-direction: column; gap: 6px; min-width: 0; width: 100%; } .field input { width: 100%; } .field label { font-size: 11px; font-weight: 600; color: #6b7280; line-height: 1; } .operator-select .ant-select { width: 100%; } .ant-input, .ant-select-selector { //border-radius: 10px !important; } .ant-input[disabled] { background: #f5f6f8 !important; border-color: #e6e8ec !important; color: #5a667a; } .env-footer { display: flex; justify-content: flex-end; padding-top: 6px; } .pip-panel { margin-top: 16px; border: 1px solid #d9d9d9; background: #f2f2f2; overflow: hidden; } .pip-panel-header { display: flex; justify-content: space-between; align-items: baseline; padding: 10px 14px; background: #e9e9e9; border-bottom: 1px solid #d9d9d9; } .pip-panel-title { font-weight: 600; color: #222; } .pip-panel-subtitle { font-size: 12px; color: #666; } .pip-panel-body { padding: 0; } .pip-fullscreen-log { color: #333; font-family: "JetBrains Mono", monospace; font-size: 13px; line-height: 1.6; margin: 0; padding: 14px; white-space: pre-wrap; overflow-y: auto; max-height: 220px; background: transparent; } .system-header { display: flex; flex-direction: column; .title { font-weight: 500; } .subtitle { font-size: 12px; color: #8c8c8c; margin-top: 2px; } } } ================================================ FILE: frontend/src/test-zone-setup.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Vitest+Angular doesn't install the ProxyZone wrapper around each test * that Karma+Jasmine implicitly provided. Without a ProxyZone in the * call chain, Angular's `fakeAsync` throws * `Expected to be running in 'ProxyZone'`. * * Wrap Vitest's `it` so each test body runs inside a freshly-forked * ProxyZone. This is a setupFile (referenced from `vitest.config.ts`), * so it executes once per test file before any spec body runs. */ import "zone.js/testing"; type ZoneType = { current: { fork: (spec: object) => { run: (fn: () => T) => T } }; ProxyZoneSpec: new () => object; }; declare const Zone: ZoneType; const ProxyZoneSpec = (Zone as unknown as { ProxyZoneSpec: new () => object }).ProxyZoneSpec; type ItFn = (name: string, fn?: (...args: unknown[]) => unknown, timeout?: number) => unknown; function wrapInProxyZone(target: T): T { const wrapped = ((name: string, fn?: (...args: unknown[]) => unknown, timeout?: number) => { if (!fn) return target(name); return target( name, function wrapper(this: unknown, ...args: unknown[]) { return new Promise((resolve, reject) => { const zone = Zone.current.fork(new ProxyZoneSpec()); zone.run(() => { try { const result = fn.apply(this, args); if (result && typeof (result as Promise).then === "function") { (result as Promise).then(() => resolve(), reject); } else { resolve(); } } catch (e) { reject(e); } }); }); }, timeout ); }) as T; return wrapped; } function patchTestRunner(name: "it" | "test"): void { const g = globalThis as unknown as Record; const original = g[name]; if (typeof original !== "function") return; const wrapped = wrapInProxyZone(original as ItFn); // Forward all enumerable AND non-enumerable properties (.skip, .only, // .todo, .each, .skipIf, .runIf, ...) so callers like `it.todo(...)` // still resolve. Wrap .skip / .only with the same ProxyZone behaviour; // .todo / .each / others pass through unchanged. for (const key of Reflect.ownKeys(original)) { if (key === "length" || key === "name" || key === "prototype") continue; const value = (original as unknown as Record)[key as string]; const transformed = (key === "skip" || key === "only") && typeof value === "function" ? wrapInProxyZone(value as ItFn) : value; Object.defineProperty(wrapped, key, { value: transformed, writable: true, configurable: true, enumerable: true, }); } g[name] = wrapped; } patchTestRunner("it"); patchTestRunner("test"); ================================================ FILE: frontend/src/tsconfig.app.json ================================================ { "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/app", "allowSyntheticDefaultImports": true }, // ask Angular to check template error during the compilation process "angularCompilerOptions": { "fullTemplateTypeCheck": true }, "files": ["main.ts"], "include": ["**/*.d.ts"] } ================================================ FILE: frontend/src/tsconfig.spec.json ================================================ { "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/spec", "types": ["node"] }, "angularCompilerOptions": { "strictTemplates": false, "strictNullInputTypes": false, "fullTemplateTypeCheck": false }, "include": ["**/*.spec.ts", "**/*.d.ts", "vitest-globals.d.ts", "jsdom-svg-polyfill.ts"] } ================================================ FILE: frontend/src/tsconfig.test.json ================================================ { "extends": "./tsconfig.app.json", "compilerOptions": { // The test build (used by `@angular/build:unit-test` for the spec compile) // surfaces template-level strict-null checks that the production build // path doesn't exercise. Loosen them just for the test build so the // migration doesn't drag in app-template fixes that are tracked // separately. "strictNullChecks": false }, "angularCompilerOptions": { "strictTemplates": false, "strictNullInputTypes": false, "fullTemplateTypeCheck": false } } ================================================ FILE: frontend/src/vitest-globals.d.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // Pulls in Vitest's global typings (describe/it/expect/vi/etc.) for spec // files. Used instead of `"types": ["vitest/globals"]` in tsconfig.spec.json // because the parent tsconfig pins typeRoots to `node_modules/@types`, and // Vitest publishes its types from its own package — not via DefinitelyTyped. /// ================================================ FILE: frontend/tools/jschardet-stub/index.js ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // No-op replacement for the LGPL-2.1 `jschardet` package, which is // ASF Category X. Redirected here via `resolutions` in // frontend/package.json. The upstream call site lives in // @codingame/monaco-vscode-api's encoding service and is only reached // when opening binary files through Monaco, which Texera never does. module.exports = { detect: () => null, }; module.exports.default = module.exports; ================================================ FILE: frontend/tools/jschardet-stub/package.json ================================================ { "name": "jschardet", "version": "3.1.3", "description": "Apache-2.0 no-op stub replacing upstream jschardet (LGPL-2.1, ASF Category X).", "license": "Apache-2.0", "main": "index.js" } ================================================ FILE: frontend/tsconfig.json ================================================ { "compileOnSave": false, "compilerOptions": { "allowSyntheticDefaultImports": true, "paths": { "path": [ "./node_modules/path-browserify" ] }, "strict": true, "skipLibCheck": true, "outDir": "./dist/out-tsc", "sourceMap": true, "declaration": false, "moduleResolution": "bundler", "emitDecoratorMetadata": true, "experimentalDecorators": true, "target": "ES2022", "typeRoots": [ "node_modules/@types" ], "lib": [ "ES2022", "dom" ], "module": "esnext", "baseUrl": "./", "useDefineForClassFields": false }, "angularCompilerOptions": { "strictTemplates": true } } ================================================ FILE: frontend/vitest.browser.config.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { fileURLToPath } from "node:url"; import { defineConfig } from "vitest/config"; import { playwright } from "@vitest/browser-playwright"; const uuidBrowser = fileURLToPath(new URL("./node_modules/uuid/dist/esm-browser/index.js", import.meta.url)); const lib0Webcrypto = fileURLToPath(new URL("./node_modules/lib0/webcrypto.js", import.meta.url)); // Browser-mode config for specs that need real DOM/SVG geometry // (getScreenCTM, getBoundingClientRect, pointer-event hit testing). // jsdom's polyfill in src/jsdom-svg-polyfill.ts returns identity stubs, // which is enough to instantiate jointjs but not to compute layout that // click/hit tests depend on. See #4866. export default defineConfig({ // Vite's default resolution picks node entries for transitive deps // because @angular/build:unit-test sets a server-like environment. // Force the browser entry for the two offenders pulled in by // workflow-graph services (uuid + lib0/webcrypto via yjs). resolve: { conditions: ["browser", "module", "import", "default"], alias: [ { find: /^uuid$/, replacement: uuidBrowser }, { find: /^lib0\/webcrypto$/, replacement: lib0Webcrypto }, ], }, test: { globals: true, setupFiles: ["src/test-zone-setup.ts"], browser: { enabled: true, provider: playwright(), headless: true, instances: [{ browser: "chromium" }], }, server: { deps: { inline: [/monaco-breakpoints/, /^uuid$/, /^lib0\//], }, }, }, }); ================================================ FILE: frontend/vitest.config.ts ================================================ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { // Make describe/it/expect/vi/beforeEach/etc available as globals so // existing Jasmine-style specs don't need a per-file import sweep. // Paired with `vitest/globals` triple-slash in src/vitest-globals.d.ts. globals: true, // Wrap `it`/`test` so each spec body runs inside an Angular ProxyZone, // which Angular's `fakeAsync` requires. Karma+Jasmine installed this // implicitly; the @angular/build:unit-test path doesn't. setupFiles: ["src/test-zone-setup.ts"], // monaco-breakpoints' entry does `import './style.css'`. By default // Vitest leaves third-party deps externalized, so Node's ESM loader // tries to import the .css and crashes with // `TypeError: Unknown file extension ".css"`. Inlining the package // routes its imports through Vite/esbuild, which rewrites the CSS // import to a no-op. server: { deps: { inline: [/monaco-breakpoints/], }, }, // Per-spec exclusions live in `angular.json` (the unit-test builder // applies them at the discovery stage, before Vitest's own filter, // which is what the Vitest team recommends — see the Vite warning // when this list is duplicated here.) }, }); ================================================ FILE: licenses/LICENSE-0BSD.txt ================================================ BSD Zero Clause License (0BSD) Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ================================================ FILE: licenses/LICENSE-BSD-2-Clause.txt ================================================ BSD 2-Clause License Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: licenses/LICENSE-BSD-3-Clause.txt ================================================ BSD 3-Clause License Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: licenses/LICENSE-CDDL-1.0.txt ================================================ COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0 1. Definitions. 1.1. "Contributor" means each individual or entity that creates or contributes to the creation of Modifications. 1.2. "Contributor Version" means the combination of the Original Software, prior Modifications used by a Contributor (if any), and the Modifications made by that particular Contributor. 1.3. "Covered Software" means (a) the Original Software, or (b) Modifications, or (c) the combination of files containing Original Software with files containing Modifications, in each case including portions thereof. 1.4. "Executable" means the Covered Software in any form other than Source Code. 1.5. "Initial Developer" means the individual or entity that first makes Original Software available under this License. 1.6. "Larger Work" means a work which combines Covered Software or portions thereof with code not governed by the terms of this License. 1.7. "License" means this document. 1.8. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently acquired, any and all of the rights conveyed herein. 1.9. "Modifications" means the Source Code and Executable form of any of the following: A. Any file that results from an addition to, deletion from or modification of the contents of a file containing Original Software or previous Modifications; B. Any new file that contains any part of the Original Software or previous Modification; or C. Any new file that is contributed or otherwise made available under the terms of this License. 1.10. "Original Software" means the Source Code and Executable form of computer software code that is originally released under this License. 1.11. "Patent Claims" means any patent claim(s), now owned or hereafter acquired, including without limitation, method, process, and apparatus claims, in any patent Licensable by grantor. 1.12. "Source Code" means (a) the common form of computer software code in which modifications are made and (b) associated documentation included in or with such code. 1.13. "You" (or "Your") means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity which controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants. 2.1. The Initial Developer Grant. Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, the Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer, to use, reproduce, modify, display, perform, sublicense and distribute the Original Software (or portions thereof), with or without Modifications, and/or as part of a Larger Work; and (b) under Patent Claims infringed by the making, using or selling of Original Software, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Software (or portions thereof). (c) The licenses granted in Sections 2.1(a) and (b) are effective on the date Initial Developer first distributes or otherwise makes the Original Software available to a third party under the terms of this License. (d) Notwithstanding Section 2.1(b) above, no patent license is granted: (1) for code that You delete from the Original Software, or (2) for infringements caused by: (i) the modification of the Original Software, or (ii) the combination of the Original Software with other software or devices. 2.2. Contributor Grant. Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by Contributor to use, reproduce, modify, display, perform, sublicense and distribute the Modifications created by such Contributor (or portions thereof), either on an unmodified basis, with other Modifications, as Covered Software and/or as part of a Larger Work; and (b) under Patent Claims infringed by the making, using, or selling of Modifications made by that Contributor either alone and/or in combination with its Contributor Version (or portions of such combination), to make, use, sell, offer for sale, have made, and/or otherwise dispose of: (1) Modifications made by that Contributor (or portions thereof); and (2) the combination of Modifications made by that Contributor with its Contributor Version (or portions of such combination). (c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective on the date Contributor first distributes or otherwise makes the Modifications available to a third party. (d) Notwithstanding Section 2.2(b) above, no patent license is granted: (1) for any code that Contributor has deleted from the Contributor Version; (2) for infringements caused by: (i) third party modifications of Contributor Version, or (ii) the combination of Modifications made by that Contributor with other software (except as part of the Contributor Version) or other devices; or (3) under Patent Claims infringed by Covered Software in the absence of Modifications made by that Contributor. 3. Distribution Obligations. 3.1. Availability of Source Code. Any Covered Software that You distribute or otherwise make available in Executable form must also be made available in Source Code form and that Source Code form must be distributed only under the terms of this License. You must include a copy of this License with every copy of the Source Code form of the Covered Software You distribute or otherwise make available. You must inform recipients of any such Covered Software in Executable form as to how they can obtain such Covered Software in Source Code form in a reasonable manner on or through a medium customarily used for software exchange. 3.2. Modifications. The Modifications that You create or to which You contribute are governed by the terms of this License. You represent that You believe Your Modifications are Your original creation(s) and/or You have sufficient rights to grant the rights conveyed by this License. 3.3. Required Notices. You must include a notice in each of Your Modifications that identifies You as the Contributor of the Modification. You may not remove or alter any copyright, patent or trademark notices contained within the Covered Software, or any notices of licensing or any descriptive text giving attribution to any Contributor or the Initial Developer. 3.4. Application of Additional Terms. You may not offer or impose any terms on any Covered Software in Source Code form that alters or restricts the applicable version of this License or the recipients' rights hereunder. You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, you may do so only on Your own behalf, and not on behalf of the Initial Developer or any Contributor. You must make it absolutely clear that any such warranty, support, indemnity or liability obligation is offered by You alone, and You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of warranty, support, indemnity or liability terms You offer. 3.5. Distribution of Executable Versions. You may distribute the Executable form of the Covered Software under the terms of this License or under the terms of a license of Your choice, which may contain terms different from this License, provided that You are in compliance with the terms of this License and that the license for the Executable form does not attempt to limit or alter the recipient's rights in the Source Code form from the rights set forth in this License. If You distribute the Covered Software in Executable form under a different license, You must make it absolutely clear that any terms which differ from this License are offered by You alone, not by the Initial Developer or Contributor. You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of any such terms You offer. 3.6. Larger Works. You may create a Larger Work by combining Covered Software with other code not governed by the terms of this License and distribute the Larger Work as a single product. In such a case, You must make sure the requirements of this License are fulfilled for the Covered Software. 4. Versions of the License. 4.1. New Versions. Sun Microsystems, Inc. is the initial license steward and may publish revised and/or new versions of this License from time to time. Each version will be given a distinguishing version number. Except as provided in Section 4.3, no one other than the license steward has the right to modify this License. 4.2. Effect of New Versions. You may always continue to use, distribute or otherwise make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. If the Initial Developer includes a notice in the Original Software prohibiting it from being distributed or otherwise made available under any subsequent version of the License, You must distribute and make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. Otherwise, You may also choose to use, distribute or otherwise make the Covered Software available under the terms of any subsequent version of the License published by the license steward. 4.3. Modified Versions. When You are an Initial Developer and You want to create a new license for Your Original Software, You may create and use a modified version of this License if You: (a) rename the license and remove any references to the name of the license steward (except to note that the license differs from this License); and (b) otherwise make it clear that the license contains terms which differ from this License. 5. DISCLAIMER OF WARRANTY. COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. 6. TERMINATION. 6.1. This License and the rights granted hereunder will terminate automatically if You fail to comply with terms herein and fail to cure such breach within 30 days of becoming aware of the breach. Provisions which, by their nature, must remain in effect beyond the termination of this License shall survive. 6.2. If You assert a patent infringement claim (excluding declaratory judgment actions) against Initial Developer or a Contributor (the Initial Developer or Contributor against whom You assert such claim is referred to as "Participant") alleging that the Participant Software (meaning the Contributor Version where the Participant is a Contributor or the Original Software where the Participant is the Initial Developer) directly or indirectly infringes any patent, then any and all rights granted directly or indirectly to You by such Participant, the Initial Developer (if the Initial Developer is not the Participant) and all Contributors under Sections 2.1 and/or 2.2 of this License shall, upon 60 days notice from Participant terminate prospectively and automatically at the expiration of such 60 day notice period, unless if within such 60 day period You withdraw Your claim with respect to the Participant Software against such Participant either unilaterally or pursuant to a written agreement with Participant. 6.3. In the event of termination under Sections 6.1 or 6.2 above, all end user licenses that have been validly granted by You or any distributor hereunder prior to termination (excluding licenses granted to You by any distributor) shall survive termination. 7. LIMITATION OF LIABILITY. UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOST PROFITS, LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. 8. U.S. GOVERNMENT END USERS. The Covered Software is a "commercial item," as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer software" (as that term is defined at 48 C.F.R. § 252.227-7014(a)(1)) and "commercial computer software documentation" as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered Software with only those rights set forth herein. This U.S. Government Rights clause is in lieu of, and supersedes, any other FAR, DFAR, or other clause or provision that addresses Government rights in computer software under this License. 9. MISCELLANEOUS. This License represents the complete agreement concerning subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by the law of the jurisdiction specified in a notice contained within the Original Software (except to the extent applicable law, if any, provides otherwise), excluding such jurisdiction's conflict-of-law provisions. Any litigation relating to this License shall be subject to the jurisdiction of the courts located in the jurisdiction and venue specified in a notice contained within the Original Software, with the losing party responsible for costs, including, without limitation, court costs and reasonable attorneys' fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not apply to this License. You agree that You alone are responsible for compliance with the United States export administration regulations (and the export control laws and regulation of any other countries) when You use, distribute or otherwise make available any Covered Software. 10. RESPONSIBILITY FOR CLAIMS. As between Initial Developer and the Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License and You agree to work with Initial Developer and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability. ================================================ FILE: licenses/LICENSE-CDDL-1.1.txt ================================================ COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.1 1. Definitions. 1.1. “Contributor” means each individual or entity that creates or contributes to the creation of Modifications. 1.2. “Contributor Version” means the combination of the Original Software, prior Modifications used by a Contributor (if any), and the Modifications made by that particular Contributor. 1.3. “Covered Software” means (a) the Original Software, or (b) Modifications, or (c) the combination of files containing Original Software with files containing Modifications, in each case including portions thereof. 1.4. “Executable” means the Covered Software in any form other than Source Code. 1.5. “Initial Developer” means the individual or entity that first makes Original Software available under this License. 1.6. “Larger Work” means a work which combines Covered Software or portions thereof with code not governed by the terms of this License. 1.7. “License” means this document. 1.8. “Licensable” means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently acquired, any and all of the rights conveyed herein. 1.9. “Modifications” means the Source Code and Executable form of any of the following: A. Any file that results from an addition to, deletion from or modification of the contents of a file containing Original Software or previous Modifications; B. Any new file that contains any part of the Original Software or previous Modification; or C. Any new file that is contributed or otherwise made available under the terms of this License. 1.10. “Original Software” means the Source Code and Executable form of computer software code that is originally released under this License. 1.11. “Patent Claims” means any patent claim(s), now owned or hereafter acquired, including without limitation, method, process, and apparatus claims, in any patent Licensable by grantor. 1.12. “Source Code” means (a) the common form of computer software code in which modifications are made and (b) associated documentation included in or with such code. 1.13. “You” (or “Your”) means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, “You” includes any entity which controls, is controlled by, or is under common control with You. For purposes of this definition, “control” means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants. 2.1. The Initial Developer Grant. Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, the Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer, to use, reproduce, modify, display, perform, sublicense and distribute the Original Software (or portions thereof), with or without Modifications, and/or as part of a Larger Work; and (b) under Patent Claims infringed by the making, using or selling of Original Software, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Software (or portions thereof). (c) The licenses granted in Sections 2.1(a) and (b) are effective on the date Initial Developer first distributes or otherwise makes the Original Software available to a third party under the terms of this License. (d) Notwithstanding Section 2.1(b) above, no patent license is granted: (1) for code that You delete from the Original Software, or (2) for infringements caused by: (i) the modification of the Original Software, or (ii) the combination of the Original Software with other software or devices. 2.2. Contributor Grant. Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by Contributor to use, reproduce, modify, display, perform, sublicense and distribute the Modifications created by such Contributor (or portions thereof), either on an unmodified basis, with other Modifications, as Covered Software and/or as part of a Larger Work; and (b) under Patent Claims infringed by the making, using, or selling of Modifications made by that Contributor either alone and/or in combination with its Contributor Version (or portions of such combination), to make, use, sell, offer for sale, have made, and/or otherwise dispose of: (1) Modifications made by that Contributor (or portions thereof); and (2) the combination of Modifications made by that Contributor with its Contributor Version (or portions of such combination). (c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective on the date Contributor first distributes or otherwise makes the Modifications available to a third party. (d) Notwithstanding Section 2.2(b) above, no patent license is granted: (1) for any code that Contributor has deleted from the Contributor Version; (2) for infringements caused by: (i) third party modifications of Contributor Version, or (ii) the combination of Modifications made by that Contributor with other software (except as part of the Contributor Version) or other devices; or (3) under Patent Claims infringed by Covered Software in the absence of Modifications made by that Contributor. 3. Distribution Obligations. 3.1. Availability of Source Code. Any Covered Software that You distribute or otherwise make available in Executable form must also be made available in Source Code form and that Source Code form must be distributed only under the terms of this License. You must include a copy of this License with every copy of the Source Code form of the Covered Software You distribute or otherwise make available. You must inform recipients of any such Covered Software in Executable form as to how they can obtain such Covered Software in Source Code form in a reasonable manner on or through a medium customarily used for software exchange. 3.2. Modifications. The Modifications that You create or to which You contribute are governed by the terms of this License. You represent that You believe Your Modifications are Your original creation(s) and/or You have sufficient rights to grant the rights conveyed by this License. 3.3. Required Notices. You must include a notice in each of Your Modifications that identifies You as the Contributor of the Modification. You may not remove or alter any copyright, patent or trademark notices contained within the Covered Software, or any notices of licensing or any descriptive text giving attribution to any Contributor or the Initial Developer. 3.4. Application of Additional Terms. You may not offer or impose any terms on any Covered Software in Source Code form that alters or restricts the applicable version of this License or the recipients' rights hereunder. You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, you may do so only on Your own behalf, and not on behalf of the Initial Developer or any Contributor. You must make it absolutely clear that any such warranty, support, indemnity or liability obligation is offered by You alone, and You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of warranty, support, indemnity or liability terms You offer. 3.5. Distribution of Executable Versions. You may distribute the Executable form of the Covered Software under the terms of this License or under the terms of a license of Your choice, which may contain terms different from this License, provided that You are in compliance with the terms of this License and that the license for the Executable form does not attempt to limit or alter the recipient's rights in the Source Code form from the rights set forth in this License. If You distribute the Covered Software in Executable form under a different license, You must make it absolutely clear that any terms which differ from this License are offered by You alone, not by the Initial Developer or Contributor. You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of any such terms You offer. 3.6. Larger Works. You may create a Larger Work by combining Covered Software with other code not governed by the terms of this License and distribute the Larger Work as a single product. In such a case, You must make sure the requirements of this License are fulfilled for the Covered Software. 4. Versions of the License. 4.1. New Versions. Oracle is the initial license steward and may publish revised and/or new versions of this License from time to time. Each version will be given a distinguishing version number. Except as provided in Section 4.3, no one other than the license steward has the right to modify this License. 4.2. Effect of New Versions. You may always continue to use, distribute or otherwise make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. If the Initial Developer includes a notice in the Original Software prohibiting it from being distributed or otherwise made available under any subsequent version of the License, You must distribute and make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. Otherwise, You may also choose to use, distribute or otherwise make the Covered Software available under the terms of any subsequent version of the License published by the license steward. 4.3. Modified Versions. When You are an Initial Developer and You want to create a new license for Your Original Software, You may create and use a modified version of this License if You: (a) rename the license and remove any references to the name of the license steward (except to note that the license differs from this License); and (b) otherwise make it clear that the license contains terms which differ from this License. 5. DISCLAIMER OF WARRANTY. COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN “AS IS” BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. 6. TERMINATION. 6.1. This License and the rights granted hereunder will terminate automatically if You fail to comply with terms herein and fail to cure such breach within 30 days of becoming aware of the breach. Provisions which, by their nature, must remain in effect beyond the termination of this License shall survive. 6.2. If You assert a patent infringement claim (excluding declaratory judgment actions) against Initial Developer or a Contributor (the Initial Developer or Contributor against whom You assert such claim is referred to as “Participant”) alleging that the Participant Software (meaning the Contributor Version where the Participant is a Contributor or the Original Software where the Participant is the Initial Developer) directly or indirectly infringes any patent, then any and all rights granted directly or indirectly to You by such Participant, the Initial Developer (if the Initial Developer is not the Participant) and all Contributors under Sections 2.1 and/or 2.2 of this License shall, upon 60 days notice from Participant terminate prospectively and automatically at the expiration of such 60 day notice period, unless if within such 60 day period You withdraw Your claim with respect to the Participant Software against such Participant either unilaterally or pursuant to a written agreement with Participant. 6.3. If You assert a patent infringement claim against Participant alleging that the Participant Software directly or indirectly infringes any patent where such claim is resolved (such as by license or settlement) prior to the initiation of patent infringement litigation, then the reasonable value of the licenses granted by such Participant under Sections 2.1 or 2.2 shall be taken into account in determining the amount or value of any payment or license. 6.4. In the event of termination under Sections 6.1 or 6.2 above, all end user licenses that have been validly granted by You or any distributor hereunder prior to termination (excluding licenses granted to You by any distributor) shall survive termination. 7. LIMITATION OF LIABILITY. UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. 8. U.S. GOVERNMENT END USERS. The Covered Software is a “commercial item,” as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of “commercial computer software” (as that term is defined at 48 C.F.R. § 252.227-7014(a)(1)) and “commercial computer software documentation” as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered Software with only those rights set forth herein. This U.S. Government Rights clause is in lieu of, and supersedes, any other FAR, DFAR, or other clause or provision that addresses Government rights in computer software under this License. 9. MISCELLANEOUS. This License represents the complete agreement concerning subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by the law of the jurisdiction specified in a notice contained within the Original Software (except to the extent applicable law, if any, provides otherwise), excluding such jurisdiction's conflict-of-law provisions. Any litigation relating to this License shall be subject to the jurisdiction of the courts located in the jurisdiction and venue specified in a notice contained within the Original Software, with the losing party responsible for costs, including, without limitation, court costs and reasonable attorneys' fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not apply to this License. You agree that You alone are responsible for compliance with the United States export administration regulations (and the export control laws and regulation of any other countries) when You use, distribute or otherwise make available any Covered Software. 10. RESPONSIBILITY FOR CLAIMS. As between Initial Developer and the Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License and You agree to work with Initial Developer and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability. NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) The code released under the CDDL shall be governed by the laws of the State of California (excluding conflict-of-law provisions). Any litigation relating to this License shall be subject to the jurisdiction of the Federal Courts of the Northern District of California and the state courts of the State of California, with venue lying in Santa Clara County, California. ================================================ FILE: licenses/LICENSE-EDL-1.0.txt ================================================ Eclipse Distribution License - v 1.0 Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the Eclipse Foundation, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: licenses/LICENSE-EPL-1.0.txt ================================================ Eclipse Public License - v 1.0 THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 1. DEFINITIONS "Contribution" means: a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and b) in the case of each subsequent Contributor: i) changes to the Program, and ii) additions to the Program; where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. "Contributor" means any person or entity that distributes the Program. "Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. "Program" means the Contributions distributed in accordance with this Agreement. "Recipient" means anyone who receives the Program under this Agreement, including all Contributors. 2. GRANT OF RIGHTS a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. 3. REQUIREMENTS A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: a) it complies with the terms and conditions of this Agreement; and b) its license agreement: i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. When the Program is made available in source code form: a) it must be made available under this Agreement; and b) a copy of this Agreement must be included with each copy of the Program. Contributors may not remove or alter any copyright notices contained within the Program. Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. 4. COMMERCIAL DISTRIBUTION Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. 5. NO WARRANTY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. 6. DISCLAIMER OF LIABILITY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 7. GENERAL If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. ================================================ FILE: licenses/LICENSE-EPL-2.0.txt ================================================ Eclipse Public License - v 2.0 THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 1. DEFINITIONS "Contribution" means: a) in the case of the initial Contributor, the initial content Distributed under this Agreement, and b) in the case of each subsequent Contributor: i) changes to the Program, and ii) additions to the Program; where such changes and/or additions to the Program originate from and are Distributed by that particular Contributor. A Contribution "originates" from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include changes or additions to the Program that are not Modified Works. "Contributor" means any person or entity that Distributes the Program. "Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. "Program" means the Contributions Distributed in accordance with this Agreement. "Recipient" means anyone who receives the Program under this Agreement or any Secondary License (as applicable), including Contributors. "Derivative Works" shall mean any work, whether in Source Code or other form, that is based on (or derived from) the Program and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. "Modified Works" shall mean any work in Source Code or other form that results from an addition to, deletion from, or modification of the contents of the Program, including, for purposes of clarity any new file in Source Code form that contains any contents of the Program. Modified Works shall not include works that contain only declarations, interfaces, types, classes, structures, or files of the Program solely in each case in order to link to, bind by name, or subclass the Program or Modified Works thereof. "Distribute" means the acts of a) distributing or b) making available in any manner that enables the transfer of a copy. "Source Code" means the form of a Program preferred for making modifications, including but not limited to software source code, documentation source, and configuration files. "Secondary License" means either the GNU General Public License, Version 2.0, or any later versions of that license, including any exceptions or additional permissions as identified by the initial Contributor. 2. GRANT OF RIGHTS a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, Distribute and sublicense the Contribution of such Contributor, if any, and such Derivative Works. b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in Source Code or other form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to Distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. e) Notwithstanding the terms of any Secondary License, no Contributor makes additional grants to any Recipient (other than those set forth in this Agreement) as a result of such Recipient's receipt of the Program under the terms of a Secondary License (if permitted under the terms of Section 3). 3. REQUIREMENTS 3.1 If a Contributor Distributes the Program in any form, then: a) the Program must also be made available as Source Code, in accordance with section 3.2, and the Contributor must accompany the Program with a statement that the Source Code for the Program is available under this Agreement, and informs Recipients how to obtain it in a reasonable manner on or through a medium customarily used for software exchange; and b) the Contributor may Distribute the Program under a license different than this Agreement, provided that such license: i) effectively disclaims on behalf of all other Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; ii) effectively excludes on behalf of all other Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; iii) does not attempt to limit or alter the recipients' rights in the Source Code under section 3.2; and iv) requires any subsequent distribution of the Program by any party to be under a license that satisfies the requirements of this section 3. 3.2 When the Program is Distributed as Source Code: a) it must be made available under this Agreement, or if the Program (i) is combined with other material in a separate file or files made available under a Secondary License, and (ii) the initial Contributor attached to the Source Code the notice described in Exhibit A of this Agreement, then the Program may be made available under the terms of such Secondary Licenses, and b) a copy of this Agreement must be included with each copy of the Program. 3.3 Contributors may not remove or alter any copyright, patent, trademark, attribution notices, disclaimers of warranty, or limitations of liability ("notices") contained within the Program from any copy of the Program which they Distribute, provided that Contributors may add their own appropriate notices. 4. COMMERCIAL DISTRIBUTION Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. 5. NO WARRANTY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. 6. DISCLAIMER OF LIABILITY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 7. GENERAL If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be Distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to Distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. Nothing in this Agreement is intended to be enforceable by any entity that is not a Contributor or Recipient. No third-party beneficiary rights are created under this Agreement. Exhibit A - Form of Secondary Licenses Notice "This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), version(s), and exceptions or additional permissions here}." Simply including a copy of this Agreement, including this Exhibit A is not sufficient to license the Source Code under Secondary Licenses. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. ================================================ FILE: licenses/LICENSE-ISC.txt ================================================ ISC License Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ================================================ FILE: licenses/LICENSE-MIT-CMU.txt ================================================ MIT-CMU License (SPDX: MIT-CMU, part of the HPND family) The only MIT-CMU-licensed dependency bundled in Apache Texera's binary distribution is Pillow. The full text governing Pillow's redistribution, reproduced verbatim from pillow-*.dist-info/LICENSE, follows: The Python Imaging Library (PIL) is Copyright © 1997-2011 by Secret Labs AB Copyright © 1995-2011 by Fredrik Lundh and contributors Pillow is the friendly PIL fork. It is Copyright © 2010 by Jeffrey A. Clark and contributors Like PIL, Pillow is licensed under the open source MIT-CMU License: By obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read, understood, and will comply with the following terms and conditions: Permission to use, copy, modify and distribute this software and its documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or the author not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ================================================ FILE: licenses/LICENSE-MIT.txt ================================================ MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: licenses/LICENSE-MPL-2.0.txt ================================================ Mozilla Public License Version 2.0 1. Definitions 1.1. “Contributor” means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. “Contributor Version” means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor’s Contribution. 1.3. “Contribution” means Covered Software of a particular Contributor. 1.4. “Covered Software” means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. “Incompatible With Secondary Licenses” means that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. “Executable Form” means any form of the work other than Source Code Form. 1.7. “Larger Work” means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. “License” means this document. 1.9. “Licensable” means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. “Modifications” means any of the following: any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or any new file in Source Code Form that contains any Covered Software. 1.11. “Patent Claims” of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. “Secondary License” means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. “Source Code Form” means the form of the work preferred for making modifications. 1.14. “You” (or “Your”) means an individual or a legal entity exercising rights under this License. For legal entities, “You” includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, “control” means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: for any code that a Contributor has removed from Covered Software; or for infringements caused by: (i) Your and any other third party’s modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients’ rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients’ rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. 6. Disclaimer of Warranty Covered Software is provided under this License on an “as is” basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer. 7. Limitation of Liability Under no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party’s negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You. 8. Litigation Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party’s ability to bring cross-claims or counter-claims. 9. Miscellaneous This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - “Incompatible With Secondary Licenses” Notice This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. ================================================ FILE: licenses/LICENSE-PSF-2.0.txt ================================================ PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated documentation. 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 Python Software Foundation; All Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee. 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python. 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this License Agreement. ================================================ FILE: licenses/LICENSE-Unlicense.txt ================================================ This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to ================================================ FILE: licenses/LICENSE-avro.txt ================================================ Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ---------------------------------------------------------------------- License for Guava classes included in this binary artifact: Copyright: 2006-2015 The Guava Authors License: https://www.apache.org/licenses/LICENSE-2.0 (see above) ================================================ FILE: licenses/LICENSE-aws-sdk.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Note: Other license terms may apply to certain, identified software files contained within or distributed with the accompanying software if such terms are included in the directory containing the accompanying software. Such other license terms will then apply in lieu of the terms of the software license above. ================================================ FILE: licenses/LICENSE-awssdk-third-party-jackson.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: licenses/LICENSE-commons-math3.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. APACHE COMMONS MATH DERIVATIVE WORKS: The Apache commons-math library includes a number of subcomponents whose implementation is derived from original sources written in C or Fortran. License terms of the original sources are reproduced below. =============================================================================== For the lmder, lmpar and qrsolv Fortran routine from minpack and translated in the LevenbergMarquardtOptimizer class in package org.apache.commons.math3.optimization.general Original source copyright and license statement: Minpack Copyright Notice (1999) University of Chicago. All rights reserved Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The end-user documentation included with the redistribution, if any, must include the following acknowledgment: "This product includes software developed by the University of Chicago, as Operator of Argonne National Laboratory. Alternately, this acknowledgment may appear in the software itself, if and wherever such third-party acknowledgments normally appear. 4. WARRANTY DISCLAIMER. THE SOFTWARE IS SUPPLIED "AS IS" WITHOUT WARRANTY OF ANY KIND. THE COPYRIGHT HOLDER, THE UNITED STATES, THE UNITED STATES DEPARTMENT OF ENERGY, AND THEIR EMPLOYEES: (1) DISCLAIM ANY WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE OR NON-INFRINGEMENT, (2) DO NOT ASSUME ANY LEGAL LIABILITY OR RESPONSIBILITY FOR THE ACCURACY, COMPLETENESS, OR USEFULNESS OF THE SOFTWARE, (3) DO NOT REPRESENT THAT USE OF THE SOFTWARE WOULD NOT INFRINGE PRIVATELY OWNED RIGHTS, (4) DO NOT WARRANT THAT THE SOFTWARE WILL FUNCTION UNINTERRUPTED, THAT IT IS ERROR-FREE OR THAT ANY ERRORS WILL BE CORRECTED. 5. LIMITATION OF LIABILITY. IN NO EVENT WILL THE COPYRIGHT HOLDER, THE UNITED STATES, THE UNITED STATES DEPARTMENT OF ENERGY, OR THEIR EMPLOYEES: BE LIABLE FOR ANY INDIRECT, INCIDENTAL, CONSEQUENTIAL, SPECIAL OR PUNITIVE DAMAGES OF ANY KIND OR NATURE, INCLUDING BUT NOT LIMITED TO LOSS OF PROFITS OR LOSS OF DATA, FOR ANY REASON WHATSOEVER, WHETHER SUCH LIABILITY IS ASSERTED ON THE BASIS OF CONTRACT, TORT (INCLUDING NEGLIGENCE OR STRICT LIABILITY), OR OTHERWISE, EVEN IF ANY OF SAID PARTIES HAS BEEN WARNED OF THE POSSIBILITY OF SUCH LOSS OR DAMAGES. =============================================================================== Copyright and license statement for the odex Fortran routine developed by E. Hairer and G. Wanner and translated in GraggBulirschStoerIntegrator class in package org.apache.commons.math3.ode.nonstiff: Copyright (c) 2004, Ernst Hairer Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. =============================================================================== Copyright and license statement for the original lapack fortran routines translated in EigenDecompositionImpl class in package org.apache.commons.math3.linear: Copyright (c) 1992-2008 The University of Tennessee. All rights reserved. $COPYRIGHT$ Additional copyrights may follow $HEADER$ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer listed in this license in the documentation and/or other materials provided with the distribution. - Neither the name of the copyright holders nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. =============================================================================== Copyright and license statement for the original Mersenne twister C routines translated in MersenneTwister class in package org.apache.commons.math3.random: Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura, All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The names of its contributors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. =============================================================================== The class "org.apache.commons.math3.exception.util.LocalizedFormatsTest" is an adapted version of "OrekitMessagesTest" test class for the Orekit library The "org.apache.commons.math3.analysis.interpolation.HermiteInterpolator" has been imported from the Orekit space flight dynamics library. Th Orekit library is described at: https://www.orekit.org/forge/projects/orekit The original files are distributed under the terms of the Apache 2 license which is: Copyright 2010 CS Communication & Systèmes ================================================ FILE: licenses/LICENSE-glassfish-hk2.txt ================================================ # Eclipse Public License - v 2.0 THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 1. DEFINITIONS "Contribution" means: a) in the case of the initial Contributor, the initial content Distributed under this Agreement, and b) in the case of each subsequent Contributor: i) changes to the Program, and ii) additions to the Program; where such changes and/or additions to the Program originate from and are Distributed by that particular Contributor. A Contribution "originates" from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include changes or additions to the Program that are not Modified Works. "Contributor" means any person or entity that Distributes the Program. "Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. "Program" means the Contributions Distributed in accordance with this Agreement. "Recipient" means anyone who receives the Program under this Agreement or any Secondary License (as applicable), including Contributors. "Derivative Works" shall mean any work, whether in Source Code or other form, that is based on (or derived from) the Program and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. "Modified Works" shall mean any work in Source Code or other form that results from an addition to, deletion from, or modification of the contents of the Program, including, for purposes of clarity any new file in Source Code form that contains any contents of the Program. Modified Works shall not include works that contain only declarations, interfaces, types, classes, structures, or files of the Program solely in each case in order to link to, bind by name, or subclass the Program or Modified Works thereof. "Distribute" means the acts of a) distributing or b) making available in any manner that enables the transfer of a copy. "Source Code" means the form of a Program preferred for making modifications, including but not limited to software source code, documentation source, and configuration files. "Secondary License" means either the GNU General Public License, Version 2.0, or any later versions of that license, including any exceptions or additional permissions as identified by the initial Contributor. 2. GRANT OF RIGHTS a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, Distribute and sublicense the Contribution of such Contributor, if any, and such Derivative Works. b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in Source Code or other form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to Distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. e) Notwithstanding the terms of any Secondary License, no Contributor makes additional grants to any Recipient (other than those set forth in this Agreement) as a result of such Recipient's receipt of the Program under the terms of a Secondary License (if permitted under the terms of Section 3). 3. REQUIREMENTS 3.1 If a Contributor Distributes the Program in any form, then: a) the Program must also be made available as Source Code, in accordance with section 3.2, and the Contributor must accompany the Program with a statement that the Source Code for the Program is available under this Agreement, and informs Recipients how to obtain it in a reasonable manner on or through a medium customarily used for software exchange; and b) the Contributor may Distribute the Program under a license different than this Agreement, provided that such license: i) effectively disclaims on behalf of all other Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; ii) effectively excludes on behalf of all other Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; iii) does not attempt to limit or alter the recipients' rights in the Source Code under section 3.2; and iv) requires any subsequent distribution of the Program by any party to be under a license that satisfies the requirements of this section 3. 3.2 When the Program is Distributed as Source Code: a) it must be made available under this Agreement, or if the Program (i) is combined with other material in a separate file or files made available under a Secondary License, and (ii) the initial Contributor attached to the Source Code the notice described in Exhibit A of this Agreement, then the Program may be made available under the terms of such Secondary Licenses, and b) a copy of this Agreement must be included with each copy of the Program. 3.3 Contributors may not remove or alter any copyright, patent, trademark, attribution notices, disclaimers of warranty, or limitations of liability ("notices") contained within the Program from any copy of the Program which they Distribute, provided that Contributors may add their own appropriate notices. 4. COMMERCIAL DISTRIBUTION Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. 5. NO WARRANTY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. 6. DISCLAIMER OF LIABILITY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 7. GENERAL If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be Distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to Distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. Nothing in this Agreement is intended to be enforceable by any entity that is not a Contributor or Recipient. No third-party beneficiary rights are created under this Agreement. Exhibit A - Form of Secondary Licenses Notice "This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), version(s), and exceptions or additional permissions here}." Simply including a copy of this Agreement, including this Exhibit A is not sufficient to license the Source Code under Secondary Licenses. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. --- ## The GNU General Public License (GPL) Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor Boston, MA 02110-1335 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. One line to give the program's name and a brief idea of what it does. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. --- ## CLASSPATH EXCEPTION Linking this library statically or dynamically with other modules is making a combined work based on this library. Thus, the terms and conditions of the GNU General Public License version 2 cover the whole combination. As a special exception, the copyright holders of this library give you permission to link this library with independent modules to produce an executable, regardless of the license terms of these independent modules, and to copy and distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module. An independent module is a module which is not derived from or based on this library. If you modify this library, you may extend this exception to your version of the library, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. ================================================ FILE: licenses/LICENSE-guice.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: licenses/LICENSE-hadoop-shaded.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- This project bundles some components that are also licensed under the Apache License Version 2.0: com.google.guava:guava:jar:30.1.1-jre com.google.j2objc:j2objc-annotations:1.3 com.google.errorprone:error_prone_annotations:2.5.1 -------------------------------------------------------------------------------- This product bundles various third-party components under other open source licenses. This section summarizes those components and their licenses. See licenses-binary/ for text of these licenses. BSD 3-Clause ------------ com.google.protobuf:protobuf-java:3.7.1 MIT License ----------- org.checkerframework:checker-qual:jar:3.8.0 ================================================ FILE: licenses/LICENSE-hadoop.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- This product bundles various third-party components under other open source licenses. This section summarizes those components and their licenses. See licenses/ for text of these licenses. Apache Software Foundation License 2.0 -------------------------------------- hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/checker/AbstractFuture.java hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/checker/TimeoutFuture.java BSD 2-Clause ------------ hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-nativetask/src/main/native/lz4/lz4.{c|h} hadoop-hdfs-project/hadoop-hdfs-native-client/src/main/native/fuse-dfs/util/tree.h hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/native/container-executor/impl/compat/{fstatat|openat|unlinkat}.h BSD 3-Clause ------------ hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/bloom/* hadoop-common-project/hadoop-common/src/main/native/gtest/gtest-all.cc hadoop-common-project/hadoop-common/src/main/native/gtest/include/gtest/gtest.h hadoop-common-project/hadoop-common/src/main/native/src/org/apache/hadoop/util/bulk_crc32_x86.c hadoop-hdfs-project/hadoop-hdfs-native-client/src/main/native/libhdfspp/third_party/protobuf/protobuf/cpp_helpers.h hadoop-hdfs-project/hadoop-hdfs-native-client/src/main/native/libhdfspp/third_party/gmock-1.7.0/*/*.{cc|h} hadoop-tools/hadoop-sls/src/main/html/js/thirdparty/d3.v3.js hadoop-hdfs-project/hadoop-hdfs/src/main/webapps/static/d3-v4.1.1.min.js MIT License ----------- hadoop-hdfs-project/hadoop-hdfs/src/main/webapps/static/bootstrap-3.4.1 hadoop-hdfs-project/hadoop-hdfs/src/main/webapps/static/dataTables.bootstrap.css hadoop-hdfs-project/hadoop-hdfs/src/main/webapps/static/dataTables.bootstrap.js hadoop-hdfs-project/hadoop-hdfs/src/main/webapps/static/dust-full-2.0.0.min.js hadoop-hdfs-project/hadoop-hdfs/src/main/webapps/static/dust-helpers-1.1.1.min.js hadoop-hdfs-project/hadoop-hdfs/src/main/webapps/static/jquery-3.5.1.min.js hadoop-hdfs-project/hadoop-hdfs/src/main/webapps/static/jquery.dataTables.min.js hadoop-hdfs-project/hadoop-hdfs/src/main/webapps/static/moment.min.js hadoop-tools/hadoop-sls/src/main/html/js/thirdparty/bootstrap.min.js hadoop-tools/hadoop-sls/src/main/html/js/thirdparty/jquery.js hadoop-tools/hadoop-sls/src/main/html/css/bootstrap.min.css hadoop-tools/hadoop-sls/src/main/html/css/bootstrap-responsive.min.css hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-applications-catalog/hadoop-yarn-applications-catalog-webapp/node_modules/.bin/r.js hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/resources/webapps/static/dt-1.10.18/* hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/resources/webapps/static/jquery hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/resources/webapps/static/jt/jquery.jstree.js hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/resources/TERMINAL uriparser2 (hadoop-hdfs-project/hadoop-hdfs-native-client/src/main/native/libhdfspp/third_party/uriparser2) hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/native/container-executor/impl/utils/cJSON.[ch] Boost Software License, Version 1.0 ------------- asio-1.10.2 (hadoop-hdfs-project/hadoop-hdfs-native-client/src/main/native/libhdfspp/third_party/asio-1.10.2) rapidxml-1.13 (hadoop-hdfs-project/hadoop-hdfs-native-client/src/main/native/libhdfspp/third_party/rapidxml-1.13) tr2 (hadoop-hdfs-project/hadoop-hdfs-native-client/src/main/native/libhdfspp/third_party/tr2) Public Domain ------------- hadoop-hdfs-project/hadoop-hdfs/src/main/webapps/static/json-bignum.js ================================================ FILE: licenses/LICENSE-iceberg-bundled-guava.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- This binary artifact contains Google Guava. Copyright: 2006-2019 The Guava Authors Home page: https://github.com/google/guava License: http://www.apache.org/licenses/LICENSE-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: licenses/LICENSE-iceberg.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- This product includes a gradle wrapper. * gradlew and gradle/wrapper/gradle-wrapper.properties Copyright: 2010-2019 Gradle Authors. Home page: https://github.com/gradle/gradle License: https://www.apache.org/licenses/LICENSE-2.0 -------------------------------------------------------------------------------- This product includes code from Apache Avro. * Conversion in DecimalWriter is based on Avro's Conversions.DecimalConversion. Copyright: 2014-2017 The Apache Software Foundation. Home page: https://avro.apache.org/ License: https://www.apache.org/licenses/LICENSE-2.0 -------------------------------------------------------------------------------- This product includes code from Apache Parquet. * DynMethods.java * DynConstructors.java * IOUtil.java readFully and tests * ByteBufferInputStream implementations and tests Copyright: 2014-2017 The Apache Software Foundation. Home page: https://parquet.apache.org/ License: https://www.apache.org/licenses/LICENSE-2.0 -------------------------------------------------------------------------------- This product includes code from Cloudera Kite. * SchemaVisitor and visit methods Copyright: 2013-2017 Cloudera Inc. Home page: https://kitesdk.org/ License: https://www.apache.org/licenses/LICENSE-2.0 -------------------------------------------------------------------------------- This product includes code from Presto. * Retry wait and jitter logic in Tasks.java * S3FileIO logic derived from PrestoS3FileSystem.java in S3InputStream.java and S3OutputStream.java * SQL grammar rules for parsing CALL statements in IcebergSqlExtensions.g4 * some aspects of handling stored procedures Copyright: 2016 Facebook and contributors Home page: https://prestodb.io/ License: https://www.apache.org/licenses/LICENSE-2.0 -------------------------------------------------------------------------------- This product includes code from Apache iBATIS. * Hive ScriptRunner.java Copyright: 2004 Clinton Begin Home page: https://ibatis.apache.org/ License: https://www.apache.org/licenses/LICENSE-2.0 -------------------------------------------------------------------------------- This product includes code from Apache Hive. * Hive metastore derby schema in hive-schema-3.1.0.derby.sql Copyright: 2011-2018 The Apache Software Foundation Home page: https://hive.apache.org/ License: https://www.apache.org/licenses/LICENSE-2.0 -------------------------------------------------------------------------------- This product includes code from Apache Spark. * dev/check-license script * vectorized reading of definition levels in BaseVectorizedParquetValuesReader.java * portions of the extensions parser * casting logic in AssignmentAlignmentSupport * implementation of SetAccumulator. * Connector expressions. Copyright: 2011-2018 The Apache Software Foundation Home page: https://spark.apache.org/ License: https://www.apache.org/licenses/LICENSE-2.0 -------------------------------------------------------------------------------- This product includes code from Delta Lake. * AssignmentAlignmentSupport is an independent development but UpdateExpressionsSupport in Delta was used as a reference. * RoaringPositionBitmap is a Java implementation of RoaringBitmapArray in Delta. Copyright: 2020 The Delta Lake Project Authors. Home page: https://delta.io/ License: https://www.apache.org/licenses/LICENSE-2.0 -------------------------------------------------------------------------------- This product includes code from Apache Commons. * Core ArrayUtil. Copyright: 2020 The Apache Software Foundation Home page: https://commons.apache.org/ License: https://www.apache.org/licenses/LICENSE-2.0 -------------------------------------------------------------------------------- This product includes code from Apache HttpComponents Client. * retry and error handling logic in ExponentialHttpRequestRetryStrategy.java Copyright: 1999-2022 The Apache Software Foundation. Home page: https://hc.apache.org/ License: https://www.apache.org/licenses/LICENSE-2.0 -------------------------------------------------------------------------------- This product includes code from Apache Flink. * Parameterized test at class level logic in ParameterizedTestExtension.java * Parameter provider annotation for parameterized tests in Parameters.java * Parameter field annotation for parameterized tests in Parameter.java Copyright: 1999-2022 The Apache Software Foundation. Home page: https://flink.apache.org/ License: https://www.apache.org/licenses/LICENSE-2.0 ================================================ FILE: licenses/LICENSE-jackson-afterburner.txt ================================================ This copy of Jackson JSON processor `jackson-module-afterburner` module is licensed under the Apache (Software) License, version 2.0 ("the License"). See the License for details about distribution rights, and the specific rights regarding derivate works. You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 Additional licensing information exists for following 3rd party library dependencies ### ASM ASM: a very small and fast Java bytecode manipulation framework Copyright (c) 2000-2011 INRIA, France Telecom All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holders nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: licenses/LICENSE-jackson-blackbird.txt ================================================ This copy of Jackson JSON processor `jackson-module-afterburner` module is licensed under the Apache (Software) License, version 2.0 ("the License"). See the License for details about distribution rights, and the specific rights regarding derivative works. You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 Additional licensing information exists for following 3rd party library dependencies ### ASM ASM: a very small and fast Java bytecode manipulation framework Copyright (c) 2000-2011 INRIA, France Telecom All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holders nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: licenses/LICENSE-jackson-core.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: licenses/LICENSE-jakarta-ee.txt ================================================ # Eclipse Public License - v 2.0 THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 1. DEFINITIONS "Contribution" means: a) in the case of the initial Contributor, the initial content Distributed under this Agreement, and b) in the case of each subsequent Contributor: i) changes to the Program, and ii) additions to the Program; where such changes and/or additions to the Program originate from and are Distributed by that particular Contributor. A Contribution "originates" from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include changes or additions to the Program that are not Modified Works. "Contributor" means any person or entity that Distributes the Program. "Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. "Program" means the Contributions Distributed in accordance with this Agreement. "Recipient" means anyone who receives the Program under this Agreement or any Secondary License (as applicable), including Contributors. "Derivative Works" shall mean any work, whether in Source Code or other form, that is based on (or derived from) the Program and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. "Modified Works" shall mean any work in Source Code or other form that results from an addition to, deletion from, or modification of the contents of the Program, including, for purposes of clarity any new file in Source Code form that contains any contents of the Program. Modified Works shall not include works that contain only declarations, interfaces, types, classes, structures, or files of the Program solely in each case in order to link to, bind by name, or subclass the Program or Modified Works thereof. "Distribute" means the acts of a) distributing or b) making available in any manner that enables the transfer of a copy. "Source Code" means the form of a Program preferred for making modifications, including but not limited to software source code, documentation source, and configuration files. "Secondary License" means either the GNU General Public License, Version 2.0, or any later versions of that license, including any exceptions or additional permissions as identified by the initial Contributor. 2. GRANT OF RIGHTS a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, Distribute and sublicense the Contribution of such Contributor, if any, and such Derivative Works. b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in Source Code or other form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to Distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. e) Notwithstanding the terms of any Secondary License, no Contributor makes additional grants to any Recipient (other than those set forth in this Agreement) as a result of such Recipient's receipt of the Program under the terms of a Secondary License (if permitted under the terms of Section 3). 3. REQUIREMENTS 3.1 If a Contributor Distributes the Program in any form, then: a) the Program must also be made available as Source Code, in accordance with section 3.2, and the Contributor must accompany the Program with a statement that the Source Code for the Program is available under this Agreement, and informs Recipients how to obtain it in a reasonable manner on or through a medium customarily used for software exchange; and b) the Contributor may Distribute the Program under a license different than this Agreement, provided that such license: i) effectively disclaims on behalf of all other Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; ii) effectively excludes on behalf of all other Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; iii) does not attempt to limit or alter the recipients' rights in the Source Code under section 3.2; and iv) requires any subsequent distribution of the Program by any party to be under a license that satisfies the requirements of this section 3. 3.2 When the Program is Distributed as Source Code: a) it must be made available under this Agreement, or if the Program (i) is combined with other material in a separate file or files made available under a Secondary License, and (ii) the initial Contributor attached to the Source Code the notice described in Exhibit A of this Agreement, then the Program may be made available under the terms of such Secondary Licenses, and b) a copy of this Agreement must be included with each copy of the Program. 3.3 Contributors may not remove or alter any copyright, patent, trademark, attribution notices, disclaimers of warranty, or limitations of liability ("notices") contained within the Program from any copy of the Program which they Distribute, provided that Contributors may add their own appropriate notices. 4. COMMERCIAL DISTRIBUTION Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. 5. NO WARRANTY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. 6. DISCLAIMER OF LIABILITY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 7. GENERAL If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be Distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to Distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. Nothing in this Agreement is intended to be enforceable by any entity that is not a Contributor or Recipient. No third-party beneficiary rights are created under this Agreement. Exhibit A - Form of Secondary Licenses Notice "This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), version(s), and exceptions or additional permissions here}." Simply including a copy of this Agreement, including this Exhibit A is not sufficient to license the Source Code under Secondary Licenses. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. --- ## The GNU General Public License (GPL) Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor Boston, MA 02110-1335 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. One line to give the program's name and a brief idea of what it does. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. --- ## CLASSPATH EXCEPTION Linking this library statically or dynamically with other modules is making a combined work based on this library. Thus, the terms and conditions of the GNU General Public License version 2 cover the whole combination. As a special exception, the copyright holders of this library give you permission to link this library with independent modules to produce an executable, regardless of the license terms of these independent modules, and to copy and distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module. An independent module is a module which is not derived from or based on this library. If you modify this library, you may extend this exception to your version of the library, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. ================================================ FILE: licenses/LICENSE-javax-activation.txt ================================================ COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0 1. Definitions. 1.1. Contributor. means each individual or entity that creates or contributes to the creation of Modifications. 1.2. Contributor Version. means the combination of the Original Software, prior Modifications used by a Contributor (if any), and the Modifications made by that particular Contributor. 1.3. Covered Software. means (a) the Original Software, or (b) Modifications, or (c) the combination of files containing Original Software with files containing Modifications, in each case including portions thereof. 1.4. Executable. means the Covered Software in any form other than Source Code. 1.5. Initial Developer. means the individual or entity that first makes Original Software available under this License. 1.6. Larger Work. means a work which combines Covered Software or portions thereof with code not governed by the terms of this License. 1.7. License. means this document. 1.8. Licensable. means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently acquired, any and all of the rights conveyed herein. 1.9. Modifications. means the Source Code and Executable form of any of the following: A. Any file that results from an addition to, deletion from or modification of the contents of a file containing Original Software or previous Modifications; B. Any new file that contains any part of the Original Software or previous Modification; or C. Any new file that is contributed or otherwise made available under the terms of this License. 1.10. Original Software. means the Source Code and Executable form of computer software code that is originally released under this License. 1.11. Patent Claims. means any patent claim(s), now owned or hereafter acquired, including without limitation, method, process, and apparatus claims, in any patent Licensable by grantor. 1.12. Source Code. means (a) the common form of computer software code in which modifications are made and (b) associated documentation included in or with such code. 1.13. You. (or .Your.) means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, .You. includes any entity which controls, is controlled by, or is under common control with You. For purposes of this definition, .control. means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants. 2.1. The Initial Developer Grant. Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, the Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer, to use, reproduce, modify, display, perform, sublicense and distribute the Original Software (or portions thereof), with or without Modifications, and/or as part of a Larger Work; and (b) under Patent Claims infringed by the making, using or selling of Original Software, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Software (or portions thereof). (c) The licenses granted in Sections 2.1(a) and (b) are effective on the date Initial Developer first distributes or otherwise makes the Original Software available to a third party under the terms of this License. (d) Notwithstanding Section 2.1(b) above, no patent license is granted: (1) for code that You delete from the Original Software, or (2) for infringements caused by: (i) the modification of the Original Software, or (ii) the combination of the Original Software with other software or devices. 2.2. Contributor Grant. Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by Contributor to use, reproduce, modify, display, perform, sublicense and distribute the Modifications created by such Contributor (or portions thereof), either on an unmodified basis, with other Modifications, as Covered Software and/or as part of a Larger Work; and (b) under Patent Claims infringed by the making, using, or selling of Modifications made by that Contributor either alone and/or in combination with its Contributor Version (or portions of such combination), to make, use, sell, offer for sale, have made, and/or otherwise dispose of: (1) Modifications made by that Contributor (or portions thereof); and (2) the combination of Modifications made by that Contributor with its Contributor Version (or portions of such combination). (c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective on the date Contributor first distributes or otherwise makes the Modifications available to a third party. (d) Notwithstanding Section 2.2(b) above, no patent license is granted: (1) for any code that Contributor has deleted from the Contributor Version; (2) for infringements caused by: (i) third party modifications of Contributor Version, or (ii) the combination of Modifications made by that Contributor with other software (except as part of the Contributor Version) or other devices; or (3) under Patent Claims infringed by Covered Software in the absence of Modifications made by that Contributor. 3. Distribution Obligations. 3.1. Availability of Source Code. Any Covered Software that You distribute or otherwise make available in Executable form must also be made available in Source Code form and that Source Code form must be distributed only under the terms of this License. You must include a copy of this License with every copy of the Source Code form of the Covered Software You distribute or otherwise make available. You must inform recipients of any such Covered Software in Executable form as to how they can obtain such Covered Software in Source Code form in a reasonable manner on or through a medium customarily used for software exchange. 3.2. Modifications. The Modifications that You create or to which You contribute are governed by the terms of this License. You represent that You believe Your Modifications are Your original creation(s) and/or You have sufficient rights to grant the rights conveyed by this License. 3.3. Required Notices. You must include a notice in each of Your Modifications that identifies You as the Contributor of the Modification. You may not remove or alter any copyright, patent or trademark notices contained within the Covered Software, or any notices of licensing or any descriptive text giving attribution to any Contributor or the Initial Developer. 3.4. Application of Additional Terms. You may not offer or impose any terms on any Covered Software in Source Code form that alters or restricts the applicable version of this License or the recipients. rights hereunder. You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, you may do so only on Your own behalf, and not on behalf of the Initial Developer or any Contributor. You must make it absolutely clear that any such warranty, support, indemnity or liability obligation is offered by You alone, and You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of warranty, support, indemnity or liability terms You offer. 3.5. Distribution of Executable Versions. You may distribute the Executable form of the Covered Software under the terms of this License or under the terms of a license of Your choice, which may contain terms different from this License, provided that You are in compliance with the terms of this License and that the license for the Executable form does not attempt to limit or alter the recipient.s rights in the Source Code form from the rights set forth in this License. If You distribute the Covered Software in Executable form under a different license, You must make it absolutely clear that any terms which differ from this License are offered by You alone, not by the Initial Developer or Contributor. You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of any such terms You offer. 3.6. Larger Works. You may create a Larger Work by combining Covered Software with other code not governed by the terms of this License and distribute the Larger Work as a single product. In such a case, You must make sure the requirements of this License are fulfilled for the Covered Software. 4. Versions of the License. 4.1. New Versions. Sun Microsystems, Inc. is the initial license steward and may publish revised and/or new versions of this License from time to time. Each version will be given a distinguishing version number. Except as provided in Section 4.3, no one other than the license steward has the right to modify this License. 4.2. Effect of New Versions. You may always continue to use, distribute or otherwise make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. If the Initial Developer includes a notice in the Original Software prohibiting it from being distributed or otherwise made available under any subsequent version of the License, You must distribute and make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. Otherwise, You may also choose to use, distribute or otherwise make the Covered Software available under the terms of any subsequent version of the License published by the license steward. 4.3. Modified Versions. When You are an Initial Developer and You want to create a new license for Your Original Software, You may create and use a modified version of this License if You: (a) rename the license and remove any references to the name of the license steward (except to note that the license differs from this License); and (b) otherwise make it clear that the license contains terms which differ from this License. 5. DISCLAIMER OF WARRANTY. COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN .AS IS. BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. 6. TERMINATION. 6.1. This License and the rights granted hereunder will terminate automatically if You fail to comply with terms herein and fail to cure such breach within 30 days of becoming aware of the breach. Provisions which, by their nature, must remain in effect beyond the termination of this License shall survive. 6.2. If You assert a patent infringement claim (excluding declaratory judgment actions) against Initial Developer or a Contributor (the Initial Developer or Contributor against whom You assert such claim is referred to as .Participant.) alleging that the Participant Software (meaning the Contributor Version where the Participant is a Contributor or the Original Software where the Participant is the Initial Developer) directly or indirectly infringes any patent, then any and all rights granted directly or indirectly to You by such Participant, the Initial Developer (if the Initial Developer is not the Participant) and all Contributors under Sections 2.1 and/or 2.2 of this License shall, upon 60 days notice from Participant terminate prospectively and automatically at the expiration of such 60 day notice period, unless if within such 60 day period You withdraw Your claim with respect to the Participant Software against such Participant either unilaterally or pursuant to a written agreement with Participant. 6.3. In the event of termination under Sections 6.1 or 6.2 above, all end user licenses that have been validly granted by You or any distributor hereunder prior to termination (excluding licenses granted to You by any distributor) shall survive termination. 7. LIMITATION OF LIABILITY. UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOST PROFITS, LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY.S NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. 8. U.S. GOVERNMENT END USERS. The Covered Software is a .commercial item,. as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of .commercial computer software. (as that term is defined at 48 C.F.R. � 252.227-7014(a)(1)) and .commercial computer software documentation. as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered Software with only those rights set forth herein. This U.S. Government Rights clause is in lieu of, and supersedes, any other FAR, DFAR, or other clause or provision that addresses Government rights in computer software under this License. 9. MISCELLANEOUS. This License represents the complete agreement concerning subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by the law of the jurisdiction specified in a notice contained within the Original Software (except to the extent applicable law, if any, provides otherwise), excluding such jurisdiction.s conflict-of-law provisions. Any litigation relating to this License shall be subject to the jurisdiction of the courts located in the jurisdiction and venue specified in a notice contained within the Original Software, with the losing party responsible for costs, including, without limitation, court costs and reasonable attorneys. fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not apply to this License. You agree that You alone are responsible for compliance with the United States export administration regulations (and the export control laws and regulation of any other countries) when You use, distribute or otherwise make available any Covered Software. 10. RESPONSIBILITY FOR CLAIMS. As between Initial Developer and the Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License and You agree to work with Initial Developer and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability. NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) The code released under the CDDL shall be governed by the laws of the State of California (excluding conflict-of-law provisions). Any litigation relating to this License shall be subject to the jurisdiction of the Federal Courts of the Northern District of California and the state courts of the State of California, with venue lying in Santa Clara County, California. The GNU General Public License (GPL) Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. One line to give the program's name and a brief idea of what it does. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. "CLASSPATH" EXCEPTION TO THE GPL VERSION 2 Certain source files distributed by Sun Microsystems, Inc. are subject to the following clarification and special exception to the GPL Version 2, but only where Sun has expressly included in the particular source file's header the words "Sun designates this particular file as subject to the "Classpath" exception as provided by Sun in the License file that accompanied this code." Linking this library statically or dynamically with other modules is making a combined work based on this library. Thus, the terms and conditions of the GNU General Public License Version 2 cover the whole combination. As a special exception, the copyright holders of this library give you permission to link this library with independent modules to produce an executable, regardless of the license terms of these independent modules, and to copy and distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module.? An independent module is a module which is not derived from or based on this library.? If you modify this library, you may extend this exception to your version of the library, but you are not obligated to do so.? If you do not wish to do so, delete this exception statement from your version. ================================================ FILE: licenses/LICENSE-javax-ee-cddl.txt ================================================ COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0 1. Definitions. 1.1. Contributor. means each individual or entity that creates or contributes to the creation of Modifications. 1.2. Contributor Version. means the combination of the Original Software, prior Modifications used by a Contributor (if any), and the Modifications made by that particular Contributor. 1.3. Covered Software. means (a) the Original Software, or (b) Modifications, or (c) the combination of files containing Original Software with files containing Modifications, in each case including portions thereof. 1.4. Executable. means the Covered Software in any form other than Source Code. 1.5. Initial Developer. means the individual or entity that first makes Original Software available under this License. 1.6. Larger Work. means a work which combines Covered Software or portions thereof with code not governed by the terms of this License. 1.7. License. means this document. 1.8. Licensable. means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently acquired, any and all of the rights conveyed herein. 1.9. Modifications. means the Source Code and Executable form of any of the following: A. Any file that results from an addition to, deletion from or modification of the contents of a file containing Original Software or previous Modifications; B. Any new file that contains any part of the Original Software or previous Modification; or C. Any new file that is contributed or otherwise made available under the terms of this License. 1.10. Original Software. means the Source Code and Executable form of computer software code that is originally released under this License. 1.11. Patent Claims. means any patent claim(s), now owned or hereafter acquired, including without limitation, method, process, and apparatus claims, in any patent Licensable by grantor. 1.12. Source Code. means (a) the common form of computer software code in which modifications are made and (b) associated documentation included in or with such code. 1.13. You. (or .Your.) means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, .You. includes any entity which controls, is controlled by, or is under common control with You. For purposes of this definition, .control. means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants. 2.1. The Initial Developer Grant. Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, the Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer, to use, reproduce, modify, display, perform, sublicense and distribute the Original Software (or portions thereof), with or without Modifications, and/or as part of a Larger Work; and (b) under Patent Claims infringed by the making, using or selling of Original Software, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Software (or portions thereof). (c) The licenses granted in Sections 2.1(a) and (b) are effective on the date Initial Developer first distributes or otherwise makes the Original Software available to a third party under the terms of this License. (d) Notwithstanding Section 2.1(b) above, no patent license is granted: (1) for code that You delete from the Original Software, or (2) for infringements caused by: (i) the modification of the Original Software, or (ii) the combination of the Original Software with other software or devices. 2.2. Contributor Grant. Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by Contributor to use, reproduce, modify, display, perform, sublicense and distribute the Modifications created by such Contributor (or portions thereof), either on an unmodified basis, with other Modifications, as Covered Software and/or as part of a Larger Work; and (b) under Patent Claims infringed by the making, using, or selling of Modifications made by that Contributor either alone and/or in combination with its Contributor Version (or portions of such combination), to make, use, sell, offer for sale, have made, and/or otherwise dispose of: (1) Modifications made by that Contributor (or portions thereof); and (2) the combination of Modifications made by that Contributor with its Contributor Version (or portions of such combination). (c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective on the date Contributor first distributes or otherwise makes the Modifications available to a third party. (d) Notwithstanding Section 2.2(b) above, no patent license is granted: (1) for any code that Contributor has deleted from the Contributor Version; (2) for infringements caused by: (i) third party modifications of Contributor Version, or (ii) the combination of Modifications made by that Contributor with other software (except as part of the Contributor Version) or other devices; or (3) under Patent Claims infringed by Covered Software in the absence of Modifications made by that Contributor. 3. Distribution Obligations. 3.1. Availability of Source Code. Any Covered Software that You distribute or otherwise make available in Executable form must also be made available in Source Code form and that Source Code form must be distributed only under the terms of this License. You must include a copy of this License with every copy of the Source Code form of the Covered Software You distribute or otherwise make available. You must inform recipients of any such Covered Software in Executable form as to how they can obtain such Covered Software in Source Code form in a reasonable manner on or through a medium customarily used for software exchange. 3.2. Modifications. The Modifications that You create or to which You contribute are governed by the terms of this License. You represent that You believe Your Modifications are Your original creation(s) and/or You have sufficient rights to grant the rights conveyed by this License. 3.3. Required Notices. You must include a notice in each of Your Modifications that identifies You as the Contributor of the Modification. You may not remove or alter any copyright, patent or trademark notices contained within the Covered Software, or any notices of licensing or any descriptive text giving attribution to any Contributor or the Initial Developer. 3.4. Application of Additional Terms. You may not offer or impose any terms on any Covered Software in Source Code form that alters or restricts the applicable version of this License or the recipients. rights hereunder. You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, you may do so only on Your own behalf, and not on behalf of the Initial Developer or any Contributor. You must make it absolutely clear that any such warranty, support, indemnity or liability obligation is offered by You alone, and You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of warranty, support, indemnity or liability terms You offer. 3.5. Distribution of Executable Versions. You may distribute the Executable form of the Covered Software under the terms of this License or under the terms of a license of Your choice, which may contain terms different from this License, provided that You are in compliance with the terms of this License and that the license for the Executable form does not attempt to limit or alter the recipient.s rights in the Source Code form from the rights set forth in this License. If You distribute the Covered Software in Executable form under a different license, You must make it absolutely clear that any terms which differ from this License are offered by You alone, not by the Initial Developer or Contributor. You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of any such terms You offer. 3.6. Larger Works. You may create a Larger Work by combining Covered Software with other code not governed by the terms of this License and distribute the Larger Work as a single product. In such a case, You must make sure the requirements of this License are fulfilled for the Covered Software. 4. Versions of the License. 4.1. New Versions. Sun Microsystems, Inc. is the initial license steward and may publish revised and/or new versions of this License from time to time. Each version will be given a distinguishing version number. Except as provided in Section 4.3, no one other than the license steward has the right to modify this License. 4.2. Effect of New Versions. You may always continue to use, distribute or otherwise make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. If the Initial Developer includes a notice in the Original Software prohibiting it from being distributed or otherwise made available under any subsequent version of the License, You must distribute and make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. Otherwise, You may also choose to use, distribute or otherwise make the Covered Software available under the terms of any subsequent version of the License published by the license steward. 4.3. Modified Versions. When You are an Initial Developer and You want to create a new license for Your Original Software, You may create and use a modified version of this License if You: (a) rename the license and remove any references to the name of the license steward (except to note that the license differs from this License); and (b) otherwise make it clear that the license contains terms which differ from this License. 5. DISCLAIMER OF WARRANTY. COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN .AS IS. BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. 6. TERMINATION. 6.1. This License and the rights granted hereunder will terminate automatically if You fail to comply with terms herein and fail to cure such breach within 30 days of becoming aware of the breach. Provisions which, by their nature, must remain in effect beyond the termination of this License shall survive. 6.2. If You assert a patent infringement claim (excluding declaratory judgment actions) against Initial Developer or a Contributor (the Initial Developer or Contributor against whom You assert such claim is referred to as .Participant.) alleging that the Participant Software (meaning the Contributor Version where the Participant is a Contributor or the Original Software where the Participant is the Initial Developer) directly or indirectly infringes any patent, then any and all rights granted directly or indirectly to You by such Participant, the Initial Developer (if the Initial Developer is not the Participant) and all Contributors under Sections 2.1 and/or 2.2 of this License shall, upon 60 days notice from Participant terminate prospectively and automatically at the expiration of such 60 day notice period, unless if within such 60 day period You withdraw Your claim with respect to the Participant Software against such Participant either unilaterally or pursuant to a written agreement with Participant. 6.3. In the event of termination under Sections 6.1 or 6.2 above, all end user licenses that have been validly granted by You or any distributor hereunder prior to termination (excluding licenses granted to You by any distributor) shall survive termination. 7. LIMITATION OF LIABILITY. UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOST PROFITS, LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY.S NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. 8. U.S. GOVERNMENT END USERS. The Covered Software is a .commercial item,. as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of .commercial computer software. (as that term is defined at 48 C.F.R. ? 252.227-7014(a)(1)) and .commercial computer software documentation. as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered Software with only those rights set forth herein. This U.S. Government Rights clause is in lieu of, and supersedes, any other FAR, DFAR, or other clause or provision that addresses Government rights in computer software under this License. 9. MISCELLANEOUS. This License represents the complete agreement concerning subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by the law of the jurisdiction specified in a notice contained within the Original Software (except to the extent applicable law, if any, provides otherwise), excluding such jurisdiction.s conflict-of-law provisions. Any litigation relating to this License shall be subject to the jurisdiction of the courts located in the jurisdiction and venue specified in a notice contained within the Original Software, with the losing party responsible for costs, including, without limitation, court costs and reasonable attorneys. fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not apply to this License. You agree that You alone are responsible for compliance with the United States export administration regulations (and the export control laws and regulation of any other countries) when You use, distribute or otherwise make available any Covered Software. 10. RESPONSIBILITY FOR CLAIMS. As between Initial Developer and the Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License and You agree to work with Initial Developer and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability. NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) The code released under the CDDL shall be governed by the laws of the State of California (excluding conflict-of-law provisions). Any litigation relating to this License shall be subject to the jurisdiction of the Federal Courts of the Northern District of California and the state courts of the State of California, with venue lying in Santa Clara County, California. The GNU General Public License (GPL) Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. One line to give the program's name and a brief idea of what it does. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. "CLASSPATH" EXCEPTION TO THE GPL VERSION 2 Certain source files distributed by Sun Microsystems, Inc. are subject to the following clarification and special exception to the GPL Version 2, but only where Sun has expressly included in the particular source file's header the words "Sun designates this particular file as subject to the "Classpath" exception as provided by Sun in the License file that accompanied this code." Linking this library statically or dynamically with other modules is making a combined work based on this library. Thus, the terms and conditions of the GNU General Public License Version 2 cover the whole combination. As a special exception, the copyright holders of this library give you permission to link this library with independent modules to produce an executable, regardless of the license terms of these independent modules, and to copy and distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module.? An independent module is a module which is not derived from or based on this library.? If you modify this library, you may extend this exception to your version of the library, but you are not obligated to do so.? If you do not wish to do so, delete this exception statement from your version. ================================================ FILE: licenses/LICENSE-javax-mail.txt ================================================ COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.1 1. Definitions. 1.1. "Contributor" means each individual or entity that creates or contributes to the creation of Modifications. 1.2. "Contributor Version" means the combination of the Original Software, prior Modifications used by a Contributor (if any), and the Modifications made by that particular Contributor. 1.3. "Covered Software" means (a) the Original Software, or (b) Modifications, or (c) the combination of files containing Original Software with files containing Modifications, in each case including portions thereof. 1.4. "Executable" means the Covered Software in any form other than Source Code. 1.5. "Initial Developer" means the individual or entity that first makes Original Software available under this License. 1.6. "Larger Work" means a work which combines Covered Software or portions thereof with code not governed by the terms of this License. 1.7. "License" means this document. 1.8. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently acquired, any and all of the rights conveyed herein. 1.9. "Modifications" means the Source Code and Executable form of any of the following: A. Any file that results from an addition to, deletion from or modification of the contents of a file containing Original Software or previous Modifications; B. Any new file that contains any part of the Original Software or previous Modification; or C. Any new file that is contributed or otherwise made available under the terms of this License. 1.10. "Original Software" means the Source Code and Executable form of computer software code that is originally released under this License. 1.11. "Patent Claims" means any patent claim(s), now owned or hereafter acquired, including without limitation, method, process, and apparatus claims, in any patent Licensable by grantor. 1.12. "Source Code" means (a) the common form of computer software code in which modifications are made and (b) associated documentation included in or with such code. 1.13. "You" (or "Your") means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity which controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants. 2.1. The Initial Developer Grant. Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, the Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer, to use, reproduce, modify, display, perform, sublicense and distribute the Original Software (or portions thereof), with or without Modifications, and/or as part of a Larger Work; and (b) under Patent Claims infringed by the making, using or selling of Original Software, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Software (or portions thereof). (c) The licenses granted in Sections 2.1(a) and (b) are effective on the date Initial Developer first distributes or otherwise makes the Original Software available to a third party under the terms of this License. (d) Notwithstanding Section 2.1(b) above, no patent license is granted: (1) for code that You delete from the Original Software, or (2) for infringements caused by: (i) the modification of the Original Software, or (ii) the combination of the Original Software with other software or devices. 2.2. Contributor Grant. Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by Contributor to use, reproduce, modify, display, perform, sublicense and distribute the Modifications created by such Contributor (or portions thereof), either on an unmodified basis, with other Modifications, as Covered Software and/or as part of a Larger Work; and (b) under Patent Claims infringed by the making, using, or selling of Modifications made by that Contributor either alone and/or in combination with its Contributor Version (or portions of such combination), to make, use, sell, offer for sale, have made, and/or otherwise dispose of: (1) Modifications made by that Contributor (or portions thereof); and (2) the combination of Modifications made by that Contributor with its Contributor Version (or portions of such combination). (c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective on the date Contributor first distributes or otherwise makes the Modifications available to a third party. (d) Notwithstanding Section 2.2(b) above, no patent license is granted: (1) for any code that Contributor has deleted from the Contributor Version; (2) for infringements caused by: (i) third party modifications of Contributor Version, or (ii) the combination of Modifications made by that Contributor with other software (except as part of the Contributor Version) or other devices; or (3) under Patent Claims infringed by Covered Software in the absence of Modifications made by that Contributor. 3. Distribution Obligations. 3.1. Availability of Source Code. Any Covered Software that You distribute or otherwise make available in Executable form must also be made available in Source Code form and that Source Code form must be distributed only under the terms of this License. You must include a copy of this License with every copy of the Source Code form of the Covered Software You distribute or otherwise make available. You must inform recipients of any such Covered Software in Executable form as to how they can obtain such Covered Software in Source Code form in a reasonable manner on or through a medium customarily used for software exchange. 3.2. Modifications. The Modifications that You create or to which You contribute are governed by the terms of this License. You represent that You believe Your Modifications are Your original creation(s) and/or You have sufficient rights to grant the rights conveyed by this License. 3.3. Required Notices. You must include a notice in each of Your Modifications that identifies You as the Contributor of the Modification. You may not remove or alter any copyright, patent or trademark notices contained within the Covered Software, or any notices of licensing or any descriptive text giving attribution to any Contributor or the Initial Developer. 3.4. Application of Additional Terms. You may not offer or impose any terms on any Covered Software in Source Code form that alters or restricts the applicable version of this License or the recipients' rights hereunder. You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, you may do so only on Your own behalf, and not on behalf of the Initial Developer or any Contributor. You must make it absolutely clear that any such warranty, support, indemnity or liability obligation is offered by You alone, and You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of warranty, support, indemnity or liability terms You offer. 3.5. Distribution of Executable Versions. You may distribute the Executable form of the Covered Software under the terms of this License or under the terms of a license of Your choice, which may contain terms different from this License, provided that You are in compliance with the terms of this License and that the license for the Executable form does not attempt to limit or alter the recipient's rights in the Source Code form from the rights set forth in this License. If You distribute the Covered Software in Executable form under a different license, You must make it absolutely clear that any terms which differ from this License are offered by You alone, not by the Initial Developer or Contributor. You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of any such terms You offer. 3.6. Larger Works. You may create a Larger Work by combining Covered Software with other code not governed by the terms of this License and distribute the Larger Work as a single product. In such a case, You must make sure the requirements of this License are fulfilled for the Covered Software. 4. Versions of the License. 4.1. New Versions. Oracle is the initial license steward and may publish revised and/or new versions of this License from time to time. Each version will be given a distinguishing version number. Except as provided in Section 4.3, no one other than the license steward has the right to modify this License. 4.2. Effect of New Versions. You may always continue to use, distribute or otherwise make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. If the Initial Developer includes a notice in the Original Software prohibiting it from being distributed or otherwise made available under any subsequent version of the License, You must distribute and make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. Otherwise, You may also choose to use, distribute or otherwise make the Covered Software available under the terms of any subsequent version of the License published by the license steward. 4.3. Modified Versions. When You are an Initial Developer and You want to create a new license for Your Original Software, You may create and use a modified version of this License if You: (a) rename the license and remove any references to the name of the license steward (except to note that the license differs from this License); and (b) otherwise make it clear that the license contains terms which differ from this License. 5. DISCLAIMER OF WARRANTY. COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. 6. TERMINATION. 6.1. This License and the rights granted hereunder will terminate automatically if You fail to comply with terms herein and fail to cure such breach within 30 days of becoming aware of the breach. Provisions which, by their nature, must remain in effect beyond the termination of this License shall survive. 6.2. If You assert a patent infringement claim (excluding declaratory judgment actions) against Initial Developer or a Contributor (the Initial Developer or Contributor against whom You assert such claim is referred to as "Participant") alleging that the Participant Software (meaning the Contributor Version where the Participant is a Contributor or the Original Software where the Participant is the Initial Developer) directly or indirectly infringes any patent, then any and all rights granted directly or indirectly to You by such Participant, the Initial Developer (if the Initial Developer is not the Participant) and all Contributors under Sections 2.1 and/or 2.2 of this License shall, upon 60 days notice from Participant terminate prospectively and automatically at the expiration of such 60 day notice period, unless if within such 60 day period You withdraw Your claim with respect to the Participant Software against such Participant either unilaterally or pursuant to a written agreement with Participant. 6.3. If You assert a patent infringement claim against Participant alleging that the Participant Software directly or indirectly infringes any patent where such claim is resolved (such as by license or settlement) prior to the initiation of patent infringement litigation, then the reasonable value of the licenses granted by such Participant under Sections 2.1 or 2.2 shall be taken into account in determining the amount or value of any payment or license. 6.4. In the event of termination under Sections 6.1 or 6.2 above, all end user licenses that have been validly granted by You or any distributor hereunder prior to termination (excluding licenses granted to You by any distributor) shall survive termination. 7. LIMITATION OF LIABILITY. UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. 8. U.S. GOVERNMENT END USERS. The Covered Software is a "commercial item," as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer software" (as that term is defined at 48 C.F.R. � 252.227-7014(a)(1)) and "commercial computer software documentation" as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered Software with only those rights set forth herein. This U.S. Government Rights clause is in lieu of, and supersedes, any other FAR, DFAR, or other clause or provision that addresses Government rights in computer software under this License. 9. MISCELLANEOUS. This License represents the complete agreement concerning subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by the law of the jurisdiction specified in a notice contained within the Original Software (except to the extent applicable law, if any, provides otherwise), excluding such jurisdiction's conflict-of-law provisions. Any litigation relating to this License shall be subject to the jurisdiction of the courts located in the jurisdiction and venue specified in a notice contained within the Original Software, with the losing party responsible for costs, including, without limitation, court costs and reasonable attorneys' fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not apply to this License. You agree that You alone are responsible for compliance with the United States export administration regulations (and the export control laws and regulation of any other countries) when You use, distribute or otherwise make available any Covered Software. 10. RESPONSIBILITY FOR CLAIMS. As between Initial Developer and the Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License and You agree to work with Initial Developer and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability. ------------------------------------------------------------------------ NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) The code released under the CDDL shall be governed by the laws of the State of California (excluding conflict-of-law provisions). Any litigation relating to this License shall be subject to the jurisdiction of the Federal Courts of the Northern District of California and the state courts of the State of California, with venue lying in Santa Clara County, California. The GNU General Public License (GPL) Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor Boston, MA 02110-1335 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. One line to give the program's name and a brief idea of what it does. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. # Certain source files distributed by Oracle America, Inc. and/or its affiliates are subject to the following clarification and special exception to the GPLv2, based on the GNU Project exception for its Classpath libraries, known as the GNU Classpath Exception, but only where Oracle has expressly included in the particular source file's header the words "Oracle designates this particular file as subject to the "Classpath" exception as provided by Oracle in the LICENSE file that accompanied this code." You should also note that Oracle includes multiple, independent programs in this software package. Some of those programs are provided under licenses deemed incompatible with the GPLv2 by the Free Software Foundation and others. For example, the package includes programs licensed under the Apache License, Version 2.0. Such programs are licensed to you under their original licenses. Oracle facilitates your further distribution of this package by adding the Classpath Exception to the necessary parts of its GPLv2 code, which permits you to use that code in combination with other independent modules not licensed under the GPLv2. However, note that this would not permit you to commingle code under an incompatible license with Oracle's GPLv2 licensed code by, for example, cutting and pasting such code into a file also containing Oracle's GPLv2 licensed code and then distributing the result. Additionally, if you were to remove the Classpath Exception from any of the files to which it applies and distribute the result, you would likely be required to license some or all of the other code in that distribution under the GPLv2 as well, and since the GPLv2 is incompatible with the license terms of some items included in the distribution by Oracle, removing the Classpath Exception could therefore effectively compromise your ability to further distribute the package. Proceed with caution and we recommend that you obtain the advice of a lawyer skilled in open source matters before removing the Classpath Exception or making modifications to this package which may subsequently be redistributed and/or involve the use of third party software. CLASSPATH EXCEPTION Linking this library statically or dynamically with other modules is making a combined work based on this library. Thus, the terms and conditions of the GNU General Public License version 2 cover the whole combination. As a special exception, the copyright holders of this library give you permission to link this library with independent modules to produce an executable, regardless of the license terms of these independent modules, and to copy and distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module. An independent module is a module which is not derived from or based on this library. If you modify this library, you may extend this exception to your version of the library, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. ================================================ FILE: licenses/LICENSE-jaxb-api.txt ================================================ COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL)Version 1.1 1. Definitions. 1.1. "Contributor" means each individual or entity that creates or contributes to the creation of Modifications. 1.2. "Contributor Version" means the combination of the Original Software, prior Modifications used by a Contributor (if any), and the Modifications made by that particular Contributor. 1.3. "Covered Software" means (a) the Original Software, or (b) Modifications, or (c) the combination of files containing Original Software with files containing Modifications, in each case including portions thereof. 1.4. "Executable" means the Covered Software in any form other than Source Code. 1.5. "Initial Developer" means the individual or entity that first makes Original Software available under this License. 1.6. "Larger Work" means a work which combines Covered Software or portions thereof with code not governed by the terms of this License. 1.7. "License" means this document. 1.8. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently acquired, any and all of the rights conveyed herein. 1.9. "Modifications" means the Source Code and Executable form of any of the following: A. Any file that results from an addition to, deletion from or modification of the contents of a file containing Original Software or previous Modifications; B. Any new file that contains any part of the Original Software or previous Modification; or C. Any new file that is contributed or otherwise made available under the terms of this License. 1.10. "Original Software" means the Source Code and Executable form of computer software code that is originally released under this License. 1.11. "Patent Claims" means any patent claim(s), now owned or hereafter acquired, including without limitation, method, process, and apparatus claims, in any patent Licensable by grantor. 1.12. "Source Code" means (a) the common form of computer software code in which modifications are made and (b) associated documentation included in or with such code. 1.13. "You" (or "Your") means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity which controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants. 2.1. The Initial Developer Grant. Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, the Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer, to use, reproduce, modify, display, perform, sublicense and distribute the Original Software (or portions thereof), with or without Modifications, and/or as part of a Larger Work; and (b) under Patent Claims infringed by the making, using or selling of Original Software, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Software (or portions thereof). (c) The licenses granted in Sections 2.1(a) and (b) are effective on the date Initial Developer first distributes or otherwise makes the Original Software available to a third party under the terms of this License. (d) Notwithstanding Section 2.1(b) above, no patent license is granted: (1) for code that You delete from the Original Software, or (2) for infringements caused by: (i) the modification of the Original Software, or (ii) the combination of the Original Software with other software or devices. 2.2. Contributor Grant. Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by Contributor to use, reproduce, modify, display, perform, sublicense and distribute the Modifications created by such Contributor (or portions thereof), either on an unmodified basis, with other Modifications, as Covered Software and/or as part of a Larger Work; and (b) under Patent Claims infringed by the making, using, or selling of Modifications made by that Contributor either alone and/or in combination with its Contributor Version (or portions of such combination), to make, use, sell, offer for sale, have made, and/or otherwise dispose of: (1) Modifications made by that Contributor (or portions thereof); and (2) the combination of Modifications made by that Contributor with its Contributor Version (or portions of such combination). (c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective on the date Contributor first distributes or otherwise makes the Modifications available to a third party. (d) Notwithstanding Section 2.2(b) above, no patent license is granted: (1) for any code that Contributor has deleted from the Contributor Version; (2) for infringements caused by: (i) third party modifications of Contributor Version, or (ii) the combination of Modifications made by that Contributor with other software (except as part of the Contributor Version) or other devices; or (3) under Patent Claims infringed by Covered Software in the absence of Modifications made by that Contributor. 3. Distribution Obligations. 3.1. Availability of Source Code. Any Covered Software that You distribute or otherwise make available in Executable form must also be made available in Source Code form and that Source Code form must be distributed only under the terms of this License. You must include a copy of this License with every copy of the Source Code form of the Covered Software You distribute or otherwise make available. You must inform recipients of any such Covered Software in Executable form as to how they can obtain such Covered Software in Source Code form in a reasonable manner on or through a medium customarily used for software exchange. 3.2. Modifications. The Modifications that You create or to which You contribute are governed by the terms of this License. You represent that You believe Your Modifications are Your original creation(s) and/or You have sufficient rights to grant the rights conveyed by this License. 3.3. Required Notices. You must include a notice in each of Your Modifications that identifies You as the Contributor of the Modification. You may not remove or alter any copyright, patent or trademark notices contained within the Covered Software, or any notices of licensing or any descriptive text giving attribution to any Contributor or the Initial Developer. 3.4. Application of Additional Terms. You may not offer or impose any terms on any Covered Software in Source Code form that alters or restricts the applicable version of this License or the recipients' rights hereunder. You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, you may do so only on Your own behalf, and not on behalf of the Initial Developer or any Contributor. You must make it absolutely clear that any such warranty, support, indemnity or liability obligation is offered by You alone, and You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of warranty, support, indemnity or liability terms You offer. 3.5. Distribution of Executable Versions. You may distribute the Executable form of the Covered Software under the terms of this License or under the terms of a license of Your choice, which may contain terms different from this License, provided that You are in compliance with the terms of this License and that the license for the Executable form does not attempt to limit or alter the recipient's rights in the Source Code form from the rights set forth in this License. If You distribute the Covered Software in Executable form under a different license, You must make it absolutely clear that any terms which differ from this License are offered by You alone, not by the Initial Developer or Contributor. You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of any such terms You offer. 3.6. Larger Works. You may create a Larger Work by combining Covered Software with other code not governed by the terms of this License and distribute the Larger Work as a single product. In such a case, You must make sure the requirements of this License are fulfilled for the Covered Software. 4. Versions of the License. 4.1. New Versions. Oracle is the initial license steward and may publish revised and/or new versions of this License from time to time. Each version will be given a distinguishing version number. Except as provided in Section 4.3, no one other than the license steward has the right to modify this License. 4.2. Effect of New Versions. You may always continue to use, distribute or otherwise make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. If the Initial Developer includes a notice in the Original Software prohibiting it from being distributed or otherwise made available under any subsequent version of the License, You must distribute and make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. Otherwise, You may also choose to use, distribute or otherwise make the Covered Software available under the terms of any subsequent version of the License published by the license steward. 4.3. Modified Versions. When You are an Initial Developer and You want to create a new license for Your Original Software, You may create and use a modified version of this License if You: (a) rename the license and remove any references to the name of the license steward (except to note that the license differs from this License); and (b) otherwise make it clear that the license contains terms which differ from this License. 5. DISCLAIMER OF WARRANTY. COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. 6. TERMINATION. 6.1. This License and the rights granted hereunder will terminate automatically if You fail to comply with terms herein and fail to cure such breach within 30 days of becoming aware of the breach. Provisions which, by their nature, must remain in effect beyond the termination of this License shall survive. 6.2. If You assert a patent infringement claim (excluding declaratory judgment actions) against Initial Developer or a Contributor (the Initial Developer or Contributor against whom You assert such claim is referred to as "Participant") alleging that the Participant Software (meaning the Contributor Version where the Participant is a Contributor or the Original Software where the Participant is the Initial Developer) directly or indirectly infringes any patent, then any and all rights granted directly or indirectly to You by such Participant, the Initial Developer (if the Initial Developer is not the Participant) and all Contributors under Sections 2.1 and/or 2.2 of this License shall, upon 60 days notice from Participant terminate prospectively and automatically at the expiration of such 60 day notice period, unless if within such 60 day period You withdraw Your claim with respect to the Participant Software against such Participant either unilaterally or pursuant to a written agreement with Participant. 6.3. If You assert a patent infringement claim against Participant alleging that the Participant Software directly or indirectly infringes any patent where such claim is resolved (such as by license or settlement) prior to the initiation of patent infringement litigation, then the reasonable value of the licenses granted by such Participant under Sections 2.1 or 2.2 shall be taken into account in determining the amount or value of any payment or license. 6.4. In the event of termination under Sections 6.1 or 6.2 above, all end user licenses that have been validly granted by You or any distributor hereunder prior to termination (excluding licenses granted to You by any distributor) shall survive termination. 7. LIMITATION OF LIABILITY. UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. 8. U.S. GOVERNMENT END USERS. The Covered Software is a "commercial item," as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer software" (as that term is defined at 48 C.F.R. ? 252.227-7014(a)(1)) and "commercial computer software documentation" as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered Software with only those rights set forth herein. This U.S. Government Rights clause is in lieu of, and supersedes, any other FAR, DFAR, or other clause or provision that addresses Government rights in computer software under this License. 9. MISCELLANEOUS. This License represents the complete agreement concerning subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by the law of the jurisdiction specified in a notice contained within the Original Software (except to the extent applicable law, if any, provides otherwise), excluding such jurisdiction's conflict-of-law provisions. Any litigation relating to this License shall be subject to the jurisdiction of the courts located in the jurisdiction and venue specified in a notice contained within the Original Software, with the losing party responsible for costs, including, without limitation, court costs and reasonable attorneys' fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not apply to this License. You agree that You alone are responsible for compliance with the United States export administration regulations (and the export control laws and regulation of any other countries) when You use, distribute or otherwise make available any Covered Software. 10. RESPONSIBILITY FOR CLAIMS. As between Initial Developer and the Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License and You agree to work with Initial Developer and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability. ---------- NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) The code released under the CDDL shall be governed by the laws of the State of California (excluding conflict-of-law provisions). Any litigation relating to this License shall be subject to the jurisdiction of the Federal Courts of the Northern District of California and the state courts of the State of California, with venue lying in Santa Clara County, California. The GNU General Public License (GPL) Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. One line to give the program's name and a brief idea of what it does. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. "CLASSPATH" EXCEPTION TO THE GPL VERSION 2 Certain source files distributed by Oracle are subject to the following clarification and special exception to the GPL Version 2, but only where Oracle has expressly included in the particular source file's header the words "Oracle designates this particular file as subject to the "Classpath" exception as provided by Oracle in the License file that accompanied this code." Linking this library statically or dynamically with other modules is making a combined work based on this library. Thus, the terms and conditions of the GNU General Public License Version 2 cover the whole combination. As a special exception, the copyright holders of this library give you permission to link this library with independent modules to produce an executable, regardless of the license terms of these independent modules, and to copy and distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module. An independent module is a module which is not derived from or based on this library. If you modify this library, you may extend this exception to your version of the library, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. ================================================ FILE: licenses/LICENSE-jersey.txt ================================================ # Eclipse Public License - v 2.0 THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 1. DEFINITIONS "Contribution" means: a) in the case of the initial Contributor, the initial content Distributed under this Agreement, and b) in the case of each subsequent Contributor: i) changes to the Program, and ii) additions to the Program; where such changes and/or additions to the Program originate from and are Distributed by that particular Contributor. A Contribution "originates" from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include changes or additions to the Program that are not Modified Works. "Contributor" means any person or entity that Distributes the Program. "Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. "Program" means the Contributions Distributed in accordance with this Agreement. "Recipient" means anyone who receives the Program under this Agreement or any Secondary License (as applicable), including Contributors. "Derivative Works" shall mean any work, whether in Source Code or other form, that is based on (or derived from) the Program and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. "Modified Works" shall mean any work in Source Code or other form that results from an addition to, deletion from, or modification of the contents of the Program, including, for purposes of clarity any new file in Source Code form that contains any contents of the Program. Modified Works shall not include works that contain only declarations, interfaces, types, classes, structures, or files of the Program solely in each case in order to link to, bind by name, or subclass the Program or Modified Works thereof. "Distribute" means the acts of a) distributing or b) making available in any manner that enables the transfer of a copy. "Source Code" means the form of a Program preferred for making modifications, including but not limited to software source code, documentation source, and configuration files. "Secondary License" means either the GNU General Public License, Version 2.0, or any later versions of that license, including any exceptions or additional permissions as identified by the initial Contributor. 2. GRANT OF RIGHTS a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, Distribute and sublicense the Contribution of such Contributor, if any, and such Derivative Works. b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in Source Code or other form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to Distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. e) Notwithstanding the terms of any Secondary License, no Contributor makes additional grants to any Recipient (other than those set forth in this Agreement) as a result of such Recipient's receipt of the Program under the terms of a Secondary License (if permitted under the terms of Section 3). 3. REQUIREMENTS 3.1 If a Contributor Distributes the Program in any form, then: a) the Program must also be made available as Source Code, in accordance with section 3.2, and the Contributor must accompany the Program with a statement that the Source Code for the Program is available under this Agreement, and informs Recipients how to obtain it in a reasonable manner on or through a medium customarily used for software exchange; and b) the Contributor may Distribute the Program under a license different than this Agreement, provided that such license: i) effectively disclaims on behalf of all other Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; ii) effectively excludes on behalf of all other Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; iii) does not attempt to limit or alter the recipients' rights in the Source Code under section 3.2; and iv) requires any subsequent distribution of the Program by any party to be under a license that satisfies the requirements of this section 3. 3.2 When the Program is Distributed as Source Code: a) it must be made available under this Agreement, or if the Program (i) is combined with other material in a separate file or files made available under a Secondary License, and (ii) the initial Contributor attached to the Source Code the notice described in Exhibit A of this Agreement, then the Program may be made available under the terms of such Secondary Licenses, and b) a copy of this Agreement must be included with each copy of the Program. 3.3 Contributors may not remove or alter any copyright, patent, trademark, attribution notices, disclaimers of warranty, or limitations of liability ("notices") contained within the Program from any copy of the Program which they Distribute, provided that Contributors may add their own appropriate notices. 4. COMMERCIAL DISTRIBUTION Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. 5. NO WARRANTY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. 6. DISCLAIMER OF LIABILITY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 7. GENERAL If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be Distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to Distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. Nothing in this Agreement is intended to be enforceable by any entity that is not a Contributor or Recipient. No third-party beneficiary rights are created under this Agreement. Exhibit A - Form of Secondary Licenses Notice "This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), version(s), and exceptions or additional permissions here}." Simply including a copy of this Agreement, including this Exhibit A is not sufficient to license the Source Code under Secondary Licenses. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. --- ## The GNU General Public License (GPL) Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor Boston, MA 02110-1335 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. One line to give the program's name and a brief idea of what it does. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. --- ## CLASSPATH EXCEPTION Linking this library statically or dynamically with other modules is making a combined work based on this library. Thus, the terms and conditions of the GNU General Public License version 2 cover the whole combination. As a special exception, the copyright holders of this library give you permission to link this library with independent modules to produce an executable, regardless of the license terms of these independent modules, and to copy and distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module. An independent module is a module which is not derived from or based on this library. If you modify this library, you may extend this exception to your version of the library, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. ================================================ FILE: licenses/LICENSE-jetty-11.0.txt ================================================ Eclipse Public License - v 2.0 THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 1. DEFINITIONS "Contribution" means: a) in the case of the initial Contributor, the initial content Distributed under this Agreement, and b) in the case of each subsequent Contributor: i) changes to the Program, and ii) additions to the Program; where such changes and/or additions to the Program originate from and are Distributed by that particular Contributor. A Contribution "originates" from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include changes or additions to the Program that are not Modified Works. "Contributor" means any person or entity that Distributes the Program. "Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. "Program" means the Contributions Distributed in accordance with this Agreement. "Recipient" means anyone who receives the Program under this Agreement or any Secondary License (as applicable), including Contributors. "Derivative Works" shall mean any work, whether in Source Code or other form, that is based on (or derived from) the Program and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. "Modified Works" shall mean any work in Source Code or other form that results from an addition to, deletion from, or modification of the contents of the Program, including, for purposes of clarity any new file in Source Code form that contains any contents of the Program. Modified Works shall not include works that contain only declarations, interfaces, types, classes, structures, or files of the Program solely in each case in order to link to, bind by name, or subclass the Program or Modified Works thereof. "Distribute" means the acts of a) distributing or b) making available in any manner that enables the transfer of a copy. "Source Code" means the form of a Program preferred for making modifications, including but not limited to software source code, documentation source, and configuration files. "Secondary License" means either the GNU General Public License, Version 2.0, or any later versions of that license, including any exceptions or additional permissions as identified by the initial Contributor. 2. GRANT OF RIGHTS a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, Distribute and sublicense the Contribution of such Contributor, if any, and such Derivative Works. b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in Source Code or other form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to Distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. e) Notwithstanding the terms of any Secondary License, no Contributor makes additional grants to any Recipient (other than those set forth in this Agreement) as a result of such Recipient's receipt of the Program under the terms of a Secondary License (if permitted under the terms of Section 3). 3. REQUIREMENTS 3.1 If a Contributor Distributes the Program in any form, then: a) the Program must also be made available as Source Code, in accordance with section 3.2, and the Contributor must accompany the Program with a statement that the Source Code for the Program is available under this Agreement, and informs Recipients how to obtain it in a reasonable manner on or through a medium customarily used for software exchange; and b) the Contributor may Distribute the Program under a license different than this Agreement, provided that such license: i) effectively disclaims on behalf of all other Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; ii) effectively excludes on behalf of all other Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; iii) does not attempt to limit or alter the recipients' rights in the Source Code under section 3.2; and iv) requires any subsequent distribution of the Program by any party to be under a license that satisfies the requirements of this section 3. 3.2 When the Program is Distributed as Source Code: a) it must be made available under this Agreement, or if the Program (i) is combined with other material in a separate file or files made available under a Secondary License, and (ii) the initial Contributor attached to the Source Code the notice described in Exhibit A of this Agreement, then the Program may be made available under the terms of such Secondary Licenses, and b) a copy of this Agreement must be included with each copy of the Program. 3.3 Contributors may not remove or alter any copyright, patent, trademark, attribution notices, disclaimers of warranty, or limitations of liability ("notices") contained within the Program from any copy of the Program which they Distribute, provided that Contributors may add their own appropriate notices. 4. COMMERCIAL DISTRIBUTION Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. 5. NO WARRANTY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. 6. DISCLAIMER OF LIABILITY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 7. GENERAL If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be Distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to Distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. Nothing in this Agreement is intended to be enforceable by any entity that is not a Contributor or Recipient. No third-party beneficiary rights are created under this Agreement. Exhibit A - Form of Secondary Licenses Notice "This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), version(s), and exceptions or additional permissions here}." Simply including a copy of this Agreement, including this Exhibit A is not sufficient to license the Source Code under Secondary Licenses. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 ================================================ FILE: licenses/LICENSE-jetty-9.4.txt ================================================ This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which is available at http://www.eclipse.org/legal/epl-2.0, or the Apache Software License 2.0 which is available at https://www.apache.org/licenses/LICENSE-2.0. Eclipse Public License - v 1.0 THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 1. DEFINITIONS "Contribution" means: a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and b) in the case of each subsequent Contributor: i) changes to the Program, and ii) additions to the Program; where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. "Contributor" means any person or entity that distributes the Program. "Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. "Program" means the Contributions distributed in accordance with this Agreement. "Recipient" means anyone who receives the Program under this Agreement, including all Contributors. 2. GRANT OF RIGHTS a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. 3. REQUIREMENTS A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: a) it complies with the terms and conditions of this Agreement; and b) its license agreement: i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. When the Program is made available in source code form: a) it must be made available under this Agreement; and b) a copy of this Agreement must be included with each copy of the Program. Contributors may not remove or alter any copyright notices contained within the Program. Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. 4. COMMERCIAL DISTRIBUTION Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. 5. NO WARRANTY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. 6. DISCLAIMER OF LIABILITY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 7. GENERAL If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: licenses/LICENSE-jetty-jakarta-servlet-api.txt ================================================ This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which is available at http://www.eclipse.org/legal/epl-2.0, or the Apache Software License 2.0 which is available at https://www.apache.org/licenses/LICENSE-2.0. # Eclipse Public License - v 2.0 THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 1. DEFINITIONS "Contribution" means: a) in the case of the initial Contributor, the initial content Distributed under this Agreement, and b) in the case of each subsequent Contributor: i) changes to the Program, and ii) additions to the Program; where such changes and/or additions to the Program originate from and are Distributed by that particular Contributor. A Contribution "originates" from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include changes or additions to the Program that are not Modified Works. "Contributor" means any person or entity that Distributes the Program. "Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. "Program" means the Contributions Distributed in accordance with this Agreement. "Recipient" means anyone who receives the Program under this Agreement or any Secondary License (as applicable), including Contributors. "Derivative Works" shall mean any work, whether in Source Code or other form, that is based on (or derived from) the Program and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. "Modified Works" shall mean any work in Source Code or other form that results from an addition to, deletion from, or modification of the contents of the Program, including, for purposes of clarity any new file in Source Code form that contains any contents of the Program. Modified Works shall not include works that contain only declarations, interfaces, types, classes, structures, or files of the Program solely in each case in order to link to, bind by name, or subclass the Program or Modified Works thereof. "Distribute" means the acts of a) distributing or b) making available in any manner that enables the transfer of a copy. "Source Code" means the form of a Program preferred for making modifications, including but not limited to software source code, documentation source, and configuration files. "Secondary License" means either the GNU General Public License, Version 2.0, or any later versions of that license, including any exceptions or additional permissions as identified by the initial Contributor. 2. GRANT OF RIGHTS a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, Distribute and sublicense the Contribution of such Contributor, if any, and such Derivative Works. b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in Source Code or other form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to Distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. e) Notwithstanding the terms of any Secondary License, no Contributor makes additional grants to any Recipient (other than those set forth in this Agreement) as a result of such Recipient's receipt of the Program under the terms of a Secondary License (if permitted under the terms of Section 3). 3. REQUIREMENTS 3.1 If a Contributor Distributes the Program in any form, then: a) the Program must also be made available as Source Code, in accordance with section 3.2, and the Contributor must accompany the Program with a statement that the Source Code for the Program is available under this Agreement, and informs Recipients how to obtain it in a reasonable manner on or through a medium customarily used for software exchange; and b) the Contributor may Distribute the Program under a license different than this Agreement, provided that such license: i) effectively disclaims on behalf of all other Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; ii) effectively excludes on behalf of all other Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; iii) does not attempt to limit or alter the recipients' rights in the Source Code under section 3.2; and iv) requires any subsequent distribution of the Program by any party to be under a license that satisfies the requirements of this section 3. 3.2 When the Program is Distributed as Source Code: a) it must be made available under this Agreement, or if the Program (i) is combined with other material in a separate file or files made available under a Secondary License, and (ii) the initial Contributor attached to the Source Code the notice described in Exhibit A of this Agreement, then the Program may be made available under the terms of such Secondary Licenses, and b) a copy of this Agreement must be included with each copy of the Program. 3.3 Contributors may not remove or alter any copyright, patent, trademark, attribution notices, disclaimers of warranty, or limitations of liability ("notices") contained within the Program from any copy of the Program which they Distribute, provided that Contributors may add their own appropriate notices. 4. COMMERCIAL DISTRIBUTION Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. 5. NO WARRANTY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. 6. DISCLAIMER OF LIABILITY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 7. GENERAL If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be Distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to Distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. Nothing in this Agreement is intended to be enforceable by any entity that is not a Contributor or Recipient. No third-party beneficiary rights are created under this Agreement. Exhibit A - Form of Secondary Licenses Notice "This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), version(s), and exceptions or additional permissions here}." Simply including a copy of this Agreement, including this Exhibit A is not sufficient to license the Source Code under Secondary Licenses. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. # Apache License - v 2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. # The GNU General Public License (GPL) Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor Boston, MA 02110-1335 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. One line to give the program's name and a brief idea of what it does. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. ## CLASSPATH EXCEPTION Linking this library statically or dynamically with other modules is making a combined work based on this library. Thus, the terms and conditions of the GNU General Public License version 2 cover the whole combination. As a special exception, the copyright holders of this library give you permission to link this library with independent modules to produce an executable, regardless of the license terms of these independent modules, and to copy and distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module. An independent module is a module which is not derived from or based on this library. If you modify this library, you may extend this exception to your version of the library, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. ================================================ FILE: licenses/LICENSE-lombok.txt ================================================ Copyright (C) 2009-2021 The Project Lombok Authors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ============================================================================== Licenses for included components: org.ow2.asm:asm org.ow2.asm:asm-analysis org.ow2.asm:asm-commons org.ow2.asm:asm-tree org.ow2.asm:asm-util ASM: a very small and fast Java bytecode manipulation framework Copyright (c) 2000-2011 INRIA, France Telecom All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holders nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ------------------------------------------------------------------------------ rzwitserloot/com.zwitserloot.cmdreader Copyright © 2010 Reinier Zwitserloot. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------------------ projectlombok/lombok.patcher Copyright (C) 2009-2021 The Project Lombok Authors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------------------ ================================================ FILE: licenses/LICENSE-lucene.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Some code in core/src/java/org/apache/lucene/util/UnicodeUtil.java was derived from unicode conversion examples available at http://www.unicode.org/Public/PROGRAMS/CVTUTF. Here is the copyright from those sources: /* * Copyright 2001-2004 Unicode, Inc. * * Disclaimer * * This source code is provided as is by Unicode, Inc. No claims are * made as to fitness for any particular purpose. No warranties of any * kind are expressed or implied. The recipient agrees to determine * applicability of information provided. If this file has been * purchased on magnetic or optical media from Unicode, Inc., the * sole remedy for any claim will be exchange of defective media * within 90 days of receipt. * * Limitations on Rights to Redistribute This Code * * Unicode, Inc. hereby grants the right to freely use the information * supplied in this file in the creation of products supporting the * Unicode Standard, and to make copies of this file in any form * for internal or external distribution as long as this notice * remains attached. */ Some code in core/src/java/org/apache/lucene/util/ArrayUtil.java was derived from Python 2.4.2 sources available at http://www.python.org. Full license is here: http://www.python.org/download/releases/2.4.2/license/ Some code in core/src/java/org/apache/lucene/util/UnicodeUtil.java was derived from Python 3.1.2 sources available at http://www.python.org. Full license is here: http://www.python.org/download/releases/3.1.2/license/ Some code in core/src/java/org/apache/lucene/util/automaton was derived from Brics automaton sources available at www.brics.dk/automaton/. Here is the copyright from those sources: /* * Copyright (c) 2001-2009 Anders Moeller * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. The name of the author may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ The levenshtein automata tables in core/src/java/org/apache/lucene/util/automaton were automatically generated with the moman/finenight FSA package. Here is the copyright for those sources: # Copyright (c) 2010, Jean-Philippe Barrette-LaPierre, # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. Some code in core/src/java/org/apache/lucene/util/UnicodeUtil.java was derived from ICU (http://www.icu-project.org) The full license is available here: http://source.icu-project.org/repos/icu/icu/trunk/license.html /* * Copyright (C) 1999-2010, International Business Machines * Corporation and others. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, and/or sell copies of the * Software, and to permit persons to whom the Software is furnished to do so, * provided that the above copyright notice(s) and this permission notice appear * in all copies of the Software and that both the above copyright notice(s) and * this permission notice appear in supporting documentation. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE * LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR * ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * * Except as contained in this notice, the name of a copyright holder shall not * be used in advertising or otherwise to promote the sale, use or other * dealings in this Software without prior written authorization of the * copyright holder. */ The following license applies to the Snowball stemmers: Copyright (c) 2001, Dr Martin Porter Copyright (c) 2002, Richard Boulton All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * Neither the name of the copyright holders nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The following license applies to the KStemmer: Copyright © 2003, Center for Intelligent Information Retrieval, University of Massachusetts, Amherst. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The names "Center for Intelligent Information Retrieval" and "University of Massachusetts" must not be used to endorse or promote products derived from this software without prior written permission. To obtain permission, contact info@ciir.cs.umass.edu. THIS SOFTWARE IS PROVIDED BY UNIVERSITY OF MASSACHUSETTS AND OTHER CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The following license applies to the Morfologik project: Copyright (c) 2006 Dawid Weiss Copyright (c) 2007-2011 Dawid Weiss, Marcin Miłkowski All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Morfologik nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --- The dictionary comes from Morfologik project. Morfologik uses data from Polish ispell/myspell dictionary hosted at http://www.sjp.pl/slownik/en/ and is licenced on the terms of (inter alia) LGPL and Creative Commons ShareAlike. The part-of-speech tags were added in Morfologik project and are not found in the data from sjp.pl. The tagset is similar to IPI PAN tagset. --- The following license applies to the Morfeusz project, used by org.apache.lucene.analysis.morfologik. BSD-licensed dictionary of Polish (SGJP) http://sgjp.pl/morfeusz/ Copyright © 2011 Zygmunt Saloni, Włodzimierz Gruszczyński, Marcin Woliński, Robert Wołosz All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDERS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --- core/src/java/org/apache/lucene/util/compress/LZ4.java is a Java implementation of the LZ4 (https://github.com/lz4/lz4/tree/dev/lib) compression format for Lucene's DataInput/DataOutput abstractions. LZ4 Library Copyright (c) 2011-2016, Yann Collet All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: licenses/LICENSE-netty-tcnative-boringssl.txt ================================================ ================================================================================ META-INF/LICENSE.txt ================================================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================================ META-INF/license/LICENSE.aix-netbsd.txt ================================================================================ LICENSE ISSUES ============== The OpenSSL toolkit stays under a double license, i.e. both the conditions of the OpenSSL License and the original SSLeay license apply to the toolkit. See below for the actual license texts. OpenSSL License --------------- /* ==================================================================== * Copyright (c) 1998-2019 The OpenSSL Project. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. All advertising materials mentioning features or use of this * software must display the following acknowledgment: * "This product includes software developed by the OpenSSL Project * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" * * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to * endorse or promote products derived from this software without * prior written permission. For written permission, please contact * openssl-core@openssl.org. * * 5. Products derived from this software may not be called "OpenSSL" * nor may "OpenSSL" appear in their names without prior written * permission of the OpenSSL Project. * * 6. Redistributions of any form whatsoever must retain the following * acknowledgment: * "This product includes software developed by the OpenSSL Project * for use in the OpenSSL Toolkit (http://www.openssl.org/)" * * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. * ==================================================================== * * This product includes cryptographic software written by Eric Young * (eay@cryptsoft.com). This product includes software written by Tim * Hudson (tjh@cryptsoft.com). * */ Original SSLeay License ----------------------- /* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) * All rights reserved. * * This package is an SSL implementation written * by Eric Young (eay@cryptsoft.com). * The implementation was written so as to conform with Netscapes SSL. * * This library is free for commercial and non-commercial use as long as * the following conditions are aheared to. The following conditions * apply to all code found in this distribution, be it the RC4, RSA, * lhash, DES, etc., code; not just the SSL code. The SSL documentation * included with this distribution is covered by the same copyright terms * except that the holder is Tim Hudson (tjh@cryptsoft.com). * * Copyright remains Eric Young's, and as such any Copyright notices in * the code are not to be removed. * If this package is used in a product, Eric Young should be given attribution * as the author of the parts of the library used. * This can be in the form of a textual message at program startup or * in documentation (online or textual) provided with the package. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. All advertising materials mentioning features or use of this software * must display the following acknowledgement: * "This product includes cryptographic software written by * Eric Young (eay@cryptsoft.com)" * The word 'cryptographic' can be left out if the rouines from the library * being used are not cryptographic related :-). * 4. If you include any Windows specific code (or a derivative thereof) from * the apps directory (application code) you must include an acknowledgement: * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" * * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * The licence and distribution terms for any publically available version or * derivative of this code cannot be changed. i.e. this code cannot simply be * copied and put under another distribution licence * [including the GNU Public Licence.] */ ================================================================================ META-INF/license/LICENSE.boringssl.txt ================================================================================ BoringSSL is a fork of OpenSSL. As such, large parts of it fall under OpenSSL licensing. Files that are completely new have a Google copyright and an ISC license. This license is reproduced at the bottom of this file. Contributors to BoringSSL are required to follow the CLA rules for Chromium: https://cla.developers.google.com/clas Files in third_party/ have their own licenses, as described therein. The MIT license, for third_party/fiat, which, unlike other third_party directories, is compiled into non-test libraries, is included below. The OpenSSL toolkit stays under a dual license, i.e. both the conditions of the OpenSSL License and the original SSLeay license apply to the toolkit. See below for the actual license texts. Actually both licenses are BSD-style Open Source licenses. In case of any license issues related to OpenSSL please contact openssl-core@openssl.org. The following are Google-internal bug numbers where explicit permission from some authors is recorded for use of their work. (This is purely for our own record keeping.) 27287199 27287880 27287883 OpenSSL License --------------- /* ==================================================================== * Copyright (c) 1998-2011 The OpenSSL Project. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. All advertising materials mentioning features or use of this * software must display the following acknowledgment: * "This product includes software developed by the OpenSSL Project * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" * * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to * endorse or promote products derived from this software without * prior written permission. For written permission, please contact * openssl-core@openssl.org. * * 5. Products derived from this software may not be called "OpenSSL" * nor may "OpenSSL" appear in their names without prior written * permission of the OpenSSL Project. * * 6. Redistributions of any form whatsoever must retain the following * acknowledgment: * "This product includes software developed by the OpenSSL Project * for use in the OpenSSL Toolkit (http://www.openssl.org/)" * * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. * ==================================================================== * * This product includes cryptographic software written by Eric Young * (eay@cryptsoft.com). This product includes software written by Tim * Hudson (tjh@cryptsoft.com). * */ Original SSLeay License ----------------------- /* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) * All rights reserved. * * This package is an SSL implementation written * by Eric Young (eay@cryptsoft.com). * The implementation was written so as to conform with Netscapes SSL. * * This library is free for commercial and non-commercial use as long as * the following conditions are aheared to. The following conditions * apply to all code found in this distribution, be it the RC4, RSA, * lhash, DES, etc., code; not just the SSL code. The SSL documentation * included with this distribution is covered by the same copyright terms * except that the holder is Tim Hudson (tjh@cryptsoft.com). * * Copyright remains Eric Young's, and as such any Copyright notices in * the code are not to be removed. * If this package is used in a product, Eric Young should be given attribution * as the author of the parts of the library used. * This can be in the form of a textual message at program startup or * in documentation (online or textual) provided with the package. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. All advertising materials mentioning features or use of this software * must display the following acknowledgement: * "This product includes cryptographic software written by * Eric Young (eay@cryptsoft.com)" * The word 'cryptographic' can be left out if the rouines from the library * being used are not cryptographic related :-). * 4. If you include any Windows specific code (or a derivative thereof) from * the apps directory (application code) you must include an acknowledgement: * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" * * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * The licence and distribution terms for any publically available version or * derivative of this code cannot be changed. i.e. this code cannot simply be * copied and put under another distribution licence * [including the GNU Public Licence.] */ ISC license used for completely new code in BoringSSL: /* Copyright (c) 2015, Google Inc. * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ The code in third_party/fiat carries the MIT license: Copyright (c) 2015-2016 the fiat-crypto authors (see https://github.com/mit-plv/fiat-crypto/blob/master/AUTHORS). Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Licenses for support code ------------------------- Parts of the TLS test suite are under the Go license. This code is not included in BoringSSL (i.e. libcrypto and libssl) when compiled, however, so distributing code linked against BoringSSL does not trigger this license: Copyright (c) 2009 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. BoringSSL uses the Chromium test infrastructure to run a continuous build, trybots etc. The scripts which manage this, and the script for generating build metadata, are under the Chromium license. Distributing code linked against BoringSSL does not trigger this license. Copyright 2015 The Chromium Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================================ META-INF/license/LICENSE.mvn-wrapper.txt ================================================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================================ META-INF/license/LICENSE.tomcat-native.txt ================================================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: licenses/LICENSE-netty.txt ================================================ ================================================================================ META-INF/LICENSE.txt ================================================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================================ META-INF/license/LICENSE.base64.txt ================================================================================ The person or persons who have associated work with this document (the "Dedicator" or "Certifier") hereby either (a) certifies that, to the best of his knowledge, the work of authorship identified is in the public domain of the country from which the work is published, or (b) hereby dedicates whatever copyright the dedicators holds in the work of authorship identified below (the "Work") to the public domain. A certifier, moreover, dedicates any copyright interest he may have in the associated work, and for these purposes, is described as a "dedicator" below. A certifier has taken reasonable steps to verify the copyright status of this work. Certifier recognizes that his good faith efforts may not shield him from liability if in fact the work certified is not in the public domain. Dedicator makes this dedication for the benefit of the public at large and to the detriment of the Dedicator's heirs and successors. Dedicator intends this dedication to be an overt act of relinquishment in perpetuate of all present and future rights under copyright law, whether vested or contingent, in the Work. Dedicator understands that such relinquishment of all rights includes the relinquishment of all rights to enforce (by lawsuit or otherwise) those copyrights in the Work. Dedicator recognizes that, once placed in the public domain, the Work may be freely reproduced, distributed, transmitted, used, modified, built upon, or otherwise exploited by anyone for any purpose, commercial or non-commercial, and in any way, including by methods that have not yet been invented or conceived. ================================================================================ META-INF/license/LICENSE.bouncycastle.txt ================================================================================ The MIT License (MIT) Copyright (c) 2000 - 2013 The Legion of the Bouncy Castle Inc. (http://www.bouncycastle.org) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================================ META-INF/license/LICENSE.commons-logging.txt ================================================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ================================================================================ META-INF/license/LICENSE.felix.txt ================================================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ================================================================================ META-INF/license/LICENSE.jboss-logging.txt ================================================================================ GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! ================================================================================ META-INF/license/LICENSE.jsr166y.txt ================================================================================ The person or persons who have associated work with this document (the "Dedicator" or "Certifier") hereby either (a) certifies that, to the best of his knowledge, the work of authorship identified is in the public domain of the country from which the work is published, or (b) hereby dedicates whatever copyright the dedicators holds in the work of authorship identified below (the "Work") to the public domain. A certifier, moreover, dedicates any copyright interest he may have in the associated work, and for these purposes, is described as a "dedicator" below. A certifier has taken reasonable steps to verify the copyright status of this work. Certifier recognizes that his good faith efforts may not shield him from liability if in fact the work certified is not in the public domain. Dedicator makes this dedication for the benefit of the public at large and to the detriment of the Dedicator's heirs and successors. Dedicator intends this dedication to be an overt act of relinquishment in perpetuity of all present and future rights under copyright law, whether vested or contingent, in the Work. Dedicator understands that such relinquishment of all rights includes the relinquishment of all rights to enforce (by lawsuit or otherwise) those copyrights in the Work. Dedicator recognizes that, once placed in the public domain, the Work may be freely reproduced, distributed, transmitted, used, modified, built upon, or otherwise exploited by anyone for any purpose, commercial or non-commercial, and in any way, including by methods that have not yet been invented or conceived. ================================================================================ META-INF/license/LICENSE.jzlib.txt ================================================================================ Copyright (c) 2000,2001,2002,2003,2004 ymnk, JCraft,Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The names of the authors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================================ META-INF/license/LICENSE.log4j.txt ================================================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ================================================================================ META-INF/license/LICENSE.protobuf.txt ================================================================================ Copyright 2008, Google Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Code generated by the Protocol Buffer compiler is owned by the owner of the input file used when generating it. This code is not standalone and requires a support library to be linked with it. This support library is itself covered by the above license. ================================================================================ META-INF/license/LICENSE.slf4j.txt ================================================================================ /* * Copyright (c) 2004-2007 QOS.ch * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ ================================================================================ META-INF/license/LICENSE.webbit.txt ================================================================================ (BSD License: http://www.opensource.org/licenses/bsd-license) Copyright (c) 2011, Joe Walnes, Aslak Hellesøy and contributors All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the Webbit nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: licenses/LICENSE-pekko-actor.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --------------- pekko-actor contains MurmurHash.scala which is derived from MurmurHash3, written by Austin Appleby. He has placed his code in the public domain. The author has disclaimed copyright to that source code. MurmurHash.scala also contains changes made by the Scala-Lang team under an Apache 2.0 license. Copyright (c) 2003-2011, LAMP/EPFL --------------- pekko-actor contains code from scala-collection-compat in the `org.apache.pekko.util.ccompat` package which has released under an Apache 2.0 license. - actor/src/main/scala-2.12/org/apache/pekko/util/ccompat/package.scala Scala (https://www.scala-lang.org) Copyright EPFL and Lightbend, Inc. --------------- pekko-actor contains code from scala-library in the `org.apache.pekko.util.ccompat` package and in `org.apache.pekko.util.Helpers.scala` which was released under an Apache 2.0 license. - actor/src/main/scala-2.12/org/apache/pekko/util/ccompat/package.scala - actor/src/main/scala/org/apache/pekko/util/Helpers.scala Scala (https://www.scala-lang.org) Copyright EPFL and Lightbend, Inc. --------------- pekko-actor contains code from Netty in `org.apache.pekko.io.dns.DnsSettings.scala` which was released under an Apache 2.0 license. Copyright 2014 The Netty Project --------------- pekko-actor contains code from java-uuid-generator in `org.apache.pekko.util.UUIDComparator.scala` which was released under an Apache 2.0 license. --------------- pekko-actor contains code in `org.apache.pekko.dispatch.AbstractNodeQueue.java` and in `org.apache.pekko.dispatch.AbstractBoundedNodeQueue.java` which was based on code from https://www.1024cores.net/home/lock-free-algorithms/queues/non-intrusive-mpsc-node-based-queue which was released under the Simplified BSD license. Copyright (c) 2010-2011 Dmitry Vyukov. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY DMITRY VYUKOV "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DMITRY VYUKOV OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of Dmitry Vyukov. --------------- pekko-actor contains code in `org.apache.pekko.dispatch.AbstractBoundedNodeQueue.java` which was based on code from actors which was released under the Apache 2.0 license. --------------- pekko-actor contains code in `org.apache.pekko.util.FrequencySketch.scala` which was based on code from hash-prospector which has been placed in the public domain. This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to --------------- pekko-actor contains code in `org.apache.pekko.util.FrequencySketch.scala` which was based on code from Caffeine which was developed under the Apache 2.0 license. Copyright 2015 Ben Manes. All Rights Reserved. ================================================ FILE: licenses/LICENSE-pekko-cluster.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --------------- pekko-cluster contains VectorClock.scala which is derived from code written by Coda Hale . He has agreed to allow us to use this code under an Apache 2.0 license . ================================================ FILE: licenses/LICENSE-pekko-protobuf-v3.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --------------- pekko-protobuf-v3 contains the sources of Google protobuf 3.19.6 runtime support, moved into the source package `org.apache.pekko.protobufv3.internal` so as to avoid version conflicts. For license information see COPYING.protobuf ================================================ FILE: licenses/LICENSE-pekko-remote.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --------------- pekko-remote contains CountMinSketch.java which contains code derived from MurmurHash3, written by Austin Appleby. He has placed his code in the public domain. The author has disclaimed copyright to that source code. CountMinSketch.java also contains additional code developed under an Apache 2.0 license. Copyright 2016 AddThis --------------- pekko-remote contains code from Aeron . ./remote/src/test/java/org/apache/pekko/remote/artery/aeron/AeronStat.java ./remote/src/test/java/org/apache/pekko/remote/artery/RateReporter.java ./remote/src/main/java/org/apache/pekko/remote/artery/aeron/AeronErrorLog.java This code was released under an Apache 2.0 license. Copyright 2014 - 2016 Real Logic Ltd. ================================================ FILE: licenses/LICENSE-postgresql.txt ================================================ ================================================================================ META-INF/LICENSE ================================================================================ Copyright (c) 1997, PostgreSQL Global Development Group All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Additional License files can be found in the 'licenses' folder located in the same directory as the LICENSE file (i.e. this file) - Software produced outside the ASF which is available under other licenses (not Apache-2.0) BSD-2-Clause * com.ongres.scram:scram-client:3.1 * com.ongres.scram:scram-common:3.1 * com.ongres.stringprep:saslprep:2.2 * com.ongres.stringprep:stringprep:2.2 ================================================================================ META-INF/licenses/com.ongres.scram/scram-client-3.1/META-INF/LICENSE ================================================================================ Copyright (c) 2017 OnGres, Inc. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================================ META-INF/licenses/com.ongres.scram/scram-common-3.1/META-INF/LICENSE ================================================================================ Copyright (c) 2017 OnGres, Inc. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================================ META-INF/licenses/com.ongres.stringprep/saslprep-2.2/META-INF/LICENSE ================================================================================ Copyright (c) 2019 OnGres, Inc. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================================ META-INF/licenses/com.ongres.stringprep/stringprep-2.2/META-INF/LICENSE ================================================================================ Copyright (c) 2019 OnGres, Inc. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: licenses/LICENSE-slf4j.txt ================================================ Copyright (c) 2004-2022 QOS.ch Sarl (Switzerland) All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: licenses/LICENSE-threeten-extra.txt ================================================ Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos. All rights reserved. * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of JSR-310 nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: licenses-3rd-party-code/angular.md ================================================ # The MIT License Copyright (c) 2010-2026 Google LLC. [https://angular.dev/license](/license) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: licenses-3rd-party-code/mbknor-jackson-jsonschema.txt ================================================ The MIT License (MIT) Copyright (c) 2015 NextGenTel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: licenses-3rd-party-code/monaco-languageclient.txt ================================================ MIT License Copyright 2018 - present TypeFox GmbH Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: project/AddMetaInfLicenseFiles.scala ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. import sbt._ import sbt.Keys._ import com.typesafe.sbt.packager.universal.UniversalPlugin.autoImport._ /** * Generates per-module LICENSE files for jar META-INF and dist zip top level. * * Each jar's META-INF/LICENSE describes only what is in that specific jar: * - Modules without vendored code get Apache 2.0 only. * - workflow-operator gets Apache 2.0 plus the mbknor-jackson-jsonschema * attribution and the full MIT license text. * * NOTICE and DISCLAIMER are copied as-is from the repo root. * * See https://github.com/apache/texera/issues/4131 */ object AddMetaInfLicenseFiles { private lazy val rootDir = LocalRootProject / baseDirectory private val ThirdPartyHeader = "THIRD-PARTY DEPENDENCIES" /** Extract the Apache 2.0 license text (before the THIRD-PARTY section) from root LICENSE. */ private def apacheLicenseText(rootDir: File): String = { val lines = IO.readLines(rootDir / "LICENSE") val headerIndex = lines.indexWhere(_.trim == ThirdPartyHeader) val cutoffIndex = if (headerIndex >= 0) { // Cut at the "---" delimiter line preceding the header val delimiterIndex = lines.lastIndexWhere(_.startsWith("---"), headerIndex - 1) if (delimiterIndex >= 0) delimiterIndex else headerIndex } else { lines.length } lines.take(cutoffIndex).mkString("\n").trim + "\n" } /** The vendored code section for workflow-operator (mbknor-jackson-jsonschema). */ private def workflowOperatorVendoredSection(rootDir: File): String = { val mitLicense = IO.read(rootDir / "licenses" / "LICENSE-MIT.txt") s""" |-------------------------------------------------------------------------------- |THIRD-PARTY DEPENDENCIES |-------------------------------------------------------------------------------- | |This jar bundles compiled code from the following third-party project. |The full license text is included below. | |MIT License |-------------------------------------- | |This product bundles code derived from mbknor-jackson-jsonschema: | - com/kjetland/jackson/jsonSchema/ | Copyright (c) 2016 Kjell Tore Eliassen (mbknor) | Source: https://github.com/mbknor/mbknor-jackson-jsonschema | |-------------------------------------------------------------------------------- |Full text of the MIT License: |-------------------------------------------------------------------------------- | |${mitLicense.trim} |""".stripMargin } private def writeToMetaInf(managed: File, fileName: String, content: String): File = { val dest = managed / "META-INF" / fileName IO.write(dest, content) dest } private def copyToMetaInf(managed: File, src: File, fileName: String): File = { val dest = managed / "META-INF" / fileName IO.copyFile(src, dest) dest } private def noticeAndDisclaimer(managed: File, rootDir: File): Seq[File] = { val files = Seq(copyToMetaInf(managed, rootDir / "NOTICE", "NOTICE")) val disclaimer = rootDir / "DISCLAIMER" if (disclaimer.exists()) files :+ copyToMetaInf(managed, disclaimer, "DISCLAIMER") else files } /** Settings for modules WITHOUT vendored third-party code. * META-INF/LICENSE contains only the Apache 2.0 license text. */ lazy val defaultSettings: Seq[Setting[_]] = Seq( Compile / resourceGenerators += Def.task { val managed = (Compile / resourceManaged).value val root = rootDir.value val licenseContent = apacheLicenseText(root) writeToMetaInf(managed, "LICENSE", licenseContent) +: noticeAndDisclaimer(managed, root) }.taskValue ) /** Settings for workflow-operator which contains vendored mbknor-jackson-jsonschema code. * META-INF/LICENSE contains Apache 2.0 plus the mbknor attribution and MIT license text. */ lazy val workflowOperatorSettings: Seq[Setting[_]] = Seq( Compile / resourceGenerators += Def.task { val managed = (Compile / resourceManaged).value val root = rootDir.value val licenseContent = apacheLicenseText(root) + "\n" + workflowOperatorVendoredSection(root) writeToMetaInf(managed, "LICENSE", licenseContent) +: noticeAndDisclaimer(managed, root) }.taskValue ) /** Ships the module's per-module LICENSE-binary, NOTICE-binary, DISCLAIMER * (if present), and licenses/ at the Universal zip's top level. The * per-module files describe only the third-party content actually * bundled in this module's dist zip; the licenses/ directory at the * repo root is shared by all dist zips. DISCLAIMER is optional (it * will be removed at graduation). */ def distMappings( existing: Seq[(File, String)], rootDir: File, licenseBinary: File, noticeBinary: File ): Seq[(File, String)] = { val disclaimerFile = rootDir / "DISCLAIMER" val licensesDir = rootDir / "licenses" require(licenseBinary.isFile, s"LICENSE-binary not found at $licenseBinary; required for binary-distribution packaging.") require(noticeBinary.isFile, s"NOTICE-binary not found at $noticeBinary; required for binary-distribution packaging.") require(licensesDir.isDirectory, s"licenses/ directory not found at $licensesDir; required for binary-distribution packaging.") val reserved = Set("LICENSE", "NOTICE", "DISCLAIMER") val filtered = existing.filterNot { case (_, path) => reserved.contains(path) || path.startsWith("licenses/") } val licenseTexts = (licensesDir ** "*.txt").get.map(f => f -> s"licenses/${f.getName}") val base = Seq(licenseBinary -> "LICENSE", noticeBinary -> "NOTICE") val disclaimer = if (disclaimerFile.isFile) Seq(disclaimerFile -> "DISCLAIMER") else Seq.empty filtered ++ base ++ disclaimer ++ licenseTexts } } ================================================ FILE: project/JdkOptions.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import sbt._ import scala.io.Source /** * This file reads JDK 17+ JVM flags from .jvmopts so every JVM the build * launches shares one flag list. * * Modeled on Pekko's project/JdkOptions.scala. The JDK 8 gate is * defensive: --add-opens does not exist before JDK 9, so jvmFlags * stays empty there even though Texera ships JDK 17 only. */ object JdkOptions { /** JVM flags from .jvmopts at the build root, or empty on JDK <9. */ def jvmFlags(baseDir: File): Seq[String] = if (jdkSpecVersion < 9) Seq.empty else readJvmopts(baseDir / ".jvmopts") private def jdkSpecVersion: Int = { val raw = sys.props.getOrElse("java.specification.version", "0") val s = if (raw.startsWith("1.")) raw.drop(2) else raw s.takeWhile(_.isDigit) match { case "" => 0 case digit => digit.toInt } } private def readJvmopts(f: File): Seq[String] = if (!f.exists()) Seq.empty else { val src = Source.fromFile(f) try src.getLines() .map(_.trim) .filter(l => l.nonEmpty && !l.startsWith("#")) .toList finally src.close() } } ================================================ FILE: project/build.properties ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. sbt.version=1.12.9 ================================================ FILE: project/plugins.sbt ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.2") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.6") // Coverage instrumentation; emits jacoco.xml that Codecov consumes. // JaCoCo (vs scoverage) works on JVM bytecode, so it does not need a // per-Scala-version compiler plugin — scalac-scoverage-plugin only // publishes up to 2.13.16, but Texera builds on 2.13.18. addSbtPlugin("com.github.sbt" % "sbt-jacoco" % "3.5.0") // License reporting for dependency compliance auditing // See: https://github.com/sbt/sbt-license-report addSbtPlugin("com.github.sbt" % "sbt-license-report" % "1.7.0") libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % "0.11.1" addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.11.1") // for scalapb code gen addSbtPlugin("org.typelevel" % "sbt-fs2-grpc" % "2.11.0") // JOOQ dependencies for code generation libraryDependencies ++= Seq( "org.jooq" % "jooq-codegen" % "3.16.23", "com.typesafe" % "config" % "1.4.6", "org.postgresql" % "postgresql" % "42.7.4" ) ================================================ FILE: pyright-language-service/README.md ================================================ This folder is for the Pyright language server. The documents in src which use to start the Pyright language server is done by Typefox, you can refer to "https://github.com/TypeFox/monaco-languageclient/tree/main/packages/examples/src/python" ================================================ FILE: pyright-language-service/package.json ================================================ { "name": "pyright-language-service", "version": "0.0.1", "main": "src/main.ts", "license": "Apache-2.0", "type": "module", "dependencies": { "express": "4.20.0", "hocon-parser": "1.0.1", "hoconjs": "1.0.0", "pyright": "1.1.377", "typescript": "5.5.4", "vscode-languageserver": "9.0.1", "vscode-ws-jsonrpc": "3.3.2", "ws": "8.18.0" }, "devDependencies": { "@types/express": "4.17.21", "@types/node": "22.5.4", "@types/ws": "8.5.12", "ts-node": "10.9.2" }, "scripts": { "start": "node --loader ts-node/esm src/main.ts --port" } } ================================================ FILE: pyright-language-service/src/config.json ================================================ { "languageServerDir": "../node_modules/pyright/dist", "clientPathName": "/python-language-server" } ================================================ FILE: pyright-language-service/src/language-server-runner.ts ================================================ /* -------------------------------------------------------------------------------------------- * Copyright (c) 2024 TypeFox and others. * Licensed under the MIT License. See LICENSE in the package root for license information. * ------------------------------------------------------------------------------------------ */ // The source file can be referred to: https://github.com/TypeFox/monaco-languageclient/blob/main/packages/examples/src/common/node/language-server-runner.ts import {WebSocketServer} from "ws"; import {Server} from 'node:http'; import express from 'express'; import {getLocalDirectory, LanguageServerRunConfig, upgradeWsServer} from './server-commons.ts'; /** LSP server runner */ export const runLanguageServer = ( languageServerRunConfig: LanguageServerRunConfig ) => { process.on('uncaughtException', err => { console.error('Uncaught Exception: ', err.toString()); if (err.stack !== undefined) { console.error(err.stack); } }); // create the express application const app = express(); // server the static content, i.e. index.html const dir = getLocalDirectory(import.meta.url); app.use(express.static(dir)); // start the http server const httpServer: Server = app.listen(languageServerRunConfig.serverPort); const wss = new WebSocketServer(languageServerRunConfig.wsServerOptions); // create the web socket upgradeWsServer(languageServerRunConfig, { server: httpServer, wss }); }; ================================================ FILE: pyright-language-service/src/main.ts ================================================ /* -------------------------------------------------------------------------------------------- * Copyright (c) 2024 TypeFox and others. * Licensed under the MIT License. See LICENSE in the package root for license information. * ------------------------------------------------------------------------------------------ */ // The source file can be referred to: https://github.com/TypeFox/monaco-languageclient/blob/main/packages/examples/src/python/server/main.ts import {dirname, resolve} from "node:path"; import {runLanguageServer} from "./language-server-runner.ts"; import {getLocalDirectory, LanguageName} from "./server-commons.ts"; import fs from "fs"; import {fileURLToPath} from "url"; const runPythonServer = ( baseDir: string, relativeDir: string, serverPort: number, ) => { const processRunPath = resolve(baseDir, relativeDir); runLanguageServer({ serverName: "PYRIGHT", pathName: clientPathName, serverPort: serverPort, runCommand: LanguageName.node, runCommandArgs: [processRunPath, "--stdio"], wsServerOptions: { noServer: true, perMessageDeflate: false, clientTracking: true, }, }); }; const baseDir = getLocalDirectory(import.meta.url); const relativeDir = "./node_modules/pyright/dist/pyright-langserver.js"; const configFilePath = resolve(baseDir, "config.json"); const configContent = fs.readFileSync(configFilePath, "utf-8"); const config = JSON.parse(configContent) as Record; const clientPathName = config.clientPathName; const parseArgs = (): Record => { const args = process.argv.slice(2); const options: Record = {}; args.forEach((arg) => { if (arg.startsWith("--") && arg.includes("=")) { const [key, value] = arg.substring(2).split("="); options[key] = value; } }); return options; }; const args = parseArgs(); const pythonLanguageServerPort = args["port"] ? parseInt(args["port"]) : 3000; const runDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); runPythonServer(runDir, relativeDir, pythonLanguageServerPort); ================================================ FILE: pyright-language-service/src/server-commons.ts ================================================ /* -------------------------------------------------------------------------------------------- * Copyright (c) 2024 TypeFox and others. * Licensed under the MIT License. See LICENSE in the package root for license information. * ------------------------------------------------------------------------------------------ */ //The source file can be referred to: https://github.com/TypeFox/monaco-languageclient/blob/main/packages/examples/src/common/node/server-commons.ts import {ServerOptions, WebSocketServer} from "ws"; import {IncomingMessage, Server} from "node:http"; import {fileURLToPath, URL} from "node:url"; import {Socket} from "node:net"; import {dirname} from "node:path"; import {IWebSocket, WebSocketMessageReader, WebSocketMessageWriter} from "vscode-ws-jsonrpc"; import {createConnection, createServerProcess, forward} from "vscode-ws-jsonrpc/server"; import {InitializeParams, InitializeRequest, Message} from "vscode-languageserver"; import * as cp from "child_process"; export enum LanguageName { /** https://nodejs.org/api/cli.html */ node = "node", /** https://docs.oracle.com/en/java/javase/21/docs/specs/man/java.html */ java = "java" } export interface LanguageServerRunConfig { serverName: string; pathName: string; serverPort: number; runCommand: LanguageName | string; runCommandArgs: string[]; wsServerOptions: ServerOptions, spawnOptions?: cp.SpawnOptions; } /** * start the language server inside the current process */ export const launchLanguageServer = (runconfig: LanguageServerRunConfig, socket: IWebSocket) => { const { serverName, runCommand, runCommandArgs, spawnOptions } = runconfig; // start the language server as an external process const reader = new WebSocketMessageReader(socket); const writer = new WebSocketMessageWriter(socket); const socketConnection = createConnection(reader, writer, () => socket.dispose()); const serverConnection = createServerProcess(serverName, runCommand, runCommandArgs, spawnOptions); if (serverConnection) { forward(socketConnection, serverConnection, message => { if (Message.isRequest(message)) { console.log(`${serverName} Server received:`); console.log(message); if (message.method === InitializeRequest.type.method) { const initializeParams = message.params as InitializeParams; initializeParams.processId = process.pid; } } if (Message.isResponse(message)) { console.log(`${serverName} Server sent:`); console.log(message); } return message; }); } }; export const upgradeWsServer = (runconfig: LanguageServerRunConfig, config: { server: Server, wss: WebSocketServer }) => { config.server.on("upgrade", (request: IncomingMessage, socket: Socket, head: Buffer) => { const baseURL = `http://${request.headers.host}/`; const pathName = request.url !== undefined ? new URL(request.url, baseURL).pathname : undefined; if (pathName === runconfig.pathName) { config.wss.handleUpgrade(request, socket, head, webSocket => { const socket: IWebSocket = { send: content => webSocket.send(content, error => { if (error) { throw error; } }), onMessage: cb => webSocket.on("message", (data) => { console.log(data.toString()); cb(data); }), onError: cb => webSocket.on("error", cb), onClose: cb => webSocket.on("close", cb), dispose: () => webSocket.close(), }; // launch the server when the web socket is opened if (webSocket.readyState === webSocket.OPEN) { launchLanguageServer(runconfig, socket); } else { webSocket.on("open", () => { launchLanguageServer(runconfig, socket); }); } }); } }); }; /** * Solves: __dirname is not defined in ES module scope */ export const getLocalDirectory = (referenceUrl: string | URL) => { const __filename = fileURLToPath(referenceUrl); return dirname(__filename); }; ================================================ FILE: pyright-language-service/src/types/hocon-parser.d.ts ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ declare module 'hocon-parser' { /** * The module itself is callable, accepting a string (HOCON config) and returning a parsed object. */ function hoconParser(input: string): any; export = hoconParser; } ================================================ FILE: pyright-language-service/tsconfig.json ================================================ { "compilerOptions": { "target": "es2020", "module": "ESNext", "moduleResolution": "node", "esModuleInterop": true, "allowSyntheticDefaultImports": true, "allowImportingTsExtensions": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "outDir": "./dist", "rootDir": "./src", "typeRoots": [ "./src/types", "./node_modules/@types" ] }, "include": [ "src/**/*.ts", "src/types/**/*.d.ts" ] } ================================================ FILE: sql/changelog.xml ================================================ ================================================ FILE: sql/docker-compose.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. name: texera-liquibase services: liquibase: image: liquibase/liquibase:4.29 volumes: - .:/liquibase/sql - ./changelog.xml:/liquibase/changelog.xml entrypoint: ["/bin/sh", "-c"] command: - | mkdir -p /tmp/sql/updates for f in /liquibase/sql/updates/*.sql do sed 's/^\\c.*//g;/^SET search_path/d' "$$f" > /tmp/sql/updates/$(basename "$$f") done cp /liquibase/changelog.xml /tmp/changelog.xml liquibase --url=jdbc:postgresql://host.docker.internal:5432/texera_db --username=postgres --password=postgres --changeLogFile=changelog.xml --search-path=/tmp update # NOTE: The usernames/passwords above must match your postgres settings in storage.conf. Update them here if you changed them. # Format: --username= --password= codegen: image: sbtscala/scala-sbt:eclipse-temurin-17.0.15_6_1.12.9_2.13.18 depends_on: liquibase: condition: service_completed_successfully working_dir: /texera volumes: - ../:/texera - ${HOME}/.ivy2:/root/.ivy2 - ${HOME}/.sbt:/root/.sbt environment: STORAGE_JDBC_URL: "jdbc:postgresql://host.docker.internal:5432/texera_db?currentSchema=texera_db,public" command: sbt jooqGenerate ================================================ FILE: sql/iceberg_postgres_catalog.sql ================================================ -- Licensed to the Apache Software Foundation (ASF) under one -- or more contributor license agreements. See the NOTICE file -- distributed with this work for additional information -- regarding copyright ownership. The ASF licenses this file -- to you under the Apache License, Version 2.0 (the -- "License"); you may not use this file except in compliance -- with the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, -- software distributed under the License is distributed on an -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -- KIND, either express or implied. See the License for the -- specific language governing permissions and limitations -- under the License. -- Connect to the default postgres database \c postgres -- Create the user `texera` with `password` if it doesn't exist DO $$ BEGIN IF NOT EXISTS ( SELECT FROM pg_catalog.pg_roles WHERE rolname = 'texera' ) THEN CREATE ROLE texera LOGIN PASSWORD 'password'; END IF; END $$; -- Drop and recreate the database DROP DATABASE IF EXISTS texera_iceberg_catalog; CREATE DATABASE texera_iceberg_catalog; -- Grant and change ownership GRANT ALL PRIVILEGES ON DATABASE texera_iceberg_catalog TO texera; ALTER DATABASE texera_iceberg_catalog OWNER TO texera; -- Reconnect to the new database \c texera_iceberg_catalog -- Grant schema access GRANT ALL ON SCHEMA public TO texera; ================================================ FILE: sql/misc/tweets.sql ================================================ -- Licensed to the Apache Software Foundation (ASF) under one -- or more contributor license agreements. See the NOTICE file -- distributed with this work for additional information -- regarding copyright ownership. The ASF licenses this file -- to you under the Apache License, Version 2.0 (the -- "License"); you may not use this file except in compliance -- with the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, -- software distributed under the License is distributed on an -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -- KIND, either express or implied. See the License for the -- specific language governing permissions and limitations -- under the License. CREATE TABLE TABLE_NAME ( id BIGINT NOT NULL PRIMARY KEY, created_at DATETIME NULL, text VARCHAR(500) CHARSET utf8mb4 NULL, in_reply_to_status_id BIGINT NULL, in_reply_to_user_id BIGINT NULL, favourites_count INT NULL, retweet_count INT NULL, lang VARCHAR(10) NULL, retweeted BOOLEAN NULL, hashtags VARCHAR(500) CHARSET utf8mb4 NULL, user_mentions VARCHAR(500) CHARSET utf8mb4 NULL, user_id BIGINT NULL, user_name VARCHAR(500) CHARSET utf8mb4 NULL, user_screen_name VARCHAR(500) CHARSET utf8mb4 NULL, user_location VARCHAR(500) NULL, user_description VARCHAR(500) CHARSET utf8mb4 NULL, user_followers_count INT NULL, user_friends_count INT NULL, user_statues_count INT NULL, stateName VARCHAR(100) NULL, countyName VARCHAR(100) NULL, cityName VARCHAR(100) NULL, country VARCHAR(100) NULL, bounding_box VARCHAR(500) NULL ); CREATE FULLTEXT INDEX text_index on TABLE_NAME (text); CREATE INDEX created_at_index ON TABLE_NAME (created_at); ================================================ FILE: sql/texera_ddl.sql ================================================ -- Licensed to the Apache Software Foundation (ASF) under one -- or more contributor license agreements. See the NOTICE file -- distributed with this work for additional information -- regarding copyright ownership. The ASF licenses this file -- to you under the Apache License, Version 2.0 (the -- "License"); you may not use this file except in compliance -- with the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, -- software distributed under the License is distributed on an -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -- KIND, either express or implied. See the License for the -- specific language governing permissions and limitations -- under the License. -- ============================================ -- 0. Specify the database name -- (defaults to texera_db) -- Override the name with: -- psql -v DB_NAME= ... -- ============================================ \if :{?DB_NAME} \else \set DB_NAME 'texera_db' \endif -- ============================================ -- 1. Drop and recreate the database (psql only) -- Remove if you already created texera_db -- ============================================ \c postgres DROP DATABASE IF EXISTS :"DB_NAME"; CREATE DATABASE :"DB_NAME"; -- ============================================ -- 2. Connect to the new database (psql only) -- ============================================ \c :"DB_NAME" CREATE SCHEMA IF NOT EXISTS texera_db; SET search_path TO texera_db, public; -- ============================================ -- 3. Drop all tables if they exist -- (CASCADE handles FK dependencies) -- ============================================ DROP TABLE IF EXISTS operator_executions CASCADE; DROP TABLE IF EXISTS operator_port_executions CASCADE; DROP TABLE IF EXISTS workflow_user_access CASCADE; DROP TABLE IF EXISTS workflow_of_user CASCADE; DROP TABLE IF EXISTS user_config CASCADE; DROP TABLE IF EXISTS "user" CASCADE; DROP TABLE IF EXISTS user_last_active_time CASCADE; DROP TABLE IF EXISTS workflow CASCADE; DROP TABLE IF EXISTS workflow_version CASCADE; DROP TABLE IF EXISTS project CASCADE; DROP TABLE IF EXISTS workflow_of_project CASCADE; DROP TABLE IF EXISTS workflow_executions CASCADE; DROP TABLE IF EXISTS dataset_upload_session CASCADE; DROP TABLE IF EXISTS dataset_upload_session_part CASCADE; DROP TABLE IF EXISTS dataset CASCADE; DROP TABLE IF EXISTS dataset_user_access CASCADE; DROP TABLE IF EXISTS dataset_version CASCADE; DROP TABLE IF EXISTS public_project CASCADE; DROP TABLE IF EXISTS project_user_access CASCADE; DROP TABLE IF EXISTS workflow_user_likes CASCADE; DROP TABLE IF EXISTS workflow_user_clones CASCADE; DROP TABLE IF EXISTS workflow_view_count CASCADE; DROP TABLE IF EXISTS user_action CASCADE; DROP TABLE IF EXISTS dataset_user_likes CASCADE; DROP TABLE IF EXISTS dataset_view_count CASCADE; DROP TABLE IF EXISTS site_settings CASCADE; DROP TABLE IF EXISTS computing_unit_user_access CASCADE; -- ============================================ -- 4. Create PostgreSQL enum types -- to mimic MySQL ENUM fields -- ============================================ DROP TYPE IF EXISTS user_role_enum CASCADE; DROP TYPE IF EXISTS privilege_enum CASCADE; DROP TYPE IF EXISTS action_enum CASCADE; CREATE TYPE user_role_enum AS ENUM ('INACTIVE', 'RESTRICTED', 'REGULAR', 'ADMIN'); CREATE TYPE action_enum AS ENUM ('like', 'unlike', 'view', 'clone'); CREATE TYPE privilege_enum AS ENUM ('NONE', 'READ', 'WRITE'); CREATE TYPE workflow_computing_unit_type_enum AS ENUM ('local', 'kubernetes'); -- ============================================ -- 5. Create tables -- ============================================ -- "user" table CREATE TABLE IF NOT EXISTS "user" ( uid SERIAL PRIMARY KEY, name VARCHAR(256) NOT NULL, email VARCHAR(256) UNIQUE, password VARCHAR(256), google_id VARCHAR(256) UNIQUE, google_avatar VARCHAR(100), role user_role_enum NOT NULL DEFAULT 'INACTIVE', comment TEXT, account_creation_time TIMESTAMPTZ NOT NULL DEFAULT now(), affiliation VARCHAR(128), joining_reason VARCHAR(500), -- check that either password or google_id is not null CONSTRAINT ck_nulltest CHECK ((password IS NOT NULL) OR (google_id IS NOT NULL)) ); -- user_config CREATE TABLE IF NOT EXISTS user_config ( uid INT NOT NULL, key VARCHAR(256) NOT NULL, value TEXT NOT NULL, PRIMARY KEY (uid, key), FOREIGN KEY (uid) REFERENCES "user"(uid) ON DELETE CASCADE ); -- workflow CREATE TABLE IF NOT EXISTS workflow ( wid SERIAL PRIMARY KEY, name VARCHAR(128) NOT NULL, description TEXT, content TEXT NOT NULL, creation_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, last_modified_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, is_public BOOLEAN NOT NULL DEFAULT false ); -- workflow_of_user CREATE TABLE IF NOT EXISTS workflow_of_user ( uid INT NOT NULL, wid INT NOT NULL, PRIMARY KEY (uid, wid), FOREIGN KEY (uid) REFERENCES "user"(uid) ON DELETE CASCADE, FOREIGN KEY (wid) REFERENCES workflow(wid) ON DELETE CASCADE ); -- workflow_user_access CREATE TABLE IF NOT EXISTS workflow_user_access ( uid INT NOT NULL, wid INT NOT NULL, privilege privilege_enum NOT NULL DEFAULT 'NONE', PRIMARY KEY (uid, wid), FOREIGN KEY (uid) REFERENCES "user"(uid) ON DELETE CASCADE, FOREIGN KEY (wid) REFERENCES workflow(wid) ON DELETE CASCADE ); -- workflow_version CREATE TABLE IF NOT EXISTS workflow_version ( vid SERIAL PRIMARY KEY, wid INT NOT NULL, content TEXT NOT NULL, creation_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (wid) REFERENCES workflow(wid) ON DELETE CASCADE ); -- project CREATE TABLE IF NOT EXISTS project ( pid SERIAL PRIMARY KEY, name VARCHAR(128) NOT NULL, description VARCHAR(10000), owner_id INT NOT NULL, creation_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, color VARCHAR(6), UNIQUE (owner_id, name), FOREIGN KEY (owner_id) REFERENCES "user"(uid) ON DELETE CASCADE ); -- workflow_of_project CREATE TABLE IF NOT EXISTS workflow_of_project ( wid INT NOT NULL, pid INT NOT NULL, PRIMARY KEY (wid, pid), FOREIGN KEY (wid) REFERENCES workflow(wid) ON DELETE CASCADE, FOREIGN KEY (pid) REFERENCES project(pid) ON DELETE CASCADE ); -- project_user_access CREATE TABLE IF NOT EXISTS project_user_access ( uid INT NOT NULL, pid INT NOT NULL, privilege privilege_enum NOT NULL DEFAULT 'NONE', PRIMARY KEY (uid, pid), FOREIGN KEY (uid) REFERENCES "user"(uid) ON DELETE CASCADE, FOREIGN KEY (pid) REFERENCES project(pid) ON DELETE CASCADE ); -- workflow_computing_unit table CREATE TABLE IF NOT EXISTS workflow_computing_unit ( uid INT NOT NULL, name VARCHAR(128) NOT NULL, cuid SERIAL PRIMARY KEY, creation_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, terminate_time TIMESTAMP DEFAULT NULL, type workflow_computing_unit_type_enum, uri TEXT NOT NULL DEFAULT '', resource TEXT DEFAULT '', FOREIGN KEY (uid) REFERENCES "user"(uid) ON DELETE CASCADE ); -- workflow_executions CREATE TABLE IF NOT EXISTS workflow_executions ( eid SERIAL PRIMARY KEY, vid INT NOT NULL, uid INT NOT NULL, cuid INT, status SMALLINT NOT NULL DEFAULT 1, result TEXT, starting_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, last_update_time TIMESTAMP, bookmarked BOOLEAN DEFAULT FALSE, name VARCHAR(128) NOT NULL DEFAULT 'Untitled Execution', environment_version VARCHAR(128) NOT NULL, log_location TEXT, runtime_stats_uri TEXT, runtime_stats_size INT DEFAULT 0, FOREIGN KEY (vid) REFERENCES workflow_version(vid) ON DELETE CASCADE, FOREIGN KEY (uid) REFERENCES "user"(uid) ON DELETE CASCADE, FOREIGN KEY (cuid) REFERENCES workflow_computing_unit(cuid) ON DELETE CASCADE ); -- public_project CREATE TABLE IF NOT EXISTS public_project ( pid INT PRIMARY KEY, uid INT, FOREIGN KEY (pid) REFERENCES project(pid) ON DELETE CASCADE -- Note: MySQL schema doesn't define a foreign key for uid ); -- dataset CREATE TABLE IF NOT EXISTS dataset ( did SERIAL PRIMARY KEY, owner_uid INT NOT NULL, name VARCHAR(128) NOT NULL, repository_name VARCHAR(128), is_public BOOLEAN NOT NULL DEFAULT TRUE, is_downloadable BOOLEAN NOT NULL DEFAULT TRUE, description TEXT NOT NULL, creation_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, cover_image varchar(255), FOREIGN KEY (owner_uid) REFERENCES "user"(uid) ON DELETE CASCADE ); -- dataset_user_access CREATE TABLE IF NOT EXISTS dataset_user_access ( did INT NOT NULL, uid INT NOT NULL, privilege privilege_enum NOT NULL DEFAULT 'NONE', PRIMARY KEY (did, uid), FOREIGN KEY (did) REFERENCES dataset(did) ON DELETE CASCADE, FOREIGN KEY (uid) REFERENCES "user"(uid) ON DELETE CASCADE ); -- dataset_version CREATE TABLE IF NOT EXISTS dataset_version ( dvid SERIAL PRIMARY KEY, did INT NOT NULL, creator_uid INT NOT NULL, name VARCHAR(128) NOT NULL, version_hash VARCHAR(64) NOT NULL, creation_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (did) REFERENCES dataset(did) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS dataset_upload_session ( did INT NOT NULL, uid INT NOT NULL, file_path TEXT NOT NULL, upload_id VARCHAR(256) NOT NULL UNIQUE, physical_address TEXT, num_parts_requested INT NOT NULL, file_size_bytes BIGINT NOT NULL, part_size_bytes BIGINT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (uid, did, file_path), FOREIGN KEY (did) REFERENCES dataset(did) ON DELETE CASCADE, FOREIGN KEY (uid) REFERENCES "user"(uid) ON DELETE CASCADE, CONSTRAINT chk_dataset_upload_session_num_parts_requested_positive CHECK (num_parts_requested >= 1), CONSTRAINT chk_dataset_upload_session_file_size_bytes_positive CHECK (file_size_bytes > 0), CONSTRAINT chk_dataset_upload_session_part_size_bytes_positive CHECK (part_size_bytes > 0), CONSTRAINT chk_dataset_upload_session_part_size_bytes_s3_upper_bound CHECK (part_size_bytes <= 5368709120) ); CREATE TABLE IF NOT EXISTS dataset_upload_session_part ( upload_id VARCHAR(256) NOT NULL, part_number INT NOT NULL, etag TEXT NOT NULL DEFAULT '', PRIMARY KEY (upload_id, part_number), CONSTRAINT chk_part_number_positive CHECK (part_number > 0), FOREIGN KEY (upload_id) REFERENCES dataset_upload_session(upload_id) ON DELETE CASCADE ); -- operator_executions (modified to match MySQL: no separate primary key; added console_messages_uri) CREATE TABLE IF NOT EXISTS operator_executions ( workflow_execution_id INT NOT NULL, operator_id VARCHAR(100) NOT NULL, console_messages_uri TEXT, console_messages_size INT DEFAULT 0, PRIMARY KEY (workflow_execution_id, operator_id), FOREIGN KEY (workflow_execution_id) REFERENCES workflow_executions(eid) ON DELETE CASCADE ); -- operator_port_executions CREATE TABLE operator_port_executions ( workflow_execution_id INT NOT NULL, global_port_id VARCHAR(200) NOT NULL, result_uri TEXT, result_size INT DEFAULT 0, PRIMARY KEY (workflow_execution_id, global_port_id), FOREIGN KEY (workflow_execution_id) REFERENCES workflow_executions(eid) ON DELETE CASCADE ); -- workflow_user_likes CREATE TABLE IF NOT EXISTS workflow_user_likes ( uid INT NOT NULL, wid INT NOT NULL, PRIMARY KEY (uid, wid), FOREIGN KEY (uid) REFERENCES "user"(uid) ON DELETE CASCADE, FOREIGN KEY (wid) REFERENCES workflow(wid) ON DELETE CASCADE ); -- workflow_user_clones CREATE TABLE IF NOT EXISTS workflow_user_clones ( uid INT NOT NULL, wid INT NOT NULL, PRIMARY KEY (uid, wid), FOREIGN KEY (uid) REFERENCES "user"(uid) ON DELETE CASCADE, FOREIGN KEY (wid) REFERENCES workflow(wid) ON DELETE CASCADE ); -- workflow_view_count CREATE TABLE IF NOT EXISTS workflow_view_count ( wid INT NOT NULL PRIMARY KEY, view_count INT NOT NULL DEFAULT 0, FOREIGN KEY (wid) REFERENCES workflow(wid) ON DELETE CASCADE ); -- user_action table CREATE TABLE IF NOT EXISTS user_action ( user_action_id BIGSERIAL PRIMARY KEY, uid INTEGER, ip VARCHAR(15), action_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, resource_type VARCHAR(15) NOT NULL, resource_id INTEGER NOT NULL, action texera_db.action_enum NOT NULL, FOREIGN KEY (uid) REFERENCES "user"(uid) ON DELETE SET NULL ); -- dataset_user_likes table CREATE TABLE IF NOT EXISTS dataset_user_likes ( uid INTEGER NOT NULL, did INTEGER NOT NULL, PRIMARY KEY (uid, did), FOREIGN KEY (uid) REFERENCES "user"(uid) ON DELETE CASCADE, FOREIGN KEY (did) REFERENCES dataset(did) ON DELETE CASCADE ); -- dataset_view_count table CREATE TABLE IF NOT EXISTS dataset_view_count ( did INTEGER NOT NULL, view_count INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (did), FOREIGN KEY (did) REFERENCES dataset(did) ON DELETE CASCADE ); -- site_settings table CREATE TABLE IF NOT EXISTS site_settings ( key VARCHAR(255) PRIMARY KEY, value TEXT NOT NULL, updated_by VARCHAR(50), updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- user_last_active_time table CREATE TABLE IF NOT EXISTS user_last_active_time ( uid INT NOT NULL PRIMARY KEY REFERENCES "user"(uid), last_active_time TIMESTAMPTZ ); -- computing_unit_user_access table CREATE TABLE IF NOT EXISTS computing_unit_user_access ( cuid INT NOT NULL, uid INT NOT NULL, privilege privilege_enum NOT NULL DEFAULT 'NONE', PRIMARY KEY (cuid, uid), FOREIGN KEY (cuid) REFERENCES workflow_computing_unit(cuid) ON DELETE CASCADE, FOREIGN KEY (uid) REFERENCES "user"(uid) ON DELETE CASCADE ); -- START Fulltext search index creation (DO NOT EDIT THIS LINE) CREATE EXTENSION IF NOT EXISTS pgroonga; DO $$ DECLARE r RECORD; stem_filter TEXT := ''; plugin_status TEXT; BEGIN -- Drop all GIN and PGroonga indexes FOR r IN SELECT indexname FROM pg_indexes WHERE (indexdef ILIKE '%USING gin%' OR indexdef ILIKE '%USING pgroonga%') AND tablename IN ('workflow', 'user', 'project', 'dataset', 'dataset_version') LOOP EXECUTE format('DROP INDEX IF EXISTS %I;', r.indexname); END LOOP; -- Check if TokenFilterStem plugin is registered WITH plugin_registration AS ( SELECT pgroonga_command('plugin_register token_filters/stem') AS result ) SELECT CASE WHEN result::jsonb @> '[true]' THEN 'Plugin registered successfully' ELSE 'Plugin registration failed' END INTO plugin_status FROM plugin_registration; -- Set the stem_filter based on plugin status IF plugin_status = 'Plugin registered successfully' THEN stem_filter := ', plugins=''token_filters/stem'', token_filters=''TokenFilterStem'''; RAISE NOTICE 'Using TokenMecab + TokenFilterStem'; ELSE RAISE NOTICE 'Using TokenMecab only'; END IF; -- Create PGroonga indexes dynamically with correct TokenFilterStem usage FOR r IN SELECT tablename, CASE WHEN tablename = 'workflow' THEN '(COALESCE(name, '''') || '' '' || COALESCE(description, '''') || '' '' || COALESCE(content, ''''))' WHEN tablename IN ('project', 'dataset') THEN '(COALESCE(name, '''') || '' '' || COALESCE(description, ''''))' ELSE 'COALESCE(name, '''')' END AS index_column FROM (VALUES ('workflow'), ('user'), ('project'), ('dataset'), ('dataset_version')) AS t(tablename) LOOP -- Create PGroonga index with proper TokenFilterStem usage EXECUTE format( 'CREATE INDEX idx_%s_pgroonga ON %I USING pgroonga (%s) WITH (tokenizer = ''TokenMecab''%s);', r.tablename, r.tablename, r.index_column, stem_filter ); END LOOP; END $$; -- END Fulltext search index creation (DO NOT EDIT THIS LINE) ================================================ FILE: sql/texera_lakefs.sql ================================================ -- Licensed to the Apache Software Foundation (ASF) under one -- or more contributor license agreements. See the NOTICE file -- distributed with this work for additional information -- regarding copyright ownership. The ASF licenses this file -- to you under the Apache License, Version 2.0 (the -- "License"); you may not use this file except in compliance -- with the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, -- software distributed under the License is distributed on an -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -- KIND, either express or implied. See the License for the -- specific language governing permissions and limitations -- under the License. \c postgres DROP DATABASE IF EXISTS texera_lakefs; CREATE DATABASE texera_lakefs; ================================================ FILE: sql/texera_lakekeeper.sql ================================================ -- Licensed to the Apache Software Foundation (ASF) under one -- or more contributor license agreements. See the NOTICE file -- distributed with this work for additional information -- regarding copyright ownership. The ASF licenses this file -- to you under the Apache License, Version 2.0 (the -- "License"); you may not use this file except in compliance -- with the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, -- software distributed under the License is distributed on an -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -- KIND, either express or implied. See the License for the -- specific language governing permissions and limitations -- under the License. \c postgres DROP DATABASE IF EXISTS texera_lakekeeper; CREATE DATABASE texera_lakekeeper; ================================================ FILE: sql/updates/01.sql ================================================ -- Licensed to the Apache Software Foundation (ASF) under one -- or more contributor license agreements. See the NOTICE file -- distributed with this work for additional information -- regarding copyright ownership. The ASF licenses this file -- to you under the Apache License, Version 2.0 (the -- "License"); you may not use this file except in compliance -- with the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, -- software distributed under the License is distributed on an -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -- KIND, either express or implied. See the License for the -- specific language governing permissions and limitations -- under the License. -- ============================================ -- 1. Connect to the texera_db database -- ============================================ \c texera_db SET search_path TO texera_db; -- ============================================ -- 2. Update the table schema -- ============================================ BEGIN; -- 1. Rename original table temporarily ALTER TABLE operator_port_executions RENAME TO operator_port_executions_old; -- 2. Create the new table with columns in the correct order CREATE TABLE operator_port_executions ( workflow_execution_id INT NOT NULL, operator_id VARCHAR(100) NOT NULL, layer_name VARCHAR(100) NOT NULL DEFAULT 'main', port_id INT NOT NULL, result_uri TEXT, PRIMARY KEY (workflow_execution_id, operator_id, layer_name, port_id), FOREIGN KEY (workflow_execution_id) REFERENCES workflow_executions(eid) ON DELETE CASCADE ); -- 3. Copy data from old table (use the default value for `layer_id`) INSERT INTO operator_port_executions (workflow_execution_id, operator_id, port_id, result_uri) SELECT workflow_execution_id, operator_id, port_id, result_uri FROM operator_port_executions_old; -- 4. Drop the old table after copying data DROP TABLE operator_port_executions_old; COMMIT; ================================================ FILE: sql/updates/02.sql ================================================ -- Licensed to the Apache Software Foundation (ASF) under one -- or more contributor license agreements. See the NOTICE file -- distributed with this work for additional information -- regarding copyright ownership. The ASF licenses this file -- to you under the Apache License, Version 2.0 (the -- "License"); you may not use this file except in compliance -- with the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, -- software distributed under the License is distributed on an -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -- KIND, either express or implied. See the License for the -- specific language governing permissions and limitations -- under the License. \c texera_db; set search_path to texera_db, public; CREATE EXTENSION IF NOT EXISTS pgroonga; DO $$ DECLARE r RECORD; stem_filter TEXT := ''; plugin_status TEXT; BEGIN -- Drop all GIN and PGroonga indexes FOR r IN SELECT indexname FROM pg_indexes WHERE (indexdef ILIKE '%USING gin%' OR indexdef ILIKE '%USING pgroonga%') AND tablename IN ('workflow', 'user', 'project', 'dataset', 'dataset_version') LOOP EXECUTE format('DROP INDEX IF EXISTS %I;', r.indexname); END LOOP; -- Check if TokenFilterStem plugin is registered WITH plugin_registration AS ( SELECT pgroonga_command('plugin_register token_filters/stem') AS result ) SELECT CASE WHEN result::jsonb @> '[true]' THEN 'Plugin registered successfully' ELSE 'Plugin registration failed' END INTO plugin_status FROM plugin_registration; -- Set the stem_filter based on plugin status IF plugin_status = 'Plugin registered successfully' THEN stem_filter := ', plugins=''token_filters/stem'', token_filters=''TokenFilterStem'''; RAISE NOTICE 'Using TokenMecab + TokenFilterStem'; ELSE RAISE NOTICE 'Using TokenMecab only'; END IF; -- Create PGroonga indexes dynamically with correct TokenFilterStem usage FOR r IN SELECT tablename, CASE WHEN tablename = 'workflow' THEN '(COALESCE(name, '''') || '' '' || COALESCE(description, '''') || '' '' || COALESCE(content, ''''))' WHEN tablename IN ('project', 'dataset') THEN '(COALESCE(name, '''') || '' '' || COALESCE(description, ''''))' ELSE 'COALESCE(name, '''')' END AS index_column FROM (VALUES ('workflow'), ('user'), ('project'), ('dataset'), ('dataset_version')) AS t(tablename) LOOP -- Create PGroonga index with proper TokenFilterStem usage EXECUTE format( 'CREATE INDEX idx_%s_pgroonga ON %I USING pgroonga (%s) WITH (tokenizer = ''TokenMecab''%s);', r.tablename, r.tablename, r.index_column, stem_filter ); END LOOP; END $$; ================================================ FILE: sql/updates/03.sql ================================================ -- Licensed to the Apache Software Foundation (ASF) under one -- or more contributor license agreements. See the NOTICE file -- distributed with this work for additional information -- regarding copyright ownership. The ASF licenses this file -- to you under the Apache License, Version 2.0 (the -- "License"); you may not use this file except in compliance -- with the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, -- software distributed under the License is distributed on an -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -- KIND, either express or implied. See the License for the -- specific language governing permissions and limitations -- under the License. -- ============================================ -- 1. Connect to the texera_db database -- ============================================ \c texera_db SET search_path TO texera_db; -- ============================================ -- 2. Update the table schema -- ============================================ BEGIN; -- 1. Drop the existing table DROP TABLE IF EXISTS operator_port_executions; -- 2. Create the new table with updated columns CREATE TABLE operator_port_executions ( workflow_execution_id INT NOT NULL, global_port_id VARCHAR(200) NOT NULL, result_uri TEXT, PRIMARY KEY (workflow_execution_id, global_port_id), FOREIGN KEY (workflow_execution_id) REFERENCES workflow_executions(eid) ON DELETE CASCADE ); COMMIT; ================================================ FILE: sql/updates/04.sql ================================================ -- Licensed to the Apache Software Foundation (ASF) under one -- or more contributor license agreements. See the NOTICE file -- distributed with this work for additional information -- regarding copyright ownership. The ASF licenses this file -- to you under the Apache License, Version 2.0 (the -- "License"); you may not use this file except in compliance -- with the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, -- software distributed under the License is distributed on an -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -- KIND, either express or implied. See the License for the -- specific language governing permissions and limitations -- under the License. -- ============================================ -- 1. Connect to the texera_db database -- ============================================ \c texera_db SET search_path TO texera_db; -- ============================================ -- 2. Update the table schema -- ============================================ BEGIN; CREATE TABLE IF NOT EXISTS workflow_computing_unit ( uid INT NOT NULL, name VARCHAR(128) NOT NULL, cuid SERIAL PRIMARY KEY, creation_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, terminate_time TIMESTAMP DEFAULT NULL, FOREIGN KEY (uid) REFERENCES "user"(uid) ON DELETE CASCADE ); COMMIT; ================================================ FILE: sql/updates/05.sql ================================================ -- Licensed to the Apache Software Foundation (ASF) under one -- or more contributor license agreements. See the NOTICE file -- distributed with this work for additional information -- regarding copyright ownership. The ASF licenses this file -- to you under the Apache License, Version 2.0 (the -- "License"); you may not use this file except in compliance -- with the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, -- software distributed under the License is distributed on an -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -- KIND, either express or implied. See the License for the -- specific language governing permissions and limitations -- under the License. \c texera_db SET search_path TO texera_db; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'texera_db' AND table_name = 'user' AND column_name = 'comment' ) THEN ALTER TABLE "user" ADD COLUMN comment TEXT; END IF; END $$; ================================================ FILE: sql/updates/06.sql ================================================ -- Licensed to the Apache Software Foundation (ASF) under one -- or more contributor license agreements. See the NOTICE file -- distributed with this work for additional information -- regarding copyright ownership. The ASF licenses this file -- to you under the Apache License, Version 2.0 (the -- "License"); you may not use this file except in compliance -- with the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, -- software distributed under the License is distributed on an -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -- KIND, either express or implied. See the License for the -- specific language governing permissions and limitations -- under the License. \c texera_db SET search_path TO texera_db; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'texera_db' AND table_name = 'workflow_executions' AND column_name = 'runtime_stats_size' ) THEN ALTER TABLE workflow_executions ADD COLUMN runtime_stats_size INT DEFAULT 0; END IF; END $$; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'texera_db' AND table_name = 'operator_executions' AND column_name = 'console_messages_size' ) THEN ALTER TABLE operator_executions ADD COLUMN console_messages_size INT DEFAULT 0; END IF; END $$; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'texera_db' AND table_name = 'operator_port_executions' AND column_name = 'result_size' ) THEN ALTER TABLE operator_port_executions ADD COLUMN result_size INT DEFAULT 0; END IF; END $$; ================================================ FILE: sql/updates/07.sql ================================================ -- Licensed to the Apache Software Foundation (ASF) under one -- or more contributor license agreements. See the NOTICE file -- distributed with this work for additional information -- regarding copyright ownership. The ASF licenses this file -- to you under the Apache License, Version 2.0 (the -- "License"); you may not use this file except in compliance -- with the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, -- software distributed under the License is distributed on an -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -- KIND, either express or implied. See the License for the -- specific language governing permissions and limitations -- under the License. \c texera_db SET search_path TO texera_db; DO $$ BEGIN -- 1. Add cuid field to workflow_executions if not exist IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'texera_db' AND table_name = 'workflow_executions' AND column_name = 'cuid' ) THEN ALTER TABLE workflow_executions ADD COLUMN cuid INT; ALTER TABLE workflow_executions ADD CONSTRAINT workflow_executions_cuid_fkey FOREIGN KEY (cuid) REFERENCES workflow_computing_unit(cuid) ON DELETE CASCADE; END IF; -- 2. Add ENUM type for workflow_computing_unit.type IF NOT EXISTS ( SELECT 1 FROM pg_type WHERE typname = 'workflow_computing_unit_type_enum' ) THEN CREATE TYPE workflow_computing_unit_type_enum AS ENUM ('local', 'kubernetes'); END IF; -- 3. Add type column to workflow_computing_unit if not exist IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'texera_db' AND table_name = 'workflow_computing_unit' AND column_name = 'type' ) THEN ALTER TABLE workflow_computing_unit ADD COLUMN type workflow_computing_unit_type_enum; END IF; -- 4. Add uri column to workflow_computing_unit if not exist IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'texera_db' AND table_name = 'workflow_computing_unit' AND column_name = 'uri' ) THEN ALTER TABLE workflow_computing_unit ADD COLUMN uri TEXT NOT NULL DEFAULT ''; END IF; -- 5. Add resource column to workflow_computing_unit if not exist IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'texera_db' AND table_name = 'workflow_computing_unit' AND column_name = 'resource' ) THEN ALTER TABLE workflow_computing_unit ADD COLUMN resource TEXT DEFAULT ''; END IF; END $$; ================================================ FILE: sql/updates/08.sql ================================================ -- Licensed to the Apache Software Foundation (ASF) under one -- or more contributor license agreements. See the NOTICE file -- distributed with this work for additional information -- regarding copyright ownership. The ASF licenses this file -- to you under the Apache License, Version 2.0 (the -- "License"); you may not use this file except in compliance -- with the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, -- software distributed under the License is distributed on an -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -- KIND, either express or implied. See the License for the -- specific language governing permissions and limitations -- under the License. \c texera_db SET search_path TO texera_db; DO $$ BEGIN -- 1. Create site_settings table if it does not exist IF NOT EXISTS ( SELECT 1 FROM information_schema.tables WHERE table_schema = 'texera_db' AND table_name = 'site_settings' ) THEN CREATE TABLE site_settings ( key VARCHAR(255) PRIMARY KEY, value TEXT NOT NULL, updated_by VARCHAR(50), updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); END IF; END $$; ================================================ FILE: sql/updates/09.sql ================================================ -- Licensed to the Apache Software Foundation (ASF) under one -- or more contributor license agreements. See the NOTICE file -- distributed with this work for additional information -- regarding copyright ownership. The ASF licenses this file -- to you under the Apache License, Version 2.0 (the -- "License"); you may not use this file except in compliance -- with the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, -- software distributed under the License is distributed on an -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -- KIND, either express or implied. See the License for the -- specific language governing permissions and limitations -- under the License. \c texera_db SET search_path TO texera_db; CREATE TABLE IF NOT EXISTS computing_unit_user_access ( cuid INT NOT NULL, uid INT NOT NULL, privilege privilege_enum NOT NULL DEFAULT 'NONE', PRIMARY KEY (cuid, uid), FOREIGN KEY (cuid) REFERENCES workflow_computing_unit(cuid) ON DELETE CASCADE, FOREIGN KEY (uid) REFERENCES "user"(uid) ON DELETE CASCADE ); ================================================ FILE: sql/updates/10.sql ================================================ -- Licensed to the Apache Software Foundation (ASF) under one -- or more contributor license agreements. See the NOTICE file -- distributed with this work for additional information -- regarding copyright ownership. The ASF licenses this file -- to you under the Apache License, Version 2.0 (the -- "License"); you may not use this file except in compliance -- with the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, -- software distributed under the License is distributed on an -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -- KIND, either express or implied. See the License for the -- specific language governing permissions and limitations -- under the License. \c texera_db SET search_path TO texera_db; BEGIN; CREATE TABLE time_log ( uid INT NOT NULL PRIMARY KEY REFERENCES "user"(uid), last_login TIMESTAMPTZ ); COMMIT; ================================================ FILE: sql/updates/11.sql ================================================ -- Licensed to the Apache Software Foundation (ASF) under one -- or more contributor license agreements. See the NOTICE file -- distributed with this work for additional information -- regarding copyright ownership. The ASF licenses this file -- to you under the Apache License, Version 2.0 (the -- "License"); you may not use this file except in compliance -- with the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, -- software distributed under the License is distributed on an -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -- KIND, either express or implied. See the License for the -- specific language governing permissions and limitations -- under the License. \c texera_db SET search_path TO texera_db; BEGIN; -- Add downloadable field to dataset table ALTER TABLE dataset ADD COLUMN IF NOT EXISTS is_downloadable BOOLEAN NOT NULL DEFAULT TRUE; -- Update existing datasets: downloadable can only be true if dataset is public UPDATE dataset SET is_downloadable = is_public WHERE is_downloadable != is_public; COMMIT; ================================================ FILE: sql/updates/12.sql ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ \c texera_db SET search_path TO texera_db; BEGIN; ALTER TABLE time_log RENAME TO user_last_active_time; ALTER TABLE user_last_active_time RENAME COLUMN last_login TO last_active_time; COMMIT; ================================================ FILE: sql/updates/13.sql ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ \c texera_db SET search_path TO texera_db; BEGIN; -- Rename the old table to migrate later ALTER TABLE user_activity RENAME TO user_action_old; -- Validate existing values for "activate" DO $do$ BEGIN IF EXISTS ( SELECT 1 FROM user_action_old WHERE lower(activate) NOT IN ('like','unlike','clone','view') ) THEN RAISE EXCEPTION 'Error.'; END IF; END $do$; -- Create enum type DO $do$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace WHERE t.typname = 'action_enum' AND n.nspname = 'texera_db' ) THEN EXECUTE 'CREATE TYPE texera_db.action_enum AS ENUM (''like'',''unlike'',''clone'',''view'')'; END IF; END $do$; -- Create the new table CREATE TABLE user_action ( user_action_id BIGSERIAL PRIMARY KEY, uid INTEGER, ip VARCHAR(15), action_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, resource_type VARCHAR(15) NOT NULL, resource_id INTEGER NOT NULL, action texera_db.action_enum NOT NULL ); -- Copy data INSERT INTO user_action (uid, ip, action_time, resource_type, resource_id, action) SELECT CASE WHEN ua.uid = 0 OR u.uid IS NULL THEN NULL ELSE ua.uid END AS uid, ua.ip, ua.activity_time AS action_time, ua."type" AS resource_type, ua.id AS resource_id, lower(ua.activate)::texera_db.action_enum AS action FROM user_action_old ua LEFT JOIN "user" u ON u.uid = ua.uid; -- Add FK ALTER TABLE user_action ADD CONSTRAINT fk_user_action_uid FOREIGN KEY (uid) REFERENCES "user"(uid) ON DELETE SET NULL; -- Drop the old table DROP TABLE user_action_old; COMMIT; ================================================ FILE: sql/updates/14.sql ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ \c texera_db SET search_path TO texera_db; BEGIN; ALTER TABLE "user" ADD COLUMN IF NOT EXISTS account_creation_time TIMESTAMPTZ; WITH ts AS ( SELECT '2025-01-01 00:00:00-00'::timestamptz AS t ) UPDATE "user" u SET account_creation_time = ts.t FROM ts WHERE u.account_creation_time IS NULL; ALTER TABLE "user" ALTER COLUMN account_creation_time SET NOT NULL, ALTER COLUMN account_creation_time SET DEFAULT now(); COMMIT; ================================================ FILE: sql/updates/15.sql ================================================ -- Licensed to the Apache Software Foundation (ASF) under one -- or more contributor license agreements. See the NOTICE file -- distributed with this work for additional information -- regarding copyright ownership. The ASF licenses this file -- to you under the Apache License, Version 2.0 (the -- "License"); you may not use this file except in compliance -- with the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, -- software distributed under the License is distributed on an -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -- KIND, either express or implied. See the License for the -- specific language governing permissions and limitations -- under the License. -- ============================================ -- 1. Connect to the texera_db database -- ============================================ \c texera_db SET search_path TO texera_db; -- ============================================ -- 2. Update the table schema -- ============================================ BEGIN; -- 1. Add new column repository_name to dataset table. ALTER TABLE dataset ADD COLUMN repository_name varchar(128); -- 2. Copy the data from name column to repository_name column. UPDATE dataset SET repository_name = name; COMMIT; ================================================ FILE: sql/updates/16.sql ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ \c texera_db SET search_path TO texera_db; BEGIN; ALTER TABLE "user" ADD COLUMN IF NOT EXISTS affiliation VARCHAR(128); COMMIT; ================================================ FILE: sql/updates/17.sql ================================================ -- Licensed to the Apache Software Foundation (ASF) under one -- or more contributor license agreements. See the NOTICE file -- distributed with this work for additional information -- regarding copyright ownership. The ASF licenses this file -- to you under the Apache License, Version 2.0 (the -- "License"); you may not use this file except in compliance -- with the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, -- software distributed under the License is distributed on an -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -- KIND, either express or implied. See the License for the -- specific language governing permissions and limitations -- under the License. -- ============================================ -- 1. Connect to the texera_db database -- ============================================ \c texera_db SET search_path TO texera_db; -- ============================================ -- 2. Update the table schema -- ============================================ BEGIN; -- 1. Drop old tables (if exist) DROP TABLE IF EXISTS dataset_upload_session CASCADE; DROP TABLE IF EXISTS dataset_upload_session_part CASCADE; -- 2. Create dataset upload session table CREATE TABLE IF NOT EXISTS dataset_upload_session ( did INT NOT NULL, uid INT NOT NULL, file_path TEXT NOT NULL, upload_id VARCHAR(256) NOT NULL UNIQUE, physical_address TEXT, num_parts_requested INT NOT NULL, PRIMARY KEY (uid, did, file_path), FOREIGN KEY (did) REFERENCES dataset(did) ON DELETE CASCADE, FOREIGN KEY (uid) REFERENCES "user"(uid) ON DELETE CASCADE ); -- 3. Create dataset upload session parts table CREATE TABLE IF NOT EXISTS dataset_upload_session_part ( upload_id VARCHAR(256) NOT NULL, part_number INT NOT NULL, etag TEXT NOT NULL DEFAULT '', PRIMARY KEY (upload_id, part_number), CONSTRAINT chk_part_number_positive CHECK (part_number > 0), FOREIGN KEY (upload_id) REFERENCES dataset_upload_session(upload_id) ON DELETE CASCADE ); COMMIT; ================================================ FILE: sql/updates/18.sql ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ \c texera_db SET search_path TO texera_db; BEGIN; -- 1. Add new column cover_image to dataset table. ALTER TABLE dataset ADD COLUMN cover_image varchar(246); COMMIT; ================================================ FILE: sql/updates/19.sql ================================================ -- Licensed to the Apache Software Foundation (ASF) under one -- or more contributor license agreements. See the NOTICE file -- distributed with this work for additional information -- regarding copyright ownership. The ASF licenses this file -- to you under the Apache License, Version 2.0 (the -- "License"); you may not use this file except in compliance -- with the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, -- software distributed under the License is distributed on an -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -- KIND, either express or implied. See the License for the -- specific language governing permissions and limitations -- under the License. -- ============================================ -- 1. Connect to the texera_db database -- ============================================ \c texera_db SET search_path TO texera_db, public; -- ============================================ -- 2. Update the table schema -- ============================================ BEGIN; -- Add the 2 columns (defaults backfill existing rows safely) ALTER TABLE dataset_upload_session ADD COLUMN IF NOT EXISTS file_size_bytes BIGINT NOT NULL DEFAULT 1, ADD COLUMN IF NOT EXISTS part_size_bytes BIGINT NOT NULL DEFAULT 5242880; -- Drop any old/alternate constraint names from previous attempts (so we end up with exactly the new names) ALTER TABLE dataset_upload_session DROP CONSTRAINT IF EXISTS dataset_upload_session_num_parts_requested_positive, DROP CONSTRAINT IF EXISTS chk_dataset_upload_session_num_parts_requested_positive, DROP CONSTRAINT IF EXISTS chk_dataset_upload_session_file_size_bytes_positive, DROP CONSTRAINT IF EXISTS chk_dataset_upload_session_part_size_bytes_positive, DROP CONSTRAINT IF EXISTS dataset_upload_session_part_size_bytes_positive, DROP CONSTRAINT IF EXISTS dataset_upload_session_part_size_bytes_s3_upper_bound, DROP CONSTRAINT IF EXISTS chk_dataset_upload_session_part_size_bytes_s3_upper_bound; -- Add constraints exactly like the new CREATE TABLE ALTER TABLE dataset_upload_session ADD CONSTRAINT chk_dataset_upload_session_num_parts_requested_positive CHECK (num_parts_requested >= 1), ADD CONSTRAINT chk_dataset_upload_session_file_size_bytes_positive CHECK (file_size_bytes > 0), ADD CONSTRAINT chk_dataset_upload_session_part_size_bytes_positive CHECK (part_size_bytes > 0), ADD CONSTRAINT chk_dataset_upload_session_part_size_bytes_s3_upper_bound CHECK (part_size_bytes <= 5368709120); -- Match CREATE TABLE (no defaults) ALTER TABLE dataset_upload_session ALTER COLUMN file_size_bytes DROP DEFAULT, ALTER COLUMN part_size_bytes DROP DEFAULT; COMMIT; ================================================ FILE: sql/updates/20.sql ================================================ -- Licensed to the Apache Software Foundation (ASF) under one -- or more contributor license agreements. See the NOTICE file -- distributed with this work for additional information -- regarding copyright ownership. The ASF licenses this file -- to you under the Apache License, Version 2.0 (the -- "License"); you may not use this file except in compliance -- with the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, -- software distributed under the License is distributed on an -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -- KIND, either express or implied. See the License for the -- specific language governing permissions and limitations -- under the License. -- ============================================ -- 1. Connect to the texera_db database -- ============================================ \c texera_db SET search_path TO texera_db, public; BEGIN; -- Step 1: Add the column (no default yet) ALTER TABLE dataset_upload_session ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ; -- Step 2: Add the default for future inserts ALTER TABLE dataset_upload_session ALTER COLUMN created_at SET DEFAULT now(); ALTER TABLE dataset_upload_session ALTER COLUMN created_at SET NOT NULL; COMMIT; ================================================ FILE: sql/updates/21.sql ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ \c texera_db SET search_path TO texera_db; BEGIN; ALTER TABLE "user" ADD COLUMN IF NOT EXISTS joining_reason VARCHAR(500); COMMIT; ================================================ FILE: sql/updates/22.sql ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ \c texera_db SET search_path TO texera_db; BEGIN; -- Change description column from VARCHAR to TEXT to support Markdown content. ALTER TABLE dataset ALTER COLUMN description TYPE TEXT; ALTER TABLE workflow ALTER COLUMN description TYPE TEXT; COMMIT; ================================================ FILE: workflow-compiling-service/LICENSE-binary ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Support. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or support. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================================ THIRD-PARTY COMPONENTS ================================================================================ Apache Texera's binary distribution of this service includes the following third-party components, grouped by license. Each section references licenses/ for the full text of the applicable license. Components under the Apache License, Version 2.0 are governed by the same license terms as Apache Texera itself and are listed for completeness. Locations within the distribution: - Scala/Java jars listed below ship under the lib/ directory of this service's Universal zip. - Source files derived from third-party projects are compiled into the bundled jars under lib/ and live at the listed paths in the Apache Texera source tree. -------------------------------------------------------------------------------- Dependencies under the Apache License, Version 2.0 -------------------------------------------------------------------------------- Scala/Java jars: - com.fasterxml.classmate-1.7.0.jar - com.fasterxml.jackson.core.jackson-annotations-2.18.6.jar - com.fasterxml.jackson.core.jackson-core-2.18.6.jar - com.fasterxml.jackson.core.jackson-databind-2.18.6.jar - com.fasterxml.jackson.dataformat.jackson-dataformat-yaml-2.16.1.jar - com.fasterxml.jackson.datatype.jackson-datatype-guava-2.16.1.jar - com.fasterxml.jackson.datatype.jackson-datatype-jdk8-2.16.1.jar - com.fasterxml.jackson.datatype.jackson-datatype-jsr310-2.16.1.jar - com.fasterxml.jackson.jakarta.rs.jackson-jakarta-rs-base-2.16.1.jar - com.fasterxml.jackson.jakarta.rs.jackson-jakarta-rs-json-provider-2.16.1.jar - com.fasterxml.jackson.jaxrs.jackson-jaxrs-base-2.10.5.jar - com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider-2.10.5.jar - com.fasterxml.jackson.module.jackson-module-blackbird-2.16.1.jar - com.fasterxml.jackson.module.jackson-module-jakarta-xmlbind-annotations-2.16.1.jar - com.fasterxml.jackson.module.jackson-module-jsonSchema-2.18.6.jar - com.fasterxml.jackson.module.jackson-module-no-ctor-deser-2.18.6.jar - com.fasterxml.jackson.module.jackson-module-parameter-names-2.16.1.jar - com.fasterxml.jackson.module.jackson-module-scala_2.13-2.18.6.jar - com.fasterxml.woodstox.woodstox-core-5.3.0.jar - com.github.ben-manes.caffeine.caffeine-3.1.8.jar - com.github.sisyphsu.dateparser-1.0.11.jar - com.github.sisyphsu.retree-1.0.4.jar - com.github.stephenc.jcip.jcip-annotations-1.0-1.jar - com.github.tototoshi.scala-csv_2.13-1.3.10.jar - com.google.android.annotations-4.1.1.4.jar - com.google.api.grpc.proto-google-common-protos-2.22.0.jar - com.google.code.findbugs.jsr305-3.0.2.jar - com.google.code.gson.gson-2.10.1.jar - com.google.errorprone.error_prone_annotations-2.25.0.jar - com.google.flatbuffers.flatbuffers-java-23.5.26.jar - com.google.guava.failureaccess-1.0.2.jar - com.google.guava.guava-33.0.0-jre.jar - com.google.guava.listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar - com.google.inject.extensions.guice-servlet-4.0.jar - com.google.inject.guice-4.0.jar - com.google.j2objc.j2objc-annotations-2.8.jar - com.googlecode.javaewah.JavaEWAH-1.1.12.jar - com.helger.profiler-1.1.1.jar - com.nimbusds.nimbus-jose-jwt-9.8.1.jar - com.squareup.okhttp.okhttp-2.7.5.jar - com.squareup.okhttp3.logging-interceptor-4.12.0.jar - com.squareup.okhttp3.okhttp-4.12.0.jar - com.squareup.okio.okio-3.6.0.jar - com.squareup.okio.okio-jvm-3.6.0.jar - com.thesamet.scalapb.lenses_2.13-0.11.20.jar - com.thesamet.scalapb.scalapb-json4s_2.13-0.12.0.jar - com.thesamet.scalapb.scalapb-runtime_2.13-0.11.20.jar - com.typesafe.config-1.4.6.jar - com.typesafe.scala-logging.scala-logging_2.13-3.9.5.jar - com.univocity.univocity-parsers-2.9.1.jar - commons-beanutils.commons-beanutils-1.9.4.jar - commons-cli.commons-cli-1.2.jar - commons-codec.commons-codec-1.17.1.jar - commons-collections.commons-collections-3.2.2.jar - commons-io.commons-io-2.16.1.jar - commons-logging.commons-logging-1.2.jar - commons-net.commons-net-3.6.jar - commons-pool.commons-pool-1.6.jar - dev.failsafe.failsafe-3.3.2.jar - io.airlift.aircompressor-0.27.jar - io.dropwizard.dropwizard-configuration-4.0.7.jar - io.dropwizard.dropwizard-core-4.0.7.jar - io.dropwizard.dropwizard-health-4.0.7.jar - io.dropwizard.dropwizard-jackson-4.0.7.jar - io.dropwizard.dropwizard-jersey-4.0.7.jar - io.dropwizard.dropwizard-jetty-4.0.7.jar - io.dropwizard.dropwizard-lifecycle-4.0.7.jar - io.dropwizard.dropwizard-logging-4.0.7.jar - io.dropwizard.dropwizard-metrics-4.0.7.jar - io.dropwizard.dropwizard-request-logging-4.0.7.jar - io.dropwizard.dropwizard-servlets-4.0.7.jar - io.dropwizard.dropwizard-util-4.0.7.jar - io.dropwizard.dropwizard-validation-4.0.7.jar - io.dropwizard.logback.logback-throttling-appender-1.4.2.jar - io.dropwizard.metrics.metrics-annotation-4.2.25.jar - io.dropwizard.metrics.metrics-core-4.2.25.jar - io.dropwizard.metrics.metrics-healthchecks-4.2.25.jar - io.dropwizard.metrics.metrics-jakarta-servlets-4.2.25.jar - io.dropwizard.metrics.metrics-jersey3-4.2.25.jar - io.dropwizard.metrics.metrics-jetty11-4.2.25.jar - io.dropwizard.metrics.metrics-jmx-4.2.25.jar - io.dropwizard.metrics.metrics-json-4.2.25.jar - io.dropwizard.metrics.metrics-jvm-4.2.25.jar - io.dropwizard.metrics.metrics-logback-4.2.25.jar - io.grpc.grpc-api-1.60.0.jar - io.grpc.grpc-context-1.60.0.jar - io.grpc.grpc-core-1.60.0.jar - io.grpc.grpc-netty-1.60.0.jar - io.grpc.grpc-protobuf-1.60.0.jar - io.grpc.grpc-protobuf-lite-1.60.0.jar - io.grpc.grpc-stub-1.60.0.jar - io.grpc.grpc-util-1.60.0.jar - io.gsonfire.gson-fire-1.8.5.jar - io.lakefs.sdk-1.51.0.jar - io.netty.netty-3.10.6.Final.jar - io.netty.netty-buffer-4.1.104.Final.jar - io.netty.netty-codec-4.1.104.Final.jar - io.netty.netty-codec-http-4.1.100.Final.jar - io.netty.netty-codec-http2-4.1.100.Final.jar - io.netty.netty-codec-socks-4.1.100.Final.jar - io.netty.netty-common-4.1.104.Final.jar - io.netty.netty-handler-4.1.104.Final.jar - io.netty.netty-handler-proxy-4.1.100.Final.jar - io.netty.netty-resolver-4.1.104.Final.jar - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-linux-aarch_64.jar - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-linux-x86_64.jar - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-osx-aarch_64.jar - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-osx-x86_64.jar - io.netty.netty-tcnative-boringssl-static-2.0.61.Final-windows-x86_64.jar - io.netty.netty-tcnative-boringssl-static-2.0.61.Final.jar - io.netty.netty-tcnative-classes-2.0.61.Final.jar - io.netty.netty-transport-4.1.104.Final.jar - io.netty.netty-transport-native-unix-common-4.1.104.Final.jar - io.perfmark.perfmark-api-0.26.0.jar - io.r2dbc.r2dbc-spi-0.9.0.RELEASE.jar - jakarta.inject.jakarta.inject-api-2.0.1.jar - jakarta.validation.jakarta.validation-api-3.0.2.jar - javax.inject.javax.inject-1.jar - javax.validation.validation-api-2.0.1.Final.jar - log4j.log4j-1.2.17.jar - net.minidev.accessors-smart-2.4.2.jar - net.minidev.json-smart-2.4.2.jar - org.apache.arrow.arrow-format-15.0.2.jar - org.apache.arrow.arrow-memory-core-15.0.2.jar - org.apache.arrow.arrow-memory-netty-15.0.2.jar - org.apache.arrow.arrow-vector-15.0.2.jar - org.apache.arrow.flight-core-15.0.2.jar - org.apache.arrow.flight-grpc-15.0.2.jar - org.apache.avro.avro-1.12.0.jar - org.apache.commons.commons-compress-1.27.1.jar - org.apache.commons.commons-configuration2-2.1.1.jar - org.apache.commons.commons-jcs3-core-3.2.jar - org.apache.commons.commons-lang3-3.16.0.jar - org.apache.commons.commons-math3-3.1.1.jar - org.apache.commons.commons-text-1.11.0.jar - org.apache.commons.commons-vfs2-2.9.0.jar - org.apache.curator.curator-client-4.2.0.jar - org.apache.curator.curator-framework-4.2.0.jar - org.apache.curator.curator-recipes-4.2.0.jar - org.apache.hadoop.hadoop-annotations-3.3.1.jar - org.apache.hadoop.hadoop-auth-3.3.1.jar - org.apache.hadoop.hadoop-common-3.3.1.jar - org.apache.hadoop.hadoop-hdfs-client-3.3.1.jar - org.apache.hadoop.hadoop-mapreduce-client-core-3.3.1.jar - org.apache.hadoop.hadoop-yarn-api-3.3.1.jar - org.apache.hadoop.hadoop-yarn-client-3.3.1.jar - org.apache.hadoop.hadoop-yarn-common-3.3.1.jar - org.apache.hadoop.thirdparty.hadoop-shaded-guava-1.1.1.jar - org.apache.hadoop.thirdparty.hadoop-shaded-protobuf_3_7-1.1.1.jar - org.apache.htrace.htrace-core4-4.1.0-incubating.jar - org.apache.httpcomponents.client5.httpclient5-5.4.jar - org.apache.httpcomponents.core5.httpcore5-5.3.jar - org.apache.httpcomponents.core5.httpcore5-h2-5.3.jar - org.apache.httpcomponents.httpasyncclient-4.1.5.jar - org.apache.httpcomponents.httpclient-4.5.13.jar - org.apache.httpcomponents.httpcore-4.4.16.jar - org.apache.httpcomponents.httpcore-nio-4.4.13.jar - org.apache.httpcomponents.httpmime-4.5.13.jar - org.apache.iceberg.iceberg-api-1.7.1.jar - org.apache.iceberg.iceberg-aws-1.7.1.jar - org.apache.iceberg.iceberg-bundled-guava-1.7.1.jar - org.apache.iceberg.iceberg-common-1.7.1.jar - org.apache.iceberg.iceberg-core-1.7.1.jar - org.apache.iceberg.iceberg-data-1.7.1.jar - org.apache.iceberg.iceberg-parquet-1.7.1.jar - org.apache.kerby.kerb-admin-1.0.1.jar - org.apache.kerby.kerb-client-1.0.1.jar - org.apache.kerby.kerb-common-1.0.1.jar - org.apache.kerby.kerb-core-1.0.1.jar - org.apache.kerby.kerb-crypto-1.0.1.jar - org.apache.kerby.kerb-identity-1.0.1.jar - org.apache.kerby.kerb-server-1.0.1.jar - org.apache.kerby.kerb-simplekdc-1.0.1.jar - org.apache.kerby.kerb-util-1.0.1.jar - org.apache.kerby.kerby-asn1-1.0.1.jar - org.apache.kerby.kerby-config-1.0.1.jar - org.apache.kerby.kerby-pkix-1.0.1.jar - org.apache.kerby.kerby-util-1.0.1.jar - org.apache.kerby.kerby-xdr-1.0.1.jar - org.apache.kerby.token-provider-1.0.1.jar - org.apache.lucene.lucene-analyzers-common-8.11.4.jar - org.apache.lucene.lucene-core-8.11.4.jar - org.apache.lucene.lucene-memory-8.7.0.jar - org.apache.lucene.lucene-queries-8.7.0.jar - org.apache.lucene.lucene-queryparser-8.7.0.jar - org.apache.lucene.lucene-sandbox-8.7.0.jar - org.apache.orc.orc-core-1.9.4-nohive.jar - org.apache.orc.orc-shims-1.9.4.jar - org.apache.parquet.parquet-avro-1.13.1.jar - org.apache.parquet.parquet-column-1.13.1.jar - org.apache.parquet.parquet-common-1.13.1.jar - org.apache.parquet.parquet-encoding-1.13.1.jar - org.apache.parquet.parquet-format-structures-1.13.1.jar - org.apache.parquet.parquet-hadoop-1.13.1.jar - org.apache.parquet.parquet-jackson-1.13.1.jar - org.apache.yetus.audience-annotations-0.13.0.jar - org.apache.zookeeper.zookeeper-3.5.6.jar - org.apache.zookeeper.zookeeper-jute-3.5.6.jar - org.eclipse.jetty.jetty-http-11.0.20.jar - org.eclipse.jetty.jetty-io-11.0.20.jar - org.eclipse.jetty.jetty-security-11.0.20.jar - org.eclipse.jetty.jetty-server-11.0.20.jar - org.eclipse.jetty.jetty-servlet-11.0.20.jar - org.eclipse.jetty.jetty-servlets-11.0.20.jar - org.eclipse.jetty.jetty-util-11.0.20.jar - org.eclipse.jetty.toolchain.jetty-jakarta-servlet-api-5.0.2.jar - org.eclipse.jetty.toolchain.setuid.jetty-setuid-java-1.0.4.jar - org.eclipse.jetty.websocket.websocket-api-9.4.40.v20210413.jar - org.eclipse.jetty.websocket.websocket-client-9.4.40.v20210413.jar - org.eclipse.jetty.websocket.websocket-common-9.4.40.v20210413.jar - org.ehcache.sizeof-0.4.3.jar - org.hibernate.validator.hibernate-validator-7.0.5.Final.jar - org.javassist.javassist-3.30.2-GA.jar - org.jboss.logging.jboss-logging-3.5.3.Final.jar - org.jetbrains.annotations-17.0.0.jar - org.jetbrains.kotlin.kotlin-stdlib-1.9.10.jar - org.jetbrains.kotlin.kotlin-stdlib-common-1.9.10.jar - org.jetbrains.kotlin.kotlin-stdlib-jdk7-1.9.10.jar - org.jetbrains.kotlin.kotlin-stdlib-jdk8-1.9.10.jar - org.jheaps.jheaps-0.11.jar - org.jooq.jooq-3.16.23.jar - org.json4s.json4s-ast_2.13-4.0.1.jar - org.json4s.json4s-jackson-core_2.13-4.0.1.jar - org.openapitools.jackson-databind-nullable-0.2.6.jar - org.roaringbitmap.RoaringBitmap-1.3.0.jar - org.scala-lang.modules.scala-collection-compat_2.13-2.13.0.jar - org.scala-lang.scala-library-2.13.18.jar - org.scala-lang.scala-reflect-2.13.18.jar - org.slf4j.jcl-over-slf4j-2.0.12.jar - org.slf4j.log4j-over-slf4j-2.0.12.jar - org.xerial.snappy.snappy-java-1.1.8.3.jar - org.yaml.snakeyaml-2.2.jar - software.amazon.awssdk.annotations-2.29.51.jar - software.amazon.awssdk.apache-client-2.29.51.jar - software.amazon.awssdk.arns-2.29.51.jar - software.amazon.awssdk.auth-2.29.51.jar - software.amazon.awssdk.aws-core-2.29.51.jar - software.amazon.awssdk.aws-query-protocol-2.29.51.jar - software.amazon.awssdk.aws-xml-protocol-2.29.51.jar - software.amazon.awssdk.checksums-2.29.51.jar - software.amazon.awssdk.checksums-spi-2.29.51.jar - software.amazon.awssdk.crt-core-2.29.51.jar - software.amazon.awssdk.endpoints-spi-2.29.51.jar - software.amazon.awssdk.http-auth-2.29.51.jar - software.amazon.awssdk.http-auth-aws-2.29.51.jar - software.amazon.awssdk.http-auth-aws-eventstream-2.29.51.jar - software.amazon.awssdk.http-auth-spi-2.29.51.jar - software.amazon.awssdk.http-client-spi-2.29.51.jar - software.amazon.awssdk.identity-spi-2.29.51.jar - software.amazon.awssdk.json-utils-2.29.51.jar - software.amazon.awssdk.metrics-spi-2.29.51.jar - software.amazon.awssdk.netty-nio-client-2.29.51.jar - software.amazon.awssdk.profiles-2.29.51.jar - software.amazon.awssdk.protocol-core-2.29.51.jar - software.amazon.awssdk.regions-2.29.51.jar - software.amazon.awssdk.retries-2.29.51.jar - software.amazon.awssdk.retries-spi-2.29.51.jar - software.amazon.awssdk.s3-2.29.51.jar - software.amazon.awssdk.sdk-core-2.29.51.jar - software.amazon.awssdk.sts-2.29.51.jar - software.amazon.awssdk.third-party-jackson-core-2.29.51.jar - software.amazon.awssdk.utils-2.29.51.jar - software.amazon.eventstream.eventstream-1.0.1.jar -------------------------------------------------------------------------------- Dependencies under the MIT License -------------------------------------------------------------------------------- Source files derived from third-party MIT-licensed projects: - mbknor-jackson-jsonschema common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaDraft.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaArrayWithUniqueItems.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaBool.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDefault.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDescription.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaExamples.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaFormat.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInject.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInt.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaOptions.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaString.java common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaTitle.java https://github.com/mbknor/mbknor-jackson-jsonschema Scala/Java jars: - com.konghq.unirest-java-3.14.2.jar - io.github.classgraph.classgraph-4.8.157.jar - net.sourceforge.argparse4j.argparse4j-0.9.0.jar - org.checkerframework.checker-qual-3.52.0.jar - org.codehaus.mojo.animal-sniffer-annotations-1.23.jar - org.projectlombok.lombok-1.18.24.jar - org.reactivestreams.reactive-streams-1.0.4.jar - org.slf4j.jul-to-slf4j-2.0.12.jar - org.slf4j.slf4j-api-2.0.16.jar -------------------------------------------------------------------------------- Dependencies under the BSD 3-Clause License -------------------------------------------------------------------------------- Scala/Java jars: - com.google.protobuf.protobuf-java-3.25.8.jar - com.google.re2j.re2j-1.1.jar - com.jcraft.jsch-0.1.55.jar - com.thoughtworks.paranamer.paranamer-2.8.jar - org.jline.jline-3.9.0.jar - org.ow2.asm.asm-8.0.1.jar - org.threeten.threeten-extra-1.7.1.jar -------------------------------------------------------------------------------- Dependencies under the BSD 2-Clause License -------------------------------------------------------------------------------- Scala/Java jars: - com.github.luben.zstd-jni-1.5.0-1.jar - dnsjava.dnsjava-2.1.7.jar - org.codehaus.woodstox.stax2-api-4.2.1.jar - org.postgresql.postgresql-42.7.10.jar -------------------------------------------------------------------------------- Dependencies under the Eclipse Public License, Version 2.0 (some are dual licensed with GPL-2.0 with Classpath Exception) -------------------------------------------------------------------------------- Scala/Java jars: - jakarta.annotation.jakarta.annotation-api-2.1.1.jar - jakarta.el.jakarta.el-api-4.0.0.jar - jakarta.servlet.jakarta.servlet-api-5.0.0.jar - jakarta.ws.rs.jakarta.ws.rs-api-3.0.0.jar - javax.ws.rs.javax.ws.rs-api-2.1.1.jar - org.glassfish.hk2.external.aopalliance-repackaged-3.0.6.jar - org.glassfish.hk2.hk2-api-3.0.6.jar - org.glassfish.hk2.hk2-locator-3.0.3.jar - org.glassfish.hk2.hk2-utils-3.0.6.jar - org.glassfish.hk2.osgi-resource-locator-1.0.3.jar - org.glassfish.jakarta.el-4.0.2.jar - org.glassfish.jersey.containers.jersey-container-servlet-3.0.12.jar - org.glassfish.jersey.containers.jersey-container-servlet-core-3.0.12.jar - org.glassfish.jersey.core.jersey-client-3.0.12.jar - org.glassfish.jersey.core.jersey-common-3.0.12.jar - org.glassfish.jersey.core.jersey-server-3.0.12.jar - org.glassfish.jersey.ext.jersey-bean-validation-3.0.12.jar - org.glassfish.jersey.ext.jersey-metainf-services-3.0.12.jar - org.glassfish.jersey.inject.jersey-hk2-3.0.12.jar - org.jgrapht.jgrapht-core-1.4.0.jar -------------------------------------------------------------------------------- Dependencies under the Eclipse Public License, Version 1.0 (Logback is dual licensed with LGPL-2.1) -------------------------------------------------------------------------------- Scala/Java jars: - ch.qos.logback.logback-access-1.4.14.jar - ch.qos.logback.logback-classic-1.4.14.jar - ch.qos.logback.logback-core-1.4.14.jar -------------------------------------------------------------------------------- Dependencies under the Common Development and Distribution License (CDDL) (some are dual licensed with GPL-2.0 with Classpath Exception) -------------------------------------------------------------------------------- CDDL 1.0 ~~~~~~~~ Scala/Java jars: - javax.annotation.javax.annotation-api-1.3.2.jar - javax.servlet.javax.servlet-api-3.1.0.jar - javax.ws.rs.jsr311-api-1.1.1.jar CDDL 1.1 ~~~~~~~~ Scala/Java jars: - com.sun.jersey.contribs.jersey-guice-1.19.jar -------------------------------------------------------------------------------- Dependencies under the Eclipse Distribution License, Version 1.0 -------------------------------------------------------------------------------- Scala/Java jars: - com.sun.activation.jakarta.activation-2.0.1.jar - jakarta.activation.jakarta.activation-api-2.1.0.jar - jakarta.xml.bind.jakarta.xml.bind-api-3.0.1.jar - org.eclipse.collections.eclipse-collections-11.1.0.jar - org.eclipse.collections.eclipse-collections-api-11.1.0.jar - org.eclipse.jgit.org.eclipse.jgit-5.13.0.202109080827-r.jar -------------------------------------------------------------------------------- Dependencies in the Public Domain (CC0) -------------------------------------------------------------------------------- Scala/Java jars: - aopalliance.aopalliance-1.0.jar - org.tukaani.xz-1.9.jar Individual jars may contain their own META-INF/LICENSE and META-INF/NOTICE files that apply to their specific contents; those files continue to govern the use of those components. ================================================ FILE: workflow-compiling-service/NOTICE-binary ================================================ Apache Texera (Incubating) Copyright 2025-2026 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). -------------------------------------------------------------------------------- Apache Hadoop -------------------------------------------------------------------------------- Apache Hadoop Copyright 2006 and onwards The Apache Software Foundation. Export Control Notice --------------------- This distribution includes cryptographic software. The country in which you currently reside may have restrictions on the import, possession, use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. See for more information. The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this software as Export Commodity Control Number (ECCN) 5D002.C.1, which includes information security software using or performing cryptographic functions with asymmetric algorithms. The form and manner of this Apache Software Foundation distribution makes it eligible for export under the License Exception ENC Technology Software Unrestricted (TSU) exception (see the BIS Export Administration Regulations, Section 740.13) for both object code and source code. The following provides more details on the included cryptographic software: This software uses the SSL libraries from the Jetty project written by mortbay.org. Hadoop Yarn Server Web Proxy uses the BouncyCastle Java cryptography APIs written by the Legion of the Bouncy Castle Inc. -------------------------------------------------------------------------------- Apache Lucene -------------------------------------------------------------------------------- Apache Lucene Copyright 2001-2021 The Apache Software Foundation Includes software from other Apache Software Foundation projects, including, but not limited to Apache Ant, Apache Jakarta Regexp, Apache Commons, and Apache Xerces. ICU4J (under analysis/icu) is licensed under an MIT-style license and Copyright (c) 1995-2008 International Business Machines Corporation and others. Some data files (under analysis/icu/src/data) are derived from Unicode data such as the Unicode Character Database. See http://unicode.org/copyright.html for more details. Brics Automaton (under core/src/java/org/apache/lucene/util/automaton) is BSD-licensed, created by Anders Moller. See http://www.brics.dk/automaton/ The levenshtein automata tables (under core/src/java/org/apache/lucene/util/automaton) were automatically generated with the moman/finenight FSA library, created by Jean-Philippe Barrette-LaPierre. This library is available under an MIT license. The class org.apache.lucene.util.WeakIdentityMap was derived from the Apache CXF project and is Apache License 2.0. The class org.apache.lucene.util.compress.LZ4 is a Java rewrite of the LZ4 compression library (https://github.com/lz4/lz4/tree/dev/lib) that is licensed under the 2-clause BSD license. The Google Code Prettify is Apache License 2.0. This product includes code (JaspellTernarySearchTrie) from Java Spelling Checking Package (jaspell): http://jaspell.sourceforge.net/ (BSD License). The snowball stemmers (in analysis/common/src/java/net/sf/snowball) were developed by Martin Porter and Richard Boulton. The KStem stemmer in analysis/common/src/org/apache/lucene/analysis/en was developed by Bob Krovetz and Sergio Guzman-Lara (CIIR-UMass Amherst) under the BSD license. Arabic, Persian, Romanian, Bulgarian, Hindi and Bengali analyzer stopword lists are BSD-licensed and were created by Jacques Savoy. The German, Spanish, Finnish, French, Hungarian, Italian, Portuguese, Russian and Swedish light stemmers are based on BSD-licensed reference implementations created by Jacques Savoy and Ljiljana Dolamic. The Stempel analyzer includes BSD-licensed software developed by the Egothor project (http://egothor.sf.net/), created by Leo Galambos, Martin Kvapil, and Edmond Nolan. The Polish analyzer stopword list is BSD-licensed and was created by the Carrot2 project. The SmartChineseAnalyzer source code (smartcn) was provided by Xiaoping Gao and copyright 2009 by www.imdict.net. WordBreakTestUnicode_*.java is derived from Unicode data such as the Unicode Character Database. -------------------------------------------------------------------------------- Apache Parquet -------------------------------------------------------------------------------- Apache Parquet MR Copyright 2014-2024 The Apache Software Foundation This product includes code from Apache Avro. Apache Avro Copyright 2010-2024 The Apache Software Foundation -------------------------------------------------------------------------------- Apache Iceberg -------------------------------------------------------------------------------- Apache Iceberg Copyright 2017-2024 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). -------------------------------------------------------------------------------- This project includes code from Kite, developed at Cloudera, Inc. with the following copyright notice: | Copyright 2013 Cloudera Inc. | | Licensed under the Apache License, Version 2.0 (the "License"); | you may not use this file except in compliance with the License. | You may obtain a copy of the License at | | http://www.apache.org/licenses/LICENSE-2.0 | | Unless required by applicable law or agreed to in writing, software | distributed under the License is distributed on an "AS IS" BASIS, | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | See the License for the specific language governing permissions and | limitations under the License. Apache Arrow (arrow-format, arrow-memory-core, arrow-memory-netty, arrow-vector, flight-core, flight-grpc) Copyright 2016-2023 The Apache Software Foundation Apache Avro Copyright 2009-2024 The Apache Software Foundation Apache Commons BeanUtils Copyright 2000-2019 The Apache Software Foundation Apache Commons CLI Copyright 2001-2022 The Apache Software Foundation Apache Commons Codec Copyright 2002-2024 The Apache Software Foundation Apache Commons Collections (3.x and 4.x) Copyright 2001-2024 The Apache Software Foundation Apache Commons Compress Copyright 2002-2024 The Apache Software Foundation Apache Commons Configuration Copyright 2001-2024 The Apache Software Foundation Apache Commons IO Copyright 2002-2024 The Apache Software Foundation Apache Commons JCS Copyright 2002-2024 The Apache Software Foundation Apache Commons Lang (2.x and 3.x) Copyright 2001-2024 The Apache Software Foundation Apache Commons Logging Copyright 2003-2014 The Apache Software Foundation Apache Commons Math Copyright 2001-2016 The Apache Software Foundation Apache Commons Net Copyright 2001-2023 The Apache Software Foundation Apache Commons Pool Copyright 2001-2024 The Apache Software Foundation Apache Commons Text Copyright 2014-2024 The Apache Software Foundation Apache Commons VFS Copyright 2002-2024 The Apache Software Foundation Apache Curator Copyright 2011-2024 The Apache Software Foundation Apache HttpComponents (httpclient, httpcore, httpasyncclient, httpclient5, httpcore5, httpcore5-h2, httpmime) Copyright 1999-2024 The Apache Software Foundation Apache HTrace (Incubating) Copyright 2016-2017 The Apache Software Foundation Apache Iceberg Copyright 2017-2024 The Apache Software Foundation Apache Kerby (kerb-* and kerby-* subprojects) Copyright 2014-2017 The Apache Software Foundation Apache Maven (many subprojects including wagon-*) Copyright 2001-2024 The Apache Software Foundation Apache ORC Copyright 2013-2024 The Apache Software Foundation Apache Yetus Copyright 2015-2023 The Apache Software Foundation Apache ZooKeeper Copyright 2008-2024 The Apache Software Foundation Apache log4j 1.2 / reload4j Copyright 2007 The Apache Software Foundation -------------------------------------------------------------------------------- Netty -------------------------------------------------------------------------------- The Netty Project Copyright 2011-2024 The Netty Project (https://netty.io/). This product contains the extensions to Java Collections Framework derived from the works by JSR-166 EG, Doug Lea, and Jason T. Greene (Public Domain). This product contains a modified version of Robert Harder's Public Domain Base64 Encoder and Decoder. This product contains a modified version of 'JZlib', a re-implementation of zlib in pure Java (BSD-style license, http://www.jcraft.com/jzlib/). This product contains a modified version of 'Webbit' (BSD License, https://github.com/joewalnes/webbit). This product optionally depends on 'Protocol Buffers' (New BSD License, http://code.google.com/p/protobuf/), 'Bouncy Castle Crypto APIs' (MIT License, http://www.bouncycastle.org/), 'SLF4J' (MIT License, http://www.slf4j.org/), 'Apache Commons Logging' (Apache License 2.0), 'Apache Log4J' (Apache License 2.0), 'JBoss Logging' (GNU LGPL 2.1), and 'Apache Felix' (Apache License 2.0). -------------------------------------------------------------------------------- Eclipse Jetty -------------------------------------------------------------------------------- Jetty Web Container Copyright 1995-2018 Mort Bay Consulting Pty Ltd. The Jetty Web Container is Copyright Mort Bay Consulting Pty Ltd unless otherwise noted. Jetty is dual licensed under both the Apache 2.0 License and the Eclipse Public 1.0 License; Texera redistributes it under the Apache 2.0 terms. Jetty bundles select artifacts under secondary licenses: * Eclipse Public License: org.eclipse.jetty.orbit:org.eclipse.jdt.core, javax.security.auth.message (EPL + ASL2), javax.mail.glassfish (EPL + CDDL 1.0) * CDDL + GPLv2 with classpath exception: javax.servlet:javax.servlet-api, javax.annotation:javax.annotation-api, javax.transaction:javax.transaction-api, javax.websocket:javax.websocket-api * OW2 license: org.ow2.asm:asm-commons, org.ow2.asm:asm * MortBay ASL2: org.mortbay.jasper:apache-jsp, apache-el (based on selected classes from Apache Tomcat) The UnixCrypt.java code implements one-way cryptography used by Unix systems for simple password protection. Copyright 1996 Aki Yoshida, modified April 2001 by Iris Van den Broeke, Daniel Deville. -------------------------------------------------------------------------------- Jackson (FasterXML) -------------------------------------------------------------------------------- Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. Copyright 2007- Tatu Saloranta (tatu.saloranta@iki.fi) Jackson 2.x core and extension components are licensed under Apache License 2.0. This attribution applies to jackson-core, jackson-databind, jackson-annotations, and every jackson-datatype-*, jackson-module-*, jackson-dataformat-*, and jackson-jaxrs-* artifact bundled in this distribution. Java ClassMate library (com.fasterxml:classmate) was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), with contributions from Brian Langel. -------------------------------------------------------------------------------- Google Guice -------------------------------------------------------------------------------- Google Guice - Core Library (and guice-servlet extension) Copyright 2006-2015 Google, Inc. -------------------------------------------------------------------------------- AWS SDK for Java 2.0 -------------------------------------------------------------------------------- AWS SDK for Java 2.0 Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. This product includes software developed by Amazon Technologies, Inc (http://www.amazon.com/). The AWS SDK bundles the following third-party works: * XML parsing and utility functions from JetS3t Copyright 2006-2009 James Murty. * PKCS#1 PEM encoded private key parsing and utility functions from oauth.googlecode.com - Copyright 1998-2010 AOL Inc. * Apache Commons Lang (https://github.com/apache/commons-lang) * Netty Reactive Streams (https://github.com/playframework/netty-reactive-streams) * Jackson-core (https://github.com/FasterXML/jackson-core), shaded as software.amazon.awssdk:third-party-jackson-core * Jackson-dataformat-cbor (https://github.com/FasterXML/jackson-dataformats-binary) Required Apache Commons Lang attribution: Apache Commons Lang Copyright 2001-2020 The Apache Software Foundation -------------------------------------------------------------------------------- Jackson core (verbatim upstream NOTICE) -------------------------------------------------------------------------------- # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. ## Copyright Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) ## Licensing Jackson 2.x core and extension components are licensed under Apache License 2.0 To find the details that apply to this artifact see the accompanying LICENSE file. ## Credits A list of contributors may be found from CREDITS(-2.x) file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. ## FastDoubleParser jackson-core bundles a shaded copy of FastDoubleParser . That code is available under an MIT license under the following copyright. Copyright © 2023 Werner Randelshofer, Switzerland. MIT License. See FastDoubleParser-NOTICE for details of other source code included in FastDoubleParser and the licenses and copyrights that apply to that code. # FastDoubleParser This is a Java port of Daniel Lemire's fast_float project. This project provides parsers for double, float, BigDecimal and BigInteger values. ## Copyright Copyright © 2024 Werner Randelshofer, Switzerland. ## Licensing This code is licensed under MIT License. https://github.com/wrandelshofer/FastDoubleParser/blob/522be16e145f43308c43b23094e31d5efcaa580e/LICENSE (The file 'LICENSE' is included in the sources and classes Jar files that are released by this project - as is required by that license.) Some portions of the code have been derived from other projects. All these projects require that we include a copyright notice, and some require that we also include some text of their license file. fast_double_parser, Copyright (c) 2022 Daniel Lemire. BSL License. https://github.com/lemire/fast_double_parser https://github.com/lemire/fast_double_parser/blob/07d9189a8fb815fe800cb15ca022e7a07093236e/LICENSE.BSL (The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project - as is required by that license.) fast_float, Copyright (c) 2021 The fast_float authors. MIT License. https://github.com/fastfloat/fast_float https://github.com/fastfloat/fast_float/blob/cc1e01e9eee74128e48d51488a6b1df4a767a810/LICENSE-MIT (The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project - as is required by that license.) bigint, Copyright 2020 Tim Buktu. 2-clause BSD License. https://github.com/tbuktu/bigint/tree/floatfft https://github.com/tbuktu/bigint/blob/617c8cd8a7c5e4fb4d919c6a4d11e2586107f029/LICENSE https://github.com/wrandelshofer/FastDoubleParser/blob/39e123b15b71f29a38a087d16a0bc620fc879aa6/bigint-LICENSE (We only use those portions of the bigint project that can be licensed under 2-clause BSD License.) (The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project - as is required by that license.) -------------------------------------------------------------------------------- Jackson modules and datatypes -------------------------------------------------------------------------------- # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. ## Copyright Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) ## Licensing Jackson 2.x core and extension components are licensed under Apache License 2.0 To find the details that apply to this artifact see the accompanying LICENSE file. ## Credits A list of contributors may be found from CREDITS(-2.x) file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers, as well as supported commercially by FasterXML.com. ## Licensing Jackson core and extension components (as well their dependencies) may be licensed under different licenses. To find the details that apply to this artifact see the accompanying LICENSE file. For more information, including possible other licensing options, contact FasterXML.com (http://fasterxml.com). ## Credits A list of contributors may be found from CREDITS file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers, as well as supported commercially by FasterXML.com. ## Licensing Jackson core and extension components may be licensed under different licenses. To find the details that apply to this artifact see the accompanying LICENSE file. For more information, including possible other licensing options, contact FasterXML.com (http://fasterxml.com). ## Credits A list of contributors may be found from CREDITS file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. ## Copyright Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) ## Licensing Jackson components are licensed under Apache (Software) License, version 2.0, as per accompanying LICENSE file. ## Credits A list of contributors may be found from CREDITS file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers, as well as supported commercially by FasterXML.com. ## Licensing Jackson core and extension components may licensed under different licenses. To find the details that apply to this artifact see the accompanying LICENSE file. For more information, including possible other licensing options, contact FasterXML.com (http://fasterxml.com). ## Credits A list of contributors may be found from CREDITS file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. ## Licensing Jackson components are licensed under Apache (Software) License, version 2.0, as per accompanying LICENSE file. ## Credits A list of contributors may be found from CREDITS file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. -------------------------------------------------------------------------------- Eclipse Jetty 11.0 -------------------------------------------------------------------------------- Notices for Eclipse Jetty ========================= This content is produced and maintained by the Eclipse Jetty project. Project home: https://eclipse.dev/jetty/ Trademarks ---------- Eclipse Jetty, and Jetty are trademarks of the Eclipse Foundation. Copyright --------- All contributions are the property of the respective authors or of entities to which copyright has been assigned by the authors (eg. employer). Declared Project Licenses ------------------------- This artifacts of this project are made available under the terms of: * the Eclipse Public License v2.0 https://www.eclipse.org/legal/epl-2.0 SPDX-License-Identifier: EPL-2.0 or * the Apache License, Version 2.0 https://www.apache.org/licenses/LICENSE-2.0 SPDX-License-Identifier: Apache-2.0 The following dependencies are EPL. * org.eclipse.jetty.orbit:org.eclipse.jdt.core The following dependencies are EPL and ASL2. * org.eclipse.jetty.orbit:javax.security.auth.message The following dependencies are EPL and CDDL 1.0. * org.eclipse.jetty.orbit:javax.mail.glassfish The following dependencies are CDDL + GPLv2 with classpath exception. https://glassfish.dev.java.net/nonav/public/CDDL+GPL.html * jakarta.servlet:jakarta.servlet-api * javax.annotation:javax.annotation-api * javax.transaction:javax.transaction-api * javax.websocket:javax.websocket-api The following dependencies are licensed by the OW2 Foundation according to the terms of http://asm.ow2.org/license.html * org.ow2.asm:asm-commons * org.ow2.asm:asm The following dependencies are ASL2 licensed. * org.apache.taglibs:taglibs-standard-spec * org.apache.taglibs:taglibs-standard-impl The following dependencies are ASL2 licensed. Based on selected classes from following Apache Tomcat jars, all ASL2 licensed. * org.mortbay.jasper:apache-jsp * org.apache.tomcat:tomcat-jasper * org.apache.tomcat:tomcat-juli * org.apache.tomcat:tomcat-jsp-api * org.apache.tomcat:tomcat-el-api * org.apache.tomcat:tomcat-jasper-el * org.apache.tomcat:tomcat-api * org.apache.tomcat:tomcat-util-scan * org.apache.tomcat:tomcat-util * org.mortbay.jasper:apache-el * org.apache.tomcat:tomcat-jasper-el * org.apache.tomcat:tomcat-el-api The following artifacts are CDDL + GPLv2 with classpath exception. https://glassfish.dev.java.net/nonav/public/CDDL+GPL.html * org.eclipse.jetty.toolchain:jetty-schemas Cryptography ------------ Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. The UnixCrypt.java code implements the one way cryptography used by Unix systems for simple password protection. Copyright 1996 Aki Yoshida, modified April 2001 by Iris Van den Broeke, Daniel Deville. Permission to use, copy, modify and distribute UnixCrypt for non-commercial or commercial purposes and without fee is granted provided that the copyright notice appears in all copies. -------------------------------------------------------------------------------- Apache Parquet (per-component supplementary notices) -------------------------------------------------------------------------------- Apache Parquet MR (Incubating) Copyright 2014-2015 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). -------------------------------------------------------------------------------- This product includes code from Apache Avro, which includes the following in its NOTICE file: Apache Avro Copyright 2010-2015 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Jackson JSON processor Jackson is a high-performance, Free/Open Source JSON processing library. It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has been in development since 2007. It is currently developed by a community of developers. ## Licensing Jackson 2.x core and extension components are licensed under Apache License 2.0 To find the details that apply to this artifact see the accompanying LICENSE file. ## Credits A list of contributors may be found from CREDITS(-2.x) file, which is included in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. -------------------------------------------------------------------------------- R2DBC SPI -------------------------------------------------------------------------------- Reactive Relational Database Connectivity Copyright 2017-2021 the original author or authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- Eclipse Jersey (jersey-container-servlet, jersey-container-servlet-core, jersey-client, jersey-hk2, jersey-media-jaxb) -------------------------------------------------------------------------------- # Notice for Jersey This content is produced and maintained by the Eclipse Jersey project. * Project home: https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Angular JS, v1.6.6 * License MIT (http://www.opensource.org/licenses/mit-license.php) * Project: http://angularjs.org * Coyright: (c) 2010-2017 Google, Inc. aopalliance Version 1 * License: all the source code provided by AOP Alliance is Public Domain. * Project: http://aopalliance.sourceforge.net * Copyright: Material in the public domain is not protected by copyright Bean Validation API 3.0.2 * License: Apache License, 2.0 * Project: http://beanvalidation.org/1.1/ * Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors * by the @authors tag. Hibernate Validator CDI, 7.0.5.Final * License: Apache License, 2.0 * Project: https://beanvalidation.org/ * Repackaged in org.glassfish.jersey.server.validation.internal.hibernate Bootstrap v3.3.7 * License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE) * Project: http://getbootstrap.com * Copyright: 2011-2016 Twitter, Inc Google Guava Version 18.0 * License: Apache License, 2.0 * Copyright (C) 2009 The Guava Authors jakarta.inject Version: 1 * License: Apache License, 2.0 * Copyright (C) 2009 The JSR-330 Expert Group Javassist Version 3.29.2-GA * License: Apache License, 2.0 * Project: http://www.javassist.org/ * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. Jackson JAX-RS Providers Version 2.15.3 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers * Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. jQuery v1.12.4 * License: jquery.org/license * Project: jquery.org * Copyright: (c) jQuery Foundation jQuery Barcode plugin 0.3 * License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html) * Project: http://www.pasella.it/projects/jQuery/barcode * Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com JSR-166 Extension - JEP 266 * License: CC0 * No copyright * Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/ KineticJS, v4.7.1 * License: MIT license (http://www.opensource.org/licenses/mit-license.php) * Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS * Copyright: Eric Rowell org.objectweb.asm Version 9.6 * License: Modified BSD (https://asm.ow2.io/license.html) * Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved. org.osgi.core version 6.0.0 * License: Apache License, 2.0 * Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved. org.glassfish.jersey.server.internal.monitoring.core * License: Apache License, 2.0 * Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. * Copyright 2010-2013 Coda Hale and Yammer, Inc. W3.org documents * License: W3C License * Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ -------------------------------------------------------------------------------- Eclipse Jersey Core Server -------------------------------------------------------------------------------- # Notice for Jersey Core Server module This content is produced and maintained by the Eclipse Jersey project. * https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content org.glassfish.jersey.server.internal.monitoring.core * License: Apache License, 2.0 * Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. * Copyright 2010-2013 Coda Hale and Yammer, Inc. org.objectweb.asm Version 9.6 * License: Modified BSD (https://asm.ow2.io/license.html) * Copyright: (c) 2000-2011 INRIA, France Telecom. All rights reserved. W3.org documents * License: W3C License * Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ # Notice for Jersey This content is produced and maintained by the Eclipse Jersey project. * Project home: https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Angular JS, v1.6.6 * License MIT (http://www.opensource.org/licenses/mit-license.php) * Project: http://angularjs.org * Coyright: (c) 2010-2017 Google, Inc. aopalliance Version 1 * License: all the source code provided by AOP Alliance is Public Domain. * Project: http://aopalliance.sourceforge.net * Copyright: Material in the public domain is not protected by copyright Bean Validation API 3.0.2 * License: Apache License, 2.0 * Project: http://beanvalidation.org/1.1/ * Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors * by the @authors tag. Hibernate Validator CDI, 7.0.5.Final * License: Apache License, 2.0 * Project: https://beanvalidation.org/ * Repackaged in org.glassfish.jersey.server.validation.internal.hibernate Bootstrap v3.3.7 * License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE) * Project: http://getbootstrap.com * Copyright: 2011-2016 Twitter, Inc Google Guava Version 18.0 * License: Apache License, 2.0 * Copyright (C) 2009 The Guava Authors jakarta.inject Version: 1 * License: Apache License, 2.0 * Copyright (C) 2009 The JSR-330 Expert Group Javassist Version 3.29.2-GA * License: Apache License, 2.0 * Project: http://www.javassist.org/ * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. Jackson JAX-RS Providers Version 2.15.3 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers * Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. jQuery v1.12.4 * License: jquery.org/license * Project: jquery.org * Copyright: (c) jQuery Foundation jQuery Barcode plugin 0.3 * License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html) * Project: http://www.pasella.it/projects/jQuery/barcode * Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com JSR-166 Extension - JEP 266 * License: CC0 * No copyright * Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/ KineticJS, v4.7.1 * License: MIT license (http://www.opensource.org/licenses/mit-license.php) * Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS * Copyright: Eric Rowell org.objectweb.asm Version 9.6 * License: Modified BSD (https://asm.ow2.io/license.html) * Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved. org.osgi.core version 6.0.0 * License: Apache License, 2.0 * Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved. org.glassfish.jersey.server.internal.monitoring.core * License: Apache License, 2.0 * Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. * Copyright 2010-2013 Coda Hale and Yammer, Inc. W3.org documents * License: W3C License * Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ -------------------------------------------------------------------------------- Eclipse Jersey Core Common -------------------------------------------------------------------------------- # Notice for Jersey Core Common module This content is produced and maintained by the Eclipse Jersey project. * https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Google Guava Version 18.0 * License: Apache License, 2.0 * Copyright: (C) 2009 The Guava Authors JSR-166 Extension - JEP 266 * License: Creative Commons 1.0 (CC0) * No copyright * Written by Doug Lea with assistance from members of JCP JSR-166 * Expert Group and released to the public domain, as explained at * http://creativecommons.org/publicdomain/zero/1.0/ # Notice for Jersey This content is produced and maintained by the Eclipse Jersey project. * Project home: https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Angular JS, v1.6.6 * License MIT (http://www.opensource.org/licenses/mit-license.php) * Project: http://angularjs.org * Coyright: (c) 2010-2017 Google, Inc. aopalliance Version 1 * License: all the source code provided by AOP Alliance is Public Domain. * Project: http://aopalliance.sourceforge.net * Copyright: Material in the public domain is not protected by copyright Bean Validation API 3.0.2 * License: Apache License, 2.0 * Project: http://beanvalidation.org/1.1/ * Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors * by the @authors tag. Hibernate Validator CDI, 7.0.5.Final * License: Apache License, 2.0 * Project: https://beanvalidation.org/ * Repackaged in org.glassfish.jersey.server.validation.internal.hibernate Bootstrap v3.3.7 * License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE) * Project: http://getbootstrap.com * Copyright: 2011-2016 Twitter, Inc Google Guava Version 18.0 * License: Apache License, 2.0 * Copyright (C) 2009 The Guava Authors jakarta.inject Version: 1 * License: Apache License, 2.0 * Copyright (C) 2009 The JSR-330 Expert Group Javassist Version 3.29.2-GA * License: Apache License, 2.0 * Project: http://www.javassist.org/ * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. Jackson JAX-RS Providers Version 2.15.3 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers * Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. jQuery v1.12.4 * License: jquery.org/license * Project: jquery.org * Copyright: (c) jQuery Foundation jQuery Barcode plugin 0.3 * License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html) * Project: http://www.pasella.it/projects/jQuery/barcode * Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com JSR-166 Extension - JEP 266 * License: CC0 * No copyright * Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/ KineticJS, v4.7.1 * License: MIT license (http://www.opensource.org/licenses/mit-license.php) * Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS * Copyright: Eric Rowell org.objectweb.asm Version 9.6 * License: Modified BSD (https://asm.ow2.io/license.html) * Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved. org.osgi.core version 6.0.0 * License: Apache License, 2.0 * Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved. org.glassfish.jersey.server.internal.monitoring.core * License: Apache License, 2.0 * Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. * Copyright 2010-2013 Coda Hale and Yammer, Inc. W3.org documents * License: W3C License * Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ -------------------------------------------------------------------------------- Eclipse Jersey Bean Validation -------------------------------------------------------------------------------- # Notice for Jersey Bean Validation module This content is produced and maintained by the Eclipse Jersey project. * https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Hibernate Validator CDI, 7.0.5.Final * License: Apache License, 2.0 * Project: https://beanvalidation.org/ * Repackaged in org.glassfish.jersey.server.validation.internal.hibernate # Notice for Jersey This content is produced and maintained by the Eclipse Jersey project. * Project home: https://projects.eclipse.org/projects/ee4j.jersey ## Trademarks Eclipse Jersey is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jersey ## Third-party Content Angular JS, v1.6.6 * License MIT (http://www.opensource.org/licenses/mit-license.php) * Project: http://angularjs.org * Coyright: (c) 2010-2017 Google, Inc. aopalliance Version 1 * License: all the source code provided by AOP Alliance is Public Domain. * Project: http://aopalliance.sourceforge.net * Copyright: Material in the public domain is not protected by copyright Bean Validation API 3.0.2 * License: Apache License, 2.0 * Project: http://beanvalidation.org/1.1/ * Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors * by the @authors tag. Hibernate Validator CDI, 7.0.5.Final * License: Apache License, 2.0 * Project: https://beanvalidation.org/ * Repackaged in org.glassfish.jersey.server.validation.internal.hibernate Bootstrap v3.3.7 * License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE) * Project: http://getbootstrap.com * Copyright: 2011-2016 Twitter, Inc Google Guava Version 18.0 * License: Apache License, 2.0 * Copyright (C) 2009 The Guava Authors jakarta.inject Version: 1 * License: Apache License, 2.0 * Copyright (C) 2009 The JSR-330 Expert Group Javassist Version 3.29.2-GA * License: Apache License, 2.0 * Project: http://www.javassist.org/ * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. Jackson JAX-RS Providers Version 2.15.3 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers * Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. jQuery v1.12.4 * License: jquery.org/license * Project: jquery.org * Copyright: (c) jQuery Foundation jQuery Barcode plugin 0.3 * License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html) * Project: http://www.pasella.it/projects/jQuery/barcode * Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com JSR-166 Extension - JEP 266 * License: CC0 * No copyright * Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/ KineticJS, v4.7.1 * License: MIT license (http://www.opensource.org/licenses/mit-license.php) * Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS * Copyright: Eric Rowell org.objectweb.asm Version 9.6 * License: Modified BSD (https://asm.ow2.io/license.html) * Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved. org.osgi.core version 6.0.0 * License: Apache License, 2.0 * Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved. org.glassfish.jersey.server.internal.monitoring.core * License: Apache License, 2.0 * Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. * Copyright 2010-2013 Coda Hale and Yammer, Inc. W3.org documents * License: W3C License * Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ -------------------------------------------------------------------------------- Eclipse GlassFish HK2 (aopalliance-repackaged, hk2-api, hk2-locator, hk2-utils) -------------------------------------------------------------------------------- # Notices for Eclipse GlassFish This content is produced and maintained by the Eclipse GlassFish project. * Project home: https://projects.eclipse.org/projects/ee4j.glassfish ## Trademarks Eclipse GlassFish, and GlassFish are trademarks of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/glassfish-ha-api * https://github.com/eclipse-ee4j/glassfish-logging-annotation-processor * https://github.com/eclipse-ee4j/glassfish-shoal * https://github.com/eclipse-ee4j/glassfish-cdi-porting-tck * https://github.com/eclipse-ee4j/glassfish-jsftemplating * https://github.com/eclipse-ee4j/glassfish-hk2-extra * https://github.com/eclipse-ee4j/glassfish-hk2 * https://github.com/eclipse-ee4j/glassfish-fighterfish ## Third-party Content This project leverages the following third party content. None ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. -------------------------------------------------------------------------------- Eclipse Jetty Servlet API (jakarta-servlet-api 5.0.2) -------------------------------------------------------------------------------- # Notices for Eclipse Project for Servlet This content is produced and maintained by the Eclipse Project for Servlet project. * Project home: https://projects.eclipse.org/projects/ee4j.servlet ## Trademarks Eclipse Project for Servlet is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/servlet-api * https://github.com/eclipse/jetty.toolchain ## Third-party Content ## Jakarta The following artifacts are EPL 2.0 + GPLv2 with classpath exception. https://projects.eclipse.org/projects/ee4j.servlet * jakarta.servlet:jakarta.servlet-api ## GlassFish The following artifacts are CDDL + GPLv2 with classpath exception. https://glassfish.dev.java.net/nonav/public/CDDL+GPL.html * org.eclipse.jetty.toolchain:jetty-schemas -------------------------------------------------------------------------------- Jakarta XML Binding API (jakarta.xml.bind-api 3.0.x) -------------------------------------------------------------------------------- [//]: # " Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. " [//]: # " " [//]: # " This program and the accompanying materials are made available under the " [//]: # " terms of the Eclipse Distribution License v. 1.0, which is available at " [//]: # " http://www.eclipse.org/org/documents/edl-v10.php. " [//]: # " " [//]: # " SPDX-License-Identifier: BSD-3-Clause " # Notices for Jakarta XML Binding This content is produced and maintained by the Jakarta XML Binding project. * Project home: https://projects.eclipse.org/projects/ee4j.jaxb ## Trademarks Jakarta XML Binding is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Distribution License v. 1.0 which is available at http://www.eclipse.org/org/documents/edl-v10.php. SPDX-License-Identifier: BSD-3-Clause ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jaxb-api * https://github.com/eclipse-ee4j/jaxb-tck ## Third-party Content This project leverages the following third party content. Apache River (3.0.0) * License: Apache-2.0 AND BSD-3-Clause ASM 7 (n/a) * License: BSD-3-Clause * Project: https://asm.ow2.io/ * Source: https://repository.ow2.org/nexus/#nexus-search;gav~org.ow2.asm~asm-commons~~~~kw,versionexpand JTHarness (5.0) * License: (GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0) * Project: https://wiki.openjdk.java.net/display/CodeTools/JT+Harness * Source: http://hg.openjdk.java.net/code-tools/jtharness/ normalize.css (3.0.2) * License: MIT SigTest (n/a) * License: GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. -------------------------------------------------------------------------------- Jakarta RESTful Web Services API (jakarta.ws.rs-api 3.0.x / 3.1.0) -------------------------------------------------------------------------------- # Notices for Jakarta RESTful Web Services This content is produced and maintained by the **Jakarta RESTful Web Services** project. * Project home: https://projects.eclipse.org/projects/ee4j.jaxrs ## Trademarks **Jakarta RESTful Web Services** is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jaxrs-api ## Third-party Content This project leverages the following third party content. javaee-api (7.0) * License: Apache-2.0 AND W3C JUnit (4.11) * License: Common Public License 1.0 Mockito (2.16.0) * Project: http://site.mockito.org * Source: https://github.com/mockito/mockito/releases/tag/v2.16.0 ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. -------------------------------------------------------------------------------- Jakarta Expression Language API (jakarta.el-api 4.0.0, glassfish jakarta.el 4.0.2) -------------------------------------------------------------------------------- # Notices for Jakarta Expression Language This content is produced and maintained by the Jakarta Expression Language project. * Project home: https://projects.eclipse.org/projects/ee4j.el ## Trademarks Jakarta Expression Language is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/el-ri ## Third-party Content ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. -------------------------------------------------------------------------------- Jakarta Annotations API (jakarta.annotation-api 2.1.1 and 3.0.0) -------------------------------------------------------------------------------- # Notices for Jakarta Annotations This content is produced and maintained by the Jakarta Annotations project. * Project home: https://projects.eclipse.org/projects/ee4j.ca ## Trademarks Jakarta Annotations is a trademark of the Eclipse Foundation. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU General Public License, version 2 with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/common-annotations-api ## Third-party Content ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Notices for Jakarta Annotations This content is produced and maintained by the Jakarta Annotations project. * Project home: https://projects.eclipse.org/projects/ee4j.ca ## Trademarks Jakarta Annotations™ is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Public License v. 2.0 which is available at https://www.eclipse.org/legal/epl-2.0. This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License v. 2.0 are satisfied: GPL-2.0 with Classpath-exception-2.0 which is available at https://openjdk.java.net/legal/gplv2+ce.html. SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0 ## Source Code The project maintains the following source code repositories: * https://github.com/jakartaee/common-annotations-api ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. -------------------------------------------------------------------------------- Jakarta Inject API (jakarta.inject-api 2.0.1) -------------------------------------------------------------------------------- # Notices for Eclipse Jakarta Dependency Injection This content is produced and maintained by the Eclipse Jakarta Dependency Injection project. * Project home: https://projects.eclipse.org/projects/cdi.batch ## Trademarks Jakarta Dependency Injection is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Apache License, Version 2.0 which is available at https://www.apache.org/licenses/LICENSE-2.0. SPDX-License-Identifier: Apache-2.0 ## Source Code The project maintains the following source code repositories: https://github.com/eclipse-ee4j/injection-api https://github.com/eclipse-ee4j/injection-spec https://github.com/eclipse-ee4j/injection-tck ## Third-party Content This project leverages the following third party content. None ## Cryptography None -------------------------------------------------------------------------------- Jakarta Activation (jakarta.activation 2.0.0, 2.0.1, jakarta.activation-api 1.2.1, 2.1.0) -------------------------------------------------------------------------------- # Notices for Eclipse Project for JAF This content is produced and maintained by the Eclipse Project for JAF project. * Project home: https://projects.eclipse.org/projects/ee4j.jaf ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Distribution License v. 1.0, which is available at http://www.eclipse.org/org/documents/edl-v10.php. SPDX-License-Identifier: BSD-3-Clause ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jaf ## Third-party Content This project leverages the following third party content. JUnit (4.12) * License: Eclipse Public License - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Notices for Jakarta Activation This content is produced and maintained by Jakarta Activation project. * Project home: https://projects.eclipse.org/projects/ee4j.jaf ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Distribution License v. 1.0, which is available at http://www.eclipse.org/org/documents/edl-v10.php. SPDX-License-Identifier: BSD-3-Clause ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jaf ## Third-party Content This project leverages the following third party content. JUnit (4.12) * License: Eclipse Public License - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Notices for Jakarta Activation This content is produced and maintained by Jakarta Activation project. * Project home: https://projects.eclipse.org/projects/ee4j.jaf ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the Eclipse Distribution License v. 1.0, which is available at http://www.eclipse.org/org/documents/edl-v10.php. SPDX-License-Identifier: BSD-3-Clause ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse-ee4j/jaf ================================================ FILE: workflow-compiling-service/build.sbt ================================================ // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. ///////////////////////////////////////////////////////////////////////////// // Project Settings ///////////////////////////////////////////////////////////////////////////// name := "workflow-compiling-service" enablePlugins(JavaAppPackaging) // Ship LICENSE-binary, NOTICE-binary, DISCLAIMER, and the licenses/ // directory at the top of the Universal dist zip. // See project/AddMetaInfLicenseFiles.scala. Universal / mappings := AddMetaInfLicenseFiles.distMappings( (Universal / mappings).value, (ThisBuild / baseDirectory).value, baseDirectory.value / "LICENSE-binary", baseDirectory.value / "NOTICE-binary" ) // Enable semanticdb for Scalafix ThisBuild / semanticdbEnabled := true ThisBuild / semanticdbVersion := scalafixSemanticdb.revision // Manage dependency conflicts by always using the latest revision ThisBuild / conflictManager := ConflictManager.latestRevision // Restrict parallel execution of tests to avoid conflicts Global / concurrentRestrictions += Tags.limit(Tags.Test, 1) ///////////////////////////////////////////////////////////////////////////// // Compiler Options ///////////////////////////////////////////////////////////////////////////// // Scala compiler options Compile / scalacOptions ++= Seq( "-Xelide-below", "WARNING", // Turn on optimizations with "WARNING" as the threshold "-feature", // Check feature warnings "-deprecation", // Check deprecation warnings "-Ywarn-unused:imports" // Check for unused imports ) ///////////////////////////////////////////////////////////////////////////// // Version Variables ///////////////////////////////////////////////////////////////////////////// val dropwizardVersion = "4.0.7" val mockitoVersion = "5.4.0" val assertjVersion = "3.24.2" ///////////////////////////////////////////////////////////////////////////// // Test-related Dependencies ///////////////////////////////////////////////////////////////////////////// libraryDependencies ++= Seq( "org.scalamock" %% "scalamock" % "5.2.0" % Test, // ScalaMock "org.scalatest" %% "scalatest" % "3.2.17" % Test, // ScalaTest "io.dropwizard" % "dropwizard-testing" % dropwizardVersion % Test, // Dropwizard Testing "org.mockito" % "mockito-core" % mockitoVersion % Test, // Mockito for mocking "org.assertj" % "assertj-core" % assertjVersion % Test, // AssertJ for assertions "com.novocode" % "junit-interface" % "0.11" % Test // SBT interface for JUnit ) ///////////////////////////////////////////////////////////////////////////// // Dependencies ///////////////////////////////////////////////////////////////////////////// // Core Dependencies libraryDependencies ++= Seq( "io.dropwizard" % "dropwizard-core" % dropwizardVersion, "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.18.6" ) ================================================ FILE: workflow-compiling-service/project/build.properties ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. sbt.version=1.12.9 ================================================ FILE: workflow-compiling-service/src/main/resources/workflow-compiling-service-config.yaml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. server: applicationConnectors: - type: http port: 9090 adminConnectors: [] requestLog: type: classic appenders: [] logging: level: ${TEXERA_SERVICE_LOG_LEVEL:-INFO} loggers: "io.dropwizard": ${TEXERA_SERVICE_LOG_LEVEL:-INFO} appenders: - type: console - type: file currentLogFilename: log/workflow-compiling-service.log threshold: ALL queueSize: 512 discardingThreshold: 0 archive: true archivedLogFilenamePattern: log/workflow-compiling-service-%d{yyyy-MM-dd}.log.gz archivedFileCount: 7 bufferSize: 8KiB immediateFlush: true ================================================ FILE: workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/WorkflowCompiler.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.compiler import com.google.protobuf.timestamp.Timestamp import com.typesafe.scalalogging.{LazyLogging, Logger} import org.apache.texera.amber.compiler.WorkflowCompiler.{ collectOutputSchemaFromPhysicalPlan, convertErrorListToWorkflowFatalErrorMap } import org.apache.texera.amber.compiler.model.{LogicalPlan, LogicalPlanPojo} import org.apache.texera.amber.core.tuple.Schema import org.apache.texera.amber.core.virtualidentity.OperatorIdentity import org.apache.texera.amber.core.workflow.{ PhysicalLink, PhysicalPlan, PortIdentity, WorkflowContext } import org.apache.texera.amber.core.workflowruntimestate.FatalErrorType.COMPILATION_ERROR import org.apache.texera.amber.core.workflowruntimestate.WorkflowFatalError import java.time.Instant import scala.collection.mutable import scala.collection.mutable.ArrayBuffer import scala.jdk.CollectionConverters.IteratorHasAsScala object WorkflowCompiler { // util function for extracting the error causes private def getStackTraceWithAllCauses(err: Throwable, topLevel: Boolean = true): String = { val header = if (topLevel) { "Stack trace for developers: \n\n" } else { "\n\nCaused by:\n" } val message = header + err.toString + "\n" + err.getStackTrace.mkString("\n") if (err.getCause != null) { message + getStackTraceWithAllCauses(err.getCause, topLevel = false) } else { message } } // util function for convert the error list to error map, and report the error in log private def convertErrorListToWorkflowFatalErrorMap( logger: Logger, errorList: List[(OperatorIdentity, Throwable)] ): Map[OperatorIdentity, WorkflowFatalError] = { val opIdToError = mutable.Map[OperatorIdentity, WorkflowFatalError]() errorList.map { case (opId, err) => // map each error to WorkflowFatalError, and report them in the log logger.error(s"Error occurred in logical plan compilation for opId: $opId", err) opIdToError += (opId -> WorkflowFatalError( COMPILATION_ERROR, Timestamp(Instant.now), err.toString, getStackTraceWithAllCauses(err), opId.id )) } opIdToError.toMap } private def collectOutputSchemaFromPhysicalPlan( physicalPlan: PhysicalPlan, errorList: ArrayBuffer[(OperatorIdentity, Throwable)] ): Map[OperatorIdentity, Map[PortIdentity, Option[Schema]]] = { // Collect output schemas per physical operator val physicalOutputSchemas = physicalPlan.operators.map { physicalOp => val portSchemas = physicalOp.outputPorts.values .filterNot(_._1.id.internal) .map { case (port, _, schema) => schema match { case Left(err) => errorList.append((physicalOp.id.logicalOpId, err)) port.id -> None case Right(validSchema) => port.id -> Some(validSchema) } } .toMap physicalOp.id -> portSchemas } // Group by logical operator ID and merge port schemas physicalOutputSchemas .groupBy(_._1.logicalOpId) .view .mapValues { list => list.flatMap(_._2).toMap } .toMap } } case class WorkflowCompilationResult( physicalPlan: Option[PhysicalPlan], // if physical plan is none, the compilation is failed operatorIdToOutputSchemas: Map[OperatorIdentity, Map[PortIdentity, Option[Schema]]], operatorIdToError: Map[OperatorIdentity, WorkflowFatalError] ) class WorkflowCompiler( context: WorkflowContext ) extends LazyLogging { // function to expand logical plan to physical plan private def expandLogicalPlan( logicalPlan: LogicalPlan, errorList: Option[ArrayBuffer[(OperatorIdentity, Throwable)]] ): PhysicalPlan = { var physicalPlan = PhysicalPlan(operators = Set.empty, links = Set.empty) logicalPlan.getTopologicalOpIds.asScala.foreach { logicalOpId => val logicalOp = logicalPlan.getOperator(logicalOpId) val allUpstreamLinks = logicalPlan.getUpstreamLinks(logicalOp.operatorIdentifier) try { val subPlan = logicalOp.getPhysicalPlan(context.workflowId, context.executionId) subPlan .topologicalIterator() .map(subPlan.getOperator) .foreach { physicalOp => val externalLinks = allUpstreamLinks .filter(link => physicalOp.inputPorts.contains(link.toPortId)) .flatMap { link => physicalPlan .getPhysicalOpsOfLogicalOp(link.fromOpId) .find(_.outputPorts.contains(link.fromPortId)) .map(fromOp => PhysicalLink(fromOp.id, link.fromPortId, physicalOp.id, link.toPortId) ) } val internalLinks = subPlan.getUpstreamPhysicalLinks(physicalOp.id) // Add the operator to the physical plan physicalPlan = physicalPlan.addOperator(physicalOp.propagateSchema()) // Add all the links to the physical plan physicalPlan = (externalLinks ++ internalLinks).foldLeft(physicalPlan) { (plan, link) => plan.addLink(link) } // **Check for Python-based operator errors during code generation** if (physicalOp.isPythonBased) { val code = physicalOp.getCode val exceptionPattern = """#EXCEPTION DURING CODE GENERATION:\s*(.*)""".r exceptionPattern.findFirstMatchIn(code).foreach { matchResult => val errorMessage = matchResult.group(1).trim val error = new RuntimeException(s"Operator is not configured properly: $errorMessage") errorList match { case Some(list) => list.append((logicalOpId, error)) // Store error and continue case None => throw error // Throw immediately if no error list is provided } } } } } catch { case e: Throwable => errorList match { case Some(list) => list.append((logicalOpId, e)) // Store error case None => throw e // Throw if no list is provided } } } physicalPlan } /** * Compile a workflow to physical plan, along with the schema propagation result and error(if any) * * @param logicalPlanPojo the pojo parsed from workflow str provided by user * @return WorkflowCompilationResult, containing the physical plan, input schemas per op and error per op */ def compile( logicalPlanPojo: LogicalPlanPojo ): WorkflowCompilationResult = { val errorList = new ArrayBuffer[(OperatorIdentity, Throwable)]() var opIdToOutputSchema: Map[OperatorIdentity, Map[PortIdentity, Option[Schema]]] = Map() // 1. convert the pojo to logical plan val logicalPlan: LogicalPlan = LogicalPlan(logicalPlanPojo) // 2. resolve the file name in each scan source operator logicalPlan.resolveScanSourceOpFileName(Some(errorList)) // 3. expand the logical plan to the physical plan val physicalPlan = expandLogicalPlan(logicalPlan, Some(errorList)) // 4. collect the output schema for each logical op // even if error is encountered when logical => physical, we still want to get the input schemas for rest no-error operators opIdToOutputSchema = collectOutputSchemaFromPhysicalPlan(physicalPlan, errorList) WorkflowCompilationResult( physicalPlan = if (errorList.nonEmpty) None else Some(physicalPlan), operatorIdToOutputSchemas = opIdToOutputSchema, // map each error from OpId to WorkflowFatalError, and report them via logger operatorIdToError = convertErrorListToWorkflowFatalErrorMap(logger, errorList.toList) ) } } ================================================ FILE: workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/model/LogicalLink.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.compiler.model import com.fasterxml.jackson.annotation.{JsonCreator, JsonProperty} import org.apache.texera.amber.core.virtualidentity.OperatorIdentity import org.apache.texera.amber.core.workflow.PortIdentity case class LogicalLink( @JsonProperty("fromOpId") fromOpId: OperatorIdentity, fromPortId: PortIdentity, @JsonProperty("toOpId") toOpId: OperatorIdentity, toPortId: PortIdentity ) { @JsonCreator def this( @JsonProperty("fromOpId") fromOpId: String, fromPortId: PortIdentity, @JsonProperty("toOpId") toOpId: String, toPortId: PortIdentity ) = { this(OperatorIdentity(fromOpId), fromPortId, OperatorIdentity(toOpId), toPortId) } } ================================================ FILE: workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/model/LogicalPlan.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.compiler.model import com.typesafe.scalalogging.LazyLogging import org.apache.texera.amber.core.storage.FileResolver import org.apache.texera.amber.core.virtualidentity.OperatorIdentity import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.LogicalOp import org.apache.texera.amber.operator.source.scan.ScanSourceOpDesc import org.jgrapht.graph.DirectedAcyclicGraph import org.jgrapht.util.SupplierUtil import java.util import scala.collection.mutable.ArrayBuffer import scala.util.{Failure, Success, Try} object LogicalPlan { private def toJgraphtDAG( operatorList: List[LogicalOp], links: List[LogicalLink] ): DirectedAcyclicGraph[OperatorIdentity, LogicalLink] = { val workflowDag = new DirectedAcyclicGraph[OperatorIdentity, LogicalLink]( null, // vertexSupplier SupplierUtil.createSupplier(classOf[LogicalLink]), // edgeSupplier false, // weighted true // allowMultipleEdges ) operatorList.foreach(op => workflowDag.addVertex(op.operatorIdentifier)) links.foreach(l => workflowDag.addEdge( l.fromOpId, l.toOpId, l ) ) workflowDag } def apply( pojo: LogicalPlanPojo ): LogicalPlan = { LogicalPlan(pojo.operators, pojo.links) } } case class LogicalPlan( operators: List[LogicalOp], links: List[LogicalLink] ) extends LazyLogging { private lazy val operatorMap: Map[OperatorIdentity, LogicalOp] = operators.map(op => (op.operatorIdentifier, op)).toMap private lazy val jgraphtDag: DirectedAcyclicGraph[OperatorIdentity, LogicalLink] = LogicalPlan.toJgraphtDAG(operators, links) def getTopologicalOpIds: util.Iterator[OperatorIdentity] = jgraphtDag.iterator() def getOperator(opId: String): LogicalOp = operatorMap(OperatorIdentity(opId)) def getOperator(opId: OperatorIdentity): LogicalOp = operatorMap(opId) def addOperator(op: LogicalOp): LogicalPlan = { // TODO: fix schema for the new operator this.copy(operators :+ op, links) } def addLink( fromOpId: OperatorIdentity, fromPortId: PortIdentity, toOpId: OperatorIdentity, toPortId: PortIdentity ): LogicalPlan = { val newLink = LogicalLink( fromOpId, fromPortId, toOpId, toPortId ) val newLinks = links :+ newLink this.copy(operators, newLinks) } def getUpstreamLinks(opId: OperatorIdentity): List[LogicalLink] = { links.filter(l => l.toOpId == opId) } /** * Resolve all user-given filename for the scan source operators to URIs, and call op.setFileUri to set the URi * @param errorList if given, put errors during resolving to it */ def resolveScanSourceOpFileName( errorList: Option[ArrayBuffer[(OperatorIdentity, Throwable)]] ): Unit = { operators.foreach { case operator @ (scanOp: ScanSourceOpDesc) => Try { // Resolve file path for ScanSourceOpDesc val fileName = scanOp.fileName.getOrElse(throw new RuntimeException("no input file name")) val fileUri = FileResolver.resolve(fileName) // Convert to URI // Set the URI in the ScanSourceOpDesc scanOp.setResolvedFileName(fileUri) } match { case Success(_) => // Successfully resolved and set the file URI case Failure(err) => logger.error("Error resolving file path for ScanSourceOpDesc", err) errorList.foreach(_.append((operator.operatorIdentifier, err))) } case _ => // Skip non-ScanSourceOpDesc operators } } } ================================================ FILE: workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/model/LogicalPlanPojo.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.amber.compiler.model import org.apache.texera.amber.operator.LogicalOp case class LogicalPlanPojo( operators: List[LogicalOp], links: List[LogicalLink], opsToViewResult: List[String], opsToReuseResult: List[String] ) ================================================ FILE: workflow-compiling-service/src/main/scala/org/apache/texera/service/WorkflowCompilingService.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service import com.fasterxml.jackson.module.scala.DefaultScalaModule import io.dropwizard.configuration.{EnvironmentVariableSubstitutor, SubstitutingSourceProvider} import io.dropwizard.core.Application import io.dropwizard.core.setup.{Bootstrap, Environment} import org.apache.texera.amber.config.StorageConfig import org.apache.texera.amber.util.ObjectMapperUtils import org.apache.texera.dao.SqlServer import org.apache.texera.service.resource.{HealthCheckResource, WorkflowCompilationResource} import org.eclipse.jetty.servlet.FilterHolder import java.nio.file.Path class WorkflowCompilingService extends Application[WorkflowCompilingServiceConfiguration] { override def initialize(bootstrap: Bootstrap[WorkflowCompilingServiceConfiguration]): Unit = { // enable environment variable substitution in YAML config bootstrap.setConfigurationSourceProvider( new SubstitutingSourceProvider( bootstrap.getConfigurationSourceProvider, new EnvironmentVariableSubstitutor(false) ) ) // register scala module to dropwizard default object mapper bootstrap.getObjectMapper.registerModule(DefaultScalaModule) } override def run( configuration: WorkflowCompilingServiceConfiguration, environment: Environment ): Unit = { ObjectMapperUtils.warmupObjectMapperForOperatorsSerde() // serve backend at /api environment.jersey.setUrlPattern("/api/*") SqlServer.initConnection( StorageConfig.jdbcUrl, StorageConfig.jdbcUsername, StorageConfig.jdbcPassword ) environment.jersey.register(classOf[HealthCheckResource]) // register the compilation endpoint environment.jersey.register(classOf[WorkflowCompilationResource]) // Route request logs through SLF4J, controlled by TEXERA_SERVICE_LOG_LEVEL val requestLogger = org.slf4j.LoggerFactory.getLogger("org.eclipse.jetty.server.RequestLog") environment.getApplicationContext.addFilter( new FilterHolder(new jakarta.servlet.Filter { override def doFilter( request: jakarta.servlet.ServletRequest, response: jakarta.servlet.ServletResponse, chain: jakarta.servlet.FilterChain ): Unit = { chain.doFilter(request, response) if (requestLogger.isInfoEnabled) { val req = request.asInstanceOf[jakarta.servlet.http.HttpServletRequest] val resp = response.asInstanceOf[jakarta.servlet.http.HttpServletResponse] requestLogger.info( s"""${req.getRemoteAddr} - "${req.getMethod} ${req.getRequestURI} ${req.getProtocol}" ${resp.getStatus}""" ) } } }), "/*", java.util.EnumSet.allOf(classOf[jakarta.servlet.DispatcherType]) ) } } object WorkflowCompilingService { def main(args: Array[String]): Unit = { // set the configuration file's path val configFilePath = Path .of(sys.env.getOrElse("TEXERA_HOME", ".")) .resolve("workflow-compiling-service") .resolve("src") .resolve("main") .resolve("resources") .resolve("workflow-compiling-service-config.yaml") .toAbsolutePath .toString // Start the Dropwizard application new WorkflowCompilingService().run("server", configFilePath) } } ================================================ FILE: workflow-compiling-service/src/main/scala/org/apache/texera/service/WorkflowCompilingServiceConfiguration.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service import io.dropwizard.core.Configuration class WorkflowCompilingServiceConfiguration extends Configuration {} ================================================ FILE: workflow-compiling-service/src/main/scala/org/apache/texera/service/resource/HealthCheckResource.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.resource import jakarta.ws.rs.core.MediaType import jakarta.ws.rs.{GET, Path, Produces} @Path("/healthcheck") @Produces(Array(MediaType.APPLICATION_JSON)) class HealthCheckResource { @GET def healthCheck: Map[String, String] = Map("status" -> "ok") } ================================================ FILE: workflow-compiling-service/src/main/scala/org/apache/texera/service/resource/WorkflowCompilationResource.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.resource import com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo} import com.typesafe.scalalogging.LazyLogging import jakarta.annotation.security.RolesAllowed import jakarta.ws.rs.core.MediaType import jakarta.ws.rs.{Consumes, POST, Path, Produces} import org.apache.texera.amber.compiler.WorkflowCompiler import org.apache.texera.amber.compiler.model.LogicalPlanPojo import org.apache.texera.amber.core.tuple.Attribute import org.apache.texera.amber.core.virtualidentity.WorkflowIdentity import org.apache.texera.amber.core.workflow.{PhysicalPlan, WorkflowContext} import org.apache.texera.amber.core.workflowruntimestate.WorkflowFatalError import org.apache.texera.amber.util.serde.PortIdentityKeySerializer @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type" ) @JsonSubTypes( Array( new JsonSubTypes.Type(value = classOf[WorkflowCompilationSuccess], name = "success"), new JsonSubTypes.Type(value = classOf[WorkflowCompilationFailure], name = "failure") ) ) trait WorkflowCompilationResponse case class WorkflowCompilationSuccess( physicalPlan: PhysicalPlan, operatorOutputSchemas: Map[String, Map[String, Option[List[Attribute]]]] ) extends WorkflowCompilationResponse case class WorkflowCompilationFailure( operatorErrors: Map[String, WorkflowFatalError], operatorOutputSchemas: Map[String, Map[String, Option[List[Attribute]]]] ) extends WorkflowCompilationResponse @Consumes(Array(MediaType.APPLICATION_JSON)) @Produces(Array(MediaType.APPLICATION_JSON)) @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/compile") class WorkflowCompilationResource extends LazyLogging { @POST @Path("") def compileWorkflow( logicalPlanPojo: LogicalPlanPojo ): WorkflowCompilationResponse = { // a placeholder workflow context, as compiling a workflow doesn't require a wid from the frontend val context = new WorkflowContext(workflowId = WorkflowIdentity(0)) // Compile the pojo using WorkflowCompiler val compilationResult = new WorkflowCompiler(context).compile(logicalPlanPojo) val operatorOutputSchemas = compilationResult.operatorIdToOutputSchemas.map { case (operatorIdentity, schemas) => val opId = operatorIdentity.id val portIdAndAttributes = schemas.map { case (portId, schemaOption) => { if (schemaOption.isEmpty) { (PortIdentityKeySerializer.portIdToString(portId), None) } else { ( PortIdentityKeySerializer.portIdToString(portId), Some(schemaOption.get.attributes) ) } } } (opId, portIdAndAttributes) } // Handle success case: No errors in the compilation result if (compilationResult.operatorIdToError.isEmpty && compilationResult.physicalPlan.nonEmpty) { WorkflowCompilationSuccess( physicalPlan = compilationResult.physicalPlan.get, operatorOutputSchemas = operatorOutputSchemas ) } // Handle failure case: Errors found during compilation else { WorkflowCompilationFailure( operatorErrors = compilationResult.operatorIdToError.map { case (operatorIdentity, error) => (operatorIdentity.id, error) }, operatorOutputSchemas = operatorOutputSchemas ) } } } ================================================ FILE: workflow-compiling-service/src/test/resources/country_sales_small.csv ================================================ Region,Country,Item Type,Sales Channel,Order Priority,Order Date,Order ID,Ship Date,Units Sold,Unit Price,Unit Cost,Total Revenue,Total Cost,Total Profit Australia and Oceania,Tuvalu,Baby Food,Offline,H,5/28/2010,669165933,6/27/2010,9925,255.28,159.42,2533654.00,1582243.50,951410.50 Central America and the Caribbean,Grenada,Cereal,Online,C,8/22/2012,963881480,9/15/2012,2804,205.70,117.11,576782.80,328376.44,248406.36 Europe,Russia,Office Supplies,Offline,L,5/2/2014,341417157,5/8/2014,1779,651.21,524.96,1158502.59,933903.84,224598.75 Sub-Saharan Africa,Sao Tome and Principe,Fruits,Online,C,6/20/2014,514321792,7/5/2014,8102,9.33,6.92,75591.66,56065.84,19525.82 Sub-Saharan Africa,Rwanda,Office Supplies,Offline,L,2/1/2013,115456712,2/6/2013,5062,651.21,524.96,3296425.02,2657347.52,639077.50 Australia and Oceania,Solomon Islands,Baby Food,Online,C,2/4/2015,547995746,2/21/2015,2974,255.28,159.42,759202.72,474115.08,285087.64 Sub-Saharan Africa,Angola,Household,Offline,M,4/23/2011,135425221,4/27/2011,4187,668.27,502.54,2798046.49,2104134.98,693911.51 Sub-Saharan Africa,Burkina Faso,Vegetables,Online,H,7/17/2012,871543967,7/27/2012,8082,154.06,90.93,1245112.92,734896.26,510216.66 Sub-Saharan Africa,Republic of the Congo,Personal Care,Offline,M,7/14/2015,770463311,8/25/2015,6070,81.73,56.67,496101.10,343986.90,152114.20 Sub-Saharan Africa,Senegal,Cereal,Online,H,4/18/2014,616607081,5/30/2014,6593,205.70,117.11,1356180.10,772106.23,584073.87 Asia,Kyrgyzstan,Vegetables,Online,H,6/24/2011,814711606,7/12/2011,124,154.06,90.93,19103.44,11275.32,7828.12 Sub-Saharan Africa,Cape Verde,Clothes,Offline,H,8/2/2014,939825713,8/19/2014,4168,109.28,35.84,455479.04,149381.12,306097.92 Asia,Bangladesh,Clothes,Online,L,1/13/2017,187310731,3/1/2017,8263,109.28,35.84,902980.64,296145.92,606834.72 Central America and the Caribbean,Honduras,Household,Offline,H,2/8/2017,522840487,2/13/2017,8974,668.27,502.54,5997054.98,4509793.96,1487261.02 Asia,Mongolia,Personal Care,Offline,C,2/19/2014,832401311,2/23/2014,4901,81.73,56.67,400558.73,277739.67,122819.06 Europe,Bulgaria,Clothes,Online,M,4/23/2012,972292029,6/3/2012,1673,109.28,35.84,182825.44,59960.32,122865.12 Asia,Sri Lanka,Cosmetics,Offline,M,11/19/2016,419123971,12/18/2016,6952,437.20,263.33,3039414.40,1830670.16,1208744.24 Sub-Saharan Africa,Cameroon,Beverages,Offline,C,4/1/2015,519820964,4/18/2015,5430,47.45,31.79,257653.50,172619.70,85033.80 Asia,Turkmenistan,Household,Offline,L,12/30/2010,441619336,1/20/2011,3830,668.27,502.54,2559474.10,1924728.20,634745.90 Australia and Oceania,East Timor,Meat,Online,L,7/31/2012,322067916,9/11/2012,5908,421.89,364.69,2492526.12,2154588.52,337937.60 Europe,Norway,Baby Food,Online,L,5/14/2014,819028031,6/28/2014,7450,255.28,159.42,1901836.00,1187679.00,714157.00 Europe,Portugal,Baby Food,Online,H,7/31/2015,860673511,9/3/2015,1273,255.28,159.42,324971.44,202941.66,122029.78 Central America and the Caribbean,Honduras,Snacks,Online,L,6/30/2016,795490682,7/26/2016,2225,152.58,97.44,339490.50,216804.00,122686.50 Australia and Oceania,New Zealand,Fruits,Online,H,9/8/2014,142278373,10/4/2014,2187,9.33,6.92,20404.71,15134.04,5270.67 Europe,Moldova ,Personal Care,Online,L,5/7/2016,740147912,5/10/2016,5070,81.73,56.67,414371.10,287316.90,127054.20 Europe,France,Cosmetics,Online,H,5/22/2017,898523128,6/5/2017,1815,437.20,263.33,793518.00,477943.95,315574.05 Australia and Oceania,Kiribati,Fruits,Online,M,10/13/2014,347140347,11/10/2014,5398,9.33,6.92,50363.34,37354.16,13009.18 Sub-Saharan Africa,Mali,Fruits,Online,L,5/7/2010,686048400,5/10/2010,5822,9.33,6.92,54319.26,40288.24,14031.02 Europe,Norway,Beverages,Offline,C,7/18/2014,435608613,7/30/2014,5124,47.45,31.79,243133.80,162891.96,80241.84 Sub-Saharan Africa,The Gambia,Household,Offline,L,5/26/2012,886494815,6/9/2012,2370,668.27,502.54,1583799.90,1191019.80,392780.10 Europe,Switzerland,Cosmetics,Offline,M,9/17/2012,249693334,10/20/2012,8661,437.20,263.33,3786589.20,2280701.13,1505888.07 Sub-Saharan Africa,South Sudan,Personal Care,Offline,C,12/29/2013,406502997,1/28/2014,2125,81.73,56.67,173676.25,120423.75,53252.50 Australia and Oceania,Australia,Office Supplies,Online,C,10/27/2015,158535134,11/25/2015,2924,651.21,524.96,1904138.04,1534983.04,369155.00 Asia,Myanmar,Household,Offline,H,1/16/2015,177713572,3/1/2015,8250,668.27,502.54,5513227.50,4145955.00,1367272.50 Sub-Saharan Africa,Djibouti,Snacks,Online,M,2/25/2017,756274640,2/25/2017,7327,152.58,97.44,1117953.66,713942.88,404010.78 Central America and the Caribbean,Costa Rica,Personal Care,Offline,L,5/8/2017,456767165,5/21/2017,6409,81.73,56.67,523807.57,363198.03,160609.54 Middle East and North Africa,Syria,Fruits,Online,L,11/22/2011,162052476,12/3/2011,3784,9.33,6.92,35304.72,26185.28,9119.44 Sub-Saharan Africa,The Gambia,Meat,Online,M,1/14/2017,825304400,1/23/2017,4767,421.89,364.69,2011149.63,1738477.23,272672.40 Asia,Brunei,Office Supplies,Online,L,4/1/2012,320009267,5/8/2012,6708,651.21,524.96,4368316.68,3521431.68,846885.00 Europe,Bulgaria,Office Supplies,Online,M,2/16/2012,189965903,2/28/2012,3987,651.21,524.96,2596374.27,2093015.52,503358.75 Sub-Saharan Africa,Niger,Personal Care,Online,H,3/11/2017,699285638,3/28/2017,3015,81.73,56.67,246415.95,170860.05,75555.90 Middle East and North Africa,Azerbaijan,Cosmetics,Online,M,2/6/2010,382392299,2/25/2010,7234,437.20,263.33,3162704.80,1904929.22,1257775.58 Sub-Saharan Africa,The Gambia,Cereal,Offline,H,6/7/2012,994022214,6/8/2012,2117,205.70,117.11,435466.90,247921.87,187545.03 Europe,Slovakia,Vegetables,Online,H,10/6/2012,759224212,11/10/2012,171,154.06,90.93,26344.26,15549.03,10795.23 Asia,Myanmar,Clothes,Online,H,11/14/2015,223359620,11/18/2015,5930,109.28,35.84,648030.40,212531.20,435499.20 Sub-Saharan Africa,Comoros,Cereal,Offline,H,3/29/2016,902102267,4/29/2016,962,205.70,117.11,197883.40,112659.82,85223.58 Europe,Iceland,Cosmetics,Online,C,12/31/2016,331438481,12/31/2016,8867,437.20,263.33,3876652.40,2334947.11,1541705.29 Europe,Switzerland,Personal Care,Online,M,12/23/2010,617667090,1/31/2011,273,81.73,56.67,22312.29,15470.91,6841.38 Europe,Macedonia,Clothes,Offline,C,10/14/2014,787399423,11/14/2014,7842,109.28,35.84,856973.76,281057.28,575916.48 Sub-Saharan Africa,Mauritania,Office Supplies,Offline,C,1/11/2012,837559306,1/13/2012,1266,651.21,524.96,824431.86,664599.36,159832.50 Europe,Albania,Clothes,Online,C,2/2/2010,385383069,3/18/2010,2269,109.28,35.84,247956.32,81320.96,166635.36 Sub-Saharan Africa,Lesotho,Fruits,Online,L,8/18/2013,918419539,9/18/2013,9606,9.33,6.92,89623.98,66473.52,23150.46 Middle East and North Africa,Saudi Arabia,Cereal,Online,M,3/25/2013,844530045,3/28/2013,4063,205.70,117.11,835759.10,475817.93,359941.17 Sub-Saharan Africa,Sierra Leone,Office Supplies,Offline,M,11/26/2011,441888415,1/7/2012,3457,651.21,524.96,2251232.97,1814786.72,436446.25 Sub-Saharan Africa,Sao Tome and Principe,Fruits,Offline,H,9/17/2013,508980977,10/24/2013,7637,9.33,6.92,71253.21,52848.04,18405.17 Sub-Saharan Africa,Cote d'Ivoire,Clothes,Online,C,6/8/2012,114606559,6/27/2012,3482,109.28,35.84,380512.96,124794.88,255718.08 Australia and Oceania,Fiji,Clothes,Offline,C,6/30/2010,647876489,8/1/2010,9905,109.28,35.84,1082418.40,354995.20,727423.20 Europe,Austria,Cosmetics,Offline,H,2/23/2015,868214595,3/2/2015,2847,437.20,263.33,1244708.40,749700.51,495007.89 Europe,United Kingdom,Household,Online,L,1/5/2012,955357205,2/14/2012,282,668.27,502.54,188452.14,141716.28,46735.86 Sub-Saharan Africa,Djibouti,Cosmetics,Offline,H,4/7/2014,259353148,4/19/2014,7215,437.20,263.33,3154398.00,1899925.95,1254472.05 Australia and Oceania,Australia,Cereal,Offline,H,6/9/2013,450563752,7/2/2013,682,205.70,117.11,140287.40,79869.02,60418.38 Europe,San Marino,Baby Food,Online,L,6/26/2013,569662845,7/1/2013,4750,255.28,159.42,1212580.00,757245.00,455335.00 Sub-Saharan Africa,Cameroon,Office Supplies,Online,M,11/7/2011,177636754,11/15/2011,5518,651.21,524.96,3593376.78,2896729.28,696647.50 Middle East and North Africa,Libya,Clothes,Offline,H,10/30/2010,705784308,11/17/2010,6116,109.28,35.84,668356.48,219197.44,449159.04 Central America and the Caribbean,Haiti,Cosmetics,Offline,H,10/13/2013,505716836,11/16/2013,1705,437.20,263.33,745426.00,448977.65,296448.35 Sub-Saharan Africa,Rwanda,Cosmetics,Offline,H,10/11/2013,699358165,11/25/2013,4477,437.20,263.33,1957344.40,1178928.41,778415.99 Sub-Saharan Africa,Gabon,Personal Care,Offline,L,7/8/2012,228944623,7/9/2012,8656,81.73,56.67,707454.88,490535.52,216919.36 Central America and the Caribbean,Belize,Clothes,Offline,M,7/25/2016,807025039,9/7/2016,5498,109.28,35.84,600821.44,197048.32,403773.12 Europe,Lithuania,Office Supplies,Offline,H,10/24/2010,166460740,11/17/2010,8287,651.21,524.96,5396577.27,4350343.52,1046233.75 Sub-Saharan Africa,Madagascar,Clothes,Offline,L,4/25/2015,610425555,5/28/2015,7342,109.28,35.84,802333.76,263137.28,539196.48 Asia,Turkmenistan,Office Supplies,Online,M,4/23/2013,462405812,5/20/2013,5010,651.21,524.96,3262562.10,2630049.60,632512.50 Middle East and North Africa,Libya,Fruits,Online,L,8/14/2015,816200339,9/30/2015,673,9.33,6.92,6279.09,4657.16,1621.93 Sub-Saharan Africa,Democratic Republic of the Congo,Beverages,Online,C,5/26/2011,585920464,7/15/2011,5741,47.45,31.79,272410.45,182506.39,89904.06 Sub-Saharan Africa,Djibouti,Cereal,Online,H,5/20/2017,555990016,6/17/2017,8656,205.70,117.11,1780539.20,1013704.16,766835.04 Middle East and North Africa,Pakistan,Cosmetics,Offline,L,7/5/2013,231145322,8/16/2013,9892,437.20,263.33,4324782.40,2604860.36,1719922.04 North America,Mexico,Household,Offline,C,11/6/2014,986435210,12/12/2014,6954,668.27,502.54,4647149.58,3494663.16,1152486.42 Australia and Oceania,Federated States of Micronesia,Beverages,Online,C,10/28/2014,217221009,11/15/2014,9379,47.45,31.79,445033.55,298158.41,146875.14 Asia,Laos,Vegetables,Offline,C,9/15/2011,789176547,10/23/2011,3732,154.06,90.93,574951.92,339350.76,235601.16 Europe,Monaco,Baby Food,Offline,H,5/29/2012,688288152,6/2/2012,8614,255.28,159.42,2198981.92,1373243.88,825738.04 Australia and Oceania,Samoa ,Cosmetics,Online,H,7/20/2013,670854651,8/7/2013,9654,437.20,263.33,4220728.80,2542187.82,1678540.98 Europe,Spain,Household,Offline,L,10/21/2012,213487374,11/30/2012,4513,668.27,502.54,3015902.51,2267963.02,747939.49 Middle East and North Africa,Lebanon,Clothes,Online,L,9/18/2012,663110148,10/8/2012,7884,109.28,35.84,861563.52,282562.56,579000.96 Middle East and North Africa,Iran,Cosmetics,Online,H,11/15/2016,286959302,12/8/2016,6489,437.20,263.33,2836990.80,1708748.37,1128242.43 Sub-Saharan Africa,Zambia,Snacks,Online,L,1/4/2011,122583663,1/5/2011,4085,152.58,97.44,623289.30,398042.40,225246.90 Sub-Saharan Africa,Kenya,Vegetables,Online,L,3/18/2012,827844560,4/7/2012,6457,154.06,90.93,994765.42,587135.01,407630.41 North America,Mexico,Personal Care,Offline,L,2/17/2012,430915820,3/20/2012,6422,81.73,56.67,524870.06,363934.74,160935.32 Sub-Saharan Africa,Sao Tome and Principe,Beverages,Offline,C,1/16/2011,180283772,1/21/2011,8829,47.45,31.79,418936.05,280673.91,138262.14 Sub-Saharan Africa,The Gambia,Baby Food,Offline,M,2/3/2014,494747245,3/20/2014,5559,255.28,159.42,1419101.52,886215.78,532885.74 Middle East and North Africa,Kuwait,Fruits,Online,M,4/30/2012,513417565,5/18/2012,522,9.33,6.92,4870.26,3612.24,1258.02 Europe,Slovenia,Beverages,Offline,C,10/23/2016,345718562,11/25/2016,4660,47.45,31.79,221117.00,148141.40,72975.60 Sub-Saharan Africa,Sierra Leone,Office Supplies,Offline,H,12/6/2016,621386563,12/14/2016,948,651.21,524.96,617347.08,497662.08,119685.00 Australia and Oceania,Australia,Beverages,Offline,H,7/7/2014,240470397,7/11/2014,9389,47.45,31.79,445508.05,298476.31,147031.74 Middle East and North Africa,Azerbaijan,Office Supplies,Online,M,6/13/2012,423331391,7/24/2012,2021,651.21,524.96,1316095.41,1060944.16,255151.25 Europe,Romania,Cosmetics,Online,H,11/26/2010,660643374,12/25/2010,7910,437.20,263.33,3458252.00,2082940.30,1375311.70 Central America and the Caribbean,Nicaragua,Beverages,Offline,C,2/8/2011,963392674,3/21/2011,8156,47.45,31.79,387002.20,259279.24,127722.96 Sub-Saharan Africa,Mali,Clothes,Online,M,7/26/2011,512878119,9/3/2011,888,109.28,35.84,97040.64,31825.92,65214.72 Asia,Malaysia,Fruits,Offline,L,11/11/2011,810711038,12/28/2011,6267,9.33,6.92,58471.11,43367.64,15103.47 Sub-Saharan Africa,Sierra Leone,Vegetables,Offline,C,6/1/2016,728815257,6/29/2016,1485,154.06,90.93,228779.10,135031.05,93748.05 North America,Mexico,Personal Care,Offline,M,7/30/2015,559427106,8/8/2015,5767,81.73,56.67,471336.91,326815.89,144521.02 Sub-Saharan Africa,Mozambique,Household,Offline,L,2/10/2012,665095412,2/15/2012,5367,668.27,502.54,3586605.09,2697132.18,889472.91 ================================================ FILE: workflow-compiling-service/src/test/scala/org/apache/texera/service/resource/WorkflowCompilationResourceSpec.scala ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.texera.service.resource import com.fasterxml.jackson.databind.node.ObjectNode import io.dropwizard.testing.junit5.ResourceExtension import jakarta.ws.rs.client.Entity import jakarta.ws.rs.core.{MediaType, Response} import org.apache.texera.amber.compiler.model.{LogicalLink, LogicalPlanPojo} import org.apache.texera.amber.core.tuple.{Attribute, AttributeType} import org.apache.texera.amber.core.workflow.PortIdentity import org.apache.texera.amber.operator.filter.{ ComparisonType, FilterOpDesc, FilterPredicate, SpecializedFilterOpDesc } import org.apache.texera.amber.operator.limit.LimitOpDesc import org.apache.texera.amber.operator.projection.{AttributeUnit, ProjectionOpDesc} import org.apache.texera.amber.operator.source.scan.csv.CSVScanSourceOpDesc import org.apache.texera.amber.util.JSONUtils.objectMapper import org.apache.texera.amber.util.serde.PortIdentityKeySerializer import org.assertj.core.api.Assertions.assertThat import org.scalatest.BeforeAndAfterAll import org.scalatest.flatspec.AnyFlatSpec class WorkflowCompilationResourceSpec extends AnyFlatSpec with BeforeAndAfterAll { private val resources: ResourceExtension = ResourceExtension .builder() .addResource(new WorkflowCompilationResource()) .setMapper(objectMapper) .build() override protected def beforeAll(): Unit = { resources.before() } override protected def afterAll(): Unit = { resources.after() } // utility function to create a csv scan op private def getCsvScanOpDesc( fileName: String, header: Boolean ): CSVScanSourceOpDesc = { val csvOp = new CSVScanSourceOpDesc() csvOp.fileName = Some(fileName) csvOp.customDelimiter = Some(",") csvOp.hasHeader = header csvOp } // utility function to create a projection op private def getProjectionOpDesc( attributeNames: List[String], isDrop: Boolean = false ): ProjectionOpDesc = { val projectionOpDesc = new ProjectionOpDesc() projectionOpDesc.attributes = attributeNames.map(name => new AttributeUnit(name, "")) projectionOpDesc.isDrop = isDrop projectionOpDesc } // utility function to create a limit op private def getLimitOpDesc( limit: Int ): LimitOpDesc = { val limitOpDesc = new LimitOpDesc limitOpDesc.limit = limit limitOpDesc } // utility function to create a filter op private def getFilterOpDesc( filterPredicates: List[FilterPredicate] ): FilterOpDesc = { val filterOpDesc = new SpecializedFilterOpDesc filterOpDesc.predicates = filterPredicates filterOpDesc } // utility function to transform a logical plan pojo to json that can be deserialized correctly by the compile endpoint private def transformLogicalPlanPojoToJsonString(logicalPlanPojo: LogicalPlanPojo): String = { val jsonNode = objectMapper.valueToTree[ObjectNode](logicalPlanPojo) // iterate over the "links" array and replace nested "id" fields val linksArray = jsonNode.withArray("links") linksArray.forEach { linkNode => // replace "fromOpId" with its "id" field value val fromOpIdNode = linkNode.get("fromOpId") linkNode.asInstanceOf[ObjectNode].put("fromOpId", fromOpIdNode.get("id").asText()) // replace "toOpId" with its "id" field value if it exists val toOpIdNode = linkNode.get("toOpId") linkNode.asInstanceOf[ObjectNode].put("toOpId", toOpIdNode.get("id").asText()) } // convert the modified JSON node back to a string objectMapper.writeValueAsString(jsonNode) } // utility function for asserting the successful compilation private def assertSuccessfulCompilation(response: Response): WorkflowCompilationSuccess = { val responseBody = response.readEntity(classOf[String]) val compilationResponse = objectMapper.readValue(responseBody, classOf[WorkflowCompilationResponse]) assertThat(compilationResponse.asInstanceOf[WorkflowCompilationSuccess]) compilationResponse.asInstanceOf[WorkflowCompilationSuccess] } it should "compile workflow successfully with multiple filter and limit operations" in { // construct the LogicalPlan: CSVScan --> Projection --> Limit --> Filter (TotalProfit > 10000) --> Filter (Region != "JPN") --> Limit val localCsvFilePath = "workflow-compiling-service/src/test/resources/country_sales_small.csv" val csvSourceOp = getCsvScanOpDesc(localCsvFilePath, header = true) val projectionOpDesc = getProjectionOpDesc(List("Region", "Total Profit")) val limitOpDesc1 = getLimitOpDesc(10) // Create the filter predicate for TotalProfit > 10000 val filterPredicate1 = new FilterPredicate("Total Profit", ComparisonType.GREATER_THAN, "10000") val filterOpDesc1 = getFilterOpDesc(List(filterPredicate1)) // Create the filter predicate for Region != "JPN" val filterPredicate2 = new FilterPredicate("Region", ComparisonType.NOT_EQUAL_TO, "JPN") val filterOpDesc2 = getFilterOpDesc(List(filterPredicate2)) // Add a second limit operation val limitOpDesc2 = getLimitOpDesc(5) val logicalPlanPojo = LogicalPlanPojo( operators = List( csvSourceOp, projectionOpDesc, limitOpDesc1, filterOpDesc1, filterOpDesc2, limitOpDesc2 ), links = List( LogicalLink( csvSourceOp.operatorIdentifier, PortIdentity(0), projectionOpDesc.operatorIdentifier, PortIdentity(0) ), LogicalLink( projectionOpDesc.operatorIdentifier, PortIdentity(0), limitOpDesc1.operatorIdentifier, PortIdentity(0) ), LogicalLink( limitOpDesc1.operatorIdentifier, PortIdentity(0), filterOpDesc1.operatorIdentifier, PortIdentity(0) ), LogicalLink( filterOpDesc1.operatorIdentifier, PortIdentity(0), filterOpDesc2.operatorIdentifier, PortIdentity(0) ), LogicalLink( filterOpDesc2.operatorIdentifier, PortIdentity(0), limitOpDesc2.operatorIdentifier, PortIdentity(0) ) ), opsToViewResult = List(), opsToReuseResult = List() ) // transform the LogicalPlanPojo to a modified JSON string val modifiedLogicalPlanJsonString = transformLogicalPlanPojoToJsonString(logicalPlanPojo) // send the request to compile endpoint val response = resources .target("/compile") .request(MediaType.APPLICATION_JSON) .post(Entity.json(modifiedLogicalPlanJsonString)) assertThat(response.getStatus).isEqualTo(200) // verify the schema is correctly propagated for the final limit operator val compilationResult = assertSuccessfulCompilation(response) val finalLimitInputSchema = compilationResult.operatorOutputSchemas(filterOpDesc2.operatorIdentifier.id) assert( finalLimitInputSchema( PortIdentityKeySerializer.portIdToString(PortIdentity(id = 0, internal = false)) ).get.equals( List( new Attribute("Region", AttributeType.STRING), new Attribute("Total Profit", AttributeType.DOUBLE) ) ) ) } }